Files
2026-06-16 11:55:14 +08:00

351 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模块:分佣 SAAS 数据模型
SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。
子服务只管说"发生了什么",SAAS 负责"怎么分"。"""
import uuid
import datetime
from django.db import models
from gvsds.model_base import QModel
from gvsds.apps.core.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, verbose_name='事件UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES, verbose_name='事件类型')
EventKey = models.CharField(max_length=128, verbose_name='事件键')
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='总金额')
SourceUserUUID = UUIDField(null=True, blank=True, verbose_name='来源用户UUID')
EventPayload = models.JSONField(default=dict, verbose_name='事件载荷')
Participants = models.JSONField(default=dict, verbose_name='参与方')
EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES, verbose_name='事件状态')
EventTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='事件时间')
ProcessTime = models.DateTimeField(null=True, blank=True, verbose_name='处理时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'CommissionEvent'
app_label = 'commission'
verbose_name = '分佣事件'
verbose_name_plural = '分佣事件'
unique_together = [('TenantUUID', 'EventKey')]
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, verbose_name='费率表UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
TableCode = models.CharField(max_length=64, verbose_name='费率表编码')
TableName = models.CharField(max_length=128, verbose_name='费率表名称')
TableDescription = models.CharField(max_length=512, default='', verbose_name='费率表描述')
TableStatus = models.SmallIntegerField(default=1, verbose_name='费率表状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间')
class Meta:
db_table = 'CommissionRateTable'
app_label = 'commission'
verbose_name = '费率表'
verbose_name_plural = '费率表'
unique_together = [('TenantUUID', 'TableCode')]
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, verbose_name='条目UUID')
RateTable = models.ForeignKey(
RateTable, on_delete=models.CASCADE,
related_name='Entries',
verbose_name='费率表',
)
RateKey = models.CharField(max_length=64, verbose_name='费率键')
RateLabel = models.CharField(max_length=128, default='', verbose_name='费率标签')
RateValue = models.DecimalField(max_digits=10, decimal_places=6, verbose_name='费率值')
EntryStatus = models.SmallIntegerField(default=1, verbose_name='条目状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'CommissionRateEntry'
app_label = 'commission'
verbose_name = '费率条目'
verbose_name_plural = '费率条目'
unique_together = [('RateTable', 'RateKey')]
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, verbose_name='规则UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES, verbose_name='事件类型')
RuleName = models.CharField(max_length=128, verbose_name='规则名称')
RuleDescription = models.CharField(max_length=512, default='', verbose_name='规则描述')
ParticipantRules = models.JSONField(default=list, verbose_name='参与方规则')
RateTable = models.ForeignKey(
RateTable, on_delete=models.SET_NULL,
null=True, blank=True, related_name='rules',
verbose_name='费率表',
)
MaxInviteLevel = models.SmallIntegerField(default=2, verbose_name='最大邀请层级')
RulePriority = models.IntegerField(default=0, verbose_name='规则优先级')
RuleStatus = models.SmallIntegerField(default=1, verbose_name='规则状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间')
class Meta:
db_table = 'CommissionRule'
app_label = 'commission'
verbose_name = '分佣规则'
verbose_name_plural = '分佣规则'
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, verbose_name='记录UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
Event = models.ForeignKey(
CommissionEvent, on_delete=models.CASCADE,
related_name='records',
verbose_name='分佣事件',
)
Rule = models.ForeignKey(
CommissionRule, on_delete=models.SET_NULL,
null=True, related_name='records',
verbose_name='分佣规则',
)
ParticipantID = models.CharField(max_length=128, verbose_name='参与方ID')
ParticipantRole = models.CharField(max_length=64, verbose_name='参与方角色')
SplitType = models.CharField(max_length=16, verbose_name='分账类型')
Amount = models.DecimalField(max_digits=18, decimal_places=4, verbose_name='金额')
RuleSnapshot = models.JSONField(default=dict, verbose_name='规则快照')
Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES, verbose_name='状态')
TierIndex = models.IntegerField(default=0, verbose_name='阶梯索引')
SettleTime = models.DateTimeField(null=True, blank=True, verbose_name='结算时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'CommissionRecord'
app_label = 'commission'
verbose_name = '分佣记录'
verbose_name_plural = '分佣记录'
ordering = ['-CreateTime']
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, verbose_name='预结算UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
EventKey = models.CharField(max_length=128, verbose_name='事件键')
ExternalPlatform = models.CharField(max_length=64, default='', verbose_name='外部平台')
ExternalOrderID = models.CharField(max_length=128, default='', verbose_name='外部订单ID')
ParticipantID = models.CharField(max_length=128, verbose_name='参与方ID')
ParticipantRole = models.CharField(max_length=64, default='worker', verbose_name='参与方角色')
Amount = models.DecimalField(max_digits=18, decimal_places=4, verbose_name='金额')
OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='订单金额')
Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES, verbose_name='状态')
ConfirmedBy = models.CharField(max_length=128, default='', verbose_name='确认人')
ConfirmDeadline = models.DateTimeField(null=True, blank=True, verbose_name='确认截止时间')
ConfirmedAt = models.DateTimeField(null=True, blank=True, verbose_name='确认时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间')
class Meta:
db_table = 'CommissionPreSettlement'
app_label = 'commission'
verbose_name = '预结算'
verbose_name_plural = '预结算'
unique_together = [('EventKey', 'ParticipantID')]
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'])