# 联调经验与方法(长期维护) > 目的:沉淀可直接复用的前后端联调经验、统一的原则、方法与经验,支持本地开发环境(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 ` - 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 " \ -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 " \ -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 "" >> 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 ``` ### 架构优势总结 - **混合架构最优**:数据库容器化 + 应用本地化 - **热重载必备**:大幅提升开发效率 - **脚本自动化**:减少重复操作和人为错误 - **环境隔离**:开发环境独立,不影响生产 - **问题预案**:常见问题有标准解决流程 ---