运行时:BusinessBaseController 及相关组件
xlyEntry/src/main/java/com/xly/web/businessweb/ 是元数据驱动运行时所在位置。本页是维护人员理解大部分通用表单运行时 controller 和 service 的地图。
承重 controller
| 类 | 包 | 角色 | 最常引用端点 |
|---|---|---|---|
BusinessBaseController |
web/businessweb/ |
元数据驱动模块的通用 CRUD。每个表单的默认 API 接口面。 |
/business/getModelBysId/{moduleId}、/business/getBusinessDataByFormcustomId/{formId}、/business/addUpdateDelBusinessData、/business/getSelectDataBysControlId/{controlId}
|
BusinessConfigformController |
web/businessweb/ |
已有表单的每用户 / 每组显示定制,不是基础表单定义 CRUD。 |
/configform/getConfigformData/{moduleId}、/configform/sHandleConfigform、/configform/sCopyConfigform
|
GdsmoduleController |
web/systemweb/ |
builder 侧使用的模块树和模块定义 CRUD。 |
/gdsmodule/getModuleTreePro、/gdsmodule/addGdsmodule、/gdsmodule/updateGdsmodule
|
GdsconfigformController |
web/systemweb/ |
form-master 和 form-slave 元数据 CRUD。 |
/gdsconfigform/* 下端点 |
GdsconfigtbController |
web/systemweb/ |
虚拟表 master / slave 元数据 CRUD。 |
/gdsconfigtb/* 下端点 |
BusinessTreeGridController |
web/businessweb/ |
树表格端点。当前分支实现了存储过程支撑路径,普通 getTreeGrid service 方法仍是空实现。 |
/treegrid/getTreeGridByPro/{formId} |
GenericProcedureCallController |
web/businessweb/ |
按名称 + 参数通用调用存储过程。 | /procedureCall/doGenericProcedureCall |
ConfigformPanelController |
web/businessweb/ |
gdsconfigformpanel 中的面板布局持久化。 |
/panel/get/{sFormId}、/panel/save/{sFormId}
|
CheckFlowController |
web/businessweb/ |
空壳。 类文件只有 22 行:一个 @RestController @RequestMapping(value="/checkflow"),没有 handler 方法。/checkflow/* 返回 404。真正的工作流审核 / 驳回 / 完成 URL 在 CurrencyFlowController(位于 xlyFlow,经 xlyEntry context-path 暴露)中;见 Activiti 集成。 |
无,空类 |
BusinessModelCenterController |
web/businessweb/ |
FROUNT 首页的“KPI 工作中心”看板:聚合标记为 gdsmodule.bUnTask=1 的模块上的未清任务。尽管 UI 像工作流,它不是 Activiti 驱动;它读取 gdsmodule 行,按 sUnType ∈ {Pending, PendingCheck, MyWarning} 分桶,并按用户缓存。见下面的 KPI 工作中心。 |
/modelCenter/getModelCenter、/modelCenter/getModelCenterCalculation
|
注意 controller 分布在两个包中:businessweb/ 承载运行时端点,systemweb/ 承载 builder 侧元数据 CRUD 端点。两者都编译进同一个 xlyEntry WAR。
KPI 工作中心(FROUNT 首页看板) {#kpi-工作中心front-端首页-dashboard}
用户进入 FROUNT(http://<host>:8598/indexPage)时,首页会显示一个标题为 KPI监控 的卡片。尽管名字叫 KPI,它不是分析意义上的 KPI 看板:没有目标、没有图表、没有指标度量。它是一个未清任务聚合器:把“需要你处理的事”按角色和业务流程汇总成统一入口。
处理链路:
-
POST /xlyEntry/modelCenter/getModelCenter→BusinessModelCenterController.java:44-48→BusinessModelCenterServiceImpl.getKPIModelList(很薄的一层 wrapper)→xlyBusinessService中的BusinessModelKpiServiceImpl.getKPIModelList。 -
POST /xlyEntry/modelCenter/getModelCenterCalculation会重新计算数量,并清掉getKpiModelByUser缓存区域。
getModelCenter 的 Javadoc 称它为“初次获取KPI工作中心”,这是框架内部对该接口面的命名。
数据来源:gdsmodule 未清任务标记
KPI 工作中心读取的是 gdsmodule,不是 Activiti 的 act_* 表。参与看板的每个模块都会在自己的 gdsmodule 行上设置四个列:
| 列 | 注释 | 含义 |
|---|---|---|
bUnTask |
是否增加到未清 |
1 表示纳入看板,0 表示忽略 |
sUnType |
未清类型 |
分桶:Pending、PendingCheck、MyWarning 之一 |
sChineseUnMemo、sEnglishUnMemo、sBig5UnMemo
|
未清描述 |
各语言显示标签 |
实时 dev DB 中,bUnTask = 1 的模块有 92 行,分布如下:
sUnType |
数量 | 前端分桶标签 |
|---|---|---|
Pending |
79 | 待处理事务 |
PendingCheck |
1 | 发起新事务 |
MyWarning |
3 | 我的预警报表 |
| NULL | 9 | 未标记,排除 |
对每个已标记模块,BusinessModelKpiServiceImpl 会执行该模块的每用户数量查询,并按两种维度聚合:
-
角色(UI 中“按角色”):JOIN
sftlogininfojurisdictiongroup⋈sisjurisdictionclassify,把当前用户映射到角色,再按角色拆分数量。 -
流程(UI 中“按流程”):
估价管理流程/订单生产流程这类标签来自gdsmodule树中的父模块。每个业务流程是一个父模块,下面包含 1 到 N 个有序子模块。UI 中 “01/04、02/04 …” 的步骤编号,就是子模块在父流程下的iOrder。
缓存
@Cacheable(value="getKpiModelByUser", key=...) 会缓存每用户结果;getModelCenterCalculation 以及任何修改 gdsmodule 的路径都会通过 CleanRedisServiceImpl 失效这个区域。由于 bUnTask / sUnType 可通过 BACK 的“系统模块配置”页面编辑,维护人员要给看板加一个新的“未清任务”,改的是元数据,不是 Java。
为什么不是 Activiti
Activiti 的职责是审批工作流:当某行需要经过 N 步 act_re_procdef 图签核时,act_ru_task / biz_todo_item 表会按处理人保存待办任务。它和 KPI 工作中心是不同接口面,虽然两者都会向用户展示“需要处理的事”。
当前 dev DB 中,biz_todo_item 和 biz_flow 都是空表(0 行),但 KPI 工作中心仍能显示非零数量。这是两套系统相互独立的实证。启用 Activiti 流程的部署会通过工作流 controller 和单独的流程面板暴露这些任务,而不是通过 KPI 工作中心。
五键读取 {#five-key-read}
对任何元数据驱动模块,请求生命周期(见概念 → 请求生命周期)可归结为:
public Map<String, Object> getModelBysId(Map<String, Object> map) {
// 1. formData:gdsconfigformmaster 按 sParentId=sModelsId 过滤,
// LEFT JOIN gdsconfigformpersonalize(每租户),再为每个 master 行
// 加载 gdsconfigformslave + gdsconfigformcustomslave 覆盖。
// gdsmodule 本身只通过 id 引用,不被 SELECT。
List<Map<String, Object>> formList = this.getModelConfigByModleId(map);
// 2. gdsformconst:仅按 sParentId;sLanguage 决定返回哪列标签;不按租户过滤。
List<Map<String, Object>> fList = businessGdsconfigformsService.getFormconstData(qMap);
// 3. sysjurisdiction:每用户授权,JOIN sftlogininfojurisdictiongroup
// + sisjurisdictionclassify;ADMIN 跳过。
// 虽然返回 map key 叫 `gdsjurisdiction`,实际源表是 sysjurisdiction。
List<Map<String, Object>> jList = businessGdsconfigformsService.getJurisdictionData(qMap);
// 4. sysbillnosettings(每租户、每表单)。
Map<String, Object> billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param);
// 5. sysreport(每租户、每表单)。
List<Map<String, Object>> reportList = printReportService.getReportData(qMap);
return composite(formList, fList, jList, billnosettingMap, reportList);
}
先读 BusinessBaseServiceImpl.java 中这个方法;运行时其余部分都是它的变体。
返回的五键复合体
| Key | 来源 | 前端用途 |
|---|---|---|
formData |
gdsconfigformmaster(按 sParentId = sModelsId 过滤)⋈ gdsconfigformpersonalize 覆盖;每个 master 行再加载 gdsconfigformslave + gdsconfigformcustomslave。gdsmodule 只作为 id 来源被引用,不参与 join。 |
表单布局 |
gdsformconst |
gdsformconst(仅按 sParentId 过滤;sLanguage 决定返回哪列标签;不按租户过滤) |
表单级常量、下拉标签 |
gdsjurisdiction |
sysjurisdiction(JOIN sftlogininfojurisdictiongroup + sisjurisdictionclassify 得到每用户 / 用户组授权);ADMIN 跳过。注意: map key 名称 gdsjurisdiction 有误导性,gdsjurisdiction 是配置侧动作目录表;这里读取的每用户授权实际来自 sysjurisdiction。 |
按钮 / 数据权限 |
billnosetting |
sysbillnosettings(每租户、每表单) |
单据编号规则 |
report |
sysreport(每租户、每表单) |
打印模板 |
保存端点
POST /business/addUpdateDelBusinessData 把新增 + 更新 + 删除打包为一个事务批次。前端为每行提供 sTable 和 column map:
{
"addData": [{"sTable": "<table>", "column": {"sId": "", "...": "..."}}],
"updateData": [{"sTable": "<table>", "column": {"sId": "", "...": "..."}}],
"delData": [{"sTable": "<table>", "column": {"sId": "", "...": "..."}}]
}
基础新增 / 更新路径总是通过 BusinessBaseServiceImpl.addBusinessData / updateBusinessData(xlyBusinessService/.../BusinessBaseServiceImpl.java:1014 和 :1250),再委托 businessBaseDao.add(map) / businessBaseDao.update(map),对 sTable 命名的表执行操作。
gdsmodule.sSaveProName(及其兄弟列 sSaveProNameBefore)不是替换基础路径的二选一分支;它命名的是叠加在基础路径之上的额外存储过程钩子:保存后 / 保存前由 BusinessBaseServiceImpl.java:1824 的 checkUpdate(...,"sSaveProName") 和 CheckSaveServiceImpl.java 分发。AddDelUpdCommonServiceImpl(@Service("addDelUpdCommonService"))是另一套可复用 insertByMap / updateByMap / delByMap / addBatch helper,被工单计划、OEE、多报价、订单采购等领域 service 使用;它不是 addUpdateDelBusinessData 的默认新增 / 更新路径。
多租户边界
每个 controller 方法的第一条非平凡语句都是:
RequestAddParamUtil.me().addParams(params, userInfo);
这是把用户 session 身份(sBrandsId、sSubsidiaryId、sBrId、sSuId、sLoginId、sLanguage、sTeamId、sMachineId 及其他 key)注入下游 params map 的单点。任何引用 #{sBrandsId} / #{sSubsidiaryId} 的 MyBatis 查询或存储过程调用都会从这里自动获得作用域。
新的 controller 方法如果不调用 RequestAddParamUtil,就是多租户 bug。
需要审计的安全关注点
-
addUpdateDelBusinessData中的sTable校验:已确认缺失。 前端直接命名目标表,而运行时不会交叉检查传入表是否属于该表单授权的支撑表。BusinessBaseServiceImpl.sTableNameList(162-169 行)是多租户作用域绕过列表(四张全局框架元数据表,写入时剥掉sBrandsId/sSubsidiaryId;见 165 行//不需要公司子公司的表注释),不是支撑表白名单。整个流程里唯一的模块 / 表交叉检查是 1768 行的硬编码特例(mftproductionplanslave)。确实存在一些缓解措施(RequestAddParamUtil的租户作用域、可选的保存前 / 后存储过程校验),但它们都不是支撑表白名单。完整追踪见切片 1 follow-up。 -
ADMIN 绕过权限。
BusinessBaseServiceImpl对UserType.ADMIN完全跳过gdsjurisdiction加载。ADMIN 账号治理必须来自应用外部。
“通用 CRUD”在实践中意味着什么
“一个 controller 写任意表中的任意行”是 xly 数据驱动设计的核心动作,也会把风险集中到少数路径上:
-
BusinessBaseServiceImpl已经约 3,900 行,其中缠在一起的逻辑包括每租户作用域绕过列表、特定表硬编码(第 1768 行的mftproductionplanslave)、保存前 / 保存后钩子分发、以及由sTable驱动的写入路由。每个 bug fix 都必须穿过这个大类。 -
它是整个业务运行时的单点故障。
addUpdateDelBusinessData中的回归会同时破坏所有租户的所有表单保存。模块专用 controller 可以把爆炸半径限制在一个模块内;通用 controller 做不到。 -
Map<String, Object>没有类型系统。 前端传来一袋 key/value。运行时相信 key 与列名匹配、value 能转换成列类型。不匹配时通常在 DAO 层抛BadSqlGrammarException,离错误来源已经很远。这里没有 schema-aware 的请求校验。 -
可发现性差。 “哪些端点会写
mftproductionplanslave?”不能靠 IDE find-usages 回答。真实答案是:任何调用BusinessBaseServiceImpl.addBusinessData且把sTable设为mftproductionplanslave的 controller,也就是几乎所有通用保存入口。
通用模式让数据驱动论点成立;它也是新增模块几乎免费的原因,同时也是修改运行时几乎从不免费的原因。
缓存失效
BACK 保存元数据变更时,保存 service 会同步调用 BusinessCleanRedisData.delCleanRedisData*,进而触发 CleanRedisServiceImpl 上相关缓存区域的 @CacheEvict。另有一个名字相近的 JMS 路径(ConsumerChangeGdsModuleThread),但它通过存储过程做基础数据合并,不做缓存失效。完整说明见元数据变更后的缓存失效,包括跨节点一致性问题。