Jelajahi Sumber

feat: 返回结构统一

BaiLuoYan 5 hari lalu
induk
melakukan
c0625f062a

+ 7 - 5
internal/handler/auth/changePasswordHandler_test.go

@@ -28,9 +28,10 @@ func TestChangePasswordHandler_MalformedBodyReturns400(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 400, body.Code,
+	assert.False(t, body.Success)
+	assert.Equal(t, 400, body.ErrorCode,
 		"handler 必须把 httpx.Parse 的错误包成 400; 旧实现有时会被吞成 500")
-	assert.NotContains(t, body.Msg, "原密码", "400 的文案不应提到 logic 层的业务字段语义")
+	assert.NotContains(t, body.ErrorMessage, "原密码", "400 的文案不应提到 logic 层的业务字段语义")
 }
 
 // TC-0799: handler 薄层契约 —— ChangePasswordHandler 在缺必填字段时也必须 400,
@@ -46,9 +47,10 @@ func TestChangePasswordHandler_MissingFieldsReturns400(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 400, body.Code)
+	assert.False(t, body.Success)
+	assert.Equal(t, 400, body.ErrorCode)
 	// 必填字段缺失应该精确到字段名, 便于客户端自动纠错
 	assert.True(t,
-		strings.Contains(body.Msg, "oldPassword") || strings.Contains(body.Msg, "newPassword"),
-		"缺字段文案必须点名到 oldPassword / newPassword; 实际: %q", body.Msg)
+		strings.Contains(body.ErrorMessage, "oldPassword") || strings.Contains(body.ErrorMessage, "newPassword"),
+		"缺字段文案必须点名到 oldPassword / newPassword; 实际: %q", body.ErrorMessage)
 }

+ 3 - 2
internal/handler/auth/logoutHandler_test.go

@@ -35,8 +35,9 @@ func TestLogoutHandler_UnauthorizedWhenNoUserCtx(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 401, body.Code)
-	assert.Contains(t, body.Msg, "未登录")
+	assert.False(t, body.Success)
+	assert.Equal(t, 401, body.ErrorCode)
+	assert.Contains(t, body.ErrorMessage, "未登录")
 }
 
 // TC-0797: handler 薄层契约 —— LogoutHandler 在有效认证上下文下必须 200 且无响应体(httpx.Ok);

+ 28 - 21
internal/handler/product/fetchInitialCredentialsHandler_test.go

@@ -77,11 +77,12 @@ func TestFetchInitialCredentialsHandler_MalformedBodyReturns400(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 400, body.Code,
-		"handler 必须把 httpx.Parse 错误包成 400,实际 code=%d msg=%q", body.Code, body.Msg)
-	assert.NotContains(t, strings.ToLower(body.Msg), "sql", "错误文案不得泄露 SQL 细节")
-	assert.NotContains(t, strings.ToLower(body.Msg), "redis", "错误文案不得泄露 Redis 细节")
-	assert.NotContains(t, strings.ToLower(body.Msg), "ticket", "解析失败阶段不得泄露 ticket 字段存在与否")
+	assert.False(t, body.Success)
+	assert.Equal(t, 400, body.ErrorCode,
+		"handler 必须把 httpx.Parse 错误包成 400,实际 code=%d msg=%q", body.ErrorCode, body.ErrorMessage)
+	assert.NotContains(t, strings.ToLower(body.ErrorMessage), "sql", "错误文案不得泄露 SQL 细节")
+	assert.NotContains(t, strings.ToLower(body.ErrorMessage), "redis", "错误文案不得泄露 Redis 细节")
+	assert.NotContains(t, strings.ToLower(body.ErrorMessage), "ticket", "解析失败阶段不得泄露 ticket 字段存在与否")
 }
 
 // TC-0962: handler 薄层契约 —— 无用户上下文必须 401,而不是 200 / 500 / panic。
@@ -101,9 +102,10 @@ func TestFetchInitialCredentialsHandler_NoUserCtxReturns401(t *testing.T) {
 
 	var resp response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
-	assert.Equal(t, 401, resp.Code,
-		"无登录上下文必须 401;实际 code=%d msg=%q", resp.Code, resp.Msg)
-	assert.Contains(t, resp.Msg, "未登录")
+	assert.False(t, resp.Success)
+	assert.Equal(t, 401, resp.ErrorCode,
+		"无登录上下文必须 401;实际 code=%d msg=%q", resp.ErrorCode, resp.ErrorMessage)
+	assert.Contains(t, resp.ErrorMessage, "未登录")
 }
 
 // TC-0963: handler 薄层契约 —— 非超管必须 403,且响应体不得泄露 ticket 存在性或业务细节。
