diff --git a/gvsds/apps/commission/migrations/0001_initial.py b/gvsds/apps/commission/migrations/0001_initial.py new file mode 100644 index 00000000..ccd845b0 --- /dev/null +++ b/gvsds/apps/commission/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/gvsds/apps/stats/migrations/0001_initial.py b/gvsds/apps/stats/migrations/0001_initial.py new file mode 100644 index 00000000..6c449c43 --- /dev/null +++ b/gvsds/apps/stats/migrations/0001_initial.py @@ -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')}, + }, + ), + ] diff --git a/gvsds/apps/stats/models.py b/gvsds/apps/stats/models.py index 9eaf1903..f017024b 100644 --- a/gvsds/apps/stats/models.py +++ b/gvsds/apps/stats/models.py @@ -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'),