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:
2026-05-22 12:18:11 +08:00
parent 53e95751b1
commit 56adc30409
4 changed files with 398 additions and 22 deletions

View File

@@ -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, "不支持的支付方式")
}