@@ -123,10 +125,11 @@ func TestFetchInitialCredentialsHandler_NonSuperAdminReturns403(t *testing.T) {
 
 	var resp response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
-	assert.Equal(t, 403, resp.Code,
-		"非超管访问 FetchInitialCredentials 必须 403;实际 code=%d msg=%q", resp.Code, resp.Msg)
-	assert.Contains(t, resp.Msg, "超级管理员")
-	assert.NotContains(t, resp.Msg, "ticket",
+	assert.False(t, resp.Success)
+	assert.Equal(t, 403, resp.ErrorCode,
+		"非超管访问 FetchInitialCredentials 必须 403;实际 code=%d msg=%q", resp.ErrorCode, resp.ErrorMessage)
+	assert.Contains(t, resp.ErrorMessage, "超级管理员")
+	assert.NotContains(t, resp.ErrorMessage, "ticket",
 		"403 文案不得提及 ticket,以防通过错误差异化探测 ticket 合法性")
 }
 
@@ -146,9 +149,10 @@ func TestFetchInitialCredentialsHandler_EmptyTicketReturns400(t *testing.T) {
 
 	var resp response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
-	assert.Equal(t, 400, resp.Code,
-		"空 ticket 必须是 400 而非 401/403/500;实际 code=%d msg=%q", resp.Code, resp.Msg)
-	assert.Contains(t, resp.Msg, "ticket")
+	assert.False(t, resp.Success)
+	assert.Equal(t, 400, resp.ErrorCode,
+		"空 ticket 必须是 400 而非 401/403/500;实际 code=%d msg=%q", resp.ErrorCode, resp.ErrorMessage)
+	assert.Contains(t, resp.ErrorMessage, "ticket")
 }
 
 // TC-0965: handler 薄层契约 —— 超管 + 未知 ticket 必须 400 "凭证票据无效或已过期"。
@@ -168,9 +172,10 @@ func TestFetchInitialCredentialsHandler_UnknownTicketReturns400(t *testing.T) {
 
 	var resp response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
-	assert.Equal(t, 400, resp.Code,
-		"未知 ticket 必须 400;实际 code=%d msg=%q", resp.Code, resp.Msg)
-	assert.Contains(t, resp.Msg, "凭证")
+	assert.False(t, resp.Success)
+	assert.Equal(t, 400, resp.ErrorCode,
+		"未知 ticket 必须 400;实际 code=%d msg=%q", resp.ErrorCode, resp.ErrorMessage)
+	assert.Contains(t, resp.ErrorMessage, "凭证")
 }
 
 // TC-0966: handler 薄层契约 —— 超管 + 已落地 ticket 必须 200 + 正确字段,且 Redis key
@@ -207,12 +212,14 @@ func TestFetchInitialCredentialsHandler_HappyPath200WithFields(t *testing.T) {
 	require.Equal(t, http.StatusOK, rr.Code, "happy path 必须 HTTP 200;body=%s", rr.Body.String())
 
 	var envelope struct {
-		Code int                               `json:"code"`
-		Msg  string                            `json:"msg"`
+		Success bool `json:"success"`
+		ErrorCode int `json:"errorCode"`
+		ErrorMessage string `json:"errorMessage"`
 		Data types.FetchInitialCredentialsResp `json:"data"`
 	}
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &envelope))
-	assert.Equal(t, 0, envelope.Code, "业务 code 必须 0;实际=%d msg=%q", envelope.Code, envelope.Msg)
+	assert.True(t, envelope.Success)
+	assert.Equal(t, 0, envelope.ErrorCode, "业务 code 必须 0;实际=%d msg=%q", envelope.ErrorCode, envelope.ErrorMessage)
 	assert.Equal(t, payload["appKey"], envelope.Data.AppKey)
 	assert.Equal(t, payload["appSecret"], envelope.Data.AppSecret)
 	assert.Equal(t, payload["adminUser"], envelope.Data.AdminUser)

