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 }