v0.1
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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user