+ 3 - 2
internal/handler/pub/adminLoginHandler_test.go

@@ -28,6 +28,7 @@ func TestAdminLoginHandler_MissingFields(t *testing.T) {
 	var body response.Body
 	err := json.Unmarshal(rr.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 400, body.Code)
-	assert.Contains(t, body.Msg, "username")
+	assert.False(t, body.Success)
+	assert.Equal(t, 400, body.ErrorCode)
+	assert.Contains(t, body.ErrorMessage, "username")
 }

+ 3 - 2
internal/handler/pub/loginHandler_test.go

@@ -32,6 +32,7 @@ func TestLoginHandler_MissingFields(t *testing.T) {
 	var body response.Body
 	err := json.Unmarshal(rr.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 400, body.Code)
-	assert.Contains(t, body.Msg, "username")
+	assert.False(t, body.Success)
+	assert.Equal(t, 400, body.ErrorCode)
+	assert.Contains(t, body.ErrorMessage, "username")
 }

+ 8 - 7
internal/handler/pub/refreshTokenHandler_test.go

@@ -29,11 +29,11 @@ func TestRefreshTokenHandler_MissingAuthorizationHeader(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.NotEqual(t, 200, body.Code, "缺 Authorization 必须报错而非 200")
-	assert.True(t, body.Code == 401 || body.Code == 400,
-		"缺 Authorization 必须是 401/400; 实际 code=%d msg=%q", body.Code, body.Msg)
-	assert.NotContains(t, strings.ToLower(body.Msg), "sql", "错误文案不得泄露 SQL 实现细节")
-	assert.NotContains(t, strings.ToLower(body.Msg), "redis", "错误文案不得泄露 Redis 实现细节")
+	assert.NotEqual(t, 200, body.ErrorCode, "缺 Authorization 必须报错而非 200")
+	assert.True(t, body.ErrorCode == 401 || body.ErrorCode == 400,
+		"缺 Authorization 必须是 401/400; 实际 code=%d msg=%q", body.ErrorCode, body.ErrorMessage)
+	assert.NotContains(t, strings.ToLower(body.ErrorMessage), "sql", "错误文案不得泄露 SQL 实现细节")
+	assert.NotContains(t, strings.ToLower(body.ErrorMessage), "redis", "错误文案不得泄露 Redis 实现细节")
 }
 
 // TC-0801: handler 薄层契约 —— RefreshTokenHandler 在 Authorization 带非法值时 401, 且不 panic。
@@ -49,6 +49,7 @@ func TestRefreshTokenHandler_GarbageBearerToken(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 401, body.Code,
-		"非法 refresh token 必须 401, 而不是 500 panic 或 200; 实际 code=%d msg=%q", body.Code, body.Msg)
+	assert.False(t, body.Success)
+	assert.Equal(t, 401, body.ErrorCode,
+		"非法 refresh token 必须 401, 而不是 500 panic 或 200; 实际 code=%d msg=%q", body.ErrorCode, body.ErrorMessage)
 }

+ 6 - 5
internal/handler/routes_test.go

