首次上传

This commit is contained in:
TermiNexus committed 2026-06-16 11:55:14 +08:00
commit 04f7349a1e
19190 files changed
+2414744

No files matched your search

+443
View File
@@ -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'))` |
+263
View File
@@ -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` 唯一约束,防止重复支付记录
+383
View File
@@ -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'))` |