# 联调经验与方法（长期维护）

> 目的：沉淀可直接复用的前后端联调经验、统一的原则、方法与经验，支持本地开发环境（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: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
```

### 架构优势总结

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

---

