# 前端接入与联调指南

## 文档定位

本文档面向：

- 前端开发者
- 负责前后端联调的后端开发者
- 编写业务需求说明的人

重点回答三类问题：

- 前端在 `ineed-core` 里真正应该依赖哪些稳定契约
- `webmvc` 与 `webflux` 两条线对前端来说哪些语义必须保持一致
- 新业务模块在需求阶段，哪些前端信息必须先写清楚，避免后续返工

如果你要先理解当前框架模块边界，先读 [module-capability-map.md](../core/module-capability-map.md)。
如果你要新建业务模块，继续读 [business-module-layout-guide.md](business-module-layout-guide.md)。
如果你要写业务需求说明，继续读 [module-requirements-template.md](module-requirements-template.md)。
如果你要用代码生成器或自动化工具同时生成后端和前端，继续读 [development-contract.md](development-contract.md) 和 [code-style-guide.md](code-style-guide.md)。

## 前端只依赖这些稳定契约

前端不应直接围绕 `entity`、`repository`、数据栈实现做联调。

前端真正应该依赖的是：

- `AO`
  作为创建、编辑、导入等提交对象
- `Query`
  作为业务筛选条件对象
- `CommonParameters`
  作为分页、搜索、排序等通用查询参数
- `VO`
  作为列表、详情、树、字典等展示对象
- `ResponseResults<T>`
  作为统一响应外壳
- `QueryResults<T>`
  作为分页结果对象

这套契约主要位于：

- `ineed-core-base`
- `ineed-core-webmvc`
- `ineed-core-webflux`

新业务模块面向前端的对象应位于后端 `core` 子模块的 `contract/ao`、`contract/query`、`contract/vo`、`contract/dto` 包下。需求、接口说明和前端联调文档统一称为 `AO / Query / VO / DTO` 契约对象，不再把这些对象写成 `pojo`。

当前前端实现主要通过以下入口消费这些契约：

- `ineed-frontend/src/shared/api/request.js`
  统一注入 token，处理 `ResponseResults`、HTTP 错误和登录过期。
- `ineed-frontend/src/shared/api/serviceFactory.js`
  通过 `createConfigCrudService(url, options)` 组合标准 CRUD / query / data status / recycle API。
- `ineed-frontend/src/shared/store/storeFactory.js`
  通过 `createConfigCrudStore(storeId, service, options)` 组合列表、编辑、详情、回收站和状态动作。
- `ineed-frontend/src/shared/composables/support/dataStatusActionConfigs.js`
  定义状态按钮、权限后缀、显示条件和确认文案。
- `ineed-frontend/src/app/router/index.js`
  使用路由 `meta.resourceKey` 或 `meta.resourceKeyBuilder` 做页面级权限判断。

新模块前端代码应优先复用这些工厂和约定，只有领域特有接口才在模块自己的 `service/*.js` 里额外补方法。

## 登录 Token 与在线会话

当前安全框架把 JWT 当作前后端传输凭证，服务端 Session 才是登录状态的权威来源。

前端接入时遵守：

- 登录接口返回的 token 只需要按统一 request 层注入到后续请求中，前端不要解析 token 内容来判断登录身份、租户或权限。
- JWT 只承载最小会话标识，例如 `sid`；不要依赖 JWT 里的过期时间、用户字段或权限字段驱动页面状态。
- token 是否仍有效由后端通过 Redis Session 校验兜底；前端收到统一未登录 / 会话失效错误后，按错误码进入重新登录或刷新当前登录态流程。
- 在线会话管理接口使用 `/security/online-session` 语义，资源标识使用 `security.online-session`；不要继续生成或消费旧的用户维度在线管理路径与资源标识。
- 在线会话列表面向 `OnlineSessionVO`，展示字段来自 VO；强制下线等动作以 `sid` 为目标会话标识，不要用用户名、用户 ID 或 token 全串作为操作主键。
- Redis 在线会话索引语义是 `ONLINE_SESSION`，前端、文档、测试样例不要继续引用旧的 `ONLINE` 命名。

