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) |
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 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://<host>: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.getKPIModelListinxlyBusinessService. -
POST /xlyEntry/modelCenter/getModelCenterCalculationrecomputes the counts and evicts the cache regiongetKpiModelByUser.
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⋈sisjurisdictionclassifyto 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 thegdsmoduletree — 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'siOrderwithin its parent flow.
Cache
@Cacheable(value="getKpiModelByUser", key=…) caches the per-user
result; getModelCenterCalculation and any
gdsmodule-changing path invalidate this region via
CleanRedisServiceImpl. 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) 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).