## 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」文件