开发规范完善
This commit is contained in:
parent
0dbbdb0813
commit
271e02032c
113
docs/前端API开发规范-Orval.md
Normal file
113
docs/前端API开发规范-Orval.md
Normal file
@ -0,0 +1,113 @@
|
||||
# 前端 API 开发规范(Orval 生成代码)
|
||||
|
||||
本规范面向 `reading-platform-frontend`,以 `src/api/generated/` 为**接口类型与路径的唯一真源**,通过 Orval 从后端 OpenAPI 自动生成 TypeScript 类型 + 客户端方法。
|
||||
|
||||
## 1. 目录与职责边界
|
||||
|
||||
- **`reading-platform-frontend/src/api/generated/`**:Orval 自动生成目录,**禁止手改**。
|
||||
- `api.ts`:`getReadingPlatformAPI()` 工厂函数,返回包含全部接口方法的对象。
|
||||
- `model/`:OpenAPI 生成的 DTO/VO/Result/PageResult/Params 类型。
|
||||
- **`reading-platform-frontend/src/api/client.ts`**:项目侧的“统一入口/别名层”,导出 `readingApi`(完整客户端实例)以及常用的类型工具(解包、分页别名等)。
|
||||
- **`reading-platform-frontend/src/api/*.ts`**:业务侧“适配层”(可选),用于:
|
||||
- 兼容既有页面期望的“扁平结构/字段名/返回形态”
|
||||
- 补齐 OpenAPI 暂未覆盖的历史接口(短期过渡)
|
||||
- 汇聚跨接口的业务逻辑(例如组合请求、额外校验)
|
||||
|
||||
## 2. 基本原则(必须遵守)
|
||||
|
||||
- **生成代码只读**:不得在 `src/api/generated/**` 内做任何手工修改(包括修复类型、改路径、加字段)。
|
||||
- **以生成类型为准**:参数/返回类型优先使用 `src/api/generated/model` 导出的类型,避免手写 `interface` 漂移。
|
||||
- **对外只暴露稳定的业务接口**:页面/组件尽量通过 `src/api/*.ts`(适配层)或 `src/api/client.ts`(直接调用)访问,避免散落调用方式导致难以迁移。
|
||||
|
||||
## 3. 推荐调用方式
|
||||
|
||||
### 3.1 直接使用 Orval 客户端
|
||||
|
||||
- 统一从 `src/api/client.ts` 引入:
|
||||
- `readingApi`: `getReadingPlatformAPI()` 的实例
|
||||
- `ApiResultOf` / `UnwrapResult` / `PageDataOf` 等类型工具
|
||||
|
||||
示例(以 `Result<T>` 为包裹结构):
|
||||
|
||||
```ts
|
||||
import { readingApi } from "@/api/client";
|
||||
import type { ResultTenant } from "@/api/generated/model";
|
||||
|
||||
async function loadTenant(id: number) {
|
||||
const res = (await readingApi.getTenant(id)) as ResultTenant;
|
||||
return res.data; // T(可能为 undefined,取决于后端返回与类型定义)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 使用“适配层”稳定返回结构
|
||||
|
||||
当页面已经依赖历史返回结构(例如直接要 `items/total/page/pageSize`),在 `src/api/*.ts` 内做一次性适配,页面只消费适配后的结构。
|
||||
|
||||
分页适配建议统一输出:
|
||||
|
||||
- `items: T[]`
|
||||
- `total: number`
|
||||
- `page: number`
|
||||
- `pageSize: number`
|
||||
|
||||
## 4. Result / PageResult 约定与解包
|
||||
|
||||
后端统一响应通常为:
|
||||
|
||||
- **普通接口**:`Result<T>`,字段一般为 `code/message/data`
|
||||
- **分页接口**:`Result<PageResult<T>>`,字段一般为 `items/total/page/pageSize`
|
||||
|
||||
在生成代码中常见类型形态:
|
||||
|
||||
- `ResultXXX`(如 `ResultTenant`、`ResultUserInfoResponse`)
|
||||
- `ResultPageResultXXX`(如 `ResultPageResultTenant`)
|
||||
- `PageResultXXX`(如 `PageResultTenant`)
|
||||
|
||||
建议做法:
|
||||
|
||||
- **组件/页面层尽量不要直接处理 `ResultXXX`**,而是由适配层解包并做兜底(空数组、默认分页参数等)。
|
||||
- **严禁在页面散落 `as any`**;确需兼容时,集中在 `src/api/*.ts` 适配层进行,并在适配层内把“最终对页面返回的类型”定义清楚。
|
||||
|
||||
## 5. 命名与重复接口(`getXxx`/`getXxx1`/`getXxx2`)
|
||||
|
||||
由于不同角色端点(teacher/school/parent/admin)可能存在同名资源,Orval 在生成时会用 `1/2/3` 后缀消歧,例如:
|
||||
|
||||
- `getTask`(teacher) vs `getTask1`(school) vs `getTask2`(parent)
|
||||
|
||||
规范建议:
|
||||
|
||||
- **业务层不要直接暴露带数字后缀的方法名**;
|
||||
- 在 `src/api/*.ts` 中封装为语义化名称,例如:
|
||||
- `teacherGetTask` / `schoolGetTask` / `parentGetTask`
|
||||
- 或按模块拆分到 `src/api/teacher/task.ts` 等(如后续重构允许)
|
||||
|
||||
## 6. 何时需要更新生成代码
|
||||
|
||||
当后端 Controller 或 DTO/VO 发生变更:
|
||||
|
||||
1. 后端更新 OpenAPI(Knife4j/SpringDoc)
|
||||
2. 前端更新规范并重新生成(项目已有脚本):
|
||||
|
||||
```bash
|
||||
cd reading-platform-frontend
|
||||
npm run api:update
|
||||
```
|
||||
|
||||
3. 提交生成物(通常包含 `api-spec.*` 与 `src/api/generated/**`)
|
||||
|
||||
> 注意:如果某接口在后端已存在但 OpenAPI 未导出(例如缺少注解/返回类型不规范),应优先修后端文档,而不是在前端“硬编码路径”长期绕过。
|
||||
|
||||
## 7. 禁止事项(高频踩坑)
|
||||
|
||||
- **禁止**:手改 `src/api/generated/**`(下次生成会被覆盖,且会引入不可追踪差异)。
|
||||
- **禁止**:页面里手写 axios 调用去访问 `/api/v1/...`(除非 OpenAPI 暂缺且已在适配层集中兜底)。
|
||||
- **禁止**:在业务代码中扩散 `any` 来“快速通过类型检查”。
|
||||
|
||||
## 8. 迁移策略(从旧 `http` 到 Orval)
|
||||
|
||||
若已有模块使用 `src/api/index.ts` 的 `http.get/post/...`:
|
||||
|
||||
- **短期**:保留旧实现,但新增/变更接口优先走 `readingApi`
|
||||
- **中期**:逐模块把旧 `http` 调用替换为 `readingApi`,并在适配层维持页面不改
|
||||
- **长期**:页面全面只依赖适配层/生成客户端,减少重复封装
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { http } from "./index";
|
||||
import { readingApi, GetTenantPageResult } from "./client";
|
||||
import { readingApi } from "./client";
|
||||
import type { ResultPageResultTenant, Tenant as ApiTenant } from "./generated/model";
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
@ -202,8 +202,25 @@ export interface TenantQuotaUpdateRequest {
|
||||
|
||||
// ==================== 租户管理 ====================
|
||||
|
||||
export const getTenants = (params: TenantQueryParams) =>
|
||||
readingApi.getTenantPage(params);
|
||||
export const getTenants = (
|
||||
params: TenantQueryParams,
|
||||
): Promise<{
|
||||
items: Tenant[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> =>
|
||||
readingApi.getTenantPage(params as any).then((res) => {
|
||||
const wrapped = res as ResultPageResultTenant;
|
||||
const pageData = wrapped.data;
|
||||
|
||||
return {
|
||||
items: (pageData?.items as unknown as ApiTenant[] as Tenant[]) ?? [],
|
||||
total: pageData?.total ?? 0,
|
||||
page: pageData?.page ?? params.page ?? 1,
|
||||
pageSize: pageData?.pageSize ?? params.pageSize ?? 10,
|
||||
};
|
||||
});
|
||||
|
||||
export const getTenant = (id: number): Promise<TenantDetail> =>
|
||||
readingApi.getTenant(id).then((res) => res.data as any);
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { readingApi } from "./client";
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse as ApiLoginResponse,
|
||||
ResultLoginResponse,
|
||||
ResultUserInfoResponse,
|
||||
UserInfoResponse,
|
||||
} from "./generated/model";
|
||||
|
||||
export type LoginParams = LoginRequest;
|
||||
// 兼容现有登录页字段命名(account)
|
||||
export interface LoginParams {
|
||||
account: string;
|
||||
password: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// Java 后端返回的平铺结构(保持与现有业务使用一致)
|
||||
export interface LoginResponse extends Required<
|
||||
@ -30,7 +34,13 @@ export interface UserProfile {
|
||||
|
||||
// 登录
|
||||
export function login(params: LoginParams): Promise<LoginResponse> {
|
||||
return readingApi.login(params).then((res) => {
|
||||
return readingApi
|
||||
.login({
|
||||
username: params.account,
|
||||
password: params.password,
|
||||
role: params.role,
|
||||
})
|
||||
.then((res) => {
|
||||
const wrapped = res as ResultLoginResponse;
|
||||
const data = (wrapped.data ?? {}) as ApiLoginResponse;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user