feat: C端订单支付拆分 — 创建订单与发起支付分离
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:
2026-03-31 12:43:34 +08:00
parent 121462c00f
commit dc4f3cb7ba
6 changed files with 276 additions and 62 deletions

View File

@@ -1597,7 +1597,7 @@ components:
DtoClientCreateOrderRequest: DtoClientCreateOrderRequest:
properties: properties:
app_type: app_type:
description: 应用类型 (official_account:公众号, miniapp:小程序) description: 应用类型(强充必传)(official_account:公众号, miniapp:小程序)
type: string type: string
identifier: identifier:
description: 资产标识符SN/IMEI/虚拟号/ICCID/MSISDN description: 资产标识符SN/IMEI/虚拟号/ICCID/MSISDN
@@ -1614,7 +1614,6 @@ components:
required: required:
- identifier - identifier
- package_ids - package_ids
- app_type
type: object type: object
DtoClientCreateOrderResponse: DtoClientCreateOrderResponse:
properties: properties:
@@ -1849,6 +1848,25 @@ components:
description: 时间戳 description: 时间戳
type: string type: string
type: object 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: DtoClientRechargeCheckResponse:
properties: properties:
force_recharge_amount: force_recharge_amount:
@@ -22879,6 +22897,78 @@ paths:
summary: 订单详情 summary: 订单详情
tags: 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: /api/c/v1/orders/create:
post: post:
requestBody: requestBody:

View File

@@ -35,6 +35,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
personalCustomerDeviceStore, personalCustomerDeviceStore,
personalCustomerOpenIDStore, personalCustomerOpenIDStore,
svc.WechatConfig, svc.WechatConfig,
svc.Order,
packageSeriesStore, packageSeriesStore,
shopSeriesAllocationStore, shopSeriesAllocationStore,
iotCardStore, iotCardStore,

View File

