feat(order): 重构退款全流程并新增申请退款中状态

- 订单状态新增「6 申请退款中」,用户申请退款原子流转并落待审核退款流水
- 退款失败回退订单至已支付并恢复被取消的权益发放任务
- 新增虚拟支付退款查单兜底任务,反查超时订单并自动订正
- 会员包来源额度不叠加,扣减/回退/回收口径统一为只取生效一份
- 退款回收流水按吊销前后有效剩余差值记账,避免无效回收记录
- 同步更新 README 与 Stitch 设计文档
This commit is contained in:
2026-09-16 03:44:09 +08:00
parent 89b59c7b91
commit b94dd6dffb
29 changed files with 2141 additions and 158 deletions
+70 -8
View File
@@ -394,10 +394,12 @@ func IsOrderRefundedStatus(status int) bool {
return status == XPayOrderStatusRefunded || status == XPayOrderStatusUserRefunded
}
// 虚拟支付退款推送的退款状态(RefundStatus 字段,以微信官方枚举为准,联调时校正)。
const (
XPayRefundStatusSuccess = "SUCCESS" // 退款成功
)
// 虚拟支付退款推送(xpay_refund_notify)的退款结果字段:官方以 RetCode 判定,
// RetCode=0 表示退款成功,非 0 表示失败(失败原因见 RetMsg)。
const XPayRefundRetCodeSuccess = 0
// XPayRefundStatusSuccess 旧版式退款推送的 RefundStatus 取值(官方现行报文已无该字段,保留兼容解析)。
const XPayRefundStatusSuccess = "SUCCESS"
// QueryOrder 查询虚拟支付订单(orderID 与 wxOrderID 二选一,非空者优先;作用于激活环境)。
func QueryOrder(ctx context.Context, openid, orderID, wxOrderID string) (*XPayOrderInfo, error) {
@@ -562,22 +564,82 @@ type XPayGoodsDeliverNotify struct {
} `json:"GoodsInfo"`
}
// XPayRefundNotify 退款推送。
// XPayRefundNotify 退款推送(xpay_refund_notify)。
//
// 字段以官方《虚拟支付:企业、个体户 → 消息推送 → 退款推送》为准:
// 官方报文中**没有** OutTradeNo / RefundStatus / Env,订单号取 MchOrderId(原支付单商户单号,
// 即本地 orders.order_no),退款结果取 RetCode(0=成功,非 0=失败,原因见 RetMsg)。
// 同时保留 RefundOrderId / RefundStatus 等旧版式字段与非官方版式的 OutTradeNo,避免版式差异导致静默失败。
type XPayRefundNotify struct {
XPayNotifyBase
RefundStatus string `json:"RefundStatus"` // 退款状态(以官方枚举为准)
RefundFee int64 `json:"RefundFee"` // 退款金额(分)
RefundOrderID string `json:"RefundOrderId"` // 商户退款单号
WxRefundID string `json:"WxRefundId"` // 微信退款单号
MchRefundID string `json:"MchRefundId"` // 商户退款单号
WxOrderID string `json:"WxOrderId"` // 退款单对应支付单的微信单号
MchOrderID string `json:"MchOrderId"` // 退款单对应支付单的商户单号(= 本地订单号)
RefundFee int64 `json:"RefundFee"` // 退款金额(分)
RetCode *int `json:"RetCode"` // 退款结果:0=成功,非 0=失败;nil 表示报文未携带
RetMsg string `json:"RetMsg"` // 退款结果详情(失败原因)
RefundStartTimestamp int64 `json:"RefundStartTimestamp"` // 开始退款时间(秒级时间戳)
RefundSuccTimestamp int64 `json:"RefundSuccTimestamp"` // 结束退款时间(秒级时间戳)
WxpayRefundTransactionID string `json:"WxpayRefundTransactionId"` // 退款单的微信支付单号
RetryTimes int `json:"RetryTimes"` // 微信重试次数(从 0 开始)
Attach string `json:"Attach"` // iOS 退款推送附带:下单 attach(选填)
WxTransactionID string `json:"WxTransactionId"` // iOS 退款推送附带:原支付单 TransactionId(选填)
// ---- 旧版式兼容字段(官方现行报文已无,保留解析以防版式回退)----
RefundStatus string `json:"RefundStatus"` // 退款状态(旧版式,SUCCESS=成功)
RefundOrderID string `json:"RefundOrderId"` // 商户退款单号(旧版式)
}
// OrderNo 取商户订单号:官方字段 MchOrderId 优先,旧版式回退公共字段 OutTradeNo。
func (n *XPayRefundNotify) OrderNo() string {
if n.MchOrderID != "" {
return n.MchOrderID
}
return n.OutTradeNo
}
// RefundNo 取商户退款单号:官方字段 MchRefundId 优先,依次回退旧版式字段与微信退款单号(仅用于幂等锁与日志)。
func (n *XPayRefundNotify) RefundNo() string {
switch {
case n.MchRefundID != "":
return n.MchRefundID
case n.RefundOrderID != "":
return n.RefundOrderID
default:
return n.WxRefundID
}
}
// Succeeded 判定退款结果:官方以 RetCode=0 表示成功;旧版式回退 RefundStatus(大小写不敏感 SUCCESS)。
// determinable=false 表示报文未携带任何可判定字段,调用方应 fail-closed(应答失败触发微信重试)。
func (n *XPayRefundNotify) Succeeded() (ok bool, determinable bool) {
if n.RetCode != nil {
return *n.RetCode == XPayRefundRetCodeSuccess, true
}
if n.RefundStatus != "" {
return strings.EqualFold(n.RefundStatus, XPayRefundStatusSuccess), true
}
return false, false
}
// XPayIOSRefundQueryNotify iOS 退款问询推送。
type XPayIOSRefundQueryNotify struct {
XPayNotifyBase
// 字段以官方文档为准;此处解析退款问询所需的最小信息。
MchOrderID string `json:"MchOrderId"` // 兼容:部分版式用商户单号命名
RefundOrderID string `json:"RefundOrderId"`
RefundFee int64 `json:"RefundFee"`
}
// OrderNo 取商户订单号:公共字段 OutTradeNo 优先(保持既有行为),为空时回退 MchOrderId。
func (n *XPayIOSRefundQueryNotify) OrderNo() string {
if n.OutTradeNo != "" {
return n.OutTradeNo
}
return n.MchOrderID
}
// ParseNotify 解析消息推送 JSON 并返回事件类型与公共字段。
// 返回值为 base(公共字段)+ 原始 body,调用方再按 Event 分发到具体结构。
func ParseNotify(raw []byte) (*XPayNotifyBase, error) {