Files
SMANAGE/docs/stats_engine.md

408 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](file:///d:/SMANAGE/gvsds/apps/stats/engine.py#L16-L60):
```python
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 删除,编排能力全部并入 gvsdsdk `StatsManager` 静态方法,`tenant_uuid` 为首参。
```python
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 初始化统计体系
```python
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 订单完成时写统计
```python
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 查询今日统计
```python
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 排行榜
```python
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 定时任务
```python
# 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 退款回滚统计
```python
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 访问 |