订单相关以及佣金相关修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s

This commit is contained in:
2026-04-30 09:52:39 +08:00
parent 4f4e42f27e
commit 66e12e9629
29 changed files with 1199 additions and 181 deletions

View File

@@ -19,7 +19,7 @@ type CommissionRecord struct {
CommissionSource string `gorm:"column:commission_source;type:varchar(20);not null;index;comment:佣金来源 cost_diff-成本价差 one_time-一次性佣金" json:"commission_source"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:佣金金额(分)" json:"amount"`
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;default:0;comment:入账后钱包余额(分)" json:"balance_after"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-已冻结 2-解冻中 3-已发放 4-已失效" json:"status"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-已冻结 2-解冻中 3-已发放 4-已失效 99-待人工修正" json:"status"`
ReleasedAt *time.Time `gorm:"column:released_at;comment:入账时间" json:"released_at"`
Remark string `gorm:"column:remark;type:varchar(500);comment:备注" json:"remark"`
}

View File

@@ -29,7 +29,7 @@ type CommissionRecordListRequest struct {
CommissionSource *string `json:"commission_source" query:"commission_source" validate:"omitempty,oneof=cost_diff one_time" description:"佣金来源 (cost_diff:成本价差, one_time:一次性佣金)"`
StartTime *string `json:"start_time" query:"start_time" validate:"omitempty" description:"开始时间"`
EndTime *string `json:"end_time" query:"end_time" validate:"omitempty" description:"结束时间"`
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2 99" description:"状态 (1:已入账, 2:已失效, 99:待人工修正)"`
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2 3 4 99" description:"状态 (1:已冻结, 2:解冻中, 3:已发放, 4:已失效, 99:待人工修正)"`
}
// CommissionRecordPageResult 佣金记录分页结果

View File

