# 切片 3 — 由视图支撑的模块(只读报表) 切片 1 追踪的是 CRUD 模块:表单读写一张表。切片 3 前进一步:模块**从数据库视图读取**,连接多张基础表,并把连接结果呈现为可排序、可过滤的表格。没有保存,没有删除,只读。 这是 xly 中所有“列表视图”的基础:订单汇总、日报、库存快照、KPI 看板。框架通过与表支撑模块*相同*的元数据管线处理视图支撑模块,唯一差异是 `gdsconfigformmaster.sType = 'view'` 会抑制写操作。 ## 记录对象 | | | |---|---| | **模块** | `工单工序明细`(Work-Order Process Details) | | **URL 片段** | `/indexPage/commonList`(通用“只读列表”页面模板) | | **模块 `sId`** | `19211681019715708462888040` | | **支撑对象** | [`viw_mftworkorderprocess`](../auto-catalog/views/viw_mftworkorderprocess.md),数据库视图,**不是**基础表 | | **字段数** | 40 | | **写过程** | 无,设计上只读 | | **打印模板** | 无,纯表格 | 该视图把工单 master / slave 表族连接成扁平的每行表示。视图本身的存在就是 xly 报表渲染约定的一部分:与其在每个读取查询中重新写 join,xly 会编写数据库侧 `CREATE VIEW`,并让一行元数据指向它。 ## 为什么选这个模块 {#why-this-module} 它是 xly 最常见运行模式之一的最简单成员:**视图支撑的列表页**。这个 dev DB 中有 405 个视图支撑表单(全部表单的 20%),仅次于表支撑表单。理解本切片后,其他运维报表都是变体。 它也反驳切片 1 可能造成的两个误解: 1. 并非每个模块都从单表读取;视图支撑模块读取 join。 2. 并非每个 URL 都是唯一拼音路径。`/indexPage/commonList` 是**共享模板 URL**,许多不同模块都使用它。URL 不标识模块;API 调用上的 `sModelsId` 参数才标识模块。 ## 追踪 ### 1. 页面 shell 与切片 1 相同:访问 `/indexPage/commonList` 进入 SPA shell;SPA 再通过侧边栏 / 菜单状态解析要加载的模块。URL 是*显示状态*,不是路由驱动。 ### 2. 元数据获取 ```text GET /xlyEntry/business/getModelBysId/19211681019715708462888040?sModelsId=19211681019715708462888040 ``` 同一个 handler,同一个五键复合响应:`formData`、`gdsformconst`、`gdsjurisdiction`、`billnosetting`、`report`。这里的 `report` 为空,因为该模块没有附加打印模板。 与切片 1 的关键差异在 `formData`:form-master 行包含: ```text sType = 'view' sTbName = 'viw_mftworkorderprocess' sSqlStr = SELECT ... FROM viw_mftworkorderprocess ... 骨架 sWhere/sOrder = 默认谓词 ``` `sType = 'view'` 是框架判断只读的唯一信号。其余元数据结构与 CRUD 模块相同。 ### 3. 表格数据获取 ```text POST /xlyEntry/business/getBusinessDataByFormcustomId/{formId}?sModelsId={moduleId} ``` 仍然是切片 1 的同一端点。handler 不关心支撑对象是表还是视图,只把 `sTbName` 代入由 `sSqlStr` + `sWhere` + `sOrder` 组合出的参数化 SQL。视图负责 join;框架负责租户过滤和分页。 > **为什么用视图而不是运行时 JOIN?** xly 把 join 写进 SQL DDL(`CREATE VIEW`),使运行时 SQL 保持扁平的 `SELECT ... FROM WHERE ...`。这样可以把 table、view、proc 等不同“形状”来源插入同一表单机制。代价是命名视图增多;此 dev DB 中有 311 个。 ### 4. 视图本身 `viw_mftworkorderprocess` 把工单 master 与每工序 slave 明细连接起来。完整 `CREATE VIEW` 定义见[该视图的自动目录页](../auto-catalog/views/viw_mftworkorderprocess.md)。 命名约定:xly 视图前缀是 `viw_` 或 `Viw_`(schema 中大小写不一致)。join 视图常常很宽,40+ 列很常见。它们几乎总是从主基础表带出 `sBrandsId` / `sSubsidiaryId`,使框架的通用租户过滤仍然有效。 ### 5. 保存:不存在 对 `sType = 'view'`,框架不提供新增 / 更新 / 删除按钮。理论上,如果被要求,`addUpdateDelBusinessData` 的 MyBatis 侧会尝试写入视图;但框架不会为它渲染按钮。需要让某个视图可写时,维护人员通常应把表单指向基础表并使用自定义保存过程,或依赖 MySQL 自动可更新视图能力;本模块都没有这样做。 ### 6. 打印报表(存在时) 一些视图支撑模块确实有打印模板:Excel via jxls、PDF via iText。机制与表格分离: - `getModelBysId` 返回 `report` 数组,来自通过 `sFormId` 关联到表单的 `sysreport` 行。 - 前端“打印” / “导出”按钮调用 `xlyEntry/src/main/java/com/xly/web/report/` 下的 controller;`PrintReportController` 是活跃类。(同目录的 `PrintReportControllerOld.java` 文件仍存在,但类体已全部注释,是死代码。)controller 加载 jxls / iText 模板,使用同一视图查询的“取全部行”包装,并把二进制文件流回。 - 本模块没有模板,因此不覆盖打印路径。 > **后续工作。** 如果后续修订选择一个确实带打印模板的模块,就能端到端追踪 jxls 导出。当前受 dev DB 状态阻塞(没有任何视图支撑表单挂接 `sysreport` 行;见下面的待验证项)。 ## 本切片引入或强化的概念 - *视图支撑模块*:`gdsconfigformmaster.sType = 'view'` 让模块只读,其余元数据流相同。 - *共享模板 URL*:`/indexPage/commonList`、`/indexPage/commonBill`、`/indexPage/commonClassify`、`/indexPage/commonNewBill` 被数百个模块复用。URL 选择页面形状;模块身份来自 `sModelsId`。 - *报表模板*(仅预览):`sysreport` 通过 `sFormId` 关联,jxls / iText 模板由 `PrintReportController` 提供。 ## 本切片使用的参考 - [配置人员:如何定义虚拟表](../reference/builder/define-vtable.md):对视图支撑模块来说,表单的 `sType` 和 `sTbName` 就是声明。 - [维护人员:运行时](../reference/maintainer/runtime.md):与切片 1 使用同一条 `BusinessBaseController` 路径;唯一新增的是 `sType` 分支。 - 新页:*视图与报表*(维护人员下):记录 `viw_` / `Viw_` 命名约定、视图必须带出租户列才能保持租户安全的规则,以及打印模板流程。 ## 待验证项 > **Item 1 — 暂缓(需要填充过的 dev DB)。** 截至最近审计,dev DB 中没有任何 view-backed form 挂接 `sysreport` 行:`SELECT … FROM gdsconfigformmaster m INNER JOIN sysreport r ON r.sFormId = m.sId WHERE m.sType='view'` 返回 0 行。该项仍是实际验证缺口,需要一个 `sysreport` 至少包含一个 view-backed form 的租户部署。 1. **带打印模板的视图支撑模块**:选择一个并端到端追踪 jxls 导出。 2. **`sType = 'proc'` 变体**:209 个表单由存储过程支撑,应另开切片说明过程如何返回结果集和参数如何流动。 3. ~~**视图租户安全:审计哪些 `viw_*` 缺少 `sBrandsId`。**~~ **已关闭:305 个中有 19 个(约 6.2%)泄漏。** 对实时 DB 执行: ```sql SELECT v.TABLE_NAME FROM information_schema.views v WHERE v.TABLE_SCHEMA = DATABASE() AND v.TABLE_NAME LIKE 'viw_%' AND v.TABLE_NAME NOT IN ( SELECT TABLE_NAME FROM information_schema.columns WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'sBrandsId' ); ``` 此 dev DB 返回 19 行,包括 `viw_purorder_slave_detail`、`viw_qlyprocesstest`、`viw_accproductstoreinvoice*` 家族、`viw_hmwxjy*` 等。如果某个表单指向这些视图且 `gdsconfigformmaster.sWhere` 没有外层租户谓词,它们就是潜在跨租户泄漏点。下一步应审计这些具体视图对应的表单层谓词;裸视图审计已经变成一条一次性 SQL。