From d5c498549b55ce9291f62ad02e37c0714cb9cf2b Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 23 May 2026 16:22:36 +0800 Subject: [PATCH] docs: system development standard v1.0 - full coding rules for Python/FastAPI/Frontend Co-authored-by: Cursor --- docs/project/system-development-standard.md | 499 ++++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 docs/project/system-development-standard.md diff --git a/docs/project/system-development-standard.md b/docs/project/system-development-standard.md new file mode 100644 index 00000000..c39cf609 --- /dev/null +++ b/docs/project/system-development-standard.md @@ -0,0 +1,499 @@ +# 系统开发完整标准 v1.0 + +> **版本**: 1.0 · 2026-05-23 +> **适用**: Python/FastAPI 后端 + 前端 全栈项目 +> **来源**: perfect-implementation.mdc + nexus-*规则 + Phase 1–4 审计实践提炼 +> **性质**: 强制规范,不得降级或妥协 + +--- + +## 零、实现铁律 + +``` +❌ 不允许为了实现而实现 — 每行代码必须有明确目的 +❌ 不允许降级实现 — 不以「先用着」为由降低安全/架构/质量 +❌ 不允许妥协实现 — 发现问题必须根治,不得绕过 +❌ 不允许留技术债 — TODO / FIXME / 临时方案禁止遗留 +✅ 无法完美实现时必须停 — 告知用户确认方案后再换路,不擅自从权 +``` + +--- + +## 一、开发执行顺序(每次必走) + +``` +需求确认 → 设计文档 → 技术文档 → 实现 → 测试验证 → changelog → 更新项目记忆文档 +``` + +| 阶段 | 产出路径 | 必含内容 | +|------|---------|---------| +| 设计文档 | `docs/design/specs/YYYY-MM-DD--design.md` | 背景与目标、方案对比、选定方案及理由、接口/数据模型、安全约束、验收标准 | +| 技术文档 | `docs/design/plans/YYYY-MM-DD-.md` | 涉及文件清单、实现步骤、依赖/配置变更、测试要点、回滚方式 | +| Changelog | `docs/changelog/YYYY-MM-DD-<主题>.md` | 日期、变更摘要、动机、涉及文件、是否需迁移/重启、验证方式 | + +**新功能/架构变更/非平凡重构**:先设计文档,再写代码。 +**改动时已有模块缺文档**:一并补全,不得只改代码不留档。 +**禁止**:把 changelog 只写进 commit message。 + +--- + +## 二、架构分层(禁止跨层直连) + +``` +HTTP 路由层 → Service 层 → Repository 层 → Domain 模型 + (api/) (services/) (*_repo.py) (models/) + +基础设施层(database / redis / ssh / telegram)仅被 Service 层或 Repository 层调用 +前端(web/app/)仅通过 HTTP API 与后端通信,不直接操作 DB/Redis +``` + +| 层 | 职责 | 禁止 | +|----|------|------| +| API 路由层 | 请求校验、鉴权 Depends、调用 Service、返回响应 | 写 SQL、直接操作 Redis | +| Service 层 | 业务编排、事务边界、审计写入 | 直接 import FastAPI 依赖 | +| Repository 层 | DB 读写、分页、批量操作 | 业务逻辑 | +| Domain 模型 | ORM 字段定义、关系 | 任何业务逻辑、FastAPI 依赖 | +| 基础设施层 | DB 连接池、Redis 客户端、SSH 池、外部 HTTP | 业务逻辑 | + +--- + +## 三、后端编码规范(Python / FastAPI) + +### 3.1 API 路由 + +```python +# ✅ 正确:Pydantic 校验 + JWT 鉴权 + 结构化错误 +@router.post("/resource", response_model=dict, status_code=201) +async def create_resource( + payload: ResourceCreate, # Pydantic 自动校验 + admin: Admin = Depends(get_current_admin), # JWT 鉴权 + service: ResourceService = Depends(...), + db: AsyncSession = Depends(get_db), +): + ... + raise HTTPException(status_code=422, detail="具体原因") # 结构化错误 + +# ❌ 禁止:裸 except、无鉴权、无 Pydantic +@router.post("/resource") +async def create(data: dict): # 无 Pydantic,无鉴权 + try: + ... + except: + pass # 禁止静默吞错 +``` + +### 3.2 鉴权矩阵(强制) + +| 调用方 | 机制 | 备注 | +|--------|------|------| +| 管理端 API | `Depends(get_current_admin)` JWT Bearer | 所有业务路由必须 | +| Agent | `X-API-Key` header + `secrets.compare_digest` | 防时序攻击 | +| WebSocket | JWT query param `?token=` | ADR-011 | +| 安装向导 | 无 JWT | 仅安装模式且安装后 403 | + +### 3.3 数据库 + +```python +# ✅ 时间戳:必须带 timezone +datetime.now(timezone.utc) # ✅ +datetime.now() # ❌ naive,与 aware 比较 → TypeError + +# ✅ 分页在 Repository 层 +async def get_paginated(offset, limit) -> tuple[list, int]: + count = await session.execute(select(func.count(Model.id))) + data = await session.execute(select(Model).offset(offset).limit(limit)) + +# ✅ 字段长度:加密后的值须 Text 而非 String(N) +ssh_key_private = Column(Text) # RSA 4096 Fernet 后 ~4300 chars +encrypted_pw = Column(String(500)) # 短密码 Fernet 后 ~100 chars,可 String + +# ✅ 外键删除策略:明确声明 +ForeignKey("servers.id", ondelete="CASCADE") # 主记录删 → 关联删 +ForeignKey("scripts.id", ondelete="SET NULL") # 主记录删 → 关联置 null +``` + +### 3.4 Redis + +```python +# ✅ 使用 pipeline 保证 INCR + EXPIRE 原子性 +pipe = redis.pipeline() +pipe.incr(key) +pipe.expire(key, window_seconds) +count, _ = await pipe.execute() + +# ❌ 非原子(崩溃在中间 → key 无 TTL 永久存在) +count = await redis.incr(key) +await redis.expire(key, window_seconds) # 若崩溃 → 永久存在 +``` + +### 3.5 SSH / Shell + +```python +# ✅ 整段 key=value 必须整体 quote +cmd = f"echo {shlex.quote(f'{key}={value}')}" # ✅ +cmd = f"echo {shlex.quote(key)}={shlex.quote(value)}" # ❌ 等号在引号外 + +# ✅ shlex.quote 结果不放进双引号(引号嵌套变字面量) +f"{shlex.quote(path)}.pid" # ✅ 产生 '/path/to/file'.pid +f'"{shlex.quote(path)}.pid"' # ❌ 产生 "'/path/to/file'.pid"(单引号字面量) + +# ✅ 多行注入防御:剥离换行 +safe_value = str(value).replace('\n',' ').replace('\r',' ').replace('\x00','') +``` + +### 3.6 凭据与加密 + +```python +# ✅ 存储:Fernet 加密 +password_col = encrypt_value(plaintext) + +# ✅ 响应:只返回布尔标记 +{"password_set": bool(server.password)} # ✅ +{"password": server.password} # ❌ 禁止返回密文或明文 + +# ✅ 变量替换:两轮替换防嵌套 +SENTINELS = {"$DB_PASS": "\x00PASS\x00", "$DB_USER": "\x00USER\x00"} +VALUES = {"\x00PASS\x00": real_password, "\x00USER\x00": real_user} +for placeholder, sentinel in SENTINELS.items(): + cmd = cmd.replace(placeholder, sentinel) +for sentinel, value in VALUES.items(): + cmd = cmd.replace(sentinel, value) + +# ✅ 日志:脱敏后记录 +logger.info(f"Redis: {_mask_url(settings.REDIS_URL)}") # 非 settings.REDIS_URL 原文 +``` + +### 3.7 错误处理 + +```python +# ✅ 向上抛,让调用方决定 +def decrypt_value(ciphertext): + try: + return fernet.decrypt(...) + except Exception as e: + raise ValueError("解密失败") from e # ✅ 不返回密文,不静默 + +# ❌ 静默吞错(禁止) + except Exception: + return ciphertext # ❌ 返回密文 = 信息泄露 + return None # ❌ 调用方看不到错误 + +# ✅ gather 异常必须检查 +results = await asyncio.gather(*tasks, return_exceptions=True) +for r in results: + if isinstance(r, Exception): + logger.error("Task failed: %s", r) +``` + +### 3.8 Datetime 规范 + +```python +# ✅ 所有 datetime 带 timezone +from datetime import datetime, timezone +now = datetime.now(timezone.utc) # ✅ + +# ✅ MySQL 返回 naive,比较前统一 +expires = record.expires_at # naive(MySQL 存储无时区) +now_cmp = datetime.now(timezone.utc) +if expires.tzinfo is None: + now_cmp = now_cmp.replace(tzinfo=None) # 统一为 naive 再比较 +if expires < now_cmp: ... + +# ✅ Cron 解析防除零 +step = int(part[2:]) +if step == 0: + return False # 无效 step,跳过 +``` + +--- + +## 四、前端编码规范 + +### 4.1 API 调用 + +```javascript +// ✅ 统一封装函数(含 Authorization + 错误处理) +const data = await apiFetch("/api/resource/"); + +// ❌ 禁止裸 fetch(漏 Authorization) +const data = await fetch("/api/resource/"); +``` + +### 4.2 XSS 防御(强制) + +```javascript +// ✅ 文本内容用 textContent 或 Alpine 绑定 +el.textContent = serverName; + +// ✅ 必须插入 HTML 时用 esc() +el.innerHTML = `${esc(serverName)}`; + +// ❌ 禁止 +el.innerHTML = `${serverName}`; // 未 esc +el.innerHTML = `