520 lines
17 KiB
Python
520 lines
17 KiB
Python
"""模块:分佣编排器
|
||
SAAS 级分佣引擎核心入口——子服务通过此引擎触发分佣事件。
|
||
|
||
职责:
|
||
1. 接收分佣事件(Dispatch)
|
||
2. 匹配分佣规则
|
||
3. 执行分账策略计算
|
||
4. 写入 CommissionRecord 流水
|
||
5. 支持预结算(跨平台确认)
|
||
6. 支持邀请链分佣(多级分佣)"""
|
||
|
||
import uuid
|
||
import datetime
|
||
import logging
|
||
from decimal import Decimal
|
||
from typing import List, Optional, Dict, Any
|
||
from django.db.models import Sum as _AggSum
|
||
from gvsdsdk.commission.models import (
|
||
CommissionEvent,
|
||
CommissionRule,
|
||
CommissionRecord,
|
||
RateTable,
|
||
PreSettlement,
|
||
)
|
||
from gvsdsdk.commission.strategies.engine import (
|
||
CreateStrategy,
|
||
RemainderStrategy,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class CommissionError(Exception):
|
||
def __init__(self, message, event=None):
|
||
super().__init__(message)
|
||
self.Event = event
|
||
|
||
|
||
class CommissionEngine:
|
||
"""分佣编排器——SAAS 主服务器核心入口
|
||
|
||
用法:
|
||
engine = CommissionEngine(tenant_uuid='xxx')
|
||
|
||
# 配置分佣规则(SAAS管理员操作)
|
||
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'),
|
||
source_user_uuid='boss_001',
|
||
participants={
|
||
'worker': 'dashou_123',
|
||
'agent': 'guanshi_456',
|
||
'leader': 'zuzhang_789',
|
||
},
|
||
tier_index=1,
|
||
payload={'order_id': 'xxx', 'game': 'LoL'},
|
||
)
|
||
|
||
# 跨平台预结算
|
||
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'),
|
||
)
|
||
"""
|
||
|
||
def __init__(self, tenant_uuid):
|
||
self.TenantUUID = tenant_uuid
|
||
|
||
def ConfigureRule(self, event_type, rule_name, participant_rules,
|
||
rule_description='', max_invite_level=2,
|
||
rate_table_uuid=None, rule_priority=0):
|
||
"""配置分佣规则——SAAS 管理员为租户配置分佣策略
|
||
|
||
Args:
|
||
event_type: 事件类型 ('order_complete', 'member_purchase', ...)
|
||
rule_name: 规则名称
|
||
participant_rules: 参与者分账规则列表
|
||
[
|
||
{'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'},
|
||
]
|
||
rule_description: 规则描述
|
||
max_invite_level: 邀请链最大层级
|
||
rate_table_uuid: 关联费率表UUID
|
||
rule_priority: 优先级(数字越大越优先)
|
||
|
||
Returns:
|
||
CommissionRule 实例
|
||
"""
|
||
rate_table = None
|
||
if rate_table_uuid:
|
||
try:
|
||
rate_table = RateTable.objects.get(
|
||
RateTableUUID=rate_table_uuid, TenantUUID=self.TenantUUID,
|
||
)
|
||
except RateTable.DoesNotExist:
|
||
raise CommissionError(f'费率表不存在: {rate_table_uuid}')
|
||
|
||
rule = CommissionRule.objects.create(
|
||
RuleUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
EventType=event_type,
|
||
RuleName=rule_name,
|
||
RuleDescription=rule_description,
|
||
ParticipantRules=participant_rules,
|
||
RateTable=rate_table,
|
||
MaxInviteLevel=max_invite_level,
|
||
RulePriority=rule_priority,
|
||
)
|
||
logger.info(f'分佣规则已配置: {rule_name}@tenant={self.TenantUUID}')
|
||
return rule
|
||
|
||
def ConfigureRateTable(self, table_code, table_name, entries,
|
||
table_description=''):
|
||
"""配置费率表
|
||
|
||
Args:
|
||
table_code: 表编码(如 'dashou_order_split')
|
||
table_name: 表名
|
||
entries: 费率条目字典 {key: value}
|
||
table_description: 描述
|
||
|
||
Returns:
|
||
RateTable 实例
|
||
"""
|
||
from gvsdsdk.commission.models import RateEntry
|
||
|
||
table = RateTable.objects.create(
|
||
RateTableUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
TableCode=table_code,
|
||
TableName=table_name,
|
||
TableDescription=table_description,
|
||
)
|
||
|
||
for key, value in entries.items():
|
||
RateEntry.objects.create(
|
||
EntryUUID=uuid.uuid4().bytes,
|
||
RateTable=table,
|
||
RateKey=str(key),
|
||
RateValue=Decimal(str(value)),
|
||
)
|
||
|
||
logger.info(f'费率表已创建: {table_code}@tenant={self.TenantUUID}')
|
||
return table
|
||
|
||
def Dispatch(self, event_type, event_key, total_amount,
|
||
source_user_uuid=None, participants=None,
|
||
tier_index=0, payload=None, auto_settle=True):
|
||
"""触发分佣事件——子服务调用的核心入口
|
||
|
||
Args:
|
||
event_type: 事件类型
|
||
event_key: 业务唯一键(幂等保护)
|
||
total_amount: 可分佣总金额
|
||
source_user_uuid: 触发者UUID(如付费用户)
|
||
participants: 参与者映射 {role: user_id}
|
||
tier_index: 阶梯索引(多次分红时用,第1次=1)
|
||
payload: 业务上下文(JSON)
|
||
auto_settle: 是否自动结算
|
||
|
||
Returns:
|
||
List[CommissionRecord] 分佣记录列表
|
||
|
||
Raises:
|
||
CommissionError: 重复事件或规则未配置
|
||
"""
|
||
if CommissionEvent.objects.filter(
|
||
TenantUUID=self.TenantUUID, EventKey=event_key,
|
||
).exists():
|
||
raise CommissionError(f'事件已处理: {event_key}')
|
||
|
||
total_amount = Decimal(str(total_amount))
|
||
|
||
event = CommissionEvent.objects.create(
|
||
EventUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
EventType=event_type,
|
||
EventKey=event_key,
|
||
TotalAmount=total_amount,
|
||
SourceUserUUID=source_user_uuid,
|
||
EventPayload=payload or {},
|
||
Participants=participants or {},
|
||
EventStatus=CommissionEvent.EVT_PENDING,
|
||
)
|
||
|
||
rules = CommissionRule.objects.filter(
|
||
TenantUUID=self.TenantUUID,
|
||
EventType=event_type,
|
||
RuleStatus=1,
|
||
).order_by('-RulePriority')
|
||
|
||
if not rules.exists():
|
||
event.MarkFailed()
|
||
raise CommissionError(f'未配置分佣规则: {event_type}', event=event)
|
||
|
||
event.MarkProcessing()
|
||
|
||
all_records = []
|
||
for rule in rules:
|
||
records = self._execute_rule(
|
||
event=event,
|
||
rule=rule,
|
||
total_amount=total_amount,
|
||
participants=participants or {},
|
||
tier_index=tier_index,
|
||
)
|
||
all_records.extend(records)
|
||
|
||
if auto_settle:
|
||
event.MarkSettled()
|
||
for rec in all_records:
|
||
rec.Settle()
|
||
|
||
return all_records
|
||
|
||
def _execute_rule(self, event, rule, total_amount, participants, tier_index):
|
||
"""执行单条分佣规则"""
|
||
rate_table = rule.RateTable
|
||
participant_rules = rule.ParticipantRules
|
||
records = []
|
||
allocated = Decimal('0')
|
||
|
||
remainder_rule = None
|
||
ordered_rules = sorted(
|
||
participant_rules,
|
||
key=lambda r: 0 if r.get('Type') == 'remainder' else 1,
|
||
)
|
||
|
||
for pr in ordered_rules:
|
||
if pr.get('Type') == 'remainder':
|
||
remainder_rule = pr
|
||
continue
|
||
|
||
strategy = CreateStrategy(pr, rate_table)
|
||
amount = strategy.Calculate(
|
||
total_amount,
|
||
tier_index=tier_index,
|
||
)
|
||
allocated += amount
|
||
|
||
participant_id = self._resolve_participant(pr, participants)
|
||
if amount > 0:
|
||
records.append(self._create_record(
|
||
event=event,
|
||
rule=rule,
|
||
participant_id=participant_id,
|
||
role=pr.get('Role', 'unknown'),
|
||
split_type=pr.get('Type', 'unknown'),
|
||
amount=amount,
|
||
rule_snapshot=pr,
|
||
))
|
||
|
||
if remainder_rule and allocated < total_amount:
|
||
strategy = CreateStrategy(remainder_rule, rate_table)
|
||
amount = strategy.Calculate(
|
||
total_amount,
|
||
allocated_amount=allocated,
|
||
)
|
||
participant_id = self._resolve_participant(remainder_rule, participants)
|
||
if amount > 0:
|
||
records.append(self._create_record(
|
||
event=event,
|
||
rule=rule,
|
||
participant_id=participant_id,
|
||
role=remainder_rule.get('role', 'platform'),
|
||
split_type='remainder',
|
||
amount=amount,
|
||
rule_snapshot=remainder_rule,
|
||
))
|
||
|
||
for rec in records:
|
||
rec.TierIndex = tier_index
|
||
|
||
return records
|
||
|
||
def _resolve_participant(self, participant_rule, participants):
|
||
role = participant_rule.get('Role', 'unknown')
|
||
Fallback = participant_rule.get('FallbackID', '')
|
||
return participants.get(role, Fallback or f'{role}_default')
|
||
|
||
def _create_record(self, event, rule, participant_id, role, split_type, amount, rule_snapshot):
|
||
return CommissionRecord.objects.create(
|
||
RecordUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
Event=event,
|
||
Rule=rule,
|
||
ParticipantID=str(participant_id),
|
||
ParticipantRole=role,
|
||
SplitType=split_type,
|
||
Amount=amount,
|
||
RuleSnapshot=rule_snapshot,
|
||
)
|
||
|
||
def PreSettle(self, event_key, external_platform, external_order_id,
|
||
participant_id, amount, order_amount=Decimal('0'),
|
||
participant_role='worker', confirm_deadline=None):
|
||
"""创建预结算记录——跨平台订单待确认
|
||
|
||
Args:
|
||
event_key: 业务唯一键
|
||
external_platform: 外部平台标识
|
||
external_order_id: 外部订单ID
|
||
participant_id: 应分账对象
|
||
amount: 预分金额
|
||
order_amount: 订单原金额
|
||
participant_role: 参与者角色
|
||
confirm_deadline: 确认截止时间
|
||
|
||
Returns:
|
||
PreSettlement 实例
|
||
"""
|
||
ps = PreSettlement.objects.create(
|
||
SettlementUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
EventKey=event_key,
|
||
ExternalPlatform=external_platform,
|
||
ExternalOrderID=external_order_id,
|
||
ParticipantID=participant_id,
|
||
ParticipantRole=participant_role,
|
||
Amount=Decimal(str(amount)),
|
||
OrderAmount=Decimal(str(order_amount)),
|
||
ConfirmDeadline=confirm_deadline,
|
||
)
|
||
logger.info(
|
||
f'预结算: {event_key}->{participant_id}={amount}'
|
||
f'@tenant={self.TenantUUID}'
|
||
)
|
||
return ps
|
||
|
||
def ConfirmPreSettlement(self, event_key, participant_id=None,
|
||
confirmed_by=''):
|
||
"""确认预结算——转为正式分佣记录
|
||
|
||
Args:
|
||
event_key: 业务唯一键
|
||
participant_id: 参与者ID(None=全部确认)
|
||
confirmed_by: 确认人
|
||
|
||
Returns:
|
||
int 确认条数
|
||
"""
|
||
qs = PreSettlement.objects.filter(
|
||
TenantUUID=self.TenantUUID,
|
||
EventKey=event_key,
|
||
Status=PreSettlement.PST_PENDING,
|
||
)
|
||
if participant_id:
|
||
qs = qs.filter(ParticipantID=participant_id)
|
||
|
||
count = 0
|
||
for ps in qs:
|
||
ps.Confirm(confirmed_by)
|
||
count += 1
|
||
return count
|
||
|
||
def RejectPreSettlement(self, event_key, participant_id=None,
|
||
confirmed_by=''):
|
||
"""拒绝预结算
|
||
|
||
Args:
|
||
event_key: 业务唯一键
|
||
participant_id: 参与者ID(None=全部拒绝)
|
||
confirmed_by: 操作人
|
||
|
||
Returns:
|
||
int 拒绝条数
|
||
"""
|
||
qs = PreSettlement.objects.filter(
|
||
TenantUUID=self.TenantUUID,
|
||
EventKey=event_key,
|
||
Status=PreSettlement.PST_PENDING,
|
||
)
|
||
if participant_id:
|
||
qs = qs.filter(ParticipantID=participant_id)
|
||
|
||
count = 0
|
||
for ps in qs:
|
||
ps.Reject(confirmed_by)
|
||
count += 1
|
||
return count
|
||
|
||
def CalculateFee(self, total_amount, rate_table_code, rate_key):
|
||
"""计算手续费——用于提现场景
|
||
|
||
Args:
|
||
total_amount: 提现金额
|
||
rate_table_code: 费率表编码
|
||
rate_key: 费率键
|
||
|
||
Returns:
|
||
Decimal 手续费金额
|
||
"""
|
||
total_amount = Decimal(str(total_amount))
|
||
try:
|
||
table = RateTable.objects.get(
|
||
TenantUUID=self.TenantUUID,
|
||
TableCode=rate_table_code,
|
||
TableStatus=1,
|
||
)
|
||
except RateTable.DoesNotExist:
|
||
return Decimal('0')
|
||
|
||
rate = table.GetRate(rate_key)
|
||
if rate is None:
|
||
return Decimal('0')
|
||
|
||
return total_amount * rate
|
||
|
||
def QueryRecords(self, participant_id=None, event_type=None,
|
||
status=None, start_date=None, end_date=None,
|
||
limit=50):
|
||
"""查询分佣记录
|
||
|
||
Args:
|
||
participant_id: 参与者ID
|
||
event_type: 事件类型
|
||
status: 状态
|
||
start_date: 开始日期
|
||
end_date: 结束日期
|
||
limit: 返回条数
|
||
|
||
Returns:
|
||
QuerySet[CommissionRecord]
|
||
"""
|
||
qs = CommissionRecord.objects.filter(TenantUUID=self.TenantUUID)
|
||
|
||
if participant_id:
|
||
qs = qs.filter(ParticipantID=participant_id)
|
||
if status is not None:
|
||
qs = qs.filter(Status=status)
|
||
if event_type:
|
||
qs = qs.filter(Event__EventType=event_type)
|
||
if start_date:
|
||
qs = qs.filter(CreateTime__gte=start_date)
|
||
if end_date:
|
||
qs = qs.filter(CreateTime__lte=end_date)
|
||
|
||
return qs.order_by('-CreateTime')[:limit]
|
||
|
||
def TotalByParticipant(self, participant_id, start_date=None, end_date=None):
|
||
"""统计某参与者的分佣总额"""
|
||
qs = CommissionRecord.objects.filter(
|
||
TenantUUID=self.TenantUUID,
|
||
ParticipantID=participant_id,
|
||
Status__gte=CommissionRecord.REC_SETTLED,
|
||
)
|
||
if start_date:
|
||
qs = qs.filter(CreateTime__gte=start_date)
|
||
if end_date:
|
||
qs = qs.filter(CreateTime__lte=end_date)
|
||
|
||
result = qs.aggregate(total=_AggSum('Amount'))
|
||
return result['total'] or Decimal('0')
|
||
|
||
def BuildInviteChainCommission(self, event_type, event_key, total_amount,
|
||
source_user_uuid, invite_chain,
|
||
rule_name, participant_rules):
|
||
"""邀请链分佣——按层级对受邀人行为进行多级分佣
|
||
|
||
Args:
|
||
event_type: 事件类型
|
||
event_key: 事件键
|
||
total_amount: 总金额
|
||
source_user_uuid: 行为触发者
|
||
invite_chain: 邀请链 [{user_uuid, level, role}, ...]
|
||
level=1 为直接邀请人,level=2 为间推
|
||
rule_name: 规则名称
|
||
participant_rules: 参与者规则(按层级配置)
|
||
例: [{'role': 'agent_l1', 'type': 'fixed', 'value': 10},
|
||
{'role': 'agent_l2', 'type': 'fixed', 'value': 5}]
|
||
|
||
Returns:
|
||
List[CommissionRecord]
|
||
"""
|
||
rule = CommissionRule.objects.filter(
|
||
TenantUUID=self.TenantUUID,
|
||
EventType=event_type,
|
||
RuleName=rule_name,
|
||
RuleStatus=1,
|
||
).first()
|
||
|
||
if not rule:
|
||
rule = self.ConfigureRule(
|
||
event_type=event_type,
|
||
rule_name=rule_name,
|
||
participant_rules=participant_rules,
|
||
)
|
||
|
||
participants = {
|
||
item.get('Role', f'invite_l{item.get("Level",0)}'): item.get('UserUUID', '')
|
||
for item in invite_chain
|
||
}
|
||
participants['source'] = source_user_uuid
|
||
|
||
return self.Dispatch(
|
||
event_type=event_type,
|
||
event_key=event_key,
|
||
total_amount=total_amount,
|
||
source_user_uuid=source_user_uuid,
|
||
participants=participants,
|
||
)
|