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:
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'),
|
||||
),
|
||||
]
|
||||
@@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user