@@ -65,14 +65,18 @@ type OrderResponse struct {
PaidAt *time.Time `json:"paid_at,omitempty" description:"支付时间"`
PaymentVoucherKey string `json:"payment_voucher_key,omitempty" description:"线下支付凭证对象存储file_key仅线下支付订单有值"`
IsPurchaseOnBehalf bool `json:"is_purchase_on_behalf" description:"是否为代购订单"`
CommissionStatus int `json:"commission_status" description:"佣金状态 (1:待计算, 2:已计算)"`
CommissionStatusName string `json:"commission_status_name" description:"佣金状态名称(中文)"`
CommissionStatus int `json:"commission_status" description:"佣金流程状态 (1:待计算, 2:已完成, 3:待人工处理)"`
CommissionStatusName string `json:"commission_status_name" description:"佣金流程状态名称(中文)"`
CommissionResult int `json:"commission_result" description:"佣金业务结果 (0:未知, 1:有佣金, 2:无佣金, 3:链路异常)"`
CommissionResultName string `json:"commission_result_name" description:"佣金业务结果名称(中文)"`
CommissionConfigVersion int `json:"commission_config_version" description:"佣金配置版本"`
SellerShopID *uint `json:"seller_shop_id,omitempty" description:"销售店铺ID业务归属字段"`
SellerShopName string `json:"seller_shop_name,omitempty" 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:"操作者名称"`
OperatorID *uint `json:"operator_id,omitempty" description:"操作者账号ID真实发起操作的账号/主体ID"`
OperatorType string `json:"operator_type,omitempty" description:"操作者账号类型 (platform:平台, agent:代理, enterprise:企业, personal_customer:个人客户)"`
OperatorName string `json:"operator_name,omitempty" description:"操作者名称(按账号语义展示)"`
ActualPaidAmount *int64 `json:"actual_paid_amount,omitempty" description:"实际支付金额(分)"`
// 订单角色

View File

@@ -37,7 +37,8 @@ type Order struct {
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at,omitempty"`
// 佣金信息
CommissionStatus int `gorm:"column:commission_status;type:int;default:1;not null;comment:佣金状态 1-待计算 2-已计算" json:"commission_status"`
CommissionStatus int `gorm:"column:commission_status;type:int;default:1;not null;comment:佣金流程状态 1-待计算 2-已完成 3-待人工处理" json:"commission_status"`
CommissionResult int `gorm:"column:commission_result;type:int;default:0;not null;comment:佣金业务结果 0-未知 1-有佣金 2-无佣金 3-链路异常" json:"commission_result"`
CommissionConfigVersion int `gorm:"column:commission_config_version;type:int;default:0;comment:佣金配置版本(订单创建时快照)" json:"commission_config_version"`
SellerShopID *uint `gorm:"column:seller_shop_id;index;comment:销售店铺ID用于成本价差佣金计算" json:"seller_shop_id,omitempty"`
SellerCostPrice int64 `gorm:"column:seller_cost_price;type:bigint;default:0;comment:销售成本价(分,用于计算利润)" json:"seller_cost_price"`
@@ -51,8 +52,11 @@ 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"`
OperatorAccountID *uint `gorm:"column:operator_account_id;index:idx_orders_operator_account_id;comment:操作者账号ID权威字段" json:"operator_account_id,omitempty"`
OperatorAccountType string `gorm:"column:operator_account_type;type:varchar(32);default:'';not null;comment:操作者账号类型platform/agent/enterprise/personal_customer" json:"operator_account_type,omitempty"`
OperatorAccountName string `gorm:"column:operator_account_name;type:varchar(255);default:'';not null;comment:操作者名称快照" json:"operator_account_name,omitempty"`
OperatorID *uint `gorm:"column:operator_id;index:idx_orders_operator_id;comment:操作者ID历史兼容字段旧语义可能保存代理店铺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"`
@@ -103,10 +107,22 @@ const (
PaymentStatusRefunded = 4 // 已退款
)
// 佣金状态常量
// 佣金流程状态常量
const (
CommissionStatusPending = 1 // 待计算
CommissionStatusCalculated = 2 // 已计算
CommissionStatusPending = 1 // 待计算
CommissionStatusCompleted = 2 // 已完成
CommissionStatusPendingReview = 3 // 待人工处理
// CommissionStatusCalculated 兼容旧命名,语义已调整为“已完成”
CommissionStatusCalculated = CommissionStatusCompleted
)
// 佣金业务结果常量
const (
CommissionResultUnknown = 0 // 未知(历史兼容态)
CommissionResultHasAmount = 1 // 有佣金
CommissionResultNoAmount = 2 // 无佣金
CommissionResultChainBroken = 3 // 链路异常
)
// 订单角色常量
@@ -117,21 +133,29 @@ const (
PurchaseRolePurchaseForSubordinate = "purchase_for_subordinate" // 给下级购买
)
// 操作者账号类型常量
const (
OperatorAccountTypePlatform = "platform" // 平台账号
OperatorAccountTypeAgent = "agent" // 代理账号
OperatorAccountTypeEnterprise = "enterprise" // 企业账号
OperatorAccountTypePersonalCustomer = "personal_customer" // 个人客户
)
// OrderItem 订单明细模型
// 记录订单中购买的套餐明细,支持一个订单购买多个套餐
type OrderItem struct {
gorm.Model
BaseModel `gorm:"embedded"`
OrderID uint `gorm:"column:order_id;index:idx_order_item_order_id;not null;comment:订单ID" json:"order_id"`
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
PackageName string `gorm:"column:package_name;type:varchar(100);not null;comment:套餐名称(快照)" json:"package_name"`
PackageType *string `gorm:"column:package_type;type:varchar(20);comment:套餐类型快照 formal-正式套餐 addon-加油包" json:"package_type"`
Quantity int `gorm:"column:quantity;type:int;default:1;not null;comment:数量" json:"quantity"`
UnitPrice int64 `gorm:"column:unit_price;type:bigint;not null;comment:单价(分)" json:"unit_price"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:小计金额(分)" json:"amount"`
PackagePriceConfigStatus int `gorm:"column:package_price_config_status;type:int;default:0;not null;comment:套餐价格配置状态快照 0-未配置 1-赠送0价 2-已配置非0" json:"package_price_config_status"`
PackageIsGift bool `gorm:"column:package_is_gift;type:boolean;default:false;not null;comment:套餐是否赠送快照 true-赠送 false-普通可售" json:"package_is_gift"`
OrderID uint `gorm:"column:order_id;index:idx_order_item_order_id;not null;comment:订单ID" json:"order_id"`
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
PackageName string `gorm:"column:package_name;type:varchar(100);not null;comment:套餐名称(快照)" json:"package_name"`
PackageType *string `gorm:"column:package_type;type:varchar(20);comment:套餐类型快照 formal-正式套餐 addon-加油包" json:"package_type"`
Quantity int `gorm:"column:quantity;type:int;default:1;not null;comment:数量" json:"quantity"`
UnitPrice int64 `gorm:"column:unit_price;type:bigint;not null;comment:单价(分)" json:"unit_price"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:小计金额(分)" json:"amount"`
PackagePriceConfigStatus int `gorm:"column:package_price_config_status;type:int;default:0;not null;comment:套餐价格配置状态快照 0-未配置 1-赠送0价 2-已配置非0" json:"package_price_config_status"`
PackageIsGift bool `gorm:"column:package_is_gift;type:boolean;default:false;not null;comment:套餐是否赠送快照 true-赠送 false-普通可售" json:"package_is_gift"`
}
// TableName 指定表名

View File

@@ -477,6 +477,28 @@ func (s *Service) fetchBuyerSnapshot(ctx context.Context, customerID uint) (phon
return phone, nickname
}
func (s *Service) buildPersonalCustomerOperatorSnapshot(ctx context.Context, customerID uint, nickname *string) (*uint, string, string) {
if customerID == 0 {
return nil, "", ""
}
name := ""
if nickname != nil {
name = strings.TrimSpace(*nickname)
}
if name == "" {
customer, err := s.personalCustomerStore.GetByID(ctx, customerID)
if err != nil {
s.logger.Warn("buildPersonalCustomerOperatorSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err))
} else if customer != nil {
name = strings.TrimSpace(customer.Nickname)
}
}
id := customerID
return &id, model.OperatorAccountTypePersonalCustomer, name
}
func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result *purchase_validation.PurchaseValidationResult, sellerCostPrice int64) (*model.Order, error) {
orderType := resolveOrderType(result)
if orderType == "" {
@@ -515,6 +537,7 @@ func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result
}
order.BuyerPhone, order.BuyerNickname = s.fetchBuyerSnapshot(ctx, customerID)
order.OperatorAccountID, order.OperatorAccountType, order.OperatorAccountName = s.buildPersonalCustomerOperatorSnapshot(ctx, customerID, order.BuyerNickname)
return order, nil
}

View File

@@ -78,8 +78,11 @@ func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error {
return errors.Wrap(errors.CodeDatabaseError, err, "获取订单失败")
}
if order.CommissionStatus == model.CommissionStatusCalculated {
s.logger.Warn("订单佣金已计算,跳过", zap.String("order_id", fmt.Sprint(orderID)))
if order.CommissionStatus == model.CommissionStatusCompleted || order.CommissionStatus == model.CommissionStatusPendingReview {
s.logger.Warn("订单佣金流程已结束,跳过",
zap.String("order_id", fmt.Sprint(orderID)),
zap.Int("commission_status", order.CommissionStatus),
)
return nil
}
@@ -88,13 +91,8 @@ func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error {
return errors.Wrap(errors.CodeInternalError, err, "计算成本价差佣金失败")
}
for _, record := range costDiffRecords {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建成本价差佣金记录失败")
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "佣金入账失败")
}
if err := s.persistCommissionRecordsInTx(ctx, tx, costDiffRecords); err != nil {
return err
}
// 代购订单不触发一次性佣金和累计充值更新
@@ -110,8 +108,16 @@ func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error {
}
}
if err := tx.Model(&model.Order{}).Where("id = ?", orderID).Update("commission_status", model.CommissionStatusCalculated).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新订单佣金状态失败")
commissionStatus, commissionResult, err := s.resolveOrderCommissionOutcomeInTx(tx, order.ID)
if err != nil {
return err
}
if err := tx.Model(&model.Order{}).Where("id = ?", orderID).Updates(map[string]any{
"commission_status": commissionStatus,
"commission_result": commissionResult,
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新订单佣金结果失败")
}
return nil
@@ -190,12 +196,7 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
Status: constants.CommissionStatusPendingReview,
Remark: fmt.Sprintf("套餐系列[%d]未分配给该代理(shop_id=%d),成本价差链路断裂,请人工核查", *order.SeriesID, currentShop.ID),
}
if createErr := s.commissionRecordStore.Create(ctx, pendingRecord); createErr != nil {
s.logger.Warn("创建待审佣金记录失败",
zap.Uint("shop_id", currentShop.ID),
zap.Uint("order_id", order.ID),
zap.Error(createErr))
}
records = append(records, pendingRecord)
break
}
@@ -284,13 +285,8 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
return err
}
for _, record := range records {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
}
if err := s.persistCommissionRecordsInTx(ctx, tx, records); err != nil {
return err
}
card.SetFirstRechargeTriggeredBySeries(seriesID, true)
@@ -368,13 +364,8 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
return err
}
for _, record := range records {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
}
if err := s.persistCommissionRecordsInTx(ctx, tx, records); err != nil {
return err
}
device.SetFirstRechargeTriggeredBySeries(seriesID, true)
@@ -544,12 +535,7 @@ func (s *Service) calculateChainOneTimeCommission(ctx context.Context, bottomSho
Status: constants.CommissionStatusPendingReview,
Remark: fmt.Sprintf("套餐系列[%d]未分配给该代理(shop_id=%d),一次性佣金链路断裂,请人工核查", seriesID, parentShopID),
}
if createErr := s.commissionRecordStore.Create(ctx, pendingRecord); createErr != nil {
s.logger.Warn("创建待审佣金记录失败",
zap.Uint("shop_id", parentShopID),
zap.Uint("order_id", order.ID),
zap.Error(createErr))
}
records = append(records, pendingRecord)
break
}
@@ -686,6 +672,48 @@ func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record
return nil
}
func (s *Service) persistCommissionRecordsInTx(ctx context.Context, tx *gorm.DB, records []*model.CommissionRecord) error {
for _, record := range records {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金记录失败")
}
if record.Status == constants.CommissionStatusPendingReview {
continue
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "佣金入账失败")
}
}
return nil
}
func (s *Service) resolveOrderCommissionOutcomeInTx(tx *gorm.DB, orderID uint) (int, int, error) {
var pendingReviewCount int64
if err := tx.Model(&model.CommissionRecord{}).
Where("order_id = ? AND status = ?", orderID, constants.CommissionStatusPendingReview).
Count(&pendingReviewCount).Error; err != nil {
return 0, 0, errors.Wrap(errors.CodeDatabaseError, err, "统计待人工处理佣金记录失败")
}
if pendingReviewCount > 0 {
return model.CommissionStatusPendingReview, model.CommissionResultChainBroken, nil
}
var releasedCount int64
if err := tx.Model(&model.CommissionRecord{}).
Where("order_id = ? AND status = ? AND amount > 0", orderID, constants.CommissionStatusReleased).
Count(&releasedCount).Error; err != nil {
return 0, 0, errors.Wrap(errors.CodeDatabaseError, err, "统计已发放佣金记录失败")
}
if releasedCount > 0 {
return model.CommissionStatusCompleted, model.CommissionResultHasAmount, nil
}
return model.CommissionStatusCompleted, model.CommissionResultNoAmount, nil
}
func (s *Service) CreditCommission(ctx context.Context, record *model.CommissionRecord) error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.creditCommissionInTx(ctx, tx, record)

