You need to sign in before continuing.
runtime.md 14.7 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 25 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.

What "universal CRUD" means in practice

The "one controller writes any row in any table" pattern is the core data-driven move. It also concentrates risk:

  • BusinessBaseServiceImpl is ~3,900 lines of tightly intertwined logic: per-tenant scope-bypass list, special-case table hardcodes (mftproductionplanslave at line 1768), pre/post-save hook dispatch, sTable-driven write routing. Every bug fix has to navigate the whole class.
  • The class is the single point of failure for the entire business runtime. A regression in addUpdateDelBusinessData breaks save for every form in every tenant simultaneously. Module-specific controllers would localise the blast radius; the universal one cannot.
  • No type system on Map<String, Object>. The frontend ships a bag of (key, value) pairs. The runtime trusts the keys match column names and the values cast to the column types. Mismatches surface as BadSqlGrammarException at the DAO layer — far from where the wrong value originated. There is no schema-aware request validation.
  • Discoverability is poor. "What endpoints write to mftproductionplanslave?" can't be answered by IDE find-usages — the answer is "any controller that calls BusinessBaseServiceImpl.addBusinessData with sTable set to mftproductionplanslave", which is everything.

The universal pattern is what makes the data-driven thesis work. It is also the reason adding a new module is essentially free and the reason that touching the runtime is essentially never free.

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 (cross-node coherence is empirically Redis-backed, no longer an open question).