283 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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
 | ||
| }
 |