"""模块:每日数据统计引擎 — SAAS 数据模型 抽象统计层:不绑定具体业务,由租户定义统计指标和维度。 核心设计: StatMetric — 统计指标定义(计数/金额/自定义) StatDimension — 统计维度定义(按角色/按类型/按来源) StatRecord — 统计流水表(按维度+时间窗口聚合的统计数据) StatRanking — 排行榜快照(日榜/月榜) StatConfig — 统计配置(周期重置、排行榜规则)""" import uuid import datetime from django.db import models from gvsds.model_base import QModel from gvsds.apps.core.models import UUIDField class StatMetric(QModel): """统计指标定义——租户自定义要统计什么 每个租户可定义多个指标,例如: · dashou_jiedan_count — 打手接单数 · dashou_shouru_amount — 打手收入金额 · guanshi_chongzhi_count — 管事充值人数 · platform_total_income — 平台总收入 """ MET_COUNT = 'count' MET_AMOUNT = 'amount' MET_DURATION = 'duration' MET_RATIO = 'ratio' MET_CUSTOM = 'custom' MET_TYPE_CHOICES = [ (MET_COUNT, '计数'), (MET_AMOUNT, '金额/数值'), (MET_DURATION, '时长'), (MET_RATIO, '比率'), (MET_CUSTOM, '自定义'), ] MetricUUID = UUIDField(primary_key=True, verbose_name='指标UUID') TenantUUID = UUIDField(verbose_name='租户UUID') MetricCode = models.CharField(max_length=64, verbose_name='指标编码') MetricName = models.CharField(max_length=128, verbose_name='指标名称') MetricType = models.CharField(max_length=16, choices=MET_TYPE_CHOICES, default=MET_COUNT, verbose_name='指标类型') Unit = models.CharField(max_length=16, default='次', verbose_name='单位') DefaultValue = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='默认值') MetricStatus = models.SmallIntegerField(default=1, verbose_name='指标状态') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: db_table = 'StatMetric' app_label = 'stats' verbose_name = '统计指标' verbose_name_plural = '统计指标' unique_together = [('TenantUUID', 'MetricCode')] indexes = [ models.Index(fields=['TenantUUID', 'MetricStatus'], name='idx_statmet_tenant_status'), ] def __str__(self): return f'{self.MetricCode}:{self.MetricName}' class StatDimension(QModel): """统计维度定义——按什么切分统计 每个指标可以有多个维度,例如: · role — 按角色(打手/管事/组长/商家) · platform — 按平台(微信/支付宝/银行卡) · order_type — 按订单类型(平台订单/商家订单) · source — 按来源渠道 """ DimensionUUID = UUIDField(primary_key=True, verbose_name='维度UUID') TenantUUID = 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='创建时间') class Meta: db_table = 'StatDimension' app_label = 'stats' verbose_name = '统计维度' verbose_name_plural = '统计维度' unique_together = [('TenantUUID', 'DimensionCode')] def __str__(self): return f'{self.DimensionCode}:{self.DimensionName}' class StatRecord(QModel): """统计记录表——按维度+时间窗口聚合的统计数据 核心结构: 租户 + 日期 + 指标 + 维度值 → 数值 用法: 租户 'al_long',日期 '2026-06-11', 指标 'dashou_jiedan_count',维度值 {'role': 'dashou', 'user_id': '123'} → value = 5(今天打手123接了5单) """ PERIOD_DAILY = 'daily' PERIOD_HOURLY = 'hourly' PERIOD_MONTHLY = 'monthly' PERIOD_CUSTOM = 'custom' PERIOD_CHOICES = [ (PERIOD_DAILY, '每日'), (PERIOD_HOURLY, '每小时'), (PERIOD_MONTHLY, '每月'), (PERIOD_CUSTOM, '自定义'), ] RecordUUID = UUIDField(primary_key=True, verbose_name='记录UUID') TenantUUID = UUIDField(verbose_name='租户UUID') PeriodType = models.CharField(max_length=16, choices=PERIOD_CHOICES, default=PERIOD_DAILY, verbose_name='周期类型') PeriodKey = models.CharField(max_length=32, verbose_name='周期键') Metric = models.ForeignKey( StatMetric, on_delete=models.CASCADE, related_name='records', verbose_name='统计指标', ) DimensionValues = models.JSONField(default=dict, verbose_name='维度值') Value = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='数值') UpdateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: db_table = 'StatRecord' app_label = 'stats' verbose_name = '统计记录' verbose_name_plural = '统计记录' # 唯一约束不含 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'), models.Index(fields=['CreateTime'], name='idx_statreq_create_time'), ] def __str__(self): dim_str = ','.join(f'{k}={v}' for k, v in self.DimensionValues.items()) return f'{self.PeriodKey}/{self.Metric.MetricCode}/{dim_str}={self.Value}' class StatRanking(QModel): """排行榜快照——周期性的排名记录 按指标+维度分组,在指定周期内的排名快照。 通常由定时任务生成,前端直接查询即可。 """ RNK_DAILY = 'daily' RNK_MONTHLY = 'monthly' RNK_PERIOD_CHOICES = [ (RNK_DAILY, '日榜'), (RNK_MONTHLY, '月榜'), ] RankingUUID = UUIDField(primary_key=True, verbose_name='排行UUID') TenantUUID = UUIDField(verbose_name='租户UUID') RankingPeriod = models.CharField(max_length=16, choices=RNK_PERIOD_CHOICES, verbose_name='排行周期') RankingDate = models.DateField(verbose_name='排行日期') Metric = models.ForeignKey( StatMetric, on_delete=models.CASCADE, related_name='rankings', 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(max_digits=18, decimal_places=4, verbose_name='排名数值') TargetName = models.CharField(max_length=128, default='', verbose_name='目标名称') TargetAvatar = models.CharField(max_length=512, default='', verbose_name='目标头像') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: db_table = 'StatRanking' app_label = 'stats' verbose_name = '排行榜' verbose_name_plural = '排行榜' # 唯一约束不含 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'), ] def __str__(self): return f'{self.RankingDate} {self.RankingPeriod} #{self.Rank} {self.TargetID}' class StatConfig(QModel): """统计配置——租户级统计行为配置 定义: · 哪些指标需要周期性重置(日清零/月清零) · 排行榜生成规则 · 数据保留天数 """ ConfigUUID = UUIDField(primary_key=True, verbose_name='配置UUID') TenantUUID = UUIDField(verbose_name='租户UUID') Metric = models.ForeignKey( StatMetric, on_delete=models.CASCADE, related_name='configs', verbose_name='统计指标', ) AutoReset = models.BooleanField(default=False, verbose_name='自动重置') ResetCron = models.CharField(max_length=64, default='0 0 * * *', 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(max_length=8, default='desc', 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='创建时间') class Meta: db_table = 'StatConfig' app_label = 'stats' verbose_name = '统计配置' verbose_name_plural = '统计配置' unique_together = [('TenantUUID', 'Metric')] def __str__(self): return f'StatConfig:{self.Metric.MetricCode}'