切片 1 — CRUD 模块(Hello World)
xly 中最简单的端到端动作:打开一个系统模块、查看数据、编辑一行、保存。我们从 URL 栏一直追踪到运行时渲染到表格中的行,中间经过每一层。
本章基于观察到的网络流量和匹配源码。仍为假设的内容(例如尚未实际执行的保存路径)会明确标注。
记录对象
| 模块 |
系统常量配置(System Constants Config) |
| URL 片段 | /xtclpz |
模块 sId |
13 |
| 支撑表 |
gdsformconst(dev 中 2,986 行) |
表单 sId |
19211681019715574676360040 |
| 字段数 | 10 |
| 保存 / 删除过程 | 无,使用框架默认路径 |
| 审批工作流 | 无(bCheck = 0) |
这个模块本身就是框架的一部分:它是 PM 编辑系统常量的 后台 页面。记录它具有“元”意义:我们会看到框架如何配置框架。
为什么选这个模块
第一,sId = 13 说明它是靠近根部的基础模块。第二,它没有自定义过程:保存 / 删除使用框架默认 CRUD 路径,这是读者应先学会的规范代码路径。自定义过程属于后续切片。
四个元数据层
运行时读取四张元数据表来渲染页面:
| 层 | 表 | 含义 |
|---|---|---|
| 1. 模块目录 | gdsmodule |
模块树。sId='13' 是本切片对象。 |
| 2. URL 白名单(客户端侧) | gdsroute |
URL /xtclpz 在这里注册。SPA 读取该列表决定渲染哪些侧边栏 / deep-link;服务端不会对未注册路径返回 404。 |
| 3. 表单布局 | gdsconfigformmaster |
一行 sParentId='13' 声明表单:支撑表、类型、默认 SQL / where / order、分页和权限。 |
| 4. 字段布局 | gdsconfigformslave |
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)
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 拉取元数据
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 → 服务端(初始数据加载)
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}
保存、删除、新增都进入同一通用端点:
POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId}
请求体形状:
{
"addData": [{"sTable": "<table>", "column": {"sId": "", "...": "..."}}],
"updateData": [{"sTable": "<table>", "column": {"sId": "", "...": "..."}}],
"delData": [{"sTable": "<table>", "column": {"sId": "", "...": "..."}}]
}
三种操作打包成一个原子请求。前端告诉后端要写哪张表和哪些列;没有每模块专用写 API,元数据驱动 UI 根据 gdsconfigformmaster.sTbName 和 gdsconfigformslave 字段列表生成 payload。
-
sSaveProName为空时,运行时走AddDelUpdCommonServiceImpl.java的默认 Add/Update 路径,生成参数化INSERT/UPDATE/DELETE。 -
sSaveProName非空时,运行时调用指定存储过程。xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql是工程师编写这类过程时使用的脚手架。
待验证: 真实保存请求体、响应体以及
syslog4j中的实际 SQL 还未捕获。为了避免修改共享 dev DB 中的框架常量表,本轮没有执行保存。端点、handler 和 payload 形状已从源码确认。安全相关架构备注: 前端在 payload 中直接提供
sTable。BusinessBaseServiceImpl.addUpdateDelBusinessData会读取该值并分发删除 / 更新 / 新增。类级sTableNameList主要作为缓存失效 gate,而不是“该表是否被此表单授权”的 gate。已确认的缓解包括租户作用域自动注入,以及模块可通过sSaveProNameBefore/sSaveProName做验证;但权限规则是按钮级,不是表级。净结论:通用保存端点信任前端的sTable值,值得维护 ticket。
5. 缓存失效
修改任何四张元数据表中的 gds* 行,都会让每个运行节点失效缓存副本。xly 通过 JMS 消息做到这一点:xlyErpJmsConsumer/.../ConsumerChangeGdsModuleThread.java 监听“module changed”事件并清相关 Redis key。见元数据变更后的缓存失效。
6. 浏览器确认
保存返回成功;前端要么就地 patch 该行,要么用同一 getBusinessDataByFormcustomId 端点重新拉取表格。追踪结束。
本切片引入的概念
- 数据驱动的基本论点:为什么 xly 把布局存为数据。
- 模块、表单、虚拟表:三个核心名词。
- 元数据驱动的请求生命周期:四表读取 + 五键结果 map。
-
主从单据模式:
gdsconfigformmaster/slave本身就是该模式实例。 -
无物理外键、语义外键的现实:
gdsconfigformmaster.sParentId = gdsmodule.sId是语义 FK。
本切片使用的参考
配置人员:
维护人员:
- 运行时:BusinessBaseController 及相关组件:执行四表读取的 controller 和 service 层。
- 元数据变更后的缓存失效:JMS 驱动 Redis flush。
-
多服务部署:
xlyEntryvsxlyApivsxlyInterface;本切片完全运行在xlyEntry上。
待验证项 {#open-verification-items}
- 真实捕获一次保存。 端点、handler 和 payload 形状已从源码确认,但实际保存请求体尚未捕获。需要打开模块、点击新增、填写、保存,并捕获 JSON body 和响应。
-
保存 / 删除发出的精确 SQL,从
syslog4j或 MyBatis debug log 捕获。 -
已关闭:运行时不会把前端提供的addUpdateDelBusinessData中的sTable校验。sTable与表单授权支撑表交叉检查。已作为维护关注点记录。
前两项属于切片 1 v2,需要对 dev DB 做实际写入;为避免修改共享状态而暂缓。