@@ -14,7 +14,7 @@ import (
) )
// ClientOrderHandler C 端订单处理器 // ClientOrderHandler C 端订单处理器
// 提供 D1~D3 下单、列表、详情接口。 // 提供 D1~D4 下单、列表、详情、支付接口。
type ClientOrderHandler struct { type ClientOrderHandler struct {
clientOrderService *clientorder.Service clientOrderService *clientorder.Service
logger *zap.Logger 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) 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 订单详情。 // GetOrderDetail D3 订单详情。
// GET /api/c/v1/orders/:id // GET /api/c/v1/orders/:id
func (h *ClientOrderHandler) GetOrderDetail(c *fiber.Ctx) error { func (h *ClientOrderHandler) GetOrderDetail(c *fiber.Ctx) error {

View File

@@ -8,15 +8,16 @@ package dto
type ClientCreateOrderRequest struct { type ClientCreateOrderRequest struct {
Identifier string `json:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符SN/IMEI/虚拟号/ICCID/MSISDN"` 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列表"` 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 客户端创建订单响应 // ClientCreateOrderResponse D1 客户端创建订单响应
type ClientCreateOrderResponse struct { type ClientCreateOrderResponse struct {
OrderType string `json:"order_type" description:"订单类型 (package:套餐订单, recharge:充值订单)"` 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:"充值订单信息"` 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:"关联套餐信息"` LinkedPackageInfo *LinkedPackageInfo `json:"linked_package_info,omitempty" description:"关联套餐信息"`
} }
@@ -38,7 +39,7 @@ type ClientRechargeInfo struct {
AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"` AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"`
} }
// ClientPayConfig D1 支付配置 // ClientPayConfig 微信支付配置
type ClientPayConfig struct { type ClientPayConfig struct {
AppID string `json:"app_id" description:"应用ID"` AppID string `json:"app_id" description:"应用ID"`
Timestamp string `json:"timestamp" description:"时间戳"` Timestamp string `json:"timestamp" description:"时间戳"`
@@ -111,3 +112,26 @@ type ClientOrderPackageItem struct {
Price int64 `json:"price" description:"单价(分)"` Price int64 `json:"price" description:"单价(分)"`
Quantity int `json:"quantity" 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
}

View File

@@ -214,6 +214,14 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
Output: &dto.ClientOrderDetailResponse{}, 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{ Register(authGroup, doc, basePath, "GET", "/exchange/pending", handlers.ClientExchange.GetPending, RouteSpec{
Summary: "查询待处理换货单", Summary: "查询待处理换货单",
Tags: []string{"个人客户 - 换货"}, Tags: []string{"个人客户 - 换货"},

View File

@@ -34,6 +34,12 @@ type WechatConfigServiceInterface interface {
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) 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 强充要求。 // ForceRechargeRequirement 强充要求。
type ForceRechargeRequirement struct { type ForceRechargeRequirement struct {
NeedForceRecharge bool NeedForceRecharge bool
@@ -50,6 +56,7 @@ type Service struct {
personalDeviceStore *postgres.PersonalCustomerDeviceStore personalDeviceStore *postgres.PersonalCustomerDeviceStore
openIDStore *postgres.PersonalCustomerOpenIDStore openIDStore *postgres.PersonalCustomerOpenIDStore
wechatConfigService WechatConfigServiceInterface wechatConfigService WechatConfigServiceInterface
orderPaymentService OrderWalletPayServiceInterface
packageSeriesStore *postgres.PackageSeriesStore packageSeriesStore *postgres.PackageSeriesStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
iotCardStore *postgres.IotCardStore iotCardStore *postgres.IotCardStore
@@ -69,6 +76,7 @@ func New(
personalDeviceStore *postgres.PersonalCustomerDeviceStore, personalDeviceStore *postgres.PersonalCustomerDeviceStore,
openIDStore *postgres.PersonalCustomerOpenIDStore, openIDStore *postgres.PersonalCustomerOpenIDStore,
wechatConfigService WechatConfigServiceInterface, wechatConfigService WechatConfigServiceInterface,
orderPaymentService OrderWalletPayServiceInterface,
packageSeriesStore *postgres.PackageSeriesStore, packageSeriesStore *postgres.PackageSeriesStore,
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
iotCardStore *postgres.IotCardStore, iotCardStore *postgres.IotCardStore,
@@ -86,6 +94,7 @@ func New(
personalDeviceStore: personalDeviceStore, personalDeviceStore: personalDeviceStore,
openIDStore: openIDStore, openIDStore: openIDStore,
wechatConfigService: wechatConfigService, wechatConfigService: wechatConfigService,
orderPaymentService: orderPaymentService,
packageSeriesStore: packageSeriesStore, packageSeriesStore: packageSeriesStore,
shopSeriesAllocationStore: shopSeriesAllocationStore, shopSeriesAllocationStore: shopSeriesAllocationStore,
iotCardStore: iotCardStore, iotCardStore: iotCardStore,
@@ -97,6 +106,8 @@ func New(
} }
// CreateOrder 创建客户端订单。 // CreateOrder 创建客户端订单。
// 普通套餐下单:仅创建待支付订单,不发起支付,需后续调用 POST /orders/:id/pay 支付。
// 强充场景:检测到需要强充时,直接创建充值单并发起微信支付(一步完成),此时 app_type 必传。
func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.ClientCreateOrderRequest) (*dto.ClientCreateOrderResponse, error) { func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.ClientCreateOrderRequest) (*dto.ClientCreateOrderResponse, error) {
if req == nil { if req == nil {
return nil, errors.New(errors.CodeInvalidParam) 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 服务未配置") return nil, errors.New(errors.CodeInternalError, "Redis 服务未配置")
} }
skipPermissionCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{}) skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
assetInfo, err := s.assetService.Resolve(skipPermissionCtx, strings.TrimSpace(req.Identifier)) assetInfo, err := s.assetService.Resolve(skipCtx, strings.TrimSpace(req.Identifier))
if err != nil { if err != nil {
return nil, err 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 return nil, err
} }
validationResult, err := s.validatePurchase(skipPermissionCtx, assetInfo, req.PackageIDs) validationResult, err := s.validatePurchase(skipCtx, assetInfo, req.PackageIDs)
if err != nil { if err != nil {
return nil, err 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) return nil, errors.New(errors.CodeNeedRealname)
} }
activeConfig, appID, err := s.resolveWechatConfig(skipPermissionCtx, req.AppType) // 先判断是否需要强充,避免普通下单时不必要地校验微信配置
if err != nil { forceRecharge := s.checkForceRechargeRequirement(skipCtx, validationResult)
return nil, err
}
openID, err := s.resolveCustomerOpenID(skipPermissionCtx, customerID, appID) businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req.PackageIDs)
if err != nil {
return nil, err
}
businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req)
redisKey := constants.RedisClientPurchaseIdempotencyKey(businessKey) redisKey := constants.RedisClientPurchaseIdempotencyKey(businessKey)
lockKey := constants.RedisClientPurchaseLockKey(assetInfo.AssetType, assetInfo.AssetID) 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 { if err != nil {
s.logger.Warn("获取客户端购买分布式锁失败,继续尝试幂等标记", s.logger.Warn("获取客户端购买分布式锁失败,继续尝试幂等标记",
zap.Error(err), 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, "订单正在创建中,请勿重复提交") 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 err != nil {
if lockAcquired { if lockAcquired {
_ = s.redis.Del(skipPermissionCtx, lockKey).Err() _ = s.redis.Del(skipCtx, lockKey).Err()
} }
return nil, errors.Wrap(errors.CodeInternalError, err, "设置客户端购买幂等标记失败") return nil, errors.Wrap(errors.CodeInternalError, err, "设置客户端购买幂等标记失败")
} }
if !claimed { if !claimed {
if lockAcquired { if lockAcquired {
_ = s.redis.Del(skipPermissionCtx, lockKey).Err() _ = s.redis.Del(skipCtx, lockKey).Err()
} }
return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交") return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
} }
@@ -167,24 +171,31 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
created := false created := false
defer func() { defer func() {
if lockAcquired { if lockAcquired {
_ = s.redis.Del(skipPermissionCtx, lockKey).Err() _ = s.redis.Del(skipCtx, lockKey).Err()
} }
if !created { 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 { 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 { 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( func (s *Service) createPackageOrder(
ctx context.Context, ctx context.Context,
customerID uint, customerID uint,
openID string,
validationResult *purchase_validation.PurchaseValidationResult, validationResult *purchase_validation.PurchaseValidationResult,
activeConfig *model.WechatConfig,
redisKey string, redisKey string,
paymentProvider PaymentProvider,
created *bool, created *bool,
) (*dto.ClientCreateOrderResponse, error) { ) (*dto.ClientCreateOrderResponse, error) {
order, err := s.buildPendingOrder(customerID, validationResult, activeConfig) order, err := s.buildPendingOrder(customerID, validationResult)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -291,17 +299,6 @@ func (s *Service) createPackageOrder(
} }
s.markClientPurchaseCreated(ctx, redisKey, order.OrderNo) 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 *created = true
return &dto.ClientCreateOrderResponse{ return &dto.ClientCreateOrderResponse{
@@ -313,7 +310,6 @@ func (s *Service) createPackageOrder(
PaymentStatus: order.PaymentStatus, PaymentStatus: order.PaymentStatus,
CreatedAt: formatClientServiceTime(order.CreatedAt), CreatedAt: formatClientServiceTime(order.CreatedAt),
}, },
PayConfig: buildClientPayConfigFromResult(paymentResult),
}, nil }, nil
} }
@@ -393,7 +389,7 @@ func (s *Service) createForceRechargeOrder(
}, nil }, 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) orderType := resolveOrderType(result)
if orderType == "" { if orderType == "" {
return nil, errors.New(errors.CodeInvalidParam) return nil, errors.New(errors.CodeInvalidParam)
@@ -411,14 +407,12 @@ func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation
BuyerType: model.BuyerTypePersonal, BuyerType: model.BuyerTypePersonal,
BuyerID: customerID, BuyerID: customerID,
TotalAmount: result.TotalPrice, TotalAmount: result.TotalPrice,
PaymentMethod: model.PaymentMethodWechat,
PaymentStatus: model.PaymentStatusPending, PaymentStatus: model.PaymentStatusPending,
CommissionStatus: model.CommissionStatusPending, CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0, CommissionConfigVersion: 0,
Source: constants.OrderSourceClient, Source: constants.OrderSourceClient,
Generation: resolveGeneration(result), Generation: resolveGeneration(result),
ExpiresAt: &expiresAt, ExpiresAt: &expiresAt,
PaymentConfigID: &activeConfig.ID,
} }
if result.Card != nil { if result.Card != nil {
@@ -648,17 +642,17 @@ func extractPackageIDs(packages []*model.Package) []uint {
return ids return ids
} }
func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, req *dto.ClientCreateOrderRequest) string { func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, packageIDs []uint) string {
packageIDs := make([]uint, 0, len(req.PackageIDs)) sorted := make([]uint, len(packageIDs))
packageIDs = append(packageIDs, req.PackageIDs...) copy(sorted, packageIDs)
slices.Sort(packageIDs) slices.Sort(sorted)
parts := make([]string, 0, len(packageIDs)) parts := make([]string, 0, len(sorted))
for _, packageID := range packageIDs { for _, id := range sorted {
parts = append(parts, strconv.FormatUint(uint64(packageID), 10)) 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 { func rechargeStatusToClientStatus(status int) int {
@@ -810,6 +804,77 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
}, nil }, 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) { func (s *Service) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
switch assetType { switch assetType {
case "card": case "card":