@@ -82,18 +82,19 @@ func TestRefreshTokenRoute_RateLimit_EnforcedOnSameIP(t *testing.T) {
 
 	// 第 1 次:放行,进入 RefreshTokenHandler 后因缺 Authorization 返回 401(业务层)
 	_, body1 := doRequest("198.51.100.7:40001")
-	assert.NotEqual(t, 429, body1.Code, "首次请求必须放行,由业务层决定返回码;实际 code=%d msg=%q", body1.Code, body1.Msg)
+	assert.NotEqual(t, 429, body1.ErrorCode, "首次请求必须放行,由业务层决定返回码;实际 code=%d msg=%q", body1.ErrorCode, body1.ErrorMessage)
 
 	// 第 2 次:同 IP 不同端口,必须被限流拦截,返回 429 "请求过于频繁..."
 	_, body2 := doRequest("198.51.100.7:40002")
-	assert.Equal(t, 429, body2.Code,
-		"/api/auth/refreshToken 必须受 IP 维度限流保护;quota=1 时第 2 次必须 429。实际 code=%d msg=%q", body2.Code, body2.Msg)
-	assert.Contains(t, body2.Msg, "过于频繁",
+	assert.False(t, body2.Success)
+	assert.Equal(t, 429, body2.ErrorCode,
+		"/api/auth/refreshToken 必须受 IP 维度限流保护;quota=1 时第 2 次必须 429。实际 code=%d msg=%q", body2.ErrorCode, body2.ErrorMessage)
+	assert.Contains(t, body2.ErrorMessage, "过于频繁",
 		"429 的业务文案必须是用户可读的限流提示,而不是原始 limiter 错误")
 
 	// 不同 IP 必须不受影响,证明限流是 per-IP 而不是全局。
 	_, body3 := doRequest("203.0.113.9:55555")
-	assert.NotEqual(t, 429, body3.Code, "不同 IP 必须独立计数;不应被前一 IP 的 burst 牵连,实际 code=%d", body3.Code)
+	assert.NotEqual(t, 429, body3.ErrorCode, "不同 IP 必须独立计数;不应被前一 IP 的 burst 牵连,实际 code=%d", body3.ErrorCode)
 }
 
 // ---------------------------------------------------------------------------

+ 2 - 2
internal/logic/pub/capEndpointLogic.go

@@ -31,9 +31,9 @@ func NewCapEndpointLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CapEn
 func (l *CapEndpointLogic) CapEndpoint() (*types.CapEndpointResp, error) {
 	cfg := l.svcCtx.Config.Capjs
 	if cfg.Enable != 1 || cfg.EndpointURL == "" || cfg.Key == "" {
-		return &types.CapEndpointResp{Code: 0, Msg: "success", Data: ""}, nil
+		return &types.CapEndpointResp{Data: ""}, nil
 	}
 	url := cfg.EndpointURL + "/" + cfg.Key + "/"
-	return &types.CapEndpointResp{Code: 0, Msg: "success", Data: url}, nil
+	return &types.CapEndpointResp{Data: url}, nil
 }
 

+ 39 - 26
internal/middleware/jwtauthMiddleware_test.go

@@ -137,8 +137,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err := json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 401, body.Code)
-		assert.Equal(t, "未登录", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 401, body.ErrorCode)
+		assert.Equal(t, "未登录", body.ErrorMessage)
 	})
 
 	t.Run("no Bearer prefix", func(t *testing.T) {
@@ -154,8 +155,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err := json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 401, body.Code)
-		assert.Equal(t, "token格式错误", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 401, body.ErrorCode)
+		assert.Equal(t, "token格式错误", body.ErrorMessage)
 	})
 
 	t.Run("invalid token", func(t *testing.T) {
@@ -171,8 +173,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err := json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 401, body.Code)
-		assert.Equal(t, "token无效或已过期", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 401, body.ErrorCode)
+		assert.Equal(t, "token无效或已过期", body.ErrorMessage)
 	})
 
 	t.Run("wrong secret", func(t *testing.T) {
@@ -192,8 +195,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err := json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 401, body.Code)
-		assert.Equal(t, "token无效或已过期", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 401, body.ErrorCode)
+		assert.Equal(t, "token无效或已过期", body.ErrorMessage)
 	})
 
 	t.Run("expired token", func(t *testing.T) {
@@ -213,8 +217,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err := json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 401, body.Code)
-		assert.Equal(t, "token无效或已过期", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 401, body.ErrorCode)
+		assert.Equal(t, "token无效或已过期", body.ErrorMessage)
 	})
 
 	// TC-0264: refresh token 不应被中间件接受
@@ -235,8 +240,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err := json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 401, body.Code)
-		assert.Equal(t, "token无效或类型错误", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 401, body.ErrorCode)
+		assert.Equal(t, "token无效或类型错误", body.ErrorMessage)
 	})
 
 	t.Run("frozen user rejected", func(t *testing.T) {
@@ -284,8 +290,9 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		var body response.Body
 		err = json.Unmarshal(rr.Body.Bytes(), &body)
 		require.NoError(t, err)
-		assert.Equal(t, 403, body.Code)
-		assert.Equal(t, "账号已被冻结", body.Msg)
+		assert.False(t, body.Success)
+		assert.Equal(t, 403, body.ErrorCode)
+		assert.Equal(t, "账号已被冻结", body.ErrorMessage)
 	})
 }
 
