- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
319 lines
8.2 KiB
Markdown
319 lines
8.2 KiB
Markdown
# 考培练系统规范与约定(团队基线)
|
||
|
||
> 最后更新: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是否匹配
|
||
- [ ] 检查静态资源能否访问
|