From 2f0104be07cf73711bcc5a35626b72bc129c4df7 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 30 Jul 2026 18:04:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BC=80=E6=94=BE=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=97=A5=E5=BF=97=E5=B9=B6=E4=BF=AE=E5=A4=8D=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E9=A1=B5=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/open_v1.go | 63 +++++++++++++++---- backend/internal/middleware/open_auth.go | 17 +++++ .../internal/middleware/source_open_auth.go | 13 ++++ frontend/vite.config.ts | 3 +- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/backend/internal/handler/open_v1.go b/backend/internal/handler/open_v1.go index 233eb3e..7f91384 100644 --- a/backend/internal/handler/open_v1.go +++ b/backend/internal/handler/open_v1.go @@ -7,6 +7,7 @@ import ( "affiliate_dash/internal/middleware" "affiliate_dash/internal/model" + "affiliate_dash/internal/pkg/openlog" "affiliate_dash/internal/pkg/response" "affiliate_dash/internal/service" @@ -26,11 +27,14 @@ func NewOpenV1Handler(merchantSvc *service.MerchantService, fulfillmentSvc *serv func (h *OpenV1Handler) ListProducts(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + openlog.Info(c, "list_products start page=%d size=%d", page, size) list, total, err := h.merchantSvc.ListMerchantProducts(middleware.GetMerchantID(c), page, size, true) if err != nil { + openlog.Warn(c, "list_products fail err=%v", err) response.ServerError(c, err.Error()) return } + openlog.Info(c, "list_products ok total=%d returned=%d", total, len(list)) response.Page(c, list, total, page, size) } @@ -45,10 +49,12 @@ type openCreateOrderReq struct { func (h *OpenV1Handler) CreateOrder(c *gin.Context) { var req openCreateOrderReq if err := c.ShouldBindJSON(&req); err != nil { + openlog.Warn(c, "create_order bind_fail err=%v", err) response.BadRequest(c, "参数错误:client_order_no 与 sku 必填") return } if key := c.GetHeader("Idempotency-Key"); key != "" && key != req.ClientOrderNo { + openlog.Warn(c, "create_order idempotency_key_mismatch header=%s body=%s", key, req.ClientOrderNo) response.BadRequest(c, "Idempotency-Key 必须与 client_order_no 一致") return } @@ -56,12 +62,15 @@ func (h *OpenV1Handler) CreateOrder(c *gin.Context) { if len(req.Data) > 0 { var decoded interface{} if err := json.Unmarshal(req.Data, &decoded); err != nil { + openlog.Warn(c, "create_order bad_data err=%v", err) response.BadRequest(c, "data 必须是有效 JSON") return } data = decoded } client := middleware.GetAPIClient(c) + openlog.Info(c, "create_order start client_order_no=%s sku=%s quantity=%d buyer=%s", + req.ClientOrderNo, req.SKU, req.Quantity, req.BuyerReference) result, err := h.fulfillmentSvc.CreateOrder(service.CreateFulfillmentOrderInput{ MerchantID: middleware.GetMerchantID(c), APIClientID: client.ID, @@ -72,6 +81,8 @@ func (h *OpenV1Handler) CreateOrder(c *gin.Context) { RequestData: data, }) if err != nil { + openlog.Warn(c, "create_order fail client_order_no=%s sku=%s err=%v", + req.ClientOrderNo, req.SKU, err) response.BadRequest(c, err.Error()) return } @@ -79,6 +90,8 @@ func (h *OpenV1Handler) CreateOrder(c *gin.Context) { if !result.Idempotent { status = http.StatusCreated } + openlog.Info(c, "create_order ok order_no=%s idempotent=%v amount=%d", + result.Order.OrderNo, result.Idempotent, result.Order.Amount) c.JSON(status, response.Body{ Code: 0, Message: "ok", @@ -90,8 +103,11 @@ func (h *OpenV1Handler) CreateOrder(c *gin.Context) { } func (h *OpenV1Handler) QueryOrder(c *gin.Context) { - order, err := h.fulfillmentSvc.GetOrder(middleware.GetMerchantID(c), c.Param("order_no")) + orderNo := c.Param("order_no") + openlog.Info(c, "query_order start order_no=%s", orderNo) + order, err := h.fulfillmentSvc.GetOrder(middleware.GetMerchantID(c), orderNo) if err != nil { + openlog.Warn(c, "query_order fail order_no=%s err=%v", orderNo, err) if err.Error() == "订单不存在" { response.NotFound(c, err.Error()) return @@ -99,6 +115,8 @@ func (h *OpenV1Handler) QueryOrder(c *gin.Context) { response.ServerError(c, err.Error()) return } + openlog.Info(c, "query_order ok order_no=%s status=%s payment=%s", + order.OrderNo, order.FulfillmentStatus, order.PaymentStatus) response.OK(c, buildOpenOrderResponse(order)) } @@ -109,15 +127,20 @@ type openCancelOrderReq struct { func (h *OpenV1Handler) CancelOrder(c *gin.Context) { var req openCancelOrderReq if err := c.ShouldBindJSON(&req); err != nil { + openlog.Warn(c, "cancel_order bind_fail err=%v", err) response.BadRequest(c, "参数错误") return } + orderNo := c.Param("order_no") + openlog.Info(c, "cancel_order start order_no=%s reason=%s", orderNo, req.Reason) client := middleware.GetAPIClient(c) - order, err := h.fulfillmentSvc.CancelOrder(middleware.GetMerchantID(c), client.ID, c.Param("order_no"), req.Reason) + order, err := h.fulfillmentSvc.CancelOrder(middleware.GetMerchantID(c), client.ID, orderNo, req.Reason) if err != nil { + openlog.Warn(c, "cancel_order fail order_no=%s err=%v", orderNo, err) response.BadRequest(c, err.Error()) return } + openlog.Info(c, "cancel_order ok order_no=%s", order.OrderNo) response.OK(c, buildOpenOrderResponse(order)) } @@ -132,6 +155,7 @@ type openShipNotifyReq struct { func (h *OpenV1Handler) ShipNotify(c *gin.Context) { var req openShipNotifyReq if err := c.ShouldBindJSON(&req); err != nil { + openlog.Warn(c, "client_ship_notify bind_fail err=%v", err) response.BadRequest(c, "参数错误:ship_status 必填") return } @@ -144,12 +168,14 @@ func (h *OpenV1Handler) ShipNotify(c *gin.Context) { case "failed": status = model.FulfillmentStatusFailed default: + openlog.Warn(c, "client_ship_notify bad_status=%s", req.ShipStatus) response.BadRequest(c, "ship_status 仅支持 processing、success、failed") return } var result interface{} if len(req.Result) > 0 { if err := json.Unmarshal(req.Result, &result); err != nil { + openlog.Warn(c, "client_ship_notify bad_result err=%v", err) response.BadRequest(c, "result 必须是有效 JSON") return } @@ -160,9 +186,12 @@ func (h *OpenV1Handler) ShipNotify(c *gin.Context) { orderNo = req.OrderNo } if orderNo == "" { + openlog.Warn(c, "client_ship_notify missing_order_no") response.BadRequest(c, "order_no 必填") return } + openlog.Info(c, "client_ship_notify start order_no=%s ship_status=%s provider_no=%s", + orderNo, req.ShipStatus, req.ProviderOrderNo) order, err := h.fulfillmentSvc.UpdateFulfillment(service.FulfillmentUpdateInput{ MerchantID: middleware.GetMerchantID(c), APIClientID: client.ID, @@ -173,18 +202,26 @@ func (h *OpenV1Handler) ShipNotify(c *gin.Context) { ResultData: result, }) if err != nil { + openlog.Warn(c, "client_ship_notify fail order_no=%s ship_status=%s err=%v", + orderNo, req.ShipStatus, err) response.BadRequest(c, err.Error()) return } + openlog.Info(c, "client_ship_notify ok order_no=%s status=%s", + order.OrderNo, order.FulfillmentStatus) response.OK(c, buildOpenOrderResponse(order)) } func (h *OpenV1Handler) GetWallet(c *gin.Context) { + openlog.Info(c, "get_wallet start") wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c)) if err != nil { + openlog.Warn(c, "get_wallet fail err=%v", err) response.ServerError(c, err.Error()) return } + openlog.Info(c, "get_wallet ok balance=%d frozen=%d", + wallet.AvailableBalance, wallet.FrozenBalance) response.OK(c, wallet) } @@ -201,18 +238,18 @@ func buildOpenOrderResponse(order *model.FulfillmentOrder) gin.H { "sku": order.ProductSKU, "name": order.ProductName, }, - "quantity": order.Quantity, - "base_amount": order.BaseAmount, - "fee_type": order.FeeType, + "quantity": order.Quantity, + "base_amount": order.BaseAmount, + "fee_type": order.FeeType, "service_fee_amount": order.ServiceFeeAmount, - "amount": order.Amount, - "currency": order.Currency, - "buyer_reference": order.BuyerReference, - "provider_order_no": order.ProviderOrderNo, - "failure_reason": order.FailureReason, - "created_at": order.CreatedAt, - "delivered_at": order.DeliveredAt, - "cancelled_at": order.CancelledAt, + "amount": order.Amount, + "currency": order.Currency, + "buyer_reference": order.BuyerReference, + "provider_order_no": order.ProviderOrderNo, + "failure_reason": order.FailureReason, + "created_at": order.CreatedAt, + "delivered_at": order.DeliveredAt, + "cancelled_at": order.CancelledAt, } if json.Valid([]byte(order.RequestData)) { data["data"] = json.RawMessage(order.RequestData) diff --git a/backend/internal/middleware/open_auth.go b/backend/internal/middleware/open_auth.go index 3d3fc74..2e18017 100644 --- a/backend/internal/middleware/open_auth.go +++ b/backend/internal/middleware/open_auth.go @@ -48,6 +48,7 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { c.Header("X-Request-Id", reqID) if cfg.DB == nil || cfg.Codec == nil { + openlog.Warn(c, "open_auth uninitialized") response.ServerError(c, "开放接口认证服务未初始化") c.Abort() return @@ -57,17 +58,20 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { nonce := c.GetHeader("X-Nonce") sign := c.GetHeader("X-Sign") if appKey == "" || timestamp == "" || nonce == "" || sign == "" { + openlog.Warn(c, "open_auth missing_headers app_key=%s", openlog.MaskKey(appKey)) response.Unauthorized(c, "缺少鉴权头:X-App-Key、X-Timestamp、X-Nonce、X-Sign") c.Abort() return } if len(nonce) < 8 || len(nonce) > 96 { + openlog.Warn(c, "open_auth bad_nonce_len len=%d app_key=%s", len(nonce), openlog.MaskKey(appKey)) response.Unauthorized(c, "X-Nonce 长度需在 8~96 之间") c.Abort() return } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || abs64(time.Now().Unix()-ts) > cfg.SkewSeconds { + openlog.Warn(c, "open_auth expired app_key=%s ts=%s skew=%d", openlog.MaskKey(appKey), timestamp, cfg.SkewSeconds) response.Unauthorized(c, "请求已过期或 X-Timestamp 格式错误") c.Abort() return @@ -75,6 +79,7 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { + openlog.Warn(c, "open_auth read_body_fail err=%v", err) response.BadRequest(c, "读取请求体失败") c.Abort() return @@ -83,17 +88,20 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { var client model.APIClient if err := cfg.DB.Where("app_key = ? AND status = ?", appKey, model.APIClientStatusActive).First(&client).Error; err != nil { + openlog.Warn(c, "open_auth invalid_key app_key=%s", openlog.MaskKey(appKey)) response.Unauthorized(c, "无效的 API Key") c.Abort() return } if client.ExpiresAt != nil && client.ExpiresAt.Before(time.Now()) { + openlog.Warn(c, "open_auth key_expired app_key=%s client_id=%d", openlog.MaskKey(appKey), client.ID) response.Unauthorized(c, "API Key 已过期") c.Abort() return } secret, err := cfg.Codec.Decrypt(client.SecretCiphertext) if err != nil { + openlog.Warn(c, "open_auth decrypt_fail app_key=%s client_id=%d err=%v", openlog.MaskKey(appKey), client.ID, err) response.ServerError(c, "API 客户端密钥不可用") c.Abort() return @@ -103,6 +111,10 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { path := c.Request.URL.Path expected := BuildOpenV1Sign(secret, appKey, timestamp, nonce, method, path, bodyBytes) if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) { + bodyHash := sha256.Sum256(bodyBytes) + openlog.Warn(c, "open_auth sign_mismatch app_key=%s method=%s path=%s body_sha256=%s sign=%s expected=%s", + openlog.MaskKey(appKey), method, path, hex.EncodeToString(bodyHash[:]), + openlog.MaskSign(sign), openlog.MaskSign(expected)) response.Unauthorized(c, "签名校验失败") c.Abort() return @@ -118,11 +130,13 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { } created := cfg.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&nonceRow) if created.Error != nil { + openlog.Warn(c, "open_auth nonce_db_fail app_key=%s err=%v", openlog.MaskKey(appKey), created.Error) response.ServerError(c, "记录请求 nonce 失败") c.Abort() return } if created.RowsAffected == 0 { + openlog.Warn(c, "open_auth nonce_replay app_key=%s nonce=%s", openlog.MaskKey(appKey), nonce) response.Unauthorized(c, "重复的 X-Nonce(请勿重放请求)") c.Abort() return @@ -133,6 +147,9 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { c.Set(CtxMerchantID, client.MerchantID) c.Set(openlog.CtxAPIKey, appKey) _ = cfg.DB.Model(&model.APIClient{}).Where("id = ?", client.ID).Update("last_used_at", now).Error + + openlog.Info(c, "open_auth ok app_key=%s merchant_id=%d method=%s path=%s body_size=%d", + openlog.MaskKey(appKey), client.MerchantID, method, path, len(bodyBytes)) c.Next() } } diff --git a/backend/internal/middleware/source_open_auth.go b/backend/internal/middleware/source_open_auth.go index e26669c..aa08845 100644 --- a/backend/internal/middleware/source_open_auth.go +++ b/backend/internal/middleware/source_open_auth.go @@ -60,6 +60,7 @@ func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc { c.Header("X-Request-Id", reqID) if cfg.APIKey == "" || cfg.APISecret == "" { + openlog.Warn(c, "source_open_auth uninitialized") response.ServerError(c, "服务端未配置 OPEN_API_KEY / OPEN_API_SECRET") c.Abort() return @@ -69,22 +70,26 @@ func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc { nonce := c.GetHeader("X-Nonce") sign := c.GetHeader("X-Sign") if apiKey == "" || timestamp == "" || nonce == "" || sign == "" { + openlog.Warn(c, "source_open_auth missing_headers") response.Unauthorized(c, "缺少鉴权头:需要 X-Api-Key、X-Timestamp、X-Nonce、X-Sign") c.Abort() return } if apiKey != cfg.APIKey { + openlog.Warn(c, "source_open_auth invalid_key api_key=%s", openlog.MaskKey(apiKey)) response.Unauthorized(c, "无效的 API Key") c.Abort() return } if len(nonce) < 8 || len(nonce) > 64 { + openlog.Warn(c, "source_open_auth bad_nonce_len len=%d", len(nonce)) response.Unauthorized(c, "X-Nonce 长度需在 8~64 之间") c.Abort() return } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || abs64(time.Now().Unix()-ts) > cfg.SkewSeconds { + openlog.Warn(c, "source_open_auth expired ts=%s skew=%d", timestamp, cfg.SkewSeconds) response.Unauthorized(c, "请求已过期或 X-Timestamp 格式错误") c.Abort() return @@ -92,6 +97,7 @@ func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc { bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { + openlog.Warn(c, "source_open_auth read_body_fail err=%v", err) response.BadRequest(c, "读取请求体失败") c.Abort() return @@ -99,16 +105,23 @@ func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc { c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) if store.seen(apiKey+":"+nonce, time.Now().Unix(), cfg.SkewSeconds) { + openlog.Warn(c, "source_open_auth nonce_replay nonce=%s", nonce) response.Unauthorized(c, "重复的 X-Nonce(请勿重放请求)") c.Abort() return } expected := BuildOpenSign(apiKey, cfg.APISecret, timestamp, nonce, c.Request.Method, c.Request.URL.Path, string(bodyBytes)) if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) { + openlog.Warn(c, "source_open_auth sign_mismatch method=%s path=%s body=%s sign=%s expected=%s", + c.Request.Method, c.Request.URL.Path, openlog.Truncate(string(bodyBytes), 200), + openlog.MaskSign(sign), openlog.MaskSign(expected)) response.Unauthorized(c, "签名校验失败") c.Abort() return } + + openlog.Info(c, "source_open_auth ok method=%s path=%s body_size=%d", + c.Request.Method, c.Request.URL.Path, len(bodyBytes)) c.Next() } } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 766d0ec..b4c36fa 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,7 +12,8 @@ export default defineConfig(({ mode }) => { server: { port, proxy: { - '/api': { + // 只代理真实后端 API,避免 /api-debug 这类前端路由刷新时被误转发到后端。 + '^/api(/|$)': { target: apiTarget, changeOrigin: true, },