175 lines
10 KiB
Go
175 lines
10 KiB
Go
package mysql
|
||
|
||
import "context"
|
||
|
||
func (r *Repository) ensureAgencyOpeningTables(ctx context.Context) error {
|
||
statements := []string{
|
||
`CREATE TABLE IF NOT EXISTS agency_opening_cycles (
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||
activity_code VARCHAR(64) NOT NULL DEFAULT 'agency_opening' COMMENT '活动编码',
|
||
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '周期标题',
|
||
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled/settling/settled',
|
||
start_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 开始,包含',
|
||
end_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 结束,不包含',
|
||
min_host_count INT NOT NULL DEFAULT 10 COMMENT '申请要求的有效主播数',
|
||
max_agency_age_days INT NOT NULL DEFAULT 7 COMMENT '代理创建后允许申请的最大天数,0 表示不限制',
|
||
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建管理员',
|
||
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新管理员',
|
||
settled_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '结算完成时间',
|
||
locked_by VARCHAR(96) NOT NULL DEFAULT '' COMMENT '结算 worker 锁',
|
||
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '结算锁过期时间',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, cycle_id),
|
||
KEY idx_agency_opening_cycle_current (app_code, activity_code, status, start_ms, end_ms),
|
||
KEY idx_agency_opening_cycle_due (app_code, activity_code, status, end_ms, lock_until_ms)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业活动周期'`,
|
||
`CREATE TABLE IF NOT EXISTS agency_opening_cycle_rewards (
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||
rank_no INT NOT NULL COMMENT '档位序号',
|
||
threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '命中该档需要的代理房间贡献值 HeatValue',
|
||
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '奖励金币',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, cycle_id, rank_no)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水奖励档位'`,
|
||
`CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||
application_id VARCHAR(96) NOT NULL COMMENT '申请 ID',
|
||
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||
agency_owner_user_id BIGINT NOT NULL COMMENT '代理 owner 用户 ID',
|
||
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/approved/pending/granted/failed/expired',
|
||
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '历史字段名:周期内开业房间贡献值 HeatValue',
|
||
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算命中的档位序号',
|
||
reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值',
|
||
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '结算奖励金币',
|
||
wallet_command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包幂等命令',
|
||
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||
approved_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '后台同意时间',
|
||
score_start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分开始时间',
|
||
score_end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分截止时间',
|
||
reviewed_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '审核管理员',
|
||
granted_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发奖时间',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, application_id),
|
||
UNIQUE KEY uk_agency_opening_owner_once (app_code, agency_owner_user_id),
|
||
KEY idx_agency_opening_wallet_command (app_code, wallet_command_id),
|
||
KEY idx_agency_opening_application_owner (app_code, agency_owner_user_id, applied_at_ms),
|
||
KEY idx_agency_opening_agency_cycle (app_code, cycle_id, agency_id),
|
||
KEY idx_agency_opening_application_rank (app_code, cycle_id, score_coin_amount, applied_at_ms, agency_id),
|
||
KEY idx_agency_opening_application_status (app_code, cycle_id, status, updated_at_ms)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业申请和结算事实'`,
|
||
`CREATE TABLE IF NOT EXISTS agency_opening_score_events (
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||
source_event_id VARCHAR(128) NOT NULL COMMENT 'room-service outbox event_id',
|
||
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||
application_id VARCHAR(96) NOT NULL COMMENT '命中的申请 ID',
|
||
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||
target_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID',
|
||
score_delta BIGINT NOT NULL COMMENT '本次房间贡献值 HeatValue',
|
||
occurred_at_ms BIGINT NOT NULL COMMENT '事件发生时间,UTC epoch ms',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, source_event_id),
|
||
KEY idx_agency_opening_score_event_application (app_code, application_id, occurred_at_ms)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水事件幂等表'`,
|
||
}
|
||
for _, statement := range statements {
|
||
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if err := r.ensureAgencyOpeningRewardTierColumns(ctx); err != nil {
|
||
return err
|
||
}
|
||
return r.ensureAgencyOpeningApplicationOwnerOnceIndex(ctx)
|
||
}
|
||
|
||
func (r *Repository) ensureAgencyOpeningRewardTierColumns(ctx context.Context) error {
|
||
if exists, err := r.columnExists(ctx, "agency_opening_cycle_rewards", "threshold_coin_spent"); err != nil {
|
||
return err
|
||
} else if !exists {
|
||
// 旧本地表按 Top 名次发奖;新增阈值列后,rank_no 只作为档位序号保留,实际结算按 threshold_coin_spent 命中。
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_cycle_rewards ADD COLUMN threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '命中该档需要的代理房间流水金币' AFTER rank_no`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "reward_threshold_coin_spent"); err != nil {
|
||
return err
|
||
} else if !exists {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值' AFTER reward_rank_no`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "approved_at_ms"); err != nil {
|
||
return err
|
||
} else if !exists {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN approved_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '后台同意时间' AFTER applied_at_ms`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "score_start_ms"); err != nil {
|
||
return err
|
||
} else if !exists {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN score_start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分开始时间' AFTER approved_at_ms`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "score_end_ms"); err != nil {
|
||
return err
|
||
} else if !exists {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN score_end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分截止时间' AFTER score_start_ms`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "reviewed_by_admin_id"); err != nil {
|
||
return err
|
||
} else if !exists {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN reviewed_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '审核管理员' AFTER score_end_ms`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r *Repository) ensureAgencyOpeningApplicationOwnerOnceIndex(ctx context.Context) error {
|
||
ownerOnceExists, err := r.indexExists(ctx, "agency_opening_applications", "uk_agency_opening_owner_once")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !ownerOnceExists {
|
||
// 申请资格是用户维度的一次性活动,唯一键先落在 owner_user_id 上,避免新周期或更换 agency 后重复参与。
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD UNIQUE KEY uk_agency_opening_owner_once (app_code, agency_owner_user_id)`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
oldAgencyCycleUniqueExists, err := r.indexExists(ctx, "agency_opening_applications", "uk_agency_opening_agency_cycle")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if oldAgencyCycleUniqueExists {
|
||
// 旧唯一键只限制同一周期同一 agency,不能表达“每个用户只能参与一次”,降级成普通查询索引即可。
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications DROP INDEX uk_agency_opening_agency_cycle`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
agencyCycleIndexExists, err := r.indexExists(ctx, "agency_opening_applications", "idx_agency_opening_agency_cycle")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !agencyCycleIndexExists {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD KEY idx_agency_opening_agency_cycle (app_code, cycle_id, agency_id)`); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|