runtime.md 13.1 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/ 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. (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 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/getModelCenterBusinessModelCenterController.java:44-48BusinessModelCenterServiceImpl.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 sftlogininfojurisdictiongroupsisjurisdictionclassify 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. 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:

  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).