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 (
|
||||
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