282 lines
9.6 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 {
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
}