task: 完成支付宝C端入口
新增支付宝支付方式支持: - DTO:ClientPaymentLink、ClientPayOrderRequest/Response 增加 alipay;ClientCreateOrderRequest 增加 payment_method;ClientCreateOrderResponse 增加 payment_link;ClientCreateRechargeRequest 允许 alipay,app_type 改为微信必填;ClientRechargeResponse PayConfig 改为可选指针,增加 payment_link - Service(client_order):PayOrder 新增 alipay 分支(不需要 app_type/OpenID,复用或新建 pending payment,生成 WAP URL);CreateOrder 强充路径按 payment_method 分支,alipay 不需要 app_type/OpenID;幂等命中时支付宝强充返回同一充值单对应的 payment_link;新增 getOrBuildAlipayPaymentLink(过期 payment 自动续建);新增 createAlipayForceRechargeOrder(事务创建充值单+支付单,WAP URL 生成失败则标记 failed) - Handler(client_wallet):CreateRecharge 移除微信唯一限制,按 payment_method 分发到 createWechatRecharge / createAlipayRecharge;alipay 分支事务内创建充值单+支付单,然后生成 WAP URL,失败标记 payment failed - 依赖 worker-1 共享 API:pkg/alipay.BuildWapPayURL/CalcExpireAt、Payment.ExpireAt、WechatConfig.AliReturnURL/AliPayExpireMinutes、PaymentStore.FindLatestPendingByOrderAndMethod;本 worktree 暂不能独立构建,待 leader 集成 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
|
||||
@@ -262,10 +263,6 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.PaymentMethod != constants.RechargeMethodWechat {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -289,6 +286,23 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
|
||||
switch req.PaymentMethod {
|
||||
case constants.RechargeMethodAlipay:
|
||||
return h.createAlipayRecharge(c, resolved, config, wallet, req)
|
||||
default:
|
||||
// 微信充值(默认):app_type 必填
|
||||
return h.createWechatRecharge(c, resolved, config, wallet, req)
|
||||
}
|
||||
}
|
||||
|
||||
// createWechatRecharge 微信充值分支
|
||||
func (h *ClientWalletHandler) createWechatRecharge(
|
||||
c *fiber.Ctx,
|
||||
resolved *resolvedWalletAssetContext,
|
||||
config *model.WechatConfig,
|
||||
wallet *model.AssetWallet,
|
||||
req dto.ClientCreateRechargeRequest,
|
||||
) error {
|
||||
appID, err := pickAppIDByType(config, req.AppType)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -350,17 +364,99 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||||
}
|
||||
|
||||
resp := &dto.ClientRechargeResponse{
|
||||
return response.Success(c, &dto.ClientRechargeResponse{
|
||||
Recharge: dto.ClientRechargeResult{
|
||||
RechargeID: rechargeOrder.ID,
|
||||
RechargeNo: rechargeOrder.RechargeOrderNo,
|
||||
Amount: rechargeOrder.Amount,
|
||||
Status: rechargeOrder.Status,
|
||||
},
|
||||
PayConfig: payConfig,
|
||||
PayConfig: &payConfig,
|
||||
})
|
||||
}
|
||||
|
||||
// createAlipayRecharge 支付宝充值分支。
|
||||
// WAP URL 为本地签名生成,事务内先写 DB,再生成链接;链接失败则标记 payment failed。
|
||||
func (h *ClientWalletHandler) createAlipayRecharge(
|
||||
c *fiber.Ctx,
|
||||
resolved *resolvedWalletAssetContext,
|
||||
config *model.WechatConfig,
|
||||
wallet *model.AssetWallet,
|
||||
req dto.ClientCreateRechargeRequest,
|
||||
) error {
|
||||
rechargeNo := generateClientRechargeNo()
|
||||
paymentNo := generateClientPaymentNo()
|
||||
expireAt := alipay.CalcExpireAt(config)
|
||||
|
||||
rechargeOrder := &model.RechargeOrder{
|
||||
RechargeOrderNo: rechargeNo,
|
||||
UserID: resolved.CustomerID,
|
||||
AssetWalletID: wallet.ID,
|
||||
ResourceType: resolved.ResourceType,
|
||||
ResourceID: resolved.Asset.AssetID,
|
||||
Amount: req.Amount,
|
||||
Status: model.RechargeOrderStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
OperatorType: constants.OperatorTypePersonalCustomer,
|
||||
Generation: resolved.Generation,
|
||||
}
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: req.Amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
ExpireAt: &expireAt,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
// WAP URL 是本地签名,不需要先调第三方,在事务内创建充值单和支付单
|
||||
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
payment.OrderID = rechargeOrder.ID
|
||||
return h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wapURL, err := alipay.BuildWapPayURL(resolved.SkipPermissionCtx, config, payment, "资产钱包充值", config.AliReturnURL)
|
||||
if err != nil {
|
||||
if updateErr := h.paymentStore.UpdateStatus(resolved.SkipPermissionCtx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil {
|
||||
h.logger.Warn("标记支付宝支付单 failed 失败",
|
||||
zap.String("payment_no", paymentNo),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("支付宝充值支付单已创建",
|
||||
zap.String("payment_no", paymentNo),
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int64("amount", req.Amount),
|
||||
zap.Time("expire_at", expireAt),
|
||||
zap.Uint("config_id", config.ID),
|
||||
)
|
||||
|
||||
expireStr := expireAt.Format(time.RFC3339)
|
||||
return response.Success(c, &dto.ClientRechargeResponse{
|
||||
Recharge: dto.ClientRechargeResult{
|
||||
RechargeID: rechargeOrder.ID,
|
||||
RechargeNo: rechargeOrder.RechargeOrderNo,
|
||||
Amount: rechargeOrder.Amount,
|
||||
Status: rechargeOrder.Status,
|
||||
},
|
||||
PaymentLink: &dto.ClientPaymentLink{
|
||||
PaymentNo: paymentNo,
|
||||
QRLink: wapURL,
|
||||
CopyLink: wapURL,
|
||||
PayExpireAt: expireStr,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRechargeList C5 充值记录列表
|
||||
|
||||
@@ -8,8 +8,10 @@ package dto
|
||||
type ClientCreateOrderRequest struct {
|
||||
Identifier string `json:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)"`
|
||||
PackageIDs []uint `json:"package_ids" validate:"required,min=1,dive,gt=0" required:"true" description:"套餐ID列表"`
|
||||
// app_type 仅强充场景需要(服务端检测到强充时会直接发起微信支付);普通下单时可不传
|
||||
AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(强充必传)(official_account:公众号, miniapp:小程序)"`
|
||||
// PaymentMethod 指定支付方式;强充场景必传。wechat 时 app_type 也必传,alipay 时不需要 app_type
|
||||
PaymentMethod string `json:"payment_method" validate:"omitempty,oneof=wechat alipay" description:"支付方式(强充必传)(wechat:微信, alipay:支付宝)"`
|
||||
// AppType 仅强充 + 微信支付场景必传;支付宝强充无需传
|
||||
AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(微信强充必传)(official_account:公众号, miniapp:小程序)"`
|
||||
}
|
||||
|
||||
// ClientCreateOrderResponse D1 客户端创建订单响应
|
||||
@@ -17,7 +19,8 @@ type ClientCreateOrderResponse struct {
|
||||
OrderType string `json:"order_type" description:"订单类型 (package:套餐订单, recharge:充值订单)"`
|
||||
Order *ClientOrderInfo `json:"order,omitempty" description:"套餐订单信息(普通下单时返回,无支付参数,需单独调支付接口)"`
|
||||
Recharge *ClientRechargeInfo `json:"recharge,omitempty" description:"充值订单信息"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(仅强充场景返回)"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(强充微信支付时返回)"`
|
||||
PaymentLink *ClientPaymentLink `json:"payment_link,omitempty" description:"支付宝支付链接(强充支付宝支付时返回)"`
|
||||
LinkedPackageInfo *LinkedPackageInfo `json:"linked_package_info,omitempty" description:"关联套餐信息"`
|
||||
Idempotent bool `json:"idempotent,omitempty" description:"幂等标志(true 表示返回的是已存在的待支付充值订单)"`
|
||||
}
|
||||
@@ -122,17 +125,26 @@ type ClientOrderPackageItem struct {
|
||||
// D4 订单支付
|
||||
// ========================================
|
||||
|
||||
// ClientPaymentLink C 端支付宝支付链接
|
||||
type ClientPaymentLink struct {
|
||||
PaymentNo string `json:"payment_no" description:"支付单号"`
|
||||
QRLink string `json:"qr_link" description:"二维码内容链接,前端用该链接生成二维码"`
|
||||
CopyLink string `json:"copy_link" description:"复制到普通浏览器打开的支付链接(与 qr_link 相同)"`
|
||||
PayExpireAt string `json:"pay_expire_at" description:"支付链接过期时间(RFC3339 格式)"`
|
||||
}
|
||||
|
||||
// ClientPayOrderRequest D4 订单支付请求
|
||||
type ClientPayOrderRequest struct {
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet wechat" required:"true" description:"支付方式 (wallet:钱包, wechat:微信)"`
|
||||
// AppType 仅 payment_method=wechat 时必填
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet wechat alipay" required:"true" description:"支付方式 (wallet:钱包, wechat:微信, alipay:支付宝)"`
|
||||
// AppType 仅 payment_method=wechat 时必填,alipay 无需传
|
||||
AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(微信支付必传)(official_account:公众号, miniapp:小程序)"`
|
||||
}
|
||||
|
||||
// ClientPayOrderResponse D4 订单支付响应
|
||||
type ClientPayOrderResponse struct {
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(payment_method=wechat 时返回)"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(payment_method=wechat 时返回)"`
|
||||
PaymentLink *ClientPaymentLink `json:"payment_link,omitempty" description:"支付宝支付链接(payment_method=alipay 时返回)"`
|
||||
}
|
||||
|
||||
// ClientPayOrderParams D4 订单支付文档参数(路径参数 + 请求体,仅供文档生成器使用)
|
||||
|
||||
@@ -77,15 +77,17 @@ type ClientRechargeCheckResponse struct {
|
||||
// ClientCreateRechargeRequest C4 创建充值订单请求
|
||||
type ClientCreateRechargeRequest struct {
|
||||
Identifier string `json:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)"`
|
||||
Amount int64 `json:"amount" validate:"required,min=100,max=10000000" required:"true" minimum:"100" maximum:"10000000" description:"充值金额(分)"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat" required:"true" description:"支付方式 (wechat:微信支付)"`
|
||||
AppType string `json:"app_type" validate:"required,oneof=official_account miniapp" required:"true" description:"应用类型 (official_account:公众号, miniapp:小程序)"`
|
||||
Amount int64 `json:"amount" validate:"required,min=1,max=10000000" required:"true" minimum:"1" maximum:"10000000" description:"充值金额(分)"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat alipay" required:"true" description:"支付方式 (wechat:微信支付, alipay:支付宝)"`
|
||||
// AppType 仅 payment_method=wechat 时必填,alipay 无需传
|
||||
AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(微信支付必传)(official_account:公众号, miniapp:小程序)"`
|
||||
}
|
||||
|
||||
// ClientRechargeResponse C4 创建充值订单响应
|
||||
type ClientRechargeResponse struct {
|
||||
Recharge ClientRechargeResult `json:"recharge" description:"充值信息"`
|
||||
PayConfig ClientRechargePayConfig `json:"pay_config" description:"支付配置"`
|
||||
Recharge ClientRechargeResult `json:"recharge" description:"充值信息"`
|
||||
PayConfig *ClientRechargePayConfig `json:"pay_config,omitempty" description:"微信支付配置(payment_method=wechat 时返回)"`
|
||||
PaymentLink *ClientPaymentLink `json:"payment_link,omitempty" description:"支付宝支付链接(payment_method=alipay 时返回)"`
|
||||
}
|
||||
|
||||
// ClientRechargeResult C4 充值信息
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/bytedance/sonic"
|
||||
@@ -178,7 +179,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
if err == nil && strings.HasPrefix(existingValue, "CRCH") {
|
||||
existingOrder, queryErr := s.rechargeOrderStore.GetByRechargeOrderNo(skipCtx, existingValue)
|
||||
if queryErr == nil && existingOrder.Status == model.RechargeOrderStatusPending {
|
||||
return &dto.ClientCreateOrderResponse{
|
||||
resp := &dto.ClientCreateOrderResponse{
|
||||
OrderType: "recharge",
|
||||
Recharge: &dto.ClientRechargeInfo{
|
||||
RechargeID: existingOrder.ID,
|
||||
@@ -189,7 +190,23 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
AutoPurchaseStatus: existingOrder.AutoPurchaseStatus,
|
||||
},
|
||||
Idempotent: true,
|
||||
}, nil
|
||||
}
|
||||
// 支付宝强充幂等:复用已有待支付 payment 或生成新链接
|
||||
if req.PaymentMethod == model.PaymentMethodAlipay {
|
||||
paymentLink, linkErr := s.getOrBuildAlipayPaymentLink(
|
||||
skipCtx, existingOrder.ID, model.PaymentOrderTypeRecharge,
|
||||
existingOrder.Amount, "余额充值",
|
||||
)
|
||||
if linkErr != nil {
|
||||
s.logger.Warn("强充幂等复用支付宝链接失败",
|
||||
zap.String("recharge_no", existingOrder.RechargeOrderNo),
|
||||
zap.Error(linkErr),
|
||||
)
|
||||
} else {
|
||||
resp.PaymentLink = paymentLink
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
if queryErr == nil {
|
||||
_ = s.redis.Del(skipCtx, redisKey).Err()
|
||||
@@ -209,7 +226,18 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
}()
|
||||
|
||||
if forceRecharge.NeedForceRecharge {
|
||||
// 强充场景需要微信支付,此时 app_type 必须传入
|
||||
if req.PaymentMethod == model.PaymentMethodAlipay {
|
||||
// 支付宝强充:不需要 app_type / OpenID
|
||||
activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付配置失败")
|
||||
}
|
||||
if activeConfig == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "未找到生效的支付配置")
|
||||
}
|
||||
return s.createAlipayForceRechargeOrder(skipCtx, customerID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, &created)
|
||||
}
|
||||
// 微信强充:app_type 必须传入
|
||||
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -813,6 +841,214 @@ func generateClientPaymentNo() string {
|
||||
return fmt.Sprintf("PAY%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000))
|
||||
}
|
||||
|
||||
// getOrBuildAlipayPaymentLink 查找未过期的支付宝待支付单并返回链接;
|
||||
// 过期则标记 failed 并创建新单;新建失败则整体返回错误。
|
||||
func (s *Service) getOrBuildAlipayPaymentLink(
|
||||
ctx context.Context,
|
||||
orderID uint,
|
||||
orderType string,
|
||||
amount int64,
|
||||
subject string,
|
||||
) (*dto.ClientPaymentLink, error) {
|
||||
activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付配置失败")
|
||||
}
|
||||
if activeConfig == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "未找到生效的支付配置")
|
||||
}
|
||||
|
||||
// 查找最新的 alipay 待支付单
|
||||
existing, _ := s.paymentStore.FindLatestPendingByOrderAndMethod(
|
||||
ctx, orderID, orderType, model.PaymentByAlipay,
|
||||
)
|
||||
|
||||
var payment *model.Payment
|
||||
now := time.Now()
|
||||
if existing != nil && existing.ExpireAt != nil && existing.ExpireAt.After(now) {
|
||||
// 复用未过期的支付单
|
||||
payment = existing
|
||||
} else {
|
||||
// 过期或不存在,标记旧单 failed 并新建
|
||||
if existing != nil {
|
||||
if updateErr := s.paymentStore.UpdateStatus(ctx, existing.ID, model.PaymentRecordStatusFailed); updateErr != nil {
|
||||
s.logger.Warn("标记过期支付宝支付单 failed 失败",
|
||||
zap.Uint("payment_id", existing.ID),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
expireAt := alipay.CalcExpireAt(activeConfig)
|
||||
newPayment := &model.Payment{
|
||||
PaymentNo: generateClientPaymentNo(),
|
||||
OrderID: orderID,
|
||||
OrderType: orderType,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
ExpireAt: &expireAt,
|
||||
}
|
||||
if err := s.paymentStore.Create(ctx, newPayment); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付宝支付单失败")
|
||||
}
|
||||
payment = newPayment
|
||||
s.logger.Info("创建支付宝支付单",
|
||||
zap.String("payment_no", payment.PaymentNo),
|
||||
zap.String("order_type", orderType),
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.Int64("amount", amount),
|
||||
zap.Time("expire_at", expireAt),
|
||||
zap.Uint("config_id", activeConfig.ID),
|
||||
)
|
||||
}
|
||||
|
||||
wapURL, err := alipay.BuildWapPayURL(ctx, activeConfig, payment, subject, activeConfig.AliReturnURL)
|
||||
if err != nil {
|
||||
// 新建的 payment 生成链接失败,标记 failed
|
||||
if existing == nil || payment.ID != existing.ID {
|
||||
_ = s.paymentStore.UpdateStatus(ctx, payment.ID, model.PaymentRecordStatusFailed)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expireStr := ""
|
||||
if payment.ExpireAt != nil {
|
||||
expireStr = payment.ExpireAt.Format(time.RFC3339)
|
||||
}
|
||||
return &dto.ClientPaymentLink{
|
||||
PaymentNo: payment.PaymentNo,
|
||||
QRLink: wapURL,
|
||||
CopyLink: wapURL,
|
||||
PayExpireAt: expireStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createAlipayForceRechargeOrder 强充场景下创建支付宝充值单并返回支付链接。
|
||||
func (s *Service) createAlipayForceRechargeOrder(
|
||||
ctx context.Context,
|
||||
customerID uint,
|
||||
assetInfo *dto.AssetResolveResponse,
|
||||
validationResult *purchase_validation.PurchaseValidationResult,
|
||||
activeConfig *model.WechatConfig,
|
||||
forceRecharge *ForceRechargeRequirement,
|
||||
redisKey string,
|
||||
created *bool,
|
||||
) (*dto.ClientCreateOrderResponse, error) {
|
||||
resourceType, resourceID, err := resolveWalletResource(validationResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wallet, err := s.getOrCreateWallet(ctx, resourceType, resourceID, resolveSellerShopID(validationResult))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
linkedPackageIDs, err := sonic.Marshal(extractPackageIDs(validationResult.Packages))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化关联套餐失败")
|
||||
}
|
||||
|
||||
rechargeOrderNo := generateClientRechargeNo()
|
||||
var iotCardID *uint
|
||||
var deviceID *uint
|
||||
if validationResult.Card != nil {
|
||||
iotCardID = &validationResult.Card.ID
|
||||
}
|
||||
if validationResult.Device != nil {
|
||||
deviceID = &validationResult.Device.ID
|
||||
}
|
||||
|
||||
rechargeOrder := &model.RechargeOrder{
|
||||
RechargeOrderNo: rechargeOrderNo,
|
||||
UserID: customerID,
|
||||
AssetWalletID: wallet.ID,
|
||||
ResourceType: resourceType,
|
||||
ResourceID: resourceID,
|
||||
IotCardID: iotCardID,
|
||||
DeviceID: deviceID,
|
||||
Amount: forceRecharge.ForceRechargeAmount,
|
||||
Status: model.RechargeOrderStatusPending,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
OperatorType: "personal_customer",
|
||||
Generation: resolveGeneration(validationResult),
|
||||
LinkedPackageIDs: datatypes.JSON(linkedPackageIDs),
|
||||
LinkedOrderType: resolveOrderType(validationResult),
|
||||
LinkedCarrierType: assetInfo.AssetType,
|
||||
LinkedCarrierID: &resourceID,
|
||||
AutoPurchaseStatus: model.AutoPurchaseStatusPending,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
}
|
||||
|
||||
expireAt := alipay.CalcExpireAt(activeConfig)
|
||||
paymentNo := generateClientPaymentNo()
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: forceRecharge.ForceRechargeAmount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
ExpireAt: &expireAt,
|
||||
}
|
||||
|
||||
// 事务创建充值单 + 支付单(WAP URL 本地签名,不需要先调第三方)
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.rechargeOrderStore.CreateWithTx(ctx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
payment.OrderID = rechargeOrder.ID
|
||||
return s.paymentStore.CreateWithTx(ctx, tx, payment)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wapURL, err := alipay.BuildWapPayURL(ctx, activeConfig, payment, "余额充值", activeConfig.AliReturnURL)
|
||||
if err != nil {
|
||||
if updateErr := s.paymentStore.UpdateStatus(ctx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil {
|
||||
s.logger.Warn("标记支付宝支付单 failed 失败",
|
||||
zap.String("payment_no", paymentNo),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("支付宝强充支付单已创建",
|
||||
zap.String("payment_no", paymentNo),
|
||||
zap.String("recharge_no", rechargeOrderNo),
|
||||
zap.Int64("amount", forceRecharge.ForceRechargeAmount),
|
||||
zap.Time("expire_at", expireAt),
|
||||
zap.Uint("config_id", activeConfig.ID),
|
||||
)
|
||||
|
||||
s.markClientPurchaseCreated(ctx, redisKey, rechargeOrderNo)
|
||||
*created = true
|
||||
|
||||
clientStatus := rechargeStatusToClientStatus(int(rechargeOrder.Status))
|
||||
expireStr := expireAt.Format(time.RFC3339)
|
||||
return &dto.ClientCreateOrderResponse{
|
||||
OrderType: "recharge",
|
||||
Recharge: &dto.ClientRechargeInfo{
|
||||
RechargeID: rechargeOrder.ID,
|
||||
RechargeNo: rechargeOrderNo,
|
||||
Amount: rechargeOrder.Amount,
|
||||
Status: clientStatus,
|
||||
StatusName: clientRechargeStatusName(clientStatus),
|
||||
AutoPurchaseStatus: rechargeOrder.AutoPurchaseStatus,
|
||||
},
|
||||
PaymentLink: &dto.ClientPaymentLink{
|
||||
PaymentNo: paymentNo,
|
||||
QRLink: wapURL,
|
||||
CopyLink: wapURL,
|
||||
PayExpireAt: expireStr,
|
||||
},
|
||||
LinkedPackageInfo: buildLinkedPackageInfo(validationResult, forceRecharge),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListOrders 查询 C 端订单列表。
|
||||
func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.ClientOrderListRequest) ([]dto.ClientOrderListItem, int64, error) {
|
||||
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
@@ -1058,6 +1294,36 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
||||
PayConfig: buildClientPayConfigFromResult(paymentResult),
|
||||
}, nil
|
||||
|
||||
case model.PaymentMethodAlipay:
|
||||
// 支付宝支付:不需要 app_type / OpenID
|
||||
activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付配置失败")
|
||||
}
|
||||
if activeConfig == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "未找到生效的支付配置")
|
||||
}
|
||||
|
||||
paymentLink, err := s.getOrBuildAlipayPaymentLink(
|
||||
skipCtx, orderID, model.PaymentOrderTypePackage, order.TotalAmount, "套餐购买",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 更新订单支付配置 ID,兼容回调配置回溯
|
||||
if dbErr := s.db.WithContext(skipCtx).Model(&model.Order{}).
|
||||
Where("id = ?", orderID).
|
||||
Update("payment_config_id", activeConfig.ID).Error; dbErr != nil {
|
||||
s.logger.Warn("更新订单支付配置 ID 失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.Error(dbErr),
|
||||
)
|
||||
}
|
||||
return &dto.ClientPayOrderResponse{
|
||||
PaymentMethod: model.PaymentMethodAlipay,
|
||||
PaymentLink: paymentLink,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, errors.New(errors.CodeInvalidParam, "不支持的支付方式")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user