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

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

43 KiB
Raw Permalink Blame History

联调经验与方法(长期维护)

目的沉淀可直接复用的前后端联调经验、统一的原则、方法与经验支持本地开发环境localhost全链路“真落库、真查库”对接。本文不记录按轮次的事件仅保留可执行的经验与规范。


一、环境与配置基线(本地)

  • 端口:后端 8000前端 3001MySQL 3306Redis 6379
  • 数据库 DSNPython/SQLAlchemymysql+aiomysql://root:Kaopeilian2025!@#@localhost:3306/kaopeilian?charset=utf8mb4
  • 前端环境.env.development 必备):
    • VITE_API_BASE_URL=http://localhost:8000
    • VITE_WS_BASE_URL=ws://localhost:8000
    • VITE_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"
    

二、标准化联调步骤(落地版)

  1. 启动依赖与服务(推荐混合架构)
# 启动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化方案
  1. 健康检查与基础验证
curl -s http://localhost:8000/health
  1. 启动前端(确保关闭 mock
cd kaopeilian-frontend
npm run dev -- --host --port 3001
  1. 快速端到端核查
# 登录拿 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',[])))"
  1. 浏览器 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=Truevalues_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=FalseUTF-8 输出)
  • 前端/代理:请求头显式 application/json; charset=utf-8;必要时在代理补充 charset=utf-8
  • 存量数据:如发现乱码,使用 utf8mb4 连接重新更新存量记录

六、认证与权限

  • 受保护接口必须携带 Authorization: Bearer <access_token>
  • Token 刷新统一由拦截器/守卫处理;失败清理本地状态并跳转登录
  • 菜单/路由可见性需与守卫一致(如 canAccessRoute(path)),避免“展示但不可访问”
  • 登出约定:前端必须调用 POST /api/v1/auth/logout,并清理 localStorageaccess_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

后端运行与测试(最小流程,新增)

  1. 启动后端(二选一):
    • uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
    • python app/main.py
  2. 运行基础单测:pytest tests/test_main.py
  3. 仅使用本机 localhost 验证,无需考虑外网 IP。

十二点五、登出联调核查(新增)

  • 点击“退出登录”后应:
    • 命中 POST /api/v1/auth/logout 返回 200
    • localStorageaccess_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/profileGET/PUT /api/v1/users/me
  • 字段映射:name↔full_nameavatar↔avatar_url
  • 可写字段:email/phone/full_name/avatar_url/bio/gender/school/major
  • 统计接口:GET /api/v1/users/me/statistics
  • 新增字段2025-09-22school(学校)、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}/membersbody: { "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_byupdated_by字段的引用
    • 添加成员到岗位:POST /api/v1/admin/positions/{position_id}/membersbody: { "user_ids": [user_id] }
    • 从岗位移除成员:DELETE /api/v1/admin/positions/{position_id}/members/{user_id}
  • 前端表单字段:内部使用 position: number|null岗位ID保存时先更新基础信息再对比当前岗位并调用增删接口
  • 实现逻辑:
    1. 编辑用户时,并行加载用户详情、团队信息和岗位信息
    2. 保存时先调用 updateUser 更新基础信息(姓名、邮箱、电话等)
    3. 对比选中岗位与现有岗位,计算需要移除和添加的岗位
    4. 调用相应的添加/移除API完成岗位同步

3) 岗位管理(扩展)

  • 成员管理:GET/POST/DELETE /api/v1/admin/positions/{id}/members
  • 课程管理:GET/POST/PUT/DELETE /api/v1/admin/positions/{id}/courses
  • 关联表:position_membersposition_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_materialsdelete_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

