345 lines
12 KiB
Go
345 lines
12 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 {
|
||
// 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
|
||
}
|