# The runtime: BusinessBaseController & friends `xlyEntry/src/main/java/com/xly/web/businessweb/` is where the metadata-driven runtime lives. This page is the maintainer's map of the controllers and services that carry most of the generic form runtime. ## The load-bearing controllers | Class | Package | Role | Most-cited endpoints | |---|---|---|---| | `BusinessBaseController` | `web/businessweb/` | Universal CRUD for metadata-driven modules. The default API surface for every form. | `/business/getModelBysId/{moduleId}`, `/business/getBusinessDataByFormcustomId/{formId}`, `/business/addUpdateDelBusinessData`, `/business/getSelectDataBysControlId/{controlId}` | | `BusinessConfigformController` | `web/businessweb/` | Per-user / per-group display customization for an existing form, not the base form-definition CRUD. | `/configform/getConfigformData/{moduleId}`, `/configform/sHandleConfigform`, `/configform/sCopyConfigform` | | `GdsmoduleController` | `web/systemweb/` | Module-tree and module-definition CRUD used by the builder side. | `/gdsmodule/getModuleTreePro`, `/gdsmodule/addGdsmodule`, `/gdsmodule/updateGdsmodule` | | `GdsconfigformController` | `web/systemweb/` | Form-master and form-slave metadata CRUD. | endpoints under `/gdsconfigform/*` | | `GdsconfigtbController` | `web/systemweb/` | Virtual-table master/slave metadata CRUD. | endpoints under `/gdsconfigtb/*` | | `BusinessTreeGridController` | `web/businessweb/` | Tree-grid endpoints. In this branch, the proc-backed path is implemented; the plain `getTreeGrid` service method is still a stub. | `/treegrid/getTreeGridByPro/{formId}` | | `GenericProcedureCallController` | `web/businessweb/` | Generic stored-procedure invocation by name + parameters. | `/procedureCall/doGenericProcedureCall` | | `ConfigformPanelController` | `web/businessweb/` | Panel-layout persistence in `gdsconfigformpanel`. | `/panel/get/{sFormId}`, `/panel/save/{sFormId}` | | `CheckFlowController` | `web/businessweb/` | **Empty shell.** The class file is 22 lines: just a `@RestController @RequestMapping(value="/checkflow")` with no handler methods. `/checkflow/*` returns 404. The actual workflow approve/reject/complete URLs live in `CurrencyFlowController` (in `xlyFlow`, served via xlyEntry's context-path) — see [Activiti integration](activiti.md#urls-the-modeler-exposes-xlyflow-controllers-on-xlyentrys-port). | (none — empty class) | | `BusinessModelCenterController` | `web/businessweb/` | The "KPI Work Center" home dashboard on FROUNT — open-task aggregation across modules tagged with `gdsmodule.bUnTask=1`. **Not Activiti-driven** despite the workflow-flavoured UI; reads from `gdsmodule` rows partitioned by `sUnType` ∈ {`Pending`, `PendingCheck`, `MyWarning`}. Cached per user. See [The KPI Work Center](#the-kpi-work-center-front-end-home-dashboard) below. | `/modelCenter/getModelCenter`, `/modelCenter/getModelCenterCalculation` | Note that the controllers split across **two packages**: `businessweb/` hosts the runtime-facing endpoints, while `systemweb/` hosts the metadata-CRUD endpoints used by the builder side. Both compile into the same `xlyEntry` WAR. ## The KPI Work Center (front-end home dashboard) When a user lands on FROUNT (`http://:8598/indexPage`), the home page shows a card titled **`KPI监控`** ("KPI Monitor"). Despite the name, this is *not* a KPI dashboard in the analytics sense — there are no targets, no charts, no measurements. It's an **open-task aggregator**: a unified inbox of "things you owe action on", grouped by role and by business flow. The handler: - `POST /xlyEntry/modelCenter/getModelCenter` → `BusinessModelCenterController.java:44-48` → `BusinessModelCenterServiceImpl.getKPIModelList` (a thin wrapper) → `BusinessModelKpiServiceImpl.getKPIModelList` in `xlyBusinessService`. - `POST /xlyEntry/modelCenter/getModelCenterCalculation` recomputes the counts and evicts the cache region `getKpiModelByUser`. The Javadoc on `getModelCenter` calls it **"初次获取KPI工作中心"** ("initial fetch of KPI Work Center") — that's the framework's internal name for this surface. ### Data source — `gdsmodule` open-task tagging The KPI Work Center reads from `gdsmodule`, not from Activiti's `act_*` tables. Every module that participates in the dashboard sets four columns on its `gdsmodule` row: | Column | Comment | Meaning | |---|---|---| | `bUnTask` | `是否增加到未清` ("add to open list?") | `1` = include in the dashboard, `0` = ignore | | `sUnType` | `未清类型` ("open type") | Bucket: one of `Pending`, `PendingCheck`, `MyWarning` | | `sChineseUnMemo`, `sEnglishUnMemo`, `sBig5UnMemo` | `未清描述` ("open description") | Display label per language | In the live dev DB, `bUnTask = 1` is set on **92** module rows, split: | `sUnType` | Count | Frontend bucket label | |---|---:|---| | `Pending` | 79 | 待处理事务 (pending tasks) | | `PendingCheck` | 1 | 发起新事务 (initiate new) | | `MyWarning` | 3 | 我的预警报表 (my warnings) | | (NULL) | 9 | (untagged — excluded) | For each tagged module, `BusinessModelKpiServiceImpl` runs the module's per-user count query and aggregates results by: - **Role** (按角色 in the UI): joins `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify` to map the calling user to roles, then partitions counts by role. - **Flow** (按流程 in the UI): the labels like `估价管理流程` / `订单生产流程` come from the module's parent in the `gdsmodule` tree — each business flow is a parent module containing 1-N ordered child modules. The "01/04, 02/04…" step numbering in the UI is the child module's `iOrder` within its parent flow. ### Cache `@Cacheable(value="getKpiModelByUser", key=…)` caches the per-user result; `getModelCenterCalculation` and any `gdsmodule`-changing path invalidate this region via [`CleanRedisServiceImpl`](cache-invalidation.md). With `bUnTask` / `sUnType` editable through BACK's 系统模块配置 screen, a maintainer adding a new "open task" to the dashboard does so by editing metadata, not Java. ### Why not Activiti? Activiti's job is **approval workflow** — when a row needs sign-off through an N-step `act_re_procdef` graph, the `act_ru_task` / `biz_todo_item` tables hold pending tasks per assignee. That is a *different* surface from the KPI Work Center, even though both present "things to do" to the user. In this dev DB, `biz_todo_item` and `biz_flow` are both empty (0 rows) — yet the KPI Work Center still shows non-zero counts. That's the empirical proof that the two systems are independent. A deployment with active Activiti flows would surface those tasks through `CheckFlowController` (`/checkflow/*`) and a separate flow panel — not through the KPI Work Center. ## The five-key read For any metadata-driven module, the request lifecycle (see [Concepts → request lifecycle](../../concepts/request-lifecycle.md)) boils down to: ```java public Map getModelBysId(Map map) { // 1. formData — gdsconfigformmaster filtered by sParentId=sModelsId, // LEFT JOIN gdsconfigformpersonalize (per-tenant), then for each // master row load its gdsconfigformslave + gdsconfigformcustomslave // overlays. gdsmodule itself is referenced only by id, not SELECT-ed. List> formList = this.getModelConfigByModleId(map); // 2. gdsformconst — by sParentId only; sLanguage selects which label // column to return; NOT tenant-scoped. List> fList = businessGdsconfigformsService.getFormconstData(qMap); // 3. sysjurisdiction (per-user grants joined to sftlogininfojurisdictiongroup // + sisjurisdictionclassify); skipped for ADMIN. // Returned under map key `gdsjurisdiction` despite the source table // being sysjurisdiction. List> jList = businessGdsconfigformsService.getJurisdictionData(qMap); // 4. sysbillnosettings (per-tenant, per-form). Map billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param); // 5. sysreport (per-tenant, per-form). List> reportList = printReportService.getReportData(qMap); return composite(formList, fList, jList, billnosettingMap, reportList); } ``` — `BusinessBaseServiceImpl.java`. Read this method first; the rest of the runtime is variations on it. ## The five-key composite returned | Key | Source | Frontend uses for | |---|---|---| | `formData` | `gdsconfigformmaster` (filtered by `sParentId = sModelsId`) ⋈ `gdsconfigformpersonalize` overlay; per master row, `gdsconfigformslave` + `gdsconfigformcustomslave`. `gdsmodule` is referenced only as the id source, not joined. | Form layout | | `gdsformconst` | `gdsformconst` (filtered by `sParentId` only; `sLanguage` selects which label column to return; NOT tenant-scoped) | Form-level constants, dropdown labels | | `gdsjurisdiction` | `sysjurisdiction` (joined to `sftlogininfojurisdictiongroup` + `sisjurisdictionclassify` for per-user/group grants); skipped for ADMIN. **Note:** the map-key name `gdsjurisdiction` is misleading — `gdsjurisdiction` is the builder-side action *catalogue* table; the per-user *grant* read here actually queries `sysjurisdiction`. | Per-button / per-data permissions | | `billnosetting` | `sysbillnosettings` (per-tenant, per-form) | Document numbering rules | | `report` | `sysreport` (per-tenant, per-form) | Print templates | ## The save endpoint `POST /business/addUpdateDelBusinessData` bundles add+update+delete in one transactional batch. The frontend supplies `sTable` and a `column` map per row, in this shape: ```json { "addData": [{"sTable": "", "column": {"sId": "", ...}}], "updateData": [{"sTable": "
", "column": {"sId": "", ...}}], "delData": [{"sTable": "
", "column": {"sId": "", ...}}] } ``` The base add/update path always runs through `BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData` (`xlyBusinessService/.../BusinessBaseServiceImpl.java:1014` and `:1250`), which delegate to `businessBaseDao.add(map)` / `businessBaseDao.update(map)` against the table named by `sTable`. `gdsmodule.sSaveProName` (and its sibling `sSaveProNameBefore`) is **not** an either/or branch that swaps the path; it names an extra stored proc that runs as a post-save (or pre-save) hook on top of the base path, dispatched by `checkUpdate(...,"sSaveProName")` at `BusinessBaseServiceImpl.java:1824` and `CheckSaveServiceImpl.java`. `AddDelUpdCommonServiceImpl` (`@Service("addDelUpdCommonService")`) is a separate utility of reusable `insertByMap`/`updateByMap`/ `delByMap`/`addBatch` helpers used by domain services (work-order-plan, oee, many-quo, order-procurement, etc.); it is **not** the runtime's default add/update path for `addUpdateDelBusinessData`. ## Multi-tenant boundary Every controller method's first non-trivial line is: ```java RequestAddParamUtil.me().addParams(params, userInfo); ``` — `xlyPersist/.../RequestAddParamUtil.java`, 56 lines (xlyApi has its own near-identical 57-line copy at `xlyApi/.../api/util/RequestAddParamUtil.java`). This is the single point where the user's session identity (`sBrandsId`, `sSubsidiaryId`, `sBrId`, `sSuId`, `sLoginId`, `sLanguage`, `sTeamId`, `sMachineId`, plus eight more — sixteen keys total) is injected into the downstream `params` map. Every MyBatis query and stored-procedure call that references `#{sBrandsId}`/`#{sSubsidiaryId}` is automatically scoped from there. A new controller method that doesn't call `RequestAddParamUtil` is a **multi-tenant bug**. ## Security concerns to audit Two flagged in slices that belong here permanently: 1. **`sTable` validation in `addUpdateDelBusinessData` — CONFIRMED MISSING.** The frontend names the target table directly and the runtime does **not** cross-check that the supplied table is one of the form's authorised backing tables. `BusinessBaseServiceImpl.sTableNameList` (lines 162-169) is a multi-tenant scope-bypass list (the four framework-metadata tables that are global, so `sBrandsId` / `sSubsidiaryId` are stripped from writes against them — see the `//不需要公司子公司的表` comment at line 165), not a backing-table whitelist. The hardcoded special case at line 1768 (`mftproductionplanslave`) is the only module/table cross-check in the whole flow. Mitigations exist (tenant scoping via `RequestAddParamUtil`, optional pre/post-save proc validation), but none of them is a backing-table whitelist. See [Slice 1 follow-up](../../slices/01-hello-world.md#open-verification-items) for the full trace. 2. **ADMIN bypasses permissions.** `BusinessBaseServiceImpl` skips the `gdsjurisdiction` load entirely for `UserType.ADMIN`. ADMIN account governance must come from outside the app. ## Cache invalidation When BACK saves a metadata change, the save service synchronously calls `BusinessCleanRedisData.delCleanRedisData*`, which fires `@CacheEvict` on the relevant cache regions in `CleanRedisServiceImpl`. A separate JMS path (`ConsumerChangeGdsModuleThread`) exists with a similar name but does base-data merging via stored proc, not cache invalidation. See [cache invalidation on metadata change](cache-invalidation.md) for the full story (including the open question about cross-node coherence).