## AO / Query / VO 的前端语义

### AO

`AO` 是前端提交给后端的应用对象，主要用于：

- 新增表单
- 编辑表单
- 批量导入
- 自定义业务动作入参

基础约束来自 `AbstractAO`：

- 统一带 `id`
- 保存时 `id` 必须为空
- 更新时 `id` 不能为空

这意味着前端在新增和编辑时，不能把同一份未区分状态的表单对象直接复用成两套语义。

### Query

`Query` 是业务筛选条件对象，只承载业务字段，不承担分页与排序。

例如：

- 名称
- 编码
- 类型
- 状态
- 时间区间

分页、搜索关键字、排序这些通用参数不应塞进业务 `Query` 里，而是继续通过 `CommonParameters` 传递。

### VO

`VO` 是前端看到的展示对象，主要用于：

- 列表行
- 详情页
- 树节点
- 字典项
- 导出对象

基础约束来自 `AbstractVO`：

- 统一带 `id`

同一个业务对象在 `webmvc` 与 `webflux` 两条线中的 `VO` 字段语义应保持一致，不允许一边有字段、另一边缺字段。

## 通用查询参数怎么传

标准查询入口会把前端传入的 `CommonParameters + Query` 组装成 `QueryParameters`。

前端真正需要关注的是 `CommonParameters`：

- `dc`
  防缓存动态参数
- `searchKey`
  通用关键字搜索
- `current`
  当前页码
- `pageSize`
  每页条数
- `sortJson`
  排序 JSON 字符串

`sortJson` 对应的排序对象字段为：

- `property`
  排序属性。这里必须传后端实体属性名，例如 `createdTimestamp`，不要传 SQL 片段、列别名表达式或 `id desc` 这类拼接内容。
- `direction`
  排序方向，支持 `asc`、`ASC`、`ascend`、`desc`、`DESC`、`descend`

当前 JDBC / R2DBC / MyBatis-Flex 标准查询实现会按实体字段做排序白名单校验。非法或未知的 `property` 不应作为有效排序条件进入 SQL。

标准查询接口对前端更接近下面这种调用方式：

```text
GET {controllerRoot}
  ?current=1
  &pageSize=20
  &searchKey=zhang
  &sortJson=[{"property":"createdTimestamp","direction":"descend"}]
  &name=张三
  &enabled=true
```

其中：

- `current / pageSize / searchKey / sortJson` 来自 `CommonParameters`
- `name / enabled` 这类业务字段来自具体 `Query`

除非某个自定义接口明确要求，否则前端不需要自己手工构造 `QueryParameters` JSON。

## 当前前端服务与 Store 落地方式

标准模块的前端服务通常长这样：

```javascript
import request from '@/shared/api/request';
import { API_PREFIX } from '@/shared/config/constants';
import { createConfigCrudService } from '@/shared/api/serviceFactory';

const URL = 'business/customer';

export const customerDataStatusCapabilities = {
    enable: true,
    lock: false,
    default: false,
    primary: false,
    publish: false,
    reserve: false
};

export default {
    ...createConfigCrudService(URL, { dataStatusCapabilities: customerDataStatusCapabilities }),
    findProfile(id) {
        return request.get(`${API_PREFIX}/${URL}/${id}/profile`);
    }
};
```

约定如下：

- `URL` 不带 `/api` 前缀，由 `createConfigCrudService` 内部拼接 `API_PREFIX`。
- 只有自定义接口需要直接引用 `API_PREFIX`。
- `dataStatusCapabilities` 必须来自需求或后端实体的 `DataStatusFeature` 开关，不能默认全开。
- Store 优先使用 `createConfigCrudStore(storeId, service, { tree, dataStatusCapabilities })`。
- 如果模块是树结构，前端 service/store 要同时打开 `tree: true` 并确认后端提供 `/tree` 查询能力。

字段和页面代码应围绕 `AO / Query / VO` 分开设计：

