加入了 GVSDSDK 模块,进行了 QModel 兼容层的尝试,生产环境可用

This commit is contained in:
2026-06-16 00:13:10 +08:00
parent 9d9cfa53a4
commit 63e0f4edfc
68 changed files with 10256 additions and 1143 deletions

View File

@@ -0,0 +1,28 @@
"""gvsdsdk.commission — 分佣引擎"""
__all__ = [
'CommissionEvent', 'RateTable', 'RateEntry', 'CommissionRule',
'CommissionRecord', 'PreSettlement',
'CommissionEngine', 'CommissionError',
'CreateStrategy', 'BaseSplitStrategy', 'PercentageStrategy',
'FixedStrategy', 'TieredStrategy', 'RemainderStrategy', 'WithdrawalFeeStrategy',
]
def __getattr__(name):
if name in ('CommissionEvent', 'RateTable', 'RateEntry', 'CommissionRule', 'CommissionRecord', 'PreSettlement'):
from gvsdsdk.commission import models as _m
val = getattr(_m, name)
globals()[name] = val
return val
if name in ('CommissionEngine', 'CommissionError'):
from gvsdsdk.commission import manager as _m
val = getattr(_m, name)
globals()[name] = val
return val
if name in ('CreateStrategy', 'BaseSplitStrategy', 'PercentageStrategy', 'FixedStrategy', 'TieredStrategy', 'RemainderStrategy', 'WithdrawalFeeStrategy'):
from gvsdsdk.commission.strategies import engine as _m
val = getattr(_m, name)
globals()[name] = val
return val
raise AttributeError(f"module 'gvsdsdk.commission' has no attribute {name!r}")

View File

