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