@@ -405,9 +412,10 @@ func TestJwtAuthMiddleware_TokenVersionCheckedBeforeProductStatus(t *testing.T)
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
 
-	assert.Equal(t, 401, body.Code,
+	assert.False(t, body.Success)
+	assert.Equal(t, 401, body.ErrorCode,
 		"L-B:TokenVersion 失配必须先于产品禁用被识别(返回 401 而非 403)")
-	assert.Equal(t, "登录状态已失效,请重新登录", body.Msg,
+	assert.Equal(t, "登录状态已失效,请重新登录", body.ErrorMessage,
 		"L-B:文案必须是'登录状态已失效'而不是'该产品已被禁用',否则用户会被无关信息误导")
 }
 
@@ -471,8 +479,9 @@ func TestJwtAuthMiddleware_ProductDisabledAfterVersionOk(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 403, body.Code)
-	assert.Equal(t, "该产品已被禁用", body.Msg)
+	assert.False(t, body.Success)
+	assert.Equal(t, 403, body.ErrorCode)
+	assert.Equal(t, "该产品已被禁用", body.ErrorMessage)
 }
 
 // --- 鉴权优先级完整矩阵(-B 延伸,TC-0754 ~ TC-0758)---
@@ -513,9 +522,10 @@ func TestJwtAuthMiddleware_UserDeletedBeatsTokenVersion(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 401, body.Code,
+	assert.False(t, body.Success)
+	assert.Equal(t, 401, body.ErrorCode,
 		"L-B 矩阵: Username empty 必须在 TokenVersion 之前裁决")
-	assert.Equal(t, "用户不存在或已被删除", body.Msg,
+	assert.Equal(t, "用户不存在或已被删除", body.ErrorMessage,
 		"L-B 矩阵: 用户被删除时文案不可退化成 '登录已失效',否则泄漏软删除语义")
 }
 
@@ -572,9 +582,10 @@ func TestJwtAuthMiddleware_FrozenBeatsEverything(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 403, body.Code,
+	assert.False(t, body.Success)
+	assert.Equal(t, 403, body.ErrorCode,
 		"L-B 矩阵: 账号冻结(403) 胜出, 而非 TokenVersion(401) 或 ProductStatus(403/禁用)")
-	assert.Equal(t, "账号已被冻结", body.Msg,
+	assert.Equal(t, "账号已被冻结", body.ErrorMessage,
 		"L-B 矩阵: 冻结文案必须先于 '登录已失效'/'产品禁用' 返回给客户端")
 }
 
@@ -623,8 +634,9 @@ func TestJwtAuthMiddleware_NonMemberRejected(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 403, body.Code)
-	assert.Equal(t, "您已不是该产品的有效成员", body.Msg,
+	assert.False(t, body.Success)
+	assert.Equal(t, 403, body.ErrorCode)
+	assert.Equal(t, "您已不是该产品的有效成员", body.ErrorMessage,
 		"L-B 矩阵: MemberType 空 + 非超管 + 产品启用 必须精确命中'不是有效成员'文案")
 }
 
@@ -712,8 +724,9 @@ func TestJwtAuthMiddleware_FrozenBeatsTokenVersionNoProduct(t *testing.T) {
 
 	var body response.Body
 	require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
-	assert.Equal(t, 403, body.Code)
-	assert.Equal(t, "账号已被冻结", body.Msg)
+	assert.False(t, body.Success)
+	assert.Equal(t, 403, body.ErrorCode)
+	assert.Equal(t, "账号已被冻结", body.ErrorMessage)
 }
 
 // 覆盖目标:ParseWithHMAC 上移到 middleware 层作为唯一入口。

+ 3 - 2
internal/middleware/ratelimitMiddleware_test.go

@@ -79,8 +79,9 @@ func TestRateLimit_OverQuotaRejected(t *testing.T) {
 	var body response.Body
 	err := json.Unmarshal(w.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 429, body.Code)
-	assert.Equal(t, "请求过于频繁,请稍后再试", body.Msg)
+	assert.False(t, body.Success)
+	assert.Equal(t, 429, body.ErrorCode)
+	assert.Equal(t, "请求过于频繁,请稍后再试", body.ErrorMessage)
 }
 
 // TC-0548: behindProxy=false时XFF被忽略

+ 64 - 16
internal/response/response.go

@@ -4,50 +4,98 @@ import (
 	"context"
 	"errors"
 	"net/http"
+	"os"
 
 	"github.com/zeromicro/go-zero/core/logx"
 	"github.com/zeromicro/go-zero/rest/httpx"
+	"go.opentelemetry.io/otel/trace"
 )
 
+// Body 是统一 HTTP 响应体,格式与前端 API.Result<T> 对齐:
+// 成功:{ "success": true, "data": ... }
+// 失败:{ "success": false, "errorCode": N, "errorMessage": "...", "showType": N }
 type Body struct {
-	Code int         `json:"code"`
-	Msg  string      `json:"msg"`
-	Data interface{} `json:"data,omitempty"`
+	Success      bool        `json:"success"`
+	Data         interface{} `json:"data,omitempty"`
+	ErrorCode    int         `json:"errorCode,omitempty"`
+	ErrorMessage string      `json:"errorMessage,omitempty"`
+	ShowType     int         `json:"showType,omitempty"`
+	TraceId      string      `json:"traceId,omitempty"`
+	Host         string      `json:"host,omitempty"`
+}
+
+// ShowType 与前端 ErrorShowType 枚举对齐
+const (
+	ShowTypeSilent       = 0 // SILENT
+	ShowTypeWarnMessage  = 1 // WARN_MESSAGE
+	ShowTypeErrorMessage = 2 // ERROR_MESSAGE
+	ShowTypeNotification = 4 // NOTIFICATION
+	ShowTypeRedirect     = 9 // REDIRECT
+)
+
+var hostname string
+
+func init() {
+	hostname, _ = os.Hostname()
 }
 
 type CodeError struct {
-	code int
-	msg  string
+	code     int
+	msg      string
+	showType int
 }
 
 func NewCodeError(code int, msg string) *CodeError {
-	return &CodeError{code: code, msg: msg}
+	return &CodeError{code: code, msg: msg, showType: ShowTypeErrorMessage}
 }
 
-func (e *CodeError) Error() string { return e.msg }
-func (e *CodeError) Code() int     { return e.code }
+func (e *CodeError) Error() string    { return e.msg }
+func (e *CodeError) Code() int        { return e.code }
+func (e *CodeError) ShowType() int    { return e.showType }
 
-func ErrBadRequest(msg string) *CodeError   { return NewCodeError(400, msg) }
-func ErrUnauthorized(msg string) *CodeError { return NewCodeError(401, msg) }
-func ErrForbidden(msg string) *CodeError    { return NewCodeError(403, msg) }
-func ErrNotFound(msg string) *CodeError     { return NewCodeError(404, msg) }
+func ErrBadRequest(msg string) *CodeError      { return NewCodeError(400, msg) }
+func ErrUnauthorized(msg string) *CodeError    { return NewCodeError(401, msg) }
+func ErrForbidden(msg string) *CodeError       { return NewCodeError(403, msg) }
+func ErrNotFound(msg string) *CodeError        { return NewCodeError(404, msg) }
 func ErrConflict(msg string) *CodeError        { return NewCodeError(409, msg) }
 func ErrTooManyRequests(msg string) *CodeError { return NewCodeError(429, msg) }
 
+func traceIdFromCtx(ctx context.Context) string {
+	if sc := trace.SpanFromContext(ctx).SpanContext(); sc.IsValid() {
+		return sc.TraceID().String()
+	}
+	return ""
+}
+
 func Setup() {
 	httpx.SetOkHandler(func(_ context.Context, v any) any {
 		if v == nil {
-			return Body{Code: 0, Msg: "ok"}
+			return Body{Success: true, Host: hostname}
 		}
-		return Body{Code: 0, Msg: "ok", Data: v}
+		return Body{Success: true, Data: v, Host: hostname}
 	})
 
 	httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
+		traceId := traceIdFromCtx(ctx)
 		var ce *CodeError
 		if errors.As(err, &ce) {
-			return http.StatusOK, Body{Code: ce.Code(), Msg: ce.Error()}
+			return http.StatusOK, Body{
+				Success:      false,
+				ErrorCode:    ce.Code(),
+				ErrorMessage: ce.Error(),
+				ShowType:     ce.ShowType(),
+				TraceId:      traceId,
+				Host:         hostname,
+			}
 		}
 		logx.WithContext(ctx).Errorf("internal error: %+v", err)
-		return http.StatusOK, Body{Code: 500, Msg: "服务器内部错误"}
+		return http.StatusOK, Body{
+			Success:      false,
+			ErrorCode:    500,
+			ErrorMessage: "服务器内部错误",
+			ShowType:     ShowTypeErrorMessage,
+			TraceId:      traceId,
+			Host:         hostname,
+		}
 	})
 }