| 前端文件类型 | 对应后端契约 | 说明 |
|--------------|--------------|------|
| `service/*.js` | Controller 路径与标准后缀 | 只封装 API，不承载页面状态 |
| `store/*.js` | `ResponseResults` / `QueryResults` | 只处理加载、列表、编辑、详情、动作状态 |
| `routes.js` | `resourceKey` / 页面入口 | 路由 `meta.resourceKey` 必须能映射到后端权限点 |
| `pages/*.vue` | VO / Query / action 能力 | 组合表格、表单、状态动作和自定义页面行为 |
| schema / columns 文件 | AO / Query / VO 字段 | 维护表单字段、查询字段、列表列、详情字段、校验规则 |

如果一个模块没有 schema/columns 独立文件而是内联在页面中，新增字段时也必须按上表逐项核对，不要只改表单或只改列表。

## 统一返回外壳

无论是 `webmvc` 还是 `webflux`，对前端可见的统一响应模型都应保持同一语义：

- `traceId`
  请求追踪号
- `success`
  是否成功
- `errorCode`
  业务错误码
- `errorMessage`
  错误消息
- `alertMessage`
  友好提示
- `data`
  业务数据

成功响应示例：

```json
{
  "traceId": "7a1c6f3a0f0d4e84",
  "success": true,
  "errorCode": null,
  "errorMessage": null,
  "alertMessage": "保存成功",
  "data": {
    "id": "1001",
    "name": "示例对象"
  }
}
```

分页响应示例：

```json
{
  "traceId": "7a1c6f3a0f0d4e84",
  "success": true,
  "data": {
    "total": 52,
    "first": 1,
    "pageCount": 3,
    "pageSize": 20,
    "current": 1,
    "list": [
      {
        "id": "1001",
        "name": "示例对象"
      }
    ]
  }
}
```

前端联调时建议遵守：

- 先按 HTTP 状态码区分是否成功返回
- 再按 `success` 做业务成功判断
- 任何异常反馈都保留 `traceId`，便于后端排查
- `alertMessage` 作为提示文案使用，不要把它当成错误码
- 需要分支处理时优先依赖 `errorCode`，不要只匹配中文报错文案

## 分页对象语义

标准分页对象 `QueryResults<T>` 至少包含：

- `total`
  总记录数
- `first`
  第一条序号
- `pageCount`
  总页数
- `pageSize`
  每页条数
- `current`
  当前页码
- `list`
  当前页数据

前端表格、分页器、导出预览应统一围绕这组字段消费，不要为不同数据栈分别写三套分页适配。

## 标准接口能力一览

控制器根路径由业务控制器自己的类级 `@RequestMapping` 决定。
下面列的是标准后缀语义，来源于 `CoreConstant.Urls`：

| 能力 | 方法 | 标准路径后缀 | 典型入参 | 典型返回 |
|------|------|--------------|----------|----------|
| 创建 | `POST` | `{root}` | `AO` | `ResponseResults<VO>` |
| 批量创建 | `POST` | `{root}/batch` | `Collection<AO>` | `ResponseResults<Iterable<VO>>` |
| 更新 | `PUT` | `{root}` | `AO` | `ResponseResults<VO>` |
| 详情 | `GET` | `{root}/{id}` | `id` | `ResponseResults<VO>` |
| 批量详情 | `GET` | `{root}/batch?idSet=...` | `idSet` | `ResponseResults<Iterable<VO>>` |
| 条件查询 | `GET` | `{root}` | `CommonParameters + Query` | `ResponseResults<QueryResults<VO>>` |
| 逻辑删除 | `DELETE` | `{root}/{id}` | `id` | `ResponseResults<Void>` |
| 批量逻辑删除 | `DELETE` | `{root}?idSet=...` | `idSet` | `ResponseResults<Void>` |
| 恢复 | `PUT` | `{root}/recovery/{id}` | `id` | `ResponseResults<Void>` |
| 批量恢复 | `PUT` | `{root}/recovery` | `Set<String>` | `ResponseResults<Void>` |
| 启用/禁用 | `PUT` | `{root}/enable/{id}`、`{root}/disable/{id}` | `id` | `ResponseResults<Void>` |
| 锁定/解锁 | `PUT` | `{root}/lock/{id}`、`{root}/unlock/{id}` | `id` | `ResponseResults<Void>` |
| 发布/撤销发布 | `PUT` | `{root}/publish/{id}`、`{root}/withdraw/{id}` | `id` | `ResponseResults<Void>` |
| 默认/取消默认 | `PUT` | `{root}/default/{id}`、`{root}/non-default/{id}` | `id` | `ResponseResults<Void>` |
| 主要/取消主要 | `PUT` | `{root}/primary/{id}`、`{root}/non-primary/{id}` | `id` | `ResponseResults<Void>` |
| 账号能力 | `PUT` | `{root}/account/passwd/reset/{id}` 等 | `id` | `ResponseResults<Void>` |

