# 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/` | Activiti workflow surface (approve / reject / view) — only meaningful when workflow is deployed. | endpoints under `/checkflow/*` (the class file is `CheckFlowController.java` in camelCase but the `@RequestMapping` value is all-lowercase) | 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 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).