15 KiB
15 KiB
SMANAGE 每日数据统计引擎 — 设计文档
一、设计背景
通过对阿龙电竞平台统计机制的完整分析,提取出以下 7 种核心统计模式:
| 模式 | 阿龙实现 | 本质 |
|---|---|---|
| 实时多角色日统计 | 6 张独立日统计表(DashouRiTongji 等) | 按日期+角色+用户 聚合计数/金额 |
| 即时计数器 | jinrijiedan/jinrishouyi(今日接单/今日收益) | 内存/实时字段累加 |
| 累计统计 | jiedanzongliang/chengjiaozongliang | 永不清零的计数 |
| 定时清零 | celery beat daily_reset_task | 每天 0 点清今日计数器 |
| 平台流水 | Ptliushui 单行聚合表 | 平台级汇总快照 |
| 排行榜 | RankingRecord 日榜/月榜 | 排序快照,前端直接读 |
| 行为触发更新 | update_dashou_daily_by_action() | 业务事件驱动统计 |
核心洞察:所有统计都是 "指标 + 维度 + 时间窗口 → 数值" 的变体。
二、设计原则
| 原则 | 说明 |
|---|---|
| 指标可定义 | 租户通过 DefineMetric() 自定义统计什么(计数/金额/时长/比率) |
| 维度可配置 | 统计按任意维度切分(按角色/按平台/按渠道/按用户),JSON 存储 |
| 原子增量 | 使用 SELECT FOR UPDATE + F() 实现并发安全的原子累加 |
| 时间窗口化 | 日统计/时统计/月统计统一处理,PeriodKey 区分 |
| 排行榜快照 | 定时生成排名快照,前端直接查询,不实时计算 |
| 周期重置 | 可配置的每日/每月清零规则 |
| 租户隔离 | 全部操作通过 X-Tenant-UUID 头识别租户 |
三、架构分层
子服务(如阿龙电竞)
│
│ 订单完成 → stats.Increment('dashou_jiedan', {'role':'dashou','user_id':'123'}, 1)
│ 充值到账 → stats.IncrementMulti([...])
│ 会员购买 → stats.Increment('vip_sale', {'level':'vip1'}, 1)
│
└──→ StatsManager(gvsdsdk)
│
├── 写: AtomicIncrement() → StatRecord(并发安全)
├── 读: QueryToday() / QueryRange() / AggregateTopN()
├── 排: GenerateDailyRanking() → StatRanking(快照)
├── 清: DailyReset() / MonthlyReset()(定时任务触发)
└── 配: DefineMetric() / ConfigureAutoReset()
四、文件清单(10 个文件)
| 层级 | 文件 | 职责 |
|---|---|---|
| 模型 | models.py |
5 张表:Metric / Dimension / Record / Ranking / Config |
| 原子引擎 | gvsdsdk/stats/manager.py |
StatsManager 底层:AtomicIncrement / QueryPeriod / AggregateByDimension / GenerateRanking 等 8 个原子方法 |
| 编排器 | gvsdsdk/stats/manager.py |
StatsManager 编排:DefineMetric / ConfigureAutoReset / QueryRanking / RunScheduledResets / BuildRankings 等(原 StatEngine 已并入) |
| API | views.py |
10 个 REST 端点 |
| 序列化 | serializers.py |
14 个 Serializer |
| 路由 | urls.py |
10 个 URL 路由 |
| 配置 | apps.py |
AppConfig |
| 管理 | admin.py |
5 个 Django Admin 注册 |
五、核心数据模型
5.1 StatMetric — 统计指标定义
租户自定义要统计什么:
| 字段 | 类型 | 说明 |
|---|---|---|
| MetricUUID | UUID | 主键 |
| TenantUUID | UUID | 租户 |
| MetricCode | CharField(64) | 指标编码(dashou_jiedan) |
| MetricName | CharField(128) | 指标名称(打手接单数) |
| MetricType | CharField(16) | count/amount/duration/ratio/custom |
| Unit | CharField(16) | 单位(次/元/分钟) |
| DefaultValue | DecimalField | 默认值 |
唯一约束:(TenantUUID, MetricCode)
5.2 StatDimension — 统计维度定义
按什么切分统计:
| 字段 | 类型 | 说明 |
|---|---|---|
| DimensionCode | CharField(64) | 维度编码(role, platform, user_id) |
| DimensionName | CharField(128) | 维度名称(角色, 平台, 用户) |
5.3 StatRecord — 统计记录(核心表)
按维度+时间窗口聚合的统计数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| RecordUUID | UUID | 主键 |
| TenantUUID | UUID | 租户 |
| PeriodType | CharField(16) | daily / hourly / monthly |
| PeriodKey | CharField(32) | 周期键(2026-06-11 / 2026-06) |
| Metric | ForeignKey | 关联指标 |
| DimensionValues | JSONField | 维度值 {"role":"dashou","user_id":"123"} |
| Value | DecimalField | 统计值 |
唯一约束:(TenantUUID, PeriodType, PeriodKey, Metric, DimensionValues)
单条记录示例:
TenantUUID='al_long', PeriodType='daily', PeriodKey='2026-06-11',
Metric='dashou_jiedan', DimensionValues={'role':'dashou','user_id':'123'},
Value=5 → 打手123在2026-06-11接了5单
5.4 StatRanking — 排行榜快照
| 字段 | 类型 | 说明 |
|---|---|---|
| RankingPeriod | CharField(16) | daily / monthly |
| RankingDate | DateField | 排行日期 |
| Metric | ForeignKey | 按什么指标排 |
| DimensionFilter | JSONField | 过滤条件(如只排打手) |
| TargetID | CharField(128) | 排行对象ID |
| Rank | IntegerField | 名次(1开始) |
| RankValue | DecimalField | 排行值 |
| TargetName | CharField(128) | 对象名 |
| TargetAvatar | CharField(512) | 对象头像URL |
5.5 StatConfig — 统计行为配置
| 字段 | 类型 | 说明 |
|---|---|---|
| Metric | ForeignKey | 关联指标 |
| AutoReset | BooleanField | 是否每日自动清零 |
| ResetCron | CharField(64) | 清零时间cron |
| EnableRanking | BooleanField | 是否生成排行榜 |
| RankingTopN | IntegerField | 排行取前N名 |
| RetentionDays | IntegerField | 数据保留天数 |
六、原子增量引擎
核心函数 AtomicIncrement:
def AtomicIncrement(tenant_uuid, period_type, period_key, metric_uuid,
dimension_values, delta):
"""使用 SELECT FOR UPDATE + F() 实现原子累加"""
with transaction.atomic():
existing = StatRecord.objects.select_for_update().filter(...)
if existing:
record.Value = F('Value') + delta # 数据库层面原子累加
record.save(update_fields=['Value'])
else:
record = StatRecord.objects.create(..., Value=delta)
return record, created
并发安全保证:
select_for_update()对匹配的行加排他锁,防止并发写竞争F('Value') + delta在数据库层面完成累加,避免读-改-写竞争transaction.atomic()确保锁在事务结束前持有
七、StatsManager 编排器(gvsdsdk)
原
StatEngine(gvsds/apps/stats/manager.py)已于 2026-08-07 删除,编排能力全部并入 gvsdsdkStatsManager静态方法,tenant_uuid为首参。
class StatsManager: # 全部为静态方法,tenant_uuid 作首参
# 指标管理
def DefineMetric(tenant_uuid, metric_code, metric_name, metric_type, unit) # 定义统计指标
def DefineDimension(tenant_uuid, dimension_code, dimension_name) # 定义统计维度
def ListMetrics(tenant_uuid, status) / ListDimensions(tenant_uuid, status) # 查询
# 配置
def ConfigureAutoReset(tenant_uuid, metric_uuid, auto_reset, enable_ranking, ...) # 配置重置/排行
# 数据写入
def AtomicIncrement(tenant_uuid, period_type, period_key, metric_uuid, dimension_values, delta) # 原子增量
def AtomicIncrementMulti(tenant_uuid, period_type, period_key, increments) # 批量原子增量
# 数据查询
def QueryPeriod(tenant_uuid, metric_uuid, period_type, period_key) # 特定周期
def QueryRange(tenant_uuid, metric_uuid, period_type, start_key, end_key) # 时间范围
def AggregateByDimension(tenant_uuid, metric_uuid, period_type, start_key, end_key, dimension_key) # 聚合排名
# 排行榜
def GenerateDailyRanking(tenant_uuid, metric_uuid, ranking_date, top_n) # 生成日榜
def GenerateMonthlyRanking(tenant_uuid, metric_uuid, ranking_date, top_n) # 生成月榜
def QueryRanking(tenant_uuid, ranking_period, ranking_date, metric_uuid) # 查询排行榜
# 维护
def DailyReset(tenant_uuid, metric_uuid, target_date) # 手动日清
def MonthlyReset(tenant_uuid, metric_uuid, target_date) # 手动月清
def RunScheduledResets(tenant_uuid, target_date) # 定时清零(Celery调用)
def BuildRankings(tenant_uuid, target_date) # 定时生成排行(Celery调用)
def PurgeOld(tenant_uuid, before_date, period_type) # 清理旧数据
八、API 端点
# 指标与维度管理
GET /api/stats/metric/ # 查询所有指标
POST /api/stats/metric/ # 创建统计指标
GET /api/stats/dimension/ # 查询所有维度
POST /api/stats/dimension/ # 创建统计维度
# 数据写入
POST /api/stats/increment/ # 单条原子增量
POST /api/stats/increment/multi/ # 批量原子增量
# 数据查询
GET /api/stats/query/ # 查询统计记录
GET /api/stats/aggregate/ # 聚合排名(实时计算)
# 排行榜
GET /api/stats/ranking/ # 查询排行榜快照
POST /api/stats/ranking/ # 手动生成排行榜
# 配置
GET /api/stats/config/ # 查询统计配置
POST /api/stats/config/ # 创建/更新统计配置
# 维护
POST /api/stats/reset/ # 手动清零
POST /api/stats/purge/ # 清理过期数据
九、子服务集成示例
9.1 初始化统计体系
from gvsdsdk.stats.manager import StatsManager
tenant = 'al_long_001'
# 定义指标
jiedan = StatsManager.DefineMetric(tenant, 'dashou_jiedan', '打手接单数', 'count', '单')
shouru = StatsManager.DefineMetric(tenant, 'dashou_shouru', '打手收入', 'amount', '元')
chongzhi = StatsManager.DefineMetric(tenant, 'guanshi_chongzhi', '管事充值人数', 'count', '人')
vip = StatsManager.DefineMetric(tenant, 'vip_sale', '会员销量', 'count', '份')
# 定义维度
StatsManager.DefineDimension(tenant, 'role', '角色')
StatsManager.DefineDimension(tenant, 'user_id', '用户ID')
StatsManager.DefineDimension(tenant, 'platform', '平台')
StatsManager.DefineDimension(tenant, 'order_type', '订单类型')
# 配置每日清零 + 排行榜
StatsManager.ConfigureAutoReset(
tenant_uuid=tenant,
metric_uuid=jiedan.MetricUUID,
auto_reset=True,
enable_ranking=True,
ranking_top_n=50,
)
9.2 订单完成时写统计
import datetime
# 订单完成 → 写多条统计(原子批量)
StatsManager.AtomicIncrementMulti(
tenant_uuid=tenant,
period_type='daily',
period_key=datetime.date.today().strftime('%Y-%m-%d'),
increments=[
(jiedan.MetricUUID, {'role': 'dashou', 'user_id': 'd_123'}, 1),
(jiedan.MetricUUID, {'role': 'dashou', 'user_id': 'd_123', 'order_type': 'platform'}, 1),
(jiedan.MetricUUID, {'role': 'guanshi', 'user_id': 'g_456'}, 1),
(shouru.MetricUUID, {'role': 'dashou', 'user_id': 'd_123'}, 80.00),
(shouru.MetricUUID, {'role': 'platform'}, 20.00),
],
)
9.3 查询今日统计
import datetime
# 查询打手123今日统计
records = StatsManager.QueryPeriod(
tenant_uuid=tenant,
metric_uuid=jiedan.MetricUUID,
period_type='daily',
period_key=datetime.date.today().strftime('%Y-%m-%d'),
dimension_filter={'user_id': 'd_123'},
)
# → [{period_key:'2026-06-11', dimension_values:{...}, value: 5}]
# 查询今日所有打手排名(实时聚合)
top = StatsManager.AggregateByDimension(
tenant_uuid=tenant,
metric_uuid=jiedan.MetricUUID,
period_type='daily',
start_key=datetime.date.today().strftime('%Y-%m-%d'),
end_key=datetime.date.today().strftime('%Y-%m-%d'),
dimension_key='user_id',
)[:10]
# → [{dim_value:'d_123', total: 5}, {dim_value:'d_456', total: 3}, ...]
9.4 排行榜
import datetime
# 生成今日排行榜快照(定时任务调用)
StatsManager.GenerateDailyRanking(
tenant_uuid=tenant,
metric_uuid=jiedan.MetricUUID,
dimension_filter={'role': 'dashou'},
top_n=50,
)
# 前端查询日榜
rankings = StatsManager.QueryRanking(
tenant_uuid=tenant,
ranking_period='daily',
ranking_date=datetime.date.today(),
metric_uuid=jiedan.MetricUUID,
)
# → [{target_id:'d_123', rank:1, rank_value:15, target_name:'张三', target_avatar:'...'}, ...]
9.5 Celery 定时任务
# celery.py 中注册每日任务(实际实现见 gvsds/apps/stats/tasks.py)
@shared_task
def stats_daily_reset():
"""每日统计清零 + 排行榜生成"""
from gvsds.apps.stats.tasks import _GetActiveTenants
from gvsdsdk.stats.manager import StatsManager
for tenant in _GetActiveTenants():
StatsManager.RunScheduledResets(tenant)
StatsManager.BuildRankings(tenant)
9.6 退款回滚统计
import datetime
# 退款 → 回滚相关统计(负增量)
StatsManager.AtomicIncrement(
tenant_uuid=tenant,
period_type='daily',
period_key=datetime.date.today().strftime('%Y-%m-%d'),
metric_uuid=jiedan.MetricUUID,
dimension_values={'role': 'dashou', 'user_id': 'd_123'},
delta=-1,
)
StatsManager.AtomicIncrement(
tenant_uuid=tenant,
period_type='daily',
period_key=datetime.date.today().strftime('%Y-%m-%d'),
metric_uuid=shouru.MetricUUID,
dimension_values={'role': 'dashou', 'user_id': 'd_123'},
delta=-80.00,
)
十、与阿龙电竞模式的对比
| 阿龙模式 | SMANAGE 抽象 |
|---|---|
| 6 张独立日统计表 | 1 张 StatRecord + 维度字段区分 |
| 硬编码的 jinri/jinyue 字段 | 可定义的 Metric(任意指标) |
| 固定角色(打手/管事/商家) | JSON DimensionValues(任意维度) |
| celery beat 定时清零 | ConfigureAutoReset() 配置化 |
| RankingRecord 独立表 | StatRanking 快照表 + Generate 方法 |
| 手动 update_daily 函数 | AtomicIncrement + F() 原子累加 |
| Ptliushui 单行汇总 | AggregateByDimension / QueryRange |
十一、配置变更记录
| 文件 | 变更 |
|---|---|
settings.py |
新增 'gvsds.apps.stats' 到 INSTALLED_APPS |
db_router.py |
新增 'stats': 'default' 到 app_db_map |
urls.py |
新增 path('api/stats/', include('gvsds.apps.stats.urls')) |
gvsds/apps/stats/engine.py |
已删除(2026-08-07):8 个模块级函数并入 gvsdsdk StatsManager |
gvsds/apps/stats/manager.py |
已删除(2026-08-07):StatEngine 编排方法(DefineMetric / ConfigureAutoReset / QueryRanking / RunScheduledResets / BuildRankings 等)并入 gvsdsdk StatsManager,视图与 Celery 任务统一经 gvsdsdk 访问 |