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

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

8.2 KiB
Raw Permalink Blame History

考培练系统规范与约定(团队基线)

最后更新2026-01-21 | 所有开发必须遵循


核心规范速查

规范 核心原则 检查项
静态资源 使用相对路径,禁止硬编码域名 无localhost、无IP、无端口
页面动态数据 从API获取禁止硬编码 无固定标题、名称等占位符
API响应 res.coderes.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 流式请求必须使用原生 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/预览容器)

// ❌ 错误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

配置加载优先级

  1. 管理库 tenant_configs 表(按 TENANT_CODE 查询)
  2. 环境变量fallback
  3. 代码默认值(仅用于开发)

容器必须的环境变量

# .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是否匹配
  • 检查静态资源能否访问