- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
1129 lines
43 KiB
Markdown
1129 lines
43 KiB
Markdown
# 联调经验与方法(长期维护)
|
||
|
||
> 目的:沉淀可直接复用的前后端联调经验、统一的原则、方法与经验,支持本地开发环境(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
|
||
```
|
||
|
||
### 架构优势总结
|
||
|
||
- **混合架构最优**:数据库容器化 + 应用本地化
|
||
- **热重载必备**:大幅提升开发效率
|
||
- **脚本自动化**:减少重复操作和人为错误
|
||
- **环境隔离**:开发环境独立,不影响生产
|
||
- **问题预案**:常见问题有标准解决流程
|
||
|
||
---
|
||
|