feat: C端订单支付拆分 — 创建订单与发起支付分离
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
- CreateOrder 改造:普通下单只建单(无支付参数),强充场景保持原有微信支付一步完成 - 新增 POST /orders/:id/pay 统一支付入口,支持 wallet(钱包)和 wechat(微信 JSAPI) - 移除 POST /orders/:id/wallet-pay,合并至统一支付接口 - app_type 改为可选字段(强充场景必传,普通下单无需传) - CreateOrderResponse.pay_config 改为 omitempty(普通下单不返回支付参数)
This commit is contained in:
@@ -1597,7 +1597,7 @@ components:
|
||||
DtoClientCreateOrderRequest:
|
||||
properties:
|
||||
app_type:
|
||||
description: 应用类型 (official_account:公众号, miniapp:小程序)
|
||||
description: 应用类型(强充必传)(official_account:公众号, miniapp:小程序)
|
||||
type: string
|
||||
identifier:
|
||||
description: 资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)
|
||||
@@ -1614,7 +1614,6 @@ components:
|
||||
required:
|
||||
- identifier
|
||||
- package_ids
|
||||
- app_type
|
||||
type: object
|
||||
DtoClientCreateOrderResponse:
|
||||
properties:
|
||||
@@ -1849,6 +1848,25 @@ components:
|
||||
description: 时间戳
|
||||
type: string
|
||||
type: object
|
||||
DtoClientPayOrderParams:
|
||||
properties:
|
||||
app_type:
|
||||
description: 应用类型(微信支付必传)(official_account:公众号, miniapp:小程序)
|
||||
type: string
|
||||
payment_method:
|
||||
description: 支付方式 (wallet:钱包, wechat:微信)
|
||||
type: string
|
||||
required:
|
||||
- payment_method
|
||||
type: object
|
||||
DtoClientPayOrderResponse:
|
||||
properties:
|
||||
pay_config:
|
||||
$ref: '#/components/schemas/DtoClientPayConfig'
|
||||
payment_method:
|
||||
description: 支付方式
|
||||
type: string
|
||||
type: object
|
||||
DtoClientRechargeCheckResponse:
|
||||
properties:
|
||||
force_recharge_amount:
|
||||
@@ -22879,6 +22897,78 @@ paths:
|
||||
summary: 订单详情
|
||||
tags:
|
||||
- 个人客户 - 订单
|
||||
/api/c/v1/orders/{id}/pay:
|
||||
post:
|
||||
parameters:
|
||||
- description: ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoClientPayOrderParams'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
code:
|
||||
description: 响应码
|
||||
example: 0
|
||||
type: integer
|
||||
data:
|
||||
$ref: '#/components/schemas/DtoClientPayOrderResponse'
|
||||
msg:
|
||||
description: 响应消息
|
||||
example: success
|
||||
type: string
|
||||
timestamp:
|
||||
description: 时间戳
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
- msg
|
||||
- data
|
||||
- timestamp
|
||||
type: object
|
||||
description: 成功
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 订单支付
|
||||
tags:
|
||||
- 个人客户 - 订单
|
||||
/api/c/v1/orders/create:
|
||||
post:
|
||||
requestBody:
|
||||
|
||||
@@ -35,6 +35,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
personalCustomerDeviceStore,
|
||||
personalCustomerOpenIDStore,
|
||||
svc.WechatConfig,
|
||||
svc.Order,
|
||||
packageSeriesStore,
|
||||
shopSeriesAllocationStore,
|
||||
iotCardStore,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// ClientOrderHandler C 端订单处理器
|
||||
// 提供 D1~D3 下单、列表、详情接口。
|
||||
// 提供 D1~D4 下单、列表、详情、支付接口。
|
||||
type ClientOrderHandler struct {
|
||||
clientOrderService *clientorder.Service
|
||||
logger *zap.Logger
|
||||
@@ -83,6 +83,32 @@ func (h *ClientOrderHandler) ListOrders(c *fiber.Ctx) error {
|
||||
return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// PayOrder D4 订单支付。
|
||||
// POST /api/c/v1/orders/:id/pay
|
||||
func (h *ClientOrderHandler) PayOrder(c *fiber.Ctx) error {
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
orderID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || orderID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
var req dto.ClientPayOrderRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resp, err := h.clientOrderService.PayOrder(c.UserContext(), customerID, uint(orderID), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetOrderDetail D3 订单详情。
|
||||
// GET /api/c/v1/orders/:id
|
||||
func (h *ClientOrderHandler) GetOrderDetail(c *fiber.Ctx) error {
|
||||
|
||||
@@ -8,15 +8,16 @@ 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列表"`
|
||||
AppType string `json:"app_type" validate:"required,oneof=official_account miniapp" required:"true" description:"应用类型 (official_account:公众号, miniapp:小程序)"`
|
||||
// app_type 仅强充场景需要(服务端检测到强充时会直接发起微信支付);普通下单时可不传
|
||||
AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(强充必传)(official_account:公众号, miniapp:小程序)"`
|
||||
}
|
||||
|
||||
// ClientCreateOrderResponse D1 客户端创建订单响应
|
||||
type ClientCreateOrderResponse struct {
|
||||
OrderType string `json:"order_type" description:"订单类型 (package:套餐订单, recharge:充值订单)"`
|
||||
Order *ClientOrderInfo `json:"order,omitempty" description:"套餐订单信息"`
|
||||
Order *ClientOrderInfo `json:"order,omitempty" description:"套餐订单信息(普通下单时返回,无支付参数,需单独调支付接口)"`
|
||||
Recharge *ClientRechargeInfo `json:"recharge,omitempty" description:"充值订单信息"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config" description:"支付配置"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(仅强充场景返回)"`
|
||||
LinkedPackageInfo *LinkedPackageInfo `json:"linked_package_info,omitempty" description:"关联套餐信息"`
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ type ClientRechargeInfo struct {
|
||||
AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"`
|
||||
}
|
||||
|
||||
// ClientPayConfig D1 支付配置
|
||||
// ClientPayConfig 微信支付配置
|
||||
type ClientPayConfig struct {
|
||||
AppID string `json:"app_id" description:"应用ID"`
|
||||
Timestamp string `json:"timestamp" description:"时间戳"`
|
||||
@@ -111,3 +112,26 @@ type ClientOrderPackageItem struct {
|
||||
Price int64 `json:"price" description:"单价(分)"`
|
||||
Quantity int `json:"quantity" description:"数量"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// D4 订单支付
|
||||
// ========================================
|
||||
|
||||
// ClientPayOrderRequest D4 订单支付请求
|
||||
type ClientPayOrderRequest struct {
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet wechat" required:"true" description:"支付方式 (wallet:钱包, wechat:微信)"`
|
||||
// AppType 仅 payment_method=wechat 时必填
|
||||
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 时返回)"`
|
||||
}
|
||||
|
||||
// ClientPayOrderParams D4 订单支付文档参数(路径参数 + 请求体,仅供文档生成器使用)
|
||||
type ClientPayOrderParams struct {
|
||||
IDReq
|
||||
ClientPayOrderRequest
|
||||
}
|
||||
|
||||
@@ -214,6 +214,14 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
||||
Output: &dto.ClientOrderDetailResponse{},
|
||||
})
|
||||
|
||||
Register(authGroup, doc, basePath, "POST", "/orders/:id/pay", handlers.ClientOrder.PayOrder, RouteSpec{
|
||||
Summary: "订单支付",
|
||||
Tags: []string{"个人客户 - 订单"},
|
||||
Auth: true,
|
||||
Input: &dto.ClientPayOrderParams{},
|
||||
Output: &dto.ClientPayOrderResponse{},
|
||||
})
|
||||
|
||||
Register(authGroup, doc, basePath, "GET", "/exchange/pending", handlers.ClientExchange.GetPending, RouteSpec{
|
||||
Summary: "查询待处理换货单",
|
||||
Tags: []string{"个人客户 - 换货"},
|
||||
|
||||
@@ -34,6 +34,12 @@ type WechatConfigServiceInterface interface {
|
||||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
}
|
||||
|
||||
// OrderWalletPayServiceInterface 订单钱包支付服务接口。
|
||||
// 用于将钱包扣款、套餐激活、佣金计算等核心逻辑委托给 B 端 order.Service 处理。
|
||||
type OrderWalletPayServiceInterface interface {
|
||||
WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error
|
||||
}
|
||||
|
||||
// ForceRechargeRequirement 强充要求。
|
||||
type ForceRechargeRequirement struct {
|
||||
NeedForceRecharge bool
|
||||
@@ -50,6 +56,7 @@ type Service struct {
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
orderPaymentService OrderWalletPayServiceInterface
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
@@ -69,6 +76,7 @@ func New(
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
orderPaymentService OrderWalletPayServiceInterface,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
@@ -86,6 +94,7 @@ func New(
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
openIDStore: openIDStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
orderPaymentService: orderPaymentService,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
@@ -97,6 +106,8 @@ func New(
|
||||
}
|
||||
|
||||
// CreateOrder 创建客户端订单。
|
||||
// 普通套餐下单:仅创建待支付订单,不发起支付,需后续调用 POST /orders/:id/pay 支付。
|
||||
// 强充场景:检测到需要强充时,直接创建充值单并发起微信支付(一步完成),此时 app_type 必传。
|
||||
func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.ClientCreateOrderRequest) (*dto.ClientCreateOrderResponse, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
@@ -105,17 +116,17 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
return nil, errors.New(errors.CodeInternalError, "Redis 服务未配置")
|
||||
}
|
||||
|
||||
skipPermissionCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
assetInfo, err := s.assetService.Resolve(skipPermissionCtx, strings.TrimSpace(req.Identifier))
|
||||
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
assetInfo, err := s.assetService.Resolve(skipCtx, strings.TrimSpace(req.Identifier))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.checkAssetOwnership(skipPermissionCtx, customerID, assetInfo.VirtualNo); err != nil {
|
||||
if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validationResult, err := s.validatePurchase(skipPermissionCtx, assetInfo, req.PackageIDs)
|
||||
validationResult, err := s.validatePurchase(skipCtx, assetInfo, req.PackageIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,21 +136,14 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
return nil, errors.New(errors.CodeNeedRealname)
|
||||
}
|
||||
|
||||
activeConfig, appID, err := s.resolveWechatConfig(skipPermissionCtx, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 先判断是否需要强充,避免普通下单时不必要地校验微信配置
|
||||
forceRecharge := s.checkForceRechargeRequirement(skipCtx, validationResult)
|
||||
|
||||
openID, err := s.resolveCustomerOpenID(skipPermissionCtx, customerID, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req)
|
||||
businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req.PackageIDs)
|
||||
redisKey := constants.RedisClientPurchaseIdempotencyKey(businessKey)
|
||||
lockKey := constants.RedisClientPurchaseLockKey(assetInfo.AssetType, assetInfo.AssetID)
|
||||
|
||||
lockAcquired, err := s.redis.SetNX(skipPermissionCtx, lockKey, time.Now().String(), clientPurchaseLockTTL).Result()
|
||||
lockAcquired, err := s.redis.SetNX(skipCtx, lockKey, time.Now().String(), clientPurchaseLockTTL).Result()
|
||||
if err != nil {
|
||||
s.logger.Warn("获取客户端购买分布式锁失败,继续尝试幂等标记",
|
||||
zap.Error(err),
|
||||
@@ -150,16 +154,16 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
|
||||
}
|
||||
|
||||
claimed, err := s.redis.SetNX(skipPermissionCtx, redisKey, "processing", clientPurchaseIdempotencyTTL).Result()
|
||||
claimed, err := s.redis.SetNX(skipCtx, redisKey, "processing", clientPurchaseIdempotencyTTL).Result()
|
||||
if err != nil {
|
||||
if lockAcquired {
|
||||
_ = s.redis.Del(skipPermissionCtx, lockKey).Err()
|
||||
_ = s.redis.Del(skipCtx, lockKey).Err()
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "设置客户端购买幂等标记失败")
|
||||
}
|
||||
if !claimed {
|
||||
if lockAcquired {
|
||||
_ = s.redis.Del(skipPermissionCtx, lockKey).Err()
|
||||
_ = s.redis.Del(skipCtx, lockKey).Err()
|
||||
}
|
||||
return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
|
||||
}
|
||||
@@ -167,24 +171,31 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
created := false
|
||||
defer func() {
|
||||
if lockAcquired {
|
||||
_ = s.redis.Del(skipPermissionCtx, lockKey).Err()
|
||||
_ = s.redis.Del(skipCtx, lockKey).Err()
|
||||
}
|
||||
if !created {
|
||||
_ = s.redis.Del(skipPermissionCtx, redisKey).Err()
|
||||
_ = s.redis.Del(skipCtx, redisKey).Err()
|
||||
}
|
||||
}()
|
||||
|
||||
paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceRecharge := s.checkForceRechargeRequirement(skipPermissionCtx, validationResult)
|
||||
if forceRecharge.NeedForceRecharge {
|
||||
return s.createForceRechargeOrder(skipPermissionCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created)
|
||||
// 强充场景需要微信支付,此时 app_type 必须传入
|
||||
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openID, err := s.resolveCustomerOpenID(skipCtx, customerID, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.createForceRechargeOrder(skipCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created)
|
||||
}
|
||||
|
||||
return s.createPackageOrder(skipPermissionCtx, customerID, openID, validationResult, activeConfig, redisKey, paymentProvider, &created)
|
||||
return s.createPackageOrder(skipCtx, customerID, validationResult, redisKey, &created)
|
||||
}
|
||||
|
||||
func (s *Service) checkAssetOwnership(ctx context.Context, customerID uint, virtualNo string) error {
|
||||
@@ -269,14 +280,11 @@ func (s *Service) resolveCustomerOpenID(ctx context.Context, customerID uint, ap
|
||||
func (s *Service) createPackageOrder(
|
||||
ctx context.Context,
|
||||
customerID uint,
|
||||
openID string,
|
||||
validationResult *purchase_validation.PurchaseValidationResult,
|
||||
activeConfig *model.WechatConfig,
|
||||
redisKey string,
|
||||
paymentProvider PaymentProvider,
|
||||
created *bool,
|
||||
) (*dto.ClientCreateOrderResponse, error) {
|
||||
order, err := s.buildPendingOrder(customerID, validationResult, activeConfig)
|
||||
order, err := s.buildPendingOrder(customerID, validationResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -291,17 +299,6 @@ func (s *Service) createPackageOrder(
|
||||
}
|
||||
|
||||
s.markClientPurchaseCreated(ctx, redisKey, order.OrderNo)
|
||||
|
||||
description := "套餐购买"
|
||||
if len(items) > 0 && items[0] != nil && items[0].PackageName != "" {
|
||||
description = items[0].PackageName
|
||||
}
|
||||
|
||||
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
*created = true
|
||||
|
||||
return &dto.ClientCreateOrderResponse{
|
||||
@@ -313,7 +310,6 @@ func (s *Service) createPackageOrder(
|
||||
PaymentStatus: order.PaymentStatus,
|
||||
CreatedAt: formatClientServiceTime(order.CreatedAt),
|
||||
},
|
||||
PayConfig: buildClientPayConfigFromResult(paymentResult),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -393,7 +389,7 @@ func (s *Service) createForceRechargeOrder(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation.PurchaseValidationResult, activeConfig *model.WechatConfig) (*model.Order, error) {
|
||||
func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation.PurchaseValidationResult) (*model.Order, error) {
|
||||
orderType := resolveOrderType(result)
|
||||
if orderType == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
@@ -411,14 +407,12 @@ func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation
|
||||
BuyerType: model.BuyerTypePersonal,
|
||||
BuyerID: customerID,
|
||||
TotalAmount: result.TotalPrice,
|
||||
PaymentMethod: model.PaymentMethodWechat,
|
||||
PaymentStatus: model.PaymentStatusPending,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
CommissionConfigVersion: 0,
|
||||
Source: constants.OrderSourceClient,
|
||||
Generation: resolveGeneration(result),
|
||||
ExpiresAt: &expiresAt,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
}
|
||||
|
||||
if result.Card != nil {
|
||||
@@ -648,17 +642,17 @@ func extractPackageIDs(packages []*model.Package) []uint {
|
||||
return ids
|
||||
}
|
||||
|
||||
func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, req *dto.ClientCreateOrderRequest) string {
|
||||
packageIDs := make([]uint, 0, len(req.PackageIDs))
|
||||
packageIDs = append(packageIDs, req.PackageIDs...)
|
||||
slices.Sort(packageIDs)
|
||||
func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, packageIDs []uint) string {
|
||||
sorted := make([]uint, len(packageIDs))
|
||||
copy(sorted, packageIDs)
|
||||
slices.Sort(sorted)
|
||||
|
||||
parts := make([]string, 0, len(packageIDs))
|
||||
for _, packageID := range packageIDs {
|
||||
parts = append(parts, strconv.FormatUint(uint64(packageID), 10))
|
||||
parts := make([]string, 0, len(sorted))
|
||||
for _, id := range sorted {
|
||||
parts = append(parts, strconv.FormatUint(uint64(id), 10))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d:%s:%d:%s:%s", customerID, assetInfo.AssetType, assetInfo.AssetID, req.AppType, strings.Join(parts, ","))
|
||||
return fmt.Sprintf("%d:%s:%d:%s", customerID, assetInfo.AssetType, assetInfo.AssetID, strings.Join(parts, ","))
|
||||
}
|
||||
|
||||
func rechargeStatusToClientStatus(status int) int {
|
||||
@@ -810,6 +804,77 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PayOrder D4 对待支付的 C 端订单发起支付。
|
||||
// 支持 wallet(钱包扣款)和 wechat(微信 JSAPI)两种支付方式。
|
||||
// 先校验订单归属和状态,再按 payment_method 路由到对应支付逻辑:
|
||||
// - wallet:委托 B 端 WalletPay(乐观锁扣款 + 激活套餐 + 佣金入队)
|
||||
// - wechat:查微信配置 + OpenID → 预下单 → 更新 payment_config_id → 返回 JS 支付参数
|
||||
func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, req *dto.ClientPayOrderRequest) (*dto.ClientPayOrderResponse, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
// 跳过 GORM 店铺过滤,C 端订单查询不依赖 seller_shop_id 过滤
|
||||
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
|
||||
order, err := s.orderStore.GetByID(skipCtx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
|
||||
}
|
||||
|
||||
if order.BuyerType != model.BuyerTypePersonal || order.BuyerID != customerID {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
switch req.PaymentMethod {
|
||||
case model.PaymentMethodWallet:
|
||||
if err := s.orderPaymentService.WalletPay(skipCtx, orderID, model.BuyerTypePersonal, customerID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.ClientPayOrderResponse{PaymentMethod: model.PaymentMethodWallet}, nil
|
||||
|
||||
case model.PaymentMethodWechat:
|
||||
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openID, err := s.resolveCustomerOpenID(skipCtx, customerID, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录本次支付使用的微信配置,供支付回调验签使用
|
||||
if err := s.db.WithContext(skipCtx).Model(&model.Order{}).
|
||||
Where("id = ?", orderID).
|
||||
Update("payment_config_id", activeConfig.ID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新订单支付配置失败")
|
||||
}
|
||||
|
||||
paymentResult, err := paymentProvider.CreateJSAPIPayment(skipCtx, order.OrderNo, "套餐购买", openID, int(order.TotalAmount))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.ClientPayOrderResponse{
|
||||
PaymentMethod: model.PaymentMethodWechat,
|
||||
PayConfig: buildClientPayConfigFromResult(paymentResult),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, errors.New(errors.CodeInvalidParam, "不支持的支付方式")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||
switch assetType {
|
||||
case "card":
|
||||
|
||||
Reference in New Issue
Block a user