feat(db): 补齐 commission/stats 缺失的初始迁移并建表

这两个 app 此前没有任何迁移文件,新环境 migrate 不会建表。本次用 Django 自带 autodetector 生成初始迁移(项目全局存在「迁移状态字段名 snake_case / 模型 PascalCase」漂移,makemigrations 在检测阶段即报错,故按 app 单独生成)。

同时把 StatRecord/StatRanking 的 unique_together 去掉 JSON 列(MySQL 不支持 JSON 列索引,否则建表报 3152),去重由 StatsManager 的 select_for_update 在应用层保证。schema 校验差异 51 → 2。
This commit is contained in:
TermiNexus committed 2026-09-13 22:24:04 +08:00
1 parent 163903c946
commit c26286af1a
3 files changed
+290 -2

No files matched your search

@@ -0,0 +1,162 @@
# Generated by Django 6.0.5 on 2026-09-13 22:21
import datetime
import django.db.models.deletion
import gvsds.apps.core.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='CommissionEvent',
fields=[
('EventUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='事件UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('EventType', models.CharField(choices=[('order_complete', '订单完成'), ('member_purchase', '会员购买'), ('recharge', '充值到账'), ('penalty', '罚款缴纳'), ('register', '用户注册'), ('custom', '自定义事件')], max_length=32, verbose_name='事件类型')),
('EventKey', models.CharField(max_length=128, verbose_name='事件键')),
('TotalAmount', models.DecimalField(decimal_places=4, default=0, max_digits=18, verbose_name='总金额')),
('SourceUserUUID', gvsds.apps.core.models.UUIDField(blank=True, null=True, verbose_name='来源用户UUID')),
('EventPayload', models.JSONField(default=dict, verbose_name='事件载荷')),
('Participants', models.JSONField(default=dict, verbose_name='参与方')),
('EventStatus', models.SmallIntegerField(choices=[(0, '待处理'), (1, '处理中'), (2, '已结算'), (3, '结算失败')], default=0, verbose_name='事件状态')),
('EventTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='事件时间')),
('ProcessTime', models.DateTimeField(blank=True, null=True, verbose_name='处理时间')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '分佣事件',
'verbose_name_plural': '分佣事件',
'db_table': 'CommissionEvent',
'indexes': [models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'), models.Index(fields=['EventKey'], name='idx_cmevt_event_key'), models.Index(fields=['CreateTime'], name='idx_cmevt_create_time')],
'unique_together': {('TenantUUID', 'EventKey')},
},
),
migrations.CreateModel(
name='PreSettlement',
fields=[
('SettlementUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='预结算UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('EventKey', models.CharField(max_length=128, verbose_name='事件键')),
('ExternalPlatform', models.CharField(default='', max_length=64, verbose_name='外部平台')),
('ExternalOrderID', models.CharField(default='', max_length=128, verbose_name='外部订单ID')),
('ParticipantID', models.CharField(max_length=128, verbose_name='参与方ID')),
('ParticipantRole', models.CharField(default='worker', max_length=64, verbose_name='参与方角色')),
('Amount', models.DecimalField(decimal_places=4, max_digits=18, verbose_name='金额')),
('OrderAmount', models.DecimalField(decimal_places=4, default=0, max_digits=18, verbose_name='订单金额')),
('Status', models.SmallIntegerField(choices=[(0, '待确认'), (1, '已确认'), (2, '已拒绝')], default=0, verbose_name='状态')),
('ConfirmedBy', models.CharField(default='', max_length=128, verbose_name='确认人')),
('ConfirmDeadline', models.DateTimeField(blank=True, null=True, verbose_name='确认截止时间')),
('ConfirmedAt', models.DateTimeField(blank=True, null=True, verbose_name='确认时间')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(blank=True, null=True, verbose_name='更新时间')),
],
options={
'verbose_name': '预结算',
'verbose_name_plural': '预结算',
'db_table': 'CommissionPreSettlement',
'indexes': [models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'), models.Index(fields=['EventKey'], name='idx_cmpst_event_key'), models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order')],
'unique_together': {('EventKey', 'ParticipantID')},
},
),
migrations.CreateModel(
name='RateTable',
fields=[
('RateTableUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='费率表UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('TableCode', models.CharField(max_length=64, verbose_name='费率表编码')),
('TableName', models.CharField(max_length=128, verbose_name='费率表名称')),
('TableDescription', models.CharField(default='', max_length=512, verbose_name='费率表描述')),
('TableStatus', models.SmallIntegerField(default=1, verbose_name='费率表状态')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(blank=True, null=True, verbose_name='更新时间')),
],
options={
'verbose_name': '费率表',
'verbose_name_plural': '费率表',
'db_table': 'CommissionRateTable',
'indexes': [models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant')],
'unique_together': {('TenantUUID', 'TableCode')},
},
),
migrations.CreateModel(
name='CommissionRule',
fields=[
('RuleUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='规则UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('EventType', models.CharField(choices=[('order_complete', '订单完成'), ('member_purchase', '会员购买'), ('recharge', '充值到账'), ('penalty', '罚款缴纳'), ('register', '用户注册'), ('custom', '自定义事件')], max_length=32, verbose_name='事件类型')),
('RuleName', models.CharField(max_length=128, verbose_name='规则名称')),
('RuleDescription', models.CharField(default='', max_length=512, verbose_name='规则描述')),
('ParticipantRules', models.JSONField(default=list, verbose_name='参与方规则')),
('MaxInviteLevel', models.SmallIntegerField(default=2, verbose_name='最大邀请层级')),
('RulePriority', models.IntegerField(default=0, verbose_name='规则优先级')),
('RuleStatus', models.SmallIntegerField(default=1, verbose_name='规则状态')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(blank=True, null=True, verbose_name='更新时间')),
('RateTable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rules', to='commission.ratetable', verbose_name='费率表')),
],
options={
'verbose_name': '分佣规则',
'verbose_name_plural': '分佣规则',
'db_table': 'CommissionRule',
},
),
migrations.CreateModel(
name='CommissionRecord',
fields=[
('RecordUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='记录UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('ParticipantID', models.CharField(max_length=128, verbose_name='参与方ID')),
('ParticipantRole', models.CharField(max_length=64, verbose_name='参与方角色')),
('SplitType', models.CharField(max_length=16, verbose_name='分账类型')),
('Amount', models.DecimalField(decimal_places=4, max_digits=18, verbose_name='金额')),
('RuleSnapshot', models.JSONField(default=dict, verbose_name='规则快照')),
('Status', models.SmallIntegerField(choices=[(0, '待结算'), (1, '已结算'), (2, '已提现'), (3, '已取消')], default=0, verbose_name='状态')),
('TierIndex', models.IntegerField(default=0, verbose_name='阶梯索引')),
('SettleTime', models.DateTimeField(blank=True, null=True, verbose_name='结算时间')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('Event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='commission.commissionevent', verbose_name='分佣事件')),
('Rule', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='records', to='commission.commissionrule', verbose_name='分佣规则')),
],
options={
'verbose_name': '分佣记录',
'verbose_name_plural': '分佣记录',
'db_table': 'CommissionRecord',
'ordering': ['-CreateTime'],
'indexes': [models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'), models.Index(fields=['Event'], name='idx_cmrec_event'), models.Index(fields=['CreateTime'], name='idx_cmrec_create_time')],
},
),
migrations.CreateModel(
name='RateEntry',
fields=[
('EntryUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='条目UUID')),
('RateKey', models.CharField(max_length=64, verbose_name='费率键')),
('RateLabel', models.CharField(default='', max_length=128, verbose_name='费率标签')),
('RateValue', models.DecimalField(decimal_places=6, max_digits=10, verbose_name='费率值')),
('EntryStatus', models.SmallIntegerField(default=1, verbose_name='条目状态')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('RateTable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='Entries', to='commission.ratetable', verbose_name='费率表')),
],
options={
'verbose_name': '费率条目',
'verbose_name_plural': '费率条目',
'db_table': 'CommissionRateEntry',
'indexes': [models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key')],
'unique_together': {('RateTable', 'RateKey')},
},
),
migrations.AddIndex(
model_name='commissionrule',
index=models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'),
),
migrations.AddIndex(
model_name='commissionrule',
index=models.Index(fields=['RulePriority'], name='idx_cmrule_priority'),
),
]
+123
View File
@@ -0,0 +1,123 @@
# Generated by Django 6.0.5 on 2026-09-13 22:23
import datetime
import django.db.models.deletion
import gvsds.apps.core.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='StatDimension',
fields=[
('DimensionUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='维度UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('DimensionCode', models.CharField(max_length=64, verbose_name='维度编码')),
('DimensionName', models.CharField(max_length=128, verbose_name='维度名称')),
('DimensionStatus', models.SmallIntegerField(default=1, verbose_name='维度状态')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '统计维度',
'verbose_name_plural': '统计维度',
'db_table': 'StatDimension',
'unique_together': {('TenantUUID', 'DimensionCode')},
},
),
migrations.CreateModel(
name='StatMetric',
fields=[
('MetricUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='指标UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('MetricCode', models.CharField(max_length=64, verbose_name='指标编码')),
('MetricName', models.CharField(max_length=128, verbose_name='指标名称')),
('MetricType', models.CharField(choices=[('count', '计数'), ('amount', '金额/数值'), ('duration', '时长'), ('ratio', '比率'), ('custom', '自定义')], default='count', max_length=16, verbose_name='指标类型')),
('Unit', models.CharField(default='次', max_length=16, verbose_name='单位')),
('DefaultValue', models.DecimalField(decimal_places=4, default=0, max_digits=18, verbose_name='默认值')),
('MetricStatus', models.SmallIntegerField(default=1, verbose_name='指标状态')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '统计指标',
'verbose_name_plural': '统计指标',
'db_table': 'StatMetric',
'indexes': [models.Index(fields=['TenantUUID', 'MetricStatus'], name='idx_statmet_tenant_status')],
'unique_together': {('TenantUUID', 'MetricCode')},
},
),
migrations.CreateModel(
name='StatConfig',
fields=[
('ConfigUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='配置UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('AutoReset', models.BooleanField(default=False, verbose_name='自动重置')),
('ResetCron', models.CharField(default='0 0 * * *', max_length=64, verbose_name='重置Cron表达式')),
('EnableRanking', models.BooleanField(default=False, verbose_name='启用排行榜')),
('RankingDimensionFilter', models.JSONField(default=dict, verbose_name='排行维度筛选')),
('RankingTopN', models.IntegerField(default=50, verbose_name='排行TopN')),
('RankingSortOrder', models.CharField(default='desc', max_length=8, verbose_name='排行排序方式')),
('RetentionDays', models.IntegerField(default=365, verbose_name='数据保留天数')),
('ConfigStatus', models.SmallIntegerField(default=1, verbose_name='配置状态')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('Metric', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='configs', to='stats.statmetric', verbose_name='统计指标')),
],
options={
'verbose_name': '统计配置',
'verbose_name_plural': '统计配置',
'db_table': 'StatConfig',
'unique_together': {('TenantUUID', 'Metric')},
},
),
migrations.CreateModel(
name='StatRanking',
fields=[
('RankingUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='排行UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('RankingPeriod', models.CharField(choices=[('daily', '日榜'), ('monthly', '月榜')], max_length=16, verbose_name='排行周期')),
('RankingDate', models.DateField(verbose_name='排行日期')),
('DimensionFilter', models.JSONField(default=dict, verbose_name='维度筛选')),
('TargetID', models.CharField(max_length=128, verbose_name='目标ID')),
('Rank', models.IntegerField(verbose_name='排名')),
('RankValue', models.DecimalField(decimal_places=4, max_digits=18, verbose_name='排名数值')),
('TargetName', models.CharField(default='', max_length=128, verbose_name='目标名称')),
('TargetAvatar', models.CharField(default='', max_length=512, verbose_name='目标头像')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('Metric', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rankings', to='stats.statmetric', verbose_name='统计指标')),
],
options={
'verbose_name': '排行榜',
'verbose_name_plural': '排行榜',
'db_table': 'StatRanking',
'indexes': [models.Index(fields=['TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric'], name='idx_statrnk_composite'), models.Index(fields=['TargetID', 'RankingPeriod'], name='idx_statrnk_target')],
'unique_together': {('TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric', 'Rank')},
},
),
migrations.CreateModel(
name='StatRecord',
fields=[
('RecordUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='记录UUID')),
('TenantUUID', gvsds.apps.core.models.UUIDField(verbose_name='租户UUID')),
('PeriodType', models.CharField(choices=[('daily', '每日'), ('hourly', '每小时'), ('monthly', '每月'), ('custom', '自定义')], default='daily', max_length=16, verbose_name='周期类型')),
('PeriodKey', models.CharField(max_length=32, verbose_name='周期键')),
('DimensionValues', models.JSONField(default=dict, verbose_name='维度值')),
('Value', models.DecimalField(decimal_places=4, default=0, max_digits=18, verbose_name='数值')),
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('Metric', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='stats.statmetric', verbose_name='统计指标')),
],
options={
'verbose_name': '统计记录',
'verbose_name_plural': '统计记录',
'db_table': 'StatRecord',
'indexes': [models.Index(fields=['TenantUUID', 'PeriodType', 'PeriodKey'], name='idx_statreq_tenant_period'), models.Index(fields=['Metric', 'PeriodKey'], name='idx_statreq_metric_period'), models.Index(fields=['CreateTime'], name='idx_statreq_create_time')],
'unique_together': {('TenantUUID', 'PeriodType', 'PeriodKey', 'Metric')},
},
),
]
+5 -2
View File
@@ -133,7 +133,9 @@ class StatRecord(QModel):
app_label = 'stats'
verbose_name = '统计记录'
verbose_name_plural = '统计记录'
unique_together = [('TenantUUID', 'PeriodType', 'PeriodKey', 'Metric', 'DimensionValues')]
# 唯一约束不含 JSON 列 DimensionValues:MySQL 不支持对 JSON 列建索引,
# 去重由 StatsManager 的 select_for_update + filter 在应用层保证
unique_together = [('TenantUUID', 'PeriodType', 'PeriodKey', 'Metric')]
indexes = [
models.Index(fields=['TenantUUID', 'PeriodType', 'PeriodKey'], name='idx_statreq_tenant_period'),
models.Index(fields=['Metric', 'PeriodKey'], name='idx_statreq_metric_period'),
@@ -182,7 +184,8 @@ class StatRanking(QModel):
app_label = 'stats'
verbose_name = '排行榜'
verbose_name_plural = '排行榜'
unique_together = [('TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric', 'DimensionFilter', 'Rank')]
# 唯一约束不含 JSON 列 DimensionFilter:MySQL 不支持对 JSON 列建索引
unique_together = [('TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric', 'Rank')]
indexes = [
models.Index(fields=['TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric'], name='idx_statrnk_composite'),
models.Index(fields=['TargetID', 'RankingPeriod'], name='idx_statrnk_target'),