# Slice 1 — a CRUD module (Hello World) The simplest end-to-end action in xly: open a system module, look at the data, edit a row, save. We trace it from the URL bar all the way to the rows the runtime renders in the grid, hitting every layer in between. This chapter is built from observed network traffic + matching source. Where something is still a hypothesis (e.g., the save path, which we haven't exercised yet), it's marked as such. ## What we're documenting | | | |---|---| | **Module** | `系统常量配置` (System Constants Config) | | **URL fragment** | `/xtclpz` | | **Module `sId`** | `13` | | **Backing table** | [`gdsformconst`](../auto-catalog/tables/gdsformconst.md) | | **Form `sId`** | `19211681019715574676360040` | | **Field count** | 10 | | **Save / delete proc** | *(none — uses framework default)* | | **Approval workflow** | none (`bCheck = 0`) | This module is itself part of the framework: it's the BACK page where a PM edits system constants. Documenting it is meta-perfect — we'll watch the framework configure the framework. ## Why this module Two reasons. First, `sId = 13` makes it a foundational, top-of-tree module — about as old as the system gets. Second, it has **no custom procs**: the framework uses its default CRUD path for save/delete, which is the canonical code-path we want a reader to learn first. Custom procs are Slice 2's job. ## The four metadata layers The runtime reads four metadata tables to render this page: | Layer | Table | Meaning | |---|---|---| | 1. Module catalog | [`gdsmodule`](../auto-catalog/tables/gdsmodule.md) | Tree of modules. Row `sId='13'` is our subject. | | 2. URL whitelist (client-side) | [`gdsroute`](../auto-catalog/tables/gdsroute.md) | URL `/xtclpz` is registered here. The SPA consults this list to decide which sidebar/deep-link entries to render — the server does *not* 404 unregistered paths (it always serves the SPA shell). | | 3. Form layout | [`gdsconfigformmaster`](../auto-catalog/tables/gdsconfigformmaster.md) | One row, `sParentId='13'`, declares the form: backing table = `gdsformconst`, type = `table`, default SQL/where/order, paging, jurisdiction. | | 4. Field layout | [`gdsconfigformslave`](../auto-catalog/tables/gdsconfigformslave.md) | One row per field, `sParentId` = the form's `sId`. Each row carries control type, i18n labels, validation, default, dropdown SQL, etc. | That four-table read is **the lifecycle of every metadata-driven page in xly**. Once you've internalised it, the rest of the framework is variations on a theme. ## The trace, with observed evidence ### 0. Service shape The BACK URL `http://118.178.19.35:8597` is served by the **`xlyEntry`** WAR (observed: every `/business/*` call has context-path `/xlyEntry/`). Earlier hypotheses guessed `xlyApi` — that turns out to be wrong; `xlyApi` is a sibling WAR with its own `application.yml` and context-path `/xlyApi`, and serves a different role. *Maintainer chapter on [deployment](../reference/maintainer/deployment.md) will cover the split.* ### 1. Browser → server (page shell) ``` GET /xtclpz ``` The URL `/xtclpz` is one entry in `gdsroute`, but at runtime it functions as **display state**, not a route driver. Navigating directly to `http://118.178.19.35:8597/xtclpz` after login does *not* deeplink to the "System Constants Config" module — it loads the same SPA shell that any other URL would load, and shows the default landing module (系统模块配置). The sidebar then has to be clicked for the SPA to switch modules. Worth correcting an earlier hypothesis here: `gdsroute` is **not enforced server-side**. A direct probe of an unregistered path (`GET /xtclpz_NOT_A_REAL_ROUTE`) returns the same 200 + SPA shell as the registered `/xtclpz`. The whitelist is a client-side construct: the SPA reads `gdsroute` to know which sidebar links / deep-links it is willing to mount, but the static-file server serves the SPA shell for any path. The *actual* dispatch happens entirely client-side once the SPA is loaded. ### 2. User clicks the sidebar item; SPA fetches metadata When the user clicks `系统常量配置` in the sidebar: ``` GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK ``` - Path variable and query param are both `13` — the **module's `sId`** in `gdsmodule`. The handler is `BusinessBaseController.getModelBysId()` at `xlyEntry/src/main/java/com/xly/web/businessweb/BusinessBaseController.java:63-69`. > **Naming caution:** the parameter is called `sModelsId` and the helper is > called `getModelConfigByModleId` ("Modle", a typo for "Model"). Both > *actually* take a module `sId`. The original Javadoc on line 60 says > "传入窗体sId" (passes form sId), which is incorrect. Throughout the > codebase, `Models / Modle / Model` are used interchangeably to mean > "module"; treat them as one concept while reading. The implementation (`BusinessBaseServiceImpl.getModelBysId()` at `xlyBusinessService/src/main/java/com/xly/service/impl/BusinessBaseServiceImpl.java:181-211`) returns a single composite map with five keys: | Key | Source | What it contains | |---|---|---| | `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave` (+ personalize) | The form layout — the spine. | | `gdsformconst` | `gdsformconst` rows scoped by `sBrandsId`/`sSubsidiaryId`/language | Form-level constants — labels, defaults, dropdown text. | | `gdsjurisdiction` | `gdsjurisdiction` rows for the user's role | Per-button and per-data permissions. **Skipped for `ADMIN` users** (line 196-198) — admins see everything. | | `billnosetting` | `sysbillnosettings` row for this module | Document-numbering rule (irrelevant for `gdsformconst` but always loaded). | | `report` | print templates linked to this form | Printable-report definitions, if any. | **Multi-tenancy** is enforced inside this read: every sub-query is scoped by `sBrandsId` (manufacturer) and `sSubsidiaryId` (subsidiary), pulled from the authenticated user's session via `RequestAddParamUtil.me().addParams()`. Tenants do not see each other's metadata. ### 3. SPA → server (initial data load) With the metadata in hand, the SPA renders the empty grid and asks for rows: ``` POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200 OK ``` - Path variable: the **form's `sId`** (`19211681019715574676360040`). - Query param: the **module's `sId`** (`13`). - Handler: `BusinessBaseController.getBusinessDataByFormcustomId()` at `xlyEntry/.../BusinessBaseController.java:105-123`. The handler branches on whether `sGroupList` is in the request body (line 113): no group-by → `getBusinessDataByFormcustomId`; group-by present → `getBusinessDataByGroup`. Slice 1's module (gdsformconst) hits the simple path. The SQL is composed from `gdsconfigformmaster.sSqlStr` + `sWhere` + `sOrder`, parameterised with the user's tenant and language, then run through MyBatis. The same path also handles edit-row "fetch one" by detail-id when `sId` is in the body. ### 4. User edits a row, clicks save The "save" button (and the "delete" button, and the "add" button) all funnel through one universal endpoint: ``` POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} ``` - Handler: `BusinessBaseController.addUpdateDelBusinessData()` at `xlyEntry/.../BusinessBaseController.java:165-177`. It delegates to `BusinessBaseService.addUpdateDelBusinessData(params)`. - **Request body shape (per the Javadoc on lines 161-163):** ```json { "addData": [{"sTable": "