"""核心模块的跨域基础:Tenant、Org、AuditLog 模型。""" from django.conf import settings from django.db import models class Tenant(models.Model): """租户:每个经销商/企业 = 1 个 Tenant。 阶段 1 落地;多租户隔离的所有业务表都必须带 tenant_id。 """ code = models.SlugField(max_length=64, unique=True, help_text="租户代码(小写字母数字)") name = models.CharField(max_length=128, help_text="租户名称(公司/品牌名)") contact = models.CharField(max_length=64, blank=True, default="") phone = models.CharField(max_length=32, blank=True, default="") is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "core_tenant" ordering = ["code"] def __str__(self): return f"{self.code} ({self.name})" class TenantMembership(models.Model): """用户可访问的租户关系。 租户头只选择上下文,真正的访问权由这张关系表决定。保留角色字段, 先用于最小授权边界,后续可在不改变租户隔离模型的情况下细分权限。 """ ROLE_CHOICES = [ ("owner", "所有者"), ("admin", "管理员"), ("member", "成员"), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="tenant_memberships", ) tenant = models.ForeignKey( "core.Tenant", on_delete=models.CASCADE, related_name="memberships", ) role = models.CharField(max_length=16, choices=ROLE_CHOICES, default="member") is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "core_tenant_membership" constraints = [ models.UniqueConstraint( fields=["user", "tenant"], name="core_membership_user_tenant_uniq" ), ] indexes = [ models.Index(fields=["user", "is_active"]), models.Index(fields=["tenant", "is_active"]), ] def __str__(self): return f"{self.user} -> {self.tenant.code} ({self.role})" class Org(models.Model): """组织:租户下的多组织/多门店/多仓库。 预留点:分销 ERP 多机构独立核算需要。 通过 org_path 支持层级结构(0001.0002.0003)。 """ tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, related_name="orgs", ) code = models.CharField(max_length=64, help_text="组织代码(在租户内唯一)") name = models.CharField(max_length=128) org_path = models.CharField( max_length=255, blank=True, default="", help_text="组织层级路径,如 0001.0002.0003;用于快速子树查询", ) parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", ) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "core_org" unique_together = [("tenant", "code")] ordering = ["org_path", "code"] def __str__(self): return f"{self.tenant.code}/{self.code} {self.name}" class AuditLog(models.Model): """审计日志:记录关键操作(增删改、登录、敏感查看)。""" ACTION_CHOICES = [ ("create", "创建"), ("update", "更新"), ("delete", "删除"), ("view", "查看"), ("login", "登录"), ("logout", "登出"), ("export", "导出"), ] tenant = models.ForeignKey( Tenant, null=True, blank=True, on_delete=models.SET_NULL, related_name="audit_logs", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="audit_logs", ) action = models.CharField(max_length=16, choices=ACTION_CHOICES) target_type = models.CharField(max_length=64, help_text="目标模型名,如 SalesOrder") target_id = models.CharField(max_length=64, blank=True, default="") detail = models.JSONField(default=dict, blank=True, help_text="操作详情 JSON") ip = models.GenericIPAddressField(null=True, blank=True) user_agent = models.CharField(max_length=255, blank=True, default="") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "core_audit_log" indexes = [ models.Index(fields=["tenant", "-created_at"]), models.Index(fields=["target_type", "target_id"]), models.Index(fields=["user", "-created_at"]), ] ordering = ["-created_at"] def __str__(self): return f"[{self.created_at}] {self.action} {self.target_type}#{self.target_id}"