178 lines
12 KiB
Markdown
178 lines
12 KiB
Markdown
## 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」文件
|