Files
Django/gvsdsdk/commission/models.py

353 lines
12 KiB
Python
Raw 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 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'])