2026-01-22 16:04:12 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"fmt"
|
2026-03-04 20:07:41 +08:00
|
|
|
|
"net/url"
|
2026-01-22 16:04:12 +08:00
|
|
|
|
"strconv"
|
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
"tyc-server/app/main/api/internal/config"
|
|
|
|
|
|
"tyc-server/app/main/model"
|
|
|
|
|
|
"tyc-server/pkg/lzkit/lzUtils"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/smartwalle/alipay/v3"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-06 14:05:53 +08:00
|
|
|
|
// bak 支付宝仅用于 [AlipayBakRefundStart, AlipayBakRefundEnd) 区间内支付订单的退款,区间外使用正式 client(CST)
|
|
|
|
|
|
var (
|
|
|
|
|
|
AlipayBakRefundStart = time.Date(2026, 1, 25, 16, 38, 17, 0, time.FixedZone("CST", 8*3600)) // Sun Jan 25 2026 16:38:17 GMT+0800 之前用正式
|
|
|
|
|
|
AlipayBakRefundEnd = time.Date(2026, 2, 2, 18, 26, 0, 0, time.FixedZone("CST", 8*3600)) // 2026-02-02 18:26 之后用正式
|
|
|
|
|
|
)
|
2026-02-06 13:34:49 +08:00
|
|
|
|
|
2026-01-22 16:04:12 +08:00
|
|
|
|
type AliPayService struct {
|
2026-02-06 13:34:49 +08:00
|
|
|
|
config config.AlipayConfig
|
|
|
|
|
|
AlipayClient *alipay.Client
|
2026-02-06 14:05:53 +08:00
|
|
|
|
AlipayClientBak *alipay.Client // 仅用于 [2026-01-25 16:38:17, 2026-02-02 18:26) 区间内订单的退款
|
2026-01-22 16:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 20:07:41 +08:00
|
|
|
|
// clientForMerchant 根据商户标识与订单时间选择对应的支付宝 client。
|
|
|
|
|
|
// - merchant == "two" 且 Bak 存在:优先返回 Bak;
|
|
|
|
|
|
// - merchant == "one" 或空:默认返回主商户;
|
|
|
|
|
|
// - merchant 为空且 orderPayTime 落在备份时间区间:兼容老订单,走 Bak。
|
|
|
|
|
|
func (a *AliPayService) clientForMerchant(merchant string, orderPayTime *time.Time) *alipay.Client {
|
|
|
|
|
|
// 显式指定 two,则优先走 Bak
|
|
|
|
|
|
if merchant == "two" && a.AlipayClientBak != nil {
|
|
|
|
|
|
return a.AlipayClientBak
|
|
|
|
|
|
}
|
|
|
|
|
|
// 显式指定 one 或其他未知标识,一律走主商户
|
|
|
|
|
|
if merchant == "one" || merchant == "" {
|
|
|
|
|
|
// 对于老订单未写入 merchant 的情况,继续保留时间区间兜底逻辑
|
|
|
|
|
|
if merchant == "" && orderPayTime != nil && a.AlipayClientBak != nil &&
|
|
|
|
|
|
!orderPayTime.Before(AlipayBakRefundStart) && orderPayTime.Before(AlipayBakRefundEnd) {
|
|
|
|
|
|
return a.AlipayClientBak
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.AlipayClient
|
|
|
|
|
|
}
|
|
|
|
|
|
// 兜底:未知标识时仍走主商户,避免因为配置问题导致整体不可用
|
|
|
|
|
|
return a.AlipayClient
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 16:04:12 +08:00
|
|
|
|
// NewAliPayService 是一个构造函数,用于初始化 AliPayService
|
|
|
|
|
|
func NewAliPayService(c config.Config) *AliPayService {
|
|
|
|
|
|
client, err := alipay.New(c.Alipay.AppID, c.Alipay.PrivateKey, c.Alipay.IsProduction)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("创建支付宝客户端失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
// 加载支付宝公钥
|
|
|
|
|
|
// err = client.LoadAliPayPublicKey(c.Alipay.AlipayPublicKey)
|
|
|
|
|
|
// if err != nil {
|
|
|
|
|
|
// panic(fmt.Sprintf("加载支付宝公钥失败: %v", err))
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// 加载证书
|
|
|
|
|
|
if err = client.LoadAppCertPublicKeyFromFile(c.Alipay.AppCertPath); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("加载应用公钥证书失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err = client.LoadAlipayCertPublicKeyFromFile(c.Alipay.AlipayCertPath); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("加载支付宝公钥证书失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err = client.LoadAliPayRootCertFromFile(c.Alipay.AlipayRootCertPath); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("加载根证书失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 13:34:49 +08:00
|
|
|
|
svc := &AliPayService{
|
2026-01-22 16:04:12 +08:00
|
|
|
|
config: c.Alipay,
|
|
|
|
|
|
AlipayClient: client,
|
|
|
|
|
|
}
|
2026-02-06 13:34:49 +08:00
|
|
|
|
|
2026-02-06 14:05:53 +08:00
|
|
|
|
// 初始化 bak 支付宝客户端(仅用于 [2026-01-25 16:38:17, 2026-02-02 18:26) 区间内订单的退款)
|
2026-02-06 13:34:49 +08:00
|
|
|
|
if c.Alipay.AppIDBak != "" && c.Alipay.PrivateKeyBak != "" {
|
|
|
|
|
|
bakClient, err := alipay.New(c.Alipay.AppIDBak, c.Alipay.PrivateKeyBak, c.Alipay.IsProduction)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("创建支付宝 bak 客户端失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err = bakClient.LoadAppCertPublicKeyFromFile(c.Alipay.AppCertPathBak); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("加载 bak 应用公钥证书失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err = bakClient.LoadAlipayCertPublicKeyFromFile(c.Alipay.AlipayCertPathBak); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("加载 bak 支付宝公钥证书失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err = bakClient.LoadAliPayRootCertFromFile(c.Alipay.AlipayRootCertPathBak); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("加载 bak 根证书失败: %v", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
svc.AlipayClientBak = bakClient
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return svc
|
2026-01-22 16:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 20:07:41 +08:00
|
|
|
|
func (a *AliPayService) CreateAlipayAppOrder(merchant string, amount float64, subject string, outTradeNo string) (string, error) {
|
|
|
|
|
|
client := a.clientForMerchant(merchant, nil)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
totalAmount := lzUtils.ToAlipayAmount(amount)
|
|
|
|
|
|
// 构造移动支付请求
|
|
|
|
|
|
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支付订单
|
2026-03-04 20:07:41 +08:00
|
|
|
|
func (a *AliPayService) CreateAlipayH5Order(merchant string, amount float64, subject string, outTradeNo string) (string, error) {
|
|
|
|
|
|
client := a.clientForMerchant(merchant, nil)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
totalAmount := lzUtils.ToAlipayAmount(amount)
|
|
|
|
|
|
// 构造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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 20:07:41 +08:00
|
|
|
|
// CreateAlipayOrder 根据平台类型和商户标识创建支付宝支付订单
|
|
|
|
|
|
// merchant: 商户标识,目前约定 "one"=主商户, "two"=备商户
|
|
|
|
|
|
func (a *AliPayService) CreateAlipayOrder(ctx context.Context, merchant string, amount float64, subject string, outTradeNo string) (string, error) {
|
2026-01-22 16:04:12 +08:00
|
|
|
|
// 根据 ctx 中的 platform 判断平台
|
|
|
|
|
|
platform, platformOk := ctx.Value("platform").(string)
|
|
|
|
|
|
if !platformOk {
|
|
|
|
|
|
return "", fmt.Errorf("无的支付平台: %s", platform)
|
|
|
|
|
|
}
|
|
|
|
|
|
switch platform {
|
|
|
|
|
|
case model.PlatformApp:
|
|
|
|
|
|
// 调用App支付的创建方法
|
2026-03-04 20:07:41 +08:00
|
|
|
|
return a.CreateAlipayAppOrder(merchant, amount, subject, outTradeNo)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
case model.PlatformH5:
|
|
|
|
|
|
// 调用H5支付的创建方法,并传入 returnUrl
|
2026-03-04 20:07:41 +08:00
|
|
|
|
return a.CreateAlipayH5Order(merchant, amount, subject, outTradeNo)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
default:
|
|
|
|
|
|
return "", fmt.Errorf("不支持的支付平台: %s", platform)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 20:07:41 +08:00
|
|
|
|
// AliRefund 发起支付宝退款。
|
|
|
|
|
|
// merchant: 支付商户标识(one/two)。为空时按老逻辑仅在备份时间区间内使用 Bak。
|
|
|
|
|
|
// orderPayTime 为订单支付时间(或创建时间);用于老订单按时间区间选择商户;传 nil 则忽略时间区间。
|
2026-02-06 14:12:07 +08:00
|
|
|
|
// outRequestNo 为商户退款请求号,同一笔退款需唯一;传空则使用 "refund-"+outTradeNo,重试时建议传入唯一号避免支付宝报重复。
|
2026-03-04 20:07:41 +08:00
|
|
|
|
func (a *AliPayService) AliRefund(ctx context.Context, merchant string, outTradeNo string, refundAmount float64, orderPayTime *time.Time, outRequestNo string) (*alipay.TradeRefundRsp, error) {
|
|
|
|
|
|
client := a.clientForMerchant(merchant, orderPayTime)
|
2026-02-06 13:34:49 +08:00
|
|
|
|
|
2026-02-06 14:12:07 +08:00
|
|
|
|
if outRequestNo == "" {
|
|
|
|
|
|
outRequestNo = fmt.Sprintf("refund-%s", outTradeNo)
|
|
|
|
|
|
}
|
2026-01-22 16:04:12 +08:00
|
|
|
|
refund := alipay.TradeRefund{
|
|
|
|
|
|
OutTradeNo: outTradeNo,
|
|
|
|
|
|
RefundAmount: lzUtils.ToAlipayAmount(refundAmount),
|
2026-02-06 14:12:07 +08:00
|
|
|
|
OutRequestNo: outRequestNo,
|
2026-01-22 16:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 13:34:49 +08:00
|
|
|
|
refundResp, err := client.TradeRefund(ctx, refund)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("支付宝退款请求错误:%v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return refundResp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 20:07:41 +08:00
|
|
|
|
// HandleAliPaymentNotification 支付宝支付回调验签。
|
|
|
|
|
|
// 由上层根据 out_trade_no 查出订单并传入对应商户标识 merchant。
|
|
|
|
|
|
func (a *AliPayService) HandleAliPaymentNotification(merchant string, form url.Values) (*alipay.Notification, error) {
|
|
|
|
|
|
client := a.clientForMerchant(merchant, nil)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
// 解析并验证通知,DecodeNotification 会自动验证签名
|
2026-03-04 20:07:41 +08:00
|
|
|
|
notification, err := client.DecodeNotification(form)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("验证签名失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return notification, nil
|
|
|
|
|
|
}
|
2026-03-04 20:07:41 +08:00
|
|
|
|
// QueryOrderStatus 按商户标识查询支付宝订单状态
|
|
|
|
|
|
func (a *AliPayService) QueryOrderStatus(ctx context.Context, merchant string, outTradeNo string) (*alipay.TradeQueryRsp, error) {
|
|
|
|
|
|
client := a.clientForMerchant(merchant, nil)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
queryRequest := alipay.TradeQuery{
|
|
|
|
|
|
OutTradeNo: outTradeNo,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发起查询请求
|
2026-03-04 20:07:41 +08:00
|
|
|
|
resp, err := client.TradeQuery(ctx, queryRequest)
|
2026-01-22 16:04:12 +08:00
|
|
|
|
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 float64, // 转账金额
|
|
|
|
|
|
remark string, // 转账备注
|
|
|
|
|
|
outBizNo string, // 商户转账唯一订单号(可使用GenerateOutTradeNo生成)
|
|
|
|
|
|
) (*alipay.FundTransUniTransferRsp, error) {
|
|
|
|
|
|
// 参数校验
|
|
|
|
|
|
if payeeAccount == "" {
|
|
|
|
|
|
return nil, fmt.Errorf("收款账户不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
if amount <= 0 {
|
|
|
|
|
|
return nil, fmt.Errorf("转账金额必须大于0")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造转账请求
|
|
|
|
|
|
req := alipay.FundTransUniTransfer{
|
2026-01-25 16:38:17 +08:00
|
|
|
|
OutBizNo: outBizNo,
|
|
|
|
|
|
TransAmount: lzUtils.ToAlipayAmount(amount), // 金额格式转换
|
|
|
|
|
|
ProductCode: "TRANS_ACCOUNT_NO_PWD", // 单笔无密转账到支付宝账户
|
|
|
|
|
|
BizScene: "DIRECT_TRANSFER", // 单笔转账
|
|
|
|
|
|
OrderTitle: "账户提现", // 转账标题
|
|
|
|
|
|
Remark: remark,
|
|
|
|
|
|
TransferSceneName: "佣金报酬",
|
|
|
|
|
|
TransferSceneReportInfo: []*alipay.TransferSceneReportInfo{
|
|
|
|
|
|
{
|
|
|
|
|
|
InfoType: "佣金报酬说明",
|
|
|
|
|
|
InfoContent: "推广佣金报酬",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-22 16:04:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|