首次上传
This commit is contained in:
commit
04f7349a1e
19190 files changed
+2414744
No files matched your search
@@ -0,0 +1,443 @@
|
||||
# SMANAGE 抽象分佣引擎 — 设计文档
|
||||
|
||||
## 一、设计背景
|
||||
|
||||
通过对阿龙电竞平台分佣机制的完整分析,提取出 **6 种核心分佣模式**:
|
||||
订单分佣、邀请链分佣、阶梯分佣、提现抽成、罚款分红、跨平台预结算。
|
||||
|
||||
这些模式都有一个共同的本质:**事件 → 规则 → 分账**。SMANAGE 分佣引擎将这一本质抽象为 SAAS 级通用模块,子服务只管说"发生了什么",SAAS 负责"怎么分"。
|
||||
|
||||
---
|
||||
|
||||
## 二、设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **事件驱动** | 子服务触发分佣事件(order_complete / member_purchase / recharge / penalty),引擎自动匹配规则计算分账 |
|
||||
| **规则外置** | 分账比例、层级、公式全部存储于 `CommissionRule` 表,零硬编码 |
|
||||
| **策略模式** | 比例/固定/阶梯/剩余 四种策略独立封装,新增分账逻辑只需添加策略类 |
|
||||
| **参与方可配置** | 参与者身份和金额由 JSON 配置,适配任意业务场景 |
|
||||
| **邀请链通用化** | 不管叫"管事/组长"还是"上级/上上级",都是链式分佣 |
|
||||
| **幂等保护** | `(TenantUUID, EventKey)` 联合唯一约束,防止重复分账 |
|
||||
| **快照不可变** | `CommissionRecord` 生成后冻结,规则变更不影响历史记录 |
|
||||
| **与支付解耦** | 分佣算出"该分多少",支付模块负责"怎么打到账" |
|
||||
|
||||
---
|
||||
|
||||
## 三、架构分层
|
||||
|
||||
```
|
||||
子服务(如阿龙电竞)
|
||||
│
|
||||
│ 订单完成 / 会员购买 / 用户充值 / 罚款缴纳
|
||||
│
|
||||
└──→ CommissionEngine.Dispatch(event_type, event_key, total_amount, ...)
|
||||
│
|
||||
├── 1. 写入 CommissionEvent(幂等检查)
|
||||
├── 2. 匹配 CommissionRule(按优先级)
|
||||
├── 3. 调用策略引擎计算分账
|
||||
│ ├── PercentageStrategy → total × rate
|
||||
│ ├── FixedStrategy → 固定金额
|
||||
│ ├── TieredStrategy → 按次数/档位查表
|
||||
│ └── RemainderStrategy → 剩余归平台
|
||||
├── 4. 写入 CommissionRecord(不可变流水)
|
||||
└── 5. 返回分账结果
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、文件清单(10 个文件)
|
||||
|
||||
| 层级 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| **模型** | `models.py` | 6 张核心表:Event / Rule / RateTable / RateEntry / Record / PreSettlement |
|
||||
| **策略** | `strategies/engine.py` | 5 种策略类 + 策略工厂 `CreateStrategy()` |
|
||||
| **编排器** | `manager.py` | `CommissionEngine` — Dispatch / PreSettle / 邀请链 / 费率计算 / 统计查询 |
|
||||
| **API** | `views.py` | 10 个 REST 端点 |
|
||||
| **序列化** | `serializers.py` | 12 个 Serializer |
|
||||
| **路由** | `urls.py` | 10 个 URL 路由 |
|
||||
| **配置** | `apps.py` | AppConfig |
|
||||
| **管理** | `admin.py` | 6 个 Django Admin 注册 |
|
||||
|
||||
---
|
||||
|
||||
## 五、核心数据模型
|
||||
|
||||
### 5.1 CommissionEvent — 分佣事件
|
||||
|
||||
子服务触发的分佣事件记录:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| EventUUID | UUID | 主键 |
|
||||
| TenantUUID | UUID | 租户 |
|
||||
| EventType | CharField(32) | `order_complete` / `member_purchase` / `recharge` / `penalty` / `register` / `custom` |
|
||||
| EventKey | CharField(128) | 业务唯一键(幂等保护),如订单ID |
|
||||
| TotalAmount | DecimalField | 可分佣总金额 |
|
||||
| SourceUserUUID | UUID | 触发者(如付费用户) |
|
||||
| EventPayload | JSONField | 业务上下文(由子服务自行解析) |
|
||||
| Participants | JSONField | 参与者映射 `{role: user_id}` |
|
||||
| EventStatus | SmallIntegerField | 0=待处理 1=处理中 2=已结算 3=失败 |
|
||||
|
||||
唯一约束:`(TenantUUID, EventKey)` — 同一事件不可重复分账
|
||||
|
||||
### 5.2 CommissionRule — 分佣规则
|
||||
|
||||
定义"怎么分":
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| RuleUUID | UUID | 主键 |
|
||||
| TenantUUID | UUID | 租户 |
|
||||
| EventType | CharField(32) | 绑定的分佣事件类型 |
|
||||
| RuleName | CharField(128) | 规则名称 |
|
||||
| ParticipantRules | JSONField | 参与者分账规则列表 |
|
||||
| RateTable | ForeignKey | 可选关联费率表 |
|
||||
| MaxInviteLevel | SmallIntegerField | 邀请链最大层级(默认2级) |
|
||||
| RulePriority | IntegerField | 优先级 |
|
||||
|
||||
#### ParticipantRules JSON 结构示例
|
||||
|
||||
```json
|
||||
[
|
||||
{ "role": "worker", "type": "percentage", "value": 0.80 },
|
||||
{ "role": "agent", "type": "fixed", "value": 10 },
|
||||
{ "role": "leader", "type": "tiered", "tiers": { "1": 5, "2": 8, "3": 10 } },
|
||||
{ "role": "platform", "type": "remainder" }
|
||||
]
|
||||
```
|
||||
|
||||
### 5.3 RateTable + RateEntry — 费率表
|
||||
|
||||
可复用的动态费率配置:
|
||||
|
||||
```
|
||||
RateTable: { TableCode: 'dashou_order_split', TableName: '打手订单分成费率' }
|
||||
├── RateEntry: { RateKey: '1', RateLabel: '平台订单打手分成', RateValue: 0.8000 }
|
||||
├── RateEntry: { RateKey: '3', RateLabel: '商家订单打手分成', RateValue: 0.7000 }
|
||||
├── RateEntry: { RateKey: '5', RateLabel: '打手提现抽成', RateValue: 0.0100 }
|
||||
└── RateEntry: { RateKey: '6', RateLabel: '管事提现抽成', RateValue: 0.0100 }
|
||||
```
|
||||
|
||||
### 5.4 CommissionRecord — 分佣记录(不可变)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| RecordUUID | UUID | 主键 |
|
||||
| TenantUUID | UUID | 租户 |
|
||||
| Event | ForeignKey | 关联分佣事件 |
|
||||
| Rule | ForeignKey | 关联分佣规则(可空,规则删除后保留快照) |
|
||||
| ParticipantID | CharField(128) | 分账对象ID |
|
||||
| ParticipantRole | CharField(64) | 分账角色(worker / agent / platform / ...) |
|
||||
| SplitType | CharField(16) | percentage / fixed / tiered / remainder |
|
||||
| Amount | DecimalField | 分账金额 |
|
||||
| RuleSnapshot | JSONField | 规则快照(规则变更后保留历史) |
|
||||
| TierIndex | IntegerField | 阶梯索引(多次分红时标识第几次) |
|
||||
| Status | SmallIntegerField | 0=待结算 1=已结算 2=已提现 3=已取消 |
|
||||
|
||||
### 5.5 PreSettlement — 预结算
|
||||
|
||||
跨平台订单待确认款项:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| EventKey | CharField(128) | 业务唯一键 |
|
||||
| ExternalPlatform | CharField(64) | 外部平台标识 |
|
||||
| ExternalOrderID | CharField(128) | 外部订单ID |
|
||||
| ParticipantID | CharField(128) | 应分账对象 |
|
||||
| Amount | DecimalField | 预分金额 |
|
||||
| Status | SmallIntegerField | 0=待确认 1=已确认 2=已拒绝 |
|
||||
|
||||
---
|
||||
|
||||
## 六、策略引擎
|
||||
|
||||
策略模式实现,四种核心策略 + 一种扩展策略:
|
||||
|
||||
### 6.1 PercentageStrategy — 比例分账
|
||||
|
||||
```python
|
||||
# 配置: { role: 'worker', type: 'percentage', value: 0.80 }
|
||||
amount = total_amount × 0.80
|
||||
# 支持保底/封顶:
|
||||
# { role: 'worker', type: 'percentage', value: 0.80, min_amount: 5, max_amount: 500 }
|
||||
```
|
||||
|
||||
### 6.2 FixedStrategy — 固定金额
|
||||
|
||||
```python
|
||||
# 配置: { role: 'agent', type: 'fixed', value: 10 }
|
||||
amount = 10 # 不受 total_amount 影响
|
||||
# 也支持从费率表读取:
|
||||
# { role: 'agent', type: 'fixed', rate_table_key: 'guanshi_fc' }
|
||||
```
|
||||
|
||||
### 6.3 TieredStrategy — 阶梯分账
|
||||
|
||||
```python
|
||||
# 配置: { role: 'agent', type: 'tiered', tiers: { "1": 5, "2": 8, "3": 10 } }
|
||||
# tier_index=1 → amount=5
|
||||
# tier_index=2 → amount=8
|
||||
# tier_index=3 → amount=10
|
||||
# 支持百分比阶梯: { "1": "5%", "2": "8%" }
|
||||
```
|
||||
|
||||
### 6.4 RemainderStrategy — 剩余归平台
|
||||
|
||||
```python
|
||||
# 配置: { role: 'platform', type: 'remainder' }
|
||||
amount = total_amount - SUM(其他所有参与者分账)
|
||||
# 自动排在最后计算,确保剩余金额正确
|
||||
```
|
||||
|
||||
### 6.5 WithdrawalFeeStrategy — 提现手续费(扩展)
|
||||
|
||||
```python
|
||||
# 配置: { role: 'platform', type: 'withdrawal_fee', value: 0.01 }
|
||||
fee = total_amount × 0.01
|
||||
```
|
||||
|
||||
### 新增策略只需两步
|
||||
|
||||
```python
|
||||
# 1. 实现策略类
|
||||
class FormulaStrategy(BaseSplitStrategy):
|
||||
def Calculate(self, total_amount, tier_index=0, **context):
|
||||
return total_amount * Decimal('0.5') + Decimal('10')
|
||||
|
||||
# 2. 注册到策略映射
|
||||
STRATEGY_MAP['formula'] = FormulaStrategy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、CommissionEngine 编排器
|
||||
|
||||
```python
|
||||
class CommissionEngine:
|
||||
def __init__(self, tenant_uuid)
|
||||
|
||||
# 规则管理
|
||||
def ConfigureRule(event_type, rule_name, participant_rules, ...) # 配置分佣规则
|
||||
def ConfigureRateTable(table_code, table_name, entries, ...) # 配置费率表
|
||||
|
||||
# 核心分佣
|
||||
def Dispatch(event_type, event_key, total_amount, participants, ...) # 触发分佣
|
||||
def BuildInviteChainCommission(event_type, event_key, ...) # 邀请链分佣
|
||||
|
||||
# 预结算
|
||||
def PreSettle(event_key, external_platform, external_order_id, ...) # 创建预结算
|
||||
def ConfirmPreSettlement(event_key, participant_id, ...) # 确认
|
||||
def RejectPreSettlement(event_key, participant_id, ...) # 拒绝
|
||||
|
||||
# 查询
|
||||
def CalculateFee(total_amount, rate_table_code, rate_key) # 计算手续费
|
||||
def QueryRecords(participant_id, event_type, status, ...) # 查询分佣记录
|
||||
def TotalByParticipant(participant_id, start_date, end_date) # 统计总额
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、API 端点
|
||||
|
||||
```
|
||||
# 规则管理
|
||||
GET /api/commission/rule/ # 查询分佣规则列表
|
||||
POST /api/commission/rule/ # 创建分佣规则
|
||||
DELETE /api/commission/rule/ # 禁用分佣规则
|
||||
|
||||
# 费率表管理
|
||||
GET /api/commission/rate/ # 查询费率表列表
|
||||
POST /api/commission/rate/ # 创建费率表
|
||||
|
||||
# 分佣操作
|
||||
POST /api/commission/dispatch/ # 触发分佣事件
|
||||
POST /api/commission/dispatch/invite_chain/ # 邀请链分佣
|
||||
|
||||
# 查询统计
|
||||
GET /api/commission/query/ # 查询分佣记录
|
||||
GET /api/commission/total/<participant_id>/ # 统计某人分佣总额
|
||||
POST /api/commission/fee/ # 计算手续费
|
||||
|
||||
# 预结算
|
||||
POST /api/commission/presettle/ # 创建预结算
|
||||
POST /api/commission/presettle/confirm/ # 确认预结算
|
||||
|
||||
# 事件查询
|
||||
GET /api/commission/event/ # 查询分佣事件列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、子服务集成示例
|
||||
|
||||
### 9.1 订单分佣
|
||||
|
||||
```python
|
||||
from gvsds.apps.commission.manager import CommissionEngine
|
||||
|
||||
engine = CommissionEngine(tenant_uuid='al_long_001')
|
||||
|
||||
# 配置分佣规则
|
||||
engine.ConfigureRule(
|
||||
event_type='order_complete',
|
||||
rule_name='平台订单打手8成',
|
||||
participant_rules=[
|
||||
{'role': 'worker', 'type': 'percentage', 'value': 0.80},
|
||||
{'role': 'platform', 'type': 'remainder'},
|
||||
],
|
||||
)
|
||||
|
||||
# 订单完成时触发分佣
|
||||
records = engine.Dispatch(
|
||||
event_type='order_complete',
|
||||
event_key='ORDER-20260611-001', # 幂等键
|
||||
total_amount=Decimal('100.00'),
|
||||
participants={
|
||||
'worker': 'dashou_123',
|
||||
},
|
||||
payload={'order_id': 'xxx', 'game': 'LoL', 'shop_id': 'shop_001'},
|
||||
)
|
||||
|
||||
# records = [
|
||||
# {ParticipantID: 'dashou_123', ParticipantRole: 'worker', Amount: 80},
|
||||
# {ParticipantID: 'platform_default', ParticipantRole: 'platform', Amount: 20},
|
||||
# ]
|
||||
```
|
||||
|
||||
### 9.2 会员购买+邀请链
|
||||
|
||||
```python
|
||||
# 配置会员购买分佣规则
|
||||
engine.ConfigureRule(
|
||||
event_type='member_purchase',
|
||||
rule_name='会员购买管事+组长分红',
|
||||
participant_rules=[
|
||||
{'role': 'agent', 'type': 'fixed', 'value': 10},
|
||||
{'role': 'leader', 'type': 'fixed', 'value': 5},
|
||||
{'role': 'platform', 'type': 'remainder'},
|
||||
],
|
||||
)
|
||||
|
||||
# 打手购买会员 → 触发邀请链分佣
|
||||
records = engine.BuildInviteChainCommission(
|
||||
event_type='member_purchase',
|
||||
event_key='MEMBER-VIP001-20260611-001',
|
||||
total_amount=Decimal('50.00'),
|
||||
source_user_uuid='dashou_123',
|
||||
invite_chain=[
|
||||
{'user_uuid': 'guanshi_456', 'level': 1, 'role': 'agent'},
|
||||
{'user_uuid': 'zuzhang_789', 'level': 2, 'role': 'leader'},
|
||||
],
|
||||
rule_name='会员购买管事+组长分红',
|
||||
participant_rules=[
|
||||
{'role': 'agent', 'type': 'fixed', 'value': 10},
|
||||
{'role': 'leader', 'type': 'fixed', 'value': 5},
|
||||
{'role': 'platform', 'type': 'remainder'},
|
||||
],
|
||||
)
|
||||
|
||||
# records = [
|
||||
# {ParticipantID: 'guanshi_456', ParticipantRole: 'agent', Amount: 10},
|
||||
# {ParticipantID: 'zuzhang_789', ParticipantRole: 'leader', Amount: 5},
|
||||
# {ParticipantID: 'platform_default', ParticipantRole: 'platform', Amount: 35},
|
||||
# ]
|
||||
```
|
||||
|
||||
### 9.3 阶梯分佣(多次购买)
|
||||
|
||||
```python
|
||||
# 第一次购买
|
||||
engine.Dispatch(
|
||||
event_type='member_purchase',
|
||||
event_key='STAGED-VIP001-DASHOU_123-1',
|
||||
total_amount=Decimal('50.00'),
|
||||
tier_index=1,
|
||||
participants={'agent': 'guanshi_456'},
|
||||
)
|
||||
|
||||
# 第二次购买(金额不同)
|
||||
engine.Dispatch(
|
||||
event_type='member_purchase',
|
||||
event_key='STAGED-VIP001-DASHOU_123-2',
|
||||
total_amount=Decimal('50.00'),
|
||||
tier_index=2,
|
||||
participants={'agent': 'guanshi_456'},
|
||||
)
|
||||
```
|
||||
|
||||
### 9.4 提现手续费
|
||||
|
||||
```python
|
||||
# 配置提现费率表
|
||||
engine.ConfigureRateTable(
|
||||
table_code='withdrawal_fee',
|
||||
table_name='提现手续费率',
|
||||
entries={
|
||||
'dashou': '0.0100', # 打手提现 1%
|
||||
'guanshi': '0.0100', # 管事提现 1%
|
||||
'zuzhang': '0.0100', # 组长提现 1%
|
||||
},
|
||||
)
|
||||
|
||||
# 计算手续费
|
||||
fee = engine.CalculateFee(
|
||||
total_amount=Decimal('1000.00'),
|
||||
rate_table_code='withdrawal_fee',
|
||||
rate_key='dashou',
|
||||
)
|
||||
# fee = 10.00 → 实际到账 990.00
|
||||
```
|
||||
|
||||
### 9.5 跨平台预结算
|
||||
|
||||
```python
|
||||
# 创建预结算
|
||||
ps = engine.PreSettle(
|
||||
event_key='ORDER-CROSS-001',
|
||||
external_platform='partner_club_002',
|
||||
external_order_id='P-ORDER-888',
|
||||
participant_id='dashou_123',
|
||||
amount=Decimal('80.00'),
|
||||
order_amount=Decimal('100.00'),
|
||||
)
|
||||
|
||||
# 对方确认后
|
||||
engine.ConfirmPreSettlement(
|
||||
event_key='ORDER-CROSS-001',
|
||||
confirmed_by='partner_admin',
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、与支付模块的关系
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ 子服务业务层 │
|
||||
│ │
|
||||
│ 订单完成 ──→ CommissionEngine.Dispatch() ──→ 算出分账 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 用户提现 ──→ PaymentManager.Transfer() ──→ 执行打款 │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
分佣引擎 → 算出来"该分多少"
|
||||
支付网关 → 执行"怎么打到账"
|
||||
|
||||
两者通过 CommissionRecord.Amount 桥接:
|
||||
· CommissionRecord 中的分账金额作为 PaymentManager.Transfer 的输入
|
||||
· 打款完成后,CommissionRecord 状态从 'settled' → 'withdrawn'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、配置变更记录
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `settings.py` | 新增 `'gvsds.apps.commission'` 到 `INSTALLED_APPS` |
|
||||
| `db_router.py` | 新增 `'commission': 'default'` 到 `app_db_map` |
|
||||
| `urls.py` | 新增 `path('api/commission/', include('gvsds.apps.commission.urls'))` |
|
||||
@@ -0,0 +1,263 @@
|
||||
# SMANAGE 抽象支付网关 — 设计文档
|
||||
|
||||
## 一、设计目标
|
||||
|
||||
SMANAGE 是 SAAS 系统,不绑定具体业务。支付网关抽象层提供**多平台支付能力的统一接入**,子服务只需要关心"付了多少钱"和"通过哪个渠道付",SAAS 负责"怎么付"。
|
||||
|
||||
### 核心原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **适配器模式** | 每个支付平台是一个适配器,新增平台只需实现接口 + 注册一行 |
|
||||
| **密钥走构造传参** | `PaymentCredentials` 在网关实例创建时注入,网关层不持有状态 |
|
||||
| **租户隔离** | 所有操作通过 `X-Tenant-UUID` 头识别租户,配置和交易记录完全分离 |
|
||||
| **加密存储** | 密钥使用 XOR + Base64 加密,密钥通过 `settings.PAYMENT_ENCRYPTION_KEY` 管理 |
|
||||
| **回调不认证** | 回调端点关闭 DRF 认证,由网关自身 `VerifyNotify()` 签名验证保证安全 |
|
||||
|
||||
---
|
||||
|
||||
## 二、架构分层
|
||||
|
||||
```
|
||||
子服务(如阿龙电竞)
|
||||
│
|
||||
├── 注入租户密钥 → PaymentGatewayConfig(SAAS配置)
|
||||
│
|
||||
└── 调用 PaymentManager.pay(platform='wechat', amount=100)
|
||||
│
|
||||
├── 根据 platform 从 GatewayRegistry 获取适配器类
|
||||
├── 网关实例由密钥创建(构造时传参,非来自数据库)
|
||||
└── 执行 Pay / Query / Refund / Transfer / Close
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、文件清单(12 个文件)
|
||||
|
||||
| 层级 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| **抽象层** | `gateways/base.py` | `AbstractPaymentGateway` 抽象基类 + 7 个数据模型 |
|
||||
| **注册表** | `gateways/registry.py` | `GatewayRegistry` 渠道注册工厂 + `payment_registry` 全局单例 |
|
||||
| **适配器** | `gateways/wechat.py` | `WechatPaymentGateway` — 微信支付(JSAPI/Native/H5/小程序 + 转账 + 回调验签) |
|
||||
| **适配器** | `gateways/alipay.py` | `AlipayPaymentGateway` — 支付宝(APP/H5/Native + 转账 + 回调验签) |
|
||||
| **适配器** | `gateways/bank.py` | `BankPaymentGateway` — 银行卡(银联/网银 + 代付到银行卡) |
|
||||
| **模型** | `models.py` | `PaymentGatewayConfig`(密钥加密存储)+ `PaymentTransaction`(交易流水) |
|
||||
| **编排器** | `manager.py` | `PaymentManager` — 配置/支付/查询/退款/转账/回调六合一 |
|
||||
| **API** | `views.py` | 7 个 REST 端点(含无需认证的 NotifyView) |
|
||||
| **序列化** | `serializers.py` | 5 个 Serializer |
|
||||
| **路由** | `urls.py` | 7 个 URL 路由 |
|
||||
| **配置** | `apps.py` | AppConfig,`ready()` 时自动注册三个渠道 |
|
||||
| **管理** | `admin.py` | Django Admin 注册 |
|
||||
|
||||
---
|
||||
|
||||
## 四、核心数据模型
|
||||
|
||||
### 4.1 PaymentCredentials(非持久化)
|
||||
|
||||
网关实例创建时传入,不存储到数据库:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PaymentCredentials:
|
||||
AppID: str = ''
|
||||
MchID: str = ''
|
||||
APIKey: str = ''
|
||||
PrivateKey: str = ''
|
||||
PublicKey: str = ''
|
||||
CertPath: str = ''
|
||||
CertKeyPath: str = ''
|
||||
NotifyURL: str = ''
|
||||
ReturnURL: str = ''
|
||||
Extra: Dict[str, str] = field(default_factory=dict)
|
||||
```
|
||||
|
||||
### 4.2 PaymentGatewayConfig(数据库持久化—加密)
|
||||
|
||||
每个租户每个渠道一条配置,密钥字段加密存储:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ConfigUUID | UUID | 主键 |
|
||||
| TenantUUID | UUID | 租户标识 |
|
||||
| Platform | CharField(32) | wechat / alipay / bank |
|
||||
| AppID | CharField(64) | 应用ID |
|
||||
| MchID | CharField(64) | 商户号 |
|
||||
| APIKeyEncrypted | CharField(512) | 加密的 API 密钥 |
|
||||
| PrivateKeyEncrypted | TextField | 加密的私钥 |
|
||||
| NotifyURL | CharField(512) | 回调地址 |
|
||||
| ConfigStatus | SmallIntegerField | 1=启用 0=禁用 |
|
||||
|
||||
### 4.3 PaymentTransaction(数据库持久化)
|
||||
|
||||
每笔支付/退款/转账操作的统一流水记录:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| TransactionUUID | UUID | 主键 |
|
||||
| TenantUUID | UUID | 租户 |
|
||||
| Platform | CharField(32) | 支付平台 |
|
||||
| TransactionType | CharField(16) | pay / refund / transfer |
|
||||
| OutTradeNO | CharField(64) | 商户订单号(唯一) |
|
||||
| TotalAmount | DecimalField | 金额(最大18位,4位小数) |
|
||||
| TransactionStatus | SmallIntegerField | 0=处理中 1=成功 2=失败 3=已关闭 |
|
||||
|
||||
### 4.4 请求/响应数据模型(dataclass,非持久化)
|
||||
|
||||
| 数据类 | 用途 |
|
||||
|--------|------|
|
||||
| `PaymentRequest` | 支付请求标准化入参 |
|
||||
| `PaymentResponse` | 支付响应标准化出参(含 `PrepayID` 属性) |
|
||||
| `RefundRequest` | 退款请求 |
|
||||
| `RefundResponse` | 退款响应 |
|
||||
| `TransferRequest` | 转账请求(企业付款到零钱/银行卡) |
|
||||
| `TransferResponse` | 转账响应 |
|
||||
| `QueryResponse` | 订单查询响应(含 `IsPaid`/`IsRefunded` 属性) |
|
||||
|
||||
---
|
||||
|
||||
## 五、抽象网关接口
|
||||
|
||||
```python
|
||||
class AbstractPaymentGateway(ABC):
|
||||
Channel: PaymentChannel = 'custom'
|
||||
Name: str = 'Abstract'
|
||||
|
||||
def __init__(self, credentials: PaymentCredentials):
|
||||
self._credentials = credentials
|
||||
self._validate_credentials()
|
||||
|
||||
@abstractmethod
|
||||
def Pay(self, request: PaymentRequest) -> PaymentResponse: ...
|
||||
|
||||
@abstractmethod
|
||||
def Query(self, out_trade_no: str) -> QueryResponse: ...
|
||||
|
||||
@abstractmethod
|
||||
def Refund(self, request: RefundRequest) -> RefundResponse: ...
|
||||
|
||||
@abstractmethod
|
||||
def Close(self, out_trade_no: str) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]: ...
|
||||
|
||||
def Transfer(self, request: TransferRequest) -> TransferResponse:
|
||||
raise NotImplementedError(f'{self.Name} 不支持转账功能')
|
||||
```
|
||||
|
||||
### 已实现适配器
|
||||
|
||||
| 渠道 | Channel | 类名 | 支持的方法 | Transfer |
|
||||
|------|---------|------|------------|----------|
|
||||
| 微信支付 | `wechat` | `WechatPaymentGateway` | jsapi / native / h5 / miniapp | 支持 |
|
||||
| 支付宝 | `alipay` | `AlipayPaymentGateway` | app / h5 / native | 支持 |
|
||||
| 银行卡 | `bank` | `BankPaymentGateway` | app / h5 / native | 仅 bank 类型 |
|
||||
|
||||
### 新增支付渠道只需三步
|
||||
|
||||
```python
|
||||
# 1. 实现适配器
|
||||
class FooPaymentGateway(AbstractPaymentGateway):
|
||||
Channel = 'foo'
|
||||
Name = '某支付'
|
||||
|
||||
def Pay(self, request: PaymentRequest) -> PaymentResponse:
|
||||
...
|
||||
|
||||
# 2. 在 apps.py 注册
|
||||
payment_registry.register('foo', FooPaymentGateway)
|
||||
|
||||
# 3. 子服务配置密钥后即可使用
|
||||
manager = PaymentManager(tenant_uuid)
|
||||
manager.Pay(platform='foo', amount=100)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、PaymentManager 编排器
|
||||
|
||||
```python
|
||||
class PaymentManager:
|
||||
def __init__(self, tenant_uuid)
|
||||
|
||||
def ConfigureGateway(platform, app_id, mch_id, api_key, ...) # 配置支付渠道
|
||||
def Pay(platform, amount, subject, method, ...) # 发起支付
|
||||
def Query(out_trade_no, platform=None) # 查询订单
|
||||
def Refund(out_trade_no, amount, reason, ...) # 发起退款
|
||||
def Transfer(platform, amount, payee_account, ...) # 企业付款
|
||||
def HandleNotify(platform, raw_data, headers=None) # 处理回调
|
||||
def GetGatewayInstance(platform) # 获取网关实例(供子服务自定义)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、API 端点
|
||||
|
||||
```
|
||||
POST /api/payment/config/ # 配置支付渠道(SAAS管理员)
|
||||
GET /api/payment/config/ # 查看已配置渠道列表
|
||||
DELETE /api/payment/config/ # 禁用渠道
|
||||
|
||||
POST /api/payment/pay/ # 发起支付
|
||||
GET /api/payment/query/<out_trade_no>/ # 查询订单
|
||||
POST /api/payment/refund/ # 发起退款
|
||||
POST /api/payment/transfer/ # 企业付款
|
||||
|
||||
POST /api/payment/notify/<platform>/ # 支付回调(微信/支付宝等)
|
||||
GET /api/payment/channels/ # 列出所有可用渠道
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、子服务集成示例
|
||||
|
||||
```python
|
||||
from gvsds.apps.payment.manager import PaymentManager, PaymentError
|
||||
|
||||
manager = PaymentManager(tenant_uuid='al_long_001')
|
||||
|
||||
# 配置微信支付
|
||||
manager.ConfigureGateway(
|
||||
platform='wechat',
|
||||
app_id='wx1234567890',
|
||||
mch_id='1234567890',
|
||||
api_key='your_api_key',
|
||||
notify_url='https://al_long.gvsds.com/api/payment/notify/wechat/',
|
||||
)
|
||||
|
||||
# 发起支付
|
||||
tx = manager.Pay(
|
||||
platform='wechat',
|
||||
amount=Decimal('100.00'),
|
||||
subject='订单支付',
|
||||
method='jsapi',
|
||||
open_id='user_openid',
|
||||
)
|
||||
|
||||
# tx._prepay_info 包含前端调起支付所需的参数
|
||||
return {'prepay_info': tx._prepay_info}
|
||||
|
||||
# 查询订单
|
||||
result = manager.Query(out_trade_no='PY-ABC123')
|
||||
if result.IsPaid:
|
||||
# 更新子服务订单状态
|
||||
...
|
||||
|
||||
# 退款
|
||||
tx = manager.Refund(out_trade_no='PY-ABC123', amount=Decimal('100.00'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、安全设计
|
||||
|
||||
1. **密钥加密存储**:`APIKey`、`PrivateKey`、`PublicKey` 使用 XOR + Base64 加密后入库
|
||||
2. **加密密钥外置**:通过 `settings.PAYMENT_ENCRYPTION_KEY`(从 secrets 文件读取)管理
|
||||
3. **回调签名验证**:每个网关适配器自行实现 `VerifyNotify()` 验证回调签名的合法性
|
||||
4. **回调端点不认证**:`NotifyView` 关闭 DRF 的认证和权限类,但网关层签名验证保证安全
|
||||
5. **密钥不输出**:`PaymentCredentials.AsDict()` 方法中 `api_key` 显示为 `***`
|
||||
6. **幂等保护**:`OutTradeNO` 唯一约束,防止重复支付记录
|
||||
@@ -0,0 +1,383 @@
|
||||
# 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)
|
||||
│
|
||||
└──→ StatEngine
|
||||
│
|
||||
├── 写: AtomicIncrement() → StatRecord(并发安全)
|
||||
├── 读: QueryToday() / QueryRange() / AggregateTopN()
|
||||
├── 排: GenerateDailyRanking() → StatRanking(快照)
|
||||
├── 清: DailyReset() / MonthlyReset()(定时任务触发)
|
||||
└── 配: DefineMetric() / ConfigureAutoReset()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、文件清单(10 个文件)
|
||||
|
||||
| 层级 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| **模型** | `models.py` | 5 张表:Metric / Dimension / Record / Ranking / Config |
|
||||
| **引擎** | `engine.py` | 7 个原子函数:AtomicIncrement / QueryPeriod / AggregateByDimension / GenerateRanking |
|
||||
| **编排器** | `manager.py` | StatEngine — 15 个方法覆盖增删查改排清配 |
|
||||
| **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()` 确保锁在事务结束前持有
|
||||
|
||||
---
|
||||
|
||||
## 七、StatEngine 编排器
|
||||
|
||||
```python
|
||||
class StatEngine:
|
||||
def __init__(self, tenant_uuid)
|
||||
|
||||
# 指标管理
|
||||
def DefineMetric(metric_code, metric_name, metric_type, unit) # 定义统计指标
|
||||
def DefineDimension(dimension_code, dimension_name) # 定义统计维度
|
||||
def GetMetrics() / GetDimensions() # 查询
|
||||
|
||||
# 配置
|
||||
def ConfigureAutoReset(metric_uuid, auto_reset, enable_ranking, ...) # 配置重置/排行
|
||||
|
||||
# 数据写入
|
||||
def Increment(metric_uuid, dimension_values, delta, date) # 原子增量
|
||||
def IncrementMulti(increments, date) # 批量原子增量
|
||||
def Decrement(metric_uuid, dimension_values, delta, date) # 原子减量
|
||||
|
||||
# 数据查询
|
||||
def QueryToday(metric_uuid, dimension_filter) # 今日统计
|
||||
def QueryDate(metric_uuid, date, period_type) # 指定日期
|
||||
def QueryRange(metric_uuid, start_date, end_date) # 时间范围
|
||||
def AggregateTopN(metric_uuid, start_date, end_date, dimension_key) # 聚合排名
|
||||
|
||||
# 排行榜
|
||||
def GenerateDailyRanking(metric_uuid, ranking_date, top_n) # 生成日榜
|
||||
def GenerateMonthlyRanking(metric_uuid, ranking_date, top_n) # 生成月榜
|
||||
def QueryRanking(ranking_period, ranking_date, metric_uuid) # 查询排行榜
|
||||
|
||||
# 维护
|
||||
def DailyReset(metric_uuid, target_date) # 手动日清
|
||||
def MonthlyReset(metric_uuid, target_date) # 手动月清
|
||||
def RunScheduledResets(target_date) # 定时清零(Celery调用)
|
||||
def BuildRankings(target_date) # 定时生成排行(Celery调用)
|
||||
def PurgeOld(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 gvsds.apps.stats.manager import StatEngine
|
||||
|
||||
engine = StatEngine(tenant_uuid='al_long_001')
|
||||
|
||||
# 定义指标
|
||||
jiedan = engine.DefineMetric('dashou_jiedan', '打手接单数', 'count', '单')
|
||||
shouru = engine.DefineMetric('dashou_shouru', '打手收入', 'amount', '元')
|
||||
chongzhi = engine.DefineMetric('guanshi_chongzhi', '管事充值人数', 'count', '人')
|
||||
vip = engine.DefineMetric('vip_sale', '会员销量', 'count', '份')
|
||||
|
||||
# 定义维度
|
||||
engine.DefineDimension('role', '角色')
|
||||
engine.DefineDimension('user_id', '用户ID')
|
||||
engine.DefineDimension('platform', '平台')
|
||||
engine.DefineDimension('order_type', '订单类型')
|
||||
|
||||
# 配置每日清零 + 排行榜
|
||||
engine.ConfigureAutoReset(
|
||||
metric_uuid=jiedan.MetricUUID,
|
||||
auto_reset=True,
|
||||
enable_ranking=True,
|
||||
ranking_top_n=50,
|
||||
)
|
||||
```
|
||||
|
||||
### 9.2 订单完成时写统计
|
||||
|
||||
```python
|
||||
# 订单完成 → 写多条统计(原子批量)
|
||||
engine.IncrementMulti(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
|
||||
# 查询打手123今日统计
|
||||
records = engine.QueryToday(
|
||||
metric_uuid=jiedan.MetricUUID,
|
||||
dimension_filter={'user_id': 'd_123'},
|
||||
)
|
||||
# → [{period_key:'2026-06-11', dimension_values:{...}, value: 5}]
|
||||
|
||||
# 查询今日所有打手排名(实时聚合)
|
||||
top = engine.AggregateTopN(
|
||||
metric_uuid=jiedan.MetricUUID,
|
||||
start_date=datetime.date.today(),
|
||||
end_date=datetime.date.today(),
|
||||
dimension_key='user_id',
|
||||
top_n=10,
|
||||
)
|
||||
# → [{dim_value:'d_123', total: 5}, {dim_value:'d_456', total: 3}, ...]
|
||||
```
|
||||
|
||||
### 9.4 排行榜
|
||||
|
||||
```python
|
||||
# 生成今日排行榜快照(定时任务调用)
|
||||
engine.GenerateDailyRanking(
|
||||
metric_uuid=jiedan.MetricUUID,
|
||||
dimension_filter={'role': 'dashou'},
|
||||
top_n=50,
|
||||
)
|
||||
|
||||
# 前端查询日榜
|
||||
rankings = engine.QueryRanking(
|
||||
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 中注册每日任务
|
||||
@app.task
|
||||
def stats_daily_reset():
|
||||
"""每日统计清零 + 排行榜生成"""
|
||||
from gvsds.apps.stats.models import StatConfig
|
||||
from gvsds.apps.stats.manager import StatEngine
|
||||
|
||||
tenants = StatConfig.objects.filter(AutoReset=True).values_list('TenantUUID', flat=True).distinct()
|
||||
for tenant in tenants:
|
||||
engine = StatEngine(tenant)
|
||||
engine.RunScheduledResets()
|
||||
engine.BuildRankings()
|
||||
```
|
||||
|
||||
### 9.6 退款回滚统计
|
||||
|
||||
```python
|
||||
# 退款 → 回滚相关统计
|
||||
engine.Decrement(
|
||||
metric_uuid=jiedan.MetricUUID,
|
||||
dimension_values={'role': 'dashou', 'user_id': 'd_123'},
|
||||
delta=1,
|
||||
)
|
||||
engine.Decrement(
|
||||
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'))` |
|
||||
Reference in new issue
Block a user