runtime.md 8.88 KB

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) boils down to:

public Map<String, Object> getModelBysId(Map<String, Object> 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<Map<String, Object>> formList = this.getModelConfigByModleId(map);
    // 2. gdsformconst — by sParentId only; sLanguage selects which label
    //    column to return; NOT tenant-scoped.
    List<Map<String, Object>> 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<Map<String, Object>> jList = businessGdsconfigformsService.getJurisdictionData(qMap);
    // 4. sysbillnosettings (per-tenant, per-form).
    Map<String, Object> billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param);
    // 5. sysreport (per-tenant, per-form).
    List<Map<String, Object>> 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:

{
  "addData":    [{"sTable": "<table>", "column": {"sId": "", ...}}],
  "updateData": [{"sTable": "<table>", "column": {"sId": "", ...}}],
  "delData":    [{"sTable": "<table>", "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:

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 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 for the full story (including the open question about cross-node coherence).