fix(force-recharge): 补充强充配置缺失的接口和数据库字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m19s

- 订单管理:增加 payment_method 字段支持,合并代购订单逻辑
- 套餐系列分配:增加强充配置字段(enable_force_recharge、force_recharge_amount、force_recharge_trigger_type)
- 数据库迁移:添加 force_recharge_trigger_type 字段
- 测试:更新订单服务测试用例
- OpenSpec:归档 fix-force-recharge-missing-interfaces 变更
This commit is contained in:
2026-01-31 15:34:32 +08:00
parent d309951493
commit d81bd242a4
21 changed files with 1090 additions and 388 deletions

View File

@@ -32,11 +32,24 @@ func (h *OrderHandler) Create(c *fiber.Ctx) error {
userType := middleware.GetUserTypeFromContext(ctx)
shopID := middleware.GetShopIDFromContext(ctx)
if userType != constants.UserTypeAgent {
return errors.New(errors.CodeForbidden, "只有代理账号可以创建订单")
if req.PaymentMethod == model.PaymentMethodOffline {
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
return errors.New(errors.CodeForbidden, "只有平台可以使用线下支付")
}
} else if req.PaymentMethod == model.PaymentMethodWallet {
if userType != constants.UserTypeAgent && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
return errors.New(errors.CodeForbidden, "无权创建订单")
}
}
order, err := h.service.Create(ctx, &req, model.BuyerTypeAgent, shopID)
buyerType := ""
buyerID := uint(0)
if userType == constants.UserTypeAgent {
buyerType = model.BuyerTypeAgent
buyerID = shopID
}
order, err := h.service.Create(ctx, &req, buyerType, buyerID)
if err != nil {
return err
}

View File