+ 18 - 12
internal/response/response_test.go

@@ -17,15 +17,16 @@ func TestNewCodeError(t *testing.T) {
 	err := NewCodeError(400, "bad request")
 	assert.Equal(t, 400, err.Code())
 	assert.Equal(t, "bad request", err.Error())
+	assert.Equal(t, 2, err.ShowType())
 }
 
 // TC-0265: 触发404等
 func TestCodeErrorHelpers(t *testing.T) {
 	tests := []struct {
-		name   string
-		fn     func(string) *CodeError
-		code   int
-		msg    string
+		name string
+		fn   func(string) *CodeError
+		code int
+		msg  string
 	}{
 		{"ErrBadRequest", ErrBadRequest, 400, "test bad"},
 		{"ErrUnauthorized", ErrUnauthorized, 401, "test unauth"},
@@ -39,6 +40,7 @@ func TestCodeErrorHelpers(t *testing.T) {
 			err := tt.fn(tt.msg)
 			assert.Equal(t, tt.code, err.Code())
 			assert.Equal(t, tt.msg, err.Error())
+			assert.Equal(t, 2, err.ShowType())
 		})
 	}
 }
@@ -80,8 +82,10 @@ func TestSetup_ErrorHandler_CodeError(t *testing.T) {
 	var body Body
 	err := json.Unmarshal(rr.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 400, body.Code)
-	assert.Equal(t, "参数错误", body.Msg)
+	assert.False(t, body.Success)
+	assert.Equal(t, 400, body.ErrorCode)
+	assert.Equal(t, "参数错误", body.ErrorMessage)
+	assert.Equal(t, 2, body.ShowType)
 	assert.Nil(t, body.Data)
 }
 
