Files
012-kaopeilian/docs/规划/全链路联调/规范与约定-团队基线.md
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

319 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 考培练系统规范与约定(团队基线)
> 最后更新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 流式请求必须使用原生 fetchAxios 不支持 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是否匹配
- [ ] 检查静态资源能否访问