feat: 实现代理钱包订单创建和订单角色追踪功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
新增功能: - 代理在后台使用 wallet 支付时,订单直接完成(扣款 + 激活套餐) - 支持代理自购和代理代购场景 - 新增订单角色追踪字段(operator_id、operator_type、actual_paid_amount、purchase_role) - 订单查询支持 OR 逻辑(buyer_id 或 operator_id) - 钱包流水记录交易子类型和关联店铺 - 佣金逻辑调整:代理代购不产生佣金 数据库变更: - 订单表新增 4 个字段和 2 个索引 - 钱包流水表新增 2 个字段 - 包含迁移脚本和回滚脚本 文档: - 功能总结文档 - 部署指南 - OpenAPI 文档更新 - Specs 同步(新增 agent-order-role-tracking capability) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,13 +41,15 @@ type AgentWalletTransaction struct {
|
||||
AgentWalletID uint `gorm:"column:agent_wallet_id;not null;index;comment:代理钱包ID" json:"agent_wallet_id"`
|
||||
ShopID uint `gorm:"column:shop_id;not null;index;comment:店铺ID(冗余字段,便于查询)" json:"shop_id"`
|
||||
UserID uint `gorm:"column:user_id;not null;comment:操作人用户ID" json:"user_id"`
|
||||
TransactionType string `gorm:"column:transaction_type;type:varchar(20);not null;comment:交易类型(recharge-充值 | deduct-扣款 | refund-退款 | commission-分佣 | withdrawal-提现)" json:"transaction_type"`
|
||||
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:变动金额(单位:分,正数为增加,负数为减少)" json:"amount"`
|
||||
TransactionType string `gorm:"column:transaction_type;type:varchar(20);not null;comment:交易类型(recharge-充值 | deduct-扣款 | refund-退款 | commission-分佣 | withdrawal-提现)" json:"transaction_type"`
|
||||
TransactionSubtype *string `gorm:"column:transaction_subtype;type:varchar(50);comment:交易子类型(细分 order_payment 场景)" json:"transaction_subtype,omitempty"`
|
||||
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:变动金额(单位:分,正数为增加,负数为减少)" json:"amount"`
|
||||
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;comment:变动前余额(单位:分)" json:"balance_before"`
|
||||
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;comment:变动后余额(单位:分)" json:"balance_after"`
|
||||
Status int `gorm:"column:status;type:int;not null;default:1;comment:交易状态(1-成功 2-失败 3-处理中)" json:"status"`
|
||||
ReferenceType *string `gorm:"column:reference_type;type:varchar(50);comment:关联业务类型(order | commission | withdrawal | topup)" json:"reference_type,omitempty"`
|
||||
ReferenceID *uint `gorm:"column:reference_id;comment:关联业务ID" json:"reference_id,omitempty"`
|
||||
RelatedShopID *uint `gorm:"column:related_shop_id;comment:关联店铺ID(代购时记录下级店铺)" json:"related_shop_id,omitempty"`
|
||||
Remark *string `gorm:"column:remark;type:text;comment:备注" json:"remark,omitempty"`
|
||||
Metadata *string `gorm:"column:metadata;type:jsonb;comment:扩展信息(如手续费、支付方式等)" json:"metadata,omitempty"`
|
||||
Creator uint `gorm:"column:creator;not null;comment:创建人ID" json:"creator"`
|
||||
|
||||
@@ -16,6 +16,7 @@ type OrderListRequest struct {
|
||||
PaymentStatus *int `json:"payment_status" query:"payment_status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"支付状态 (1:待支付, 2:已支付, 3:已取消, 4:已退款)"`
|
||||
OrderType string `json:"order_type" query:"order_type" validate:"omitempty,oneof=single_card device" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
|
||||
OrderNo string `json:"order_no" query:"order_no" validate:"omitempty,max=30" maxLength:"30" description:"订单号(精确查询)"`
|
||||
PurchaseRole string `json:"purchase_role" query:"purchase_role" validate:"omitempty,oneof=self_purchase purchased_by_parent purchased_by_platform purchase_for_subordinate" description:"订单角色 (self_purchase:自己购买, purchased_by_parent:上级代理购买, purchased_by_platform:平台代购, purchase_for_subordinate:给下级购买)"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time" description:"创建时间起始"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time" description:"创建时间结束"`
|
||||
}
|
||||
@@ -49,9 +50,21 @@ type OrderResponse struct {
|
||||
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:"订单明细列表"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
|
||||
// 操作者信息
|
||||
OperatorID *uint `json:"operator_id,omitempty" description:"操作者ID"`
|
||||
OperatorType string `json:"operator_type,omitempty" description:"操作者类型 (platform:平台, agent:代理)"`
|
||||
OperatorName string `json:"operator_name,omitempty" description:"操作者名称"`
|
||||
ActualPaidAmount *int64 `json:"actual_paid_amount,omitempty" description:"实际支付金额(分)"`
|
||||
|
||||
// 订单角色
|
||||
PurchaseRole string `json:"purchase_role,omitempty" description:"订单角色 (self_purchase:自己购买, purchased_by_parent:上级代理购买, purchased_by_platform:平台代购, purchase_for_subordinate:给下级购买)"`
|
||||
IsPurchasedByParent bool `json:"is_purchased_by_parent" description:"是否由上级代理购买"`
|
||||
PurchaseRemark string `json:"purchase_remark,omitempty" description:"购买备注"`
|
||||
|
||||
Items []*OrderItemResponse `json:"items" description:"订单明细列表"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
type OrderListResponse struct {
|
||||
|
||||
@@ -42,6 +42,16 @@ type Order struct {
|
||||
|
||||
// 代购信息
|
||||
IsPurchaseOnBehalf bool `gorm:"column:is_purchase_on_behalf;type:boolean;default:false;comment:是否为代购订单" json:"is_purchase_on_behalf"`
|
||||
|
||||
// 操作者信息(谁下的单)
|
||||
OperatorID *uint `gorm:"column:operator_id;index:idx_orders_operator_id;comment:操作者ID(谁下的单)" json:"operator_id,omitempty"`
|
||||
OperatorType string `gorm:"column:operator_type;type:varchar(20);comment:操作者类型(platform/agent)" json:"operator_type,omitempty"`
|
||||
|
||||
// 实际支付金额(可能与订单金额不同,如代理代购场景)
|
||||
ActualPaidAmount *int64 `gorm:"column:actual_paid_amount;type:bigint;comment:实际支付金额(分)" json:"actual_paid_amount,omitempty"`
|
||||
|
||||
// 订单角色(标识订单中的买卖关系)
|
||||
PurchaseRole string `gorm:"column:purchase_role;type:varchar(50);index:idx_orders_purchase_role;comment:订单角色(self_purchase/purchased_by_parent/purchased_by_platform/purchase_for_subordinate)" json:"purchase_role,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
@@ -83,6 +93,14 @@ const (
|
||||
CommissionStatusCalculated = 2 // 已计算
|
||||
)
|
||||
|
||||
// 订单角色常量
|
||||
const (
|
||||
PurchaseRoleSelfPurchase = "self_purchase" // 自己购买
|
||||
PurchaseRolePurchasedByParent = "purchased_by_parent" // 上级代理购买
|
||||
PurchaseRolePurchasedByPlatform = "purchased_by_platform" // 平台代购
|
||||
PurchaseRolePurchaseForSubordinate = "purchase_for_subordinate" // 给下级购买
|
||||
)
|
||||
|
||||
// OrderItem 订单明细模型
|
||||
// 记录订单中购买的套餐明细,支持一个订单购买多个套餐
|
||||
type OrderItem struct {
|
||||
|
||||
@@ -131,35 +131,37 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
|
||||
// 提取资源所属店铺ID
|
||||
var resourceShopID *uint
|
||||
var seriesID *uint
|
||||
if validationResult.Card != nil {
|
||||
resourceShopID = validationResult.Card.ShopID
|
||||
seriesID = validationResult.Card.SeriesID
|
||||
} else if validationResult.Device != nil {
|
||||
resourceShopID = validationResult.Device.ShopID
|
||||
seriesID = validationResult.Device.SeriesID
|
||||
}
|
||||
|
||||
// 初始化订单字段
|
||||
orderBuyerType := buyerType
|
||||
orderBuyerID := buyerID
|
||||
totalAmount := validationResult.TotalPrice
|
||||
paymentMethod := req.PaymentMethod
|
||||
paymentStatus := model.PaymentStatusPending
|
||||
var paidAt *time.Time
|
||||
now := time.Now()
|
||||
isPurchaseOnBehalf := false
|
||||
|
||||
var seriesID *uint
|
||||
var sellerShopID *uint
|
||||
var operatorID *uint
|
||||
operatorType := ""
|
||||
var actualPaidAmount *int64
|
||||
purchaseRole := ""
|
||||
var sellerShopID *uint = resourceShopID
|
||||
var sellerCostPrice int64
|
||||
|
||||
if validationResult.Card != nil {
|
||||
seriesID = validationResult.Card.SeriesID
|
||||
sellerShopID = validationResult.Card.ShopID
|
||||
} else if validationResult.Device != nil {
|
||||
seriesID = validationResult.Device.SeriesID
|
||||
sellerShopID = validationResult.Device.ShopID
|
||||
}
|
||||
|
||||
if sellerShopID != nil && len(validationResult.Packages) > 0 {
|
||||
firstPackageID := validationResult.Packages[0].ID
|
||||
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, *sellerShopID, firstPackageID)
|
||||
if err == nil && allocation != nil {
|
||||
sellerCostPrice = allocation.CostPrice
|
||||
}
|
||||
}
|
||||
|
||||
// 场景判断:offline(平台代购)、wallet(代理钱包支付)、其他(待支付)
|
||||
if req.PaymentMethod == model.PaymentMethodOffline {
|
||||
// ==== 场景 1:平台代购(offline)====
|
||||
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -172,6 +174,82 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
paidAt = purchasePaidAt
|
||||
isPurchaseOnBehalf = true
|
||||
sellerCostPrice = buyerCostPrice
|
||||
|
||||
// 设置操作者信息(平台代购)
|
||||
operatorID = nil
|
||||
operatorType = constants.OwnerTypePlatform
|
||||
purchaseRole = model.PurchaseRolePurchasedByPlatform
|
||||
actualPaidAmount = nil
|
||||
|
||||
} else if req.PaymentMethod == model.PaymentMethodWallet {
|
||||
// ==== 场景 2:代理钱包支付(wallet)====
|
||||
// 只有代理账号可以使用钱包支付
|
||||
if buyerType != model.BuyerTypeAgent {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
|
||||
}
|
||||
operatorShopID := buyerID
|
||||
|
||||
// 判断资源是否属于操作者
|
||||
if resourceShopID == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "资源店铺ID为空")
|
||||
}
|
||||
|
||||
// 获取第一个套餐ID用于查询成本价
|
||||
if len(validationResult.Packages) == 0 {
|
||||
return nil, errors.New(errors.CodeInternalError, "套餐列表为空")
|
||||
}
|
||||
firstPackageID := validationResult.Packages[0].ID
|
||||
|
||||
if *resourceShopID == operatorShopID {
|
||||
// ==== 子场景 2.1:代理自购 ====
|
||||
costPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderBuyerType = model.BuyerTypeAgent
|
||||
orderBuyerID = operatorShopID
|
||||
totalAmount = costPrice
|
||||
paymentMethod = model.PaymentMethodWallet
|
||||
paymentStatus = model.PaymentStatusPaid
|
||||
paidAt = &now
|
||||
isPurchaseOnBehalf = false
|
||||
|
||||
operatorID = &operatorShopID
|
||||
operatorType = "agent"
|
||||
actualPaidAmountVal := costPrice
|
||||
actualPaidAmount = &actualPaidAmountVal
|
||||
purchaseRole = model.PurchaseRoleSelfPurchase
|
||||
sellerCostPrice = costPrice
|
||||
|
||||
} else {
|
||||
// ==== 子场景 2.2:代理代购(给下级购买)====
|
||||
// 获取买家成本价
|
||||
buyerCostPrice, err := s.getCostPrice(ctx, *resourceShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取操作者成本价
|
||||
operatorCostPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderBuyerType = model.BuyerTypeAgent
|
||||
orderBuyerID = *resourceShopID
|
||||
totalAmount = buyerCostPrice
|
||||
paymentMethod = model.PaymentMethodWallet
|
||||
paymentStatus = model.PaymentStatusPaid
|
||||
paidAt = &now
|
||||
isPurchaseOnBehalf = true
|
||||
|
||||
operatorID = &operatorShopID
|
||||
operatorType = "agent"
|
||||
actualPaidAmount = &operatorCostPrice
|
||||
purchaseRole = model.PurchaseRolePurchaseForSubordinate
|
||||
sellerCostPrice = buyerCostPrice
|
||||
}
|
||||
}
|
||||
|
||||
order := &model.Order{
|
||||
@@ -195,27 +273,48 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
SellerShopID: sellerShopID,
|
||||
SellerCostPrice: sellerCostPrice,
|
||||
IsPurchaseOnBehalf: isPurchaseOnBehalf,
|
||||
OperatorID: operatorID,
|
||||
OperatorType: operatorType,
|
||||
ActualPaidAmount: actualPaidAmount,
|
||||
PurchaseRole: purchaseRole,
|
||||
}
|
||||
|
||||
items := s.buildOrderItems(userID, validationResult.Packages)
|
||||
|
||||
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
|
||||
|
||||
// 根据支付方式选择创建订单的方式
|
||||
if req.PaymentMethod == model.PaymentMethodOffline {
|
||||
// 平台代购:创建订单并立即激活套餐
|
||||
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
|
||||
if err := s.orderStore.Create(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if req.PaymentMethod == model.PaymentMethodWallet {
|
||||
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
|
||||
if operatorID == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "钱包支付场景下 operatorID 不能为空")
|
||||
}
|
||||
operatorShopID := *operatorID
|
||||
buyerShopID := orderBuyerID
|
||||
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
|
||||
} else {
|
||||
// 其他支付方式:创建待支付订单
|
||||
if err := s.orderStore.Create(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) resolvePurchaseOnBehalfInfo(ctx context.Context, result *purchase_validation.PurchaseValidationResult) (uint, int64, *time.Time, error) {
|
||||
@@ -272,6 +371,184 @@ func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package) []
|
||||
return items
|
||||
}
|
||||
|
||||
// getCostPrice 查询店铺对套餐的成本价
|
||||
// shopID: 店铺ID
|
||||
// packageID: 套餐ID
|
||||
// 返回成本价(分),如果查询失败返回错误
|
||||
func (s *Service) getCostPrice(ctx context.Context, shopID uint, packageID uint) (int64, error) {
|
||||
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeInvalidParam, "店铺没有该套餐的分配配置")
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐成本价失败")
|
||||
}
|
||||
return allocation.CostPrice, nil
|
||||
}
|
||||
|
||||
// createWalletTransaction 创建钱包流水记录
|
||||
// ctx: 上下文
|
||||
// tx: 事务对象
|
||||
// walletID: 钱包ID
|
||||
// orderID: 订单ID
|
||||
// amount: 扣款金额(正数)
|
||||
// purchaseRole: 订单角色
|
||||
// relatedShopID: 关联店铺ID(代购场景填充下级店铺ID)
|
||||
func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, walletID uint, orderID uint, amount int64, purchaseRole string, relatedShopID *uint) error {
|
||||
var subtype *string
|
||||
remark := "购买套餐"
|
||||
|
||||
// 根据订单角色确定交易子类型和备注
|
||||
switch purchaseRole {
|
||||
case model.PurchaseRoleSelfPurchase:
|
||||
subtypeVal := constants.WalletTransactionSubtypeSelfPurchase
|
||||
subtype = &subtypeVal
|
||||
|
||||
case model.PurchaseRolePurchaseForSubordinate:
|
||||
subtypeVal := constants.WalletTransactionSubtypePurchaseForSubordinate
|
||||
subtype = &subtypeVal
|
||||
|
||||
// 查询下级店铺名称,填充到备注
|
||||
if relatedShopID != nil {
|
||||
var shop model.Shop
|
||||
if err := tx.Where("id = ?", *relatedShopID).First(&shop).Error; err == nil {
|
||||
remark = fmt.Sprintf("为下级代理【%s】购买套餐", shop.ShopName)
|
||||
} else {
|
||||
remark = "为下级代理购买套餐"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
|
||||
// 创建钱包流水记录
|
||||
transaction := &model.AgentWalletTransaction{
|
||||
AgentWalletID: walletID,
|
||||
ShopID: 0, // 将在下面从钱包记录获取
|
||||
UserID: userID,
|
||||
TransactionType: constants.AgentTransactionTypeDeduct,
|
||||
TransactionSubtype: subtype,
|
||||
Amount: -amount, // 扣款为负数
|
||||
BalanceBefore: 0, // 将在下面填充
|
||||
BalanceAfter: 0, // 将在下面填充
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: strPtr(constants.ReferenceTypeOrder),
|
||||
ReferenceID: &orderID,
|
||||
RelatedShopID: relatedShopID,
|
||||
Remark: &remark,
|
||||
Creator: userID,
|
||||
ShopIDTag: 0, // 将在下面填充
|
||||
EnterpriseIDTag: nil,
|
||||
}
|
||||
|
||||
// 查询钱包记录,获取 shop_id 和余额信息
|
||||
var wallet model.AgentWallet
|
||||
if err := tx.Where("id = ?", walletID).First(&wallet).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包信息失败")
|
||||
}
|
||||
|
||||
transaction.ShopID = wallet.ShopID
|
||||
transaction.ShopIDTag = wallet.ShopIDTag
|
||||
transaction.EnterpriseIDTag = wallet.EnterpriseIDTag
|
||||
transaction.BalanceBefore = wallet.Balance
|
||||
transaction.BalanceAfter = wallet.Balance - amount
|
||||
|
||||
if err := tx.Create(transaction).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// strPtr 字符串指针辅助函数
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// createOrderWithWalletPayment 使用钱包支付创建订单并完成支付
|
||||
// 包含余额检查、扣款、创建流水、激活套餐等操作,在事务中执行
|
||||
// ctx: 上下文
|
||||
// order: 订单对象
|
||||
// items: 订单明细列表
|
||||
// operatorShopID: 操作者店铺ID(扣款的店铺)
|
||||
// buyerShopID: 买家店铺ID(代购场景下级店铺ID)
|
||||
func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model.Order, items []*model.OrderItem, operatorShopID uint, buyerShopID uint) error {
|
||||
if order.ActualPaidAmount == nil {
|
||||
return errors.New(errors.CodeInternalError, "实际支付金额不能为空")
|
||||
}
|
||||
actualAmount := *order.ActualPaidAmount
|
||||
|
||||
// 1. 事务外:检查钱包余额(快速失败)
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, operatorShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
|
||||
}
|
||||
if wallet.Balance < actualAmount {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足")
|
||||
}
|
||||
|
||||
// 2. 事务内:创建订单 + 扣款 + 创建流水 + 激活套餐
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 2.1 创建订单
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
|
||||
}
|
||||
|
||||
// 2.2 创建订单明细
|
||||
for _, item := range items {
|
||||
item.OrderID = order.ID
|
||||
}
|
||||
if err := tx.CreateInBatches(items, 100).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
|
||||
}
|
||||
|
||||
// 2.3 扣减钱包余额(乐观锁)
|
||||
result := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, actualAmount, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", actualAmount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "扣减钱包余额失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
|
||||
}
|
||||
|
||||
// 2.4 创建钱包流水
|
||||
var relatedShopID *uint
|
||||
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
|
||||
relatedShopID = &buyerShopID
|
||||
}
|
||||
if err := s.createWalletTransaction(ctx, tx, wallet.ID, order.ID, actualAmount, order.PurchaseRole, relatedShopID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2.5 激活套餐
|
||||
if err := s.activatePackage(ctx, tx, order); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 事务外:佣金计算(异步)
|
||||
// 只有平台代购才入队佣金计算(operator_id == nil)
|
||||
if order.OperatorID == nil {
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
}
|
||||
|
||||
return 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 {
|
||||
@@ -329,6 +606,9 @@ func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType
|
||||
if req.OrderNo != "" {
|
||||
filters["order_no"] = req.OrderNo
|
||||
}
|
||||
if req.PurchaseRole != "" {
|
||||
filters["purchase_role"] = req.PurchaseRole
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
filters["start_time"] = req.StartTime
|
||||
}
|
||||
@@ -1024,6 +1304,33 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
statusText = "已退款"
|
||||
}
|
||||
|
||||
// 查询操作者名称
|
||||
operatorName := ""
|
||||
if order.OperatorType == "agent" && order.OperatorID != nil {
|
||||
var shop model.Shop
|
||||
if err := s.db.Where("id = ?", *order.OperatorID).First(&shop).Error; err == nil {
|
||||
operatorName = shop.ShopName
|
||||
}
|
||||
}
|
||||
|
||||
// 生成派生字段
|
||||
isPurchasedByParent := order.PurchaseRole == model.PurchaseRolePurchasedByParent
|
||||
purchaseRemark := ""
|
||||
switch order.PurchaseRole {
|
||||
case model.PurchaseRolePurchasedByParent:
|
||||
if operatorName != "" {
|
||||
purchaseRemark = fmt.Sprintf("由上级代理【%s】购买", operatorName)
|
||||
} else {
|
||||
purchaseRemark = "由上级代理购买"
|
||||
}
|
||||
case model.PurchaseRolePurchasedByPlatform:
|
||||
purchaseRemark = "由平台代购"
|
||||
case model.PurchaseRolePurchaseForSubordinate:
|
||||
if operatorName != "" {
|
||||
purchaseRemark = fmt.Sprintf("由【%s】为下级购买", operatorName)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.OrderResponse{
|
||||
ID: order.ID,
|
||||
OrderNo: order.OrderNo,
|
||||
@@ -1040,9 +1347,21 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
|
||||
CommissionStatus: order.CommissionStatus,
|
||||
CommissionConfigVersion: order.CommissionConfigVersion,
|
||||
Items: itemResponses,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
|
||||
// 操作者信息
|
||||
OperatorID: order.OperatorID,
|
||||
OperatorType: order.OperatorType,
|
||||
OperatorName: operatorName,
|
||||
ActualPaidAmount: order.ActualPaidAmount,
|
||||
|
||||
// 订单角色
|
||||
PurchaseRole: order.PurchaseRole,
|
||||
IsPurchasedByParent: isPurchasedByParent,
|
||||
PurchaseRemark: purchaseRemark,
|
||||
|
||||
Items: itemResponses,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,18 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.Order{})
|
||||
// 应用数据权限过滤(使用 seller_shop_id 字段)
|
||||
query = middleware.ApplySellerShopFilter(ctx, query)
|
||||
|
||||
// 应用数据权限过滤
|
||||
// 代理用户:可以查看作为买家或操作者的订单
|
||||
// 平台用户/超管:可以查看所有订单
|
||||
subordinateShopIDs := middleware.GetSubordinateShopIDs(ctx)
|
||||
if len(subordinateShopIDs) > 0 {
|
||||
// 代理用户:WHERE (buyer_type = 'agent' AND buyer_id IN ?) OR operator_id IN ?
|
||||
query = query.Where(
|
||||
s.db.Where("buyer_type = ? AND buyer_id IN ?", model.BuyerTypeAgent, subordinateShopIDs).
|
||||
Or("operator_id IN ?", subordinateShopIDs),
|
||||
)
|
||||
}
|
||||
|
||||
if v, ok := filters["payment_status"]; ok {
|
||||
query = query.Where("payment_status = ?", v)
|
||||
@@ -108,6 +118,9 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
|
||||
if v, ok := filters["buyer_id"]; ok {
|
||||
query = query.Where("buyer_id = ?", v)
|
||||
}
|
||||
if v, ok := filters["purchase_role"]; ok {
|
||||
query = query.Where("purchase_role = ?", v)
|
||||
}
|
||||
if v, ok := filters["iot_card_id"]; ok {
|
||||
query = query.Where("iot_card_id = ?", v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user