@@ -28,6 +28,10 @@ func (h *OrderHandler) Create(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if req.PaymentMethod != model.PaymentMethodWallet {
return errors.New(errors.CodeInvalidParam, "H5端只支持钱包支付")
}
ctx := c.UserContext()
userType := middleware.GetUserTypeFromContext(ctx)

View File

@@ -3,10 +3,11 @@ package dto
import "time"
type CreateOrderRequest struct {
OrderType string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
IotCardID *uint `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
DeviceID *uint `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
OrderType string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
IotCardID *uint `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
DeviceID *uint `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet offline" required:"true" description:"支付方式 (wallet:钱包支付, offline:线下支付)"`
}
type OrderListRequest struct {
@@ -45,6 +46,7 @@ type OrderResponse struct {
PaymentStatus int `json:"payment_status" description:"支付状态 (1:待支付, 2:已支付, 3:已取消, 4:已退款)"`
PaymentStatusText string `json:"payment_status_text" description:"支付状态文本"`
PaidAt *time.Time `json:"paid_at,omitempty" description:"支付时间"`
IsPurchaseOnBehalf bool `json:"is_purchase_on_behalf" description:"是否为代购订单"`
CommissionStatus int `json:"commission_status" description:"佣金状态 (1:待计算, 2:已计算)"`
CommissionConfigVersion int `json:"commission_config_version" description:"佣金配置版本"`
Items []*OrderItemResponse `json:"items" description:"订单明细列表"`
@@ -87,10 +89,3 @@ type PurchaseCheckResponse struct {
WalletCredit int64 `json:"wallet_credit" description:"钱包到账金额(分)"`
Message string `json:"message" description:"提示信息"`
}
type CreatePurchaseOnBehalfRequest struct {
OrderType string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
IotCardID *uint `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
DeviceID *uint `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
}

View File

@@ -26,18 +26,24 @@ type OneTimeCommissionTierEntry struct {
// CreateShopSeriesAllocationRequest 创建套餐系列分配请求
type CreateShopSeriesAllocationRequest struct {
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
SeriesID uint `json:"series_id" validate:"required" required:"true" description:"套餐系列ID"`
BaseCommission BaseCommissionConfig `json:"base_commission" validate:"required" required:"true" description:"基础返佣配置"`
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金配置(启用一次性佣金时必填)"`
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
SeriesID uint `json:"series_id" validate:"required" required:"true" description:"套餐系列ID"`
BaseCommission BaseCommissionConfig `json:"base_commission" validate:"required" required:"true" description:"基础返佣配置"`
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金配置(启用一次性佣金时必填)"`
EnableForceRecharge *bool `json:"enable_force_recharge,omitempty" description:"是否启用强充(累计充值强充)"`
ForceRechargeAmount *int64 `json:"force_recharge_amount,omitempty" description:"强充金额(分,0表示使用阈值金额)"`
ForceRechargeTriggerType *int `json:"force_recharge_trigger_type,omitempty" description:"强充触发类型(1:单次充值, 2:累计充值)"`
}
// UpdateShopSeriesAllocationRequest 更新套餐系列分配请求
type UpdateShopSeriesAllocationRequest struct {
BaseCommission *BaseCommissionConfig `json:"base_commission" validate:"omitempty" description:"基础返佣配置"`
EnableOneTimeCommission *bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金配置"`
BaseCommission *BaseCommissionConfig `json:"base_commission" validate:"omitempty" description:"基础返佣配置"`
EnableOneTimeCommission *bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金配置"`
EnableForceRecharge *bool `json:"enable_force_recharge,omitempty" description:"是否启用强充(累计充值强充)"`
ForceRechargeAmount *int64 `json:"force_recharge_amount,omitempty" description:"强充金额(分,0表示使用阈值金额)"`
ForceRechargeTriggerType *int `json:"force_recharge_trigger_type,omitempty" description:"强充触发类型(1:单次充值, 2:累计充值)"`
}
// ShopSeriesAllocationListRequest 套餐系列分配列表请求
@@ -56,19 +62,22 @@ type UpdateShopSeriesAllocationStatusRequest struct {
// ShopSeriesAllocationResponse 套餐系列分配响应
type ShopSeriesAllocationResponse struct {
ID uint `json:"id" description:"分配ID"`
ShopID uint `json:"shop_id" description:"被分配的店铺ID"`
ShopName string `json:"shop_name" description:"被分配的店铺名称"`
SeriesID uint `json:"series_id" description:"套餐系列ID"`
SeriesName string `json:"series_name" description:"套餐系列名称"`
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID"`
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
BaseCommission BaseCommissionConfig `json:"base_commission" description:"基础返佣配置"`
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config,omitempty" description:"一次性佣金配置"`
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
ID uint `json:"id" description:"分配ID"`
ShopID uint `json:"shop_id" description:"被分配的店铺ID"`
ShopName string `json:"shop_name" description:"被分配的店铺名称"`
SeriesID uint `json:"series_id" description:"套餐系列ID"`
SeriesName string `json:"series_name" description:"套餐系列名称"`
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID"`
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
BaseCommission BaseCommissionConfig `json:"base_commission" description:"基础返佣配置"`
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config,omitempty" description:"一次性佣金配置"`
EnableForceRecharge bool `json:"enable_force_recharge" description:"是否启用强充"`
ForceRechargeAmount int64 `json:"force_recharge_amount" description:"强充金额(分)"`
ForceRechargeTriggerType int `json:"force_recharge_trigger_type" description:"强充触发类型(1:单次充值, 2:累计充值)"`
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
}
// ShopSeriesAllocationPageResult 套餐系列分配分页结果

View File

@@ -25,8 +25,9 @@ type ShopSeriesAllocation struct {
OneTimeCommissionValue int64 `gorm:"column:one_time_commission_value;type:bigint;default:0;comment:佣金金额(分)或比例(千分比)" json:"one_time_commission_value"`
// 强充配置
EnableForceRecharge bool `gorm:"column:enable_force_recharge;type:boolean;default:false;comment:是否启用强充(累计充值时可选)" json:"enable_force_recharge"`
ForceRechargeAmount int64 `gorm:"column:force_recharge_amount;type:bigint;default:0;comment:强充金额(分,0表示使用阈值金额)" json:"force_recharge_amount"`
EnableForceRecharge bool `gorm:"column:enable_force_recharge;type:boolean;default:false;comment:是否启用强充(累计充值时可选)" json:"enable_force_recharge"`
ForceRechargeAmount int64 `gorm:"column:force_recharge_amount;type:bigint;default:0;comment:强充金额(分,0表示使用阈值金额)" json:"force_recharge_amount"`
ForceRechargeTriggerType int `gorm:"column:force_recharge_trigger_type;type:int;default:2;comment:强充触发类型(1:单次充值, 2:累计充值)" json:"force_recharge_trigger_type"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
}

View File

@@ -95,6 +95,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
userID := middleware.GetUserIDFromContext(ctx)
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
orderBuyerType := buyerType
orderBuyerID := buyerID
totalAmount := validationResult.TotalPrice
paymentMethod := req.PaymentMethod
paymentStatus := model.PaymentStatusPending
var paidAt *time.Time
isPurchaseOnBehalf := false
var seriesID *uint
var sellerShopID *uint
var sellerCostPrice int64
@@ -102,6 +110,22 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
if validationResult.Allocation != nil {
seriesID = &validationResult.Allocation.SeriesID
sellerShopID = &validationResult.Allocation.ShopID
}
if req.PaymentMethod == model.PaymentMethodOffline {
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerCostPrice
} else if validationResult.Allocation != nil {
sellerCostPrice = utils.CalculateCostPrice(validationResult.Allocation, validationResult.TotalPrice)
}
@@ -112,25 +136,68 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: buyerType,
BuyerID: buyerID,
BuyerType: orderBuyerType,
BuyerID: orderBuyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: validationResult.TotalPrice,
PaymentStatus: model.PaymentStatusPending,
TotalAmount: totalAmount,
PaymentMethod: paymentMethod,
PaymentStatus: paymentStatus,
PaidAt: paidAt,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: configVersion,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
}
var items []*model.OrderItem
for _, pkg := range validationResult.Packages {
items := s.buildOrderItems(userID, validationResult.Packages)
if req.PaymentMethod == model.PaymentMethodOffline {
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
return s.buildOrderResponse(order, items), nil
}
if err := s.orderStore.Create(ctx, order, items); err != nil {
return nil, err
}
return s.buildOrderResponse(order, items), nil
}
func (s *Service) resolvePurchaseOnBehalfInfo(ctx context.Context, result *purchase_validation.PurchaseValidationResult) (uint, int64, *time.Time, error) {
var resourceShopID *uint
if result.Card != nil {
resourceShopID = result.Card.ShopID
} else if result.Device != nil {
resourceShopID = result.Device.ShopID
}
if resourceShopID == nil || *resourceShopID == 0 {
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法代购")
}
buyerAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, *resourceShopID, result.Allocation.SeriesID)
if err != nil {
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "买家没有该套餐系列的分配配置")
}
buyerCostPrice := utils.CalculateCostPrice(buyerAllocation, result.TotalPrice)
now := time.Now()
return *resourceShopID, buyerCostPrice, &now, nil
}
func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package) []*model.OrderItem {
items := make([]*model.OrderItem, 0, len(packages))
for _, pkg := range packages {
item := &model.OrderItem{
BaseModel: model.BaseModel{
Creator: userID,
Updater: userID,
Creator: operatorID,
Updater: operatorID,
},
PackageID: pkg.ID,
PackageName: pkg.PackageName,
@@ -141,11 +208,24 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
items = append(items, item)
}
if err := s.orderStore.Create(ctx, order, items); err != nil {
return nil, err
}
return items
}
return s.buildOrderResponse(order, items), nil
func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Order, items []*model.OrderItem) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建代购订单失败")
}
for _, item := range items {
item.OrderID = order.ID
}
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
return s.activatePackage(ctx, tx, order)
})
}
func (s *Service) Get(ctx context.Context, id uint) (*dto.OrderResponse, error) {
@@ -544,6 +624,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
PaymentStatus: order.PaymentStatus,
PaymentStatusText: statusText,
PaidAt: order.PaidAt,
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
CommissionStatus: order.CommissionStatus,
CommissionConfigVersion: order.CommissionConfigVersion,
Items: itemResponses,
@@ -751,119 +832,3 @@ func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRe
return response, nil
}
func (s *Service) CreatePurchaseOnBehalf(ctx context.Context, req *dto.CreatePurchaseOnBehalfRequest, operatorID uint) (*dto.OrderResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var resourceShopID *uint
var err error
if req.OrderType == model.OrderTypeSingleCard {
if req.IotCardID == nil {
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
}
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
if err != nil {
return nil, err
}
if validationResult.Card != nil {
resourceShopID = validationResult.Card.ShopID
}
} else if req.OrderType == model.OrderTypeDevice {
if req.DeviceID == nil {
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
}
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
if err != nil {
return nil, err
}
if validationResult.Device != nil {
resourceShopID = validationResult.Device.ShopID
}
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if resourceShopID == nil || *resourceShopID == 0 {
return nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法代购")
}
buyerID := *resourceShopID
buyerAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, buyerID, validationResult.Allocation.SeriesID)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "买家没有该套餐系列的分配配置")
}
buyerCostPrice := utils.CalculateCostPrice(buyerAllocation, validationResult.TotalPrice)
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
var seriesID *uint
var sellerShopID *uint
if validationResult.Allocation != nil {
seriesID = &validationResult.Allocation.SeriesID
sellerShopID = &validationResult.Allocation.ShopID
}
now := time.Now()
order := &model.Order{
BaseModel: model.BaseModel{
Creator: operatorID,
Updater: operatorID,
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: model.BuyerTypeAgent,
BuyerID: buyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: buyerCostPrice,
PaymentMethod: model.PaymentMethodOffline,
PaymentStatus: model.PaymentStatusPaid,
PaidAt: &now,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: configVersion,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: buyerCostPrice,
IsPurchaseOnBehalf: true,
}
var items []*model.OrderItem
for _, pkg := range validationResult.Packages {
item := &model.OrderItem{
BaseModel: model.BaseModel{
Creator: operatorID,
Updater: operatorID,
},
PackageID: pkg.ID,
PackageName: pkg.PackageName,
Quantity: 1,
UnitPrice: pkg.SuggestedRetailPrice,
Amount: pkg.SuggestedRetailPrice,
}
items = append(items, item)
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建代购订单失败")
}
for _, item := range items {
item.OrderID = order.ID
}
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
return s.activatePackage(ctx, tx, order)
})
if err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
return s.buildOrderResponse(order, items), nil
}

View File

@@ -44,7 +44,10 @@ func setupOrderTestEnv(t *testing.T) *testEnv {
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
})
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_ORDER",
@@ -151,9 +154,10 @@ func TestOrderService_Create(t *testing.T) {
t.Run("创建单卡订单成功", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
resp, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
@@ -170,9 +174,10 @@ func TestOrderService_Create(t *testing.T) {
t.Run("创建设备订单成功", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeDevice,
DeviceID: &env.device.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeDevice,
DeviceID: &env.device.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
resp, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
@@ -183,8 +188,9 @@ func TestOrderService_Create(t *testing.T) {
t.Run("单卡订单缺少卡ID", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
_, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
@@ -196,8 +202,9 @@ func TestOrderService_Create(t *testing.T) {
t.Run("设备订单缺少设备ID", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeDevice,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeDevice,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
_, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
@@ -206,15 +213,50 @@ func TestOrderService_Create(t *testing.T) {
require.True(t, ok)
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
})
t.Run("钱包支付创建订单", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
resp, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
assert.Equal(t, model.PaymentStatusPending, resp.PaymentStatus)
assert.False(t, resp.IsPurchaseOnBehalf)
})
t.Run("线下支付创建订单", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodOffline,
}
platformCtx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
})
resp, err := env.svc.Create(platformCtx, req, "", 0)
require.NoError(t, err)
assert.Equal(t, model.PaymentStatusPaid, resp.PaymentStatus)
assert.True(t, resp.IsPurchaseOnBehalf)
assert.NotNil(t, resp.PaidAt)
})
}
func TestOrderService_Get(t *testing.T) {
env := setupOrderTestEnv(t)
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -240,9 +282,10 @@ func TestOrderService_List(t *testing.T) {
for i := 0; i < 3; i++ {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
_, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -278,9 +321,10 @@ func TestOrderService_Cancel(t *testing.T) {
env := setupOrderTestEnv(t)
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -304,9 +348,10 @@ func TestOrderService_Cancel(t *testing.T) {
t.Run("无权操作", func(t *testing.T) {
newReq := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
newOrder, err := env.svc.Create(env.ctx, newReq, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -324,9 +369,10 @@ func TestOrderService_WalletPay(t *testing.T) {
t.Run("钱包支付成功", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -351,9 +397,10 @@ func TestOrderService_WalletPay(t *testing.T) {
t.Run("无权操作", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -367,9 +414,10 @@ func TestOrderService_WalletPay(t *testing.T) {
t.Run("重复支付-幂等成功", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -383,9 +431,10 @@ func TestOrderService_WalletPay(t *testing.T) {
t.Run("已取消订单无法支付", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -406,9 +455,10 @@ func TestOrderService_HandlePaymentCallback(t *testing.T) {
t.Run("支付回调成功", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -424,9 +474,10 @@ func TestOrderService_HandlePaymentCallback(t *testing.T) {
t.Run("幂等处理-已支付订单", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &env.card.ID,
PackageIDs: []uint{env.pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
require.NoError(t, err)
@@ -463,7 +514,10 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
})
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_IDEM",
@@ -546,9 +600,10 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
t.Run("串行幂等支付测试", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
require.NoError(t, err)
@@ -574,9 +629,10 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
t.Run("串行回调幂等测试", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
require.NoError(t, err)
@@ -598,9 +654,10 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
t.Run("已取消订单回调测试", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
created, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
require.NoError(t, err)
@@ -637,7 +694,10 @@ func TestOrderService_ForceRechargeValidation(t *testing.T) {
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
})
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_FR",
@@ -728,9 +788,10 @@ func TestOrderService_ForceRechargeValidation(t *testing.T) {
t.Run("强充验证-金额不足拒绝", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{cheapPkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{cheapPkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
_, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
@@ -742,9 +803,10 @@ func TestOrderService_ForceRechargeValidation(t *testing.T) {
t.Run("强充验证-金额足够通过", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{expensivePkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{expensivePkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
resp, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
@@ -766,9 +828,10 @@ func TestOrderService_ForceRechargeValidation(t *testing.T) {
require.NoError(t, iotCardStore.Create(ctx, card2))
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card2.ID,
PackageIDs: []uint{cheapPkg.ID},
OrderType: model.OrderTypeSingleCard,
IotCardID: &card2.ID,
PackageIDs: []uint{cheapPkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}
resp, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
@@ -793,7 +856,10 @@ func TestOrderService_GetPurchaseCheck(t *testing.T) {
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
})
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_PC",
@@ -905,140 +971,6 @@ func TestOrderService_GetPurchaseCheck(t *testing.T) {
})
}
func TestOrderService_CreatePurchaseOnBehalf(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
packageStore := postgres.NewPackageStore(tx)
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
carrierStore := postgres.NewCarrierStore(tx)
shopStore := postgres.NewShopStore(tx, rdb)
orderStore := postgres.NewOrderStore(tx, rdb)
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_POB",
CarrierName: "测试运营商代购",
CarrierType: constants.CarrierTypeCMCC,
Status: constants.StatusEnabled,
}
require.NoError(t, carrierStore.Create(ctx, carrier))
shop := &model.Shop{
ShopName: "测试店铺POB",
ShopCode: "TEST_SHOP_POB",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, shopStore.Create(ctx, shop))
series := &model.PackageSeries{
SeriesCode: "TEST_SERIES_POB",
SeriesName: "测试套餐系列代购",
Description: "测试用",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageSeriesStore.Create(ctx, series))
allocation := &model.ShopSeriesAllocation{
ShopID: shop.ID,
SeriesID: series.ID,
AllocatorShopID: 0,
BaseCommissionMode: model.CommissionModePercent,
BaseCommissionValue: 100,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
pkg := &model.Package{
PackageCode: "TEST_PKG_POB",
PackageName: "测试套餐代购",
SeriesID: series.ID,
PackageType: "formal",
DurationMonths: 1,
DataAmountMB: 1024,
SuggestedRetailPrice: 10000,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageStore.Create(ctx, pkg))
shopIDPtr := &shop.ID
card := &model.IotCard{
ICCID: "89860000000000000POB",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card))
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
t.Run("代购订单创建成功", func(t *testing.T) {
req := &dto.CreatePurchaseOnBehalfRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
}
resp, err := orderSvc.CreatePurchaseOnBehalf(ctx, req, 1)
require.NoError(t, err)
assert.NotZero(t, resp.ID)
assert.Equal(t, model.PaymentStatusPaid, resp.PaymentStatus)
assert.Equal(t, model.PaymentMethodOffline, resp.PaymentMethod)
assert.Equal(t, model.BuyerTypeAgent, resp.BuyerType)
assert.Equal(t, shop.ID, resp.BuyerID)
assert.NotNil(t, resp.PaidAt)
expectedCostPrice := pkg.SuggestedRetailPrice - (pkg.SuggestedRetailPrice * allocation.BaseCommissionValue / 1000)
assert.Equal(t, expectedCostPrice, resp.TotalAmount)
var usageCount int64
err = tx.Model(&model.PackageUsage{}).Where("order_id = ?", resp.ID).Count(&usageCount).Error
require.NoError(t, err)
assert.Equal(t, int64(1), usageCount)
})
t.Run("代购订单-资源未分配拒绝", func(t *testing.T) {
cardNoShop := &model.IotCard{
ICCID: "89860000000000NOSHOP",
ShopID: nil,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, cardNoShop))
req := &dto.CreatePurchaseOnBehalfRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &cardNoShop.ID,
PackageIDs: []uint{pkg.ID},
}
_, err := orderSvc.CreatePurchaseOnBehalf(ctx, req, 1)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
})
}
func TestOrderService_WalletPay_PurchaseOnBehalf(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
@@ -1055,7 +987,10 @@ func TestOrderService_WalletPay_PurchaseOnBehalf(t *testing.T) {
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
})
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_WP",
@@ -1134,13 +1069,14 @@ func TestOrderService_WalletPay_PurchaseOnBehalf(t *testing.T) {
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
t.Run("代购订单无法进行钱包支付", func(t *testing.T) {
req := &dto.CreatePurchaseOnBehalfRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
PaymentMethod: model.PaymentMethodOffline,
}
created, err := orderSvc.CreatePurchaseOnBehalf(ctx, req, 1)
created, err := orderSvc.Create(ctx, req, model.BuyerTypeAgent, shop.ID)
require.NoError(t, err)
err = orderSvc.WalletPay(ctx, created.ID, model.BuyerTypeAgent, shop.ID)

View File

@@ -124,6 +124,18 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
allocation.OneTimeCommissionValue = cfg.Value
}
}
// 处理强充配置
if req.EnableForceRecharge != nil {
allocation.EnableForceRecharge = *req.EnableForceRecharge
}
if req.ForceRechargeAmount != nil {
allocation.ForceRechargeAmount = *req.ForceRechargeAmount
}
if req.ForceRechargeTriggerType != nil {
allocation.ForceRechargeTriggerType = *req.ForceRechargeTriggerType
}
allocation.Creator = currentUserID
if err := s.allocationStore.Create(ctx, allocation); err != nil {
@@ -224,6 +236,17 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
allocation.OneTimeCommissionValue = 0
}
}
if req.EnableForceRecharge != nil {
allocation.EnableForceRecharge = *req.EnableForceRecharge
}
if req.ForceRechargeAmount != nil {
allocation.ForceRechargeAmount = *req.ForceRechargeAmount
}
if req.ForceRechargeTriggerType != nil {
allocation.ForceRechargeTriggerType = *req.ForceRechargeTriggerType
}
allocation.Updater = currentUserID
if configChanged {
@@ -399,10 +422,13 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
Mode: a.BaseCommissionMode,
Value: a.BaseCommissionValue,
},
EnableOneTimeCommission: a.EnableOneTimeCommission,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
EnableOneTimeCommission: a.EnableOneTimeCommission,
EnableForceRecharge: a.EnableForceRecharge,
ForceRechargeAmount: a.ForceRechargeAmount,
ForceRechargeTriggerType: a.ForceRechargeTriggerType,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
}
if a.EnableOneTimeCommission {