数据模型
三类数据,别混
这是整套设计的地基。
| 类别 | 表 | 应该在哪 | 谁维护 |
|---|---|---|---|
| 目录(catalog) | app_info、app_version、app_category、app_version_menu、app_dependency、app_plan | 平台级,全局一份 | 平台方(上架端) |
| 安装实例 | app_install、app_install_task | 每个部署各一份 | 各部署自己 |
| 迁移记录 | app_schema_history | 每个部署各一份 | 应用升级时自动写 |
关键前提:目录类表已经配置成"不按租户隔离" —— 它们列在 application.yml 的 security.tenant.excludes 里:
excludes:
- sys_menu
- sys_tenant
- ...
# 应用市场:应用目录属于平台级数据,不按租户隔离
- app_info
- app_version
- app_category
- app_version_menu
- app_dependency
- app_plan带 tenant_id 的 app_install / app_install_task / app_schema_history 不在此列,由多租户插件自动隔离。
由此得到两条推论:
- 想把目录集中到一台服务器、其它部署远程读 → 可行,而且不需要传租户参数;
- 想集中安装实例 → 不要做,那是每个部署自己的运行状态。
一张表看懂边界
┌─────────────────── 平台级(全局一份) ───────────────────┐
│ app_info 应用目录 │
│ app_version 版本与安装包(packageUrl / 校验和) │
│ app_category 分类 │
│ app_version_menu 版本 → 菜单 的归属映射(权威记录) │
│ app_dependency 应用依赖声明 │
│ app_plan 套餐/定价(预留) │
└────────────────────────────────────────────────────────┘
┌─────────────────── 部署级(按租户隔离) ─────────────────┐
│ app_install 本部署装了哪些应用、状态、版本 │
│ app_install_task 安装/升级/卸载的任务与步骤日志 │
│ app_schema_history 迁移执行账本 │
└────────────────────────────────────────────────────────┘app_version_menu:菜单归属的权威记录
它存 (version_id, menu_id, menu_key) 三元组,是"这个菜单属于哪个应用"的唯一权威来源。
为什么不直接在 sys_menu 上加一列 app_code?
- 加列要动框架的实体 / VO / Mapper XML / 菜单管理页面,代价大得多;
- 而且会和
app_version_menu形成两份真相。
反查链路(菜单归属提供者):
app_install(本租户装了的应用) → app_info(编码 / 名称)
→ app_version(这些应用的版本) → app_version_menu(版本 → 菜单)归属要连后代一起算
映射表里只有清单声明过的菜单,而按钮(menu_type='F')历史上来自平台初始化 SQL。 不收进来的话管理员能随手删掉应用按钮,恢复只能靠重装。
所以清单里必须写按钮(type: "F" + 唯一的 menu_key)。
app_schema_history:迁移账本
| 字段 | 说明 |
|---|---|
app_code | 应用编码 |
script | 脚本文件名 |
checksum | 脚本内容的 SHA-256(统一存 LF 归一化摘要) |
success | 是否成功 |
execution_ms | 执行耗时 |
tenant_id | 只记"谁最先应用" —— 不是去重判据 |
唯一键必须是 (app_code, script)
这是整个账本的关键约束,表结构与代码必须一致。
曾经 DDL 写的是 (tenant_id, app_code, version)(少了 script、多了 version), 于是一个版本里只要有第二个迁移脚本,第二次记账就撞唯一键:
Duplicate entry '000000-exam-1.0.1' for key 'app_schema_history.uk_schema_history'→ 安装/升级直接失败。
它藏得久是因为所有应用的 migrations/ 一直只有一个脚本;而它不是升级专有的毛病 —— 全新部署安装一个多脚本应用同样会炸。
存量部署执行 script/sql/update/app_market_schema_history_fix.sql (幂等:先去重、再删旧键、再建新键)。回归测试会在 DDL 与代码不一致时直接失败。
卸载会删哪些表
依据是应用包自己,平台不维护"应用 → 表"的清单:
- 扫描
<pluginDir>/migrations/<appCode>/下的脚本; - 解析出
CREATE TABLE的表名(先剥注释,且只认CREATE TABLE); - 只用这个集合去 DROP。
这样"应用拥有哪些表"这件事只有一个来源 —— 应用自己声明的。
再加上平台表前缀保护:sys_ / app_ / gen_ / flow_ / act_ / qrtz_ / snailjob_ / undo_log 开头的一律跳过并告警。
关键状态字段
| 表 | 字段 | 取值 |
|---|---|---|
app_info | status | 0 草稿 / 1 已上架 / 2 已下架 |
app_info | install_mode | system 系统应用(平台自带,不可卸载、不进市场)/ optional 可选应用 |
app_info | audience | platform 平台管理员 / tenant B 端租户管理员 / member C 端用户(逗号分隔) |
app_info | home_path | 入口路由,如 /gallery。系统应用必填;已安装应用留空则从菜单树推导 |
app_version | status | 0 待发布 / 1 已发布 / 2 已弃用 |
app_install | status | installing / installed / enabled / disabled / upgrading / failed / uninstalling / uninstalled |
"能不能在市场里看到"的完整口径
三个条件同时满足:install_mode = 'optional' 且 status = '1'(已上架) 且 命中当前身份的 audience。
audience = 'platform' 的应用只有超管看得到(普通租户被 FIND_IN_SET('tenant', audience) 滤掉)—— 这是"普通用户不该看到系统管理入口"的实现方式:靠可见范围过滤,而不是靠隐藏。
相关阅读
- 数据库表参考 —— 每张表的字段、索引与生命周期
- 核心概念
- 目录中心化与版本形态
- 数据库迁移