- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
8.2 KiB
8.2 KiB
考培练系统规范与约定(团队基线)
最后更新: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= 人名(张三、李四)- ❌ 不要存职位名称(资深美容顾问)
模拟数据
- 用户:轻医美行业常见中文姓名
- 学员示例:李美琳、王芳、陈静
前端规范
静态资源访问
// ✅ 正确:相对路径
const url = '/static/uploads/courses/1/file.pdf'
// ❌ 错误:硬编码
const url = `http://localhost:8000${path}`
API响应访问
const res = await getList()
// ✅ 正确
if (res.code === 200) { data.value = res.data }
// ❌ 错误
if (res.data.code === 200) { data.value = res.data.data }
request.get参数
// ✅ 正确
request.get(url, { params: { id: 1 } })
// ❌ 错误
request.get(url, { id: 1 })
页面动态数据获取
// ✅ 正确:从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调用前置检查
// 调用受限API前检查条件
if (userInfo.role !== 'trainee' || !userInfo.phone) {
ElMessage.warning('请先绑定手机号')
return
}
HTTP 客户端选择(2026-01-21 新增)
// ✅ 正确:普通 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/预览容器)
// ❌ 错误: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默认值
# ❌ 错误:硬编码
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, "未找到关联岗位")
业务异常处理
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路由顺序
# ✅ 正确:具体路由在前
@router.get("/mistakes") # 先定义
@router.get("/{exam_id}") # 后定义
Pydantic空字符串处理
@field_validator("category", mode="before")
def normalize(cls, v):
if isinstance(v, str) and not v.strip():
return CourseCategory.GENERAL # 空字符串→默认值
return v
AI服务规范
统一调用方式
# ✅ 正确:通过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 表
配置加载优先级:
- 管理库
tenant_configs表(按 TENANT_CODE 查询) - 环境变量(fallback)
- 代码默认值(仅用于开发)
容器必须的环境变量:
# .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
数据库配置表结构:
-- 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 管理规范(强制)
# ❌ 禁止:代码中硬编码 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
默认模型规范
# ✅ 正确:遵循"优先最强"原则
DEFAULT_MODEL = "claude-opus-4-5-20251101-thinking" # 默认使用最强模型
# ❌ 错误:使用保底模型作为默认值
DEFAULT_MODEL = "gemini-3-flash-preview" # 这是最弱的保底模型
模型常量命名:
MODEL_PRIMARY = "claude-opus-4-5-20251101-thinking" # 🥇 首选
MODEL_STANDARD = "gemini-3-pro-preview" # 🥈 标准
MODEL_FAST = "gemini-3-flash-preview" # 🥉 快速/保底
Nginx配置
SPA缓存策略
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";
}
静态文件代理
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是否匹配
- 检查静态资源能否访问