4.2) 编辑课程页 - 考试设置联调2025-09-22更新

  • 后端接口:GET /api/v1/courses/{id}/exam-settingsPOST /api/v1/courses/{id}/exam-settingsPUT /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 创建资料记录
    • 字段包括:namefile_urlfile_typefile_sizedescription
  • 权限与验证
    • 上传需要登录认证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}/positionsCoursePositionAssignmentInDB[]
    • 批量分配:POST /api/v1/courses/{id}/positionsbody: [{ 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 导致 500batch_assign_positions 中仅调用一次 scalar_one_or_none()
    • 修复向 PositionCourse 写入不存在的审计字段(该模型未继承 AuditMixin,移除相关写入)
    • 修复 SQLAlchemy 异步懒加载错误get_course_positions 中访问 assignment.position.members 触发 MissingGreenlet 错误
      • 原因:在异步上下文中访问懒加载的关系属性
      • 解决:暂时返回 member_count=0,避免访问懒加载关系
  • 前端实现要点:
    • courseApi.assignPositions(courseId, assignments) 直接提交 assignments 数组
    • 成功后刷新 courseApi.getPositions,按 course_type 分流到“必修/选修”
  • 验收要点:
    • Network 中批量分配/移除均 200返回结构符合 ResponseModel
    • 刷新页面后岗位分配与数据库一致(真落库、真查库)
    • 失败提示清晰Console 记录详细错误

十三、前端表单开发规范

  • 表单字段类型必须与后端一致;用 null 表示空值,避免空字符串
  • 数字字段不要初始化为字符串;el-selectvalue 类型与 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兼容项目配置
    • 检查字段是否已存在,避免重复添加
    • 执行后显示表结构,确认更新成功
  • 更新顺序
    1. 先执行增量脚本更新现有数据库
    2. 更新初始化SQL脚本init_database_unified.sql
    3. 同步更新数据库架构文档
    4. 记录到联调经验和规范文档

十六、维护与同步约定

  • 若调整结构/架构/配置/运行逻辑/路径/日志路径:请同步更新 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}/coursesDELETE /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

问题描述

用户反馈考试设置保存后不生效,页面刷新后数据恢复到之前的值。

问题原因

  1. 后端API错误获取课程基本信息时返回500错误导致整个数据加载流程中断
  2. 前端默认值问题当后端返回的考试设置数据为null时前端会使用硬编码的默认值
  3. 数据不同步:页面显示的"当前设置"与实际表单数据不一致

解决方案

  1. 修复后端500错误

    • 检查具体的错误日志:tail -f kaopeilian-backend/logs/backend.log
    • 验证数据库连接是否正常
    • 确保课程ID存在且数据完整
  2. 优化前端数据加载

    // 当课程基本信息加载失败时,停止后续加载
    if (courseRes.code !== 200) {
      ElMessage.error('课程信息加载失败,请刷新页面重试')
      return
    }
    
  3. 确保数据同步

    • 保存成功后主动刷新数据已有代码实现第1316-1327行
    • 页面"当前设置"应实时反映表单数据

经验总结

  • 级联错误处理当基础API失败时应阻止后续API调用避免使用错误的默认值
  • 数据一致性:前端显示的数据必须与实际的数据模型保持同步
  • 错误提示API失败时应给出明确的错误提示帮助快速定位问题

后续发现前端数据绑定问题2025-09-22

问题现象 用户反馈考试设置修改后保存仍不生效,经过深入调试发现:

  1. API请求正常发送POST /api/v1/courses/6/exam-settings
  2. 后端成功处理并返回201状态码
  3. 数据库写入成功updated_at时间戳更新
  4. 但发送的数据仍是旧值页面修改未同步到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错误。

问题原因

  1. 模型继承不完整KnowledgePoint 模型只继承了 BaseModelSoftDeleteMixin,缺少 AuditMixin
  2. 数据库字段缺失knowledge_points 表缺少 created_byupdated_by 字段
  3. 服务层字段不匹配:服务层尝试传递 created_by 参数,但模型中没有对应字段

错误信息

TypeError: 'created_by' is an invalid keyword argument for KnowledgePoint

解决方案

  1. 更新模型继承

    class KnowledgePoint(BaseModel, SoftDeleteMixin, AuditMixin):
    
  2. 数据库表结构更新

    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;
    
  3. 同步更新初始化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 后端实现

  1. 知识点分析服务 (app/services/ai/knowledge_analysis.py)

    • 创建 KnowledgeAnalysisService
    • 实现 analyze_course_material() 方法分析单个资料
    • 实现 reanalyze_course_materials() 方法重新分析所有资料
    • 集成Dify工作流API调用
  2. 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 实现异步处理
  3. 自动触发机制 (app/api/v1/courses.py)

    • 修改 add_course_material 接口,上传资料后自动触发知识点分析
    • 使用后台任务避免阻塞用户操作

10.2 前端实现

  1. 重新分析按钮 (kaopeilian-frontend/src/views/manager/edit-course.vue)

    • 在学习资料与知识点管理页面添加"重新分析"按钮
    • 实现 reanalyzeAllMaterials() 方法调用后端API
    • 添加加载状态和用户反馈
  2. 上传状态优化

    • 上传成功后显示"正在后台分析知识点..."提示
    • 资料状态从 pendinganalyzingcompleted

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

原因: 项目中混用了 loggingstructlog,前者不支持关键字参数

解决: 统一使用字符串格式化的日志记录

# 错误写法
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列表中

验证测试

  1. API测试:

    curl -X POST "http://localhost:8000/api/v1/courses/1/reanalyze" \
      -H "Authorization: Bearer <token>" \
      -H "Content-Type: application/json"
    # 返回: {"code":200,"message":"重新分析任务启动成功",...}
    
  2. 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",...}
    
  3. 功能验证:

    • 上传资料后自动触发知识点分析
    • 重新分析按钮正常工作
    • 后台任务异步执行不阻塞用户操作
    • 文件成功上传到Dify服务器
    • Dify工作流文件验证失败需要检查工作流配置

10.7 Dify工作流文件验证问题

问题: {"code":"invalid_param","message":"File validation failed for file: filename.pdf"}

分析:

  • 文件成功上传到Dify服务器并获得文件ID
  • 工作流调用时文件验证失败
  • 测试了PDF和TXT格式都失败

可能原因:

  1. Dify工作流对文件内容有特定格式要求
  2. 工作流配置中的文件类型限制
  3. 文件大小或编码问题
  4. 工作流版本或权限问题

建议解决方案:

  1. 检查Dify工作流的具体配置和文件要求
  2. 联系工作流管理员确认文件格式要求
  3. 尝试使用工作流推荐的示例文件进行测试
  4. 检查工作流是否需要特定的文件结构或元数据

经验总结

  • 异步处理: 使用 BackgroundTasks 实现耗时操作的异步处理
  • 错误处理: 外部服务调用需要完善的错误处理和重试机制
  • 日志规范: 统一项目中的日志记录方式,避免混用不同的日志库
  • API文档: 严格按照第三方服务的API文档格式传递参数
  • 网络配置: 开发环境需要正确配置代理和防火墙规则

2025-09-23 知识点分析工作流参数调整

问题描述

Dify工作流需要调整参数结构增加 material_id 参数以支持资料与知识点的精确关联。

解决方案

  1. 参数结构调整

    • 原参数:examsTitle, examsId
    • 新参数:course_name, course_id, material_id
  2. 代码修改

    • 更新 _call_dify_workflow 方法签名,增加 material_id 参数
    • 修改工作流调用payload使用新的参数名称
    • 更新日志记录,包含所有必要参数信息
  3. 文档更新

    • 更新工作流文档中的参数说明
    • 添加详细的JSON参数格式示例
    • 更新实现经验部分

技术要点

  • 参数映射: 确保后端参数与Dify工作流期望的参数名称一致
  • 向后兼容: 保持API接口的向后兼容性
  • 日志完整性: 记录所有关键参数便于调试和监控

11. 数据库从本地切换到公网配置2025-09-23

配置信息

公网数据库地址

  • 主机: 120.79.247.16aiedu.ireborn.com.cn
  • 端口: 3306
  • 数据库名: kaopeilian
  • 用户: root
  • 密码: Kaopeilian2025!@#

实施步骤

  1. 密码URL编码

    • 原始密码:Kaopeilian2025!@#
    • URL编码后Kaopeilian2025%21%40%23
    • 特殊字符编码:!%21@%40#%23
  2. 更新配置文件

    • start_mysql.py:更新数据库连接字符串
    • docker-compose.dev.yml:更新环境变量
    • 创建 start_remote.py:专门用于公网数据库启动
    • 创建 test_remote_db.py:测试连接脚本
  3. 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

注意事项

  1. 密码编码URL中的特殊字符必须进行编码否则会导致连接失败
  2. 网络延迟:公网数据库可能有一定延迟,开发时需要考虑
  3. 安全性:生产环境应使用环境变量或密钥管理服务,避免硬编码密码
  4. 切换回本地:如需使用本地数据库,修改连接字符串为: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语句验证

实现要点

  1. API设计

    • 主端点:/api/v1/sql/execute
    • 认证方式JWT Bearer Token
    • 支持查询(SELECT/SHOW/DESCRIBE)和写入(INSERT/UPDATE/CREATE等)
  2. 关键代码结构

    # 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并返回结果
    
  3. 安全措施

    • 需要用户认证
    • 支持参数化查询
    • SQL操作日志记录
    • 危险操作警告提示

集成步骤

  1. 创建API文件

    # 创建 app/api/v1/sql_executor.py
    # 实现execute、validate、tables、schema等接口
    
  2. 注册路由

    # 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"])
    
  3. 配置数据库连接

    # 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}}
  }

