# 系统开发完整标准 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 = `