如果某个实体没有开启对应数据状态能力，前端不要默认假定这些按钮和接口一定存在。

注意：当前前端通用 data status helper 中，发布的反向动作使用了 `non-publish` 命名；而 `CoreConstant.Urls` 和 WebMvc/WebFlux `IPublishController` 使用的是 `withdraw`。在代码统一前，启用 `publish` 能力的模块必须显式确认前端调用路径与后端控制器一致，不能盲目依赖默认 helper。

## URL 命名与迁移调整

业务 URL 应优先表达稳定资源层级，不要把多个资源语义压成一个横线单词。遇到历史 URL 调整时，先做全量扫描和影响面确认，再一次性对齐后端、前端、权限清单和测试。

统一命名规则：

- 前端页面路径使用业务层级，例如 `/resource/number/serial/definition`、`/resource/number/serial/pool`、`/resource/number/serial/data`。
- 后端管理 API 与页面层级保持同一资源语义，例如 `/api/resource/number/serial/definition`。
- 后端运行时 API 仍按运行时入口分组，例如 `/api/runtime/resource/number/serial/{applicationKey}/{serialNumberKey}/preview`。
- `createConfigCrudService(url, options)` 里的 `url` 不带 `/api`，但必须和后端 Controller 根路径去掉 `API_PREFIX` 后一致。
- `permissionUrl` 中，`menu/page` 写前端访问路径，`api` 写后端接口路径；两者都要随 URL 调整同步更新。
- `resourceKey`、模块目录、组件路径不因为 URL 层级调整而机械改名，除非需求明确要求一起重命名。

调整步骤：

1. 先用全文搜索列出旧路径的所有出现位置，至少覆盖后端常量、Controller、权限清单、前端 routes/service/store/page、mock、E2E。
2. 确定目标路径后，后端优先修改模块 `Constant.Urls`，避免 Controller 中散落字面量路径。
3. 同步更新权限清单中的前端菜单 URL 和后端 API URL，确保授权后台看到的路径与真实入口一致。
4. 同步更新前端 `routes.js`、模块 `service/*.js`、运行时自定义接口和 Playwright 用例。
5. 如果运行时接口的路径参数语义变化，例如从 `ruleKey` 变为 `applicationKey + serialNumberKey`，前端页面和 service 方法签名必须一起调整，不能只替换 URL 字符串。
6. 对兼容 redirect 明确做产品决策：需要兼容时只保留列出的旧入口；要求“一刀切”时删除旧路由和旧 API，不留下临时 redirect。
7. 修改后再次搜索旧 HTTP 路径，确认没有新增残留；保留的模块目录名、import 路径或 `resourceKey` 需要能解释清楚。
8. 在脏工作区中提交时，只暂存本次 URL 调整相关 diff。混有无关改动的文件必须用 selective staging 或 `git apply --cached` 精确暂存，再用 `git diff --cached` 复核。

调整示例：

