294 lines
7.9 KiB
Go
294 lines
7.9 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
"tyc-server/pkg/lzkit/lzUtils"
|
|
|
|
"github.com/smartwalle/alipay/v3"
|
|
)
|
|
|
|
// AlipayConfig 支付宝配置
|
|
type AlipayConfig struct {
|
|
AppID string `json:"appId"` // 支付宝应用ID
|
|
PrivateKey string `json:"privateKey"` // 应用私钥
|
|
AlipayPublicKey string `json:"alipayPublicKey"` // 支付宝公钥
|
|
IsProduction bool `json:"isProduction"` // 是否生产环境
|
|
NotifyURL string `json:"notifyUrl"` // 支付结果通知地址
|
|
ReturnURL string `json:"returnUrl"` // 支付完成跳转地址
|
|
}
|
|
|
|
// AliPayService 支付宝支付服务
|
|
type AliPayService struct {
|
|
config AlipayConfig
|
|
alipayClient *alipay.Client
|
|
}
|
|
|
|
// NewAliPayService 创建支付宝支付服务
|
|
func NewAliPayService(c AlipayConfig) (*AliPayService, error) {
|
|
client, err := alipay.New(c.AppID, c.PrivateKey, c.IsProduction)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
|
|
}
|
|
|
|
if err := client.LoadAliPayPublicKey(c.AlipayPublicKey); err != nil {
|
|
return nil, fmt.Errorf("加载支付宝公钥失败: %w", err)
|
|
}
|
|
|
|
return &AliPayService{
|
|
config: c,
|
|
alipayClient: client,
|
|
}, nil
|
|
}
|
|
|
|
// CreateOrder 创建支付宝支付订单
|
|
func (a *AliPayService) CreateOrder(ctx context.Context, req *AlipayCreateOrderRequest) (*AlipayCreateOrderResponse, error) {
|
|
if err := a.validateCreateOrderRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.NotifyURL == "" {
|
|
req.NotifyURL = a.config.NotifyURL
|
|
}
|
|
if req.ReturnURL == "" {
|
|
req.ReturnURL = a.config.ReturnURL
|
|
}
|
|
if req.OutTradeNo == "" {
|
|
req.OutTradeNo = a.GenerateOutTradeNo()
|
|
}
|
|
var payParams string
|
|
var err error
|
|
|
|
switch req.Platform {
|
|
case PlatformApp:
|
|
payParams, err = a.createAppOrder(req)
|
|
case PlatformH5:
|
|
payParams, err = a.createH5Order(req)
|
|
default:
|
|
return nil, errors.New("不支持的支付平台")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &AlipayCreateOrderResponse{
|
|
OrderNo: req.OutTradeNo,
|
|
PayParams: payParams,
|
|
}, nil
|
|
}
|
|
|
|
// createAppOrder 创建APP支付订单
|
|
func (a *AliPayService) createAppOrder(req *AlipayCreateOrderRequest) (string, error) {
|
|
p := alipay.TradeAppPay{
|
|
Trade: alipay.Trade{
|
|
Subject: req.Subject,
|
|
OutTradeNo: req.OutTradeNo,
|
|
TotalAmount: lzUtils.ToAlipayAmount(req.Amount),
|
|
ProductCode: "QUICK_MSECURITY_PAY",
|
|
NotifyURL: req.NotifyURL,
|
|
},
|
|
}
|
|
|
|
payStr, err := a.alipayClient.TradeAppPay(p)
|
|
if err != nil {
|
|
return "", fmt.Errorf("创建支付宝APP支付订单失败: %w", err)
|
|
}
|
|
|
|
return payStr, nil
|
|
}
|
|
|
|
// createH5Order 创建H5支付订单
|
|
func (a *AliPayService) createH5Order(req *AlipayCreateOrderRequest) (string, error) {
|
|
if req.ReturnURL == "" {
|
|
return "", errors.New("H5支付必须提供ReturnURL")
|
|
}
|
|
|
|
p := alipay.TradeWapPay{
|
|
Trade: alipay.Trade{
|
|
Subject: req.Subject,
|
|
OutTradeNo: req.OutTradeNo,
|
|
TotalAmount: lzUtils.ToAlipayAmount(req.Amount),
|
|
ProductCode: "QUICK_WAP_PAY",
|
|
NotifyURL: req.NotifyURL,
|
|
ReturnURL: req.ReturnURL,
|
|
},
|
|
}
|
|
|
|
payUrl, err := a.alipayClient.TradeWapPay(p)
|
|
if err != nil {
|
|
return "", fmt.Errorf("创建支付宝H5支付订单失败: %w", err)
|
|
}
|
|
|
|
return payUrl.String(), nil
|
|
}
|
|
|
|
// Refund 申请退款
|
|
func (a *AliPayService) Refund(ctx context.Context, req *AlipayRefundRequest) (*AlipayRefundResponse, error) {
|
|
if err := a.validateRefundRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.RefundNo == "" {
|
|
req.RefundNo = a.GenerateRefundNo()
|
|
}
|
|
|
|
refund := alipay.TradeRefund{
|
|
OutTradeNo: req.OrderNo,
|
|
RefundAmount: a.ToAlipayAmount(req.RefundAmount),
|
|
OutRequestNo: req.RefundNo,
|
|
}
|
|
|
|
resp, err := a.alipayClient.TradeRefund(ctx, refund)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("申请支付宝退款失败: %w", err)
|
|
}
|
|
|
|
if !resp.IsSuccess() {
|
|
return nil, fmt.Errorf("申请支付宝退款失败: %s", resp.SubMsg)
|
|
}
|
|
|
|
return &AlipayRefundResponse{
|
|
OrderNo: req.OrderNo,
|
|
RefundNo: req.RefundNo,
|
|
RefundID: resp.TradeNo,
|
|
Status: StatusSuccess,
|
|
RefundTime: time.Now(),
|
|
RefundAmount: alipayamountToFloat(resp.RefundFee),
|
|
}, nil
|
|
}
|
|
|
|
// QueryOrder 查询订单
|
|
func (a *AliPayService) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*QueryOrderResponse, error) {
|
|
if err := a.validateQueryRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := alipay.TradeQuery{
|
|
OutTradeNo: req.OrderNo,
|
|
}
|
|
|
|
resp, err := a.alipayClient.TradeQuery(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("查询支付宝订单失败: %w", err)
|
|
}
|
|
|
|
if !resp.IsSuccess() {
|
|
return nil, fmt.Errorf("查询支付宝订单失败: %s", resp.SubMsg)
|
|
}
|
|
|
|
amount, _ := strconv.ParseFloat(resp.TotalAmount, 64)
|
|
|
|
return &QueryOrderResponse{
|
|
OrderNo: resp.OutTradeNo,
|
|
TransactionID: resp.TradeNo,
|
|
Status: a.convertTradeStatus(resp.TradeStatus),
|
|
Amount: amount,
|
|
PayTime: a.parseAlipayTime(resp.SendPayDate),
|
|
PayMethod: MethodAlipay,
|
|
}, nil
|
|
}
|
|
|
|
// HandlePaymentNotification 处理支付结果通知
|
|
func (a *AliPayService) HandlePaymentNotification(req *http.Request) (*QueryOrderResponse, error) {
|
|
if err := req.ParseForm(); err != nil {
|
|
return nil, fmt.Errorf("解析支付宝通知失败: %w", err)
|
|
}
|
|
|
|
notification, err := a.alipayClient.DecodeNotification(req.Form)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("验证支付宝通知签名失败: %w", err)
|
|
}
|
|
|
|
amount := alipayamountToFloat(notification.TotalAmount)
|
|
|
|
return &QueryOrderResponse{
|
|
OrderNo: notification.OutTradeNo,
|
|
TransactionID: notification.TradeNo,
|
|
Status: a.convertTradeStatus(alipay.TradeStatus(notification.TradeStatus)),
|
|
Amount: amount,
|
|
PayTime: a.parseAlipayTime(notification.GmtPayment),
|
|
PayMethod: MethodAlipay,
|
|
}, nil
|
|
}
|
|
|
|
// validateCreateOrderRequest 验证创建订单请求参数
|
|
func (a *AliPayService) validateCreateOrderRequest(req *AlipayCreateOrderRequest) error {
|
|
if req.Amount <= 0 {
|
|
return errors.New("支付金额必须大于0")
|
|
}
|
|
if req.Subject == "" {
|
|
return errors.New("商品描述不能为空")
|
|
}
|
|
if req.Platform == "" {
|
|
return errors.New("支付平台不能为空")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateRefundRequest 验证退款请求参数
|
|
func (a *AliPayService) validateRefundRequest(req *AlipayRefundRequest) error {
|
|
if req.OrderNo == "" {
|
|
return errors.New("商户订单号不能为空")
|
|
}
|
|
if req.RefundAmount <= 0 {
|
|
return errors.New("退款金额必须大于0")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateQueryRequest 验证查询请求参数
|
|
func (a *AliPayService) validateQueryRequest(req *QueryOrderRequest) error {
|
|
if req.OrderNo == "" && req.TransactionID == "" {
|
|
return errors.New("商户订单号和交易号不能同时为空")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// convertTradeStatus 转换支付宝交易状态
|
|
func (a *AliPayService) convertTradeStatus(status alipay.TradeStatus) PaymentStatus {
|
|
switch status {
|
|
case alipay.TradeStatusWaitBuyerPay:
|
|
return StatusPending
|
|
case alipay.TradeStatusSuccess:
|
|
return StatusSuccess
|
|
case alipay.TradeStatusClosed:
|
|
return StatusClosed
|
|
case alipay.TradeStatusFinished:
|
|
return StatusSuccess
|
|
default:
|
|
return StatusFailed
|
|
}
|
|
}
|
|
|
|
// parseAlipayTime 解析支付宝时间字符串
|
|
func (a *AliPayService) parseAlipayTime(timeStr string) time.Time {
|
|
t, err := time.Parse("2006-01-02 15:04:05", timeStr)
|
|
if err != nil {
|
|
return time.Now()
|
|
}
|
|
return t
|
|
}
|
|
|
|
// GenerateOutTradeNo 生成商户订单号
|
|
func (a *AliPayService) GenerateOutTradeNo() string {
|
|
return GenerateOrderNo(AlipayPrefix)
|
|
}
|
|
|
|
// GenerateRefundNo 生成退款订单号
|
|
func (a *AliPayService) GenerateRefundNo() string {
|
|
return GenerateOrderNo(fmt.Sprintf("%s_%s", RefundPrefix, AlipayPrefix))
|
|
}
|
|
|
|
// ToAlipayAmount 将金额从元转换为支付宝支付 SDK 需要的字符串格式,保留两位小数
|
|
func (a *AliPayService) ToAlipayAmount(amount float64) string {
|
|
// 格式化为字符串,保留两位小数
|
|
return fmt.Sprintf("%.2f", amount)
|
|
}
|
|
func alipayamountToFloat(amount string) float64 {
|
|
amountFloat, _ := strconv.ParseFloat(amount, 64)
|
|
return amountFloat
|
|
}
|