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:
-
sTablevalidation inaddUpdateDelBusinessData— 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, sosBrandsId/sSubsidiaryIdare 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 viaRequestAddParamUtil, optional pre/post-save proc validation), but none of them is a backing-table whitelist. See Slice 1 follow-up for the full trace. -
ADMIN bypasses permissions.
BusinessBaseServiceImplskips thegdsjurisdictionload entirely forUserType.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).