Files
shanxia2.0/openspec/changes/zkxw-engineering-standards/specs/coding-conventions/spec.md
T
2026-06-01 08:21:21 +08:00

178 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## ADDED Requirements
### Requirement: 最小必要改动
同一功能或缺陷修复的代码变更 SHALL 仅包含完成任务所必需的修改;不得在同一提交中进行无关重构、大范围格式化或与需求无关的文件挪动。
#### Scenario: 修复单接口校验
- **WHEN** 修复某一接口字段校验
- **THEN** diff SHALL 主要限于该接口、对应 schema 与必要测试,而不包含同模块无关重命名
### Requirement: Web 管理端工程约定
`web-admin` SHALL 遵循 `AGENTS.md` 中的栈与工具约定:包管理使用 pnpm;格式化以 Prettier(单引号、无分号、2 空格等)为准;ESLint/Stylelint 规则与仓库配置一致;Vue 与 Element Plus 依赖自动导入策略时,新增全局组件 SHALL 遵循既有 `unplugin` 配置而非随意手写重复导入(除非文件类型要求)。
#### Scenario: 提交前检查
- **WHEN** 提交 `web-admin` 源码
- **THEN** 贡献者 SHALL 在本地或通过 CI 运行与仓库一致的 lint/format 门禁
### Requirement: 后端工程约定
`backend` SHALL 使用 Python 3.9 与既有 FastAPI、SQLAlchemy、Pydantic v2 模式;新增领域逻辑 SHALL 优先落在 `app/services/` 与 `app/crud/`,路由层保持薄;配置 SHALL 通过 `pydantic-settings` 与 `.env` 系列文件管理。
#### Scenario: 新增业务接口
- **WHEN** 新增一组管理端 API
- **THEN** 路由 SHALL 放入 `app/api/` 合适模块,schema 放入 `app/schemas/`,数据库访问放入 `app/crud/` 或复用基类
### Requirement: 路由层访问 Pydantic 请求体字段
`app/api/**` 中从请求体 `body: XxxBody` 读取字段时,SHALL 使用 `app/schemas/**` 里定义的 **Python 属性名**(多为 snake_case),SHALL NOT 使用 JSON camelCase 的 `Field(alias=...)` 字符串作为 `body.xxx`(例如 SHALL NOT 写 `body.createTimeRange`)。传 CRUD 时优先 `body.model_dump(exclude_unset=True)`。
完整约定、字段对照表与反例 **EC-API-001** 见 `api-conventions` §「JSON 字段命名(前后端 camelCase 对齐)」及 §「常见错误案例(API / Pydantic)」。
#### Scenario: 新增 POST 路由拆解 body 字段
- **WHEN** 路由需将列表查询参数传入 `app/crud/`,且 Schema 中 `view_mode` 的 alias 为 `viewMode`
- **THEN** SHALL 写 `view_mode=body.view_mode`,SHALL NOT 写 `view_mode=body.viewMode`
### Requirement: 默认租户编号 tenant_no(代码层)
本仓库门户默认租户编号 SHALL 为 **`shanxia2.0`**,与 OpenSpec `database-conventions` 中 DDL 默认值一致。
| 层级 | 约定 |
|------|------|
| 配置 | `app/core/config.py` 中 `DEFAULT_TENANT_NO` SHALL 为 `"shanxia2.0"`;本地/部署 `.env`、`.env.example` SHALL 提供 `DEFAULT_TENANT_NO=shanxia2.0` |
| ORM | `app/models/` 中含 `tenant_no` 的 `Column` SHALL 使用 `default="shanxia2.0"`(`sys_tenant.tenant_no` 除外,其为租户编码主字段) |
| CRUD / 服务 | 无登录上下文或需门户默认租户时(如 Web 登录按 `user_no` 查用户、新建实体未从 JWT 注入租户),SHALL 使用 `settings.DEFAULT_TENANT_NO`,SHALL NOT 硬编码 `zkxw` |
| 写入 | 业务写入 SHALL 将 `tenant_no` 设为当前用户租户;未显式赋值且由 ORM/服务端默认填充时,SHALL 落库为 `shanxia2.0` |
#### Scenario: 新增 SQLAlchemy 模型
- **WHEN** 在 `app/models/` 为业务表增加 `tenant_no` 字段
- **THEN** 模型 SHALL 声明 `default="shanxia2.0"`,并与 `docs/sql/` 中对应表的 `DEFAULT 'shanxia2.0'` 一致
#### Scenario: Web 管理端登录查用户
- **WHEN** 调用 `user_crud.get_by_user_no` 等按账号解析用户的入口且未传入 `tenant_no`
- **THEN** 查询条件中的租户 SHALL 为 `settings.DEFAULT_TENANT_NO`(`shanxia2.0`),与库中 `sys_user.tenant_no` 一致
#### Scenario: 代码评审
- **WHEN** 评审涉及租户隔离或新建表的变更
- **THEN** SHALL NOT 引入 `default="zkxw"` 或 `DEFAULT_TENANT_NO=zkxw`;若发现历史 `zkxw` 数据,SHALL 通过 `docs/sql/` 迁移脚本统一而非在代码中保留旧默认
### Requirement: 数据库脚本与迁移
涉及表结构变更时,维护者 SHALL 在 `docs/sql/` 中维护或更新与需求一致的 SQL 说明(全量基线导出为 `shanxia2.0.sql`;增量变更可补充同目录下其他 `.sql`),并评估 Alembic/脚本与运行环境的同步(与 `AGENTS.md` 数据库变更约定一致);含 `tenant_no` 的表 SHALL 符合 `database-conventions` 默认租户 `shanxia2.0` 约定。
#### Scenario: 新增业务表
- **WHEN** 增加新表或字段
- **THEN** 仓库 SHALL 包含可复现的 schema 片段或迁移,且与 OpenSpec `database-conventions` 中的公共字段规则一致
### Requirement: 单文件行数上限
前端(`web-admin`)与后端(`backend`)的单个源码文件 SHALL 不超过 500 行。当文件因功能增长接近或超过此限制时,SHALL 将其拆分为多个职责单一的小文件(如:前端提取子组件、composable 或工具函数;后端按领域拆分 service、crud 模块或辅助函数)。配置文件、类型声明文件、SQL 脚本与自动生成文件不在此限制范围内。
#### Scenario: 前端页面文件增长
- **WHEN** 一个 Vue 组件文件超过 500 行
- **THEN** SHALL 将弹窗、复杂表格、可复用面板等拆分为独立子组件或 composable 函数,确保主入口文件在 500 行以内
#### Scenario: 后端模块文件增长
- **WHEN** 一个 Python 模块文件(api route、service、crud 等)超过 500 行
- **THEN** SHALL 按领域或功能边界拆分为多个子模块(如 `crud/production_task.py` → `crud/production_task.py` + `crud/production_task_step.py`),确保单个文件在 500 行以内
### Requirement: 提交说明可读性
提交信息 SHALL 使用完整句子说明「改了什么、为何」,并符合仓库 Conventional Commits / commitlint 类型约束。
#### Scenario: merge request
- **WHEN** 创建合并请求
- **THEN** 描述 SHALL 使评审者无需阅读聊天历史即可理解动机与风险
### Requirement: 前端 API 返回值消费约定
`web-admin` 的 axios 拦截器(`src/utils/http/index.ts`)已对响应做两层解包:`res.data.data` → 返回值即为后端 `data` 字段的内容。因此页面组件中消费 API 函数返回值时,SHALL NOT 再做 `{ data }` 解构或 `.data` 取值,SHALL 直接从返回值中读取业务字段。
#### Scenario: 调用列表 API
- **GIVEN** 后端返回 `{"code": 200, "data": {"list": [...], "total": 12}}`
- **WHEN** 页面调用 `const result = await fetchXxxList(params)`
- **THEN** `result` 已经是 `{"list": [...], "total": 12}`,SHALL 使用 `result.list`、`result.total`,SHALL NOT 写 `const { data } = await fetchXxxList()` 再从 `data.list` 取值
#### Scenario: 调用单条 API
- **GIVEN** 后端返回 `{"code": 200, "data": {"smsContent": "..."}}`
- **WHEN** 页面调用 `const result = await fetchGenerateSms(id)`
- **THEN** `result` 已经是 `{"smsContent": "..."}`,SHALL 使用 `result.smsContent`,SHALL NOT 写 `const { data } = await fetchGenerateSms()` 再从 `data.smsContent` 取值
#### Scenario: 新增 API 调用前确认返回值层级
- **WHEN** 编写新的页面消费 API 函数
- **THEN** 开发者 SHALL 先查看 `src/utils/http/index.ts` 第 420 行的 `return res.data.data as T`,确认拦截器已剥离 `{code, data}` 外层包装,消费端直接拿返回值即可
### Requirement: 前后端统一 POST 调用
前端 API 函数 SHALL 统一使用 `request.post()` 调用后端接口(文件上传下载除外),与后端统一 POST 路由规范保持一致(详见 `api-conventions` spec「统一 POST 接口」)。
#### Scenario: 新增 API 接口
- **WHEN** 后端定义 `@router.post("/products/list")`
- **THEN** 前端对应的 `fetchProductList` SHALL 使用 `request.post({ url: '/web/crm/products/list', data: params })`
#### Scenario: 代码评审检查项
- **WHEN** 评审涉及前后端联调的变更
- **THEN** 评审者 SHALL 校验前端 `request.post()` 与后端 `@router.post()` 一一匹配,SHALL NOT 出现 `request.get()` / `request.put()` / `request.delete()` 调用业务接口
### Requirement: 已有系统迁移的蔓延控制
将已有系统(CRM、报销、地磅等)的业务模块迁移到 shanxia2.0 框架时,SHALL 严格遵循以下约束,防止功能蔓延:
1. **仅迁移源系统已有的功能**:迁移范围 SHALL 以源系统的实际代码和功能为边界。SHALL NOT 在迁移过程中自行添加源系统不存在的「增强」、「优化」或「扩展」功能。
2. **逐接口对照验证**:每个迁移的 API 端点和前端页面 SHALL 与源系统逐一对照,确保功能完整且未超出原范围。
3. **不做功能裁剪也不做功能膨胀**:源系统有的功能 SHALL 全部迁移;源系统没有的功能 SHALL NOT 自行添加。如果判断某功能不再需要,SHALL 由用户明确确认排除后记录到变更文档中。
4. **框架适配不等于功能变更**:使用 shanxia2.0 的技术栈(FastAPI、Element Plus、Tailwind CSS 4 等)重写是允许的,但业务逻辑和用户交互 SHALL 忠实还原源系统的行为。
5. **用户确认的排除项必须文档化**:所有经用户确认排除的功能(如「不需要产品定价」、「不需要复制产品」),SHALL 在对应迭代的设计文档或实施计划中明确标注为「已排除:[功能名] - 原因:[用户确认不需要]」。
#### Scenario: 迁移 CRM 产品模块
- **GIVEN** CRM 源系统有产品 CRUD、产品分类、Excel 导入导出,但用户确认不需要产品定价、标签、复制功能
- **WHEN** 实现 shanxia2.0 的产品管理模块
- **THEN** SHALL 只实现产品 CRUD、产品分类、Excel 导入导出,SHALL NOT 自行添加定价、标签、复制等功能,排除项 SHALL 记录在设计文档中
#### Scenario: 迁移过程中发现源系统的隐藏功能
- **WHEN** 在迁移过程中发现源系统有某个未被初始分析覆盖的功能(如某个批量操作或联动逻辑)
- **THEN** 开发者 SHALL 将该功能加入迁移清单并实现,SHALL NOT 以「之前没提到」为由跳过
#### Scenario: 代码评审检查项
- **WHEN** 评审迁移类代码变更
- **THEN** 评审者 SHALL 对照源系统功能清单,确认实现的功能既未遗漏也未超出
### Requirement: Blob 文件下载(Excel 导出等)
`web-admin` 调用 `responseType: 'blob'` 的接口(CRM Excel 导出、导入模板下载等)时,axios 封装(`src/utils/http/index.ts`)SHALL:
1. **二进制响应**:当 `Content-Type` 为 xlsx/octet-stream 等非 JSON 时,SHALL 跳过 JSON 业务码(`code === 200`)校验,直接返回 `Blob`。
2. **JSON 错误体**:当后端仍返回 `application/json`(如 `openpyxl` 未安装、`code: 500`)时,SHALL 解析 JSON 并走统一错误抛出,SHALL NOT 将错误 JSON 当作文件下载。
3. **返回值层级**:`request()` 在 `responseType: 'blob'` 时 SHALL 返回 `res.data`(Blob 本身),SHALL NOT 返回 `res.data.data`。
4. **错误提示**:导出类 API 函数 SHOULD 设置 `showErrorMessage: false`,由页面 `catch` 展示「导出失败」,避免与全局 `showError` 重复。
#### Scenario: CRM 线索导出成功
- **WHEN** 前端 `exportClue` 使用 `responseType: 'blob'` 且后端返回 xlsx 流
- **THEN** 拦截器 SHALL 不抛 `HttpError`,页面 SHALL 能通过 `URL.createObjectURL` 触发下载
#### Scenario: CRM 导出后端异常
- **WHEN** 后端返回 JSON `{ "code": 500, "message": "..." }` 且 HTTP 状态仍为 200
- **THEN** 前端 SHALL 进入 `catch` 并提示失败,SHALL NOT 下载损坏的「伪 xlsx」文件