Files
tyapi-server/internal/shared/payment/wechatpay.go
2025-12-12 15:27:15 +08:00

354 lines
12 KiB
Go
Raw 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 payment
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"tyapi-server/internal/config"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
"go.uber.org/zap"
)
const (
TradeStateSuccess = "SUCCESS" // 支付成功
TradeStateRefund = "REFUND" // 转入退款
TradeStateNotPay = "NOTPAY" // 未支付
TradeStateClosed = "CLOSED" // 已关闭
TradeStateRevoked = "REVOKED" // 已撤销(付款码支付)
TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付)
TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败)
)
// resolveCertPath 解析证书文件路径,支持相对路径和绝对路径
// 如果是相对路径,会从多个候选位置查找文件
func resolveCertPath(relativePath string, logger *zap.Logger) (string, error) {
if relativePath == "" {
return "", fmt.Errorf("证书路径为空")
}
// 如果已经是绝对路径,直接返回
if filepath.IsAbs(relativePath) {
if _, err := os.Stat(relativePath); err == nil {
return relativePath, nil
}
return "", fmt.Errorf("证书文件不存在: %s", relativePath)
}
// 候选路径列表(按优先级排序)
var candidatePaths []string
// 优先级1: 从可执行文件所在目录查找(生产环境)
if execPath, err := os.Executable(); err == nil {
execDir := filepath.Dir(execPath)
// 处理符号链接
if realPath, err := filepath.EvalSymlinks(execPath); err == nil {
execDir = filepath.Dir(realPath)
}
candidatePaths = append(candidatePaths, filepath.Join(execDir, relativePath))
}
// 优先级2: 从工作目录查找(开发环境)
if workDir, err := os.Getwd(); err == nil {
candidatePaths = append(candidatePaths,
filepath.Join(workDir, relativePath),
filepath.Join(workDir, "tyapi-server", relativePath),
)
}
// 尝试每个候选路径
for _, candidatePath := range candidatePaths {
absPath, err := filepath.Abs(candidatePath)
if err != nil {
continue
}
if logger != nil {
logger.Debug("尝试查找证书文件", zap.String("path", absPath))
}
// 检查文件是否存在
if info, err := os.Stat(absPath); err == nil && !info.IsDir() {
if logger != nil {
logger.Info("找到证书文件", zap.String("path", absPath))
}
return absPath, nil
}
}
// 所有候选路径都不存在,返回错误
return "", fmt.Errorf("证书文件不存在,已尝试的路径: %v", candidatePaths)
}
// InitType 初始化类型
type InitType string
const (
InitTypePlatformCert InitType = "platform_cert" // 平台证书初始化
InitTypeWxPayPubKey InitType = "wxpay_pubkey" // 微信支付公钥初始化
)
type WechatPayService struct {
config config.Config
wechatClient *core.Client
notifyHandler *notify.Handler
logger *zap.Logger
}
// NewWechatPayService 创建微信支付服务实例
func NewWechatPayService(c config.Config, initType InitType, logger *zap.Logger) *WechatPayService {
switch initType {
case InitTypePlatformCert:
return newWechatPayServiceWithPlatformCert(c, logger)
case InitTypeWxPayPubKey:
return newWechatPayServiceWithWxPayPubKey(c, logger)
default:
logger.Error("不支持的初始化类型", zap.String("init_type", string(initType)))
panic(fmt.Sprintf("初始化失败,服务停止: %s", initType))
}
}
// newWechatPayServiceWithPlatformCert 使用平台证书初始化微信支付服务
func newWechatPayServiceWithPlatformCert(c config.Config, logger *zap.Logger) *WechatPayService {
// 从配置中加载商户信息
mchID := c.Wxpay.MchID
mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber
mchAPIv3Key := c.Wxpay.MchApiv3Key
// 解析证书路径
privateKeyPath, err := resolveCertPath(c.Wxpay.MchPrivateKeyPath, logger)
if err != nil {
logger.Error("解析商户私钥路径失败", zap.Error(err), zap.String("path", c.Wxpay.MchPrivateKeyPath))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 从文件中加载商户私钥
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath)
if err != nil {
logger.Error("加载商户私钥失败", zap.Error(err), zap.String("path", privateKeyPath))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 使用商户私钥和其他参数初始化微信支付客户端
opts := []core.ClientOption{
option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key),
}
client, err := core.NewClient(context.Background(), opts...)
if err != nil {
logger.Error("创建微信支付客户端失败", zap.Error(err))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 在初始化时获取证书访问器并创建 notifyHandler
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID)
notifyHandler, err := notify.NewRSANotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
if err != nil {
logger.Error("获取证书访问器失败", zap.Error(err))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
logger.Info("微信支付客户端初始化成功(平台证书方式)")
return &WechatPayService{
config: c,
wechatClient: client,
notifyHandler: notifyHandler,
logger: logger,
}
}
// newWechatPayServiceWithWxPayPubKey 使用微信支付公钥初始化微信支付服务
func newWechatPayServiceWithWxPayPubKey(c config.Config, logger *zap.Logger) *WechatPayService {
// 从配置中加载商户信息
mchID := c.Wxpay.MchID
mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber
mchAPIv3Key := c.Wxpay.MchApiv3Key
mchPublicKeyID := c.Wxpay.MchPublicKeyID
// 解析证书路径
privateKeyPath, err := resolveCertPath(c.Wxpay.MchPrivateKeyPath, logger)
if err != nil {
logger.Error("解析商户私钥路径失败", zap.Error(err), zap.String("path", c.Wxpay.MchPrivateKeyPath))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
publicKeyPath, err := resolveCertPath(c.Wxpay.MchPublicKeyPath, logger)
if err != nil {
logger.Error("解析微信支付平台证书路径失败", zap.Error(err), zap.String("path", c.Wxpay.MchPublicKeyPath))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 从文件中加载商户私钥
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath)
if err != nil {
logger.Error("加载商户私钥失败", zap.Error(err), zap.String("path", privateKeyPath))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 从文件中加载微信支付平台证书
mchPublicKey, err := utils.LoadPublicKeyWithPath(publicKeyPath)
if err != nil {
logger.Error("加载微信支付平台证书失败", zap.Error(err), zap.String("path", publicKeyPath))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 使用商户私钥和其他参数初始化微信支付客户端
opts := []core.ClientOption{
option.WithWechatPayPublicKeyAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchPublicKeyID, mchPublicKey),
}
client, err := core.NewClient(context.Background(), opts...)
if err != nil {
logger.Error("创建微信支付客户端失败", zap.Error(err))
panic(fmt.Sprintf("初始化失败,服务停止: %v", err))
}
// 初始化 notify.Handler
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID)
notifyHandler := notify.NewNotifyHandler(
mchAPIv3Key,
verifiers.NewSHA256WithRSACombinedVerifier(certificateVisitor, mchPublicKeyID, *mchPublicKey))
logger.Info("微信支付客户端初始化成功(微信支付公钥方式)")
return &WechatPayService{
config: c,
wechatClient: client,
notifyHandler: notifyHandler,
logger: logger,
}
}
// CreateWechatNativeOrder 创建微信Native扫码支付订单
func (w *WechatPayService) CreateWechatNativeOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) {
totalAmount := ToWechatAmount(amount)
req := native.PrepayRequest{
Appid: core.String(w.config.Wxpay.AppID),
Mchid: core.String(w.config.Wxpay.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(w.config.Wxpay.NotifyUrl),
Amount: &native.Amount{
Total: core.Int64(totalAmount),
},
}
svc := native.NativeApiService{Client: w.wechatClient}
resp, result, err := svc.Prepay(ctx, req)
if err != nil {
statusCode := 0
if result != nil && result.Response != nil {
statusCode = result.Response.StatusCode
}
return "", fmt.Errorf("微信扫码下单失败: %v, 状态码: %d", err, statusCode)
}
if resp.CodeUrl == nil || *resp.CodeUrl == "" {
return "", fmt.Errorf("微信扫码下单成功但未返回code_url")
}
// 返回二维码链接,由前端生成二维码
return map[string]string{"code_url": *resp.CodeUrl}, nil
}
// CreateWechatOrder 创建微信支付订单(仅 Native 扫码)
func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) {
return w.CreateWechatNativeOrder(ctx, amount, description, outTradeNo)
}
// HandleWechatPayNotification 处理微信支付回调
func (w *WechatPayService) HandleWechatPayNotification(ctx context.Context, req *http.Request) (*payments.Transaction, error) {
transaction := new(payments.Transaction)
_, err := w.notifyHandler.ParseNotifyRequest(ctx, req, transaction)
if err != nil {
return nil, fmt.Errorf("微信支付通知处理失败: %v", err)
}
// 返回交易信息
return transaction, nil
}
// HandleRefundNotification 处理微信退款回调
func (w *WechatPayService) HandleRefundNotification(ctx context.Context, req *http.Request) (*refunddomestic.Refund, error) {
refund := new(refunddomestic.Refund)
_, err := w.notifyHandler.ParseNotifyRequest(ctx, req, refund)
if err != nil {
return nil, fmt.Errorf("微信退款回调通知处理失败: %v", err)
}
return refund, nil
}
// QueryOrderStatus 主动查询订单状态(根据商户订单号)
func (w *WechatPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*payments.Transaction, error) {
svc := native.NativeApiService{Client: w.wechatClient}
// 调用 QueryOrderByOutTradeNo 方法查询订单状态
resp, result, err := svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
OutTradeNo: core.String(outTradeNo),
Mchid: core.String(w.config.Wxpay.MchID),
})
if err != nil {
return nil, fmt.Errorf("订单查询失败: %v, 状态码: %d", err, result.Response.StatusCode)
}
return resp, nil
}
// WeChatRefund 申请微信退款
func (w *WechatPayService) WeChatRefund(ctx context.Context, outTradeNo string, refundAmount float64, totalAmount float64) error {
// 生成唯一的退款单号
outRefundNo := fmt.Sprintf("%s-refund", outTradeNo)
// 初始化退款服务
svc := refunddomestic.RefundsApiService{Client: w.wechatClient}
// 创建退款请求
resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{
OutTradeNo: core.String(outTradeNo),
OutRefundNo: core.String(outRefundNo),
NotifyUrl: core.String(w.config.Wxpay.RefundNotifyUrl),
Amount: &refunddomestic.AmountReq{
Currency: core.String("CNY"),
Refund: core.Int64(ToWechatAmount(refundAmount)),
Total: core.Int64(ToWechatAmount(totalAmount)),
},
})
if err != nil {
return fmt.Errorf("微信订单申请退款错误: %v", err)
}
// 打印退款结果
w.logger.Info("退款申请成功",
zap.Int("status_code", result.Response.StatusCode),
zap.String("out_refund_no", *resp.OutRefundNo),
zap.String("refund_id", *resp.RefundId))
return nil
}
// GenerateOutTradeNo 生成唯一订单号
func (w *WechatPayService) GenerateOutTradeNo() string {
length := 16
timestamp := time.Now().UnixNano()
timeStr := strconv.FormatInt(timestamp, 10)
randomPart := strconv.Itoa(int(timestamp % 1e6))
combined := timeStr + randomPart
if len(combined) >= length {
return combined[:length]
}
for len(combined) < length {
combined += strconv.Itoa(int(timestamp % 10))
}
return combined
}