911 lines
30 KiB
Go
911 lines
30 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"crypto/rsa"
|
|||
|
|
"dianshang/internal/config"
|
|||
|
|
"dianshang/internal/model"
|
|||
|
|
"dianshang/internal/repository"
|
|||
|
|
"dianshang/pkg/logger"
|
|||
|
|
"dianshang/pkg/utils"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"log"
|
|||
|
|
"strconv"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
|||
|
|
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
|||
|
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
|||
|
|
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
|||
|
|
wechatutils "github.com/wechatpay-apiv3/wechatpay-go/utils"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type WeChatPayService struct {
|
|||
|
|
config *config.WeChatPayConfig
|
|||
|
|
client *core.Client
|
|||
|
|
jsapiSvc *jsapi.JsapiApiService
|
|||
|
|
refundSvc *refunddomestic.RefundsApiService
|
|||
|
|
privateKey *rsa.PrivateKey
|
|||
|
|
orderRepo *repository.OrderRepository
|
|||
|
|
refundSvcRef *RefundService
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.OrderRepository, refundService *RefundService) (*WeChatPayService, error) {
|
|||
|
|
// 检查是否为沙盒环境
|
|||
|
|
if cfg.Environment == "sandbox" {
|
|||
|
|
logger.Info("微信支付配置为沙盒模式,将使用模拟支付")
|
|||
|
|
return &WeChatPayService{
|
|||
|
|
config: cfg,
|
|||
|
|
orderRepo: orderRepo,
|
|||
|
|
refundSvcRef: refundService,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生产环境:加载商户私钥
|
|||
|
|
privateKey, err := wechatutils.LoadPrivateKeyWithPath(cfg.KeyPath)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Warn("加载商户私钥失败,将使用模拟模式", "error", err)
|
|||
|
|
// 在开发环境下允许没有私钥,使用模拟模式
|
|||
|
|
return &WeChatPayService{
|
|||
|
|
config: cfg,
|
|||
|
|
orderRepo: orderRepo,
|
|||
|
|
refundSvcRef: refundService,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx := context.Background()
|
|||
|
|
// 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力
|
|||
|
|
opts := []core.ClientOption{
|
|||
|
|
option.WithWechatPayAutoAuthCipher(cfg.MchID, cfg.SerialNo, privateKey, cfg.APIv3Key),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
client, err := core.NewClient(ctx, opts...)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Warn("初始化微信支付客户端失败,将使用模拟模式", "error", err)
|
|||
|
|
// 在开发环境下允许客户端初始化失败,使用模拟模式
|
|||
|
|
return &WeChatPayService{
|
|||
|
|
config: cfg,
|
|||
|
|
orderRepo: orderRepo,
|
|||
|
|
refundSvcRef: refundService,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建JSAPI服务
|
|||
|
|
jsapiSvc := &jsapi.JsapiApiService{Client: client}
|
|||
|
|
|
|||
|
|
// 创建退款服务
|
|||
|
|
refundSvc := &refunddomestic.RefundsApiService{Client: client}
|
|||
|
|
|
|||
|
|
logger.Info("微信支付客户端初始化成功",
|
|||
|
|
"mchId", cfg.MchID,
|
|||
|
|
"serialNo", cfg.SerialNo,
|
|||
|
|
"environment", cfg.Environment)
|
|||
|
|
|
|||
|
|
return &WeChatPayService{
|
|||
|
|
config: cfg,
|
|||
|
|
client: client,
|
|||
|
|
jsapiSvc: jsapiSvc,
|
|||
|
|
refundSvc: refundSvc,
|
|||
|
|
privateKey: privateKey,
|
|||
|
|
orderRepo: orderRepo,
|
|||
|
|
refundSvcRef: refundService,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateOrder 创建支付订单
|
|||
|
|
func (s *WeChatPayService) CreateOrder(ctx context.Context, order *model.Order, openID string) (*WeChatPayResponse, error) {
|
|||
|
|
// 生成唯一的微信支付订单号
|
|||
|
|
wechatOutTradeNo := utils.GenerateWechatOutTradeNo()
|
|||
|
|
|
|||
|
|
logger.Info("开始创建微信支付订单",
|
|||
|
|
"orderNo", order.OrderNo,
|
|||
|
|
"wechatOutTradeNo", wechatOutTradeNo,
|
|||
|
|
"openID", openID,
|
|||
|
|
"totalAmount", order.TotalAmount,
|
|||
|
|
"hasClient", s.client != nil)
|
|||
|
|
|
|||
|
|
// 更新订单的微信支付订单号
|
|||
|
|
err := s.orderRepo.UpdateByOrderNo(order.OrderNo, map[string]interface{}{
|
|||
|
|
"wechat_out_trade_no": wechatOutTradeNo,
|
|||
|
|
"updated_at": time.Now(),
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("更新订单微信支付订单号失败", "error", err, "orderNo", order.OrderNo)
|
|||
|
|
return nil, fmt.Errorf("更新订单失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有客户端(开发环境),使用模拟数据
|
|||
|
|
if s.client == nil {
|
|||
|
|
logger.Warn("开发环境下使用模拟支付数据")
|
|||
|
|
return s.createMockPayment(order, openID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建预支付请求,使用唯一的微信支付订单号
|
|||
|
|
req := jsapi.PrepayRequest{
|
|||
|
|
Appid: core.String(s.config.AppID),
|
|||
|
|
Mchid: core.String(s.config.MchID),
|
|||
|
|
Description: core.String(fmt.Sprintf("订单号: %s", order.OrderNo)),
|
|||
|
|
OutTradeNo: core.String(wechatOutTradeNo), // 使用唯一的微信支付订单号
|
|||
|
|
NotifyUrl: core.String(s.config.NotifyURL),
|
|||
|
|
Amount: &jsapi.Amount{
|
|||
|
|
Total: core.Int64(int64(order.TotalAmount)), // 金额已经是分为单位,无需转换
|
|||
|
|
Currency: core.String("CNY"),
|
|||
|
|
},
|
|||
|
|
Payer: &jsapi.Payer{
|
|||
|
|
Openid: core.String(openID),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用PrepayWithRequestPayment方法,直接获取调起支付的参数
|
|||
|
|
resp, result, err := s.jsapiSvc.PrepayWithRequestPayment(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("call PrepayWithRequestPayment err:%s", err)
|
|||
|
|
logger.Error("创建支付订单失败", "error", err, "orderNo", order.OrderNo)
|
|||
|
|
return nil, fmt.Errorf("创建支付订单失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if result.Response.StatusCode != 200 {
|
|||
|
|
log.Printf("PrepayWithRequestPayment status=%d", result.Response.StatusCode)
|
|||
|
|
return nil, fmt.Errorf("预支付请求失败,状态码: %d", result.Response.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("PrepayWithRequestPayment success, prepay_id=%s", *resp.PrepayId)
|
|||
|
|
logger.Info("微信支付API响应",
|
|||
|
|
"prepayId", *resp.PrepayId,
|
|||
|
|
"orderNo", order.OrderNo)
|
|||
|
|
|
|||
|
|
// 直接使用SDK返回的支付参数
|
|||
|
|
payParams := &MiniProgramPayParams{
|
|||
|
|
AppID: *resp.Appid,
|
|||
|
|
TimeStamp: *resp.TimeStamp,
|
|||
|
|
NonceStr: *resp.NonceStr,
|
|||
|
|
Package: *resp.Package,
|
|||
|
|
SignType: *resp.SignType,
|
|||
|
|
PaySign: *resp.PaySign,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &WeChatPayResponse{
|
|||
|
|
Code: 0,
|
|||
|
|
Message: "success",
|
|||
|
|
Data: map[string]interface{}{
|
|||
|
|
"payInfo": payParams,
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// createMockPayment 创建模拟支付数据(沙盒环境使用)
|
|||
|
|
func (s *WeChatPayService) createMockPayment(order *model.Order, openID string) (*WeChatPayResponse, error) {
|
|||
|
|
mockPrepayID := fmt.Sprintf("wx%d%s", time.Now().Unix(), generateNonceStr()[:8])
|
|||
|
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
|||
|
|
nonceStr := generateNonceStr()
|
|||
|
|
|
|||
|
|
// 生成更真实的模拟签名
|
|||
|
|
mockSign := fmt.Sprintf("sandbox_%s_%s", nonceStr[:16], timestamp)
|
|||
|
|
|
|||
|
|
payParams := &MiniProgramPayParams{
|
|||
|
|
AppID: s.config.AppID,
|
|||
|
|
TimeStamp: timestamp,
|
|||
|
|
NonceStr: nonceStr,
|
|||
|
|
Package: fmt.Sprintf("prepay_id=%s", mockPrepayID),
|
|||
|
|
SignType: "RSA",
|
|||
|
|
PaySign: mockSign,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("生成沙盒支付参数",
|
|||
|
|
"environment", s.config.Environment,
|
|||
|
|
"prepayId", mockPrepayID,
|
|||
|
|
"orderNo", order.OrderNo,
|
|||
|
|
"openID", openID,
|
|||
|
|
"totalAmount", order.TotalAmount,
|
|||
|
|
"description", fmt.Sprintf("订单号: %s", order.OrderNo))
|
|||
|
|
|
|||
|
|
return &WeChatPayResponse{
|
|||
|
|
Code: 0,
|
|||
|
|
Message: "沙盒支付创建成功",
|
|||
|
|
Data: map[string]interface{}{
|
|||
|
|
"payInfo": payParams,
|
|||
|
|
"sandbox": true,
|
|||
|
|
"tips": "这是沙盒环境的模拟支付,可以直接调用成功",
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// QueryOrder 查询订单
|
|||
|
|
func (s *WeChatPayService) QueryOrder(ctx context.Context, orderNo string) (*model.Order, error) {
|
|||
|
|
logger.Info("开始查询订单",
|
|||
|
|
"orderNo", orderNo,
|
|||
|
|
"hasClient", s.client != nil,
|
|||
|
|
"environment", s.config.Environment)
|
|||
|
|
|
|||
|
|
// 如果没有客户端(沙盒环境或开发环境),返回模拟数据
|
|||
|
|
if s.client == nil {
|
|||
|
|
if s.config.Environment == "sandbox" {
|
|||
|
|
logger.Info("沙盒环境下返回模拟查询结果")
|
|||
|
|
} else {
|
|||
|
|
logger.Warn("开发环境下返回模拟查询结果")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟不同的支付状态,让测试更真实
|
|||
|
|
var status int
|
|||
|
|
if time.Now().Unix()%3 == 0 {
|
|||
|
|
status = 1 // 未付款
|
|||
|
|
} else {
|
|||
|
|
status = 2 // 已付款
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &model.Order{
|
|||
|
|
OrderNo: orderNo,
|
|||
|
|
TotalAmount: 100.0, // 模拟金额
|
|||
|
|
Status: status,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 首先从数据库获取订单信息
|
|||
|
|
order, err := s.orderRepo.GetByOrderNo(orderNo)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("从数据库获取订单失败", "error", err, "orderNo", orderNo)
|
|||
|
|
return nil, fmt.Errorf("订单不存在: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有微信支付订单号,说明还没有创建过微信支付订单
|
|||
|
|
if order.WechatOutTradeNo == "" {
|
|||
|
|
logger.Warn("订单尚未创建微信支付订单", "orderNo", orderNo)
|
|||
|
|
return order, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用微信支付订单号查询微信支付状态
|
|||
|
|
req := jsapi.QueryOrderByOutTradeNoRequest{
|
|||
|
|
OutTradeNo: core.String(order.WechatOutTradeNo),
|
|||
|
|
Mchid: core.String(s.config.MchID),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resp, result, err := s.jsapiSvc.QueryOrderByOutTradeNo(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("call QueryOrderByOutTradeNo err:%s", err)
|
|||
|
|
logger.Error("查询微信支付订单失败", "error", err, "wechatOutTradeNo", order.WechatOutTradeNo)
|
|||
|
|
return nil, fmt.Errorf("查询微信支付订单失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if result.Response.StatusCode != 200 {
|
|||
|
|
log.Printf("QueryOrderByOutTradeNo status=%d", result.Response.StatusCode)
|
|||
|
|
return nil, fmt.Errorf("查询微信支付订单失败,状态码: %d", result.Response.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("QueryOrderByOutTradeNo success, resp=%+v", resp)
|
|||
|
|
logger.Info("查询微信支付订单成功",
|
|||
|
|
"orderNo", orderNo,
|
|||
|
|
"wechatOutTradeNo", order.WechatOutTradeNo,
|
|||
|
|
"tradeState", *resp.TradeState)
|
|||
|
|
|
|||
|
|
// 更新订单的微信交易号和支付状态
|
|||
|
|
wechatStatus := convertWeChatPayStatus(*resp.TradeState)
|
|||
|
|
updates := map[string]interface{}{
|
|||
|
|
"updated_at": time.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有微信交易号,保存到数据库
|
|||
|
|
if resp.TransactionId != nil {
|
|||
|
|
updates["wechat_transaction_id"] = *resp.TransactionId
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果微信支付状态是已支付,更新订单状态
|
|||
|
|
if wechatStatus == 2 && order.Status == 1 {
|
|||
|
|
updates["status"] = 2
|
|||
|
|
updates["pay_status"] = 1
|
|||
|
|
updates["paid_at"] = time.Now()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新订单信息
|
|||
|
|
if len(updates) > 1 { // 除了updated_at还有其他字段需要更新
|
|||
|
|
err = s.orderRepo.UpdateByOrderNo(orderNo, updates)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("更新订单微信支付信息失败", "error", err, "orderNo", orderNo)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新订单对象的状态和金额信息
|
|||
|
|
order.TotalAmount = float64(*resp.Amount.Total) // 保持分为单位,与系统内部一致
|
|||
|
|
order.Status = wechatStatus
|
|||
|
|
if resp.TransactionId != nil {
|
|||
|
|
order.WechatTransactionID = *resp.TransactionId
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return order, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HandleNotify 处理支付回调
|
|||
|
|
func (s *WeChatPayService) HandleNotify(ctx context.Context, body []byte, headers map[string]string) (*WeChatPayNotify, error) {
|
|||
|
|
// 解析回调数据
|
|||
|
|
var notify WeChatPayNotify
|
|||
|
|
if err := json.Unmarshal(body, ¬ify); err != nil {
|
|||
|
|
return nil, fmt.Errorf("解析回调数据失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("收到微信支付回调",
|
|||
|
|
"eventType", notify.EventType,
|
|||
|
|
"id", notify.ID,
|
|||
|
|
"algorithm", notify.Resource.Algorithm)
|
|||
|
|
|
|||
|
|
// 解密resource中的数据
|
|||
|
|
if notify.Resource.Ciphertext != "" {
|
|||
|
|
// 使用AEAD_AES_256_GCM算法解密
|
|||
|
|
decryptedData, err := s.decryptNotifyResource(
|
|||
|
|
notify.Resource.Ciphertext,
|
|||
|
|
notify.Resource.Nonce,
|
|||
|
|
notify.Resource.AssociatedData,
|
|||
|
|
)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("解密回调数据失败", "error", err)
|
|||
|
|
return nil, fmt.Errorf("解密回调数据失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析解密后的JSON数据
|
|||
|
|
var paymentData WeChatPayNotifyData
|
|||
|
|
if err := json.Unmarshal(decryptedData, &paymentData); err != nil {
|
|||
|
|
logger.Error("解析解密数据失败", "error", err, "data", string(decryptedData))
|
|||
|
|
return nil, fmt.Errorf("解析解密数据失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
notify.DecryptedData = &paymentData
|
|||
|
|
logger.Info("成功解密回调数据",
|
|||
|
|
"outTradeNo", paymentData.OutTradeNo,
|
|||
|
|
"transactionID", paymentData.TransactionID,
|
|||
|
|
"tradeState", paymentData.TradeState)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ¬ify, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// decryptNotifyResource 解密回调通知中的resource数据
|
|||
|
|
func (s *WeChatPayService) decryptNotifyResource(ciphertext, nonce, associatedData string) ([]byte, error) {
|
|||
|
|
// 使用wechatpay-go SDK提供的解密工具
|
|||
|
|
plaintext, err := wechatutils.DecryptAES256GCM(s.config.APIv3Key, associatedData, nonce, ciphertext)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("AES解密失败: %v", err)
|
|||
|
|
}
|
|||
|
|
return []byte(plaintext), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ProcessPaymentSuccess 处理支付成功回调
|
|||
|
|
func (s *WeChatPayService) ProcessPaymentSuccess(ctx context.Context, notify *WeChatPayNotify) error {
|
|||
|
|
if notify.EventType != "TRANSACTION.SUCCESS" {
|
|||
|
|
return fmt.Errorf("不是支付成功回调: %s", notify.EventType)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("开始处理支付成功回调", "eventType", notify.EventType)
|
|||
|
|
|
|||
|
|
// 解析回调数据中的订单信息
|
|||
|
|
var orderNo string
|
|||
|
|
var transactionID string
|
|||
|
|
|
|||
|
|
// 如果有解密数据,从中获取订单号
|
|||
|
|
if notify.DecryptedData != nil {
|
|||
|
|
orderNo = notify.DecryptedData.OutTradeNo
|
|||
|
|
transactionID = notify.DecryptedData.TransactionID
|
|||
|
|
logger.Info("从解密数据中获取订单信息", "orderNo", orderNo, "transactionID", transactionID)
|
|||
|
|
} else {
|
|||
|
|
// 开发环境下,可能需要从Resource字段中解析
|
|||
|
|
// 或者从其他地方获取订单号
|
|||
|
|
logger.Warn("回调数据中没有解密数据,尝试从Resource字段获取")
|
|||
|
|
|
|||
|
|
// 在开发环境下,我们可以尝试解析Resource中的数据
|
|||
|
|
if notify.Resource.Ciphertext != "" {
|
|||
|
|
// 这里可以添加解密逻辑,但在开发环境下我们先跳过
|
|||
|
|
logger.Info("Resource中有加密数据,但开发环境暂不解密")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果无法获取订单号,我们可以从最近的订单中查找
|
|||
|
|
// 这是一个临时的开发环境解决方案
|
|||
|
|
logger.Warn("无法从回调数据中获取订单号,这可能是开发环境的模拟回调")
|
|||
|
|
return fmt.Errorf("无法从回调数据中获取订单号")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if orderNo == "" {
|
|||
|
|
return fmt.Errorf("回调数据中缺少订单号")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("处理支付成功回调", "orderNo", orderNo)
|
|||
|
|
|
|||
|
|
// 查询订单
|
|||
|
|
order, err := s.orderRepo.GetOrderByWechatOutTradeNo(orderNo)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("根据微信订单号查询订单失败", "error", err, "wechatOutTradeNo", orderNo)
|
|||
|
|
return fmt.Errorf("订单不存在: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查订单状态,避免重复处理
|
|||
|
|
if order.Status >= 2 {
|
|||
|
|
logger.Info("订单已经是已支付状态,跳过处理", "orderNo", order.OrderNo, "status", order.Status)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("开始更新订单状态", "orderNo", order.OrderNo, "currentStatus", order.Status)
|
|||
|
|
|
|||
|
|
// 更新订单状态为已支付
|
|||
|
|
updates := map[string]interface{}{
|
|||
|
|
"status": 2, // 已支付
|
|||
|
|
"pay_status": 1, // 已支付
|
|||
|
|
"pay_time": time.Now(),
|
|||
|
|
"updated_at": time.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有微信交易号,也保存
|
|||
|
|
if transactionID != "" {
|
|||
|
|
updates["wechat_transaction_id"] = transactionID
|
|||
|
|
logger.Info("保存微信交易号", "transactionID", transactionID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err = s.orderRepo.UpdateByOrderNo(order.OrderNo, updates)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("更新订单支付状态失败", "error", err, "orderNo", order.OrderNo)
|
|||
|
|
return fmt.Errorf("更新订单状态失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("订单支付状态更新成功", "orderNo", order.OrderNo, "newStatus", 2)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ProcessPaymentSuccessByOrderNo 根据订单号手动处理支付成功(用于测试)
|
|||
|
|
func (s *WeChatPayService) ProcessPaymentSuccessByOrderNo(ctx context.Context, orderNo string) error {
|
|||
|
|
logger.Info("手动处理支付成功", "orderNo", orderNo)
|
|||
|
|
|
|||
|
|
// 查询订单
|
|||
|
|
order, err := s.orderRepo.GetByOrderNo(orderNo)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("查询订单失败", "error", err, "orderNo", orderNo)
|
|||
|
|
return fmt.Errorf("订单不存在: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查订单状态,避免重复处理
|
|||
|
|
if order.Status >= 2 {
|
|||
|
|
logger.Info("订单已经是已支付状态,跳过处理", "orderNo", order.OrderNo, "status", order.Status)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("开始更新订单状态", "orderNo", order.OrderNo, "currentStatus", order.Status)
|
|||
|
|
|
|||
|
|
// 更新订单状态为已支付
|
|||
|
|
updates := map[string]interface{}{
|
|||
|
|
"status": 2, // 已支付
|
|||
|
|
"pay_status": 1, // 已支付
|
|||
|
|
"pay_time": time.Now(),
|
|||
|
|
"updated_at": time.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err = s.orderRepo.UpdateByOrderNo(order.OrderNo, updates)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("更新订单支付状态失败", "error", err, "orderNo", order.OrderNo)
|
|||
|
|
return fmt.Errorf("更新订单状态失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("订单支付状态更新成功", "orderNo", order.OrderNo, "newStatus", 2)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// CreateRefund 创建微信退款
|
|||
|
|
func (s *WeChatPayService) CreateRefund(ctx context.Context, refundRecord *model.Refund, order *model.Order) (*WeChatRefundResponse, error) {
|
|||
|
|
logger.Info("开始创建微信退款",
|
|||
|
|
"refundNo", refundRecord.RefundNo,
|
|||
|
|
"orderNo", order.OrderNo,
|
|||
|
|
"refundAmount", refundRecord.RefundAmount,
|
|||
|
|
"hasClient", s.client != nil)
|
|||
|
|
|
|||
|
|
// 如果没有客户端(开发环境),使用模拟数据
|
|||
|
|
if s.client == nil {
|
|||
|
|
logger.Warn("开发环境下使用模拟退款数据")
|
|||
|
|
return s.createMockRefund(refundRecord, order)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建退款请求
|
|||
|
|
req := refunddomestic.CreateRequest{
|
|||
|
|
OutTradeNo: core.String(order.WechatOutTradeNo),
|
|||
|
|
OutRefundNo: core.String(refundRecord.WechatOutRefundNo),
|
|||
|
|
Reason: core.String(refundRecord.RefundReason),
|
|||
|
|
FundsAccount: (*refunddomestic.ReqFundsAccount)(core.String("AVAILABLE")), // 可用余额退款
|
|||
|
|
Amount: &refunddomestic.AmountReq{
|
|||
|
|
Refund: core.Int64(int64(refundRecord.RefundAmount)),
|
|||
|
|
Total: core.Int64(int64(order.TotalAmount)),
|
|||
|
|
Currency: core.String("CNY"),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只有当RefundNotifyURL不为空时才设置NotifyUrl
|
|||
|
|
if s.config.RefundNotifyURL != "" {
|
|||
|
|
req.NotifyUrl = core.String(s.config.RefundNotifyURL)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有微信交易号,优先使用
|
|||
|
|
if order.WechatTransactionID != "" {
|
|||
|
|
req.TransactionId = core.String(order.WechatTransactionID)
|
|||
|
|
req.OutTradeNo = nil // 使用微信交易号时,不需要商户订单号
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用微信退款API
|
|||
|
|
resp, result, err := s.refundSvc.Create(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("call CreateRefund err:%s", err)
|
|||
|
|
logger.Error("创建微信退款失败", "error", err, "refundNo", refundRecord.RefundNo)
|
|||
|
|
return nil, fmt.Errorf("创建微信退款失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if result.Response.StatusCode != 200 {
|
|||
|
|
log.Printf("CreateRefund status=%d", result.Response.StatusCode)
|
|||
|
|
return nil, fmt.Errorf("微信退款请求失败,状态码: %d", result.Response.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("CreateRefund success, refund_id=%s", *resp.RefundId)
|
|||
|
|
logger.Info("微信退款API响应",
|
|||
|
|
"refundId", *resp.RefundId,
|
|||
|
|
"refundNo", refundRecord.RefundNo,
|
|||
|
|
"status", *resp.Status)
|
|||
|
|
|
|||
|
|
return &WeChatRefundResponse{
|
|||
|
|
Code: 0,
|
|||
|
|
Message: "success",
|
|||
|
|
Data: map[string]interface{}{
|
|||
|
|
"refund_id": *resp.RefundId,
|
|||
|
|
"out_refund_no": *resp.OutRefundNo,
|
|||
|
|
"transaction_id": getStringValue(resp.TransactionId),
|
|||
|
|
"out_trade_no": getStringValue(resp.OutTradeNo),
|
|||
|
|
"channel": getChannelValue(resp.Channel),
|
|||
|
|
"user_received_account": getStringValue(resp.UserReceivedAccount),
|
|||
|
|
"success_time": getTimeValue(resp.SuccessTime),
|
|||
|
|
"create_time": getTimeValue(resp.CreateTime),
|
|||
|
|
"status": getStatusValue(*resp.Status),
|
|||
|
|
"funds_account": getFundsAccountValue(resp.FundsAccount),
|
|||
|
|
"amount": map[string]interface{}{
|
|||
|
|
"total": *resp.Amount.Total,
|
|||
|
|
"refund": *resp.Amount.Refund,
|
|||
|
|
"payer_total": getInt64Value(resp.Amount.PayerTotal),
|
|||
|
|
"payer_refund": getInt64Value(resp.Amount.PayerRefund),
|
|||
|
|
"settlement_refund": getInt64Value(resp.Amount.SettlementRefund),
|
|||
|
|
"settlement_total": getInt64Value(resp.Amount.SettlementTotal),
|
|||
|
|
"discount_refund": getInt64Value(resp.Amount.DiscountRefund),
|
|||
|
|
"currency": *resp.Amount.Currency,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// createMockRefund 创建模拟退款数据(开发环境使用)
|
|||
|
|
func (s *WeChatPayService) createMockRefund(refundRecord *model.Refund, order *model.Order) (*WeChatRefundResponse, error) {
|
|||
|
|
logger.Info("创建模拟退款数据", "refundNo", refundRecord.RefundNo)
|
|||
|
|
|
|||
|
|
// 生成模拟的微信退款ID
|
|||
|
|
mockRefundID := fmt.Sprintf("mock_refund_%d", time.Now().Unix())
|
|||
|
|
|
|||
|
|
return &WeChatRefundResponse{
|
|||
|
|
Code: 0,
|
|||
|
|
Message: "success",
|
|||
|
|
Data: map[string]interface{}{
|
|||
|
|
"refund_id": mockRefundID,
|
|||
|
|
"out_refund_no": refundRecord.WechatOutRefundNo,
|
|||
|
|
"transaction_id": order.WechatTransactionID,
|
|||
|
|
"out_trade_no": order.WechatOutTradeNo,
|
|||
|
|
"channel": "ORIGINAL",
|
|||
|
|
"user_received_account": "招商银行信用卡0403",
|
|||
|
|
"success_time": time.Now().Format("2006-01-02T15:04:05+08:00"),
|
|||
|
|
"create_time": time.Now().Format("2006-01-02T15:04:05+08:00"),
|
|||
|
|
"status": "SUCCESS",
|
|||
|
|
"funds_account": "AVAILABLE",
|
|||
|
|
"amount": map[string]interface{}{
|
|||
|
|
"total": int64(order.TotalAmount),
|
|||
|
|
"refund": int64(refundRecord.RefundAmount),
|
|||
|
|
"payer_total": int64(order.TotalAmount),
|
|||
|
|
"payer_refund": int64(refundRecord.RefundAmount),
|
|||
|
|
"settlement_refund": int64(refundRecord.RefundAmount),
|
|||
|
|
"settlement_total": int64(order.TotalAmount),
|
|||
|
|
"discount_refund": int64(0),
|
|||
|
|
"currency": "CNY",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// QueryRefund 查询微信退款状态
|
|||
|
|
func (s *WeChatPayService) QueryRefund(ctx context.Context, outRefundNo string) (*model.Refund, error) {
|
|||
|
|
logger.Info("查询微信退款状态", "outRefundNo", outRefundNo)
|
|||
|
|
|
|||
|
|
// 如果没有客户端(开发环境),返回模拟数据
|
|||
|
|
if s.client == nil {
|
|||
|
|
logger.Warn("开发环境下使用模拟退款查询")
|
|||
|
|
return s.queryMockRefund(outRefundNo)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建查询请求
|
|||
|
|
req := refunddomestic.QueryByOutRefundNoRequest{
|
|||
|
|
OutRefundNo: core.String(outRefundNo),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用微信查询退款API
|
|||
|
|
resp, result, err := s.refundSvc.QueryByOutRefundNo(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("call QueryRefund err:%s", err)
|
|||
|
|
logger.Error("查询微信退款失败", "error", err, "outRefundNo", outRefundNo)
|
|||
|
|
return nil, fmt.Errorf("查询微信退款失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if result.Response.StatusCode != 200 {
|
|||
|
|
log.Printf("QueryRefund status=%d", result.Response.StatusCode)
|
|||
|
|
return nil, fmt.Errorf("查询微信退款失败,状态码: %d", result.Response.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("QueryRefund success, resp=%+v", resp)
|
|||
|
|
logger.Info("查询微信退款成功",
|
|||
|
|
"outRefundNo", outRefundNo,
|
|||
|
|
"refundId", *resp.RefundId,
|
|||
|
|
"status", *resp.Status)
|
|||
|
|
|
|||
|
|
// 构建返回的退款记录(这里只是示例,实际应该从数据库获取完整记录)
|
|||
|
|
refundRecord := &model.Refund{
|
|||
|
|
WechatRefundID: *resp.RefundId,
|
|||
|
|
WechatOutRefundNo: *resp.OutRefundNo,
|
|||
|
|
WechatRefundStatus: getStatusValue(*resp.Status),
|
|||
|
|
WechatUserReceivedAccount: getStringValue(resp.UserReceivedAccount),
|
|||
|
|
WechatRefundAccount: getFundsAccountValue(resp.FundsAccount),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果退款成功,设置成功时间
|
|||
|
|
if getStatusValue(*resp.Status) == "SUCCESS" && resp.SuccessTime != nil {
|
|||
|
|
successTime, err := time.Parse("2006-01-02T15:04:05+08:00", getTimeValue(resp.SuccessTime))
|
|||
|
|
if err == nil {
|
|||
|
|
refundRecord.WechatSuccessTime = &successTime
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return refundRecord, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// queryMockRefund 查询模拟退款数据(开发环境使用)
|
|||
|
|
func (s *WeChatPayService) queryMockRefund(outRefundNo string) (*model.Refund, error) {
|
|||
|
|
logger.Info("查询模拟退款数据", "outRefundNo", outRefundNo)
|
|||
|
|
|
|||
|
|
now := time.Now()
|
|||
|
|
return &model.Refund{
|
|||
|
|
WechatRefundID: fmt.Sprintf("mock_refund_%d", now.Unix()),
|
|||
|
|
WechatOutRefundNo: outRefundNo,
|
|||
|
|
WechatRefundStatus: "SUCCESS",
|
|||
|
|
WechatUserReceivedAccount: "招商银行信用卡0403",
|
|||
|
|
WechatRefundAccount: "AVAILABLE",
|
|||
|
|
WechatSuccessTime: &now,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HandleRefundNotify 处理微信退款回调
|
|||
|
|
func (s *WeChatPayService) HandleRefundNotify(ctx context.Context, body []byte, headers map[string]string) (*WeChatRefundNotify, error) {
|
|||
|
|
// 解析回调数据
|
|||
|
|
var notify WeChatRefundNotify
|
|||
|
|
if err := json.Unmarshal(body, ¬ify); err != nil {
|
|||
|
|
return nil, fmt.Errorf("解析退款回调数据失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("收到微信退款回调",
|
|||
|
|
"eventType", notify.EventType,
|
|||
|
|
"id", notify.ID,
|
|||
|
|
"algorithm", notify.Resource.Algorithm)
|
|||
|
|
|
|||
|
|
// 解密resource中的数据
|
|||
|
|
if notify.Resource.Ciphertext != "" {
|
|||
|
|
// 使用AEAD_AES_256_GCM算法解密
|
|||
|
|
decryptedData, err := s.decryptNotifyResource(
|
|||
|
|
notify.Resource.Ciphertext,
|
|||
|
|
notify.Resource.Nonce,
|
|||
|
|
notify.Resource.AssociatedData,
|
|||
|
|
)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("解密退款回调数据失败", "error", err)
|
|||
|
|
return nil, fmt.Errorf("解密退款回调数据失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析解密后的JSON数据
|
|||
|
|
var refundData WeChatRefundNotifyData
|
|||
|
|
if err := json.Unmarshal(decryptedData, &refundData); err != nil {
|
|||
|
|
logger.Error("解析解密数据失败", "error", err, "data", string(decryptedData))
|
|||
|
|
return nil, fmt.Errorf("解析解密数据失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
notify.DecryptedData = &refundData
|
|||
|
|
logger.Info("成功解密退款回调数据",
|
|||
|
|
"outRefundNo", refundData.OutRefundNo,
|
|||
|
|
"refundId", refundData.RefundId,
|
|||
|
|
"refundStatus", refundData.RefundStatus)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ¬ify, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ProcessRefundSuccess 处理退款成功回调
|
|||
|
|
func (s *WeChatPayService) ProcessRefundSuccess(ctx context.Context, notify *WeChatRefundNotify) error {
|
|||
|
|
if notify.EventType != "REFUND.SUCCESS" {
|
|||
|
|
return fmt.Errorf("不是退款成功回调: %s", notify.EventType)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("开始处理退款成功回调", "eventType", notify.EventType)
|
|||
|
|
|
|||
|
|
// 解析回调数据中的退款信息
|
|||
|
|
var outRefundNo string
|
|||
|
|
var refundID string
|
|||
|
|
|
|||
|
|
// 如果有解密数据,从中获取退款单号
|
|||
|
|
if notify.DecryptedData != nil {
|
|||
|
|
outRefundNo = notify.DecryptedData.OutRefundNo
|
|||
|
|
refundID = notify.DecryptedData.RefundId
|
|||
|
|
logger.Info("从解密数据中获取退款信息", "outRefundNo", outRefundNo, "refundId", refundID)
|
|||
|
|
} else {
|
|||
|
|
logger.Warn("退款回调数据中没有解密数据")
|
|||
|
|
return fmt.Errorf("无法从回调数据中获取退款单号")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if outRefundNo == "" {
|
|||
|
|
return fmt.Errorf("回调数据中缺少退款单号")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("处理退款成功回调", "outRefundNo", outRefundNo)
|
|||
|
|
|
|||
|
|
// 这里应该调用退款服务来更新退款状态
|
|||
|
|
// 由于这是在微信支付服务中,我们只记录日志,实际更新由退款服务处理
|
|||
|
|
logger.Info("退款成功回调处理完成", "outRefundNo", outRefundNo, "refundId", refundID)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 辅助函数
|
|||
|
|
func getStringValue(ptr *string) string {
|
|||
|
|
if ptr == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return *ptr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getInt64Value(ptr *int64) int64 {
|
|||
|
|
if ptr == nil {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return *ptr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getChannelValue(ptr *refunddomestic.Channel) string {
|
|||
|
|
if ptr == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return string(*ptr)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getFundsAccountValue(ptr *refunddomestic.FundsAccount) string {
|
|||
|
|
if ptr == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return string(*ptr)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getTimeValue(ptr *time.Time) string {
|
|||
|
|
if ptr == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return ptr.Format("2006-01-02T15:04:05+08:00")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getStatusValue(status refunddomestic.Status) string {
|
|||
|
|
return string(status)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// generateNonceStr 生成随机字符串用于微信支付
|
|||
|
|
func generateNonceStr() string {
|
|||
|
|
return utils.GenerateRandomString(32)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// convertWeChatPayStatus 将微信支付状态转换为订单状态
|
|||
|
|
func convertWeChatPayStatus(wechatStatus string) int {
|
|||
|
|
switch wechatStatus {
|
|||
|
|
case "SUCCESS":
|
|||
|
|
return model.OrderStatusPaid
|
|||
|
|
case "REFUND":
|
|||
|
|
return model.OrderStatusRefunded
|
|||
|
|
case "NOTPAY":
|
|||
|
|
return model.OrderStatusPending
|
|||
|
|
case "CLOSED":
|
|||
|
|
return model.OrderStatusCancelled
|
|||
|
|
case "REVOKED":
|
|||
|
|
return model.OrderStatusCancelled
|
|||
|
|
case "USERPAYING":
|
|||
|
|
return model.OrderStatusPending
|
|||
|
|
case "PAYERROR":
|
|||
|
|
return model.OrderStatusCancelled
|
|||
|
|
default:
|
|||
|
|
return model.OrderStatusPending
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 微信支付相关数据结构
|
|||
|
|
type MiniProgramPayParams struct {
|
|||
|
|
AppID string `json:"appId"`
|
|||
|
|
TimeStamp string `json:"timeStamp"`
|
|||
|
|
NonceStr string `json:"nonceStr"`
|
|||
|
|
Package string `json:"package"`
|
|||
|
|
SignType string `json:"signType"`
|
|||
|
|
PaySign string `json:"paySign"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WeChatPayResponse struct {
|
|||
|
|
Code int `json:"code"`
|
|||
|
|
Message string `json:"message"`
|
|||
|
|
Data map[string]interface{} `json:"data"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WeChatPayNotify struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
CreateTime string `json:"create_time"`
|
|||
|
|
ResourceType string `json:"resource_type"`
|
|||
|
|
EventType string `json:"event_type"`
|
|||
|
|
Summary string `json:"summary"`
|
|||
|
|
Resource struct {
|
|||
|
|
OriginalType string `json:"original_type"`
|
|||
|
|
Algorithm string `json:"algorithm"`
|
|||
|
|
Ciphertext string `json:"ciphertext"`
|
|||
|
|
AssociatedData string `json:"associated_data"`
|
|||
|
|
Nonce string `json:"nonce"`
|
|||
|
|
} `json:"resource"`
|
|||
|
|
DecryptedData *WeChatPayNotifyData `json:"decrypted_data,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WeChatPayNotifyData struct {
|
|||
|
|
MchID string `json:"mchid"`
|
|||
|
|
AppID string `json:"appid"`
|
|||
|
|
OutTradeNo string `json:"out_trade_no"`
|
|||
|
|
TransactionID string `json:"transaction_id"`
|
|||
|
|
TradeType string `json:"trade_type"`
|
|||
|
|
TradeState string `json:"trade_state"`
|
|||
|
|
BankType string `json:"bank_type"`
|
|||
|
|
SuccessTime string `json:"success_time"`
|
|||
|
|
Payer struct {
|
|||
|
|
OpenID string `json:"openid"`
|
|||
|
|
} `json:"payer"`
|
|||
|
|
Amount struct {
|
|||
|
|
Total int `json:"total"`
|
|||
|
|
PayerTotal int `json:"payer_total"`
|
|||
|
|
Currency string `json:"currency"`
|
|||
|
|
PayerCurrency string `json:"payer_currency"`
|
|||
|
|
} `json:"amount"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WeChatRefundResponse struct {
|
|||
|
|
Code int `json:"code"`
|
|||
|
|
Message string `json:"message"`
|
|||
|
|
Data map[string]interface{} `json:"data"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WeChatRefundNotify struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
CreateTime string `json:"create_time"`
|
|||
|
|
ResourceType string `json:"resource_type"`
|
|||
|
|
EventType string `json:"event_type"`
|
|||
|
|
Summary string `json:"summary"`
|
|||
|
|
Resource struct {
|
|||
|
|
OriginalType string `json:"original_type"`
|
|||
|
|
Algorithm string `json:"algorithm"`
|
|||
|
|
Ciphertext string `json:"ciphertext"`
|
|||
|
|
AssociatedData string `json:"associated_data"`
|
|||
|
|
Nonce string `json:"nonce"`
|
|||
|
|
} `json:"resource"`
|
|||
|
|
DecryptedData *WeChatRefundNotifyData `json:"decrypted_data,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WeChatRefundNotifyData struct {
|
|||
|
|
MchID string `json:"mchid"`
|
|||
|
|
OutTradeNo string `json:"out_trade_no"`
|
|||
|
|
TransactionID string `json:"transaction_id"`
|
|||
|
|
OutRefundNo string `json:"out_refund_no"`
|
|||
|
|
RefundId string `json:"refund_id"`
|
|||
|
|
RefundStatus string `json:"refund_status"`
|
|||
|
|
SuccessTime string `json:"success_time"`
|
|||
|
|
UserReceivedAccount string `json:"user_received_account"`
|
|||
|
|
Amount struct {
|
|||
|
|
Total int `json:"total"`
|
|||
|
|
Refund int `json:"refund"`
|
|||
|
|
PayerTotal int `json:"payer_total"`
|
|||
|
|
PayerRefund int `json:"payer_refund"`
|
|||
|
|
} `json:"amount"`
|
|||
|
|
}
|