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 { 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()) } // 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("参数缺失") } 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 { return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg) } return nil }