task: 完成支付宝共享基础(迁移/WechatConfig/PaymentStore/pkg/alipay)

This commit is contained in:
2026-05-22 12:09:55 +08:00
parent 2768deb0b6
commit 58bdb2f18e
13 changed files with 342 additions and 4 deletions

4
go.mod
View File

@@ -69,6 +69,10 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/smartwalle/alipay/v3 v3.2.29 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.1.0 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect

8
go.sum
View File

@@ -208,6 +208,14 @@ github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dI
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartwalle/alipay/v3 v3.2.29 h1:roGFqlml8hDa//0TpFmlyxZhndTYs7rbYLu/HlNFNJo=
github.com/smartwalle/alipay/v3 v3.2.29/go.mod h1:XarBLuAkwK3ah7mYjVtghRu+ysxzlex9sRkgqNMzMRU=
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
github.com/smartwalle/ngx v1.1.0 h1:q8nANgWSPRGeI/u+ixBoA4mf68DrUq6vZ+n9L5UKv9I=
github.com/smartwalle/ngx v1.1.0/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=

View File

@@ -10,7 +10,7 @@ import (
type CreateWechatConfigRequest struct {
Name string `json:"name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"配置名称"`
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置描述"`
ProviderType string `json:"provider_type" validate:"required,oneof=wechat fuiou" required:"true" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
ProviderType string `json:"provider_type" validate:"required,oneof=wechat wechat_v2 fuiou" required:"true" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
OaAppID string `json:"oa_app_id" validate:"omitempty,max=100" maxLength:"100" description:"公众号AppID"`
OaAppSecret string `json:"oa_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"公众号AppSecret"`
@@ -36,13 +36,22 @@ type CreateWechatConfigRequest struct {
FyPublicKey string `json:"fy_public_key" validate:"omitempty" description:"富友公钥(PEM格式)"`
FyAPIURL string `json:"fy_api_url" validate:"omitempty,max=500" maxLength:"500" description:"富友API地址"`
FyNotifyURL string `json:"fy_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"富友支付回调地址"`
// 支付宝配置字段(并存能力,不受 provider_type 限制)
AliAppID string `json:"ali_app_id" validate:"omitempty,max=100" maxLength:"100" description:"支付宝应用ID"`
AliPrivateKey string `json:"ali_private_key" validate:"omitempty" description:"支付宝应用私钥(PEM格式)"`
AliPublicKey string `json:"ali_public_key" validate:"omitempty" description:"支付宝公钥(PEM格式用于验签)"`
AliNotifyURL string `json:"ali_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝异步通知地址"`
AliReturnURL string `json:"ali_return_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝同步跳转地址"`
AliProduction bool `json:"ali_production" description:"是否生产环境 (true:生产, false:沙箱)"`
AliPayExpireMinutes int `json:"ali_pay_expire_minutes" validate:"omitempty,min=1,max=1440" minimum:"1" maximum:"1440" description:"支付过期分钟数 (默认30分钟)"`
}
// UpdateWechatConfigRequest 更新微信参数配置请求
type UpdateWechatConfigRequest struct {
Name *string `json:"name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"配置名称"`
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置描述"`
ProviderType *string `json:"provider_type" validate:"omitempty,oneof=wechat fuiou" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
ProviderType *string `json:"provider_type" validate:"omitempty,oneof=wechat wechat_v2 fuiou" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
OaAppID *string `json:"oa_app_id" validate:"omitempty,max=100" maxLength:"100" description:"公众号AppID"`
OaAppSecret *string `json:"oa_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"公众号AppSecret"`
@@ -68,6 +77,15 @@ type UpdateWechatConfigRequest struct {
FyPublicKey *string `json:"fy_public_key" validate:"omitempty" description:"富友公钥(PEM格式)"`
FyAPIURL *string `json:"fy_api_url" validate:"omitempty,max=500" maxLength:"500" description:"富友API地址"`
FyNotifyURL *string `json:"fy_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"富友支付回调地址"`
// 支付宝配置字段(并存能力,不受 provider_type 限制)
AliAppID *string `json:"ali_app_id" validate:"omitempty,max=100" maxLength:"100" description:"支付宝应用ID"`
AliPrivateKey *string `json:"ali_private_key" validate:"omitempty" description:"支付宝应用私钥(PEM格式)"`
AliPublicKey *string `json:"ali_public_key" validate:"omitempty" description:"支付宝公钥(PEM格式用于验签)"`
AliNotifyURL *string `json:"ali_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝异步通知地址"`
AliReturnURL *string `json:"ali_return_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝同步跳转地址"`
AliProduction *bool `json:"ali_production" description:"是否生产环境 (true:生产, false:沙箱)"`
AliPayExpireMinutes *int `json:"ali_pay_expire_minutes" validate:"omitempty,min=1,max=1440" minimum:"1" maximum:"1440" description:"支付过期分钟数"`
}
// UpdateWechatConfigParams 更新微信参数配置聚合参数 (用于文档生成)
@@ -80,7 +98,7 @@ type UpdateWechatConfigParams struct {
type WechatConfigListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
ProviderType *string `json:"provider_type" query:"provider_type" validate:"omitempty,oneof=wechat fuiou" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
ProviderType *string `json:"provider_type" query:"provider_type" validate:"omitempty,oneof=wechat wechat_v2 fuiou" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
IsActive *bool `json:"is_active" query:"is_active" description:"是否激活 (true:已激活, false:未激活)"`
}
@@ -89,7 +107,7 @@ type WechatConfigResponse struct {
ID uint `json:"id" description:"配置ID"`
Name string `json:"name" description:"配置名称"`
Description string `json:"description" description:"配置描述"`
ProviderType string `json:"provider_type" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
ProviderType string `json:"provider_type" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
IsActive bool `json:"is_active" description:"是否激活"`
OaAppID string `json:"oa_app_id" description:"公众号AppID"`
@@ -117,6 +135,15 @@ type WechatConfigResponse struct {
FyAPIURL string `json:"fy_api_url" description:"富友API地址"`
FyNotifyURL string `json:"fy_notify_url" description:"富友支付回调地址"`
// 支付宝配置字段(敏感字段已脱敏)
AliAppID string `json:"ali_app_id" description:"支付宝应用ID"`
AliPrivateKey string `json:"ali_private_key" description:"支付宝应用私钥(配置状态)"`
AliPublicKey string `json:"ali_public_key" description:"支付宝公钥(配置状态)"`
AliNotifyURL string `json:"ali_notify_url" description:"支付宝异步通知地址"`
AliReturnURL string `json:"ali_return_url" description:"支付宝同步跳转地址"`
AliProduction bool `json:"ali_production" description:"是否生产环境"`
AliPayExpireMinutes int `json:"ali_pay_expire_minutes" description:"支付过期分钟数"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
}
@@ -180,6 +207,14 @@ func FromWechatConfigModel(m *model.WechatConfig) *WechatConfigResponse {
FyAPIURL: m.FyAPIURL,
FyNotifyURL: m.FyNotifyURL,
AliAppID: m.AliAppID,
AliPrivateKey: MaskLongSecret(m.AliPrivateKey),
AliPublicKey: MaskLongSecret(m.AliPublicKey),
AliNotifyURL: m.AliNotifyURL,
AliReturnURL: m.AliReturnURL,
AliProduction: m.AliProduction,
AliPayExpireMinutes: m.AliPayExpireMinutes,
CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: m.UpdatedAt.Format("2006-01-02 15:04:05"),
}

View File

@@ -17,6 +17,7 @@ type Payment struct {
PaymentConfigID *uint `gorm:"column:payment_config_id" json:"payment_config_id,omitempty"`
PaymentVoucherKey string `gorm:"column:payment_voucher_key;type:varchar(500)" json:"payment_voucher_key,omitempty"`
PaidAt *time.Time `gorm:"column:paid_at" json:"paid_at,omitempty"`
ExpireAt *time.Time `gorm:"column:expire_at" json:"expire_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`

View File

@@ -9,6 +9,9 @@ const (
ProviderTypeFuiou = "fuiou" // 富友
)
// 支付宝支付过期默认分钟数
const DefaultAliPayExpireMinutes = 30
// WechatConfig 微信参数配置模型
// 管理微信公众号 OAuth、小程序、微信直连支付、富友支付等多套配置
// 同一时间只有一条记录处于 is_active=true全局唯一生效配置
@@ -49,6 +52,15 @@ type WechatConfig struct {
FyPublicKey string `gorm:"column:fy_public_key;type:text;default:'';comment:富友公钥" json:"fy_public_key"`
FyAPIURL string `gorm:"column:fy_api_url;type:varchar(500);default:'';comment:富友API地址" json:"fy_api_url"`
FyNotifyURL string `gorm:"column:fy_notify_url;type:varchar(500);default:'';comment:富友支付回调地址" json:"fy_notify_url"`
// 支付-支付宝(与微信/富友并存provider_type 不新增 alipay
AliAppID string `gorm:"column:ali_app_id;type:varchar(100);not null;default:'';comment:支付宝应用ID" json:"ali_app_id"`
AliPrivateKey string `gorm:"column:ali_private_key;type:text;not null;default:'';comment:支付宝应用私钥(敏感)" json:"ali_private_key"`
AliPublicKey string `gorm:"column:ali_public_key;type:text;not null;default:'';comment:支付宝公钥(用于验签)" json:"ali_public_key"`
AliNotifyURL string `gorm:"column:ali_notify_url;type:varchar(500);not null;default:'';comment:支付宝异步通知地址" json:"ali_notify_url"`
AliReturnURL string `gorm:"column:ali_return_url;type:varchar(500);not null;default:'';comment:支付宝同步跳转地址" json:"ali_return_url"`
AliProduction bool `gorm:"column:ali_production;type:boolean;not null;default:false;comment:是否生产环境" json:"ali_production"`
AliPayExpireMinutes int `gorm:"column:ali_pay_expire_minutes;type:integer;not null;default:30;comment:支付过期分钟数" json:"ali_pay_expire_minutes"`
}
// TableName 指定表名

View File

@@ -103,6 +103,18 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateWechatConfigRequest
FyPublicKey: req.FyPublicKey,
FyAPIURL: req.FyAPIURL,
FyNotifyURL: req.FyNotifyURL,
AliAppID: req.AliAppID,
AliPrivateKey: req.AliPrivateKey,
AliPublicKey: req.AliPublicKey,
AliNotifyURL: req.AliNotifyURL,
AliReturnURL: req.AliReturnURL,
AliProduction: req.AliProduction,
AliPayExpireMinutes: req.AliPayExpireMinutes,
}
// 支付宝过期分钟数默认值
if config.AliPayExpireMinutes == 0 {
config.AliPayExpireMinutes = model.DefaultAliPayExpireMinutes
}
config.Creator = middleware.GetUserIDFromContext(ctx)
@@ -229,6 +241,19 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateWechatConf
s.mergeStringField(&config.FyAPIURL, req.FyAPIURL)
s.mergeStringField(&config.FyNotifyURL, req.FyNotifyURL)
// 支付宝支付
s.mergeStringField(&config.AliAppID, req.AliAppID)
s.mergeSensitiveField(&config.AliPrivateKey, req.AliPrivateKey)
s.mergeSensitiveField(&config.AliPublicKey, req.AliPublicKey)
s.mergeStringField(&config.AliNotifyURL, req.AliNotifyURL)
s.mergeStringField(&config.AliReturnURL, req.AliReturnURL)
if req.AliProduction != nil {
config.AliProduction = *req.AliProduction
}
if req.AliPayExpireMinutes != nil && *req.AliPayExpireMinutes > 0 {
config.AliPayExpireMinutes = *req.AliPayExpireMinutes
}
config.Updater = middleware.GetUserIDFromContext(ctx)
if err := s.store.Update(ctx, config); err != nil {

View File

@@ -150,3 +150,18 @@ func (s *PaymentStore) GetByOrderIDAndStatus(ctx context.Context, orderID uint,
}
return &payment, nil
}
// FindLatestPendingByOrderAndMethod 按业务单、支付方式查找最新一条待支付记录
// 用于支付宝支付单复用:同一业务单同一支付方式只允许一个有效 pending 支付单
func (s *PaymentStore) FindLatestPendingByOrderAndMethod(ctx context.Context, orderID uint, orderType string, paymentMethod string) (*model.Payment, error) {
var payment model.Payment
err := s.db.WithContext(ctx).
Where("order_id = ? AND order_type = ? AND payment_method = ? AND status = ?",
orderID, orderType, paymentMethod, model.PaymentRecordStatusPending).
Order("created_at DESC").
First(&payment).Error
if err != nil {
return nil, err
}
return &payment, nil
}

View File

@@ -0,0 +1,16 @@
-- 回滚:删除新增的索引
DROP INDEX IF EXISTS idx_tb_payment_order_method_status;
-- 回滚:删除 tb_payment 的 expire_at 字段
ALTER TABLE tb_payment
DROP COLUMN IF EXISTS expire_at;
-- 回滚:删除 tb_wechat_config 的支付宝字段
ALTER TABLE tb_wechat_config
DROP COLUMN IF EXISTS ali_app_id,
DROP COLUMN IF EXISTS ali_private_key,
DROP COLUMN IF EXISTS ali_public_key,
DROP COLUMN IF EXISTS ali_notify_url,
DROP COLUMN IF EXISTS ali_return_url,
DROP COLUMN IF EXISTS ali_production,
DROP COLUMN IF EXISTS ali_pay_expire_minutes;

View File

@@ -0,0 +1,17 @@
-- 新增支付宝配置字段到 tb_wechat_config
ALTER TABLE tb_wechat_config
ADD COLUMN IF NOT EXISTS ali_app_id varchar(100) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS ali_private_key text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS ali_public_key text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS ali_notify_url varchar(500) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS ali_return_url varchar(500) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS ali_production boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS ali_pay_expire_minutes integer NOT NULL DEFAULT 30;
-- 新增支付单过期时间字段
ALTER TABLE tb_payment
ADD COLUMN IF NOT EXISTS expire_at timestamptz;
-- 新增索引:按业务单+支付方式+状态查询
CREATE INDEX IF NOT EXISTS idx_tb_payment_order_method_status
ON tb_payment (order_id, order_type, payment_method, status, created_at DESC);

88
pkg/alipay/amount.go Normal file
View File

@@ -0,0 +1,88 @@
// Package alipay 支付宝支付能力封装
// 提供 client 构建、金额转换、WAP 支付链接生成、回调验签等辅助函数
package alipay
import (
"fmt"
"strings"
)
// FenToYuan 将分(整数)转换为元字符串,精确到小数点后两位。
// 例如123 -> "1.23"100 -> "1.00"1 -> "0.01"
func FenToYuan(amountFen int64) string {
// 整除得元,取余得分
yuan := amountFen / 100
fen := amountFen % 100
if fen < 0 {
fen = -fen
}
return fmt.Sprintf("%d.%02d", yuan, fen)
}
// YuanToFen 将元字符串转换为分(整数)。
// 只接受合法金额格式:最多两位小数,纯数字加可选小数点,拒绝科学计数法和负数。
// 例如:"1.23" -> 123"1.00" -> 100"1" -> 100
func YuanToFen(value string) (int64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, fmt.Errorf("金额字符串不能为空")
}
// 禁止负数
if strings.HasPrefix(value, "-") {
return 0, fmt.Errorf("金额不能为负数: %s", value)
}
// 禁止科学计数法
if strings.ContainsAny(value, "eE") {
return 0, fmt.Errorf("金额格式非法,禁止科学计数法: %s", value)
}
parts := strings.Split(value, ".")
if len(parts) > 2 {
return 0, fmt.Errorf("金额格式非法: %s", value)
}
// 校验整数部分
yuanStr := parts[0]
if !isDigits(yuanStr) {
return 0, fmt.Errorf("金额格式非法: %s", value)
}
// 解析整数部分
var yuan int64
for _, c := range yuanStr {
yuan = yuan*10 + int64(c-'0')
}
var fen int64
if len(parts) == 2 {
fenStr := parts[1]
if len(fenStr) == 0 || len(fenStr) > 2 {
return 0, fmt.Errorf("金额小数位数非法(最多两位): %s", value)
}
if !isDigits(fenStr) {
return 0, fmt.Errorf("金额格式非法: %s", value)
}
// 补齐到两位
if len(fenStr) == 1 {
fenStr += "0"
}
for _, c := range fenStr {
fen = fen*10 + int64(c-'0')
}
}
return yuan*100 + fen, nil
}
// isDigits 判断字符串是否全为数字字符
func isDigits(s string) bool {
if s == "" {
return false
}
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}

36
pkg/alipay/client.go Normal file
View File

@@ -0,0 +1,36 @@
package alipay
import (
"fmt"
"github.com/smartwalle/alipay/v3"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// NewClientFromConfig 从 WechatConfig 构建支付宝 SDK client。
// 校验 ali_app_id、ali_private_key、ali_public_key 必须非空。
// ali_production=true 时使用生产环境,否则使用沙箱环境。
func NewClientFromConfig(cfg *model.WechatConfig) (*alipay.Client, error) {
if cfg.AliAppID == "" {
return nil, fmt.Errorf("支付宝配置缺失ali_app_id 不能为空")
}
if cfg.AliPrivateKey == "" {
return nil, fmt.Errorf("支付宝配置缺失ali_private_key 不能为空")
}
if cfg.AliPublicKey == "" {
return nil, fmt.Errorf("支付宝配置缺失ali_public_key 不能为空")
}
client, err := alipay.New(cfg.AliAppID, cfg.AliPrivateKey, cfg.AliProduction)
if err != nil {
return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
}
// 加载支付宝公钥(公钥模式验签)
if err := client.LoadAliPayPublicKey(cfg.AliPublicKey); err != nil {
return nil, fmt.Errorf("加载支付宝公钥失败: %w", err)
}
return client, nil
}

34
pkg/alipay/notify.go Normal file
View File

@@ -0,0 +1,34 @@
package alipay
import (
"context"
"fmt"
"net/url"
"github.com/smartwalle/alipay/v3"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// DecodeNotification 对支付宝异步回调进行验签并解析通知内容。
// values 为原始 form body 构建的 url.Values调用方负责解析请求体。
// 验签使用创建支付单时的 WechatConfig 中的 ali_public_key。
// 验签失败时返回 error不推进业务。
func DecodeNotification(ctx context.Context, cfg *model.WechatConfig, values url.Values) (*alipay.Notification, error) {
client, err := NewClientFromConfig(cfg)
if err != nil {
return nil, fmt.Errorf("构建支付宝客户端失败: %w", err)
}
notification, err := client.DecodeNotification(ctx, values)
if err != nil {
return nil, fmt.Errorf("支付宝回调验签失败: %w", err)
}
return notification, nil
}
// TradeStatusSuccess 判断交易状态是否为支付成功TRADE_SUCCESS 或 TRADE_FINISHED
func TradeStatusSuccess(status alipay.TradeStatus) bool {
return status == alipay.TradeStatusSuccess || status == alipay.TradeStatusFinished
}

47
pkg/alipay/wap.go Normal file
View File

@@ -0,0 +1,47 @@
package alipay
import (
"context"
"fmt"
"time"
"github.com/smartwalle/alipay/v3"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// BuildWapPayURL 生成支付宝手机网站支付 URL。
// payment.PaymentNo 作为 out_trade_nopayment.Amount转为元字符串
// payment.ExpireAt 若非空则转换为支付宝 TimeExpire 格式yyyy-MM-dd HH:mm
// 返回签名后的完整 WAP 支付 URLqr_link 与 copy_link 可使用同一个 URL。
func BuildWapPayURL(ctx context.Context, cfg *model.WechatConfig, payment *model.Payment, subject string) (string, error) {
client, err := NewClientFromConfig(cfg)
if err != nil {
return "", err
}
totalAmount := FenToYuan(payment.Amount)
param := alipay.TradeWapPay{
Trade: alipay.Trade{
NotifyURL: cfg.AliNotifyURL,
ReturnURL: cfg.AliReturnURL,
Subject: subject,
OutTradeNo: payment.PaymentNo,
TotalAmount: totalAmount,
ProductCode: "QUICK_WAP_WAY",
},
}
// 设置支付链接过期时间
if payment.ExpireAt != nil && payment.ExpireAt.After(time.Now()) {
param.TimeExpire = payment.ExpireAt.Format("2006-01-02 15:04")
}
payURL, err := client.TradeWapPay(param)
if err != nil {
return "", fmt.Errorf("生成支付宝 WAP 支付链接失败: %w", err)
}
return payURL.String(), nil
}