@@ -102,8 +106,10 @@ func TestSetup_ErrorHandler_InternalError(t *testing.T) {
 	var body Body
 	err := json.Unmarshal(rr.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 500, body.Code)
-	assert.Equal(t, "服务器内部错误", body.Msg)
+	assert.False(t, body.Success)
+	assert.Equal(t, 500, body.ErrorCode)
+	assert.Equal(t, "服务器内部错误", body.ErrorMessage)
+	assert.Equal(t, 2, body.ShowType)
 	assert.Nil(t, body.Data)
 }
 
@@ -124,8 +130,8 @@ func TestSetup_OkHandler_WithData(t *testing.T) {
 	var body Body
 	err := json.Unmarshal(rr.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 0, body.Code)
-	assert.Equal(t, "ok", body.Msg)
+	assert.True(t, body.Success)
+	assert.Equal(t, 0, body.ErrorCode)
 	require.NotNil(t, body.Data)
 	dataMap, ok := body.Data.(map[string]interface{})
 	require.True(t, ok)
@@ -149,7 +155,7 @@ func TestSetup_OkHandler_NilData(t *testing.T) {
 	var body Body
 	err := json.Unmarshal(rr.Body.Bytes(), &body)
 	require.NoError(t, err)
-	assert.Equal(t, 0, body.Code)
-	assert.Equal(t, "ok", body.Msg)
+	assert.True(t, body.Success)
+	assert.Equal(t, 0, body.ErrorCode)
 	assert.Nil(t, body.Data)
 }

+ 0 - 2
internal/types/types.go

@@ -35,8 +35,6 @@ type BindRolesReq struct {
 }
 
 type CapEndpointResp struct {
-	Code int    `json:"code"`
-	Msg  string `json:"msg"`
 	Data string `json:"data"`
 }
 

+ 0 - 2
perm.api

@@ -23,8 +23,6 @@ type (
 		Base64Image string `json:"base64image"`
 	}
 	CapEndpointResp {
-		Code int    `json:"code"`
-		Msg  string `json:"msg"`
 		Data string `json:"data"`
 	}
 	LoginReq {