Files
ai_dianshang/server/internal/service/wechat_pay.go
2025-11-17 13:32:54 +08:00

911 lines
30 KiB
Go
Raw Permalink 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 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, &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"`
}