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

12 KiB
Raw Blame History

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