# 元数据驱动的请求生命周期
这张图会反复用到。xly 中每个元数据驱动页面都遵循同一流程;理解它之后,框架其余部分只是主题变奏。
## 流程
```text
Browser
1. 任意 URL 加载 SPA shell(服务端对每个路径返回同一 shell;
gdsroute 是客户端侧边栏 / deep-link 白名单,不是服务端 404 闸门)
2. 用户点击侧边栏项或在 SPA 内导航
3. SPA 决定加载哪个模块 → 调用 /business/...
GET /xlyEntry/business/getModelBysId/{sModelsId}?sModelsId={sModelsId}
(模块 id 同时出现在 path 和 query 中;controller 绑定路径变量,
但 service 从 @RequestParam map 读取 sModelsId,所以 SPA 也必须在 query string 中传它)
xlyEntry — BusinessBaseController.getModelBysId()
RequestAddParamUtil.addParams(params, userInfo)
→ sBrandsId、sSubsidiaryId、sUserId、sLanguage 等
→ 共 16 个 key(详见 runtime.md)
→ 租户作用域变成下游查询可用的参数。框架元数据读取按 form-id 过滤;
每租户覆盖与业务数据读取才按 sBrandsId / sSubsidiaryId 过滤。
BusinessBaseService.getModelBysId(map)
├── 1. formData
│ └── gdsconfigformmaster(按 sParentId = sModelsId 过滤;
│ gdsmodule 本身不被 SELECT,只通过 id 引用)
│ + LEFT JOIN gdsconfigformpersonalize(每租户覆盖)
│ + 每个 master 的 gdsconfigformslave
│ + 每个 master 的 gdsconfigformcustomslave(每租户覆盖)
├── 2. gdsformconst(仅按 sParentId 过滤;不按租户过滤;
│ sLanguage 决定返回哪一列标签)
├── 3. sysjurisdiction(按用户 / 用户组授权,JOIN
│ sftlogininfojurisdictiongroup + sisjurisdictionclassify;
│ ADMIN 跳过。返回 map key 仍叫 `gdsjurisdiction`,
│ 这个名字有误导性:gdsjurisdiction 表是配置侧动作目录,
│ 不是这里读取的授权表)
├── 4. sysbillnosettings(每租户、每表单)
└── 5. sysreport(每租户、每表单)
返回一个复合 map:
{ formData, gdsformconst, gdsjurisdiction, billnosetting, report }
Browser SPA
使用 formData 渲染表单;使用 gdsformconst 和按控件另取的 SQL 填充下拉项
调用 /business/getBusinessDataByFormcustomId/{formId}
并带 sModelsId={moduleId} 加载实际数据行
xlyEntry — BusinessBaseController.getBusinessDataByFormcustomId()
从 formMaster.sSqlStr / sWhere / sOrder 拼出参数化 SQL,注入
sBrandsId / sSubsidiaryId,并在表单支撑对象(table | view | proc)上执行
用户看到表格
```
## 同一流程的时序图
上面的 ASCII 图强调执行顺序;下面的时序图强调**谁调用谁**。排查真实请求时,后者通常更有用。
```mermaid
sequenceDiagram
autonumber
participant SPA as Browser SPA
participant CTRL as BusinessBaseController
participant SVC as BusinessBaseServiceImpl
participant FORMS as BusinessGdsconfigformsServiceImpl
participant DB as MySQL
participant REDIS as Redis (RedisCacheManager)
SPA->>CTRL: GET /business/getModelBysId/{sModelsId}
?sModelsId=...&Authorization=
Note over CTRL: AuthorizationInterceptor.preHandle
从 Redis 解析 UserInfo
RequestAddParamUtil.addParams(16 个 key)
CTRL->>SVC: getModelBysId(map)
Note over SVC: getModelConfigByModleId(继承自 BaseServiceImpl)
编排每个 form-master 的主表单 + 从表加载
SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow
(form-master + slaves + overlays)
REDIS-->>FORMS: cache hit?
FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize;每个 master 再读 gdsconfigformslave + gdsconfigformcustomslave
DB-->>FORMS: rows
FORMS-->>SVC: formData
SVC->>FORMS: getFormconstData(只按 form-id,不按租户)
FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=...
DB-->>FORMS: rows
FORMS-->>SVC: gdsformconst
alt sUserType != ADMIN
SVC->>FORMS: getJurisdictionData(每用户授权)
FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup
DB-->>FORMS: rows
FORMS-->>SVC: gdsjurisdiction(map key;源表是 sysjurisdiction)
else ADMIN
Note over SVC: 跳过权限加载
end
SVC->>FORMS: getBillnosettingData
FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant
DB-->>FORMS: row
FORMS-->>SVC: billnosetting
SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant
DB-->>SVC: report rows
SVC-->>CTRL: composite Map(5 个 key)
CTRL-->>SPA: AjaxResult{code:1, dataset:{...}}
SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}
?sModelsId=...
Note over CTRL,SVC: 同一次 RequestAddParamUtil 注入
随后使用每表单 sSqlStr / sWhere / sOrder
CTRL->>DB: 对表单支撑 table/view/proc 执行参数化 SELECT
DB-->>CTRL: rows
CTRL-->>SPA: dataset
```
图中第 1 行和第 22 行是两个 HTTP 往返。中间全部是服务端工作,SPA 看不到。
## 五键复合结果
`getModelBysId` 返回一个 Java `Map`,按顺序包含这些 key:
| Key | 来源 | SPA 用途 |
|---|---|---|
| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize`(每租户覆盖);每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave` 覆盖。`gdsmodule` 只通过 id 引用。 | 表单布局本身:每个字段、控件、标签、校验规则 |
| `gdsformconst` | 仅按 `sParentId` 过滤的 `gdsformconst` 行;不按租户过滤;`sLanguage` 决定返回哪一列标签 | 表单级常量:标签、默认值、下拉文本 |
| `gdsjurisdiction` | 用户的 `sysjurisdiction` 行,或通过 `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify` 读取用户组授权;ADMIN 跳过。map key 名称 `gdsjurisdiction` 有误导性:那张表是配置侧动作目录,不是这里读取的授权表。 | 按钮和数据权限 |
| `billnosetting` | 该模块的 `sysbillnosettings` 行(每租户) | 单据编号规则(工单号、报价单号) |
| `report` | 关联到该表单的 `sysreport` 行(每租户) | 打印模板(jxls Excel、iText PDF) |
## 不在这个生命周期中的内容
- **保存路径。** 保存有自己的端点 `POST /business/addUpdateDelBusinessData`,把新增 / 更新 / 删除打包进一个请求。见[切片 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save)。
- **打开已有行编辑的读取。** 与表格加载使用同一端点 `getBusinessDataByFormcustomId`,但 body 中带行 `sId`,handler 按请求一行还是多行分支。
- **工作流步骤。** 如果模块有活动审批流(`bCheck = 1`、`gdsmoduleflow` 已配置、Activiti 流程已部署,并且 `ConstantUtils.bCheckflowCheck = true`),会插入额外步骤。当前 dev DB 没有这些数据;见[切片 7(暂缓)](../slices/07-workflow.md)。
- **缓存失效。** **后台**修改元数据行时,保存服务会同步调用 `BusinessCleanRedisData` / `CleanRedisServiceImpl`,从共享 Redis 中驱逐 Spring cache region。JMS 的 `ConsumerChangeGdsModuleThread` 是另一条基础数据合并通道,不是缓存失效。
## 其他切片覆盖的变体
- [切片 1](../slices/01-hello-world.md):规范实例,带观察到的网络流量。
- [切片 2](../slices/02-multi-tenancy.md):多租户过滤如何贯穿每一步。
- [切片 3](../slices/03-report.md):视图支撑而非表支撑。
- [切片 4](../slices/04-custom-field.md):`gdsconfigformcustomslave` 合并步骤。
- [切片 5](../slices/05-customer-sql-override.md):某租户的存储过程主体被替换时。
## 读一次,并保持打开
如果后续章节出现你不认识的术语,它几乎一定指向上图中的某个框。回到这里即可。