# 元数据驱动的请求生命周期

这张图会反复用到。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}<br/>?sModelsId=...&Authorization=<bearer>
    Note over CTRL: AuthorizationInterceptor.preHandle<br/>从 Redis 解析 UserInfo<br/>RequestAddParamUtil.addParams（16 个 key）

    CTRL->>SVC: getModelBysId(map)
    Note over SVC: getModelConfigByModleId（继承自 BaseServiceImpl）<br/>编排每个 form-master 的主表单 + 从表加载
    SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow<br/>（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}<br/>?sModelsId=...
    Note over CTRL,SVC: 同一次 RequestAddParamUtil 注入<br/>随后使用每表单 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)：某租户的存储过程主体被替换时。

## 读一次，并保持打开

如果后续章节出现你不认识的术语，它几乎一定指向上图中的某个框。回到这里即可。
