- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
43 KiB
联调经验与方法(长期维护)
目的:沉淀可直接复用的前后端联调经验、统一的原则、方法与经验,支持本地开发环境(localhost)全链路“真落库、真查库”对接。本文不记录按轮次的事件,仅保留可执行的经验与规范。
一、环境与配置基线(本地)
- 端口:后端 8000,前端 3001,MySQL 3306,Redis 6379
- 数据库 DSN(Python/SQLAlchemy):
mysql+aiomysql://root:Kaopeilian2025!@#@localhost:3306/kaopeilian?charset=utf8mb4 - 前端环境(
.env.development必备):VITE_API_BASE_URL=http://localhost:8000VITE_WS_BASE_URL=ws://localhost:8000VITE_USE_MOCK_DATA=false
- CORS:包含
http://localhost:3001,允许 headers/methods/credentials - 运行模式:开发与测试仅考虑本机
localhost;启用代码自动重载
Docker化开发环境(推荐)
- 架构选择:混合架构 - 数据库用Docker,应用用本地开发
- 优势:保证数据环境一致性,同时保持开发灵活性
- Docker服务:
# 启动基础服务 docker-compose -f docker-compose.dev.yml up -d mysql-dev redis-dev # 服务状态检查 docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - 环境变量配置:
export DATABASE_URL="mysql://root:Kaopeilian2025!@#@localhost:3306/kaopeilian" export REDIS_URL="redis://localhost:6379/0"
二、标准化联调步骤(落地版)
- 启动依赖与服务(推荐混合架构)
# 启动Docker化的数据库服务
docker-compose -f docker-compose.dev.yml up -d mysql-dev redis-dev
# 启动后端服务(本地开发,支持热重载)
cd kaopeilian-backend
source venv/bin/activate
export DATABASE_URL="mysql://root:Kaopeilian2025!@#@localhost:3306/kaopeilian"
export REDIS_URL="redis://localhost:6379/0"
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# 启动前端服务(本地开发,支持热重载)
cd kaopeilian-frontend
npm run dev
或者使用一键启动脚本:
./start-dev.sh # 完全Docker化方案
- 健康检查与基础验证
curl -s http://localhost:8000/health
- 启动前端(确保关闭 mock)
cd kaopeilian-frontend
npm run dev -- --host --port 3001
- 快速端到端核查
# 登录拿 Token
curl -s http://localhost:8000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"testuser","password":"TestPass123!"}' > /tmp/login.json
ACCESS=$(python3 -c "import json;print(json.load(open('/tmp/login.json'))['data']['token']['access_token'])")
# 访问课程列表
curl -s -H "Authorization: Bearer $ACCESS" \
"http://localhost:8000/api/v1/courses?page=1&size=12" | python3 -c \
"import sys,json;o=json.load(sys.stdin);print(len(o.get('data',{}).get('items',[])))"
- 浏览器 Network + Console 必核项
- 关键请求应命中
/api/v1/auth/login、/api/v1/courses且 200 - 控制台确认“使用模拟数据: false”;无 CORS 报错、无未捕获异常
三、API 契约与路由规范
- 路径一致性:前后端路径严格一致,避免契约不匹配
- 后端标准:
/api/v1/{module}/{action} - 前端(开发环境):
/api相对路径通过 Vite 代理转发到后端 - 前端(生产/预发):
${VITE_API_BASE_URL}/api/v1/{module}/{action}构建完整 URL
- 后端标准:
- 路由聚合:仅在子路由内定义
prefix,聚合时不要重复加前缀 - 路由注册:新增模块必须在
app/api/v1/__init__.py注册(如exam_router) - 临时 API 重定向(仅联调过渡):需有 TODO 注记;处理结构不匹配;尽快下线
分页规范
- 入参:前端
page/size→ 后端PaginationParams(page, page_size) - 返回:
PaginatedResponse{items,total,page,page_size,pages}
四、枚举与数据模型
- SQLAlchemy Enum:按
.value存储;validate_strings=True;values_callable=lambda e: [i.value for i in e] - Pydantic:枚举字段加
field_validator(mode='before'),兼容名称/值及大小写 - 字段命名映射:前端驼峰 ↔ 后端下划线,保存/加载需明确映射表
五、字符集与编码
- MySQL:库/表
utf8mb4+utf8mb4_unicode_ci;会话强制client/connection/results=utf8mb4 - 后端 JSON:统一自定义响应
ensure_ascii=False(UTF-8 输出) - 前端/代理:请求头显式
application/json; charset=utf-8;必要时在代理补充charset=utf-8 - 存量数据:如发现乱码,使用
utf8mb4连接重新更新存量记录
六、认证与权限
- 受保护接口必须携带
Authorization: Bearer <access_token> - Token 刷新统一由拦截器/守卫处理;失败清理本地状态并跳转登录
- 菜单/路由可见性需与守卫一致(如
canAccessRoute(path)),避免“展示但不可访问” - 登出约定:前端必须调用
POST /api/v1/auth/logout,并清理localStorage中access_token/refresh_token/current_user;路由守卫基于isAuthenticated()拦截未登录访问。
七、错误结构与日志
- 错误返回统一结构:
{ code, message, trace_id, data? } - 服务端日志:记录
trace_id/用户ID/方法/路径/状态码/耗时;异常保留完整堆栈(ERROR 级) - 客户端:区分处理 401/403/404/5xx 并统一提示;可选上报
八、前端数据源与 Mock 管理
- 默认关闭 Mock;若必须开启:
.env.*明确VITE_USE_MOCK_DATA=true- 在 PR 说明原因与风险;上线前必须回归真实后端
- 请求实现建议:统一请求封装;响应采用
response.text() + JSON.parse以规避编码歧义
九、浏览器联调核查清单
- Network:路径/方法/状态码/耗时/响应结构逐项核对
- 无 CORS/未捕获异常
- 空列表/空字段/错误码时 UI 表现与接口约定一致
- 关键路径 404 时优先排查:路由注册 → 聚合 prefix → 代理路径
十、质量与性能(轻量)
- 合并前:后端
black/flake8/pytest,前端eslint通过 - 核心 API 本机 1-2 分钟轻压测(如
autocannon/k6),关注 P95 与慢查询
十一、常用命令速查
# 启动依赖
docker-compose -f docker-compose.dev.yml up -d mysql redis
# 数据迁移
alembic upgrade head
# 后端运行
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# 后端运行(等价方式,便于快速验证)
python app/main.py
# 前端运行
npm run dev -- --host --port 3001
# 端口占用
lsof -i :8000 || true
lsof -i :3001 || true
# 后端单测(仅运行主应用测试用例)
pytest tests/test_main.py
后端运行与测试(最小流程,新增)
- 启动后端(二选一):
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000python app/main.py
- 运行基础单测:
pytest tests/test_main.py - 仅使用本机
localhost验证,无需考虑外网 IP。
十二点五、登出联调核查(新增)
- 点击“退出登录”后应:
- 命中
POST /api/v1/auth/logout返回 200 localStorage中access_token/refresh_token/current_user被清空- 页面跳转到
/login,刷新后仍需登录 - 访问受保护路由被
beforeEach守卫重定向至登录
- 命中
十二、业务契约基线(可复用)
1) 考试模块
- 路由建议:
/api/v1/exams/start、/api/v1/exams/submit、/api/v1/exams/{id}、/api/v1/exams/records、/api/v1/exams/statistics/summary - 动态考试/错题/推荐等子域路由需评审后统一落库与实现
2) 用户中心(个人信息)
- 页面与接口:
/user/profile↔GET/PUT /api/v1/users/me - 字段映射:
name↔full_name、avatar↔avatar_url - 可写字段:
email/phone/full_name/avatar_url/bio/gender/school/major - 统计接口:
GET /api/v1/users/me/statistics - 新增字段(2025-09-22):
school(学校)、major(专业)已添加到users表,支持完整的读写操作
2.1) 用户团队管理(新增,2025-09-22)
- 前端获取团队下拉:
GET /api/v1/teams(登录可用) - 用户团队变更:使用用户-团队关联接口,而不是提交
department字段- 加入团队:
POST /api/v1/users/{user_id}/teams/{team_id}?role=member|leader - 移出团队:
DELETE /api/v1/users/{user_id}/teams/{team_id}
- 加入团队:
- 前端表单字段:内部使用
team: number|null(团队ID),保存时:先更新基础信息,再对比并调用增删团队接口,确保"真落库、真查库" - 清理:删除页面内固定团队枚举(如"销售一组/二组/三组/客服组")的硬编码,改为从后端动态加载
2.2) 用户岗位管理(新增,2025-09-22)
- 前端获取岗位下拉:
GET /api/v1/admin/positions(带分页,page_size=1000获取全部)- 如果分页请求返回空数据,尝试不带参数请求以兼容不同实现
- 获取用户当前岗位:
GET /api/v1/users/{user_id}/positions - 用户岗位变更:使用岗位-成员关联接口,而不是直接更新用户字段
- 添加岗位成员:
POST /api/v1/admin/positions/{position_id}/members,body:{ "user_ids": [userId1, userId2] } - 移除岗位成员:
DELETE /api/v1/admin/positions/{position_id}/members/{user_id}
- 添加岗位成员:
- 实现问题修复(2025-09-22):
- 问题:PositionMember模型创建时使用了不存在的
created_by字段导致500错误 - 原因:PositionMember继承了BaseModel和SoftDeleteMixin,但未继承AuditMixin
- 解决:移除API中对
created_by和updated_by字段的引用 - 添加成员到岗位:
POST /api/v1/admin/positions/{position_id}/members,body:{ "user_ids": [user_id] } - 从岗位移除成员:
DELETE /api/v1/admin/positions/{position_id}/members/{user_id}
- 问题:PositionMember模型创建时使用了不存在的
- 前端表单字段:内部使用
position: number|null(岗位ID),保存时:先更新基础信息,再对比当前岗位并调用增删接口 - 实现逻辑:
- 编辑用户时,并行加载用户详情、团队信息和岗位信息
- 保存时先调用
updateUser更新基础信息(姓名、邮箱、电话等) - 对比选中岗位与现有岗位,计算需要移除和添加的岗位
- 调用相应的添加/移除API完成岗位同步
3) 岗位管理(扩展)
- 成员管理:
GET/POST/DELETE /api/v1/admin/positions/{id}/members - 课程管理:
GET/POST/PUT/DELETE /api/v1/admin/positions/{id}/courses - 关联表:
position_members、position_courses(区分必修/选修、支持优先级/软删)
4) 课程编辑
- 基本信息:
POST/PUT /api/v1/courses/{id};状态枚举 draft/published/archived;分类枚举 technology/management/business/general - 删除策略(更新):任意状态可删除(软删),注意权限与日志审计;删除需
admin/manager - 考试设置:
GET/POST /api/v1/courses/{id}/exam-settings(前端驼峰 ↔ 后端下划线) - 岗位分配:
GET/POST/DELETE /api/v1/courses/{id}/positions - 文件资料(预留):
POST /api/v1/courses/{id}/materials;知识点 AI 分析后续接入
4.1) 编辑课程页 - 资料模块接入真实后端(2025-09-22)
- 前端页面:
/manager/edit-course/:id加载资料改为调用真实接口:- 列表:
GET /api/v1/courses/{id}/materials→[{ id, name, file_size, created_at, ... }] - 删除:
DELETE /api/v1/courses/{id}/materials/{material_id}(需管理员权限)
- 列表:
- 后端新增路由:
GET /api/v1/courses/{id}/materials(查询,过滤is_deleted=false,按sort_order,id排序)DELETE /api/v1/courses/{id}/materials/{material_id}(软删,记录deleted_at,保留审计日志)
- 服务层实现:在
course_service增加get_course_materials与delete_course_material,统一课程存在性校验;查询/删除均过滤软删。 - 初始化SQL校验:
course_materials表结构已存在,字段包含is_deleted/deleted_at,无需结构变更。 - 前端实现要点:
- 移除本地模拟
materialList,改为courseApi.getMaterials(id)结果映射 - 删除操作调用
courseApi.deleteMaterial(id, materialId),成功后从本地列表移除并刷新知识点汇总 - 错误处理:弹窗确认 + 失败提示;Console 记录详细错误
- 移除本地模拟
- 验收要点:
- Network 中资料列表/删除均 200,返回结构符合
ResponseModel - 刷新页面后资料仍与数据库一致(真落库、真查库)
- 软删记录不会在列表中出现
- 权限控制:非管理员删除返回 403/错误码
- 日志:服务端 INFO/ERROR 记录完整上下文(course_id/material_id/user_id)
- Network 中资料列表/删除均 200,返回结构符合
4.2) 编辑课程页 - 考试设置联调(2025-09-22更新)
- 后端接口:
GET /api/v1/courses/{id}/exam-settings、POST /api/v1/courses/{id}/exam-settings、PUT /api/v1/courses/{id}/exam-settings - 写入权限:从"仅管理员"调整为"管理员或经理"(admin/manager)可写,学员可读
- 前端成功判断:统一按
code===200成功;POST返回201 Created但响应体中的code仍为200 - 字段映射:
- 前端:
singleChoice/multipleChoice/trueOrFalse/fillInBlank/duration/difficulty/enabled - 后端:
single_choice_count/multiple_choice_count/true_false_count/fill_blank_count/duration_minutes/difficulty_level/is_enabled
- 前端:
- 前端数据加载问题修复(2025-09-22):
- 问题:页面加载时如果API返回null,前端显示硬编码的默认值,导致看起来"保存不生效"
- 解决:在
loadCourseData中区分处理有数据和无数据的情况,确保显示真实的后端数据 - 调试:添加console.log在加载和保存时输出数据,便于排查问题
- 验收要点:
- 保存成功后再次 GET 返回与表单一致(真落库)
- 权限不足写入返回 403;日志包含 user_id/course_id
- 读取不存在设置返回
data:null,首次 POST 可创建 - 刷新页面后数据必须与数据库一致,不能显示默认值
4.3) 文件上传功能实现(2025-09-22)
- 后端实现:
- 新增
/api/v1/upload.py模块,实现文件上传接口 - 通用上传:
POST /api/v1/upload/file(支持分类上传) - 课程资料上传:
POST /api/v1/upload/course/{course_id}/materials - 文件删除:
DELETE /api/v1/upload/file(通过file_url删除)
- 新增
- 存储路径配置:
- 基础路径:
/kaopeilian-backend/uploads/ - 课程资料:
/kaopeilian-backend/uploads/courses/{course_id}/ - 配置方式:在
config.py中添加UPLOAD_PATH属性,动态计算绝对路径
- 基础路径:
- 静态文件服务:
- 在
app/main.py中挂载静态文件目录:app.mount("/static/uploads", StaticFiles(directory=upload_path), name="uploads") - 访问路径:
http://localhost:8000/static/uploads/courses/{course_id}/{filename}
- 在
- 前端实现:
- 更新
confirmUpload函数,从模拟上传改为调用真实接口 - 使用
request.upload()方法上传文件,支持进度回调 - 上传成功后调用
courseApi.addMaterial将文件信息保存到数据库 - 添加加载动画和错误处理
- 更新
- 数据库记录:
- 上传文件后,需要调用
POST /api/v1/courses/{course_id}/materials创建资料记录 - 字段包括:
name、file_url、file_type、file_size、description
- 上传文件后,需要调用
- 权限与验证:
- 上传需要登录认证(Bearer Token)
- 课程资料上传时验证课程是否存在
- 文件类型限制:pdf、doc、docx、ppt、pptx、xls、xlsx、txt、md等
- 文件大小限制:50MB
- 调试要点:
- SQLAlchemy查询改用异步语法,避免直接执行原生SQL
- 登录响应的token结构:
data.token.access_token(嵌套对象) - 前端需要判断是否在编辑模式,新建课程时不能上传文件
- 文件类型不匹配:上传接口支持txt/md等格式,但
CourseMaterialCreate模型限制只支持特定格式- 上传API支持:pdf、doc、docx、ppt、pptx、xls、xlsx、txt、md、zip、mp4、mp3、png、jpg、jpeg
- 资料模型支持:pdf、doc、docx、ppt、pptx、xls、xlsx、mp4、mp3、zip(不支持txt、md、图片格式)
- 解决方案:统一文件类型支持,或在前端限制可上传的文件类型
- 验收标准:
- 文件成功上传到指定目录
- 数据库中创建对应的资料记录
- 通过静态文件URL可以访问上传的文件
- 刷新页面后资料列表正确显示
- 删除资料时文件和数据库记录同步删除
- 文件删除功能增强(2025-09-22):
- 删除资料时同步删除物理文件:
- 在
delete_course_material方法中添加删除文件逻辑 - 从
file_url提取相对路径,删除对应文件 - 文件删除失败不影响业务流程,仅记录日志
- 在
- 删除课程时删除整个文件夹:
- 在
delete_course方法中添加删除文件夹逻辑 - 使用
shutil.rmtree删除uploads/courses/{course_id}/目录 - 文件夹删除失败不影响业务流程,仅记录日志
- 在
- 实现要点:
- 使用
Path对象处理文件路径,避免路径拼接错误 - 删除前检查文件/文件夹是否存在
- 物理删除操作放在数据库事务提交后执行
- 异常处理确保不影响主业务流程
- 使用
- 删除资料时同步删除物理文件:
4.4) 编辑课程页 - 岗位分配接入真实后端(2025-09-22)
- 前端页面:
/manager/edit-course/:id岗位分配改为调用真实接口:- 列表:
GET /api/v1/courses/{id}/positions→CoursePositionAssignmentInDB[] - 批量分配:
POST /api/v1/courses/{id}/positions,body:[{ position_id, course_type:'required'|'optional', priority }] - 移除:
DELETE /api/v1/courses/{id}/positions/{position_id}
- 列表:
- 后端实现:使用
course_position_service.get_course_positions/batch_assign_positions/remove_position_assignment - 软删除规范:查询/删除均过滤
is_deleted=false;返回结构统一ResponseModel且成功code=200 - 缺陷修复(本次):
- 修复重复消费 SQLAlchemy Result 导致 500(
batch_assign_positions中仅调用一次scalar_one_or_none()) - 修复向
PositionCourse写入不存在的审计字段(该模型未继承AuditMixin,移除相关写入) - 修复 SQLAlchemy 异步懒加载错误:
get_course_positions中访问assignment.position.members触发MissingGreenlet错误- 原因:在异步上下文中访问懒加载的关系属性
- 解决:暂时返回
member_count=0,避免访问懒加载关系
- 修复重复消费 SQLAlchemy Result 导致 500(
- 前端实现要点:
courseApi.assignPositions(courseId, assignments)直接提交 assignments 数组- 成功后刷新
courseApi.getPositions,按course_type分流到“必修/选修”
- 验收要点:
- Network 中批量分配/移除均 200;返回结构符合
ResponseModel - 刷新页面后岗位分配与数据库一致(真落库、真查库)
- 失败提示清晰;Console 记录详细错误
- Network 中批量分配/移除均 200;返回结构符合
十三、前端表单开发规范
- 表单字段类型必须与后端一致;用
null表示空值,避免空字符串 - 数字字段不要初始化为字符串;
el-select的value类型与v-model一致 - 典型映射:
parentId(null)↔parent_id;编辑态需做类型与命名映射
十四、经验法则与常见坑(速查)
- 路由重复前缀导致 404:子路由已带
prefix时,聚合处不要再叠加 - 分页字段不一致:统一使用
page/page_size(前端传size→ 后端转换) - 编码问题:MySQL 会话与 JSON 输出统一 UTF-8,历史乱码需用正确连接重写
- 契约漂移:前端改路径或结构前先与后端确认;临时重定向需尽快下线
- API 响应结构:统一
items而非混用list,避免前端适配分歧 - 角色校验实现:避免枚举强依赖,字符串角色比较更稳健
- 模块导入错误:
ModuleNotFoundError通常是因为__init__.py中导入了不存在的模块,需要注释掉或创建对应文件 - 软删除未过滤:实现软删除功能时,列表查询必须添加
where(Model.is_deleted == False)过滤条件,否则已删除数据仍会显示 - SQLAlchemy 异步懒加载错误:在异步上下文中访问懒加载关系会导致
MissingGreenlet错误- 错误信息:
greenlet_spawn has not been called; can't call await_only() here - 解决方案:使用
selectinload预加载关系,或避免访问懒加载属性 - 示例:
len(assignment.position.members)改为返回默认值或通过单独查询获取
- 错误信息:
- 前端默认值覆盖问题:页面加载时使用硬编码默认值,导致看起来"保存不生效"
- 症状:修改数据后刷新页面,显示的还是默认值而非数据库中的值
- 原因:前端初始化了默认值,但加载数据时没有正确处理API返回null的情况
- 解决:在数据加载函数中明确区分有数据和无数据的情况,确保总是显示真实的后端数据
- 数据库事务未提交问题:SQLAlchemy异步会话可能不会自动提交事务
- 症状:API返回成功,但数据库中没有记录;文件上传成功但前端不显示
- 原因:
get_db()依赖注入只yield session,没有在正常流程中commit - 解决:修改
deps.py中的get_db()函数,在yield后添加await session.commit() - 代码示例:
async def get_db(): async with AsyncSessionLocal() as session: try: yield session await session.commit() # 确保事务提交 except Exception: await session.rollback() raise finally: await session.close()
十五、数据库增量更新规范
- 字段添加脚本:创建独立的Python脚本(如
add_school_major_fields.py)- 使用aiomysql异步执行SQL,兼容项目配置
- 检查字段是否已存在,避免重复添加
- 执行后显示表结构,确认更新成功
- 更新顺序:
- 先执行增量脚本更新现有数据库
- 更新初始化SQL脚本(
init_database_unified.sql) - 同步更新数据库架构文档
- 记录到联调经验和规范文档
十六、维护与同步约定
- 若调整结构/架构/配置/运行逻辑/路径/日志路径:请同步更新
README.md - 若修改数据库结构/字段/描述:同步更新
/kaopeilian-backend/scripts/init_database_unified.sql/kaopeilian-backend/数据库架构-统一版.md
- 每次联调完成后,同步以下文档(删除错误内容,增补经验):
考培练系统规划/全链路联调/实操联调完整Todos清单.md考培练系统规划/全链路联调/联调经验汇总.md(本文)考培练系统规划/全链路联调/规范与约定-团队基线.md
十六点五、岗位管理联调记录(2025-09-22新增)
- 前端页面:
/admin/position-management已改为“真落库、真查库”。 - 后端接口:使用
GET/POST/PUT/DELETE /api/v1/admin/positions/*真实路由(positions.py)。 - 响应契约:统一
ResponseModel,成功code=200;前端已从判断code===0改为code===200。 - 分页参数:前端使用
page/size,后端用PaginationParams(page, page_size)统一转换;课程列表调用改为size。 - 课程分配:前端调用
POST /api/v1/admin/positions/{id}/courses与DELETE /api/v1/admin/positions/{id}/courses/{course_id};取消本地假删,改为直连接口。 - 成员管理:前端调用
GET/DELETE /api/v1/admin/positions/{id}/members显示/移除成员,按code===200处理。 - 软删除过滤:后端查询岗位列表、岗位树时必须过滤
is_deleted=true的记录;详情/更新/删除接口需检查岗位是否已删除。
8. 考试设置保存不生效问题(2025-09-22)
问题描述
用户反馈考试设置保存后不生效,页面刷新后数据恢复到之前的值。
问题原因
- 后端API错误:获取课程基本信息时返回500错误,导致整个数据加载流程中断
- 前端默认值问题:当后端返回的考试设置数据为null时,前端会使用硬编码的默认值
- 数据不同步:页面显示的"当前设置"与实际表单数据不一致
解决方案
-
修复后端500错误
- 检查具体的错误日志:
tail -f kaopeilian-backend/logs/backend.log - 验证数据库连接是否正常
- 确保课程ID存在且数据完整
- 检查具体的错误日志:
-
优化前端数据加载
// 当课程基本信息加载失败时,停止后续加载 if (courseRes.code !== 200) { ElMessage.error('课程信息加载失败,请刷新页面重试') return } -
确保数据同步
- 保存成功后主动刷新数据:已有代码实现(第1316-1327行)
- 页面"当前设置"应实时反映表单数据
经验总结
- 级联错误处理:当基础API失败时,应阻止后续API调用,避免使用错误的默认值
- 数据一致性:前端显示的数据必须与实际的数据模型保持同步
- 错误提示:API失败时应给出明确的错误提示,帮助快速定位问题
后续发现:前端数据绑定问题(2025-09-22)
问题现象: 用户反馈考试设置修改后保存仍不生效,经过深入调试发现:
- API请求正常发送(POST /api/v1/courses/6/exam-settings)
- 后端成功处理并返回201状态码
- 数据库写入成功(updated_at时间戳更新)
- 但发送的数据仍是旧值,页面修改未同步到Vue数据模型
根本原因: 前端输入框的修改没有正确触发Vue的响应式数据更新,导致虽然页面显示修改了,但实际的examSettings对象仍是旧值。
调试方法:
// 检查输入框实际值vs Vue数据模型
const input = document.querySelector('input[type="number"]');
console.log('输入框值:', input.value); // 显示用户修改的值
console.log('Vue数据:', ctx.examSettings.singleChoice); // 显示实际发送的值
解决方案: 需要检查v-model绑定或事件监听器,确保输入框变化能正确更新Vue响应式数据。
9. 知识点管理功能联调问题(2025-09-22)
问题描述
编辑课程页面的学习资料与知识点管理中,添加知识点时点击保存会提示"保存知识点失败",控制台显示500错误。
问题原因
- 模型继承不完整:
KnowledgePoint模型只继承了BaseModel和SoftDeleteMixin,缺少AuditMixin - 数据库字段缺失:
knowledge_points表缺少created_by和updated_by字段 - 服务层字段不匹配:服务层尝试传递
created_by参数,但模型中没有对应字段
错误信息
TypeError: 'created_by' is an invalid keyword argument for KnowledgePoint
解决方案
-
更新模型继承
class KnowledgePoint(BaseModel, SoftDeleteMixin, AuditMixin): -
数据库表结构更新
ALTER TABLE knowledge_points ADD COLUMN created_by INT NULL COMMENT '创建人ID' AFTER deleted_at; ALTER TABLE knowledge_points ADD COLUMN updated_by INT NULL COMMENT '更新人ID' AFTER created_by; -
同步更新初始化SQL脚本
- 在
init_database_unified.sql中为knowledge_points表添加审计字段
- 在
验证结果
curl -X POST "http://localhost:8000/api/v1/courses/6/knowledge-points" \
-H "Authorization: Bearer <token>" \
-d '{"name":"测试知识点","description":"测试内容","is_required":true}'
# 返回 200 成功
经验总结
- 模型一致性:确保所有需要审计功能的模型都继承
AuditMixin - 数据库同步:模型变更后需同时更新数据库表结构和初始化脚本
- 错误诊断:500错误要查看后端日志获取具体的TypeError信息
- 渐进式修复:先修复模型定义,再更新数据库,最后验证API功能
10. Dify知识点拆解工作流集成(2025-09-23)
功能描述
实现课程资料上传后自动触发Dify工作流进行知识点拆解,以及手动重新分析功能。
实现内容
10.1 后端实现
-
知识点分析服务 (
app/services/ai/knowledge_analysis.py)- 创建
KnowledgeAnalysisService类 - 实现
analyze_course_material()方法分析单个资料 - 实现
reanalyze_course_materials()方法重新分析所有资料 - 集成Dify工作流API调用
- 创建
-
API接口 (
app/api/v1/knowledge_analysis.py)POST /api/v1/courses/{course_id}/materials/{material_id}/analyze- 分析单个资料POST /api/v1/courses/{course_id}/reanalyze- 重新分析课程所有资料- 使用
BackgroundTasks实现异步处理
-
自动触发机制 (
app/api/v1/courses.py)- 修改
add_course_material接口,上传资料后自动触发知识点分析 - 使用后台任务避免阻塞用户操作
- 修改
10.2 前端实现
-
重新分析按钮 (
kaopeilian-frontend/src/views/manager/edit-course.vue)- 在学习资料与知识点管理页面添加"重新分析"按钮
- 实现
reanalyzeAllMaterials()方法调用后端API - 添加加载状态和用户反馈
-
上传状态优化
- 上传成功后显示"正在后台分析知识点..."提示
- 资料状态从
pending→analyzing→completed
10.3 Dify工作流配置
- API服务器:
http://dify.ireborn.com.cn/v1 - API密钥:
app-LZhZcMO6CiriLMOLB2PwUGHx - 参数格式:
{ "inputs": { "file": [ { "type": "document", "transfer_method": "remote_url", "url": "http://localhost:8000/static/uploads/courses/1/filename.pdf" } ], "examsTitle": "课程标题", "examsId": "课程ID" }, "response_mode": "blocking", "user": "system_user_1" }
关键问题与解决方案
10.4 日志记录问题
问题: 使用关键字参数的日志记录导致 _log() got an unexpected keyword argument 错误
原因: 项目中混用了 logging 和 structlog,前者不支持关键字参数
解决: 统一使用字符串格式化的日志记录
# 错误写法
logger.info("消息", course_id=course_id, user_id=user_id)
# 正确写法
logger.info(f"消息 - course_id: {course_id}, user_id: {user_id}")
10.5 Dify API参数格式问题
问题: {"code":"invalid_param","message":"file in input form must be a list of files"}
原因: Dify工作流期望 file 参数为文件列表格式,而不是单个URL字符串
解决: 按照Dify API文档格式传递文件参数
10.6 网络连接问题
问题: 500 Internal Privoxy Error - 代理服务器阻止了对Dify服务的访问
解决: 将 dify.ireborn.com.cn 添加到代理的no proxy列表中
验证测试
-
API测试:
curl -X POST "http://localhost:8000/api/v1/courses/1/reanalyze" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" # 返回: {"code":200,"message":"重新分析任务启动成功",...} -
Dify文件上传测试:
curl -X POST "http://dify.ireborn.com.cn/v1/files/upload" \ -H "Authorization: Bearer app-LZhZcMO6CiriLMOLB2PwUGHx" \ -F "file=@test.pdf" -F "user=test_user" # 返回: {"id":"file-id","name":"test.pdf",...} -
功能验证:
- ✅ 上传资料后自动触发知识点分析
- ✅ 重新分析按钮正常工作
- ✅ 后台任务异步执行不阻塞用户操作
- ✅ 文件成功上传到Dify服务器
- ❌ Dify工作流文件验证失败(需要检查工作流配置)
10.7 Dify工作流文件验证问题
问题: {"code":"invalid_param","message":"File validation failed for file: filename.pdf"}
分析:
- 文件成功上传到Dify服务器并获得文件ID
- 工作流调用时文件验证失败
- 测试了PDF和TXT格式都失败
可能原因:
- Dify工作流对文件内容有特定格式要求
- 工作流配置中的文件类型限制
- 文件大小或编码问题
- 工作流版本或权限问题
建议解决方案:
- 检查Dify工作流的具体配置和文件要求
- 联系工作流管理员确认文件格式要求
- 尝试使用工作流推荐的示例文件进行测试
- 检查工作流是否需要特定的文件结构或元数据
经验总结
- 异步处理: 使用
BackgroundTasks实现耗时操作的异步处理 - 错误处理: 外部服务调用需要完善的错误处理和重试机制
- 日志规范: 统一项目中的日志记录方式,避免混用不同的日志库
- API文档: 严格按照第三方服务的API文档格式传递参数
- 网络配置: 开发环境需要正确配置代理和防火墙规则
2025-09-23 知识点分析工作流参数调整
问题描述
Dify工作流需要调整参数结构,增加 material_id 参数以支持资料与知识点的精确关联。
解决方案
-
参数结构调整:
- 原参数:
examsTitle,examsId - 新参数:
course_name,course_id,material_id
- 原参数:
-
代码修改:
- 更新
_call_dify_workflow方法签名,增加material_id参数 - 修改工作流调用payload,使用新的参数名称
- 更新日志记录,包含所有必要参数信息
- 更新
-
文档更新:
- 更新工作流文档中的参数说明
- 添加详细的JSON参数格式示例
- 更新实现经验部分
技术要点
- 参数映射: 确保后端参数与Dify工作流期望的参数名称一致
- 向后兼容: 保持API接口的向后兼容性
- 日志完整性: 记录所有关键参数便于调试和监控
11. 数据库从本地切换到公网配置(2025-09-23)
配置信息
公网数据库地址:
- 主机:
120.79.247.16或aiedu.ireborn.com.cn - 端口:
3306 - 数据库名:
kaopeilian - 用户:
root - 密码:
Kaopeilian2025!@#
实施步骤
-
密码URL编码
- 原始密码:
Kaopeilian2025!@# - URL编码后:
Kaopeilian2025%21%40%23 - 特殊字符编码:
!→%21,@→%40,#→%23
- 原始密码:
-
更新配置文件
start_mysql.py:更新数据库连接字符串docker-compose.dev.yml:更新环境变量- 创建
start_remote.py:专门用于公网数据库启动 - 创建
test_remote_db.py:测试连接脚本
-
SQLAlchemy连接字符串
mysql+aiomysql://root:Kaopeilian2025%21%40%23@120.79.247.16:3306/kaopeilian?charset=utf8mb4
启动方式
# 方式1:使用专用启动脚本
python3 start_remote.py
# 方式2:设置环境变量后启动
export DATABASE_URL="mysql+aiomysql://root:Kaopeilian2025%21%40%23@120.79.247.16:3306/kaopeilian?charset=utf8mb4"
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# 方式3:使用Docker Compose(已更新配置)
docker-compose -f docker-compose.dev.yml up backend
注意事项
- 密码编码:URL中的特殊字符必须进行编码,否则会导致连接失败
- 网络延迟:公网数据库可能有一定延迟,开发时需要考虑
- 安全性:生产环境应使用环境变量或密钥管理服务,避免硬编码密码
- 切换回本地:如需使用本地数据库,修改连接字符串为:
mysql+aiomysql://root:root@localhost:3306/kaopeilian?charset=utf8mb4
验证方法
# 测试数据库连接
python3 test_remote_db.py
# 检查服务健康状态
curl http://localhost:8000/health
# 测试登录功能
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
经验总结
- URL编码必须:密码中的特殊字符不编码会导致SQLAlchemy解析失败
- 连接测试优先:更改配置前先用独立脚本测试连接,确保网络通畅
- 多种启动方式:提供多种配置方式,方便不同场景切换
- 文档更新同步:配置变更后及时更新README和相关文档
十三、Dify SQL执行器API集成
背景需求
为了让Dify平台能够直接查询和操作考陪练系统数据库,开发了专门的SQL执行器API接口。该接口支持:
- 执行查询和写入SQL操作
- 参数化查询防止SQL注入
- 获取表结构和表列表
- SQL语句验证
实现要点
-
API设计
- 主端点:
/api/v1/sql/execute - 认证方式:JWT Bearer Token
- 支持查询(SELECT/SHOW/DESCRIBE)和写入(INSERT/UPDATE/CREATE等)
- 主端点:
-
关键代码结构
# app/api/v1/sql_executor.py @router.post("/execute") async def execute_sql( request: Dict[str, Any], current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): sql = request.get('sql', '').strip() params = request.get('params', {}) # 执行SQL并返回结果 -
安全措施
- 需要用户认证
- 支持参数化查询
- SQL操作日志记录
- 危险操作警告提示
集成步骤
-
创建API文件
# 创建 app/api/v1/sql_executor.py # 实现execute、validate、tables、schema等接口 -
注册路由
# app/api/v1/__init__.py from .sql_executor import router as sql_executor_router api_router.include_router(sql_executor_router, prefix="/sql", tags=["sql-executor"]) -
配置数据库连接
# local_config.py os.environ["DATABASE_URL"] = "mysql+aiomysql://root:Kaopeilian2025!@%23@120.79.247.16:3306/kaopeilian?charset=utf8mb4"
Dify配置示例
# 自定义工具配置
方法: POST
URL: http://localhost:8000/api/v1/sql/execute
Headers:
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json
Body:
{
"sql": "{{sql}}",
"params": {{params}}
}
使用示例
-
查询操作
{ "sql": "SELECT * FROM courses WHERE category = :category", "params": {"category": "护肤"} } -
插入操作
{ "sql": "INSERT INTO knowledge_points (title, content) VALUES (:title, :content)", "params": { "title": "知识点标题", "content": "知识点内容" } }
响应格式
-
查询响应
{ "code": 200, "data": { "type": "query", "columns": ["id", "name"], "rows": [{...}], "row_count": 10 } } -
写入响应
{ "code": 200, "data": { "type": "execute", "affected_rows": 1, "success": true } }
注意事项
- 日期时间处理:使用自定义JSON编码器处理datetime对象
- 表结构查询:MySQL的DESCRIBE语句不支持参数化,需要验证表名
- 连接池配置:设置合适的pool_size和max_overflow
- 错误处理:执行失败时自动回滚事务
测试验证
# 运行测试脚本
python3 test_sql_executor.py
# 测试结果应包括:
# ✅ 登录成功
# ✅ 获取表列表成功
# ✅ SQL验证功能正常
# ✅ 查询和写入操作成功
经验总结
- 参数化查询必须:防止SQL注入,使用
:param_name格式 - 认证集成关键:Dify需要配置正确的Bearer Token
- 日志记录重要:记录所有SQL执行,便于审计和调试
- 测试先行原则:先用独立脚本测试,再集成到Dify
简化认证方案实现
为了解决JWT Token频繁过期的问题,实现了三种持久认证方案:
-
API Key认证(推荐)
- 端点:
/api/v1/sql/execute-simple - 请求头:
X-API-Key: dify-2025-kaopeilian - 优点:永不过期,配置简单
- 在
app/core/simple_auth.py中配置
- 端点:
-
长期Token认证
- 端点:
/api/v1/sql/execute-simple - 请求头:
Authorization: Bearer permanent-token-for-dify-2025 - 优点:符合标准Bearer格式,永不过期
- 端点:
-
Dify配置要点
- 鉴权类型:选择"请求头"
- 鉴权头部前缀:选择"Custom"(API Key)或"Bearer"(长期Token)
- 避免使用"无",即使是内部服务也应有基本认证
OpenAPI文档集成
- 规范版本:使用OpenAPI 3.1.0
- 认证配置:同时支持bearerAuth和apiKey
- 服务器配置:包含公网IP、本地开发和域名访问
- 文档格式:提供YAML和JSON两种格式
部署自动化
创建了完整的部署方案:
deploy/server_setup_guide.md:详细部署步骤deploy/quick_deploy.sh:一键部署脚本- 支持systemd服务管理
- 自动配置防火墙规则
经验总结
- 简化优先:内部服务使用API Key比JWT更实用
- 文档先行:OpenAPI文档让Dify集成更容易
- 自动化部署:脚本化减少人为错误
- 多重认证:提供多种认证方式增加灵活性
- 测试完备:本地测试通过后再部署生产
十、Docker化开发环境经验总结(2025-09-26新增)
混合架构方案(推荐)
经过实践验证,最佳开发环境架构为:
- 数据层Docker化:MySQL、Redis运行在容器中,确保环境一致性
- 应用层本地开发:前后端在本地运行,保持开发灵活性和热重载
- 优势:避免完全容器化的资源消耗,同时保证数据环境统一
快速启动流程
# 1. 启动Docker化数据库服务
docker-compose -f docker-compose.dev.yml up -d mysql-dev redis-dev
# 2. 启动后端服务(支持热重载)
cd kaopeilian-backend && source venv/bin/activate
export DATABASE_URL="mysql://root:Kaopeilian2025!@#@localhost:3306/kaopeilian"
export REDIS_URL="redis://localhost:6379/0"
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# 3. 启动前端服务(支持热重载)
cd kaopeilian-frontend && npm run dev
# 或使用一键启动脚本
./start-dev.sh
核心配置文件
- docker-compose.dev.yml - 开发环境Docker配置
- start-dev.sh - 一键启动脚本(支持多种模式)
- stop-dev.sh - 环境清理脚本
- Dockerfile.dev - 前后端开发环境镜像
- 开发环境使用指南.md - 详细使用文档
常见问题解决方案
前端依赖问题
# 问题:@rollup/rollup-darwin-arm64模块缺失
# 原因:npm可选依赖bug
# 解决:
cd kaopeilian-frontend
rm -rf node_modules package-lock.json
npm install
Docker网络问题
# 问题:镜像拉取超时
# 解决:预拉取或使用镜像加速
docker-compose -f docker-compose.dev.yml pull
端口冲突处理
# 检查端口占用
lsof -i :3001 :8000 :3306 :6379
# 批量清理进程
pkill -f vite && pkill -f uvicorn
热重载验证
# 后端测试(观察控制台重载信息)
echo "# test reload" >> kaopeilian-backend/app/main.py
# 前端测试(观察浏览器自动刷新)
echo "<!-- test reload -->" >> kaopeilian-frontend/src/App.vue
开发环境状态检查
# 检查Docker服务
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 检查应用服务
curl -s -w "前端: %{http_code}\n" http://localhost:3001/ -o /dev/null
curl -s -w "后端: %{http_code}\n" http://localhost:8000/health -o /dev/null
# 检查数据库连接
docker exec kaopeilian-mysql-dev mysqladmin ping -h localhost -u root -p
架构优势总结
- 混合架构最优:数据库容器化 + 应用本地化
- 热重载必备:大幅提升开发效率
- 脚本自动化:减少重复操作和人为错误
- 环境隔离:开发环境独立,不影响生产
- 问题预案:常见问题有标准解决流程