微信支付

This commit is contained in:
2025-12-12 15:27:15 +08:00
parent 2c89b8cb26
commit 0d4953c6d3
34 changed files with 1974 additions and 279 deletions

View File

@@ -0,0 +1,25 @@
package payment
import (
"context"
"fmt"
)
// GetUidFromCtx 从context中获取用户ID
func GetUidFromCtx(ctx context.Context) (string, error) {
userID := ctx.Value("user_id")
if userID == nil {
return "", fmt.Errorf("用户ID不存在于上下文中")
}
id, ok := userID.(string)
if !ok {
return "", fmt.Errorf("用户ID类型错误")
}
if id == "" {
return "", fmt.Errorf("用户ID为空")
}
return id, nil
}

View File

@@ -0,0 +1,48 @@
package payment
import (
"context"
"fmt"
)
// UserAuthModel 用户认证模型接口
// 用于存储和管理用户的第三方认证信息如微信OpenID
type UserAuthModel interface {
FindOneByUserIdAuthType(ctx context.Context, userID string, authType string) (*UserAuth, error)
UpsertUserAuth(ctx context.Context, userID, authType, authKey string) error
}
// UserAuth 用户认证信息
type UserAuth struct {
UserID string // 用户ID
AuthType string // 认证类型
AuthKey string // 认证密钥如OpenID
}
// Platform 支付平台常量
const (
PlatformWxMini = "wx_mini" // 微信小程序
PlatformWxH5 = "wx_h5" // 微信H5
PlatformApp = "app" // APP
PlatformWxNative = "wx_native" // 微信Native扫码
)
// UserAuthType 用户认证类型常量
const (
UserAuthTypeWxMiniOpenID = "wx_mini_openid" // 微信小程序OpenID
UserAuthTypeWxh5OpenID = "wx_h5_openid" // 微信H5 OpenID
)
// DefaultUserAuthModel 默认实现(如果不需要实际数据库查询,可以返回错误)
type DefaultUserAuthModel struct{}
// FindOneByUserIdAuthType 查找用户认证信息
// 注意:这是一个占位实现,实际使用时需要注入真实的实现
func (m *DefaultUserAuthModel) FindOneByUserIdAuthType(ctx context.Context, userID string, authType string) (*UserAuth, error) {
return nil, fmt.Errorf("UserAuthModel未实现请注入真实的实现")
}
// UpsertUserAuth 占位实现
func (m *DefaultUserAuthModel) UpsertUserAuth(ctx context.Context, userID, authType, authKey string) error {
return fmt.Errorf("UserAuthModel未实现请注入真实的实现")
}

View File

@@ -0,0 +1,7 @@
package payment
// ToWechatAmount 将金额转换为微信支付金额(单位:分)
// 微信支付金额以分为单位,需要将元转换为分
func ToWechatAmount(amount float64) int64 {
return int64(amount * 100)
}

View File

@@ -0,0 +1,353 @@
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
}