```text
旧页面：/resource/serial-number-definition/manage
新页面：/resource/number/serial/definition

旧 API：/api/resource/serial-number/definition
新 API：/api/resource/number/serial/definition

旧运行时 API：/api/runtime/resource/serial-number/{ruleKey}/preview
新运行时 API：/api/runtime/resource/number/serial/{applicationKey}/{serialNumberKey}/preview
```

## `webmvc` 与 `webflux` 对前端必须保持一致的点

新增功能默认应同时评估 `webmvc` 与 `webflux` 两条线。

对前端来说，必须保持一致的不是“是不是 `Mono`”，而是下面这些外部语义：

- 控制器根路径语义
- URL 后缀语义
- `resourceKey` 与按钮权限点
- `AO / Query / VO` 字段集合
- `ResponseResults` 外壳
- `QueryResults` 分页字段
- HTTP 状态码语义
- `errorCode` 与错误消息语义

前端不应因为后端采用 `webflux` 就另写一套接口字段解析逻辑。
对前端而言，`Mono<ResponseResults<T>>` 和 `ResponseResults<T>` 的区别属于服务端实现细节，不应演变成两套 JSON 契约。

## 按钮权限点与 `resourceKey`

`IWebMvcApi` 与 `IWebFluxApi` 都要求业务控制器提供 `getResourceKey()` 和 `getResourceName()`。

`getResourceKey()` 是稳定权限资源标识，`getResourceName()` 是业务展示名。OpenAPI 输出公共接口摘要时会用 `getResourceName()` 替换 `资源` 占位，例如 `创建资源` 输出为 `创建系统身份`。
业务控制器应优先从模块常量读取这两个值：`XxxConstant.Resource.KEY_*` / `XxxConstant.Resource.NAME_*`。

最终权限点通过：

`resourceKey + "." + action`

拼出来。

`permissionKey` 统一使用点号分隔，禁止使用 `:`。推荐格式为 `{moduleKey}.{resource}.{action}`，例如 `platform.area.create`。

权限清单中需要同时区分目录、前端导航、后端接口和页面按钮：

- `catalog` 表达目录或分组，不对应具体前端组件或后端接口，也不配置 `permissionUrl` / `component`。
- `menu` 表达独立前端菜单入口，`permissionUrl` 填前端访问路径，`component` 填真实前端组件路径，`icon` 只给可见菜单节点使用。
- `page` 表达隐藏/从属页面，不独立出现在菜单中，例如详情、编辑、配置、主从资源子列表页；这类页面通常依赖主资源 ID，后端会出现 `ISearchOfMasterIdController` / `search-of-master-id` 能力。
- `api` 的 `permissionUrl` 填后端接口路径，用于表达查询、详情、字典、回收站查询、`search-of-master-id` 等接口能力。
- `button` 只表达页面上的可见操作按钮，不填 `permissionUrl/component/icon`。
- 不要把多个独立菜单入口错误包在一个 `menu` 下再生成为 `page`。需要中间分组时使用 `catalog`，例如 `安全中心 catalog -> 系统身份管理 catalog -> 系统身份 menu / 系统身份分组 menu`。
- `orderWeight` 按权限树 `DESC` 规则设计，权重越大越靠前；推荐顺序是辅助查询 API、列表查询、CRUD、状态操作、回收站操作。
- `permissionDescription` 面向后台授权人员阅读，必须写业务语义，不要把 `idSet`、`ID 集合`、`query`、`commonParameters` 等后端参数名写进说明。

常见 `action` 取值来自 `CoreConstant.ResourceKey`，例如：

- `create`
- `batch-create`
- `update`
- `logic-delete`
- `batch-logic-delete`
- `find-by-id`
- `batch-find-by-id`
- `recovery`
- `enable`
- `disable`
- `lock`
- `unlock`
- `publish`
- `default`
- `primary`
- `account-passwd-reset`

因此需求说明里必须提前写清：

- 控制器根路径
- `resourceKey`
- 页面按钮和权限点映射
- 路由 `meta.resourceKey` 或 `meta.resourceKeyBuilder`

