本來計劃是學習Docker部署的,研究了一天沒搞出來,得出結論是需要翻墻,懶得弄了,暫時放置。
一、以下是,上傳/下載代碼,和之前是重復的,只多添加了,上傳/下載功能。
測試目錄為工程根目錄下/files文件夾內
package mainimport ("context""encoding/json""fmt""io""log""net/http""os""path/filepath""regexp""time""github.com/gin-gonic/gin""github.com/jackc/pgx/v5""github.com/jackc/pgx/v5/pgxpool"swaggerFiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger"// 注意:替換為你項目的實際路徑// _ "your_project/docs" // docs 包,由 swag 生成// 如果 docs 包在根目錄,且 main.go 也在根目錄,可以這樣導入_ "HTTPServices/docs" // 假設 docs 目錄在項目根目錄下
)var db *pgxpool.Pool// 啟動函數
func main() {// 初始化數據庫連接db = InitDB()defer db.Close()// 注冊路由RegisterRouter()// 啟動 HTTP 服務go func() {StartHTTPServer()}()// 啟動 HTTP api測試服務go func() {StartDebugHTTPServer()}()// 阻塞主線程select {}
}// @title Sample API
// @version 1.0
// @description API測試頁面
// @host localhost:8080
func StartDebugHTTPServer() {r := gin.Default()// --- 掛載 Swagger UI ---// 訪問 http://localhost:8081/swagger/index.html 查看 UIr.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))port := ":8081"LogSuccess("啟動 HTTP Swagger測試服務啟動,監聽端口 %s\n", port)// 啟動服務器debugApiError := r.Run(port)if debugApiError != nil {LogError("HTTP api測試服務啟動失敗:%v", debugApiError)} else {LogSuccess("HTTP api測試服務已啟動,監聽端口 8081")}
}// 啟動 HTTP 服務
func StartHTTPServer() {address := "127.0.0.1:8080" //配置連接ip端口//配置跨域,是影響調試頁面不能訪問8080相關地址的原因handler := corsMiddleware(http.DefaultServeMux)LogSuccess("啟動 HTTP 服務,監聽端口 %s\n", address)err := http.ListenAndServe(address, handler)if err != nil {log.Fatalf("服務器啟動失敗:%v", err)}
}// corsMiddleware 是一個中間件,用于添加 CORS 頭
func corsMiddleware(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {// 設置 CORS 響應頭w.Header().Add("Access-Control-Allow-Origin", "http://localhost:8081") // ? 修改為你的前端地址w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")w.Header().Set("Access-Control-Allow-Headers","Origin, Content-Type, Accept, Authorization, X-Requested-With")// 如果需要傳遞 Cookie 或 Authorization Bearer Tokenw.Header().Set("Access-Control-Allow-Credentials", "true")// 處理預檢請求 (OPTIONS)if r.Method == "OPTIONS" {w.WriteHeader(http.StatusOK)return}// 調用下一個處理器 (即注冊的路由)next.ServeHTTP(w, r)})
}// 注冊路由
func RegisterRouter() {http.HandleFunc("/", helloHandler) //http://localhost:8080/http.HandleFunc("/time", timeHandler) //http://localhost:8080/time//查詢http.HandleFunc("/findTable", findTableNameHandler) //http://localhost:8080/findTable?tableName=name//添加http.HandleFunc("/addTable1", addTable1Handler) //http://localhost:8080/addTable1//刪除http.HandleFunc("/deleteTableValue", deleteTableHandler) //http://localhost:8080/deleteTableValue?tableName=table1&fieldName=test1&fieldValue=123test//修改http.HandleFunc("/updateTableValue", updateTableHandler) //http://localhost:8080/updateTableValue?tableName=table1&findFieldName=test1&findFieldValue=hello&setFieldName=test3&setFieldValue=456//下載文件http.HandleFunc("/downloadFile", downloadHandler) //http://localhost:8080/downloadFile?filePath=D:\GoProject\HTTPServices\README.md//上傳文件http.HandleFunc("/uploadFile", uploadHandler) //http://localhost:8080/uploadFile}// APIResponse 定義了統一的 API 響應格式
type APIResponse struct {Success bool `json:"success"` // 是否成功Status int `json:"status"` // HTTP 狀態碼Message string `json:"message,omitempty"` // 簡短消息 報錯時的提示信息Data interface{} `json:"data,omitempty"` // 主要數據內容Timestamp string `json:"timestamp"` // 時間戳 (秒)
}// SendJSONResponse 封裝了 JSON 響應的發送邏輯
func SendJSONResponse(w http.ResponseWriter, success bool, status int, message string, data interface{}) {// 設置 Content-Typew.Header().Set("Content-Type", "application/json")// 設置 HTTP 狀態碼w.WriteHeader(status)// 構造響應體response := APIResponse{Success: success,Status: status,Message: message,Data: data,Timestamp: time.Now().Format("2006-01-02 15:04:05"), // 當前時間戳格式化}// 編碼并發送 JSONif err := json.NewEncoder(w).Encode(response); err != nil {// 如果編碼失敗,記錄錯誤(但不能再次寫入 w,因為 Header 已經發送)http.Error(w, "Internal Server Error", http.StatusInternalServerError)// log.Printf("JSON encode error: %v", err) // 取消注釋以記錄日志}
}// @Summary 根目錄測試連接
// @Description
// @Tags tags1
// @Accept json
// @Produce json
// @Router / [get]
func helloHandler(w http.ResponseWriter, r *http.Request) {LogInfo("訪問路徑:%s,來源:%s\n", r.URL.Path, r.RemoteAddr)// 編碼 JSON 響應SendJSONResponse(w, true, http.StatusOK, "成功", "Hello, World! 👋")
}// @Summary 查詢服務器時間
// @Description
// @Tags tags1
// @Accept json
// @Produce json
// @Router /time [get]
func timeHandler(w http.ResponseWriter, r *http.Request) {LogInfo("訪問路徑:%s,來源:%s\n", r.URL.Path, r.RemoteAddr)currentTime := time.Now().Format("2006-01-02 15:04:05")// ? 設置響應頭SendJSONResponse(w, true, http.StatusOK, "成功", currentTime)
}// @Summary 修改指定表名中,find字段名等于指定值的set字段名的數據
// @Description 根據提供的表名、find字段名、find字段值、set字段名、set字段值,修改數據庫中的數據。
// @Tags tags1
// @Produce json
// @Param tableName query string true "要查詢的數據庫表名" default(table1)
// @Param fieldName query string true "要查詢的字段名"
// @Param fieldValue query string true "要查詢的字段值"
// @Param setFieldName query string true "要更新的字段名"
// @Param setFieldValue query string true "要更新的字段值"
// @Router /updateTableValue [get]
func updateTableHandler(w http.ResponseWriter, r *http.Request) {// 解析請求參數tableName := r.URL.Query().Get("tableName")findFieldName := r.URL.Query().Get("findFieldName")findFieldValue := r.URL.Query().Get("findFieldValue")setFieldName := r.URL.Query().Get("setFieldName")setFieldValue := r.URL.Query().Get("setFieldValue")// 完整的參數驗證if tableName == "" || findFieldName == "" || setFieldName == "" {http.Error(w, "缺少必要參數", http.StatusBadRequest)return}// 🔐 白名單驗證 - 只允許預定義的表和字段allowedTables := map[string]bool{"table1": true, "table2": true}allowedFields := map[string]bool{"test1": true, "test2": true, "test3": true,"test4": true, "test5": true, "test6": true, "test7": true,}if !allowedTables[tableName] {http.Error(w, "不允許的表名", http.StatusBadRequest)return}if !allowedFields[findFieldName] || !allowedFields[setFieldName] {http.Error(w, "不允許的字段名", http.StatusBadRequest)return}// ? 使用參數化查詢,表名和字段名通過白名單驗證后拼接query := fmt.Sprintf("UPDATE %s SET %s = $1 WHERE %s = $2",tableName, setFieldName, findFieldName,)result, err := db.Exec(context.Background(), query, setFieldValue, findFieldValue)if err != nil {http.Error(w, "更新數據失敗: "+err.Error(), http.StatusInternalServerError)return}// 檢查是否實際更新了數據rowsAffected := result.RowsAffected()if rowsAffected == 0 {http.Error(w, "未找到匹配的數據進行更新", http.StatusNotFound)return}SendJSONResponse(w, true, http.StatusOK, "成功", fmt.Sprintf("%d 行已更新", rowsAffected))}// @Summary 刪除指定表名中,指定字段名等于指定值的數據
// @Description 根據提供的表名和字段名和值,刪除數據庫中的數據。
// @Tags tags1
// @Produce json
// @Param tableName query string true "要刪除的數據庫表名"
// @Param fieldName query string true "要刪除的字段名"
// @Param fieldValue query string true "要刪除的字段值"
// @Router /deleteTableValue [get]
func deleteTableHandler(w http.ResponseWriter, r *http.Request) {// 解析請求參數tableName := r.URL.Query().Get("tableName")fieldName := r.URL.Query().Get("fieldName")fieldValue := r.URL.Query().Get("fieldValue")if tableName == "" || fieldName == "" || fieldValue == "" {http.Error(w, "參數錯誤", http.StatusBadRequest)return}// 執行 SQL 語句,使用參數化查詢query := fmt.Sprintf("DELETE FROM %s WHERE %s = $1", tableName, fieldName)_, err := db.Exec(context.Background(), query, fieldValue)if err != nil {http.Error(w, "刪除數據失敗: "+err.Error(), http.StatusInternalServerError)return}SendJSONResponse(w, true, http.StatusOK, "成功", "數據已刪除")
}// @Summary 向table1表中添加數據,字段名=test1,test2,test3,test4,test5,test6,test7
// @Description 根據提供的json數據,向數據庫table1中添加數據。
// @Tags tags1
// @Produce json
// @Param data body string true "要插入的數據對象"
// @Router /addTable1 [post]
func addTable1Handler(w http.ResponseWriter, r *http.Request) {// 定義需要插入的數據結構type requestData struct {Test1 string `json:"test1"`Test2 time.Time `json:"test2"`Test3 uint32 `json:"test3"`Test4 string `json:"test4"`Test5 float64 `json:"test5"`Test6 int32 `json:"test6"`Test7 float64 `json:"test7"`}// 解析請求參數var data requestDataerr := json.NewDecoder(r.Body).Decode(&data)if err != nil {http.Error(w, "解析請求參數失敗: "+err.Error(), http.StatusBadRequest)return}// 執行 SQL 語句,使用參數化查詢query := "INSERT INTO table1 (test1, test2, test3, test4, test5, test6, test7) VALUES ($1, $2, $3, $4, $5, $6, $7)"_, err = db.Exec(context.Background(), query, data.Test1, data.Test2, data.Test3, data.Test4, data.Test5, data.Test6, data.Test7)if err != nil {http.Error(w, "插入數據失敗: "+err.Error(), http.StatusInternalServerError)return}SendJSONResponse(w, true, http.StatusOK, "成功", "數據已插入")
}// @Summary 查詢指定表名的全部數據
// @Description 根據提供的表名查詢數據庫中的所有數據。
// @Tags tags1
// @Produce json
// @Param tableName query string true "要查詢的數據庫表名" default(table1)
// @Router /findTable [get]
func findTableNameHandler(w http.ResponseWriter, r *http.Request) {tableName := r.URL.Query().Get("tableName")if tableName == "" {http.Error(w, "tableName is empty", http.StatusBadRequest)return}// ? 安全校驗表名(防止 SQL 注入)if !isValidTableName(tableName) {http.Error(w, "invalid table name", http.StatusBadRequest)return}// ? 使用參數化方式拼接表名(僅限對象名,如表、字段)query := fmt.Sprintf("SELECT * FROM %s", tableName)rows, err := db.Query(context.Background(), query)if err != nil {http.Error(w, "查詢失敗: "+err.Error(), http.StatusInternalServerError)return}defer rows.Close()// ? 使用 pgx 內置工具自動轉為 []map[string]interface{}data, err := pgx.CollectRows(rows, pgx.RowToMap)if err != nil {http.Error(w, "解析數據失敗: "+err.Error(), http.StatusInternalServerError)return}SendJSONResponse(w, true, http.StatusOK, "成功", data)
}// 安全校驗表名(防止 SQL 注入)
func isValidTableName(name string) bool {// 只允許字母、數字、下劃線,且不能以數字開頭matched, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, name)return matched
}// @Summary 下載文件
// @Description
// @Tags tags1
// @Produce json
// @Router /downloadFile [get]
func downloadHandler(w http.ResponseWriter, r *http.Request) {fileName := "testdownloadFIle.txt"// 要下載的文件路徑filePath := "./files/" + fileName // 假設文件在項目根目錄下的 files 文件夾中// 設置響應頭,提示瀏覽器下載(可選)w.Header().Set("Content-Disposition", "attachment; filename="+fileName)// 如果不設置,瀏覽器可能會嘗試直接打開文件(如PDF、圖片)// 使用 http.ServeFile 提供文件http.ServeFile(w, r, filePath)
}// uploadHandler 處理文件上傳
// @Summary 上傳文件
// @Description 支持上傳任意文件,最大 50MB
// @Tags tags1
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "要上傳的文件"
// @Success 200 {string} string "文件上傳成功"
// @Failure 400 {object} map[string]string "請求錯誤,如文件太大或格式錯誤"
// @Failure 500 {object} map[string]string "服務器內部錯誤"
// @Router /uploadFile [post]
func uploadHandler(w http.ResponseWriter, r *http.Request) {if r.Method != "POST" {http.Error(w, "只支持POST方法", http.StatusMethodNotAllowed)return}// 解析 multipart form 文件大小限制if err := r.ParseMultipartForm(50 << 20); err != nil { // 50MB限制http.Error(w, "文件太大", http.StatusBadRequest)return}// 獲取文件file, handler, err := r.FormFile("file")if err != nil {http.Error(w, "獲取文件失敗: "+err.Error(), http.StatusBadRequest)return}defer file.Close()// 創建目標文件filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), handler.Filename)dstPath := filepath.Join("./files/", filename)dst, err := os.Create(dstPath)if err != nil {http.Error(w, "創建文件失敗: "+err.Error(), http.StatusInternalServerError)return}defer dst.Close()// 復制文件內容if _, err := io.Copy(dst, file); err != nil {http.Error(w, "保存文件失敗: "+err.Error(), http.StatusInternalServerError)return}// 返回響應SendJSONResponse(w, true, http.StatusOK, "成功", "文件上傳成功")
}
二、便捷初始化swagger+直接啟動服務小工具。
測試的時候發現總是忘記執行swagger的初始化命令,導致swagger界面不出現新添加的接口,還要反應半天才明白過來是沒有執行 swag init。
直接搞了個啟動腳本,在工程根目錄創建runTools.bat文件
@echo off
:: 設置代碼頁為 UTF-8,正確顯示中文
chcp 65001 >nul
swag init
if %ERRORLEVEL% EQU 0 (echo ===swagger 初始化完成!===
go run .
) else (echo ===swagger 初始化失敗!===
)
這樣之后每次運行服務,只要在終端輸入.\runTools.bat即可。