修复(#79): 促销管理后台API + 审查问题修复
- 实现促销管理接口:规则CRUD、优惠价配置、使用记录查询 - 修复 UpsertPrices 新增记录时提前 return 的问题 - DeleteRule/DeletePrice 不存在记录返回 code=404 - SetPromoPriceRequest.items 空数组校验 - 分页参数限制 page>0、1<=size<=200 - 下单侧促销价格按数量档总价口径计算(含 #87 修复) Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+173
-10
@@ -2,6 +2,7 @@ package promo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
@@ -15,6 +16,26 @@ type RuleWithPrice struct {
|
||||
type Model interface {
|
||||
QueryEligibleRules(ctx context.Context, subscribeId int64, quantity int64) ([]*RuleWithPrice, error)
|
||||
InsertUsage(ctx context.Context, data *Usage, tx ...*gorm.DB) error
|
||||
InsertRule(ctx context.Context, data *Rule) error
|
||||
FindRule(ctx context.Context, id int64) (*Rule, error)
|
||||
UpdateRule(ctx context.Context, data *Rule) error
|
||||
DeleteRule(ctx context.Context, id int64) error
|
||||
QueryRuleList(ctx context.Context, page, size int, ruleType string, enabled *bool, search string) (int64, []*Rule, error)
|
||||
UpsertPrices(ctx context.Context, ruleId int64, items []*SubscribePromo) error
|
||||
FindPrice(ctx context.Context, id int64) (*SubscribePromo, error)
|
||||
DeletePrice(ctx context.Context, id int64) error
|
||||
QueryPriceList(ctx context.Context, ruleId int64, page, size int) (int64, []*SubscribePromo, error)
|
||||
QueryUsageList(ctx context.Context, params UsageFilter) (int64, []*Usage, error)
|
||||
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
|
||||
}
|
||||
|
||||
type UsageFilter struct {
|
||||
Page int
|
||||
Size int
|
||||
RuleId int64
|
||||
UserId int64
|
||||
SubscribeId int64
|
||||
OrderNo string
|
||||
}
|
||||
|
||||
type defaultPromoModel struct {
|
||||
@@ -27,20 +48,16 @@ func NewModel(db *gorm.DB, _ *redis.Client) Model {
|
||||
|
||||
func (m *defaultPromoModel) QueryEligibleRules(ctx context.Context, subscribeId int64, quantity int64) ([]*RuleWithPrice, error) {
|
||||
var list []*RuleWithPrice
|
||||
err := m.eligibleRulesQuery(ctx, subscribeId, quantity).
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) eligibleRulesQuery(ctx context.Context, subscribeId int64, quantity int64) *gorm.DB {
|
||||
return m.db.WithContext(ctx).
|
||||
err := m.db.WithContext(ctx).
|
||||
Table("promo_rule AS pr").
|
||||
Select("pr.*, sp.promo_price").
|
||||
Joins("JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id AND sp.quantity = ?", quantity).
|
||||
Where("sp.subscribe_id = ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeId, true).
|
||||
Joins("JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id").
|
||||
Where("sp.subscribe_id = ? AND sp.quantity = ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeId, quantity, true).
|
||||
Where("pr.deleted_at IS NULL").
|
||||
Order("pr.priority DESC").
|
||||
Order("pr.id ASC")
|
||||
Order("pr.id ASC").
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) InsertUsage(ctx context.Context, data *Usage, tx ...*gorm.DB) error {
|
||||
@@ -50,3 +67,149 @@ func (m *defaultPromoModel) InsertUsage(ctx context.Context, data *Usage, tx ...
|
||||
}
|
||||
return db.Model(&Usage{}).Create(data).Error
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) InsertRule(ctx context.Context, data *Rule) error {
|
||||
return m.db.WithContext(ctx).Create(data).Error
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) FindRule(ctx context.Context, id int64) (*Rule, error) {
|
||||
var resp Rule
|
||||
if err := m.db.WithContext(ctx).Model(&Rule{}).Where("id = ?", id).First(&resp).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) UpdateRule(ctx context.Context, data *Rule) error {
|
||||
return m.db.WithContext(ctx).Model(&Rule{}).Where("id = ?", data.Id).Updates(map[string]interface{}{
|
||||
"name": data.Name,
|
||||
"type": data.Type,
|
||||
"params": data.Params,
|
||||
"priority": data.Priority,
|
||||
"enabled": data.Enabled,
|
||||
"start_time": data.StartTime,
|
||||
"end_time": data.EndTime,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) DeleteRule(ctx context.Context, id int64) error {
|
||||
return m.db.WithContext(ctx).Delete(&Rule{}, id).Error
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) QueryRuleList(ctx context.Context, page, size int, ruleType string, enabled *bool, search string) (int64, []*Rule, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
var total int64
|
||||
var list []*Rule
|
||||
db := m.db.WithContext(ctx).Model(&Rule{})
|
||||
if ruleType != "" {
|
||||
db = db.Where("type = ?", ruleType)
|
||||
}
|
||||
if enabled != nil {
|
||||
db = db.Where("enabled = ?", *enabled)
|
||||
}
|
||||
if search != "" {
|
||||
db = db.Where("name LIKE ?", "%"+search+"%")
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
err := db.Order("priority DESC").Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&list).Error
|
||||
return total, list, err
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) UpsertPrices(ctx context.Context, ruleId int64, items []*SubscribePromo) error {
|
||||
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
item.PromoRuleId = ruleId
|
||||
var existing SubscribePromo
|
||||
err := tx.Model(&SubscribePromo{}).
|
||||
Where("subscribe_id = ? AND quantity = ? AND promo_rule_id = ?", item.SubscribeId, item.Quantity, ruleId).
|
||||
First(&existing).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
existing.Quantity = item.Quantity
|
||||
existing.PromoPrice = item.PromoPrice
|
||||
if err := tx.Save(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) FindPrice(ctx context.Context, id int64) (*SubscribePromo, error) {
|
||||
var resp SubscribePromo
|
||||
if err := m.db.WithContext(ctx).Model(&SubscribePromo{}).Where("id = ?", id).First(&resp).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) DeletePrice(ctx context.Context, id int64) error {
|
||||
return m.db.WithContext(ctx).Delete(&SubscribePromo{}, id).Error
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) QueryPriceList(ctx context.Context, ruleId int64, page, size int) (int64, []*SubscribePromo, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
var total int64
|
||||
var list []*SubscribePromo
|
||||
db := m.db.WithContext(ctx).Model(&SubscribePromo{}).Where("promo_rule_id = ?", ruleId)
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
err := db.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&list).Error
|
||||
return total, list, err
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) QueryUsageList(ctx context.Context, params UsageFilter) (int64, []*Usage, error) {
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.Size <= 0 {
|
||||
params.Size = 10
|
||||
}
|
||||
var total int64
|
||||
var list []*Usage
|
||||
db := m.db.WithContext(ctx).Model(&Usage{})
|
||||
if params.RuleId > 0 {
|
||||
db = db.Where("promo_rule_id = ?", params.RuleId)
|
||||
}
|
||||
if params.UserId > 0 {
|
||||
db = db.Where("user_id = ?", params.UserId)
|
||||
}
|
||||
if params.SubscribeId > 0 {
|
||||
db = db.Where("subscribe_id = ?", params.SubscribeId)
|
||||
}
|
||||
if params.OrderNo != "" {
|
||||
db = db.Where("order_no = ?", params.OrderNo)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
err := db.Order("id DESC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&list).Error
|
||||
return total, list, err
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
|
||||
return m.db.WithContext(ctx).Transaction(fn)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package promo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestQueryEligibleRulesFiltersByQuantity(t *testing.T) {
|
||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
||||
SkipInitializeWithVersion: true,
|
||||
}), &gorm.Config{DryRun: true, DisableAutomaticPing: true})
|
||||
if err != nil {
|
||||
t.Fatalf("open dry-run db: %v", err)
|
||||
}
|
||||
|
||||
model := &defaultPromoModel{db: db}
|
||||
var list []*RuleWithPrice
|
||||
tx := model.eligibleRulesQuery(context.Background(), 11, 3).Find(&list)
|
||||
stmt := tx.Statement
|
||||
sql := stmt.SQL.String()
|
||||
if !strings.Contains(sql, "JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id AND sp.quantity = ?") {
|
||||
t.Fatalf("SQL missing quantity join condition: %s", sql)
|
||||
}
|
||||
|
||||
if len(stmt.Vars) < 2 {
|
||||
t.Fatalf("SQL vars length = %d, want at least 2; vars=%v", len(stmt.Vars), stmt.Vars)
|
||||
}
|
||||
if got, want := stmt.Vars[0], int64(3); got != want {
|
||||
t.Fatalf("first SQL var = %v, want quantity %d; vars=%v", got, want, stmt.Vars)
|
||||
}
|
||||
if got, want := stmt.Vars[1], int64(11); got != want {
|
||||
t.Fatalf("second SQL var = %v, want subscribe_id %d; vars=%v", got, want, stmt.Vars)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func (Rule) TableName() string {
|
||||
type SubscribePromo struct {
|
||||
Id int64 `gorm:"primaryKey"`
|
||||
SubscribeId int64 `gorm:"type:bigint unsigned;not null;comment:Subscribe ID"`
|
||||
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"`
|
||||
Quantity int64 `gorm:"type:int;not null;default:0;comment:购买数量"`
|
||||
PromoRuleId int64 `gorm:"type:bigint unsigned;not null;comment:Promo Rule ID"`
|
||||
PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
|
||||
Reference in New Issue
Block a user