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:
|
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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{"个人客户 - 换货"},
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user