diff --git a/internal/handler/order.go b/internal/handler/order.go index ba6004a..55074ed 100644 --- a/internal/handler/order.go +++ b/internal/handler/order.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "strconv" "strings" @@ -17,6 +18,9 @@ import ( "github.com/wangjia/pay/internal/util" ) +// maxOrderBodyBytes 下单请求体上限(防超大 body 内存 DoS)。 +const maxOrderBodyBytes = 64 << 10 // 64KB + // isMobileUA 粗略判断是否手机浏览器(决定支付宝走 wap.pay 拉起 App 还是 page.pay 扫码)。 func isMobileUA(ua string) bool { ua = strings.ToLower(ua) @@ -75,14 +79,14 @@ func verifyBizSign(c *gin.Context, system string, rawBody []byte) error { // 独立收款(浏览器 /paytest):只传 product_id,无需签名。 // 业务对接(jiu 后端):带 biz_system/biz_ref + HMAC 签名头,服务端校验后受理。 func (h *OrderHandler) Create(c *gin.Context) { - raw, err := io.ReadAll(c.Request.Body) + raw, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxOrderBodyBytes)) if err != nil { - util.RespondError(c, http.StatusBadRequest, "bad_request", "读取请求失败") + util.RespondError(c, http.StatusBadRequest, "bad_request", "请求体过大或读取失败") return } var req createOrderRequest if err := json.Unmarshal(raw, &req); err != nil { - util.RespondError(c, http.StatusBadRequest, "bad_request", "参数错误:"+err.Error()) + util.RespondError(c, http.StatusBadRequest, "bad_request", "参数格式错误") return } if req.ProductID == 0 { @@ -103,7 +107,9 @@ func (h *OrderHandler) Create(c *gin.Context) { util.RespondError(c, http.StatusNotFound, "product_not_found", err.Error()) return } - util.RespondError(c, http.StatusInternalServerError, "create_failed", err.Error()) + // 内部错误详情只进日志,不回给客户端(防信息泄露)。 + log.Printf("[order] 下单失败 product_id=%d biz=%q: %v", req.ProductID, req.BizSystem, err) + util.RespondError(c, http.StatusInternalServerError, "create_failed", "下单失败,请稍后重试") return } util.RespondSuccess(c, gin.H{ @@ -127,7 +133,8 @@ func (h *OrderHandler) CreateQR(c *gin.Context) { util.RespondError(c, http.StatusNotFound, "product_not_found", err.Error()) return } - util.RespondError(c, http.StatusInternalServerError, "create_failed", err.Error()) + log.Printf("[order] 扫码下单失败 product_id=%d: %v", req.ProductID, err) + util.RespondError(c, http.StatusInternalServerError, "create_failed", "下单失败,请稍后重试") return } util.RespondSuccess(c, gin.H{ diff --git a/internal/service/order.go b/internal/service/order.go index 8b9163e..2aa61d1 100644 --- a/internal/service/order.go +++ b/internal/service/order.go @@ -253,6 +253,13 @@ func (s *OrderService) GetByOutTradeNo(outTradeNo string) (*model.Order, error) // SyncPending 兜底:把近期待支付订单拿去主动查单,命中已支付则补记(防回调丢失)。 func (s *OrderService) SyncPending(ctx context.Context, maxAge time.Duration) { + // 后台循环调用;恢复 panic 防止拖垮整个进程。 + defer func() { + if r := recover(); r != nil { + log.Printf("[query_sync] 发生 panic 已恢复: %v", r) + } + }() + var orders []model.Order cutoff := time.Now().Add(-maxAge) if err := s.db.Where("status = ? AND created_at > ?", model.OrderPending, cutoff). @@ -295,6 +302,13 @@ func (s *OrderService) StartQuerySync(interval, maxAge time.Duration) { // notifyBizByOutTradeNo 向业务系统推送「支付成功」webhook(签名)。成功则置 BizNotified,失败留待重试。 func (s *OrderService) notifyBizByOutTradeNo(outTradeNo string) { + // 该函数在 goroutine 中运行(applyPaid 异步触发 + 重试循环);Go 中未恢复的 goroutine panic 会整进程崩溃。 + defer func() { + if r := recover(); r != nil { + log.Printf("[biz_notify] 订单 %s 回调发生 panic 已恢复: %v", outTradeNo, r) + } + }() + var o model.Order if err := s.db.First(&o, "out_trade_no = ?", outTradeNo).Error; err != nil { return