使用示例

  1. 查询操作

    {
      "sql": "SELECT * FROM courses WHERE category = :category",
      "params": {"category": "护肤"}
    }
    
  2. 插入操作

    {
      "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
      }
    }
    

注意事项

  1. 日期时间处理使用自定义JSON编码器处理datetime对象
  2. 表结构查询MySQL的DESCRIBE语句不支持参数化需要验证表名
  3. 连接池配置设置合适的pool_size和max_overflow
  4. 错误处理:执行失败时自动回滚事务

测试验证

# 运行测试脚本
python3 test_sql_executor.py

# 测试结果应包括:
# ✅ 登录成功
# ✅ 获取表列表成功
# ✅ SQL验证功能正常
# ✅ 查询和写入操作成功

经验总结

  • 参数化查询必须防止SQL注入使用:param_name格式
  • 认证集成关键Dify需要配置正确的Bearer Token
  • 日志记录重要记录所有SQL执行便于审计和调试
  • 测试先行原则先用独立脚本测试再集成到Dify

简化认证方案实现

为了解决JWT Token频繁过期的问题实现了三种持久认证方案

  1. API Key认证(推荐)

    • 端点:/api/v1/sql/execute-simple
    • 请求头:X-API-Key: dify-2025-kaopeilian
    • 优点:永不过期,配置简单
    • app/core/simple_auth.py中配置
  2. 长期Token认证

    • 端点:/api/v1/sql/execute-simple
    • 请求头:Authorization: Bearer permanent-token-for-dify-2025
    • 优点符合标准Bearer格式永不过期
  3. Dify配置要点

    • 鉴权类型:选择"请求头"
    • 鉴权头部前缀:选择"Custom"API Key或"Bearer"长期Token
    • 避免使用"无",即使是内部服务也应有基本认证

OpenAPI文档集成

  1. 规范版本使用OpenAPI 3.1.0
  2. 认证配置同时支持bearerAuth和apiKey
  3. 服务器配置包含公网IP、本地开发和域名访问
  4. 文档格式提供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

核心配置文件

  1. docker-compose.dev.yml - 开发环境Docker配置
  2. start-dev.sh - 一键启动脚本(支持多种模式)
  3. stop-dev.sh - 环境清理脚本
  4. Dockerfile.dev - 前后端开发环境镜像
  5. 开发环境使用指南.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

架构优势总结

  • 混合架构最优:数据库容器化 + 应用本地化
  • 热重载必备:大幅提升开发效率
  • 脚本自动化:减少重复操作和人为错误
  • 环境隔离:开发环境独立,不影响生产
  • 问题预案:常见问题有标准解决流程