如意 Django CRM 架构复盘:Django Admin 为什么不该自动绑定租户
OKOK大家好欢迎大家来到大鹏 AI 教育我是张大鹏。这次我们不讲一个新页面。我们复盘一个比页面更重要的问题Django Admin 到底是谁在用它应该看到哪个范围的数据如意 CRM 是多租户系统。最初做 Admin 首页时我把业务端的组织会员关系带了进来。系统从管理员的Profile推断唯一组织再按组织展示指标。没有Profile或同时属于多个组织时首页数据就会消失。这个实现看起来很“安全”实际上混淆了两种后台。真正需要修复的不是一条查询而是 Django Admin 的架构边界。一、从错误的单组织首页发现边界问题问题始于一段合理但前提错误的多租户推断。1.1 为什么看似安全的租户推断其实错位错误思路并不复杂CRM 是多租户系统任何入口都应先得到当前组织。但这句话漏掉了一个前提不同入口面对的使用者并不相同。先看两条路径的分叉点。问题并不是有没有org而是谁有权决定它。为什么左侧越努力寻找“唯一组织”越容易挡住合法管理员原因在于它使用了业务会员关系解释平台身份。业务入口租户员工依靠组织上下文限制业务数据。平台入口开发、运维和平台管理员维护全平台模型。身份来源Profile是 CRM 会员关系不是 staff 身份。错误后果缺少唯一会员关系时合法平台数据被隐藏。结合无Profile和多Profile场景我的判断很明确。左侧不是更严格而是使用了错误的身份来源。边界结论租户上下文只属于租户业务入口。修复行动Admin 首页移除会员数量和唯一组织推断。因此“自动选择唯一组织”没有增加真正的安全性。它只是把错误的身份模型放到了错误的入口里。1.2 四个问题重新确认 Admin 边界我把这次问题压缩成四个必须先回答的问题。它们是使用者、入口、数据范围和信任级别。这四个节点中哪一个可以等到写完代码以后再补答案是一个也不能。使用者入口服务于开发、运维和平台管理员。访问入口使用原生/admin/不是租户 CRM 工作台。数据范围默认查看全平台人可以主动筛选组织。️信任级别Django staff 和模型权限定义平台信任。我在返工中最明确的体会是四问不是一份文档模板。它是动手前的架构闸门。判断顺序先完成边界分类再讨论查询和布局。停止条件答案不清楚时停止实现不允许静默补全。答案确定后很多争论自然结束了。Admin 不从Profile、request.org、JWT 或 API Key 推断租户。它也不把跨组织指标伪装成某个组织的经营看板。二、在三种后台边界方案中做选择边界明确以后才有资格比较具体方案。2.1 单租户 Admin、平台 Admin 和物理拆分围绕 Django Admin我评估了三种方案。三张卡片真正比较的是什么它们比较的是入口、数据范围和维护成本如何组合。单租户 Admin登录后绑定一个组织只显示组织数据。️平台 Admin沿用/admin/全平台数据显式展示组织。物理拆分新建平台入口和注册白名单形成强隔离。当前只有内部人员使用 Admin也不计划建设第二套后台。因此我选择中间方案而不是把“隔离最强”当成“当前最好”。当前选择将 Django Admin 定义为内部平台控制台。⏳重选条件租户管理员进入 Admin 时重新做架构决策。单租户方案适合租户管理员直接使用 Admin 的产品。这不是当前如意 CRM 的使用方式。物理拆分边界最强但超出了本次 Admin 优化范围。2.2 为什么当前选择内部平台 Admin最终选择保留/admin/由 Django 权限控制平台操作。这符合 Django 官方对 Admin 的定位。官方将其描述为可信用户使用的、以模型为中心的内部管理工具。当需求转向业务流程界面时应编写自己的视图。不应该把整个业务前端建立在 Admin 上。https://docs.djangoproject.com/en/6.0/ref/contrib/admin/⚙️原生能力保留模型权限、CSRF、审计日志和成熟表单。范围标签首页明确标注“全平台数据”。主动筛选管理员选择组织筛选不被系统静默绑定。改造半径使用浅层AdminSite和ModelAdmin扩展。这不是说多租户不重要。而是把多租户控制放回正确的位置。业务 API 与 PostgreSQL RLS 继续保护租户数据。Django Admin 承担受信平台人员的内部治理任务。三、把架构决策写进源码文字定义边界以后源码必须执行同一个答案。3.1RuyiAdminSite不再推断当前组织RuyiAdminSite.index()只构造平台首页上下文。它不读取管理员的 CRM 会员关系。defindex(self,request,extra_contextNone):context{**(extra_contextor{})}context[ruyi_dashboard]build_admin_dashboard(request,self)returnsuper().index(request,extra_contextcontext)仪表盘依据 Django 模型权限决定指标和快捷入口是否可见。统计查询本身面向全平台。查看权限指标根据对应的view_model权限显示。➕新增权限快捷入口根据对应的add_model权限显示。️范围声明首页固定展示“全平台数据”标签。入口保护非 staff 用户仍由原生 Admin 拒绝。无会员关系、单会员关系和多会员关系不会产生三套 Admin 语义。这正是平台边界稳定后的直接结果。3.2 组织归属必须显式可见平台范围不等于忽略组织归属。管理员能跨组织操作时org更应该成为显式信息。判断节点分出的三条路径说明了什么组织信息要同时覆盖查看、筛选和写入。️组织列查看记录时立即识别它属于哪个组织。组织筛选排查问题时主动缩小到指定组织。️组织表单新增记录时明确选择记录归属。我选择在RuyiAdminSite.register()阶段统一增强。它覆盖所有已注册且含org字段的模型又不侵入业务查询。实现结论组织归属显式可见但不自动过滤数据。例外规则特殊模型必须记录理由并补充精确测试。PlatformOrgAdminMixin补充组织列、筛选器和表单字段。它不修改查询集也不自动填入某个组织。defget_list_display(self,request):displaytuple(super().get_list_display(request))returndisplayiforgindisplayelse(*display,org)Django 官方把ModelAdmin定义为模型在管理界面中的表示。它提供列表、筛选器和表单字段等扩展点。https://docs.djangoproject.com/en/6.0/ref/contrib/admin/#modeladmin-objects查看合同所属组织始终进入模型列表信息。️检索合同组织始终成为平台管理员的筛选维度。✍️写入合同新增和编辑表单必须暴露组织字段。统一注册通用混入避免业务应用逐个遗漏组织信息。四、让一次修复变成长期边界源码可以修复当前问题但不能独自约束未来行为。4.1 为什么只改源码还不够项目过去没有清晰规则说明 Admin 不是租户工作台。如果只删除一次查询同样的推断仍可能在下一次优化中回来。因此我把决策落到了五个层次。闭环里最关键的是哪一个节点关键不是单点而是测试能否重新连接到最初的架构理由。架构决策记录选择理由、影响范围和重新决策条件。行为规范AGENTS.md在 Admin 改动前设置四问闸门。专用技能ruyi-admin-boundary给出禁区和验收命令。源码约束统一AdminSite与混入类执行平台边界。✅自动测试无会员、多会员和组织字段合同阻止回归。我不再把“已经解释过”当成长期保障。只有决策可执行、执行可验证边界才真正进入项目。治理结论规范、源码和测试必须首尾相接。后续行动每次 Admin 改动从四问开始以测试结束。文档解释“为什么”规范约束“开始之前”。源码执行“现在”测试阻止“以后退回去”。4.2 验收结果和适用边界本次聚焦测试共通过 16 项。迁移漂移检查没有发现新迁移。文档治理检查和差异检查也通过。无会员场景无Profile的超级管理员可使用平台首页。多会员场景多组织会员关系不会隐藏或改变指标。跨组织统计两个组织的数据进入全平台指标。⛔非 staff 场景普通登录用户不能进入 Django Admin。️组织字段合同带org的模型暴露列、筛选和表单。三语言行为简体中文、繁体中文和英语测试继续通过。这些结果证明 Admin 平台边界和组织显式化合同成立。它们不代表业务端租户隔离的全部安全性已经被重新验收。这次没有扩展 SvelteKit、Flutter、业务 API 或 PostgreSQL RLS。也没有把桌面端和移动端业务验收混入 Django Admin 范围。边界修复既要知道必须改什么也要知道不该顺手改什么。如果未来租户管理员真的需要使用 Django Admin项目应新建架构决策设计独立入口、组织选择和权限模型。不能在当前平台 Admin 中恢复自动租户推断。这次复盘留下的原则很简单多租户入口都要有数据边界。但边界不能靠猜。先确认使用者、入口、数据范围和信任级别再让代码执行这个答案。