View File

@@ -206,6 +206,7 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
}
userID := middleware.GetUserIDFromContext(ctx)
operatorAccountID, operatorAccountType, operatorAccountName := s.buildAccountOperatorSnapshot(ctx, userID, operatorUserType)
// 提取资源所属店铺ID
var seriesID *uint
@@ -261,44 +262,44 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
actualPaidAmount = &actualPaidAmountVal
purchaseRole = model.PurchaseRoleSelfPurchase
} else {
isPlatformOwned := resourceShopID == nil || *resourceShopID == 0
isPlatformOwned := resourceShopID == nil || *resourceShopID == 0
if isPlatformOwned {
// ==== 子场景 1.1:平台自营(资源未分配给代理商)====
// 平台直接销售给终端用户,无代理商参与,使用零售价,无需查询分配配置
orderBuyerType = model.BuyerTypePersonal
orderBuyerID = 0
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
sellerCostPrice = 0
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRoleSelfPurchase
actualPaidAmount = nil
} else {
// ==== 子场景 1.2:平台代购(资源属于代理商,平台代为购买)====
purchaseBuyerID, buyerCostPriceMap, buyerTotalCost, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
if isPlatformOwned {
// ==== 子场景 1.1:平台自营(资源未分配给代理商)====
// 平台直接销售给终端用户,无代理商参与,使用零售价,无需查询分配配置
orderBuyerType = model.BuyerTypePersonal
orderBuyerID = 0
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
sellerCostPrice = 0
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRoleSelfPurchase
actualPaidAmount = nil
} else {
// ==== 子场景 1.2:平台代购(资源属于代理商,平台代为购买)====
purchaseBuyerID, buyerCostPriceMap, buyerTotalCost, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
itemUnitPriceMap = buyerCostPriceMap
totalAmount = buyerTotalCost
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerTotalCost
// 设置操作者信息(平台代购)
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
itemUnitPriceMap = buyerCostPriceMap
totalAmount = buyerTotalCost
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerTotalCost
// 设置操作者信息(平台代购)
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
}
}
} else if req.PaymentMethod == model.PaymentMethodWallet {
@@ -445,6 +446,9 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
OperatorAccountID: operatorAccountID,
OperatorAccountType: operatorAccountType,
OperatorAccountName: operatorAccountName,
OperatorID: operatorID,
OperatorType: operatorType,
ActualPaidAmount: actualPaidAmount,
@@ -475,7 +479,7 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
s.enqueueCommissionCalculation(ctx, order.ID)
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
return s.buildOrderResponse(ctx, order, items), nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
@@ -489,7 +493,7 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
return s.buildOrderResponse(ctx, order, items), nil
} else {
// 不应该到这里DTO 验证已拒绝其他支付方式)
@@ -563,6 +567,8 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
}
userID := middleware.GetUserIDFromContext(ctx)
operatorUserType := middleware.GetUserTypeFromContext(ctx)
operatorAccountID, operatorAccountType, operatorAccountName := s.buildAccountOperatorSnapshot(ctx, userID, operatorUserType)
// 提取资源所属店铺ID
var resourceShopID *uint
@@ -732,6 +738,9 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
OperatorAccountID: operatorAccountID,
OperatorAccountType: operatorAccountType,
OperatorAccountName: operatorAccountName,
OperatorID: operatorID,
OperatorType: operatorType,
ActualPaidAmount: actualPaidAmount,
@@ -752,7 +761,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
}
s.enqueueCommissionCalculation(ctx, order.ID)
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
return s.buildOrderResponse(ctx, order, items), nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
@@ -766,7 +775,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
return s.buildOrderResponse(ctx, order, items), nil
} else {
// 其他支付方式创建待支付订单H5 端支持 wechat/alipay
@@ -777,7 +786,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
return s.buildOrderResponse(ctx, order, items), nil
}
}
@@ -909,6 +918,57 @@ func (s *Service) fetchBuyerSnapshot(ctx context.Context, customerID uint) (phon
return phone, nickname
}
func (s *Service) buildAccountOperatorSnapshot(ctx context.Context, userID uint, userType int) (*uint, string, string) {
if userID == 0 {
return nil, "", ""
}
var account model.Account
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&account).Error; err != nil {
s.logger.Warn("buildAccountOperatorSnapshot: 查询账号失败", zap.Uint("user_id", userID), zap.Error(err))
id := userID
return &id, orderOperatorAccountTypeFromUserType(userType), ""
}
id := account.ID
return &id, orderOperatorAccountTypeFromUserType(account.UserType), account.Username
}
func (s *Service) buildPersonalCustomerOperatorSnapshot(ctx context.Context, customerID uint, nickname *string) (*uint, string, string) {
if customerID == 0 {
return nil, "", ""
}
name := ""
if nickname != nil {
name = strings.TrimSpace(*nickname)
}
if name == "" && s.personalCustomerStore != nil {
customer, err := s.personalCustomerStore.GetByID(ctx, customerID)
if err != nil {
s.logger.Warn("buildPersonalCustomerOperatorSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err))
} else if customer != nil {
name = strings.TrimSpace(customer.Nickname)
}
}
id := customerID
return &id, model.OperatorAccountTypePersonalCustomer, name
}
func orderOperatorAccountTypeFromUserType(userType int) string {
switch userType {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
return model.OperatorAccountTypePlatform
case constants.UserTypeAgent:
return model.OperatorAccountTypeAgent
case constants.UserTypeEnterprise:
return model.OperatorAccountTypeEnterprise
default:
return ""
}
}
// getCostPrice 查询店铺对套餐的成本价
// shopID: 店铺ID
// packageID: 套餐ID
@@ -1078,9 +1138,8 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
return err
}
// 3. 事务外:佣金计算(异步)
// 仅兼容极少数“平台代购但走钱包事务”的特殊场景,普通钱包订单不在此处入队佣金
if order.PurchaseRole == model.PurchaseRolePurchasedByPlatform {
// 3. 事务外:所有已支付且适用差价佣金的订单都进入佣金计算
if order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, order.ID)
}
@@ -1113,7 +1172,7 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.OrderResponse, error)
return nil, err
}
return s.buildOrderResponse(order, items), nil
return s.buildOrderResponse(ctx, order, items), nil
}
func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType string, buyerID uint) (*dto.OrderListResponse, error) {
@@ -1204,7 +1263,7 @@ func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType
var list []*dto.OrderResponse
for _, o := range orders {
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
list = append(list, s.buildOrderResponse(ctx, o, itemsMap[o.ID]))
}
return &dto.OrderListResponse{
@@ -1478,6 +1537,7 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
// 根据资源类型选择对应的钱包系统
now := time.Now()
actualPaidAmount := order.TotalAmount
shouldEnqueueCommission := false
if resourceType == "shop" {
// 代理钱包系统(店铺)
@@ -1527,6 +1587,7 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
actualPaidAmountSnapshot := actualPaidAmount
order.ActualPaidAmount = &actualPaidAmountSnapshot
shouldEnqueueCommission = true
walletResult := tx.Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
@@ -1645,7 +1706,9 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return err
}
s.enqueueCommissionCalculation(ctx, orderID)
if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, orderID)
}
return nil
}
@@ -1659,6 +1722,8 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
}
now := time.Now()
shouldResumeAfterPayment := false
shouldEnqueueCommission := false
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPending).
@@ -1694,6 +1759,8 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
// 回调更新成功后,同步内存对象,确保激活套餐时写入 paid_amount 快照
actualPaidAmountSnapshot := actualPaidAmount
order.ActualPaidAmount = &actualPaidAmountSnapshot
shouldResumeAfterPayment = true
shouldEnqueueCommission = true
return s.activatePackage(ctx, tx, order)
})
@@ -1702,8 +1769,12 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
return err
}
s.tryResumeAfterPayment(ctx, order)
s.enqueueCommissionCalculation(ctx, order.ID)
if shouldResumeAfterPayment {
s.tryResumeAfterPayment(ctx, order)
}
if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, order.ID)
}
return nil
}
@@ -2169,24 +2240,24 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
Creator: order.Creator,
Updater: order.Creator,
},
OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID,
PackageName: pkg.PackageName,
UsageType: order.OrderType,
DataLimitMB: pkg.RealDataMB,
OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID,
PackageName: pkg.PackageName,
UsageType: order.OrderType,
DataLimitMB: pkg.RealDataMB,
VirtualTotalMBSnapshot: virtualTotalMBSnapshot,
DisplayGainRatioSnapshot: displayGainRatioSnapshot,
EnableVirtualDataSnapshot: enableVirtualDataSnapshot,
Status: status,
Priority: priority,
MasterUsageID: &mainPackage.ID,
ActivatedAt: &activatedAt,
ExpiresAt: &expiresAt,
DataResetCycle: pkg.DataResetCycle,
PaidAmount: order.ActualPaidAmount,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
Status: status,
Priority: priority,
MasterUsageID: &mainPackage.ID,
ActivatedAt: &activatedAt,
ExpiresAt: &expiresAt,
DataResetCycle: pkg.DataResetCycle,
PaidAmount: order.ActualPaidAmount,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
if carrierType == "iot_card" {
@@ -2223,7 +2294,7 @@ func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint
zap.String("task_type", constants.TaskTypeCommission))
}
func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
func (s *Service) buildOrderResponse(ctx context.Context, order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
var itemResponses []*dto.OrderItemResponse
for _, item := range items {
itemResponses = append(itemResponses, &dto.OrderItemResponse{
@@ -2249,14 +2320,8 @@ 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
}
}
operatorID, operatorType, operatorName := s.resolveOrderOperatorView(ctx, order)
sellerShopName := s.resolveSellerShopName(ctx, order.SellerShopID)
// 生成派生字段
isPurchasedByParent := order.PurchaseRole == model.PurchaseRolePurchasedByParent
@@ -2288,6 +2353,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
actualPaidAmountSnapshot := order.TotalAmount
actualPaidAmount = &actualPaidAmountSnapshot
}
commissionResult := s.resolveOrderCommissionResult(ctx, order)
return &dto.OrderResponse{
ID: order.ID,
@@ -2308,10 +2374,14 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
CommissionStatus: order.CommissionStatus,
CommissionStatusName: constants.GetOrderCommissionStatusName(order.CommissionStatus),
CommissionResult: commissionResult,
CommissionResultName: constants.GetOrderCommissionResultName(commissionResult),
CommissionConfigVersion: order.CommissionConfigVersion,
SellerShopID: order.SellerShopID,
SellerShopName: sellerShopName,
OperatorID: order.OperatorID,
OperatorType: order.OperatorType,
OperatorID: operatorID,
OperatorType: operatorType,
OperatorName: operatorName,
ActualPaidAmount: actualPaidAmount,
@@ -2331,6 +2401,90 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
}
}
func (s *Service) resolveOrderOperatorView(ctx context.Context, order *model.Order) (*uint, string, string) {
if order.OperatorAccountID != nil || order.OperatorAccountType != "" || order.OperatorAccountName != "" {
return order.OperatorAccountID, order.OperatorAccountType, order.OperatorAccountName
}
if order.Source == constants.OrderSourceClient && order.BuyerType == model.BuyerTypePersonal {
operatorID, operatorType, operatorName := s.buildPersonalCustomerOperatorSnapshot(ctx, order.BuyerID, order.BuyerNickname)
if operatorID != nil || operatorType != "" || operatorName != "" {
return operatorID, operatorType, operatorName
}
}
if order.Creator > 0 {
operatorID, operatorType, operatorName := s.buildAccountOperatorSnapshot(ctx, order.Creator, 0)
if operatorID != nil || operatorType != "" || operatorName != "" {
return operatorID, operatorType, operatorName
}
}
if order.OperatorType == model.OperatorAccountTypeAgent && order.OperatorID != nil {
return nil, model.OperatorAccountTypeAgent, s.resolveLegacyOperatorShopName(ctx, *order.OperatorID)
}
if order.OperatorType == model.OperatorAccountTypePlatform {
return nil, model.OperatorAccountTypePlatform, "平台"
}
return nil, "", ""
}
func (s *Service) resolveLegacyOperatorShopName(ctx context.Context, shopID uint) string {
if shopID == 0 {
return ""
}
var shop model.Shop
if err := s.db.WithContext(ctx).Where("id = ?", shopID).First(&shop).Error; err != nil {
return ""
}
return shop.ShopName
}
func (s *Service) resolveSellerShopName(ctx context.Context, sellerShopID *uint) string {
if sellerShopID == nil || *sellerShopID == 0 {
return ""
}
var shop model.Shop
if err := s.db.WithContext(ctx).Where("id = ?", *sellerShopID).First(&shop).Error; err != nil {
return ""
}
return shop.ShopName
}
func (s *Service) resolveOrderCommissionResult(ctx context.Context, order *model.Order) int {
if order.CommissionResult != model.CommissionResultUnknown {
return order.CommissionResult
}
if order.CommissionStatus == model.CommissionStatusPending {
return model.CommissionResultUnknown
}
var pendingReviewCount int64
if err := s.db.WithContext(ctx).Model(&model.CommissionRecord{}).
Where("order_id = ? AND status = ?", order.ID, constants.CommissionStatusPendingReview).
Count(&pendingReviewCount).Error; err == nil && pendingReviewCount > 0 {
return model.CommissionResultChainBroken
}
var releasedCount int64
if err := s.db.WithContext(ctx).Model(&model.CommissionRecord{}).
Where("order_id = ? AND status = ? AND amount > 0", order.ID, constants.CommissionStatusReleased).
Count(&releasedCount).Error; err == nil && releasedCount > 0 {
return model.CommissionResultHasAmount
}
if order.CommissionStatus == model.CommissionStatusCompleted {
return model.CommissionResultNoAmount
}
return model.CommissionResultUnknown
}
// WechatPayJSAPI 发起微信 JSAPI 支付
// 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景
func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.WechatPayJSAPIResponse, error) {

View File

@@ -627,7 +627,7 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, shopID uint, req
ActualAmount: actualAmount,
WithdrawalMethod: req.WithdrawalMethod,
AccountInfo: accountInfoJSON,
Status: 1, // 待审核
Status: constants.WithdrawalStatusPending, // 待审核
}
withdrawalRequest.Creator = currentUserID
withdrawalRequest.Updater = currentUserID

