2026-02-18 23:23:34 +08:00

345 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package wechat
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"bindbox-game/configs"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/httpclient"
)
type orderKey struct {
OrderNumberType int `json:"order_number_type"`
TransactionID string `json:"transaction_id,omitempty"`
MchID string `json:"mchid,omitempty"`
OutTradeNo string `json:"out_trade_no,omitempty"`
}
type shippingItem struct {
TrackingNo string `json:"tracking_no,omitempty"`
ExpressCompany string `json:"express_company,omitempty"`
ItemDesc string `json:"item_desc"`
}
type payerInfo struct {
Openid string `json:"openid"`
}
type uploadShippingInfoRequest struct {
OrderKey orderKey `json:"order_key"`
LogisticsType int `json:"logistics_type"`
DeliveryMode int `json:"delivery_mode"`
IsAllDelivered *bool `json:"is_all_delivered,omitempty"`
ShippingList []shippingItem `json:"shipping_list"`
UploadTime string `json:"upload_time"`
Payer *payerInfo `json:"payer,omitempty"`
}
type commonResp struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
func isOrderNotFoundError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
if strings.Contains(msg, "errcode=10060001") {
return true
}
if strings.Contains(msg, "支付单不存在") {
return true
}
return false
}
func uploadVirtualShippingInternal(ctx core.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
if accessToken == "" {
return fmt.Errorf("access_token 不能为空")
}
if itemDesc == "" {
return fmt.Errorf("参数缺失")
}
reqBody := &uploadShippingInfoRequest{
OrderKey: key,
LogisticsType: 3,
DeliveryMode: 1,
ShippingList: []shippingItem{{ItemDesc: itemDesc}},
UploadTime: uploadTime.Format(time.RFC3339),
}
if payerOpenid != "" {
reqBody.Payer = &payerInfo{Openid: payerOpenid}
}
b, _ := json.Marshal(reqBody)
fmt.Printf("[虚拟发货] 请求 upload_shipping_info order_key=%+v body=%s\n", key, string(b))
client := httpclient.GetHttpClientWithContext(ctx.RequestContext())
resp, err := client.R().
SetQueryParam("access_token", accessToken).
SetHeader("Content-Type", "application/json").
SetBody(b).
Post("https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info")
if err != nil {
return err
}
var cr commonResp
if err := json.Unmarshal(resp.Body(), &cr); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
if cr.ErrCode != 0 {
// 10060003 = 订单已发货,视为成功
if cr.ErrCode == 10060003 {
fmt.Printf("[虚拟发货] 微信返回已发货(10060003),视为成功\n")
return nil
}
return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
}
return nil
}
func UploadVirtualShippingWithAccessToken(ctx core.Context, accessToken string, transactionID string, payerOpenid string, itemDesc string, uploadTime time.Time) error {
if transactionID == "" {
return fmt.Errorf("参数缺失")
}
key := orderKey{OrderNumberType: 2, TransactionID: transactionID}
return uploadVirtualShippingInternal(ctx, accessToken, key, payerOpenid, itemDesc, uploadTime)
}
func UploadVirtualShipping(ctx core.Context, config *WechatConfig, transactionID string, payerOpenid string, itemDesc string, uploadTime time.Time) error {
accessToken, err := GetAccessToken(ctx, config)
if err != nil {
return err
}
return UploadVirtualShippingWithAccessToken(ctx, accessToken, transactionID, payerOpenid, itemDesc, uploadTime)
}
func UploadVirtualShippingWithFallback(ctx core.Context, config *WechatConfig, transactionID string, outTradeNo string, payerOpenid string, itemDesc string, uploadTime time.Time) error {
accessToken, err := GetAccessToken(ctx, config)
if err != nil {
return err
}
if transactionID != "" {
err = uploadVirtualShippingInternal(ctx, accessToken, orderKey{OrderNumberType: 2, TransactionID: transactionID}, payerOpenid, itemDesc, uploadTime)
if err == nil {
return nil
}
if !isOrderNotFoundError(err) {
return err
}
if outTradeNo == "" {
return err
}
fmt.Printf("[虚拟发货] 使用 transaction_id 发货返回支付单不存在,等待重试 transaction_id=%s\n", transactionID)
time.Sleep(time.Second)
err = uploadVirtualShippingInternal(ctx, accessToken, orderKey{OrderNumberType: 2, TransactionID: transactionID}, payerOpenid, itemDesc, uploadTime)
if err == nil {
return nil
}
if !isOrderNotFoundError(err) {
return err
}
fmt.Printf("[虚拟发货] 使用 transaction_id 发货失败,尝试 out_trade_no=%s, 原始错误: %v\n", outTradeNo, err)
}
if outTradeNo == "" {
return fmt.Errorf("transaction_id 和 out_trade_no 均为空")
}
mchID := ""
c := configs.Get()
if c.WechatPay.MchID != "" {
mchID = c.WechatPay.MchID
}
fmt.Printf("[虚拟发货] fallback 使用 out_trade_no=%s mchid=%s\n", outTradeNo, mchID)
err = uploadVirtualShippingInternal(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, uploadTime)
if err == nil {
return nil
}
if !isOrderNotFoundError(err) {
return err
}
fmt.Printf("[虚拟发货] 使用 out_trade_no 发货返回支付单不存在,等待重试 out_trade_no=%s mchid=%s\n", outTradeNo, mchID)
time.Sleep(time.Second)
return uploadVirtualShippingInternal(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, uploadTime)
}
func SetMsgJumpPath(ctx core.Context, config *WechatConfig, path string) error {
if path == "" {
return fmt.Errorf("path 不能为空")
}
accessToken, err := GetAccessToken(ctx, config)
if err != nil {
return err
}
body := map[string]string{"path": path}
client := httpclient.GetHttpClientWithContext(ctx.RequestContext())
resp, err := client.R().
SetQueryParam("access_token", accessToken).
SetHeader("Content-Type", "application/json").
SetBody(body).
Post("https://api.weixin.qq.com/wxa/sec/order/set_msg_jump_path")
if err != nil {
return err
}
var cr commonResp
if err := json.Unmarshal(resp.Body(), &cr); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
if cr.ErrCode != 0 {
return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
}
return nil
}
// UploadVirtualShippingForBackground 后台任务虚拟发货(无 core.Context
// 用于定时开奖等后台任务场景
func UploadVirtualShippingForBackground(ctx context.Context, config *WechatConfig, transactionID string, outTradeNo string, payerOpenid string, itemDesc string) error {
accessToken, err := GetAccessTokenWithContext(ctx, config)
if err != nil {
return err
}
if transactionID != "" {
key := orderKey{OrderNumberType: 2, TransactionID: transactionID}
err = uploadVirtualShippingInternalBackground(ctx, accessToken, key, payerOpenid, itemDesc, time.Now())
if err == nil {
return nil
}
if !isOrderNotFoundError(err) {
return err
}
// 支付单可能尚未同步,等待后重试
fmt.Printf("[虚拟发货-后台] 使用 transaction_id 发货返回支付单不存在,等待重试 transaction_id=%s\n", transactionID)
time.Sleep(2 * time.Second)
err = uploadVirtualShippingInternalBackground(ctx, accessToken, key, payerOpenid, itemDesc, time.Now())
if err == nil {
return nil
}
if !isOrderNotFoundError(err) {
return err
}
if outTradeNo == "" {
return err
}
fmt.Printf("[虚拟发货-后台] 使用 transaction_id 发货失败,尝试 out_trade_no=%s\n", outTradeNo)
}
if outTradeNo == "" {
return fmt.Errorf("transaction_id 和 out_trade_no 均为空")
}
mchID := ""
c := configs.Get()
if c.WechatPay.MchID != "" {
mchID = c.WechatPay.MchID
}
fmt.Printf("[虚拟发货-后台] fallback 使用 out_trade_no=%s mchid=%s\n", outTradeNo, mchID)
err = uploadVirtualShippingInternalBackground(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, time.Now())
if err == nil {
return nil
}
if !isOrderNotFoundError(err) {
return err
}
// 支付单可能尚未同步,等待后重试
fmt.Printf("[虚拟发货-后台] 使用 out_trade_no 发货返回支付单不存在,等待重试 out_trade_no=%s mchid=%s\n", outTradeNo, mchID)
time.Sleep(2 * time.Second)
return uploadVirtualShippingInternalBackground(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, time.Now())
}
// GetOrderShippingStatusResponse 查询订单发货状态响应
type GetOrderShippingStatusResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
Order struct {
OrderState int `json:"order_state"` // 1: 待发货, 2: 已发货, 3: 确认收货, 4: 交易完成, 5: 已退款
} `json:"order"`
}
// GetOrderShippingStatus 查询订单发货状态
// 返回: orderState (1: 待发货, 2: 已发货, 3: 确认收货, 4: 交易完成, 5: 已退款), error
func GetOrderShippingStatus(ctx context.Context, accessToken string, key orderKey) (int, error) {
if accessToken == "" {
return 0, fmt.Errorf("access_token 不能为空")
}
// 文档: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#三、查询订单发货状态
// get_order 接口参数是扁平的,不使用 order_key 结构
reqBody := map[string]any{}
if key.TransactionID != "" {
reqBody["transaction_id"] = key.TransactionID
} else {
reqBody["merchant_id"] = key.MchID
reqBody["merchant_trade_no"] = key.OutTradeNo
}
b, _ := json.Marshal(reqBody)
// fmt.Printf("[虚拟发货-查询] 请求 get_order order_key=%+v\n", key) // Debug log
client := httpclient.GetHttpClient()
resp, err := client.R().
SetQueryParam("access_token", accessToken).
SetHeader("Content-Type", "application/json").
SetBody(b).
Post("https://api.weixin.qq.com/wxa/sec/order/get_order")
if err != nil {
return 0, err
}
var r GetOrderShippingStatusResponse
if err := json.Unmarshal(resp.Body(), &r); err != nil {
return 0, fmt.Errorf("解析响应失败: %v", err)
}
if r.ErrCode != 0 {
// 10060001 = 支付单不存在,视为待发货(或未知的)
if r.ErrCode == 10060001 {
return 0, nil // Not found
}
return 0, fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", r.ErrCode, r.ErrMsg)
}
return r.Order.OrderState, nil
}
// uploadVirtualShippingInternalBackground 后台虚拟发货内部实现(无 core.Context
func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
if accessToken == "" {
return fmt.Errorf("access_token 不能为空")
}
if itemDesc == "" {
return fmt.Errorf("参数缺失")
}
// Step 2: Upload shipping info
reqBody := &uploadShippingInfoRequest{
OrderKey: key,
LogisticsType: 3,
DeliveryMode: 1,
ShippingList: []shippingItem{{ItemDesc: itemDesc}},
UploadTime: uploadTime.Format(time.RFC3339),
}
if payerOpenid != "" {
reqBody.Payer = &payerInfo{Openid: payerOpenid}
}
b, _ := json.Marshal(reqBody)
fmt.Printf("[虚拟发货-后台] 请求 upload_shipping_info order_key=%+v body=%s\n", key, string(b))
client := httpclient.GetHttpClient()
resp, err := client.R().
SetQueryParam("access_token", accessToken).
SetHeader("Content-Type", "application/json").
SetBody(b).
Post("https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info")
if err != nil {
return err
}
var cr commonResp
if err := json.Unmarshal(resp.Body(), &cr); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
if cr.ErrCode != 0 {
// 10060003 = 订单已发货 (Redundant check if status check above passed but state changed or query returned 0)
if cr.ErrCode == 10060003 {
fmt.Printf("[虚拟发货-后台] 微信返回已发货(10060003),视为成功\n")
return nil
}
return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
}
return nil
}