@@ -0,0 +1,519 @@
"""模块:分佣编排器
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: 参与者IDNone=全部确认)
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: 参与者IDNone=全部拒绝)
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,
)

View File

@@ -0,0 +1,352 @@
"""模块:分佣 SAAS 数据模型
SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。
子服务只管说"发生了什么"SAAS 负责"怎么分""""
import uuid
import datetime
from django.db import models
from gvsdsdk.model_base import QModel
from gvsdsdk.models import UUIDField
class CommissionEvent(QModel):
"""分佣事件表——子服务触发的分佣事件
子服务在业务发生时(订单完成、会员购买、充值到账等)
调用 CommissionEngine.Dispatch() 写入此表。
"""
EVT_ORDER_COMPLETE = 'order_complete'
EVT_MEMBER_PURCHASE = 'member_purchase'
EVT_RECHARGE = 'recharge'
EVT_PENALTY = 'penalty'
EVT_REGISTER = 'register'
EVT_CUSTOM = 'custom'
EVT_TYPE_CHOICES = [
(EVT_ORDER_COMPLETE, '订单完成'),
(EVT_MEMBER_PURCHASE, '会员购买'),
(EVT_RECHARGE, '充值到账'),
(EVT_PENALTY, '罚款缴纳'),
(EVT_REGISTER, '用户注册'),
(EVT_CUSTOM, '自定义事件'),
]
EVT_PENDING = 0
EVT_PROCESSING = 1
EVT_SETTLED = 2
EVT_FAILED = 3
EVT_STATUS_CHOICES = [
(EVT_PENDING, '待处理'),
(EVT_PROCESSING, '处理中'),
(EVT_SETTLED, '已结算'),
(EVT_FAILED, '结算失败'),
]
EventUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES)
EventKey = models.CharField(max_length=128)
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
SourceUserUUID = UUIDField(null=True, blank=True)
EventPayload = models.JSONField(default=dict)
Participants = models.JSONField(default=dict)
EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES)
EventTime = models.DateTimeField(default=datetime.datetime.utcnow)
ProcessTime = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionEvent'
app_label = 'gvsdsdk'
verbose_name = '分佣事件'
verbose_name_plural = '分佣事件'
unique_together = [('TenantUUID', 'EventKey')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'),
models.Index(fields=['EventKey'], name='idx_cmevt_event_key'),
models.Index(fields=['CreateTime'], name='idx_cmevt_create_time'),
]
def __str__(self):
return f'{self.EventType}:{self.EventKey}'
@property
def IsSettled(self):
return self.EventStatus == self.EVT_SETTLED
def MarkProcessing(self):
self.EventStatus = self.EVT_PROCESSING
self.save(update_fields=['EventStatus'])
def MarkSettled(self):
self.EventStatus = self.EVT_SETTLED
self.ProcessTime = datetime.datetime.utcnow()
self.save(update_fields=['EventStatus', 'ProcessTime'])
def MarkFailed(self):
self.EventStatus = self.EVT_FAILED
self.ProcessTime = datetime.datetime.utcnow()
self.save(update_fields=['EventStatus', 'ProcessTime'])
class RateTable(QModel):
"""费率表——可复用的费率配置
每个租户可创建多张费率表,不同事件类型绑定不同的表。
例如:打手分成费率表、提现手续费率表、罚款分红费率表。
"""
RateTableUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
TableCode = models.CharField(max_length=64)
TableName = models.CharField(max_length=128)
TableDescription = models.CharField(max_length=512, default='')
TableStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionRateTable'
app_label = 'gvsdsdk'
verbose_name = '费率表'
verbose_name_plural = '费率表'
unique_together = [('TenantUUID', 'TableCode')]
managed = False
indexes = [
models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant'),
]
def __str__(self):
return f'{self.TableCode}:{self.TableName}'
def GetRate(self, key):
try:
entry = self.Entries.get(RateKey=key, EntryStatus=1)
return entry.RateValue
except RateEntry.DoesNotExist:
return None
def GetRates(self):
return {
e.RateKey: e.RateValue
for e in self.Entries.filter(EntryStatus=1)
}
class RateEntry(QModel):
"""费率条目——费率表中的一行"""
EntryUUID = UUIDField(primary_key=True)
RateTable = models.ForeignKey(
RateTable, on_delete=models.CASCADE,
related_name='Entries',
)
RateKey = models.CharField(max_length=64)
RateLabel = models.CharField(max_length=128, default='')
RateValue = models.DecimalField(max_digits=10, decimal_places=6)
EntryStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionRateEntry'
app_label = 'gvsdsdk'
verbose_name = '费率条目'
verbose_name_plural = '费率条目'
unique_together = [('RateTable', 'RateKey')]
managed = False
indexes = [
models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key'),
]
def __str__(self):
return f'{self.RateKey}={self.RateValue}'
class CommissionRule(QModel):
"""分佣规则表——定义"怎么分"
每条规则绑定一种事件类型,定义参与者如何分账。
支持比例分账、固定金额、阶梯分账、剩余归平台四种 SplitType。
"""
SPLT_PERCENTAGE = 'percentage'
SPLT_FIXED = 'fixed'
SPLT_TIERED = 'tiered'
SPLT_REMAINDER = 'remainder'
SPLT_TYPE_CHOICES = [
(SPLT_PERCENTAGE, '比例分账'),
(SPLT_FIXED, '固定金额'),
(SPLT_TIERED, '阶梯分账'),
(SPLT_REMAINDER, '剩余归平台'),
]
RuleUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES)
RuleName = models.CharField(max_length=128)
RuleDescription = models.CharField(max_length=512, default='')
ParticipantRules = models.JSONField(default=list)
RateTable = models.ForeignKey(
RateTable, on_delete=models.SET_NULL,
null=True, blank=True, related_name='rules',
)
MaxInviteLevel = models.SmallIntegerField(default=2)
RulePriority = models.IntegerField(default=0)
RuleStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionRule'
app_label = 'gvsdsdk'
verbose_name = '分佣规则'
verbose_name_plural = '分佣规则'
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'),
models.Index(fields=['RulePriority'], name='idx_cmrule_priority'),
]
def __str__(self):
return f'{self.RuleName}({self.EventType})'
def FindParticipantRule(self, role):
for pr in self.ParticipantRules:
if pr.get('role') == role:
return pr
return None
class CommissionRecord(QModel):
"""分佣记录表——不可变分账流水
每条记录代表一次分账结果:谁、从哪个事件、按什么规则、分了多少。
生成后即冻结,规则变更不影响历史记录。
"""
REC_PENDING = 0
REC_SETTLED = 1
REC_WITHDRAWN = 2
REC_CANCELLED = 3
REC_STATUS_CHOICES = [
(REC_PENDING, '待结算'),
(REC_SETTLED, '已结算'),
(REC_WITHDRAWN, '已提现'),
(REC_CANCELLED, '已取消'),
]
RecordUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
Event = models.ForeignKey(
CommissionEvent, on_delete=models.CASCADE,
related_name='records',
)
Rule = models.ForeignKey(
CommissionRule, on_delete=models.SET_NULL,
null=True, related_name='records',
)
ParticipantID = models.CharField(max_length=128)
ParticipantRole = models.CharField(max_length=64)
SplitType = models.CharField(max_length=16)
Amount = models.DecimalField(max_digits=18, decimal_places=4)
RuleSnapshot = models.JSONField(default=dict)
Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES)
TierIndex = models.IntegerField(default=0)
SettleTime = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionRecord'
app_label = 'gvsdsdk'
verbose_name = '分佣记录'
verbose_name_plural = '分佣记录'
ordering = ['-CreateTime']
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'),
models.Index(fields=['Event'], name='idx_cmrec_event'),
models.Index(fields=['CreateTime'], name='idx_cmrec_create_time'),
]
def __str__(self):
return f'{self.ParticipantRole}:{self.ParticipantID}={self.Amount}'
@property
def IsSettled(self):
return self.Status >= self.REC_SETTLED
def Settle(self):
self.Status = self.REC_SETTLED
self.SettleTime = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'SettleTime'])
def Cancel(self):
self.Status = self.REC_CANCELLED
self.save(update_fields=['Status'])
class PreSettlement(QModel):
"""预结算表——跨平台订单待确认款项
当订单涉及外部平台时,打手分成先挂预结算,
对方确认后转为正式 CommissionRecord。
"""
PST_PENDING = 0
PST_CONFIRMED = 1
PST_REJECTED = 2
PST_STATUS_CHOICES = [
(PST_PENDING, '待确认'),
(PST_CONFIRMED, '已确认'),
(PST_REJECTED, '已拒绝'),
]
SettlementUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventKey = models.CharField(max_length=128)
ExternalPlatform = models.CharField(max_length=64, default='')
ExternalOrderID = models.CharField(max_length=128, default='')
ParticipantID = models.CharField(max_length=128)
ParticipantRole = models.CharField(max_length=64, default='worker')
Amount = models.DecimalField(max_digits=18, decimal_places=4)
OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES)
ConfirmedBy = models.CharField(max_length=128, default='')
ConfirmDeadline = models.DateTimeField(null=True, blank=True)
ConfirmedAt = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionPreSettlement'
app_label = 'gvsdsdk'
verbose_name = '预结算'
verbose_name_plural = '预结算'
unique_together = [('EventKey', 'ParticipantID')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'),
models.Index(fields=['EventKey'], name='idx_cmpst_event_key'),
models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order'),
]
def __str__(self):
return f'预结算:{self.EventKey}->{self.ParticipantID}={self.Amount}'
def Confirm(self, confirmed_by=''):
self.Status = self.PST_CONFIRMED
self.ConfirmedBy = confirmed_by
self.ConfirmedAt = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
def Reject(self, confirmed_by=''):
self.Status = self.PST_REJECTED
self.ConfirmedBy = confirmed_by
self.ConfirmedAt = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])

View File

@@ -0,0 +1,10 @@
"""gvsdsdk.commission.strategies — 分佣策略引擎"""
def __getattr__(name):
if name in ('CreateStrategy', 'BaseSplitStrategy', 'PercentageStrategy', 'FixedStrategy', 'TieredStrategy', 'RemainderStrategy', 'WithdrawalFeeStrategy'):
from gvsdsdk.commission.strategies import engine as _m
val = getattr(_m, name)
globals()[name] = val
return val
raise AttributeError(f"module 'gvsdsdk.commission.strategies' has no attribute {name!r}")

View File

@@ -0,0 +1,156 @@
"""模块:分佣策略引擎
策略模式实现——每种分账方式独立封装,新增分账逻辑只需添加新策略类。
支持四种策略:
PercentageStrategy: 比例分账 → TotalAmount × rate
FixedStrategy: 固定金额 → 固定值(受保底/封顶约束)
TieredStrategy: 阶梯分账 → 按次数/档位查表
RemainderStrategy: 剩余归平台 → TotalAmount - SUM(其他参与者)
"""
from abc import ABC, abstractmethod
from decimal import Decimal
from typing import Dict, List, Optional
from gvsdsdk.commission.models import RateTable
class BaseSplitStrategy(ABC):
"""分账策略抽象基类"""
def __init__(self, participant_rule: dict, rate_table: Optional[RateTable] = None):
self._rule = participant_rule
self._rate_table = rate_table
@abstractmethod
def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal:
...
@property
def Role(self):
return self._rule.get('Role', 'unknown')
@property
def SplitType(self):
return self._rule.get('Type', 'unknown')
def _get_rate_from_table(self, key):
if self._rate_table:
rate = self._rate_table.GetRate(key)
if rate is not None:
return rate
return self._rule.get('Rate', Decimal('0'))
class PercentageStrategy(BaseSplitStrategy):
"""比例分账策略
配置示例:
{ role: 'worker', type: 'percentage', value: 0.80 }
{ role: 'worker', type: 'percentage', rate_table_key: '1' }
"""
def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal:
rate_key = self._rule.get('RateTableKey', '')
if rate_key:
rate = self._get_rate_from_table(rate_key)
else:
rate = Decimal(str(self._rule.get('Value', 0)))
amount = total_amount * rate
return self._apply_bounds(amount, total_amount)
def _apply_bounds(self, amount, total_amount):
minimum = self._rule.get('MinAmount')
maximum = self._rule.get('MaxAmount')
if minimum is not None:
amount = max(amount, Decimal(str(minimum)))
if maximum is not None:
amount = min(amount, Decimal(str(maximum)))
return amount
class FixedStrategy(BaseSplitStrategy):
"""固定金额策略
配置示例:
{ role: 'agent', type: 'fixed', value: 10 }
{ role: 'leader', type: 'fixed', rate_table_key: 'guanshi_fc' }
"""
def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal:
rate_key = self._rule.get('RateTableKey', '')
if rate_key:
amount = self._get_rate_from_table(rate_key)
else:
amount = Decimal(str(self._rule.get('Value', 0)))
return amount
class TieredStrategy(BaseSplitStrategy):
"""阶梯分账策略——按次数/档位查表
配置示例:
{ role: 'agent', type: 'tiered',
tiers: { 1: 5, 2: 8, 3: 10 } }
或从费率表中按 key_N 形式查找。
"""
def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal:
tiers = self._rule.get('Tiers', {})
if tiers:
tier_key = str(tier_index)
raw_value = tiers.get(tier_key, tiers.get('default', 0))
if isinstance(raw_value, str) and raw_value.endswith('%'):
return total_amount * Decimal(raw_value.rstrip('%')) / Decimal('100')
return Decimal(str(raw_value))
tier_key = self._rule.get('TierKeyPrefix', '')
if tier_key and self._rate_table:
rate = self._rate_table.GetRate(f'{tier_key}_{tier_index}')
if rate is not None:
return rate
return Decimal('0')
class RemainderStrategy(BaseSplitStrategy):
"""剩余归平台策略——扣除所有其他参与者后的余额
配置示例:
{ role: 'platform', type: 'remainder' }
"""
def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal:
allocated = context.get('AllocatedAmount', Decimal('0'))
remainder = total_amount - allocated
return remainder if remainder > 0 else Decimal('0')
class WithdrawalFeeStrategy(BaseSplitStrategy):
"""提现手续费策略
配置示例:
{ role: 'platform', type: 'withdrawal_fee', value: 0.01 }
"""
def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal:
rate_key = self._rule.get('RateTableKey', '')
if rate_key:
rate = self._get_rate_from_table(rate_key)
else:
rate = Decimal(str(self._rule.get('Value', 0)))
return total_amount * rate
STRATEGY_MAP: Dict[str, type] = {
'percentage': PercentageStrategy,
'fixed': FixedStrategy,
'tiered': TieredStrategy,
'remainder': RemainderStrategy,
'withdrawal_fee': WithdrawalFeeStrategy,
}
def CreateStrategy(participant_rule: dict, rate_table: Optional[RateTable] = None) -> BaseSplitStrategy:
split_type = participant_rule.get('Type', 'percentage')
strategy_class = STRATEGY_MAP.get(split_type, PercentageStrategy)
return strategy_class(participant_rule, rate_table)