否则前端菜单、按钮显隐和后端授权很容易对不上。

当前前端权限判断还会做少量历史动作别名兼容：

- `save` 可以映射到 `create`
- `details` 可以映射到 `details` 或 `find-by-id`
- `recycle` 可以映射到 `recovery`
- `physics-delete` 可以映射到 `physics-delete` 或 `logic-delete`

这些别名只用于兼容前端权限判断，不应作为新后端权限命名的依据。新接口仍应优先使用 `CoreConstant.ResourceKey` 中的标准 action。

## 导入导出与 Excel 约定

`AO / VO` 不只是表单和展示对象，也要预留后续导入导出能力。

当前约定是：

- 需要导入的对象优先落在 `AO`
- 需要导出的对象优先落在 `VO`
- 只要业务已经明确存在导入导出场景，就应在 `AO / VO` 上直接考虑 `@ExcelProperty`
- 即使当前阶段暂未接入 Excel，也要先把字段中文名、列含义、顺序、是否必填写进需求，避免后续返工

需求里至少要先写清：

- 哪些字段参与导入
- 哪些字段参与导出
- 列头中文名
- 是否需要模板下载
- 是否允许部分字段只导出不导入

## 写需求时前端信息至少补齐这些

参考 [module-requirements-template.md](module-requirements-template.md)，前端相关内容至少要补齐：

- 控制器根路径
- `resourceKey`
- 页面路由或页面入口
- 列表字段
- 查询字段
- 详情字段
- 创建字段
- 编辑字段
- 默认排序
- 按钮权限点
- 是否需要回收站
- 是否需要状态切换
- 是否需要账号能力
- 是否需要导入导出
- 导入导出列头与字段映射
- 是否要求 `webmvc` 与 `webflux` 两条线都开放同等能力
- 前端模块路径，例如 `ineed-frontend/src/modules/business/customer`
- service/store/page/route/schema 是否都需要新增或修改
- 是否复用 `createConfigCrudService` / `createConfigCrudStore`
- `dataStatusCapabilities` 的明确取值
- 路由名称、路由 path、菜单标题和页面级 `resourceKey`
- 表格列、查询项、创建表单、编辑表单、详情面板是否字段一致
- 自定义接口的 URL、方法、入参、返回 `data` 结构和错误码
- Playwright E2E 是否需要新增 fixture、登录凭据依赖和跳过条件

## 前端代码生成的最小输入

如果用代码生成器或自动化工具处理前端联调、页面字段整理任务，输入至少要提供：

```text
后端 Controller 根路径：
后端 resourceKey：
前端模块目录：
路由 path/name/meta.resourceKey：
列表列：
查询字段：
创建 AO 字段：
编辑 AO 字段：
详情 VO 字段：
默认排序：
dataStatusCapabilities：
自定义接口：
需要新增或更新的 service/store/page/schema/routes：
需要新增或更新的 Playwright 用例：
```

实现前必须先核对后端 `AO / Query / VO / Controller` 与前端字段表是否一致；字段不一致时先报告缺口，不要自行猜字段。

## 联调检查清单

- 前端是否只依赖 `AO / Query / VO / ResponseResults / QueryResults`
- 列表分页是否统一消费 `current / pageSize / total / list`
- 查询接口是否通过 `CommonParameters + Query` 传参，而不是自造分页对象
- 是否保留并展示 `traceId`
- 是否按 `errorCode` 做业务分支，而不是硬编码中文文案
- 页面按钮权限点是否与 `resourceKey.action` 一致
- 页面路由、后端 API、权限清单、service URL 和 E2E 是否使用同一套资源路径
- URL 迁移后是否已搜索旧路径，且兼容 redirect 是否符合本次决策
- 如果有导入导出，`AO / VO` 字段和列头是否已经在需求里写清
- 如果本次功能是公共能力，`webmvc` 与 `webflux` 是否都已对齐

> 最后更新：2026-06-01
