diff --git a/initialize/migrate/database/02154_promo_system.down.sql b/initialize/migrate/database/02154_promo_system.down.sql new file mode 100644 index 0000000..54dba39 --- /dev/null +++ b/initialize/migrate/database/02154_promo_system.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE `order` + DROP COLUMN IF EXISTS `promo_discount`, + DROP COLUMN IF EXISTS `promo_rule_id`; + +DROP TABLE IF EXISTS `promo_usage`; +DROP TABLE IF EXISTS `subscribe_promo`; +DROP TABLE IF EXISTS `promo_rule`; diff --git a/initialize/migrate/database/02154_promo_system.up.sql b/initialize/migrate/database/02154_promo_system.up.sql new file mode 100644 index 0000000..b3b7bff --- /dev/null +++ b/initialize/migrate/database/02154_promo_system.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS `promo_rule` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '规则名称,如"新客7天优惠"', + `type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '规则类型:new_user / inactive_user / campaign', + `params` JSON NOT NULL COMMENT '类型专属参数', + `priority` INT NOT NULL DEFAULT 0 COMMENT '优先级,数值越大越优先匹配', + `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + `start_time` DATETIME DEFAULT NULL COMMENT '生效开始时间,NULL=立即生效', + `end_time` DATETIME DEFAULT NULL COMMENT '生效结束时间,NULL=永不过期', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME DEFAULT NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + KEY `idx_enabled_priority` (`enabled`, `priority` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销规则表'; + +CREATE TABLE IF NOT EXISTS `subscribe_promo` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID', + `promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID', + `promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_subscribe_rule` (`subscribe_id`, `promo_rule_id`), + KEY `idx_promo_rule_id` (`promo_rule_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表'; + +CREATE TABLE IF NOT EXISTS `promo_usage` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID', + `promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '使用的规则 ID', + `subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '购买的规格 ID', + `order_no` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '关联订单号', + `promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '使用时的促销单价(分)', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_rule` (`user_id`, `promo_rule_id`), + KEY `idx_order_no` (`order_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销使用记录表'; + +SET @promo_rule_id_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'order' AND COLUMN_NAME = 'promo_rule_id'); +SET @promo_rule_id_sql = IF(@promo_rule_id_exists = 0, 'ALTER TABLE `order` ADD COLUMN `promo_rule_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''促销规则ID, 0=未使用促销'' AFTER `discount`', 'SELECT 1'); +PREPARE stmt FROM @promo_rule_id_sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @promo_discount_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'order' AND COLUMN_NAME = 'promo_discount'); +SET @promo_discount_sql = IF(@promo_discount_exists = 0, 'ALTER TABLE `order` ADD COLUMN `promo_discount` BIGINT NOT NULL DEFAULT 0 COMMENT ''促销优惠金额(分)'' AFTER `promo_rule_id`', 'SELECT 1'); +PREPARE stmt FROM @promo_discount_sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/internal/model/order/model.go b/internal/model/order/model.go index 89d1b5f..1ffdba7 100644 --- a/internal/model/order/model.go +++ b/internal/model/order/model.go @@ -22,6 +22,8 @@ type Details struct { Price int64 `gorm:"type:int;not null;default:0;comment:Original price"` Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"` Discount int64 `gorm:"type:int;not null;default:0;comment:Order Discount"` + PromoRuleID int64 `gorm:"type:bigint unsigned;not null;default:0;comment:Promo Rule ID, 0 means no promo"` + PromoDiscount int64 `gorm:"type:bigint;not null;default:0;comment:Promo Discount Amount"` Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"` CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount"` PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Id"` diff --git a/internal/model/order/order.go b/internal/model/order/order.go index cd90153..545e44d 100644 --- a/internal/model/order/order.go +++ b/internal/model/order/order.go @@ -3,32 +3,34 @@ package order import "time" type Order struct { - Id int64 `gorm:"primaryKey"` - ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"` + Id int64 `gorm:"primaryKey"` + ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"` UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id"` SubscriptionUserId int64 `gorm:"type:bigint;not null;default:0;comment:Target user ID for subscription (0=same as UserId)"` - OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"` - Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"` - Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"` - Price int64 `gorm:"type:int;not null;default:0;comment:Original price"` - Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"` - GiftAmount int64 `gorm:"type:int;not null;default:0;comment:User Gift Amount"` - Discount int64 `gorm:"type:int;not null;default:0;comment:Discount Amount"` - Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"` - CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount Amount"` - Commission int64 `gorm:"type:int;not null;default:0;comment:Order Commission"` - PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Method Id"` - Method string `gorm:"type:varchar(255);not null;default:'';comment:Payment Method"` - FeeAmount int64 `gorm:"type:int;not null;default:0;comment:Fee Amount"` - TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"` - Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"` - SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"` - SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"` - AppAccountToken string `gorm:"type:varchar(36);default:null;comment:Apple IAP App Account Token (UUID)"` - ActivationContext string `gorm:"type:text;default:null;comment:Activation context JSON (guest/redemption info for DB fallback)"` - IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"` - CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` + OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"` + Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"` + Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"` + Price int64 `gorm:"type:int;not null;default:0;comment:Original price"` + Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"` + GiftAmount int64 `gorm:"type:int;not null;default:0;comment:User Gift Amount"` + Discount int64 `gorm:"type:int;not null;default:0;comment:Discount Amount"` + PromoRuleID int64 `gorm:"type:bigint unsigned;not null;default:0;comment:Promo Rule ID, 0 means no promo"` + PromoDiscount int64 `gorm:"type:bigint;not null;default:0;comment:Promo Discount Amount"` + Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"` + CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount Amount"` + Commission int64 `gorm:"type:int;not null;default:0;comment:Order Commission"` + PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Method Id"` + Method string `gorm:"type:varchar(255);not null;default:'';comment:Payment Method"` + FeeAmount int64 `gorm:"type:int;not null;default:0;comment:Fee Amount"` + TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"` + Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"` + SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"` + SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"` + AppAccountToken string `gorm:"type:varchar(36);default:null;comment:Apple IAP App Account Token (UUID)"` + ActivationContext string `gorm:"type:text;default:null;comment:Activation context JSON (guest/redemption info for DB fallback)"` + IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } type OrdersTotal struct { diff --git a/internal/model/promo_rule/default.go b/internal/model/promo_rule/default.go new file mode 100644 index 0000000..eeb7625 --- /dev/null +++ b/internal/model/promo_rule/default.go @@ -0,0 +1,106 @@ +package promo_rule + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customPromoRuleModel)(nil) + +var cachePromoRuleIdPrefix = "cache:promo_rule:id:" + +type ( + Model interface { + promoRuleModel + customPromoRuleLogicModel + } + + promoRuleModel interface { + Insert(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error + FindOne(ctx context.Context, id int64) (*PromoRule, error) + Update(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customPromoRuleModel struct { + *defaultPromoRuleModel + } + defaultPromoRuleModel struct { + cache.CachedConn + table string + } +) + +func newPromoRuleModel(db *gorm.DB, c *redis.Client) *defaultPromoRuleModel { + return &defaultPromoRuleModel{ + CachedConn: cache.NewConn(db, c), + table: "`promo_rule`", + } +} + +func (m *defaultPromoRuleModel) getCacheKeys(data *PromoRule) []string { + if data == nil { + return []string{} + } + return []string{fmt.Sprintf("%s%v", cachePromoRuleIdPrefix, data.Id)} +} + +func (m *defaultPromoRuleModel) Insert(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }, m.getCacheKeys(data)...) +} + +func (m *defaultPromoRuleModel) FindOne(ctx context.Context, id int64) (*PromoRule, error) { + key := fmt.Sprintf("%s%v", cachePromoRuleIdPrefix, id) + var resp PromoRule + err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&PromoRule{}).Where("`id` = ?", id).First(v).Error + }) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (m *defaultPromoRuleModel) Update(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error + }, m.getCacheKeys(old)...) +} + +func (m *defaultPromoRuleModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&PromoRule{}, id).Error + }, m.getCacheKeys(data)...) +} + +func (m *defaultPromoRuleModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/promo_rule/model.go b/internal/model/promo_rule/model.go new file mode 100644 index 0000000..13ede48 --- /dev/null +++ b/internal/model/promo_rule/model.go @@ -0,0 +1,44 @@ +package promo_rule + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customPromoRuleLogicModel interface { + QueryEnabledByPriority(ctx context.Context) ([]*PromoRule, error) + QueryAvailableByPriority(ctx context.Context, now time.Time) ([]*PromoRule, error) +} + +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customPromoRuleModel{ + defaultPromoRuleModel: newPromoRuleModel(conn, c), + } +} + +func (m *customPromoRuleModel) QueryEnabledByPriority(ctx context.Context) ([]*PromoRule, error) { + var list []*PromoRule + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&PromoRule{}). + Where("enabled = ?", true). + Order("priority DESC, id DESC"). + Find(v).Error + }) + return list, err +} + +func (m *customPromoRuleModel) QueryAvailableByPriority(ctx context.Context, now time.Time) ([]*PromoRule, error) { + var list []*PromoRule + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&PromoRule{}). + Where("enabled = ?", true). + Where("(start_time IS NULL OR start_time <= ?)", now). + Where("(end_time IS NULL OR end_time >= ?)", now). + Order("priority DESC, id DESC"). + Find(v).Error + }) + return list, err +} diff --git a/internal/model/promo_rule/promo_rule.go b/internal/model/promo_rule/promo_rule.go new file mode 100644 index 0000000..49eb411 --- /dev/null +++ b/internal/model/promo_rule/promo_rule.go @@ -0,0 +1,73 @@ +package promo_rule + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "gorm.io/gorm" +) + +type JSONRawMessage json.RawMessage + +func (j *JSONRawMessage) Scan(value interface{}) error { + if value == nil { + *j = JSONRawMessage([]byte("{}")) + return nil + } + + switch v := value.(type) { + case []byte: + *j = JSONRawMessage(v) + case string: + *j = JSONRawMessage([]byte(v)) + default: + *j = JSONRawMessage([]byte("{}")) + } + return nil +} + +func (j JSONRawMessage) Value() (driver.Value, error) { + if len(j) == 0 { + return "{}", nil + } + data, err := json.RawMessage(j).MarshalJSON() + if err != nil { + return nil, err + } + return string(data), nil +} + +func (j JSONRawMessage) MarshalJSON() ([]byte, error) { + if len(j) == 0 { + return []byte("{}"), nil + } + return json.RawMessage(j).MarshalJSON() +} + +func (j *JSONRawMessage) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + *j = JSONRawMessage([]byte("{}")) + return nil + } + *j = JSONRawMessage(data) + return nil +} + +type PromoRule struct { + Id int64 `gorm:"type:bigint unsigned;primaryKey"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Name"` + Type string `gorm:"type:varchar(32);not null;default:'';comment:Rule Type: new_user, inactive_user, campaign"` + Params JSONRawMessage `gorm:"type:json;not null;comment:Rule Params"` + Priority int64 `gorm:"type:int;not null;default:0;index:idx_enabled_priority,priority:2,sort:desc;comment:Priority"` + Enabled bool `gorm:"type:tinyint(1);not null;default:1;index:idx_enabled_priority,priority:1;comment:Enabled"` + StartTime *time.Time `gorm:"type:datetime;default:null;comment:Start Time"` + EndTime *time.Time `gorm:"type:datetime;default:null;comment:End Time"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` + DeletedAt gorm.DeletedAt `gorm:"index;comment:Delete Time"` +} + +func (PromoRule) TableName() string { + return "promo_rule" +} diff --git a/internal/model/promo_usage/default.go b/internal/model/promo_usage/default.go new file mode 100644 index 0000000..8e09658 --- /dev/null +++ b/internal/model/promo_usage/default.go @@ -0,0 +1,106 @@ +package promo_usage + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customPromoUsageModel)(nil) + +var cachePromoUsageIdPrefix = "cache:promo_usage:id:" + +type ( + Model interface { + promoUsageModel + customPromoUsageLogicModel + } + + promoUsageModel interface { + Insert(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error + FindOne(ctx context.Context, id int64) (*PromoUsage, error) + Update(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customPromoUsageModel struct { + *defaultPromoUsageModel + } + defaultPromoUsageModel struct { + cache.CachedConn + table string + } +) + +func newPromoUsageModel(db *gorm.DB, c *redis.Client) *defaultPromoUsageModel { + return &defaultPromoUsageModel{ + CachedConn: cache.NewConn(db, c), + table: "`promo_usage`", + } +} + +func (m *defaultPromoUsageModel) getCacheKeys(data *PromoUsage) []string { + if data == nil { + return []string{} + } + return []string{fmt.Sprintf("%s%v", cachePromoUsageIdPrefix, data.Id)} +} + +func (m *defaultPromoUsageModel) Insert(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }, m.getCacheKeys(data)...) +} + +func (m *defaultPromoUsageModel) FindOne(ctx context.Context, id int64) (*PromoUsage, error) { + key := fmt.Sprintf("%s%v", cachePromoUsageIdPrefix, id) + var resp PromoUsage + err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&PromoUsage{}).Where("`id` = ?", id).First(v).Error + }) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (m *defaultPromoUsageModel) Update(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error + }, m.getCacheKeys(old)...) +} + +func (m *defaultPromoUsageModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&PromoUsage{}, id).Error + }, m.getCacheKeys(data)...) +} + +func (m *defaultPromoUsageModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/promo_usage/model.go b/internal/model/promo_usage/model.go new file mode 100644 index 0000000..4b60437 --- /dev/null +++ b/internal/model/promo_usage/model.go @@ -0,0 +1,71 @@ +package promo_usage + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customPromoUsageLogicModel interface { + FindByOrderNo(ctx context.Context, orderNo string) ([]*PromoUsage, error) + FindByUserAndRule(ctx context.Context, userId, promoRuleId int64) ([]*PromoUsage, error) + QueryListByPage(ctx context.Context, page, size int, userId, promoRuleId, subscribeId int64) (int64, []*PromoUsage, error) +} + +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customPromoUsageModel{ + defaultPromoUsageModel: newPromoUsageModel(conn, c), + } +} + +func (m *customPromoUsageModel) FindByOrderNo(ctx context.Context, orderNo string) ([]*PromoUsage, error) { + var list []*PromoUsage + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&PromoUsage{}). + Where("order_no = ?", orderNo). + Order("id DESC"). + Find(v).Error + }) + return list, err +} + +func (m *customPromoUsageModel) FindByUserAndRule(ctx context.Context, userId, promoRuleId int64) ([]*PromoUsage, error) { + var list []*PromoUsage + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&PromoUsage{}). + Where("user_id = ? AND promo_rule_id = ?", userId, promoRuleId). + Order("id DESC"). + Find(v).Error + }) + return list, err +} + +func (m *customPromoUsageModel) QueryListByPage(ctx context.Context, page, size int, userId, promoRuleId, subscribeId int64) (int64, []*PromoUsage, error) { + var list []*PromoUsage + var total int64 + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 20 + } + + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + db := conn.Model(&PromoUsage{}) + if userId > 0 { + db = db.Where("user_id = ?", userId) + } + if promoRuleId > 0 { + db = db.Where("promo_rule_id = ?", promoRuleId) + } + if subscribeId > 0 { + db = db.Where("subscribe_id = ?", subscribeId) + } + if err := db.Count(&total).Error; err != nil { + return err + } + return db.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(v).Error + }) + return total, list, err +} diff --git a/internal/model/promo_usage/promo_usage.go b/internal/model/promo_usage/promo_usage.go new file mode 100644 index 0000000..cf641bd --- /dev/null +++ b/internal/model/promo_usage/promo_usage.go @@ -0,0 +1,17 @@ +package promo_usage + +import "time" + +type PromoUsage struct { + Id int64 `gorm:"type:bigint unsigned;primaryKey"` + UserId int64 `gorm:"type:bigint unsigned;not null;index:idx_user_rule,priority:1;comment:User Id"` + PromoRuleId int64 `gorm:"type:bigint unsigned;not null;index:idx_user_rule,priority:2;comment:Promo Rule Id"` + SubscribeId int64 `gorm:"type:bigint unsigned;not null;comment:Subscribe Id"` + OrderNo string `gorm:"type:varchar(255);not null;default:'';index:idx_order_no;comment:Order No"` + PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` +} + +func (PromoUsage) TableName() string { + return "promo_usage" +} diff --git a/internal/model/subscribe_promo/default.go b/internal/model/subscribe_promo/default.go new file mode 100644 index 0000000..6a4648d --- /dev/null +++ b/internal/model/subscribe_promo/default.go @@ -0,0 +1,106 @@ +package subscribe_promo + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customSubscribePromoModel)(nil) + +var cacheSubscribePromoIdPrefix = "cache:subscribe_promo:id:" + +type ( + Model interface { + subscribePromoModel + customSubscribePromoLogicModel + } + + subscribePromoModel interface { + Insert(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error + FindOne(ctx context.Context, id int64) (*SubscribePromo, error) + Update(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customSubscribePromoModel struct { + *defaultSubscribePromoModel + } + defaultSubscribePromoModel struct { + cache.CachedConn + table string + } +) + +func newSubscribePromoModel(db *gorm.DB, c *redis.Client) *defaultSubscribePromoModel { + return &defaultSubscribePromoModel{ + CachedConn: cache.NewConn(db, c), + table: "`subscribe_promo`", + } +} + +func (m *defaultSubscribePromoModel) getCacheKeys(data *SubscribePromo) []string { + if data == nil { + return []string{} + } + return []string{fmt.Sprintf("%s%v", cacheSubscribePromoIdPrefix, data.Id)} +} + +func (m *defaultSubscribePromoModel) Insert(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }, m.getCacheKeys(data)...) +} + +func (m *defaultSubscribePromoModel) FindOne(ctx context.Context, id int64) (*SubscribePromo, error) { + key := fmt.Sprintf("%s%v", cacheSubscribePromoIdPrefix, id) + var resp SubscribePromo + err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&SubscribePromo{}).Where("`id` = ?", id).First(v).Error + }) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (m *defaultSubscribePromoModel) Update(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error + }, m.getCacheKeys(old)...) +} + +func (m *defaultSubscribePromoModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&SubscribePromo{}, id).Error + }, m.getCacheKeys(data)...) +} + +func (m *defaultSubscribePromoModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/subscribe_promo/model.go b/internal/model/subscribe_promo/model.go new file mode 100644 index 0000000..1f21301 --- /dev/null +++ b/internal/model/subscribe_promo/model.go @@ -0,0 +1,84 @@ +package subscribe_promo + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/promo_rule" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type RulePromo struct { + Id int64 + SubscribeId int64 + PromoRuleId int64 + PromoPrice int64 + RuleName string + RuleType string + Params promo_rule.JSONRawMessage + Priority int64 + StartTime *time.Time + EndTime *time.Time +} + +type customSubscribePromoLogicModel interface { + FindOneBySubscribeAndRule(ctx context.Context, subscribeId, promoRuleId int64) (*SubscribePromo, error) + FindBySubscribeId(ctx context.Context, subscribeId int64) ([]*SubscribePromo, error) + FindByPromoRuleId(ctx context.Context, promoRuleId int64) ([]*SubscribePromo, error) + QueryEnabledBySubscribeId(ctx context.Context, subscribeId int64) ([]*RulePromo, error) +} + +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customSubscribePromoModel{ + defaultSubscribePromoModel: newSubscribePromoModel(conn, c), + } +} + +func (m *customSubscribePromoModel) FindOneBySubscribeAndRule(ctx context.Context, subscribeId, promoRuleId int64) (*SubscribePromo, error) { + var resp SubscribePromo + err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&SubscribePromo{}). + Where("subscribe_id = ? AND promo_rule_id = ?", subscribeId, promoRuleId). + First(v).Error + }) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (m *customSubscribePromoModel) FindBySubscribeId(ctx context.Context, subscribeId int64) ([]*SubscribePromo, error) { + var list []*SubscribePromo + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&SubscribePromo{}). + Where("subscribe_id = ?", subscribeId). + Order("id DESC"). + Find(v).Error + }) + return list, err +} + +func (m *customSubscribePromoModel) FindByPromoRuleId(ctx context.Context, promoRuleId int64) ([]*SubscribePromo, error) { + var list []*SubscribePromo + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&SubscribePromo{}). + Where("promo_rule_id = ?", promoRuleId). + Order("id DESC"). + Find(v).Error + }) + return list, err +} + +func (m *customSubscribePromoModel) QueryEnabledBySubscribeId(ctx context.Context, subscribeId int64) ([]*RulePromo, error) { + var list []*RulePromo + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Table("subscribe_promo AS sp"). + Select("sp.id, sp.subscribe_id, sp.promo_rule_id, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.priority, pr.start_time, pr.end_time"). + Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL"). + Where("sp.subscribe_id = ? AND pr.enabled = ?", subscribeId, true). + Order("pr.priority DESC, pr.id DESC"). + Scan(v).Error + }) + return list, err +} diff --git a/internal/model/subscribe_promo/subscribe_promo.go b/internal/model/subscribe_promo/subscribe_promo.go new file mode 100644 index 0000000..9b85514 --- /dev/null +++ b/internal/model/subscribe_promo/subscribe_promo.go @@ -0,0 +1,16 @@ +package subscribe_promo + +import "time" + +type SubscribePromo struct { + Id int64 `gorm:"type:bigint unsigned;primaryKey"` + SubscribeId int64 `gorm:"type:bigint unsigned;not null;index:uk_subscribe_rule,unique,priority:1;comment:Subscribe Id"` + PromoRuleId int64 `gorm:"type:bigint unsigned;not null;index:uk_subscribe_rule,unique,priority:2;index:idx_promo_rule_id;comment:Promo Rule Id"` + PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (SubscribePromo) TableName() string { + return "subscribe_promo" +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index e1c5aaa..a4970f3 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -21,7 +21,10 @@ import ( logmessage "github.com/perfect-panel/server/internal/model/logmessage" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/model/promo_rule" + "github.com/perfect-panel/server/internal/model/promo_usage" "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/model/subscribe_promo" "github.com/perfect-panel/server/internal/model/system" "github.com/perfect-panel/server/internal/model/ticket" "github.com/perfect-panel/server/internal/model/traffic" @@ -45,7 +48,7 @@ type ServiceContext struct { ExchangeRate float64 GeoIP *IPLocation SignatureValidator *signature.Validator - S3Store *storage.S3Store + S3Store *storage.S3Store //NodeCache *cache.NodeCacheClient AuthModel auth.Model @@ -65,6 +68,9 @@ type ServiceContext struct { PaymentModel payment.Model DocumentModel document.Model SubscribeModel subscribe.Model + PromoRuleModel promo_rule.Model + SubscribePromoModel subscribe_promo.Model + PromoUsageModel promo_usage.Model TrafficLogModel traffic.Model AnnouncementModel announcement.Model IAPAppleTransactionModel iapapple.Model @@ -140,6 +146,9 @@ func NewServiceContext(c config.Config) *ServiceContext { PaymentModel: payment.NewModel(db, rds), DocumentModel: document.NewModel(db, rds), SubscribeModel: subscribe.NewModel(db, rds), + PromoRuleModel: promo_rule.NewModel(db, rds), + SubscribePromoModel: subscribe_promo.NewModel(db, rds), + PromoUsageModel: promo_usage.NewModel(db, rds), TrafficLogModel: traffic.NewModel(db), AnnouncementModel: announcement.NewModel(db, rds), }