Files
ai_dianshang/server/internal/service/wechat_pay.go

1010 lines
33 KiB
Go
Raw Normal View History

2025-11-17 14:11:46 +08:00
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"
2025-11-28 15:18:10 +08:00
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
2025-11-17 14:11:46 +08:00
"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
2025-11-28 15:18:10 +08:00
nativeSvc *native.NativeApiService
2025-11-17 14:11:46 +08:00
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}
2025-11-28 15:18:10 +08:00
// 创建Native扫码支付服务
nativeSvc := &native.NativeApiService{Client: client}
2025-11-17 14:11:46 +08:00
// 创建退款服务
refundSvc := &refunddomestic.RefundsApiService{Client: client}
logger.Info("微信支付客户端初始化成功",
"mchId", cfg.MchID,
"serialNo", cfg.SerialNo,
"environment", cfg.Environment)
return &WeChatPayService{
config: cfg,
client: client,
jsapiSvc: jsapiSvc,
2025-11-28 15:18:10 +08:00
nativeSvc: nativeSvc,
2025-11-17 14:11:46 +08:00
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
}
2025-11-28 15:18:10 +08:00
// CreateNativeOrder 创建Native扫码支付订单用于PC端
func (s *WeChatPayService) CreateNativeOrder(ctx context.Context, order *model.Order) (*WeChatPayResponse, error) {
// 生成唯一的微信支付订单号
wechatOutTradeNo := utils.GenerateWechatOutTradeNo()
logger.Info("开始创建Native扫码支付订单",
"orderNo", order.OrderNo,
"wechatOutTradeNo", wechatOutTradeNo,
"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("开发环境下使用模拟Native支付数据")
return s.createMockNativePayment(order)
}
// 构建Native预支付请求
req := native.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: &native.Amount{
Total: core.Int64(int64(order.TotalAmount)),
Currency: core.String("CNY"),
},
}
// 调用Native预支付API
resp, result, err := s.nativeSvc.Prepay(ctx, req)
if err != nil {
log.Printf("call Native Prepay err:%s", err)
logger.Error("创建Native支付订单失败", "error", err, "orderNo", order.OrderNo)
return nil, fmt.Errorf("创建Native支付订单失败: %v", err)
}
if result.Response.StatusCode != 200 {
log.Printf("Native Prepay status=%d", result.Response.StatusCode)
return nil, fmt.Errorf("Native预支付请求失败状态码: %d", result.Response.StatusCode)
}
log.Printf("Native Prepay success, code_url=%s", *resp.CodeUrl)
logger.Info("微信Native支付API响应",
"codeUrl", *resp.CodeUrl,
"orderNo", order.OrderNo)
return &WeChatPayResponse{
Code: 0,
Message: "success",
Data: map[string]interface{}{
"qrcode_url": *resp.CodeUrl,
"order_no": order.OrderNo,
"amount": order.TotalAmount,
},
}, nil
}
// createMockNativePayment 创建模拟Native支付数据开发环境使用
func (s *WeChatPayService) createMockNativePayment(order *model.Order) (*WeChatPayResponse, error) {
// 生成模拟的二维码URL
mockQRCodeURL := fmt.Sprintf("weixin://wxpay/bizpayurl?pr=mock_%s", utils.GenerateRandomString(10))
logger.Info("生成模拟Native支付参数",
"environment", s.config.Environment,
"qrcodeUrl", mockQRCodeURL,
"orderNo", order.OrderNo,
"totalAmount", order.TotalAmount)
return &WeChatPayResponse{
Code: 0,
Message: "模拟支付创建成功",
Data: map[string]interface{}{
"qrcode_url": mockQRCodeURL,
"order_no": order.OrderNo,
"amount": order.TotalAmount,
"sandbox": true,
"tips": "这是模拟环境的Native支付请使用测试接口模拟支付成功",
},
}, nil
}
2025-11-17 14:11:46 +08:00
// 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, &notify); 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 &notify, 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, &notify); 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 &notify, 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"`
}