View File

@@ -45,8 +45,7 @@ func (s *OrderStore) Create(ctx context.Context, order *model.Order, items []*mo
func (s *OrderStore) GetByID(ctx context.Context, id uint) (*model.Order, error) {
var order model.Order
query := s.db.WithContext(ctx).Where("id = ?", id)
// 应用数据权限过滤(使用 seller_shop_id 字段)
query = middleware.ApplySellerShopFilter(ctx, query)
query = s.applyOrderScope(ctx, query)
if err := query.First(&order).Error; err != nil {
return nil, err
}
@@ -56,8 +55,7 @@ func (s *OrderStore) GetByID(ctx context.Context, id uint) (*model.Order, error)
func (s *OrderStore) GetByIDWithItems(ctx context.Context, id uint) (*model.Order, []*model.OrderItem, error) {
var order model.Order
query := s.db.WithContext(ctx).Where("id = ?", id)
// 应用数据权限过滤(使用 seller_shop_id 字段)
query = middleware.ApplySellerShopFilter(ctx, query)
query = s.applyOrderScope(ctx, query)
if err := query.First(&order).Error; err != nil {
return nil, nil, err
}
@@ -73,8 +71,7 @@ func (s *OrderStore) GetByIDWithItems(ctx context.Context, id uint) (*model.Orde
func (s *OrderStore) GetByOrderNo(ctx context.Context, orderNo string) (*model.Order, error) {
var order model.Order
query := s.db.WithContext(ctx).Where("order_no = ?", orderNo)
// 应用数据权限过滤(使用 seller_shop_id 字段)
query = middleware.ApplySellerShopFilter(ctx, query)
query = s.applyOrderScope(ctx, query)
if err := query.First(&order).Error; err != nil {
return nil, err
}
@@ -91,17 +88,7 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
query := s.db.WithContext(ctx).Model(&model.Order{})
// 应用数据权限过滤
// 代理用户:可以查看作为买家或操作者的订单
// 平台用户/超管:可以查看所有订单
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),
)
}
query = s.applyOrderScope(ctx, query)
if v, ok := filters["payment_status"]; ok {
query = query.Where("payment_status = ?", v)
@@ -210,6 +197,19 @@ func (s *OrderStore) UpdatePaymentStatus(ctx context.Context, id uint, status in
return s.db.WithContext(ctx).Model(&model.Order{}).Where("id = ?", id).Updates(updates).Error
}
func (s *OrderStore) applyOrderScope(ctx context.Context, query *gorm.DB) *gorm.DB {
shopIDs := middleware.GetSubordinateShopIDs(ctx)
if len(shopIDs) == 0 {
return query
}
return query.Where(
s.db.Where("buyer_type = ? AND buyer_id IN ?", model.BuyerTypeAgent, shopIDs).
Or("seller_shop_id IN ?", shopIDs).
Or(s.db.Where("seller_shop_id IS NULL AND operator_type = ? AND operator_id IN ?", model.OperatorAccountTypeAgent, shopIDs)),
)
}
func (s *OrderStore) GenerateOrderNo() string {
now := time.Now()
randomNum := rand.Intn(1000000)

View File

@@ -192,7 +192,7 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
}
now := time.Now()
order, orderItems, buildErr := h.buildOrderAndItems(rechargeOrder, packages, totalAmount, seriesID, sellerCostPrice, now)
order, orderItems, buildErr := h.buildOrderAndItems(ctx, rechargeOrder, packages, totalAmount, seriesID, sellerCostPrice, now)
if buildErr != nil {
return buildErr
}
@@ -352,6 +352,7 @@ func (h *AutoPurchaseHandler) loadPackages(ctx context.Context, packageIDs []uin
}
func (h *AutoPurchaseHandler) buildOrderAndItems(
ctx context.Context,
rechargeOrder *model.RechargeOrder,
packages []*model.Package,
totalAmount int64,
@@ -376,6 +377,7 @@ func (h *AutoPurchaseHandler) buildOrderAndItems(
}
paidAmount := totalAmount
operatorAccountID, operatorAccountName := h.buildPersonalCustomerOperatorSnapshot(ctx, rechargeOrder.UserID)
order := &model.Order{
BaseModel: model.BaseModel{
Creator: rechargeOrder.UserID,
@@ -396,6 +398,9 @@ func (h *AutoPurchaseHandler) buildOrderAndItems(
Source: constants.OrderSourceClient,
Generation: generation,
ActualPaidAmount: &paidAmount,
OperatorAccountID: operatorAccountID,
OperatorAccountType: model.OperatorAccountTypePersonalCustomer,
OperatorAccountName: operatorAccountName,
SellerShopID: sellerShopID,
SeriesID: seriesID,
SellerCostPrice: sellerCostPrice,
@@ -422,6 +427,21 @@ func (h *AutoPurchaseHandler) buildOrderAndItems(
return order, items, nil
}
func (h *AutoPurchaseHandler) buildPersonalCustomerOperatorSnapshot(ctx context.Context, customerID uint) (*uint, string) {
if customerID == 0 {
return nil, ""
}
var customer model.PersonalCustomer
if err := h.db.WithContext(ctx).Where("id = ?", customerID).First(&customer).Error; err != nil {
id := customerID
return &id, ""
}
id := customerID
return &id, customer.Nickname
}
func (h *AutoPurchaseHandler) activatePackages(
ctx context.Context,
tx *gorm.DB,
@@ -596,25 +616,25 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
Creator: order.Creator,
Updater: order.Creator,
},
OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID,
PackageName: pkg.PackageName,
UsageType: order.OrderType,
DataLimitMB: pkg.RealDataMB,
OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID,
PackageName: pkg.PackageName,
UsageType: order.OrderType,
DataLimitMB: pkg.RealDataMB,
VirtualTotalMBSnapshot: virtualTotalMBSnapshot,
DisplayGainRatioSnapshot: displayGainRatioSnapshot,
EnableVirtualDataSnapshot: enableVirtualDataSnapshot,
Status: constants.PackageUsageStatusActive,
Priority: priority,
MasterUsageID: &mainPackage.ID,
ActivatedAt: &now,
ExpiresAt: expiresAt,
DataResetCycle: pkg.DataResetCycle,
Generation: order.Generation,
PaidAmount: order.ActualPaidAmount,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
Status: constants.PackageUsageStatusActive,
Priority: priority,
MasterUsageID: &mainPackage.ID,
ActivatedAt: &now,
ExpiresAt: expiresAt,
DataResetCycle: pkg.DataResetCycle,
Generation: order.Generation,
PaidAmount: order.ActualPaidAmount,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
if carrierType == constants.AssetWalletResourceTypeIotCard {