535 lines
15 KiB
Go
535 lines
15 KiB
Go
|
package payment
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"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/app"
|
||
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5"
|
||
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||
|
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||
|
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
TradeStateSuccess = "SUCCESS" // 支付成功
|
||
|
TradeStateRefund = "REFUND" // 转入退款
|
||
|
TradeStateNotPay = "NOTPAY" // 未支付
|
||
|
TradeStateClosed = "CLOSED" // 已关闭
|
||
|
TradeStateRevoked = "REVOKED" // 已撤销(付款码支付)
|
||
|
TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付)
|
||
|
TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败)
|
||
|
)
|
||
|
|
||
|
// WechatPayConfig 微信支付配置
|
||
|
type WechatPayConfig struct {
|
||
|
AppID string `json:"app_id"` // 应用ID
|
||
|
MchID string `json:"mch_id"` // 商户号
|
||
|
MchAPIv3Key string `json:"mch_api_v3_key"` // 商户APIv3密钥
|
||
|
SerialNo string `json:"serial_no"` // 商户证书序列号
|
||
|
PrivateKey string `json:"private_key"` // 商户私钥
|
||
|
NotifyURL string `json:"notify_url"` // 支付通知地址
|
||
|
RefundURL string `json:"refund_url"` // 退款通知地址
|
||
|
}
|
||
|
|
||
|
// WechatPayService 微信支付服务
|
||
|
type WechatPayService struct {
|
||
|
config WechatPayConfig
|
||
|
client *core.Client
|
||
|
jsapiService *jsapi.JsapiApiService
|
||
|
}
|
||
|
|
||
|
// NewWechatPayService 创建微信支付服务
|
||
|
func NewWechatPayService(config *WechatPayConfig) (*WechatPayService, error) {
|
||
|
if err := validateWechatPayConfig(config); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// 加载商户私钥
|
||
|
privateKey, err := utils.LoadPrivateKey(config.PrivateKey)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("加载商户私钥失败: %w", err)
|
||
|
}
|
||
|
|
||
|
// 创建微信支付客户端
|
||
|
opts := []core.ClientOption{
|
||
|
option.WithWechatPayAutoAuthCipher(config.MchID, config.SerialNo, privateKey, config.MchAPIv3Key),
|
||
|
}
|
||
|
client, err := core.NewClient(context.Background(), opts...)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("创建微信支付客户端失败: %w", err)
|
||
|
}
|
||
|
|
||
|
// 下载并加载微信支付平台证书
|
||
|
certDownloader, err := downloader.NewCertificateDownloader(context.Background(), config.MchID, privateKey, config.MchAPIv3Key, config.SerialNo)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("创建证书下载器失败: %w", err)
|
||
|
}
|
||
|
if err := certDownloader.DownloadCertificates(context.Background()); err != nil {
|
||
|
return nil, fmt.Errorf("下载微信支付平台证书失败: %w", err)
|
||
|
}
|
||
|
|
||
|
return &WechatPayService{
|
||
|
config: *config,
|
||
|
client: client,
|
||
|
jsapiService: &jsapi.JsapiApiService{Client: client},
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// CreateOrder 创建支付订单
|
||
|
func (w *WechatPayService) CreateOrder(order *Order) (*OrderResult, error) {
|
||
|
if err := validateCreateOrder(order); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
switch order.Platform {
|
||
|
case PlatformApp:
|
||
|
return w.createAppOrder(order)
|
||
|
case PlatformH5:
|
||
|
return w.createH5Order(order)
|
||
|
case PlatformMiniProg:
|
||
|
return w.createJsapiOrder(order)
|
||
|
default:
|
||
|
return nil, errors.New("不支持的支付平台")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// createAppOrder 创建APP支付订单
|
||
|
func (w *WechatPayService) createAppOrder(order *Order) (*OrderResult, error) {
|
||
|
svc := app.AppApiService{Client: w.client}
|
||
|
amount := &app.Amount{
|
||
|
Total: core.Int64(int64(order.Amount * 100)), // 转换为分
|
||
|
Currency: core.String("CNY"),
|
||
|
}
|
||
|
req := app.PrepayRequest{
|
||
|
Appid: core.String(w.config.AppID),
|
||
|
Mchid: core.String(w.config.MchID),
|
||
|
Description: core.String(order.Subject),
|
||
|
OutTradeNo: core.String(order.OutTradeNo),
|
||
|
NotifyUrl: core.String(w.config.NotifyURL),
|
||
|
Amount: amount,
|
||
|
}
|
||
|
|
||
|
resp, _, err := svc.Prepay(context.Background(), req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("创建APP支付订单失败: %w", err)
|
||
|
}
|
||
|
|
||
|
// 生成支付参数
|
||
|
params, err := w.generateAppPayParams(resp.PrepayId)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &OrderResult{
|
||
|
OrderNo: order.OutTradeNo,
|
||
|
PayParams: params,
|
||
|
ExpireTime: time.Now().Add(30 * time.Minute),
|
||
|
CreateTime: time.Now(),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// createH5Order 创建H5支付订单
|
||
|
func (w *WechatPayService) createH5Order(order *Order) (*OrderResult, error) {
|
||
|
svc := h5.H5ApiService{Client: w.client}
|
||
|
amount := &h5.Amount{
|
||
|
Total: core.Int64(int64(order.Amount * 100)), // 转换为分
|
||
|
Currency: core.String("CNY"),
|
||
|
}
|
||
|
req := h5.PrepayRequest{
|
||
|
Appid: core.String(w.config.AppID),
|
||
|
Mchid: core.String(w.config.MchID),
|
||
|
Description: core.String(order.Subject),
|
||
|
OutTradeNo: core.String(order.OutTradeNo),
|
||
|
NotifyUrl: core.String(w.config.NotifyURL),
|
||
|
Amount: amount,
|
||
|
SceneInfo: &h5.SceneInfo{
|
||
|
PayerClientIp: core.String("127.0.0.1"), // TODO: 获取真实IP
|
||
|
H5Info: &h5.H5Info{
|
||
|
Type: core.String("Wap"),
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
resp, _, err := svc.Prepay(context.Background(), req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("创建H5支付订单失败: %w", err)
|
||
|
}
|
||
|
|
||
|
return &OrderResult{
|
||
|
OrderNo: order.OutTradeNo,
|
||
|
PayParams: resp.H5Url,
|
||
|
ExpireTime: time.Now().Add(30 * time.Minute),
|
||
|
CreateTime: time.Now(),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// createJsapiOrder 创建小程序支付订单
|
||
|
func (w *WechatPayService) createJsapiOrder(order *Order) (*OrderResult, error) {
|
||
|
if order.OpenID == "" {
|
||
|
return nil, errors.New("小程序支付必须提供OpenID")
|
||
|
}
|
||
|
|
||
|
svc := jsapi.JsapiApiService{Client: w.client}
|
||
|
amount := &jsapi.Amount{
|
||
|
Total: core.Int64(int64(order.Amount * 100)), // 转换为分
|
||
|
Currency: core.String("CNY"),
|
||
|
}
|
||
|
req := jsapi.PrepayRequest{
|
||
|
Appid: core.String(w.config.AppID),
|
||
|
Mchid: core.String(w.config.MchID),
|
||
|
Description: core.String(order.Subject),
|
||
|
OutTradeNo: core.String(order.OutTradeNo),
|
||
|
NotifyUrl: core.String(w.config.NotifyURL),
|
||
|
Amount: amount,
|
||
|
Payer: &jsapi.Payer{
|
||
|
Openid: core.String(order.OpenID),
|
||
|
},
|
||
|
}
|
||
|
|
||
|
resp, _, err := svc.Prepay(context.Background(), req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("创建小程序支付订单失败: %w", err)
|
||
|
}
|
||
|
|
||
|
// 生成支付参数
|
||
|
params, err := w.generateJsapiPayParams(resp.PrepayId)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &OrderResult{
|
||
|
OrderNo: order.OutTradeNo,
|
||
|
PayParams: params,
|
||
|
ExpireTime: time.Now().Add(30 * time.Minute),
|
||
|
CreateTime: time.Now(),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// Refund 申请退款
|
||
|
func (w *WechatPayService) Refund(refund *Refund) (*RefundResult, error) {
|
||
|
if err := validateRefund(refund); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
svc := refunddomestic.RefundsApiService{Client: w.client}
|
||
|
amount := &refunddomestic.AmountReq{
|
||
|
Refund: core.Int64(int64(refund.RefundAmount * 100)), // 转换为分
|
||
|
Total: core.Int64(int64(refund.TotalAmount * 100)), // 转换为分
|
||
|
Currency: core.String("CNY"),
|
||
|
}
|
||
|
req := refunddomestic.CreateRequest{
|
||
|
OutTradeNo: core.String(refund.OrderNo),
|
||
|
OutRefundNo: core.String(refund.RefundNo),
|
||
|
Reason: core.String(refund.Reason),
|
||
|
NotifyUrl: core.String(refund.NotifyURL),
|
||
|
Amount: amount,
|
||
|
}
|
||
|
|
||
|
resp, _, err := svc.Create(context.Background(), req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("申请退款失败: %w", err)
|
||
|
}
|
||
|
|
||
|
return &RefundResult{
|
||
|
OrderNo: refund.OrderNo,
|
||
|
RefundNo: refund.RefundNo,
|
||
|
RefundID: *resp.RefundId,
|
||
|
Status: string(*resp.Status),
|
||
|
RefundTime: time.Now(),
|
||
|
RefundAmount: refund.RefundAmount,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// QueryOrder 查询订单
|
||
|
func (w *WechatPayService) QueryOrder(query *OrderQuery) (*OrderQueryResult, error) {
|
||
|
if err := validateOrderQuery(query); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
var resp *payments.Transaction
|
||
|
var err error
|
||
|
|
||
|
if query.TransactionID != "" {
|
||
|
resp, _, err = w.jsapiService.QueryOrderById(context.Background(),
|
||
|
jsapi.QueryOrderByIdRequest{
|
||
|
TransactionId: core.String(query.TransactionID),
|
||
|
})
|
||
|
} else {
|
||
|
resp, _, err = w.jsapiService.QueryOrderByOutTradeNo(context.Background(),
|
||
|
jsapi.QueryOrderByOutTradeNoRequest{
|
||
|
OutTradeNo: core.String(query.OrderNo),
|
||
|
Mchid: core.String(w.config.MchID),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("查询订单失败: %w", err)
|
||
|
}
|
||
|
|
||
|
// 转换支付状态
|
||
|
status := convertWechatPayStatus(*resp.TradeState)
|
||
|
|
||
|
// 解析支付时间
|
||
|
payTime, _ := time.Parse(time.RFC3339, *resp.SuccessTime)
|
||
|
|
||
|
return &OrderQueryResult{
|
||
|
OrderNo: *resp.OutTradeNo,
|
||
|
TransactionID: *resp.TransactionId,
|
||
|
Status: status,
|
||
|
Amount: float64(*resp.Amount.Total) / 100, // 转换为元
|
||
|
PayTime: payTime,
|
||
|
PayMethod: MethodWechat,
|
||
|
Platform: getWechatPayPlatform(*resp.TradeType),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// HandlePaymentNotification 处理支付结果通知
|
||
|
func (w *WechatPayService) HandlePaymentNotification(r *http.Request) (*OrderQueryResult, error) {
|
||
|
// 初始化通知处理器
|
||
|
handler := notify.NewNotifyHandler(w.config.MchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(downloader.MgrInstance().GetCertificateVisitor(w.config.MchID)))
|
||
|
|
||
|
// 解析通知数据
|
||
|
transaction := new(payments.Transaction)
|
||
|
notifyReq, err := handler.ParseNotifyRequest(context.Background(), r, transaction)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("解析支付通知失败: %w", err)
|
||
|
}
|
||
|
|
||
|
// 验证通知数据
|
||
|
if notifyReq.EventType != "TRANSACTION.SUCCESS" {
|
||
|
return nil, errors.New("非支付成功通知")
|
||
|
}
|
||
|
|
||
|
// 转换支付状态
|
||
|
status := convertWechatPayStatus(*transaction.TradeState)
|
||
|
|
||
|
// 解析支付时间
|
||
|
payTime, _ := time.Parse(time.RFC3339, *transaction.SuccessTime)
|
||
|
|
||
|
return &OrderQueryResult{
|
||
|
OrderNo: *transaction.OutTradeNo,
|
||
|
TransactionID: *transaction.TransactionId,
|
||
|
Status: status,
|
||
|
Amount: float64(*transaction.Amount.Total) / 100, // 转换为元
|
||
|
PayTime: payTime,
|
||
|
PayMethod: MethodWechat,
|
||
|
Platform: getWechatPayPlatform(*transaction.TradeType),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// 生成APP支付参数
|
||
|
func (w *WechatPayService) generateAppPayParams(prepayID *string) (map[string]string, error) {
|
||
|
if prepayID == nil {
|
||
|
return nil, errors.New("预支付ID不能为空")
|
||
|
}
|
||
|
|
||
|
params := make(map[string]string)
|
||
|
params["appid"] = w.config.AppID
|
||
|
params["partnerid"] = w.config.MchID
|
||
|
params["prepayid"] = *prepayID
|
||
|
params["package"] = "Sign=WXPay"
|
||
|
nonce, err := utils.GenerateNonce()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("生成随机字符串失败: %w", err)
|
||
|
}
|
||
|
params["noncestr"] = nonce
|
||
|
params["timestamp"] = fmt.Sprintf("%d", time.Now().Unix())
|
||
|
|
||
|
// 生成签名
|
||
|
sign, err := w.signParams(params)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
params["sign"] = sign
|
||
|
|
||
|
return params, nil
|
||
|
}
|
||
|
|
||
|
// 生成小程序支付参数
|
||
|
func (w *WechatPayService) generateJsapiPayParams(prepayID *string) (map[string]string, error) {
|
||
|
if prepayID == nil {
|
||
|
return nil, errors.New("预支付ID不能为空")
|
||
|
}
|
||
|
|
||
|
params := make(map[string]string)
|
||
|
params["appId"] = w.config.AppID
|
||
|
params["timeStamp"] = fmt.Sprintf("%d", time.Now().Unix())
|
||
|
nonce, err := utils.GenerateNonce()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("生成随机字符串失败: %w", err)
|
||
|
}
|
||
|
params["nonceStr"] = nonce
|
||
|
params["package"] = "prepay_id=" + *prepayID
|
||
|
params["signType"] = "RSA"
|
||
|
|
||
|
// 生成签名
|
||
|
sign, err := w.signParams(params)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
params["paySign"] = sign
|
||
|
|
||
|
return params, nil
|
||
|
}
|
||
|
|
||
|
// 签名参数
|
||
|
func (w *WechatPayService) signParams(params map[string]string) (string, error) {
|
||
|
// 按字典序排序参数
|
||
|
var keys []string
|
||
|
for k := range params {
|
||
|
keys = append(keys, k)
|
||
|
}
|
||
|
sort.Strings(keys)
|
||
|
|
||
|
var pairs []string
|
||
|
for _, k := range keys {
|
||
|
pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k]))
|
||
|
}
|
||
|
message := strings.Join(pairs, "&")
|
||
|
|
||
|
// 使用商户私钥签名
|
||
|
privateKey, err := utils.LoadPrivateKey(w.config.PrivateKey)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("加载商户私钥失败: %w", err)
|
||
|
}
|
||
|
|
||
|
signature, err := utils.SignSHA256WithRSA(message, privateKey)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("签名失败: %w", err)
|
||
|
}
|
||
|
|
||
|
return signature, nil
|
||
|
}
|
||
|
|
||
|
// 转换微信支付状态
|
||
|
func convertWechatPayStatus(tradeState string) PaymentStatus {
|
||
|
switch tradeState {
|
||
|
case "SUCCESS":
|
||
|
return StatusSuccess
|
||
|
case "REFUND":
|
||
|
return StatusRefunded
|
||
|
case "NOTPAY":
|
||
|
return StatusPending
|
||
|
case "CLOSED":
|
||
|
return StatusClosed
|
||
|
case "PAYERROR":
|
||
|
return StatusFailed
|
||
|
default:
|
||
|
return StatusPending
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 获取微信支付平台
|
||
|
func getWechatPayPlatform(tradeType string) PaymentPlatform {
|
||
|
switch tradeType {
|
||
|
case "APP":
|
||
|
return PlatformApp
|
||
|
case "JSAPI":
|
||
|
return PlatformMiniProg
|
||
|
case "NATIVE", "MWEB":
|
||
|
return PlatformH5
|
||
|
default:
|
||
|
return PlatformApp
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 验证微信支付配置
|
||
|
func validateWechatPayConfig(config *WechatPayConfig) error {
|
||
|
if config == nil {
|
||
|
return errors.New("配置不能为空")
|
||
|
}
|
||
|
if config.AppID == "" {
|
||
|
return errors.New("应用ID不能为空")
|
||
|
}
|
||
|
if config.MchID == "" {
|
||
|
return errors.New("商户号不能为空")
|
||
|
}
|
||
|
if config.MchAPIv3Key == "" {
|
||
|
return errors.New("商户APIv3密钥不能为空")
|
||
|
}
|
||
|
if config.SerialNo == "" {
|
||
|
return errors.New("商户证书序列号不能为空")
|
||
|
}
|
||
|
if config.PrivateKey == "" {
|
||
|
return errors.New("商户私钥不能为空")
|
||
|
}
|
||
|
if config.NotifyURL == "" {
|
||
|
return errors.New("支付通知地址不能为空")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// 验证创建订单参数
|
||
|
func validateCreateOrder(order *Order) error {
|
||
|
if order == nil {
|
||
|
return errors.New("订单信息不能为空")
|
||
|
}
|
||
|
if order.OutTradeNo == "" {
|
||
|
return errors.New("商户订单号不能为空")
|
||
|
}
|
||
|
if order.Subject == "" {
|
||
|
return errors.New("商品描述不能为空")
|
||
|
}
|
||
|
if order.Amount <= 0 {
|
||
|
return errors.New("支付金额必须大于0")
|
||
|
}
|
||
|
if order.NotifyURL == "" {
|
||
|
return errors.New("通知地址不能为空")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// 验证退款参数
|
||
|
func validateRefund(refund *Refund) error {
|
||
|
if refund == nil {
|
||
|
return errors.New("退款信息不能为空")
|
||
|
}
|
||
|
if refund.OrderNo == "" {
|
||
|
return errors.New("商户订单号不能为空")
|
||
|
}
|
||
|
if refund.RefundNo == "" {
|
||
|
return errors.New("商户退款单号不能为空")
|
||
|
}
|
||
|
if refund.RefundAmount <= 0 {
|
||
|
return errors.New("退款金额必须大于0")
|
||
|
}
|
||
|
if refund.TotalAmount <= 0 {
|
||
|
return errors.New("订单总金额必须大于0")
|
||
|
}
|
||
|
if refund.RefundAmount > refund.TotalAmount {
|
||
|
return errors.New("退款金额不能大于订单总金额")
|
||
|
}
|
||
|
if refund.NotifyURL == "" {
|
||
|
return errors.New("退款通知地址不能为空")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// 验证订单查询参数
|
||
|
func validateOrderQuery(query *OrderQuery) error {
|
||
|
if query == nil {
|
||
|
return errors.New("查询参数不能为空")
|
||
|
}
|
||
|
if query.OrderNo == "" && query.TransactionID == "" {
|
||
|
return errors.New("商户订单号和交易号不能同时为空")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// GenerateOutTradeNo 生成商户订单号
|
||
|
func (w *WechatPayService) GenerateOutTradeNo() string {
|
||
|
return GenerateOrderNo(WechatPrefix)
|
||
|
}
|