10-全量SQL永远是对的:数据库变更与菜单权限的治理

全量 SQL 永远是对的:数据库变更与菜单权限的治理

这个系列写到第十篇,讲两个不那么光彩但极其重要的话题:数据库变更怎么管,以及菜单权限怎么不漏。它们的共同点是:做好了没人感谢你,做砸了全是用户可见的事故,而且事故形态特别迷惑——"功能明明开发了,页面上就是用不了"。

双轨制:全量是理想态,增量是现实路径

数据库变更有两份东西必须同时存在:

全量脚本 callcenter.sql,永远保持"最新态"。 新装一个库,跑这一个文件,一步到位。它是这个系统此刻的完整定义——63 张 cc_ 开头的表、索引、初始数据、菜单种子。

增量目录 backend_server/sql/postgresql/incremental/,存量库的专属通道。 命名 V<日期>_<NN>__描述.sql,一次变更一个文件。每个文件三条硬要求:幂等(重复执行不炸——加列用 IF NOT EXISTS,插菜单先查再插)、带 undo(回滚不是奢想,是发布流程的一环)、带执行校验(执行完 SELECT 出关键状态确认生效)。

这条双轨制的背后是一个我们真踩过的坑:只改全量脚本,存量库和生产库永远拿不到变更。全量脚本只服务"新装",已经跑着的库根本不看它。反过来,只写增量、忘了同步全量,下一个新环境装出来就是错的。所以规矩是同一处改动必须"增量写一份、全量同步改",两边对不上就是缺陷。

增量脚本的执行顺序也有讲究——计费双扣那次(V20261020_01)的教训直接写进了流程:先发版应用、后执行 SQL(新代码兼容旧库结构,反之不成立);执行前 pg_dump 目标表;高风险变更(改索引、改约束)要在文件头注释里写清前置条件和回滚步骤。这些要求枯燥到没人想读,但每次事故之后都会有人翻出来重读一遍。

顺带说一个与表结构有关的偏好:新功能配置字段能上真实列就不塞 JSONB。cc_outbound_task 有个 engine_params jsonb,早期图灵活把任务级配置都往里塞,结果查询、索引、约束全都别扭,前端表单和数据库形状对不上。后来加了任务级"有效时间段/排队时长/CRM 时长"三个配置,宁可走增量 SQL 加三列(承担双轨同步成本),也不再往 JSONB 里塞。JSONB 留给真正无形状的数据(LLM 对话上下文之类)。

菜单权限:三方比对法

管理后台是菜单驱动路由的:前端页面、后端接口、菜单权限种子数据,三方各持一份权限清单。前端用 v-hasPermi 控制按钮显隐,后端用 @PreAuthorize 拦截接口,菜单表 system_menu 里的权限串决定"哪些角色能看到、能授权"。三方任何一方不一致,就会出现经典故障:

管理员:"我明明授权了,为什么坐席点这个按钮没反应?"
或者反过来:按钮显示了,一点,403。

2026 年 10 月 16 日我们做了一次彻底整改,方法土但有效——三方比对:

  1. 脚本扫描前端代码,提取全部 v-hasPermi 权限串:207 个;
  2. 扫描后端全部 @PreAuthorize:265 个;
  3. dump 菜单表权限串:404 行;
  4. 三列对齐,找出"前端有、菜单没有""后端有、菜单没挂"的每一行,补齐 75 行 UPDATE。

那次的产出固化成了检查脚本,之后每次加功能,发版清单里固定有一项"新增权限串三方对齐"。

还有个隐蔽的复发性问题:权限挂载位置。10 月 19 日又出过一次——外呼任务详情页里有三个下拉(主叫设定、满意度调查、短信模板),对应接口的权限串只挂在各自管理页面的菜单下。租户管理员如果只给坐席开了"外呼任务"没开那三个管理页,坐席打开任务页,三个下拉全部 403。修复方式是给这类"跨页面引用的接口"补专门的菜单载体(8501~8503 几个隐藏菜单项,不显示、纯挂权限)。这类问题的通性是:接口的可见消费者不止一个页面时,权限必须挂在所有消费路径都可达的位置。

菜单 id 也有纪律:项目内使用固定号段(目前用到 8494,最新 max id 是 8493 之后顺延),避免和芋道官方号段冲突。看着是小事实则大——id 撞了,升级基座时菜单会被对方安装脚本覆盖。

最后一个坑与缓存有关:直改菜单表后必须清 Redis 的 permission_menu_ids:* 和 menu_role_ids:*。应用层的权限缓存靠注解失效,你绕过应用直接 UPDATE 库,它一无所知,界面表现就是"改了等于没改"。这条现在印在增量脚本的注释模板里。

授权范围:租户能看到哪些菜单

第三个相关问题是范围问题:TenantService.handleTenantMenu 决定一个租户能授权哪些菜单。这里的逻辑演化过一版,值得记下来。

最初的实现按套餐过滤:租户套餐里有的菜单才可见。这带来一个运营侧的困扰——平台超管切换到某租户视角排查问题时,看到的是"套餐过滤后"的菜单集合,很多菜单压根不出现,排查都没入口。

10 月 19 日改成了两条规则:

  • 超管跨租户操作:全量菜单(排查、修复需要看到一切);
  • 普通租户:套餐菜单 ∪ 租户管理员已被单独授权的菜单。

后半句是关键修正——"单独授权"是运营给某个租户开小灶的通道(比如新功能试点),如果只看套餐,这些授权会凭空消失,租户"昨天还能用的功能今天没了"。并集语义保住了所有历史授权的可见性。

这次修复还有一个连带收获:彻底理清了"超管看到 0 条数据"的一类问题根源——超管切租户后,数据查询要用 TenantContextHolder 显式指定目标租户(Redis 键、缓存键都按租户分段),从会话里取是取不到的。监控大屏全 0 的那次就是它。

写在系列末尾

十篇写完,回头看会发现一个模式:这个系统里真正的难点几乎没有一个是"功能写不出来",全都是一致性问题——全量与增量的一致、三方权限清单的一致、内存态与数据库的一致(Kamailio 那篇)、话单与流水的一致(计费那篇)、代码与部署环境的一致。分布式系统教科书讲的 CAP、幂等、最终一致,在业务系统的日常里换了一副面孔反复出现。

我们的应对没什么高招:把一致性检查脚本化(三方比对、日终对账、每日话单核对),把纪律写进模板(增量 SQL 头注释、发布清单、review 检查项),把事故写成复盘留在 docs/troubleshooting/ 里。系统是这些琐碎纪律的总和——这也是这十篇想传达的最后一件事。


本文是「云呼叫中心技术内幕」系列第 10 篇(完)。

posted @ 2026-10-25 21:17  cliffdev  阅读(3)  评论(0)    收藏  举报