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

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

1129 lines
43 KiB
Markdown
Raw 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.
# 联调经验与方法(长期维护)
> 目的沉淀可直接复用的前后端联调经验、统一的原则、方法与经验支持本地开发环境localhost全链路“真落库、真查库”对接。本文不记录按轮次的事件仅保留可执行的经验与规范。
---
## 一、环境与配置基线(本地)
- **端口**:后端 8000前端 3001MySQL 3306Redis 6379
- **数据库 DSNPython/SQLAlchemy**`mysql+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服务**
```bash
# 启动基础服务
docker-compose -f docker-compose.dev.yml up -d mysql-dev redis-dev
# 服务状态检查
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
```
- **环境变量配置**
```bash
export DATABASE_URL="mysql://root:Kaopeilian2025!@#@localhost:3306/kaopeilian"
export REDIS_URL="redis://localhost:6379/0"
```
---
## 二、标准化联调步骤(落地版)
1) 启动依赖与服务(推荐混合架构)
```bash
# 启动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
```
**或者使用一键启动脚本:**
```bash
./start-dev.sh # 完全Docker化方案
```
2) 健康检查与基础验证
```bash
curl -s http://localhost:8000/health
```
3) 启动前端(确保关闭 mock
```bash
cd kaopeilian-frontend
npm run dev -- --host --port 3001
```
4) 快速端到端核查
```bash
# 登录拿 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',[])))"
```
5) 浏览器 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 与慢查询
---
## 十一、常用命令速查
```bash
# 启动依赖
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
- `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}`
- 前端表单字段:内部使用 `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_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
#### 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`,避免访问懒加载关系
- 前端实现要点:
- `courseApi.assignPositions(courseId, assignments)` 直接提交 assignments 数组
- 成功后刷新 `courseApi.getPositions`,按 `course_type` 分流到“必修/选修”
- 验收要点:
- Network 中批量分配/移除均 200返回结构符合 `ResponseModel`
- 刷新页面后岗位分配与数据库一致(真落库、真查库)
- 失败提示清晰Console 记录详细错误
---
## 十三、前端表单开发规范
- 表单字段类型必须与后端一致;用 `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()`
- 代码示例:
```python
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}/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
### 问题描述
用户反馈考试设置保存后不生效,页面刷新后数据恢复到之前的值。
### 问题原因
1. **后端API错误**获取课程基本信息时返回500错误导致整个数据加载流程中断
2. **前端默认值问题**当后端返回的考试设置数据为null时前端会使用硬编码的默认值
3. **数据不同步**:页面显示的"当前设置"与实际表单数据不一致
### 解决方案
1. **修复后端500错误**
- 检查具体的错误日志:`tail -f kaopeilian-backend/logs/backend.log`
- 验证数据库连接是否正常
- 确保课程ID存在且数据完整
2. **优化前端数据加载**
```javascript
// 当课程基本信息加载失败时,停止后续加载
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对象仍是旧值。
**调试方法**
```javascript
// 检查输入框实际值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` 模型只继承了 `BaseModel` 和 `SoftDeleteMixin`,缺少 `AuditMixin`
2. **数据库字段缺失**`knowledge_points` 表缺少 `created_by` 和 `updated_by` 字段
3. **服务层字段不匹配**:服务层尝试传递 `created_by` 参数,但模型中没有对应字段
### 错误信息
```
TypeError: 'created_by' is an invalid keyword argument for KnowledgePoint
```
### 解决方案
1. **更新模型继承**
```python
class KnowledgePoint(BaseModel, SoftDeleteMixin, AuditMixin):
```
2. **数据库表结构更新**
```sql
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` 表添加审计字段
### 验证结果
```bash
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. **上传状态优化**
- 上传成功后显示"正在后台分析知识点..."提示
- 资料状态从 `pending` → `analyzing` → `completed`
#### 10.3 Dify工作流配置
- **API服务器**: `http://dify.ireborn.com.cn/v1`
- **API密钥**: `app-LZhZcMO6CiriLMOLB2PwUGHx`
- **参数格式**:
```json
{
"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`,前者不支持关键字参数
**解决**: 统一使用字符串格式化的日志记录
```python
# 错误写法
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测试**:
```bash
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文件上传测试**:
```bash
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.16` 或 `aiedu.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
```
### 启动方式
```bash
# 方式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`
### 验证方法
```bash
# 测试数据库连接
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. **关键代码结构**
```python
# 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文件**
```bash
# 创建 app/api/v1/sql_executor.py
# 实现execute、validate、tables、schema等接口
```
2. **注册路由**
```python
# 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. **配置数据库连接**
```python
# local_config.py
os.environ["DATABASE_URL"] = "mysql+aiomysql://root:Kaopeilian2025!@%23@120.79.247.16:3306/kaopeilian?charset=utf8mb4"
```
### Dify配置示例
```yaml
# 自定义工具配置
方法: POST
URL: http://localhost:8000/api/v1/sql/execute
Headers:
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json
Body:
{
"sql": "{{sql}}",
"params": {{params}}
}
```
### 使用示例
1. **查询操作**
```json
{
"sql": "SELECT * FROM courses WHERE category = :category",
"params": {"category": "护肤"}
}
```
2. **插入操作**
```json
{
"sql": "INSERT INTO knowledge_points (title, content) VALUES (:title, :content)",
"params": {
"title": "知识点标题",
"content": "知识点内容"
}
}
```
### 响应格式
- **查询响应**
```json
{
"code": 200,
"data": {
"type": "query",
"columns": ["id", "name"],
"rows": [{...}],
"row_count": 10
}
}
```
- **写入响应**
```json
{
"code": 200,
"data": {
"type": "execute",
"affected_rows": 1,
"success": true
}
}
```
### 注意事项
1. **日期时间处理**使用自定义JSON编码器处理datetime对象
2. **表结构查询**MySQL的DESCRIBE语句不支持参数化需要验证表名
3. **连接池配置**设置合适的pool_size和max_overflow
4. **错误处理**:执行失败时自动回滚事务
### 测试验证
```bash
# 运行测试脚本
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运行在容器中确保环境一致性
- **应用层本地开发**:前后端在本地运行,保持开发灵活性和热重载
- **优势**:避免完全容器化的资源消耗,同时保证数据环境统一
### 快速启动流程
```bash
# 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** - 详细使用文档
### 常见问题解决方案
#### 前端依赖问题
```bash
# 问题:@rollup/rollup-darwin-arm64模块缺失
# 原因npm可选依赖bug
# 解决:
cd kaopeilian-frontend
rm -rf node_modules package-lock.json
npm install
```
#### Docker网络问题
```bash
# 问题:镜像拉取超时
# 解决:预拉取或使用镜像加速
docker-compose -f docker-compose.dev.yml pull
```
#### 端口冲突处理
```bash
# 检查端口占用
lsof -i :3001 :8000 :3306 :6379
# 批量清理进程
pkill -f vite && pkill -f uvicorn
```
#### 热重载验证
```bash
# 后端测试(观察控制台重载信息)
echo "# test reload" >> kaopeilian-backend/app/main.py
# 前端测试(观察浏览器自动刷新)
echo "<!-- test reload -->" >> kaopeilian-frontend/src/App.vue
```
### 开发环境状态检查
```bash
# 检查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
```
### 架构优势总结
- **混合架构最优**:数据库容器化 + 应用本地化
- **热重载必备**:大幅提升开发效率
- **脚本自动化**:减少重复操作和人为错误
- **环境隔离**:开发环境独立,不影响生产
- **问题预案**:常见问题有标准解决流程
---