修复虚拟支付

This commit is contained in:
2026-09-14 01:52:32 +08:00
parent b6e842ac9b
commit d87f904e33
3 changed files with 145 additions and 51 deletions
+61 -29
View File
@@ -289,43 +289,75 @@ func (c *virtualPayClient) callXPay(ctx context.Context, uri string, body any, o
// ---- 订单查询 / 退款 ----
// XPayOrderInfo 虚拟支付订单查询结果。
// XPayOrder query_order 响应中的 order 子对象(现金单)。
// 注意两点官方约定(易错):订单信息包在 order 里(不是平铺在顶层);status 为数字枚举。
type XPayOrder struct {
OrderID string `json:"order_id"` // 订单号(即下单时的 outTradeNo)
WxOrderID string `json:"wx_order_id"` // 微信内部单号
Status int `json:"status"` // 当前状态,见 XPayOrderStatus* 常量
BizType int `json:"biz_type"` // 业务类型 0=短剧
OrderFee int64 `json:"order_fee"` // 订单金额(分)
PaidFee int64 `json:"paid_fee"` // 用户支付金额(分)
OrderType int `json:"order_type"` // 订单类型,见 XPayOrderType* 常量
RefundFee int64 `json:"refund_fee"` // 退款单类型时的退款金额(分)
LeftFee int64 `json:"left_fee"` // 支付单退款后剩余金额(分)
PaidTime int64 `json:"paid_time"` // 支付/退款时间(秒)
ProvideTime int64 `json:"provide_time"` // 发货时间(秒)
BizMeta string `json:"biz_meta"` // 订单创建时透传的信息(下单时的 attach)
EnvType int `json:"env_type"` // 环境类型 1=现网 2=沙箱
ChannelOrderID string `json:"channel_order_id"` // 渠道单号(用户支付详情页的商户单号)
WxpayOrderID string `json:"wxpay_order_id"` // 微信支付交易单号
SettTime int64 `json:"sett_time"` // 结算时间(秒,>0 表示结算成功)
SettState int `json:"sett_state"` // 结算状态 0/3=未开始或待结算 1=结算中 2=结算成功
}
// XPayOrderInfo query_order 响应体。
type XPayOrderInfo struct {
OrderID string `json:"order_id"` // 商户订单号
WxOrderID string `json:"wx_order_id"` // 微信订单号
OpenID string `json:"openid"` // 用户 openid
ProductID string `json:"product_id"` // 道具 ID
Quantity int64 `json:"quantity"` // 购买数量
PayFee int64 `json:"pay_fee"` // 实付金额(分)
Status string `json:"status"` // 订单状态(以官方枚举为准)
RefundStatus string `json:"refund_status"` // 退款状态(以官方枚举为准,待确认字段名)
RefundFee int64 `json:"refund_fee"` // 退款金额(分,待确认字段名)
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
Order XPayOrder `json:"order"`
}
// 虚拟支付订单支付状态(query_order 返回的 status 字段,以微信官方枚举为准,联调时校正)。
// 虚拟支付订单状态(query_order 返回的 order.status,数字枚举)。
const (
XPayOrderStatusPaid = "PAID" // 已支付
XPayOrderStatusInit = 0 // 订单初始化(未创建成功,不可支付)
XPayOrderStatusCreated = 1 // 订单创建成功(未支付)
XPayOrderStatusPaid = 2 // 订单已支付,待发货
XPayOrderStatusProviding = 3 // 订单发货中
XPayOrderStatusProvided = 4 // 订单已发货
XPayOrderStatusRefunded = 5 // 订单已退款
XPayOrderStatusClosed = 6 // 订单已关闭
XPayOrderStatusRefundFailed = 7 // 订单退款失败
XPayOrderStatusUserRefunded = 8 // 用户退款完成
XPayOrderStatusAdRecovered = 9 // 回收广告金完成
XPayOrderStatusSettleBack = 10 // 分账回退完成
)
// IsOrderPaidStatus 判断 query_order 返回的订单状态是否为「已支付」。
// 白名单判断:仅命中已知成功枚举返回 true;未知状态/空串一律视为未支付,
// 供发货链路 fail-closed 使用(判定为未支付的推送会被拒绝并交由微信重试)。
func IsOrderPaidStatus(status string) bool {
return status == XPayOrderStatusPaid
// 虚拟支付订单类型(query_order 返回的 order.order_type)。
const (
XPayOrderTypeNormal = 0 // 普通虚拟支付
XPayOrderTypeRefund = 1 // 普通退款
XPayOrderTypeIOSPay = 7 // 苹果 iOS 支付
XPayOrderTypeIOSRefund = 8 // 苹果 iOS 退款
)
// IsOrderPaidStatus 判断微信侧订单是否已支付(2 已支付待发货 / 3 发货中 / 4 已发货)。
// 白名单判断:仅命中「已支付及之后」的枚举返回 true,其余(未支付/已退款/已关闭/未知)
// 一律视为未支付,供发货链路 fail-closed 使用(判定为未支付的推送会被拒绝并交由微信重试)。
// 3/4 纳入白名单是为了让「已发货后的重试推送」仍能通过反查(幂等补发货场景)。
func IsOrderPaidStatus(status int) bool {
switch status {
case XPayOrderStatusPaid, XPayOrderStatusProviding, XPayOrderStatusProvided:
return true
default:
return false
}
}
// 虚拟支付订单退款状态(query_order 返回的 refund_status 字段,以微信官方枚举为准,联调时校正)。
const (
XPayOrderRefundedStatus = "REFUNDED" // 已退款
)
// IsOrderRefundedStatus 判断 query_order 返回的订单退款状态是否为「已退款」。
// 白名单判断:仅命中已知成功枚举返回 true;未知状态/空串一律视为未退款,
// 供退款链路 fail-closed 使用(判定为未退款的推送会被拒绝并交由微信重试)。
func IsOrderRefundedStatus(status string) bool {
return status == XPayOrderRefundedStatus
// IsOrderRefundedStatus 判断微信侧订单是否已退款(5 订单已退款 / 8 用户退款完成)。
// 白名单判断,fail-closed,供退款链路使用(判定为未退款的推送会被拒绝并交由微信重试)。
func IsOrderRefundedStatus(status int) bool {
return status == XPayOrderStatusRefunded || status == XPayOrderStatusUserRefunded
}
// 虚拟支付退款推送的退款状态(RefundStatus 字段,以微信官方枚举为准,联调时校正)。
+67 -7
View File
@@ -148,25 +148,85 @@ func TestParseNotifyAndResponse(t *testing.T) {
}
}
// TestIsOrderPaidStatus 校验订单支付状态白名单判断:仅已知成功枚举为已支付,未知/空串按未支付。
// TestIsOrderPaidStatus 校验订单支付状态白名单判断:仅「已支付及之后」为已支付,
// 未支付/已退款/已关闭等一律按未支付(fail-closed)。
func TestIsOrderPaidStatus(t *testing.T) {
cases := []struct {
status string
status int
want bool
}{
{XPayOrderStatusPaid, true},
{"NOTPAY", false},
{"CLOSED", false},
{"UNKNOWN", false},
{"", false},
{XPayOrderStatusProviding, true},
{XPayOrderStatusProvided, true},
{XPayOrderStatusInit, false},
{XPayOrderStatusCreated, false},
{XPayOrderStatusRefunded, false},
{XPayOrderStatusClosed, false},
{XPayOrderStatusRefundFailed, false},
}
for _, c := range cases {
if got := IsOrderPaidStatus(c.status); got != c.want {
t.Fatalf("IsOrderPaidStatus(%q) = %v, want %v", c.status, got, c.want)
t.Fatalf("IsOrderPaidStatus(%d) = %v, want %v", c.status, got, c.want)
}
}
}
// TestIsOrderRefundedStatus 校验退款状态白名单判断:仅已退款/用户退款完成为已退款。
func TestIsOrderRefundedStatus(t *testing.T) {
cases := []struct {
status int
want bool
}{
{XPayOrderStatusRefunded, true},
{XPayOrderStatusUserRefunded, true},
{XPayOrderStatusPaid, false},
{XPayOrderStatusRefundFailed, false},
{XPayOrderStatusCreated, false},
}
for _, c := range cases {
if got := IsOrderRefundedStatus(c.status); got != c.want {
t.Fatalf("IsOrderRefundedStatus(%d) = %v, want %v", c.status, got, c.want)
}
}
}
// TestXPayOrderInfoUnmarshal 校验 query_order 响应解析:订单字段包在 order 子对象内,status 为数字枚举。
// 回归防护:早期实现把字段平铺在顶层且 status 定义为 string,导致反查恒失败、订单永远停在待支付。
func TestXPayOrderInfoUnmarshal(t *testing.T) {
raw := []byte(`{
"errcode": 0,
"errmsg": "",
"order": {
"order_id": "OD20260914014559fc1cfee91b3748a2",
"wx_order_id": "wx_order_1",
"status": 2,
"order_fee": 990,
"paid_fee": 990,
"order_type": 0,
"refund_fee": 0,
"left_fee": 990,
"env_type": 2,
"biz_meta": "monthly_vip"
}
}`)
var info XPayOrderInfo
if err := json.Unmarshal(raw, &info); err != nil {
t.Fatalf("unmarshal query_order: %v", err)
}
if info.Order.Status != XPayOrderStatusPaid {
t.Fatalf("status = %d, want %d", info.Order.Status, XPayOrderStatusPaid)
}
if info.Order.PaidFee != 990 {
t.Fatalf("paid_fee = %d, want 990", info.Order.PaidFee)
}
if info.Order.OrderID != "OD20260914014559fc1cfee91b3748a2" {
t.Fatalf("order_id = %q", info.Order.OrderID)
}
if !IsOrderPaidStatus(info.Order.Status) {
t.Fatalf("IsOrderPaidStatus(%d) = false, want true", info.Order.Status)
}
}
// TestResolveEnvKey 校验环境自动选择规则:只填现网/只填沙箱/两把都填/两把都空。
func TestResolveEnvKey(t *testing.T) {
// 只填现网 -> 现网