f
This commit is contained in:
282
internal/shared/payment/alipay.go
Normal file
282
internal/shared/payment/alipay.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
)
|
||||
|
||||
type AlipayConfig struct {
|
||||
AppID string
|
||||
PrivateKey string
|
||||
AlipayPublicKey string
|
||||
IsProduction bool
|
||||
NotifyUrl string
|
||||
ReturnURL string // 同步回调地址
|
||||
}
|
||||
type AliPayService struct {
|
||||
config AlipayConfig
|
||||
AlipayClient *alipay.Client
|
||||
}
|
||||
|
||||
// NewAliPayService 是一个构造函数,用于初始化 AliPayService
|
||||
func NewAliPayService(config AlipayConfig) *AliPayService {
|
||||
client, err := alipay.New(config.AppID, config.PrivateKey, config.IsProduction)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("创建支付宝客户端失败: %v", err))
|
||||
}
|
||||
|
||||
// 加载支付宝公钥
|
||||
err = client.LoadAliPayPublicKey(config.AlipayPublicKey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("加载支付宝公钥失败: %v", err))
|
||||
}
|
||||
return &AliPayService{
|
||||
config: config,
|
||||
AlipayClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AliPayService) CreateAlipayAppOrder(amount decimal.Decimal, subject string, outTradeNo string) (string, error) {
|
||||
client := a.AlipayClient
|
||||
totalAmount := amount.StringFixed(2) // 保留2位小数
|
||||
// 构造移动支付请求
|
||||
p := alipay.TradeAppPay{
|
||||
Trade: alipay.Trade{
|
||||
Subject: subject,
|
||||
OutTradeNo: outTradeNo,
|
||||
TotalAmount: totalAmount,
|
||||
ProductCode: "QUICK_MSECURITY_PAY", // 移动端支付专用代码
|
||||
NotifyURL: a.config.NotifyUrl, // 异步回调通知地址
|
||||
},
|
||||
}
|
||||
|
||||
// 获取APP支付字符串,这里会签名
|
||||
payStr, err := client.TradeAppPay(p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建支付宝订单失败: %v", err)
|
||||
}
|
||||
|
||||
return payStr, nil
|
||||
}
|
||||
|
||||
// CreateAlipayH5Order 创建支付宝H5支付订单
|
||||
func (a *AliPayService) CreateAlipayH5Order(amount decimal.Decimal, subject string, outTradeNo string) (string, error) {
|
||||
client := a.AlipayClient
|
||||
totalAmount := amount.StringFixed(2) // 保留2位小数
|
||||
// 构造H5支付请求
|
||||
p := alipay.TradeWapPay{
|
||||
Trade: alipay.Trade{
|
||||
Subject: subject,
|
||||
OutTradeNo: outTradeNo,
|
||||
TotalAmount: totalAmount,
|
||||
ProductCode: "QUICK_WAP_PAY", // H5支付专用产品码
|
||||
NotifyURL: a.config.NotifyUrl, // 异步回调通知地址
|
||||
ReturnURL: a.config.ReturnURL,
|
||||
},
|
||||
}
|
||||
// 获取H5支付请求字符串,这里会签名
|
||||
payUrl, err := client.TradeWapPay(p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建支付宝H5订单失败: %v", err)
|
||||
}
|
||||
|
||||
return payUrl.String(), nil
|
||||
}
|
||||
|
||||
// CreateAlipayPCOrder 创建支付宝PC端支付订单
|
||||
func (a *AliPayService) CreateAlipayPCOrder(amount decimal.Decimal, subject string, outTradeNo string) (string, error) {
|
||||
client := a.AlipayClient
|
||||
totalAmount := amount.StringFixed(2) // 保留2位小数
|
||||
|
||||
// 构造PC端支付请求
|
||||
p := alipay.TradePagePay{
|
||||
Trade: alipay.Trade{
|
||||
Subject: subject,
|
||||
OutTradeNo: outTradeNo,
|
||||
TotalAmount: totalAmount,
|
||||
ProductCode: "FAST_INSTANT_TRADE_PAY", // PC端支付专用产品码
|
||||
NotifyURL: a.config.NotifyUrl, // 异步回调通知地址
|
||||
ReturnURL: a.config.ReturnURL, // 同步回调地址
|
||||
},
|
||||
}
|
||||
|
||||
// 获取PC端支付URL,这里会签名
|
||||
payUrl, err := client.TradePagePay(p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建支付宝PC端订单失败: %v", err)
|
||||
}
|
||||
|
||||
return payUrl.String(), nil
|
||||
}
|
||||
|
||||
// CreateAlipayOrder 根据平台类型创建支付宝支付订单
|
||||
func (a *AliPayService) CreateAlipayOrder(ctx context.Context, platform string, amount decimal.Decimal, subject string, outTradeNo string) (string, error) {
|
||||
switch platform {
|
||||
case "app":
|
||||
// 调用App支付的创建方法
|
||||
return a.CreateAlipayAppOrder(amount, subject, outTradeNo)
|
||||
case "h5":
|
||||
// 调用H5支付的创建方法,并传入 returnUrl
|
||||
return a.CreateAlipayH5Order(amount, subject, outTradeNo)
|
||||
case "pc":
|
||||
// 调用PC端支付的创建方法
|
||||
return a.CreateAlipayPCOrder(amount, subject, outTradeNo)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的支付平台: %s", platform)
|
||||
}
|
||||
}
|
||||
|
||||
// AliRefund 发起支付宝退款
|
||||
func (a *AliPayService) AliRefund(ctx context.Context, outTradeNo string, refundAmount decimal.Decimal) (*alipay.TradeRefundRsp, error) {
|
||||
refund := alipay.TradeRefund{
|
||||
OutTradeNo: outTradeNo,
|
||||
RefundAmount: refundAmount.StringFixed(2), // 保留2位小数
|
||||
OutRequestNo: fmt.Sprintf("%s-refund", outTradeNo),
|
||||
}
|
||||
|
||||
// 发起退款请求
|
||||
refundResp, err := a.AlipayClient.TradeRefund(ctx, refund)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝退款请求错误:%v", err)
|
||||
}
|
||||
return refundResp, nil
|
||||
}
|
||||
|
||||
// HandleAliPaymentNotification 支付宝支付回调
|
||||
func (a *AliPayService) HandleAliPaymentNotification(r *http.Request) (*alipay.Notification, error) {
|
||||
// 解析表单
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析请求表单失败:%v", err)
|
||||
}
|
||||
// 解析并验证通知,DecodeNotification 会自动验证签名
|
||||
notification, err := a.AlipayClient.DecodeNotification(r.Form)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证签名失败: %v", err)
|
||||
}
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
func (a *AliPayService) IsAlipayPaymentSuccess(notification *alipay.Notification) bool {
|
||||
return notification.TradeStatus == alipay.TradeStatusSuccess
|
||||
}
|
||||
|
||||
func (a *AliPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*alipay.TradeQueryRsp, error) {
|
||||
queryRequest := alipay.TradeQuery{
|
||||
OutTradeNo: outTradeNo,
|
||||
}
|
||||
|
||||
// 发起查询请求
|
||||
resp, err := a.AlipayClient.TradeQuery(ctx, queryRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询支付宝订单失败: %v", err)
|
||||
}
|
||||
|
||||
// 返回交易状态
|
||||
if resp.IsSuccess() {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("查询支付宝订单失败: %v", resp.SubMsg)
|
||||
}
|
||||
|
||||
// 添加全局原子计数器
|
||||
var alipayOrderCounter uint32 = 0
|
||||
|
||||
// GenerateOutTradeNo 生成唯一订单号的函数 - 优化版本
|
||||
func (a *AliPayService) GenerateOutTradeNo() string {
|
||||
|
||||
// 获取当前时间戳(毫秒级)
|
||||
timestamp := time.Now().UnixMilli()
|
||||
timeStr := strconv.FormatInt(timestamp, 10)
|
||||
|
||||
// 原子递增计数器
|
||||
counter := atomic.AddUint32(&alipayOrderCounter, 1)
|
||||
|
||||
// 生成4字节真随机数
|
||||
randomBytes := make([]byte, 4)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
// 如果随机数生成失败,回退到使用时间纳秒数据
|
||||
randomBytes = []byte(strconv.FormatInt(time.Now().UnixNano()%1000000, 16))
|
||||
}
|
||||
randomHex := hex.EncodeToString(randomBytes)
|
||||
|
||||
// 组合所有部分: 前缀 + 时间戳 + 计数器 + 随机数
|
||||
orderNo := fmt.Sprintf("%s%06x%s", timeStr[:10], counter%0xFFFFFF, randomHex[:6])
|
||||
|
||||
// 确保长度不超过32字符(大多数支付平台的限制)
|
||||
if len(orderNo) > 32 {
|
||||
orderNo = orderNo[:32]
|
||||
}
|
||||
|
||||
return orderNo
|
||||
}
|
||||
|
||||
// AliTransfer 支付宝单笔转账到支付宝账户(提现功能)
|
||||
func (a *AliPayService) AliTransfer(
|
||||
ctx context.Context,
|
||||
payeeAccount string, // 收款方支付宝账户
|
||||
payeeName string, // 收款方姓名
|
||||
amount decimal.Decimal, // 转账金额
|
||||
remark string, // 转账备注
|
||||
outBizNo string, // 商户转账唯一订单号(可使用GenerateOutTradeNo生成)
|
||||
) (*alipay.FundTransUniTransferRsp, error) {
|
||||
// 参数校验
|
||||
if payeeAccount == "" {
|
||||
return nil, fmt.Errorf("收款账户不能为空")
|
||||
}
|
||||
if amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("转账金额必须大于0")
|
||||
}
|
||||
|
||||
// 构造转账请求
|
||||
req := alipay.FundTransUniTransfer{
|
||||
OutBizNo: outBizNo,
|
||||
TransAmount: amount.StringFixed(2), // 保留2位小数
|
||||
ProductCode: "TRANS_ACCOUNT_NO_PWD", // 单笔无密转账到支付宝账户
|
||||
BizScene: "DIRECT_TRANSFER", // 单笔转账
|
||||
OrderTitle: "账户提现", // 转账标题
|
||||
Remark: remark,
|
||||
PayeeInfo: &alipay.PayeeInfo{
|
||||
Identity: payeeAccount,
|
||||
IdentityType: "ALIPAY_LOGON_ID", // 根据账户类型选择:
|
||||
Name: payeeName,
|
||||
// ALIPAY_USER_ID/ALIPAY_LOGON_ID
|
||||
},
|
||||
}
|
||||
|
||||
// 执行转账请求
|
||||
transferRsp, err := a.AlipayClient.FundTransUniTransfer(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝转账请求失败: %v", err)
|
||||
}
|
||||
|
||||
return transferRsp, nil
|
||||
}
|
||||
func (a *AliPayService) QueryTransferStatus(
|
||||
ctx context.Context,
|
||||
outBizNo string,
|
||||
) (*alipay.FundTransOrderQueryRsp, error) {
|
||||
req := alipay.FundTransOrderQuery{
|
||||
OutBizNo: outBizNo,
|
||||
}
|
||||
response, err := a.AlipayClient.FundTransOrderQuery(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝接口调用失败: %v", err)
|
||||
}
|
||||
// 处理响应
|
||||
if response.Code.IsFailure() {
|
||||
return nil, fmt.Errorf("支付宝返回错误: %s-%s", response.Code, response.Msg)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
25
internal/shared/payment/context.go
Normal file
25
internal/shared/payment/context.go
Normal 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
|
||||
}
|
||||
48
internal/shared/payment/user_auth_model.go
Normal file
48
internal/shared/payment/user_auth_model.go
Normal 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未实现,请注入真实的实现")
|
||||
}
|
||||
7
internal/shared/payment/utils.go
Normal file
7
internal/shared/payment/utils.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package payment
|
||||
|
||||
// ToWechatAmount 将金额转换为微信支付金额(单位:分)
|
||||
// 微信支付金额以分为单位,需要将元转换为分
|
||||
func ToWechatAmount(amount float64) int64 {
|
||||
return int64(amount * 100)
|
||||
}
|
||||
352
internal/shared/payment/wechatpay.go
Normal file
352
internal/shared/payment/wechatpay.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
"hyapi-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, "hyapi-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(纯支付公钥验签)
|
||||
notifyHandler := notify.NewNotifyHandler(
|
||||
mchAPIv3Key,
|
||||
verifiers.NewSHA256WithRSAPubkeyVerifier(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
|
||||
}
|
||||
Reference in New Issue
Block a user