feat(order): 支持代购订单和强充要求检查
- OrderService 新增代购订单支持 - 强充要求检查(首次购买最低充值) - 代购订单支付限制(无需支付) - 强充金额验证 - 新增 OrderDTO 请求/响应结构 - PurchaseCheckRequest/Response(购买预检) - CreatePurchaseOnBehalfRequest(代购订单创建) - Order 模型新增支付方式 - PaymentMethodOffline(线下支付,仅平台代购使用) - OrderService 依赖注入扩展 - 新增 SeriesAllocationStore、IotCardStore、DeviceStore - 支持强充要求检查逻辑 - 完整的集成测试覆盖(534 行) - 代购订单创建、强充验证、支付限制等场景 Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -27,6 +27,9 @@ type Service struct {
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
queueClient *queue.Client
|
||||
logger *zap.Logger
|
||||
@@ -39,6 +42,9 @@ func New(
|
||||
walletStore *postgres.WalletStore,
|
||||
purchaseValidationService *purchase_validation.Service,
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
wechatPayment wechat.PaymentServiceInterface,
|
||||
queueClient *queue.Client,
|
||||
logger *zap.Logger,
|
||||
@@ -50,6 +56,9 @@ func New(
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
allocationConfigStore: allocationConfigStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
wechatPayment: wechatPayment,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
@@ -78,6 +87,11 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(validationResult)
|
||||
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
|
||||
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
|
||||
|
||||
@@ -254,6 +268,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
return errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.IsPurchaseOnBehalf {
|
||||
return errors.New(errors.CodeInvalidStatus, "代购订单无需支付")
|
||||
}
|
||||
|
||||
var resourceType string
|
||||
var resourceID uint
|
||||
|
||||
@@ -646,3 +664,206 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
|
||||
H5URL: result.H5URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ForceRechargeRequirement struct {
|
||||
NeedForceRecharge bool
|
||||
ForceRechargeAmount int64
|
||||
TriggerType string
|
||||
}
|
||||
|
||||
func (s *Service) checkForceRechargeRequirement(result *purchase_validation.PurchaseValidationResult) *ForceRechargeRequirement {
|
||||
if result.Allocation == nil {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
|
||||
allocation := result.Allocation
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
|
||||
var firstCommissionPaid bool
|
||||
if result.Card != nil {
|
||||
firstCommissionPaid = result.Card.FirstCommissionPaid
|
||||
} else if result.Device != nil {
|
||||
firstCommissionPaid = result.Device.FirstCommissionPaid
|
||||
}
|
||||
|
||||
if firstCommissionPaid {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerSingleRecharge {
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
ForceRechargeAmount: allocation.OneTimeCommissionThreshold,
|
||||
TriggerType: model.OneTimeCommissionTriggerSingleRecharge,
|
||||
}
|
||||
}
|
||||
|
||||
if allocation.EnableForceRecharge {
|
||||
forceAmount := allocation.ForceRechargeAmount
|
||||
if forceAmount == 0 {
|
||||
forceAmount = allocation.OneTimeCommissionThreshold
|
||||
}
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
ForceRechargeAmount: forceAmount,
|
||||
TriggerType: model.OneTimeCommissionTriggerAccumulatedRecharge,
|
||||
}
|
||||
}
|
||||
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
|
||||
func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRequest) (*dto.PurchaseCheckResponse, error) {
|
||||
var validationResult *purchase_validation.PurchaseValidationResult
|
||||
var err error
|
||||
|
||||
if req.OrderType == model.OrderTypeSingleCard {
|
||||
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, req.ResourceID, req.PackageIDs)
|
||||
} else if req.OrderType == model.OrderTypeDevice {
|
||||
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, req.ResourceID, req.PackageIDs)
|
||||
} else {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(validationResult)
|
||||
|
||||
response := &dto.PurchaseCheckResponse{
|
||||
TotalPackageAmount: validationResult.TotalPrice,
|
||||
NeedForceRecharge: forceRechargeCheck.NeedForceRecharge,
|
||||
ForceRechargeAmount: forceRechargeCheck.ForceRechargeAmount,
|
||||
ActualPayment: validationResult.TotalPrice,
|
||||
WalletCredit: validationResult.TotalPrice,
|
||||
}
|
||||
|
||||
if forceRechargeCheck.NeedForceRecharge {
|
||||
if validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
|
||||
response.ActualPayment = forceRechargeCheck.ForceRechargeAmount
|
||||
response.WalletCredit = forceRechargeCheck.ForceRechargeAmount
|
||||
response.Message = "首次购买需满足最低充值要求"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user