# 考培练系统规范与约定(团队基线) > 最后更新:2026-01-21 | 所有开发必须遵循 --- ## 核心规范速查 | 规范 | 核心原则 | 检查项 | |------|---------|--------| | 静态资源 | 使用相对路径,禁止硬编码域名 | 无localhost、无IP、无端口 | | 页面动态数据 | 从API获取,禁止硬编码 | 无固定标题、名称等占位符 | | API响应 | `res.code`和`res.data`,不要多套一层 | 无`res.data.code` | | request.get | 参数必须包装为`{ params }` | 无直接传对象 | | 多租户ID | 禁止硬编码默认值 | 无`id=1`默认值 | | AI服务 | 通过AIService调用,传db_session | 无直接API调用 | | **AI Key** | **从管理库加载,禁止硬编码** | **无sk-xxx字符串** | | **AI配置** | **从 kaopeilian_admin.tenant_configs 读取** | **按租户隔离** | | **默认模型** | **优先最强:Claude Opus 4.5** | **非gemini-flash** | | 时区 | 统一Asia/Shanghai | 容器TZ环境变量 | --- ## 数据库规范 ### 用户姓名字段 - `full_name` = 人名(张三、李四) - ❌ 不要存职位名称(资深美容顾问) ### 模拟数据 - 用户:轻医美行业常见中文姓名 - 学员示例:李美琳、王芳、陈静 --- ## 前端规范 ### 静态资源访问 ```typescript // ✅ 正确:相对路径 const url = '/static/uploads/courses/1/file.pdf' // ❌ 错误:硬编码 const url = `http://localhost:8000${path}` ``` ### API响应访问 ```typescript const res = await getList() // ✅ 正确 if (res.code === 200) { data.value = res.data } // ❌ 错误 if (res.data.code === 200) { data.value = res.data.data } ``` ### request.get参数 ```typescript // ✅ 正确 request.get(url, { params: { id: 1 } }) // ❌ 错误 request.get(url, { id: 1 }) ``` ### 页面动态数据获取 ```typescript // ✅ 正确:从API获取实际数据 const courseInfo = ref({ title: '加载中...', id: route.query.courseId }) onMounted(async () => { const data = await getCourseDetail(courseInfo.value.id) courseInfo.value.title = data.title || data.name }) // ❌ 错误:硬编码占位符 const courseInfo = ref({ title: '销售技巧基础训练', id: '1' }) ``` ### API调用前置检查 ```typescript // 调用受限API前检查条件 if (userInfo.role !== 'trainee' || !userInfo.phone) { ElMessage.warning('请先绑定手机号') return } ``` ### HTTP 客户端选择(2026-01-21 新增) ```typescript // ✅ 正确:普通 JSON 请求使用统一的 http 封装 import http from '@/utils/http' const response = await http.get<{ data: Course[] }>('/api/v1/courses') return response.data // ✅ 正确:SSE 流式请求必须使用原生 fetch(Axios 不支持 ReadableStream) const response = await fetch('/api/v1/course/chat', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify(params) }) return response.body // ReadableStream // ❌ 错误:普通请求也用 fetch,无法利用统一的认证、错误处理、重试机制 const response = await fetch('/api/v1/course/conversations') ``` **http 封装优势**: - 自动注入 `Authorization: Bearer {token}` - 401 自动刷新 Token 并重试 - 统一错误处理和用户提示 - 请求日志和重试机制 ### CSS高度填充(iframe/预览容器) ```scss // ❌ 错误:height:100% 依赖父元素有明确高度,在 flex 布局中常失效 .preview-content { height: 100%; .html-iframe { height: 100%; // 父元素无明确高度时计算为0 } } // ✅ 正确:使用 flex:1 填充可用空间 .preview-content { display: flex; flex-direction: column; min-height: 500px; .html-viewer { flex: 1; display: flex; flex-direction: column; .html-iframe { flex: 1; min-height: 600px; // 保底最小高度 } } } ``` --- ## 后端规范 ### 多租户ID默认值 ```python # ❌ 错误:硬编码 position_id = 1 # ✅ 正确:动态查询 result = await db.execute(select(PositionCourse.position_id).where(...)) position_id = result.scalar_one_or_none() if not position_id: raise HTTPException(400, "未找到关联岗位") ``` ### 业务异常处理 ```python try: result = await service.action() except ExternalServiceError as e: raise HTTPException(400, str(e)) # 业务错误→400 except Exception as e: raise HTTPException(500, str(e)) # 系统错误→500 ``` ### FastAPI路由顺序 ```python # ✅ 正确:具体路由在前 @router.get("/mistakes") # 先定义 @router.get("/{exam_id}") # 后定义 ``` ### Pydantic空字符串处理 ```python @field_validator("category", mode="before") def normalize(cls, v): if isinstance(v, str) and not v.strip(): return CourseCategory.GENERAL # 空字符串→默认值 return v ``` --- ## AI服务规范 ### 统一调用方式 ```python # ✅ 正确:通过AIService,传db_session ai_service = AIService(module_code="answer_judge", db_session=db) response = await ai_service.chat(messages=[...], prompt_name="answer_judge") # ❌ 错误:直接调用API、不传db_session ``` ### 提示词文件位置 `app/services/ai/prompts/{功能名}_prompts.py` ### AI 服务实现 - 所有 AI 功能使用 Python 原生实现 - 服务商策略:4sapi.com 首选 → OpenRouter 备选(自动降级) - 无外部 AI 平台依赖,100% 可控 ### AI 配置加载规范(强制!) **配置存储位置**:管理库 `kaopeilian_admin.tenant_configs` 表 **配置加载优先级**: 1. 管理库 `tenant_configs` 表(按 TENANT_CODE 查询) 2. 环境变量(fallback) 3. 代码默认值(仅用于开发) **容器必须的环境变量**: ```env # .env.{tenant} 文件必须包含 TENANT_CODE=ex # 管理库连接配置 ADMIN_DB_HOST=prod-mysql ADMIN_DB_PORT=3306 ADMIN_DB_USER=root ADMIN_DB_PASSWORD=ProdMySQL2025!@# ADMIN_DB_NAME=kaopeilian_admin ``` **数据库配置表结构**: ```sql -- kaopeilian_admin.tenant_configs SELECT config_key, config_value FROM tenant_configs WHERE tenant_id = (SELECT id FROM tenants WHERE code = 'ex') AND config_group = 'ai'; ``` ### API Key 管理规范(强制) ```python # ❌ 禁止:代码中硬编码 API Key primary_api_key = "sk-V9Qfx..." # ❌ 禁止:查询租户数据库的本地表 SELECT * FROM ai_config -- 错误!应查管理库 # ✅ 正确:从管理库加载,fallback 到环境变量 primary_api_key = await load_from_admin_db("AI_PRIMARY_API_KEY") if not primary_api_key: primary_api_key = os.getenv("AI_PRIMARY_API_KEY", "") ``` **敏感配置管理**: - 敏感配置统一存储在管理库 `tenant_configs` 表 - `.env` 文件仅存储数据库连接信息,权限设置为 600 - 更新配置后重启容器:`docker compose up -d --force-recreate` ### 默认模型规范 ```python # ✅ 正确:遵循"优先最强"原则 DEFAULT_MODEL = "claude-opus-4-5-20251101-thinking" # 默认使用最强模型 # ❌ 错误:使用保底模型作为默认值 DEFAULT_MODEL = "gemini-3-flash-preview" # 这是最弱的保底模型 ``` **模型常量命名**: ```python MODEL_PRIMARY = "claude-opus-4-5-20251101-thinking" # 🥇 首选 MODEL_STANDARD = "gemini-3-pro-preview" # 🥈 标准 MODEL_FAST = "gemini-3-flash-preview" # 🥉 快速/保底 ``` --- ## Nginx配置 ### SPA缓存策略 ```nginx location / { try_files $uri $uri/ /index.html; # HTML不缓存 add_header Cache-Control "no-cache, no-store, must-revalidate" always; } location /assets/ { # 静态资源长期缓存(带hash) expires 1y; add_header Cache-Control "public, immutable"; } ``` ### 静态文件代理 ```nginx location /static/uploads/ { proxy_pass http://kaopeilian-backend-dev:8000; } ``` --- ## 检查清单 ### 新功能开发 - [ ] 静态资源使用相对路径 - [ ] 页面动态数据从API获取,无硬编码占位符 - [ ] API响应正确访问`res.code`/`res.data` - [ ] request.get参数包装为`{ params }` - [ ] AI调用通过AIService并传db_session - [ ] 无硬编码ID默认值 ### 多租户排查 - [ ] 确认租户数据库:`docker inspect <租户>-backend | grep DATABASE` - [ ] 检查数据是否在正确的库中 - [ ] 确认ID在该租户数据库存在 ### 部署后验证 - [ ] 清除浏览器缓存测试 - [ ] 检查JS文件hash是否匹配 - [ ] 检查静态资源能否访问