12 KiB
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 前端对应的
fetchProductListSHALL 使用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 严格遵循以下约束,防止功能蔓延:
- 仅迁移源系统已有的功能:迁移范围 SHALL 以源系统的实际代码和功能为边界。SHALL NOT 在迁移过程中自行添加源系统不存在的「增强」、「优化」或「扩展」功能。
- 逐接口对照验证:每个迁移的 API 端点和前端页面 SHALL 与源系统逐一对照,确保功能完整且未超出原范围。
- 不做功能裁剪也不做功能膨胀:源系统有的功能 SHALL 全部迁移;源系统没有的功能 SHALL NOT 自行添加。如果判断某功能不再需要,SHALL 由用户明确确认排除后记录到变更文档中。
- 框架适配不等于功能变更:使用 shanxia2.0 的技术栈(FastAPI、Element Plus、Tailwind CSS 4 等)重写是允许的,但业务逻辑和用户交互 SHALL 忠实还原源系统的行为。
- 用户确认的排除项必须文档化:所有经用户确认排除的功能(如「不需要产品定价」、「不需要复制产品」),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:
- 二进制响应:当
Content-Type为 xlsx/octet-stream 等非 JSON 时,SHALL 跳过 JSON 业务码(code === 200)校验,直接返回Blob。 - JSON 错误体:当后端仍返回
application/json(如openpyxl未安装、code: 500)时,SHALL 解析 JSON 并走统一错误抛出,SHALL NOT 将错误 JSON 当作文件下载。 - 返回值层级:
request()在responseType: 'blob'时 SHALL 返回res.data(Blob 本身),SHALL NOT 返回res.data.data。 - 错误提示:导出类 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」文件