# 切片 1 — CRUD 模块(Hello World) xly 中最简单的端到端动作:打开一个系统模块、查看数据、编辑一行、保存。我们从 URL 栏一直追踪到运行时渲染到表格中的行,中间经过每一层。 本章基于观察到的网络流量和匹配源码。仍为假设的内容(例如尚未实际执行的保存路径)会明确标注。 ## 记录对象 | | | |---|---| | **模块** | `系统常量配置`(System Constants Config) | | **URL 片段** | `/xtclpz` | | **模块 `sId`** | `13` | | **支撑表** | [`gdsformconst`](../auto-catalog/tables/gdsformconst.md)(dev 中 2,986 行) | | **表单 `sId`** | `19211681019715574676360040` | | **字段数** | 10 | | **保存 / 删除过程** | 无,使用框架默认路径 | | **审批工作流** | 无(`bCheck = 0`) | 这个模块本身就是框架的一部分:它是 PM 编辑系统常量的 **后台** 页面。记录它具有“元”意义:我们会看到框架如何配置框架。 ## 为什么选这个模块 第一,`sId = 13` 说明它是靠近根部的基础模块。第二,它**没有自定义过程**:保存 / 删除使用框架默认 CRUD 路径,这是读者应先学会的规范代码路径。自定义过程属于后续切片。 ## 四个元数据层 运行时读取四张元数据表来渲染页面: | 层 | 表 | 含义 | |---|---|---| | 1. 模块目录 | [`gdsmodule`](../auto-catalog/tables/gdsmodule.md) | 模块树。`sId='13'` 是本切片对象。 | | 2. URL 白名单(客户端侧) | [`gdsroute`](../auto-catalog/tables/gdsroute.md) | URL `/xtclpz` 在这里注册。SPA 读取该列表决定渲染哪些侧边栏 / deep-link;服务端不会对未注册路径返回 404。 | | 3. 表单布局 | [`gdsconfigformmaster`](../auto-catalog/tables/gdsconfigformmaster.md) | 一行 `sParentId='13'` 声明表单:支撑表、类型、默认 SQL / where / order、分页和权限。 | | 4. 字段布局 | [`gdsconfigformslave`](../auto-catalog/tables/gdsconfigformslave.md) | 10 行,`sParentId` 等于表单 `sId`。每行是一个字段:控件类型、多语言标签、校验、默认值、下拉 SQL 等。 | 这次四表读取就是 **xly 每个元数据驱动页面的生命周期**。理解它之后,框架其余部分只是变体。 ## 追踪与证据 ### 0. 服务形状 **后台** URL `http://118.178.19.35:8597` 由 **`xlyEntry`** WAR 提供(观察:每个 `/business/*` 调用的 context-path 都是 `/xlyEntry/`)。早期猜测是 `xlyApi`,这是错误的;`xlyApi` 是兄弟 WAR,有自己的 `application.yml` 和 context-path `/xlyApi`,角色不同。部署章节会覆盖二者拆分。 ### 1. 浏览器 → 服务端(页面 shell) ```text GET /xtclpz ``` `/xtclpz` 是 `gdsroute` 中的一项,但运行时它只是**显示状态**,不是路由驱动。登录后直接访问该 URL,不会深链到“系统常量配置”模块,而是加载与其他 URL 相同的 SPA shell,并显示默认落地模块。用户仍需点击侧边栏让 SPA 切换模块。 需要修正的早期假设:`gdsroute` **不在服务端强制**。探测未注册路径 `GET /xtclpz_NOT_A_REAL_ROUTE` 也返回同一 200 + SPA shell。白名单是客户端构造:SPA 读取 `gdsroute` 判断愿意挂载哪些侧边栏 / deep-link;真正分发发生在 SPA 加载后。 ### 2. 用户点击侧边栏,SPA 拉取元数据 ```text GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK ``` 路径变量和 query 参数都是 `13`,即 `gdsmodule` 中的**模块 `sId`**。handler 是 `BusinessBaseController.getModelBysId()`。 > **命名提醒:** 参数叫 `sModelsId`,helper 叫 `getModelConfigByModleId`(Modle 是 Model 拼写错误),但它们实际都接收模块 `sId`。原 Javadoc 写“传入窗体sId”,也是错的。阅读代码时可把 `Models / Modle / Model` 统一理解为“module”。 `BusinessBaseServiceImpl.getModelBysId()` 返回一个包含五个 key 的复合 map: | Key | 来源 | 内容 | |---|---|---| | `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ personalize) | 表单布局主干 | | `gdsformconst` | 按 `sBrandsId` / `sSubsidiaryId` / language 过滤的 `gdsformconst` 行 | 表单级常量、标签、默认值、下拉文本 | | `gdsjurisdiction` | 用户角色的 `gdsjurisdiction` 行 | 按钮和数据权限;`ADMIN` 用户跳过,管理员看到全部 | | `billnosetting` | 该模块的 `sysbillnosettings` 行 | 单据编号规则;对 `gdsformconst` 无关但总会加载 | | `report` | 关联到该表单的打印模板 | 打印报表定义 | **多租户**在这次读取中执行:每个子查询都带有从认证 session 注入的 `sBrandsId` 和 `sSubsidiaryId`。租户之间看不到对方元数据。 ### 3. SPA → 服务端(初始数据加载) ```text POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200 OK ``` 路径变量是**表单 `sId`**,query 参数是**模块 `sId`**。handler 是 `BusinessBaseController.getBusinessDataByFormcustomId()`。 handler 根据请求 body 是否包含 `sGroupList` 分支:没有 group-by 就走 `getBusinessDataByFormcustomId`,有则走 `getBusinessDataByGroup`。本模块走简单路径。 SQL 由 `gdsconfigformmaster.sSqlStr` + `sWhere` + `sOrder` 组合,并带入用户租户和语言参数后通过 MyBatis 执行。同一路径也处理编辑时按明细 ID 拉取一行。 ### 4. 用户编辑一行并点击保存 {#4-user-edits-a-row-clicks-save} 保存、删除、新增都进入同一通用端点: ```text POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} ``` 请求体形状: ```json { "addData": [{"sTable": "