"""模块:分佣 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'])