feat: 实现一次性佣金功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m41s

- 新增佣金计算服务,支持一次性佣金和返佣计算
- 新增 ShopSeriesOneTimeCommissionTier 模型和存储层
- 新增两个数据库迁移:一次性佣金表和订单佣金字段
- 更新 Commission 模型,新增佣金来源和关联字段
- 更新 CommissionRecord 存储层,支持一次性佣金查询
- 更新 MyCommission 服务,集成一次性佣金计算逻辑
- 更新 ShopCommission 服务,支持一次性佣金统计
- 新增佣金计算异步任务处理器
- 更新 API 路由,新增一次性佣金相关端点
- 归档 OpenSpec 变更文档,同步规范到主规范库
This commit is contained in:
2026-01-29 09:36:12 +08:00
parent dfcf16f548
commit e87513541b
33 changed files with 1668 additions and 270 deletions

View File

@@ -35,12 +35,14 @@ func (s *CommissionRecordStore) GetByID(ctx context.Context, id uint) (*model.Co
}
type CommissionRecordListFilters struct {
ShopID uint
CommissionType string
ICCID string
DeviceNo string
OrderNo string
Status *int
ShopID uint
CommissionSource string
ICCID string
DeviceNo string
OrderNo string
StartTime *string
EndTime *string
Status *int
}
func (s *CommissionRecordStore) ListByShopID(ctx context.Context, opts *store.QueryOptions, filters *CommissionRecordListFilters) ([]*model.CommissionRecord, int64, error) {
@@ -53,8 +55,14 @@ func (s *CommissionRecordStore) ListByShopID(ctx context.Context, opts *store.Qu
if filters.ShopID > 0 {
query = query.Where("shop_id = ?", filters.ShopID)
}
if filters.CommissionType != "" {
query = query.Where("commission_type = ?", filters.CommissionType)
if filters.CommissionSource != "" {
query = query.Where("commission_source = ?", filters.CommissionSource)
}
if filters.StartTime != nil && *filters.StartTime != "" {
query = query.Where("created_at >= ?", *filters.StartTime)
}
if filters.EndTime != nil && *filters.EndTime != "" {
query = query.Where("created_at <= ?", *filters.EndTime)
}
if filters.Status != nil {
query = query.Where("status = ?", *filters.Status)
@@ -86,3 +94,95 @@ func (s *CommissionRecordStore) ListByShopID(ctx context.Context, opts *store.Qu
return records, total, nil
}
type CommissionStats struct {
TotalAmount int64
CostDiffAmount int64
OneTimeAmount int64
TierBonusAmount int64
TotalCount int64
CostDiffCount int64
OneTimeCount int64
TierBonusCount int64
}
func (s *CommissionRecordStore) GetStats(ctx context.Context, filters *CommissionRecordListFilters) (*CommissionStats, error) {
query := s.db.WithContext(ctx).Model(&model.CommissionRecord{}).
Where("status = ?", model.CommissionStatusReleased)
if filters != nil {
if filters.ShopID > 0 {
query = query.Where("shop_id = ?", filters.ShopID)
}
if filters.StartTime != nil && *filters.StartTime != "" {
query = query.Where("created_at >= ?", *filters.StartTime)
}
if filters.EndTime != nil && *filters.EndTime != "" {
query = query.Where("created_at <= ?", *filters.EndTime)
}
}
var stats CommissionStats
result := query.Select(`
COALESCE(SUM(amount), 0) as total_amount,
COALESCE(SUM(CASE WHEN commission_source = 'cost_diff' THEN amount ELSE 0 END), 0) as cost_diff_amount,
COALESCE(SUM(CASE WHEN commission_source = 'one_time' THEN amount ELSE 0 END), 0) as one_time_amount,
COALESCE(SUM(CASE WHEN commission_source = 'tier_bonus' THEN amount ELSE 0 END), 0) as tier_bonus_amount,
COUNT(*) as total_count,
COALESCE(SUM(CASE WHEN commission_source = 'cost_diff' THEN 1 ELSE 0 END), 0) as cost_diff_count,
COALESCE(SUM(CASE WHEN commission_source = 'one_time' THEN 1 ELSE 0 END), 0) as one_time_count,
COALESCE(SUM(CASE WHEN commission_source = 'tier_bonus' THEN 1 ELSE 0 END), 0) as tier_bonus_count
`).Scan(&stats)
if result.Error != nil {
return nil, result.Error
}
return &stats, nil
}
type DailyCommissionStats struct {
Date string
TotalAmount int64
TotalCount int64
}
func (s *CommissionRecordStore) GetDailyStats(ctx context.Context, filters *CommissionRecordListFilters, days int) ([]*DailyCommissionStats, error) {
if days <= 0 {
days = 30
}
query := s.db.WithContext(ctx).Model(&model.CommissionRecord{}).
Where("status = ?", model.CommissionStatusReleased)
if filters != nil {
if filters.ShopID > 0 {
query = query.Where("shop_id = ?", filters.ShopID)
}
if filters.StartTime != nil && *filters.StartTime != "" {
query = query.Where("created_at >= ?", *filters.StartTime)
}
if filters.EndTime != nil && *filters.EndTime != "" {
query = query.Where("created_at <= ?", *filters.EndTime)
}
}
var stats []*DailyCommissionStats
result := query.Select(`
DATE(created_at) as date,
COALESCE(SUM(amount), 0) as total_amount,
COUNT(*) as total_count
`).
Group("DATE(created_at)").
Order("date DESC").
Limit(days).
Scan(&stats)
if result.Error != nil {
return nil, result.Error
}
return stats, nil
}

View File

@@ -0,0 +1,61 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"gorm.io/gorm"
)
type ShopSeriesOneTimeCommissionTierStore struct {
db *gorm.DB
}
func NewShopSeriesOneTimeCommissionTierStore(db *gorm.DB) *ShopSeriesOneTimeCommissionTierStore {
return &ShopSeriesOneTimeCommissionTierStore{db: db}
}
func (s *ShopSeriesOneTimeCommissionTierStore) Create(ctx context.Context, tier *model.ShopSeriesOneTimeCommissionTier) error {
return s.db.WithContext(ctx).Create(tier).Error
}
func (s *ShopSeriesOneTimeCommissionTierStore) BatchCreate(ctx context.Context, tiers []*model.ShopSeriesOneTimeCommissionTier) error {
if len(tiers) == 0 {
return nil
}
return s.db.WithContext(ctx).Create(&tiers).Error
}
func (s *ShopSeriesOneTimeCommissionTierStore) GetByID(ctx context.Context, id uint) (*model.ShopSeriesOneTimeCommissionTier, error) {
var tier model.ShopSeriesOneTimeCommissionTier
if err := s.db.WithContext(ctx).First(&tier, id).Error; err != nil {
return nil, err
}
return &tier, nil
}
func (s *ShopSeriesOneTimeCommissionTierStore) Update(ctx context.Context, tier *model.ShopSeriesOneTimeCommissionTier) error {
return s.db.WithContext(ctx).Save(tier).Error
}
func (s *ShopSeriesOneTimeCommissionTierStore) Delete(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&model.ShopSeriesOneTimeCommissionTier{}, id).Error
}
func (s *ShopSeriesOneTimeCommissionTierStore) ListByAllocationID(ctx context.Context, allocationID uint) ([]*model.ShopSeriesOneTimeCommissionTier, error) {
var tiers []*model.ShopSeriesOneTimeCommissionTier
if err := s.db.WithContext(ctx).
Where("allocation_id = ?", allocationID).
Where("status = ?", 1).
Order("threshold_value ASC").
Find(&tiers).Error; err != nil {
return nil, err
}
return tiers, nil
}
func (s *ShopSeriesOneTimeCommissionTierStore) DeleteByAllocationID(ctx context.Context, allocationID uint) error {
return s.db.WithContext(ctx).
Where("allocation_id = ?", allocationID).
Delete(&model.ShopSeriesOneTimeCommissionTier{}).Error
}