补齐相关日志日志
This commit is contained in:
@@ -24,25 +24,32 @@ func (s *OrderService) VirtualOrderCreate(ctx context.Context, userID int64, req
|
||||
defer span.End()
|
||||
|
||||
if !wechat.IsVirtualPayEnabled() {
|
||||
slog.WarnContext(ctx, "virtual order create: virtual pay not enabled", "user_id", userID)
|
||||
return nil, errcode.VirtualPayNotEnabled
|
||||
}
|
||||
// 订单记录当前激活的虚拟支付环境(现网 0 / 沙箱 1),发货推送按此校验。
|
||||
xpayEnv, verr := wechat.GetVirtualPayEnv()
|
||||
if verr != nil {
|
||||
slog.ErrorContext(ctx, "virtual order create: resolve pay env failed", "user_id", userID, "error", verr)
|
||||
return nil, errcode.VirtualPayNotEnabled
|
||||
}
|
||||
|
||||
goods, err := s.goodsDao.GoodsGetByCode(ctx, request.GoodsCode)
|
||||
if err != nil {
|
||||
if isNotFound(err) {
|
||||
slog.InfoContext(ctx, "virtual order create: goods not found", "user_id", userID, "goods_code", request.GoodsCode)
|
||||
return nil, errcode.GoodsNotFound
|
||||
}
|
||||
slog.ErrorContext(ctx, "virtual order create: query goods failed", "goods_code", request.GoodsCode, "error", err)
|
||||
return nil, errcode.DatabaseError
|
||||
}
|
||||
if goods.Status != GoodsStatusOnShelf {
|
||||
slog.InfoContext(ctx, "virtual order create: goods off shelf", "user_id", userID, "goods_code", request.GoodsCode)
|
||||
return nil, errcode.GoodsOffShelf
|
||||
}
|
||||
if goods.ProductID == "" || goods.ProductStatus != GoodsProductPublished {
|
||||
slog.WarnContext(ctx, "virtual order create: goods not published as xpay product",
|
||||
"user_id", userID, "goods_code", request.GoodsCode, "product_id", goods.ProductID, "product_status", goods.ProductStatus)
|
||||
return nil, errcode.VirtualPayGoodsNotPublished
|
||||
}
|
||||
// iOS 端微信限制最低支付 1 元(100 分),低于此金额调起支付必失败,提前拒绝以降低垃圾订单。
|
||||
@@ -50,6 +57,8 @@ func (s *OrderService) VirtualOrderCreate(ctx context.Context, userID int64, req
|
||||
// 此校验仅作体验优化,不构成资损防线——真正的资损防线是发货推送 verifyXPayOrderPaid
|
||||
// 对微信侧订单真实性、金额、道具的反查。低金额商品在 iOS 上的垃圾订单靠过期清理兜底。
|
||||
if request.Platform == XPayPlatformIOS && goods.DiscountPrice < 100 {
|
||||
slog.InfoContext(ctx, "virtual order create: rejected low amount on ios",
|
||||
"user_id", userID, "goods_code", request.GoodsCode, "pay_amount", goods.DiscountPrice)
|
||||
return nil, errcode.VirtualPayAmountTooLow
|
||||
}
|
||||
|
||||
@@ -57,8 +66,10 @@ func (s *OrderService) VirtualOrderCreate(ctx context.Context, userID int64, req
|
||||
sessionKey, serr := redisdao.AuthSessionKeyGet(ctx, userID)
|
||||
if serr != nil {
|
||||
if serr == redis.Nil {
|
||||
slog.WarnContext(ctx, "virtual order create: session key missing", "user_id", userID)
|
||||
return nil, errcode.UnauthorizedTokenError.WithDetails("缺少 session_key,请重新登录")
|
||||
}
|
||||
slog.ErrorContext(ctx, "virtual order create: get session key failed", "user_id", userID, "error", serr)
|
||||
return nil, errcode.CacheError
|
||||
}
|
||||
|
||||
@@ -90,6 +101,8 @@ func (s *OrderService) VirtualOrderCreate(ctx context.Context, userID int64, req
|
||||
XPayEnv: int8(xpayEnv),
|
||||
}
|
||||
if cerr := s.orderDao.OrderCreate(ctx, order); cerr != nil {
|
||||
slog.ErrorContext(ctx, "virtual order create: save order failed",
|
||||
"order_no", orderNo, "user_id", userID, "goods_code", request.GoodsCode, "error", cerr)
|
||||
return nil, errcode.VirtualOrderCreateFailed
|
||||
}
|
||||
|
||||
@@ -102,9 +115,13 @@ func (s *OrderService) VirtualOrderCreate(ctx context.Context, userID int64, req
|
||||
payAmount,
|
||||
)
|
||||
if perr != nil {
|
||||
slog.ErrorContext(ctx, "virtual order create: build payment sign failed", "order_no", orderNo, "user_id", userID, "error", perr)
|
||||
return nil, errcode.VirtualOrderCreateFailed
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "virtual order created",
|
||||
"order_no", orderNo, "user_id", userID, "goods_code", request.GoodsCode, "pay_amount", payAmount, "env", xpayEnv)
|
||||
|
||||
return &dto.VirtualOrderCreateResponse{
|
||||
OrderNo: orderNo,
|
||||
PayAmount: payAmount,
|
||||
@@ -136,7 +153,8 @@ func (s *OrderService) XPayNotify(ctx context.Context, raw []byte) []byte {
|
||||
return wechat.BuildNotifyResponse(false)
|
||||
}
|
||||
if e := s.handleGoodsDeliver(ctx, &deliver, string(raw)); e != nil {
|
||||
slog.ErrorContext(ctx, "xpay deliver notify handle failed", "error", e)
|
||||
// 内部失败分支已各自记录 Error 日志,这里仅保留调用链痕迹,避免重复告警。
|
||||
slog.DebugContext(ctx, "xpay deliver notify handle failed", "error", e)
|
||||
return wechat.BuildNotifyResponse(false)
|
||||
}
|
||||
return wechat.BuildNotifyResponse(true)
|
||||
@@ -148,7 +166,8 @@ func (s *OrderService) XPayNotify(ctx context.Context, raw []byte) []byte {
|
||||
return wechat.BuildNotifyResponse(false)
|
||||
}
|
||||
if e := s.handleRefundNotify(ctx, &refund); e != nil {
|
||||
slog.ErrorContext(ctx, "xpay refund notify handle failed", "error", e)
|
||||
// 内部失败分支已各自记录 Error 日志,这里仅保留调用链痕迹,避免重复告警。
|
||||
slog.DebugContext(ctx, "xpay refund notify handle failed", "error", e)
|
||||
return wechat.BuildNotifyResponse(false)
|
||||
}
|
||||
return wechat.BuildNotifyResponse(true)
|
||||
@@ -241,7 +260,13 @@ func (s *OrderService) verifyXPayRefund(ctx context.Context, order *mysqlmodel.O
|
||||
func (s *OrderService) handleGoodsDeliver(ctx context.Context, deliver *wechat.XPayGoodsDeliverNotify, raw string) *errcode.Error {
|
||||
lockKey := fmtLockKey(redismodel.KeyLockPayCallback, deliver.OutTradeNo)
|
||||
lockVal := genUUID()
|
||||
if ok, _ := redisdao.TryLock(ctx, lockKey, lockVal, 30*time.Second); !ok {
|
||||
ok, lerr := redisdao.TryLock(ctx, lockKey, lockVal, 30*time.Second)
|
||||
if lerr != nil {
|
||||
slog.ErrorContext(ctx, "xpay deliver: acquire lock failed", "out_trade_no", deliver.OutTradeNo, "error", lerr)
|
||||
return nil // 锁不可用(Redis 故障):不重复处理,交由微信重试/持锁者。
|
||||
}
|
||||
if !ok {
|
||||
slog.DebugContext(ctx, "xpay deliver: concurrent callback, skip", "out_trade_no", deliver.OutTradeNo)
|
||||
return nil // 并发回调,交由持锁者处理。
|
||||
}
|
||||
defer func() { _ = redisdao.Unlock(ctx, lockKey, lockVal) }()
|
||||
@@ -252,6 +277,7 @@ func (s *OrderService) handleGoodsDeliver(ctx context.Context, deliver *wechat.X
|
||||
slog.ErrorContext(ctx, "xpay deliver: order not found", "out_trade_no", deliver.OutTradeNo)
|
||||
return errcode.OrderRealityCheckFailed
|
||||
}
|
||||
slog.ErrorContext(ctx, "xpay deliver: query order failed", "out_trade_no", deliver.OutTradeNo, "error", gerr)
|
||||
return errcode.DatabaseError
|
||||
}
|
||||
|
||||
@@ -287,9 +313,12 @@ func (s *OrderService) handleGoodsDeliver(ctx context.Context, deliver *wechat.X
|
||||
// 条件流转:待支付 -> 已支付,防重复回调。
|
||||
updated, uerr := s.orderDao.OrderUpdateStatusByOrderNo(ctx, deliver.OutTradeNo, OrderStatusPending, OrderStatusPaid)
|
||||
if uerr != nil {
|
||||
slog.ErrorContext(ctx, "xpay deliver: update order status failed", "out_trade_no", deliver.OutTradeNo, "error", uerr)
|
||||
return errcode.DatabaseError
|
||||
}
|
||||
if updated {
|
||||
slog.InfoContext(ctx, "xpay deliver: order paid",
|
||||
"order_no", order.OrderNo, "user_id", order.UserID, "pay_amount", order.PayAmount)
|
||||
s.finalizeVirtualPaidOrder(ctx, order, deliver.WeChatPayInfo.PaidTime, deliver.WeChatPayInfo.TransactionID, raw)
|
||||
return nil
|
||||
}
|
||||
@@ -298,33 +327,46 @@ func (s *OrderService) handleGoodsDeliver(ctx context.Context, deliver *wechat.X
|
||||
current, gerr := s.orderDao.OrderGetByOrderNo(ctx, deliver.OutTradeNo)
|
||||
if gerr != nil {
|
||||
if isNotFound(gerr) {
|
||||
slog.WarnContext(ctx, "xpay deliver: order disappeared after status conflict", "out_trade_no", deliver.OutTradeNo)
|
||||
return nil
|
||||
}
|
||||
slog.ErrorContext(ctx, "xpay deliver: reload order failed", "out_trade_no", deliver.OutTradeNo, "error", gerr)
|
||||
return errcode.DatabaseError
|
||||
}
|
||||
switch current.OrderStatus {
|
||||
case OrderStatusPaid:
|
||||
// 已被并发回调处理,兜底补发(幂等)。
|
||||
_ = s.orderDao.OrderUpdateByOrderNo(ctx, current.OrderNo, map[string]any{
|
||||
if uerr := s.orderDao.OrderUpdateByOrderNo(ctx, current.OrderNo, map[string]any{
|
||||
"pay_time": resolvePaidTime(deliver.WeChatPayInfo.PaidTime),
|
||||
"transaction_id": deliver.WeChatPayInfo.TransactionID,
|
||||
"callback_raw": raw,
|
||||
})
|
||||
}); uerr != nil {
|
||||
slog.ErrorContext(ctx, "xpay deliver: reconcile paid order pay info failed", "order_no", current.OrderNo, "error", uerr)
|
||||
}
|
||||
slog.InfoContext(ctx, "xpay deliver: order already paid, reconcile benefits", "order_no", current.OrderNo, "user_id", current.UserID)
|
||||
s.grantOrderBenefits(ctx, current)
|
||||
case OrderStatusExpired:
|
||||
// 订单被系统过期但用户已付款:过期 -> 已支付并补发货。
|
||||
if ok, uerr := s.orderDao.OrderUpdateStatusByOrderNo(ctx, deliver.OutTradeNo, OrderStatusExpired, OrderStatusPaid); uerr != nil {
|
||||
slog.ErrorContext(ctx, "xpay deliver: compensate expired order failed", "out_trade_no", deliver.OutTradeNo, "error", uerr)
|
||||
return errcode.DatabaseError
|
||||
} else if ok {
|
||||
slog.InfoContext(ctx, "xpay deliver: expired order compensated", "order_no", current.OrderNo, "user_id", current.UserID)
|
||||
s.finalizeVirtualPaidOrder(ctx, current, deliver.WeChatPayInfo.PaidTime, deliver.WeChatPayInfo.TransactionID, raw)
|
||||
} else {
|
||||
slog.DebugContext(ctx, "xpay deliver: expired order already handled by another callback", "out_trade_no", deliver.OutTradeNo)
|
||||
}
|
||||
case OrderStatusCancelled:
|
||||
// 防御分支:历史上已取消的虚拟支付订单用户仍完成了支付,补发货避免「钱已扣、权益不到账」。
|
||||
// 当前虚拟支付订单已禁止取消,此分支兜底存量数据与并发竞态。
|
||||
if ok, uerr := s.orderDao.OrderUpdateStatusByOrderNo(ctx, deliver.OutTradeNo, OrderStatusCancelled, OrderStatusPaid); uerr != nil {
|
||||
slog.ErrorContext(ctx, "xpay deliver: compensate cancelled order failed", "out_trade_no", deliver.OutTradeNo, "error", uerr)
|
||||
return errcode.DatabaseError
|
||||
} else if ok {
|
||||
slog.InfoContext(ctx, "xpay deliver: cancelled order compensated", "order_no", current.OrderNo, "user_id", current.UserID)
|
||||
s.finalizeVirtualPaidOrder(ctx, current, deliver.WeChatPayInfo.PaidTime, deliver.WeChatPayInfo.TransactionID, raw)
|
||||
} else {
|
||||
slog.DebugContext(ctx, "xpay deliver: cancelled order already handled by another callback", "out_trade_no", deliver.OutTradeNo)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -334,7 +376,13 @@ func (s *OrderService) handleGoodsDeliver(ctx context.Context, deliver *wechat.X
|
||||
func (s *OrderService) handleRefundNotify(ctx context.Context, refund *wechat.XPayRefundNotify) *errcode.Error {
|
||||
lockKey := fmtLockKey(redismodel.KeyLockRefundCallback, refund.RefundOrderID)
|
||||
lockVal := genUUID()
|
||||
if ok, _ := redisdao.TryLock(ctx, lockKey, lockVal, 30*time.Second); !ok {
|
||||
ok, lerr := redisdao.TryLock(ctx, lockKey, lockVal, 30*time.Second)
|
||||
if lerr != nil {
|
||||
slog.ErrorContext(ctx, "xpay refund: acquire lock failed", "out_refund_no", refund.RefundOrderID, "error", lerr)
|
||||
return nil // 锁不可用(Redis 故障):不重复处理,交由微信重试/持锁者。
|
||||
}
|
||||
if !ok {
|
||||
slog.DebugContext(ctx, "xpay refund: concurrent callback, skip", "out_refund_no", refund.RefundOrderID)
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = redisdao.Unlock(ctx, lockKey, lockVal) }()
|
||||
@@ -343,7 +391,8 @@ func (s *OrderService) handleRefundNotify(ctx context.Context, refund *wechat.XP
|
||||
// (退款推送按官方文档在退款完成时触发,状态枚举联调时以官方为准)。
|
||||
if refund.RefundStatus != wechat.XPayRefundStatusSuccess {
|
||||
slog.WarnContext(ctx, "xpay refund notify not success, skip revoke",
|
||||
"order_no", refund.OutTradeNo, "refund_status", refund.RefundStatus)
|
||||
"order_no", refund.OutTradeNo, "refund_order_id", refund.RefundOrderID,
|
||||
"refund_status", refund.RefundStatus, "refund_fee", refund.RefundFee)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -353,10 +402,12 @@ func (s *OrderService) handleRefundNotify(ctx context.Context, refund *wechat.XP
|
||||
slog.ErrorContext(ctx, "xpay refund: order not found", "out_trade_no", refund.OutTradeNo)
|
||||
return nil
|
||||
}
|
||||
slog.ErrorContext(ctx, "xpay refund: query order failed", "out_trade_no", refund.OutTradeNo, "error", gerr)
|
||||
return errcode.DatabaseError
|
||||
}
|
||||
// 已终态则幂等返回。
|
||||
if order.OrderStatus == OrderStatusRefunded {
|
||||
slog.DebugContext(ctx, "xpay refund: order already refunded, skip", "order_no", order.OrderNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -367,7 +418,11 @@ func (s *OrderService) handleRefundNotify(ctx context.Context, refund *wechat.XP
|
||||
}
|
||||
|
||||
// settleRefundSuccess 内部含条件流转(Paid -> Refunded),重复推送不会重复回退。
|
||||
s.settleRefundSuccess(ctx, order, refund.RefundFee)
|
||||
// 内部失败已由 settleRefundSuccess 的使用方日志覆盖:此处接收返回值并记录,避免静默。
|
||||
if serr := s.settleRefundSuccess(ctx, order, refund.RefundFee); serr != nil {
|
||||
slog.ErrorContext(ctx, "xpay refund: settle refund success failed",
|
||||
"order_no", order.OrderNo, "refund_order_id", refund.RefundOrderID, "error", serr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -377,20 +432,29 @@ func (s *OrderService) handleRefundNotify(ctx context.Context, refund *wechat.XP
|
||||
func (s *OrderService) handleIOSRefundQuery(ctx context.Context, query *wechat.XPayIOSRefundQueryNotify) []byte {
|
||||
order, err := s.orderDao.OrderGetByOrderNo(ctx, query.OutTradeNo)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "xpay ios refund query: order not found", "out_trade_no", query.OutTradeNo, "error", err)
|
||||
if isNotFound(err) {
|
||||
slog.ErrorContext(ctx, "xpay ios refund query: order not found", "out_trade_no", query.OutTradeNo)
|
||||
} else {
|
||||
slog.ErrorContext(ctx, "xpay ios refund query: query order failed", "out_trade_no", query.OutTradeNo, "error", err)
|
||||
}
|
||||
return wechat.BuildIOSRefundQueryResponse(1, "拒绝退款",
|
||||
"订单号 "+query.OutTradeNo+" 在本地无记录,无法确认权益使用情况,拒绝退款")
|
||||
}
|
||||
// 已退款:款项已退回,再次建议退款会造成重复退款资损。
|
||||
if order.OrderStatus == OrderStatusRefunded {
|
||||
slog.InfoContext(ctx, "xpay ios refund query: reject, order already refunded", "order_no", order.OrderNo)
|
||||
return wechat.BuildIOSRefundQueryResponse(1, "拒绝退款",
|
||||
"订单 "+order.OrderNo+" 已处于已退款状态,不支持重复退款")
|
||||
}
|
||||
// 权益已消耗则拒绝退款,防止「已使用权益后白嫖退款」。
|
||||
if s.iosRefundBenefitsConsumed(ctx, order) {
|
||||
slog.InfoContext(ctx, "xpay ios refund query: reject, benefits consumed",
|
||||
"order_no", order.OrderNo, "user_id", order.UserID)
|
||||
return wechat.BuildIOSRefundQueryResponse(1, "拒绝退款",
|
||||
"订单 "+order.OrderNo+" 发放的权益已被使用(上传额度已消耗或会员已生效),不支持退款")
|
||||
}
|
||||
slog.InfoContext(ctx, "xpay ios refund query: suggest refund, benefits unused",
|
||||
"order_no", order.OrderNo, "user_id", order.UserID)
|
||||
return wechat.BuildIOSRefundQueryResponse(0, "建议退款",
|
||||
"订单 "+order.OrderNo+" 发放的权益尚未使用,建议退款")
|
||||
}
|
||||
@@ -401,7 +465,10 @@ func (s *OrderService) iosRefundBenefitsConsumed(ctx context.Context, order *mys
|
||||
// 额度类:该订单发放的 Cos 凭证已消耗则视为已使用。
|
||||
if order.CosQuota > 0 {
|
||||
grants, err := mysqldao.NewQuotaGrantDao().QuotaGrantListByUserIDAndType(ctx, order.UserID, QuotaTypeCos)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "xpay ios refund query: list cos grants failed, treat as not consumed",
|
||||
"order_no", order.OrderNo, "user_id", order.UserID, "error", err)
|
||||
} else {
|
||||
for _, g := range grants {
|
||||
if g.RefID == order.OrderNo && g.Used > 0 {
|
||||
return true
|
||||
@@ -415,6 +482,9 @@ func (s *OrderService) iosRefundBenefitsConsumed(ctx context.Context, order *mys
|
||||
if user.VIPExpire > nowUnix() && user.VIPType != VIPTypeNone {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
slog.WarnContext(ctx, "xpay ios refund query: get user failed, treat vip as not consumed",
|
||||
"order_no", order.OrderNo, "user_id", order.UserID, "error", err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -423,11 +493,13 @@ func (s *OrderService) iosRefundBenefitsConsumed(ctx context.Context, order *mys
|
||||
// finalizeVirtualPaidOrder 虚拟支付发货:记录支付信息、发放权益并发送通知。
|
||||
// paidTime/transactionID 由调用方提供:推送路径取推送报文,查单补偿路径取 query_order 结果。
|
||||
func (s *OrderService) finalizeVirtualPaidOrder(ctx context.Context, order *mysqlmodel.Order, paidTime int64, transactionID, raw string) {
|
||||
_ = s.orderDao.OrderUpdateByOrderNo(ctx, order.OrderNo, map[string]any{
|
||||
if uerr := s.orderDao.OrderUpdateByOrderNo(ctx, order.OrderNo, map[string]any{
|
||||
"pay_time": resolvePaidTime(paidTime),
|
||||
"transaction_id": transactionID,
|
||||
"callback_raw": raw,
|
||||
})
|
||||
}); uerr != nil {
|
||||
slog.ErrorContext(ctx, "finalize virtual paid order: update pay info failed", "order_no", order.OrderNo, "error", uerr)
|
||||
}
|
||||
|
||||
s.grantOrderBenefits(ctx, order)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user