From 7fdf96dd9d8f4f24dda70fed5aecd9c8d2ac567a Mon Sep 17 00:00:00 2001 From: mlogclub Date: Sat, 23 May 2026 22:18:40 +0800 Subject: [PATCH] feat: implement httpx response utilities and refactor API controllers to use them --- internal/bootstrap/controller_routes.go | 43 ++------- internal/controllers/api/auth_controller.go | 100 ++++++++++---------- internal/pkg/httpx/response.go | 63 ++++++++++++ internal/pkg/httpx/response_test.go | 86 +++++++++++++++++ 4 files changed, 210 insertions(+), 82 deletions(-) create mode 100644 internal/pkg/httpx/response.go create mode 100644 internal/pkg/httpx/response_test.go diff --git a/internal/bootstrap/controller_routes.go b/internal/bootstrap/controller_routes.go index 1cb595c..d329703 100644 --- a/internal/bootstrap/controller_routes.go +++ b/internal/bootstrap/controller_routes.go @@ -7,56 +7,33 @@ import ( "cs-agent/internal/controllers/api" "cs-agent/internal/controllers/dashboard" "cs-agent/internal/controllers/third" + "cs-agent/internal/pkg/httpx" "github.com/gin-gonic/gin" "github.com/mlogclub/simple/web" ) func writeJSON(ctx *gin.Context, result *web.JsonResult) { - if result == nil { - return - } - ctx.JSON(http.StatusOK, result) + httpx.WriteJSON(ctx, result) } func pathInt64(ctx *gin.Context, name string) (int64, bool) { value, err := strconv.ParseInt(ctx.Param(name), 10, 64) if err != nil { - ctx.JSON(http.StatusBadRequest, web.JsonErrorMsg("路径参数错误")) + httpx.WriteHttpStatusJSON(ctx, http.StatusBadRequest, web.JsonErrorMsg("路径参数错误")) return 0, false } return value, true } func registerApiAuthRoutes(group *gin.RouterGroup) { - group.POST("/login", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - writeJSON(ctx, controller.PostLogin()) - }) - group.POST("/logout", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - writeJSON(ctx, controller.PostLogout()) - }) - group.GET("/profile", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - writeJSON(ctx, controller.GetProfile()) - }) - group.GET("/wxwork_callback", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - controller.GetWxwork_callback() - }) - group.POST("/wxwork_exchange", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - writeJSON(ctx, controller.PostWxwork_exchange()) - }) - group.GET("/wxwork_login", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - controller.GetWxwork_login() - }) - group.GET("/wxwork_qr_login", func(ctx *gin.Context) { - controller := &api.AuthController{Ctx: ctx} - controller.GetWxwork_qr_login() - }) + group.POST("/login", api.Login) + group.POST("/logout", api.Logout) + group.GET("/profile", api.Profile) + group.GET("/wxwork_callback", api.WxWorkCallback) + group.POST("/wxwork_exchange", api.WxWorkExchange) + group.GET("/wxwork_login", api.WxWorkLogin) + group.GET("/wxwork_qr_login", api.WxWorkQRLogin) } func registerApiChannelRoutes(group *gin.RouterGroup) { diff --git a/internal/controllers/api/auth_controller.go b/internal/controllers/api/auth_controller.go index 129062f..ffa3f12 100644 --- a/internal/controllers/api/auth_controller.go +++ b/internal/controllers/api/auth_controller.go @@ -3,6 +3,7 @@ package api import ( "cs-agent/internal/pkg/config" "cs-agent/internal/pkg/dto/request" + "cs-agent/internal/pkg/httpx" "cs-agent/internal/pkg/httpx/params" "cs-agent/internal/services" "net/http" @@ -10,91 +11,92 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" ) -type AuthController struct { - Ctx *gin.Context -} - -func (c *AuthController) PostLogin() *web.JsonResult { +func Login(ctx *gin.Context) { cfg := config.Current() req := request.LoginRequest{} - if err := params.ReadJSON(c.Ctx, &req); err != nil { - return web.JsonError(err) - } - - ret, err := services.AuthService.Login(req, cfg.Auth, c.Ctx.ClientIP(), c.Ctx.GetHeader("User-Agent")) - if err != nil { - return web.JsonError(err) - } - return web.JsonData(ret) -} - -func (c *AuthController) GetWxwork_login() { - loginURL, err := services.WxWorkLoginService.BuildWxWorkLoginURL(c.Ctx.Query("next")) - if err != nil { - c.redirectWxWorkError(err.Error()) + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) return } - c.Ctx.Redirect(http.StatusFound, loginURL) -} -func (c *AuthController) GetWxwork_qr_login() { - loginURL, err := services.WxWorkLoginService.BuildWxWorkQRCodeLoginURL(c.Ctx.Query("next")) + ret, err := services.AuthService.Login(req, cfg.Auth, ctx.ClientIP(), ctx.GetHeader("User-Agent")) if err != nil { - c.redirectWxWorkError(err.Error()) + httpx.WriteJSON(ctx, err) return } - c.Ctx.Redirect(http.StatusFound, loginURL) + httpx.WriteJSON(ctx, ret) } -func (c *AuthController) GetWxwork_callback() { +func WxWorkLogin(ctx *gin.Context) { + loginURL, err := services.WxWorkLoginService.BuildWxWorkLoginURL(ctx.Query("next")) + if err != nil { + redirectWxWorkError(ctx, err.Error()) + return + } + ctx.Redirect(http.StatusFound, loginURL) +} + +func WxWorkQRLogin(ctx *gin.Context) { + loginURL, err := services.WxWorkLoginService.BuildWxWorkQRCodeLoginURL(ctx.Query("next")) + if err != nil { + redirectWxWorkError(ctx, err.Error()) + return + } + ctx.Redirect(http.StatusFound, loginURL) +} + +func WxWorkCallback(ctx *gin.Context) { cfg := config.Current() ticket, next, err := services.WxWorkLoginService.LoginByWxWork( - c.Ctx.Query("code"), - c.Ctx.Query("state"), + ctx.Query("code"), + ctx.Query("state"), cfg.Auth, - c.Ctx.ClientIP(), - c.Ctx.GetHeader("User-Agent"), + ctx.ClientIP(), + ctx.GetHeader("User-Agent"), ) if err != nil { - c.redirectWxWorkError(err.Error()) + redirectWxWorkError(ctx, err.Error()) return } - c.Ctx.Redirect(http.StatusFound, "/dashboard/login/wxwork/callback?ticket="+url.QueryEscape(ticket)+"&next="+url.QueryEscape(next)) + ctx.Redirect(http.StatusFound, "/dashboard/login/wxwork/callback?ticket="+url.QueryEscape(ticket)+"&next="+url.QueryEscape(next)) } -func (c *AuthController) PostWxwork_exchange() *web.JsonResult { +func WxWorkExchange(ctx *gin.Context) { req := request.WxWorkExchangeRequest{} - if err := params.ReadJSON(c.Ctx, &req); err != nil { - return web.JsonError(err) + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return } ret, err := services.WxWorkLoginService.ExchangeWxWorkLoginTicket(req.Ticket) if err != nil { - return web.JsonError(err) + httpx.WriteJSON(ctx, err) + return } - return web.JsonData(ret) + httpx.WriteJSON(ctx, ret) } -func (c *AuthController) PostLogout() *web.JsonResult { - if err := services.AuthService.Logout(c.Ctx.GetHeader("Authorization")); err != nil { - return web.JsonError(err) +func Logout(ctx *gin.Context) { + if err := services.AuthService.Logout(ctx.GetHeader("Authorization")); err != nil { + httpx.WriteJSON(ctx, err) + return } - return web.JsonSuccess() + httpx.WriteJSON(ctx, nil) } -func (c *AuthController) GetProfile() *web.JsonResult { - ret, err := services.AuthService.CurrentProfile(c.Ctx) +func Profile(ctx *gin.Context) { + ret, err := services.AuthService.CurrentProfile(ctx) if err != nil { - return web.JsonError(err) + httpx.WriteJSON(ctx, err) + return } - return web.JsonData(ret) + httpx.WriteJSON(ctx, ret) } -func (c *AuthController) redirectWxWorkError(message string) { +func redirectWxWorkError(ctx *gin.Context, message string) { if idx := strings.Index(message, ": "); idx >= 0 { message = message[idx+2:] } - c.Ctx.Redirect(http.StatusFound, "/login?wxworkError="+url.QueryEscape(message)) + ctx.Redirect(http.StatusFound, "/login?wxworkError="+url.QueryEscape(message)) } diff --git a/internal/pkg/httpx/response.go b/internal/pkg/httpx/response.go new file mode 100644 index 0000000..87697a9 --- /dev/null +++ b/internal/pkg/httpx/response.go @@ -0,0 +1,63 @@ +package httpx + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/mlogclub/simple/sqls" + "github.com/mlogclub/simple/web" +) + +type cursorData struct { + results any + cursor string + hasMore bool +} + +type pageData struct { + results any + paging *sqls.Paging +} + +func CursorData(results any, cursor string, hasMore bool) any { + return cursorData{results: results, cursor: cursor, hasMore: hasMore} +} + +func PageData(results any, paging *sqls.Paging) any { + return pageData{results: results, paging: paging} +} + +func WriteJSON(ctx *gin.Context, result any) { + ctx.JSON(http.StatusOK, buildJSONResult(result)) +} + +func WriteHttpStatusJSON(ctx *gin.Context, statusCode int, result any) { + ctx.JSON(statusCode, buildJSONResult(result)) +} + +func buildJSONResult(result any) *web.JsonResult { + switch value := result.(type) { + case nil: + return web.JsonSuccess() + case *web.JsonResult: + return value + case web.JsonResult: + return &value + case *web.CodeError: + return web.JsonError(value) + case web.CodeError: + return web.JsonError(&value) + case error: + return web.JsonError(value) + case cursorData: + return web.JsonCursorData(value.results, value.cursor, value.hasMore) + case pageData: + return web.JsonPageData(value.results, value.paging) + case web.RspBuilder: + return value.JsonResult() + case *web.RspBuilder: + return value.JsonResult() + default: + return web.JsonData(result) + } +} diff --git a/internal/pkg/httpx/response_test.go b/internal/pkg/httpx/response_test.go new file mode 100644 index 0000000..0299a0e --- /dev/null +++ b/internal/pkg/httpx/response_test.go @@ -0,0 +1,86 @@ +package httpx + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/mlogclub/simple/sqls" + "github.com/mlogclub/simple/web" +) + +func TestWriteJSONWrapsCommonResultTypes(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + input any + wantStatus int + want web.JsonResult + }{ + { + name: "nil becomes success", + input: nil, + wantStatus: http.StatusOK, + want: *web.JsonSuccess(), + }, + { + name: "plain value becomes data", + input: map[string]any{"id": float64(1)}, + wantStatus: http.StatusOK, + want: *web.JsonData(map[string]any{"id": float64(1)}), + }, + { + name: "error becomes json error", + input: errors.New("boom"), + wantStatus: http.StatusOK, + want: *web.JsonError(errors.New("boom")), + }, + { + name: "page data becomes page result", + input: PageData([]any{"a"}, &sqls.Paging{Page: 1, Limit: 20, Total: 1}), + wantStatus: http.StatusOK, + want: *web.JsonPageData([]any{"a"}, &sqls.Paging{Page: 1, Limit: 20, Total: 1}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, recorder := testContext() + + WriteJSON(ctx, tt.input) + + if recorder.Code != tt.wantStatus { + t.Fatalf("status = %d, want %d", recorder.Code, tt.wantStatus) + } + var got web.JsonResult + if err := json.Unmarshal(recorder.Body.Bytes(), &got); err != nil { + t.Fatalf("decode response: %v", err) + } + if got.Success != tt.want.Success || got.ErrorCode != tt.want.ErrorCode || got.Message != tt.want.Message { + t.Fatalf("result = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestWriteHttpStatusJSONUsesProvidedStatus(t *testing.T) { + gin.SetMode(gin.TestMode) + ctx, recorder := testContext() + + WriteHttpStatusJSON(ctx, http.StatusUnauthorized, web.JsonErrorMsg("unauthorized")) + + if recorder.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want %d", recorder.Code, http.StatusUnauthorized) + } +} + +func testContext() (*gin.Context, *httptest.ResponseRecorder) { + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Request = httptest.NewRequest(http.MethodGet, "/", nil) + return ctx, recorder +}