merge: 合并远程更新并解决冲突

- 合并学校课程管理搜索与年级筛选功能
- 修复教学资源区域间距问题
- 删除已忽略的自动生成文件
- 新增排课弹窗优化(移除课程选择,自动选择第一门课程)
- 新增 lessonType 从 schedule_ref_data 解析功能
- 修复前端代理配置(端口 8480)
This commit is contained in:
Claude Opus 4.6 2026-03-18 14:46:32 +08:00
parent 43d5bc8662
commit 279fa79b56
37 changed files with 9542 additions and 2634 deletions

View File

@ -6,6 +6,17 @@
## 常用命令
### 服务端口配置
| 服务 | 端口 | 说明 |
|------|------|------|
| 后端 API | **8480** | Spring Boot 服务(已修改) |
| 前端 Dev Server | 5173 | Vite 开发服务器 |
| 数据库 MySQL | 3306 | 开发环境数据库 |
| Redis | 6379 | 缓存服务 |
**重要**: 后端服务已从默认的 8080 端口改为 **8480 端口**
### 启动服务
```bash

View File

@ -6,6 +6,207 @@
## [Unreleased]
### 配置变更
#### 后端端口修改 ✅ (2026-03-18)
- 后端服务端口从 8080 改为 8480
- 修改文件:`src/main/resources/application.yml`
- 新的 API 地址:`http://localhost:8480`
- API 文档:`http://localhost:8480/doc.html`
---
## [Unreleased]
### 三层架构代码全面审计与修复 ✅ (2026-03-18 下午)
**用户质疑引发全面审计**:用户指出数据库改动很大,需要全面审计相关代码。
**审计方法**:使用 Grep 搜索所有使用 `getPackageId()``setPackageId()` 的地方,逐一检查。
**修复清单**
**DTO 修复**
- `TenantCreateRequest.packageId``collectionId`
- 说明:租户应关联课程套餐(顶层),而非课程包(中层)
**Service 层修复**
- **TenantServiceImpl.createTenant()** - 使用 `request.getCollectionId()` 查询 CourseCollection
- **CourseServiceImpl.getTenantPackageCourses()** - 实现正确的三层架构查询:
```java
// 修复前:直接使用 packageId错误
List<Long> packageIds = tenantPackages.stream()
.map(TenantPackage::getPackageId) // ❌ deprecated
// 修复后:三层查询(正确)
List<Long> collectionIds = tenantPackages.stream()
.map(TenantPackage::getCollectionId) // ✅
// collection → CourseCollectionPackage → CoursePackage → Course
```
- **CoursePackageService.findTenantPackages()** - 标记 @Deprecated
- **CoursePackageService.renewTenantPackage()** - 标记 @Deprecated
- **CourseCollectionService.renewTenantCollection()** - 新增续费方法
**Controller 层修复**
- **AdminPackageController.grantToTenant()** - 标记 @Deprecated(授权课程包)
- **AdminCourseCollectionController.grantToTenant()** - 新增授权 API授权课程套餐
- **SchoolPackageController.renewPackage()** - 标记 @Deprecated
- **SchoolPackageController.renewCollection()** - 新增续费 API
- **SchoolCourseController** - 统一术语("课程包" → "课程"
**新增 API**
- `POST /api/v1/admin/collections/{id}/grant` - 授权课程套餐给租户(超管端)
- `POST /api/v1/school/packages/{collectionId}/renew` - 续费课程套餐(学校端)
**向后兼容性**
- 旧 API 标记 @Deprecated,保留功能
- 新 API 提供正确的三层架构支持
**影响范围**
- 9 个文件修复
- 3 个 Service 方法改进
- 4 个 Controller API 更新
**编译验证**:✅ BUILD SUCCESS
---
### 三层架构系统性审查与关联数据清理 ✅ (2026-03-18 晚间)
**审查规划**:创建全面审查规划文档 (`docs/dev-logs/2026-03-18-code-audit-plan.md`),按优先级分类审查所有可能受影响的代码。
**P0 优先级修复** - 核心业务逻辑:
1. **CoursePackageService.findAllPackages** - tenantCount 统计修复
- 问题:使用 `TenantPackage.getPackageId()` 直接统计
- 修复:实现三层查询统计(课程包 → 课程套餐 → 租户)
2. **CoursePackageService.deletePackage** - 租户检查逻辑修复
- 问题:检查租户直接使用课程包
- 修复:检查包含此课程包的套餐是否被租户使用,并清理所有关联
3. **CourseCollectionService.deleteCollection** - 关联数据清理
- 问题:没有清理 tenant_package 关联
- 修复:添加租户检查 + 清理 tenant_package + 清理 course_collection_package
**P1 优先级修复** - 重要功能:
4. **CourseServiceImpl.deleteCourse** - 关联数据清理
- 问题:没有清理 course_package_course 关联
- 修复:添加课程包关联清理
5. **TenantServiceImpl.deleteTenant** - 套餐关联清理
- 问题:没有清理 tenant_package 关联
- 修复:添加套餐关联清理
6. **SchoolScheduleController** - 审查完成,无需修改
7. **TeacherCourseController** - 发现潜在问题(需业务确认)
- 问题getCoursesByTenantId 返回所有系统课程
- 说明:需确认是否应该通过租户套餐过滤
8. **SchoolTaskController** - 审查完成,不涉及三层架构
**Service 层修复汇总**
| Service | 方法 | 修复内容 |
|---------|------|----------|
| CoursePackageService | findAllPackages | tenantCount 三层统计 |
| CoursePackageService | deletePackage | 租户检查 + 关联清理 |
| CourseCollectionService | deleteCollection | 租户检查 + 完整关联清理 |
| CourseServiceImpl | deleteCourse | 课程包关联清理 |
| TenantServiceImpl | deleteTenant | 套餐关联清理 |
**数据完整性保障**
- 所有删除操作都已正确处理关联数据
- 防止数据孤岛的产生
- 删除前检查依赖关系
**审查进度**
- P0 优先级: 3/3 (100%) ✅
- P1 优先级: 5/5 (100%) ✅
- P2 优先级: 0/3 (0%) - 低优先级,后续处理
- **总体进度**: 8/11 (73%)
**编译验证**:✅ BUILD SUCCESS
**审查文档**`docs/dev-logs/2026-03-18-code-audit-plan.md`
---
### 教师端课程过滤修复 ✅ (2026-03-18 最终修复)
**问题确认**
- 用户确认:"是的,教师应该只看到租户购买的套餐下的课程"
- 之前 getCoursesByTenantId 和 getCoursePage 返回所有系统课程
**修复内容**
1. **CourseServiceImpl.getCoursesByTenantId()**
- 修复前:返回租户课程 + 所有系统课程
- 修复后:调用 getTenantPackageCourses() 使用三层查询
2. **CourseServiceImpl.getCoursePage()**
- 修复前:返回租户课程 + 所有系统课程(分页)
- 修复后:实现支持分页和过滤的三层查询
- 支持关键词、分类、状态过滤
- 支持分页查询
**三层查询流程**
```
tenant → tenant_package → collectionIds
collectionIds → course_collection_package → packageIds
packageIds → course_package_course → courseIds
courseIds → course (分页 + 过滤)
```
**影响范围**
- TeacherCourseController - 课程列表查询
- 教师只能看到租户购买的套餐下的课程
**最终完成度**
- P0 优先级: 100% ✅
- P1 优先级: 100% ✅
- P2 优先级: 0% (低优先级)
**编译验证**:✅ BUILD SUCCESS
---
### 三层课程架构修复与优化 ✅ (2026-03-18 上午)
**修复了 V28 迁移导致的数据结构问题:**
**数据库修复**
- Flyway 迁移脚本:`V32__fix_three_tier_final.sql`
- Flyway 迁移脚本:`V33__cleanup_id_conflicts.sql`
- 创建正确的三层结构course_collection (100) → course_package (101-103) → course (101-110)
- 清理 ID 冲突数据(删除 ID 6, 7 的冲突记录)
**后端代码优化**
- 移除 CourseCollectionService 的临时过滤逻辑(针对 V28 问题的变通方案)
- 实现 SchoolScheduleServiceImpl.getCoursePackageLessonTypes() 方法
- 从硬编码数据改为实时数据库查询
- 从 schedule_ref_data JSON 字段解析课程类型
- 支持的课程类型:导入课、集体课、五大领域课(语言、艺术、科学、社会、健康)
- 统一 AdminPackageController 术语:将"课程套餐"改为"课程包"
**工具类**
- `FlywayRepair.java` - 删除失败的 Flyway 迁移记录
- `CheckDatabaseStructure.java` - 验证数据库三层结构
- `CleanupIdConflicts.java` - 清理 ID 冲突数据
**API 测试结果**
- ✅ 课程包 101 (语言启蒙): INTRODUCTION, COLLECTIVE, LANGUAGE
- ✅ 课程包 102 (艺术创作): INTRODUCTION, COLLECTIVE, ART
- ✅ 课程包 103 (科学探索): INTRODUCTION, COLLECTIVE, HEALTH, SCIENCE
**影响范围**
- 数据库course_collection, course_package, course, course_collection_package, course_package_course
- 后端CourseCollectionService, SchoolScheduleServiceImpl, AdminPackageController
- API`GET /api/v1/school/schedules/course-packages/{id}/lesson-types`
---
### 排课计划参考示例数据添加 ✅ (2026-03-18)
**添加了课程排课计划参考示例数据:**

View File

@ -80,3 +80,375 @@ feat: 添加课程包课程列表查询API
- [ ] 考虑添加 `collectionId` 存储(需要数据库迁移和 DTO 更新)
---
## 下午工作:三层课程架构修复
### 问题诊断
- **发现 V28 迁移问题**: 课程套餐和课程包 ID 冲突ID 6, 7 同时存在于两个表)
- **数据库结构混乱**: course_collection 和 course_package 存在自引用数据
### 修复过程
1. **创建 FlywayRepair.java 工具** - 成功删除失败的 V30、V31 迁移记录
2. **V32 迁移执行成功** - 创建新的三层结构数据Collection 100 → Packages 101,102,103 → Courses 101-110
3. **后端代码全面审计** - 验证所有相关 Service 和 Controller 的三层结构实现
### 实现 getCoursePackageLessonTypes()
**问题**: 方法返回硬编码数据而不是查询真实数据库
**解决**:
- 查询 course_package_course 表获取课程ID列表
- 查询 course 表获取课程详情
- 从 schedule_ref_data JSON 字段解析 lessonType
- 按课程类型分组统计数量
### API 测试验证
```bash
# 测试课程包 101 (语言启蒙): INTRODUCTION, COLLECTIVE, LANGUAGE
# 测试课程包 102 (艺术创作): INTRODUCTION, COLLECTIVE, ART
# 测试课程包 103 (科学探索): INTRODUCTION, COLLECTIVE, HEALTH, SCIENCE
```
所有测试结果符合 V32 迁移数据,三层架构工作正常。
### 待办事项
- [ ] 清理旧数据 ID 冲突course_collection 和 course_package 中的 ID 6, 7
- [ ] 移除 CourseCollectionService Line 165 的临时过滤逻辑
- [ ] 统一后端代码术语course_package 应称为"课程包"
---
## ✅ 上午工作完成总结
### 三项待办任务全部完成
#### 1. ✅ 清理旧数据 ID 冲突
**问题**: ID 6, 7 同时存在于 course_collection 和 course_package
**解决**:
- 创建 V33 迁移脚本 `V33__cleanup_id_conflicts.sql`
- 创建并执行 `CleanupIdConflicts.java` 工具
- 成功删除 2 条冲突的 course_collection 记录
- 验证结果:无剩余 ID 冲突
#### 2. ✅ 移除临时过滤逻辑
**问题**: CourseCollectionService Line 165 有针对 V28 问题的临时过滤代码
**解决**:
- 移除了 lines 158-165 的过滤逻辑
- 移除了查询 collectionIds 并过滤 packages 的代码
- 简化了 getPackagesByCollection() 方法实现
#### 3. ✅ 统一后端代码术语
**问题**: AdminPackageController 将 course_package 称为"课程套餐"
**解决**:
- 更新了 AdminPackageController 中所有 API 文档注释
- 统一术语对应关系:
- course_collection = 课程套餐- 顶层集合
- course_package = 课程包- 中层包
- course = 课程- 底层课程
### 文件变更统计
**新建文件 (7 个)**:
1. `V32__fix_three_tier_final.sql` - 三层结构修复迁移
2. `V33__cleanup_id_conflicts.sql` - ID 冲突清理迁移
3. `FlywayRepair.java` - 删除失败迁移记录工具
4. `CheckDatabaseStructure.java` - 数据库结构验证工具
5. `CleanupIdConflicts.java` - ID 冲突清理工具
6. `2026-03-18-three-layer-structure-audit.md` - 审计报告
7. `2026-03-18-summary.md` - 工作总结
**修改文件 (3 个)**:
1. `CourseCollectionService.java` - 移除临时过滤逻辑
2. `SchoolScheduleServiceImpl.java` - 实现课程类型查询
3. `AdminPackageController.java` - 统一术语
### 最终状态
**数据库**: ✅ 三层结构完整,无 ID 冲突
**后端代码**: ✅ 临时逻辑已移除,术语统一
**API 接口**: ✅ 课程类型查询正常工作
**代码编译**: ✅ BUILD SUCCESS
### 技术债务清理情况
| 问题 | 状态 | 说明 |
|------|------|------|
| V28 迁移 ID 冲突 | ✅ 已解决 | 清理了 ID 6, 7 冲突 |
| V30/V31 失败迁移 | ✅ 已解决 | 删除失败记录 |
| 临时过滤逻辑 | ✅ 已移除 | 不再需要 |
| 术语不一致 | ✅ 已统一 | AdminPackageController 更新 |
### 下一步建议
**P0 (无)** - 所有关键任务已完成
**P1 (本周)**:
- 审查 teacher/parent 端接口
- 添加单元测试
**P2 (下周)**:
- 检查 ID 3, 4, 5 数据(如需要)
---
**工作时间**: 上午 9:00-12:00
**完成任务**: 6 项核心任务 + 3 项待办任务
**代码质量**: 编译通过API 测试通过
---
## 下午工作:三层架构代码全面审计与修复
### 用户质疑与反思
**用户反馈**: "因为这次数据库改动很大,你确定你已经把所有与数据库改动有关的全部功能代码做了审查和调整是吗"
**反思**: 之前的审计不够全面,只检查了部分文件。需要进行全面的代码审计,确保所有与三层架构相关的代码都得到修正。
### 全面审计结果
通过 Grep 搜索 `getPackageId()``setPackageId()`,发现以下需要修改的文件:
#### 1. TenantCreateRequest.java ✅ 已修复
- **问题**: 使用 `packageId` 字段名
- **修复**: 改为 `collectionId`
- **说明**: 租户应关联课程套餐collection不是课程包package
#### 2. TenantServiceImpl.java ✅ 已修复
- **问题**: 使用 `request.getPackageId()` 查询 CoursePackage
- **修复**: 改为使用 `request.getCollectionId()` 查询 CourseCollection
- **修复**: `tenantPackage.setPackageId()``tenantPackage.setCollectionId()`
- **添加依赖**: CourseCollectionMapper
#### 3. CourseServiceImpl.getTenantPackageCourses() ✅ 已修复
- **问题**: 使用 deprecated `TenantPackage.getPackageId()`
- **修复**: 改为使用 `getCollectionId()` 并执行正确的三层查询
- **添加依赖**: CourseCollectionPackageMapper
#### 4. CoursePackageService.findTenantPackages() ✅ 已标记 @Deprecated
- **问题**: 方法使用 packageId 查询租户套餐
- **修复**: 标记为 @Deprecated,添加注释指向 CourseCollectionService
#### 5. CoursePackageService.renewTenantPackage() ✅ 已标记 @Deprecated
- **问题**: 续费课程包,但租户应关联课程套餐
- **修复**: 标记为 @Deprecated,在 CourseCollectionService 中创建新方法
#### 6. AdminPackageController.grantToTenant() ✅ 已标记 @Deprecated
- **问题**: 授权课程包给租户
- **修复**: 标记为 @Deprecated,在 AdminCourseCollectionController 中创建新方法
#### 7. SchoolPackageController.renewPackage() ✅ 已标记 @Deprecated
- **问题**: 续费方法路径和语义不清晰
- **修复**: 添加新的 `renewCollection()` 方法,旧方法标记为 @Deprecated
#### 8. SchoolCourseController 术语 ✅ 已修复
- **问题**: 注释和 API 文档称课程为"课程包"
- **修复**: 统一术语:
- "课程包管理" → "课程管理"
- "获取学校课程包列表" → "获取学校课程列表"
### 新增功能
#### CourseCollectionService.renewTenantCollection()
```java
@Transactional(rollbackFor = Exception.class)
public void renewTenantCollection(Long tenantId, Long collectionId,
LocalDate endDate, Long pricePaid)
```
- 续费或新办租户课程套餐
- 支持查询现有记录并更新
- 支持创建新的租户套餐关联
#### AdminCourseCollectionController.grantToTenant()
```java
@PostMapping("/{id}/grant")
public Result<Void> grantToTenant(@PathVariable Long id,
@Valid @RequestBody GrantCollectionRequest request)
```
- 超管端授权课程套餐给租户
- API 路径: `/api/v1/admin/collections/{id}/grant`
#### SchoolPackageController.renewCollection()
```java
@PostMapping("/{collectionId}/renew")
public Result<Void> renewCollection(@PathVariable Long collectionId,
@RequestBody RenewRequest request)
```
- 学校端续费课程套餐
- API 路径: `/api/v1/school/packages/{collectionId}/renew`
### 修复总结
| 文件 | 修复类型 | 说明 |
|------|---------|------|
| TenantCreateRequest.java | 字段重命名 | packageId → collectionId |
| TenantServiceImpl.java | 逻辑修复 | 使用 CourseCollection 而非 CoursePackage |
| CourseServiceImpl.java | 三层查询 | 正确实现 Collection → Package → Course 查询 |
| CoursePackageService.java | 标记 @Deprecated | 保留向后兼容 |
| CourseCollectionService.java | 新增方法 | 续费课程套餐功能 |
| AdminPackageController.java | 标记 @Deprecated | 指向新的 API |
| AdminCourseCollectionController.java | 新增 API | 授权课程套餐给租户 |
| SchoolPackageController.java | 新增 API + 标记 @Deprecated | 续费课程套餐 |
| SchoolCourseController.java | 术语修正 | 课程包 → 课程 |
### 编译验证
```bash
mvn clean compile -DskipTests
[INFO] BUILD SUCCESS
[INFO] Total time: 3.817 s
```
### 待办事项(剩余)
- [ ] 审查 teacher/parent 端接口
- [ ] 添加单元测试
- [ ] 更新前端 API 调用(如有使用 deprecated API
---
**下午工作时间**: 14:00 - 16:00
**完成任务**: 9 个文件的审计与修复
**代码质量**: 编译通过,架构统一
---
## 晚间工作:系统性代码审查与修复 (16:30 - 18:00)
### 用户要求
"我还是有点不放心,你从设计方案和需求分析的角度出发,做一个代码审查的规划,列出所有可能受数据库变动影响的功能清单"
### 审查方法
1. 创建全面的审查规划文档 (`2026-03-18-code-audit-plan.md`)
2. 按优先级分类P0核心、P1重要、P2辅助
3. 逐一审查并修复
### P0 优先级 - 核心业务逻辑3项✅ 全部完成
#### 1. CoursePackageService.findAllPackages - tenantCount 统计 ✅
**问题**: 使用 `TenantPackage.getPackageId()` 统计
**修复**: 实现正确的三层查询统计
```java
// 查询包含此课程包的课程套餐 → 统计使用这些套餐的租户
```
#### 2. CoursePackageService.deletePackage - 租户检查 ✅
**问题**: 检查租户直接使用课程包
**修复**: 改为检查包含此课程包的套餐是否被租户使用,并清理关联
#### 3. CourseCollectionService.deleteCollection - 关联清理 ✅
**问题**: 没有清理 tenant_package 关联
**修复**: 添加租户检查 + 清理所有关联数据
### P1 优先级 - 重要功能5项✅ 全部完成
#### 1. AdminCourseController - deleteCourse ✅
**问题**: 没有清理 course_package_course 关联
**修复**: 添加关联数据清理
#### 2. AdminTenantController - deleteTenant ✅
**问题**: 没有清理套餐关联
**修复**: 添加 tenant_package 清理
#### 3. SchoolScheduleController ✅
**审查结果**: 不直接涉及三层架构,已正确实现
#### 4. TeacherCourseController ⚠️
**发现**: getCoursesByTenantId 返回所有系统课程,未通过套餐过滤
**说明**: 需与产品确认业务需求
#### 5. SchoolTaskController ✅
**审查结果**: 不涉及三层架构
### 本次修复汇总
| 类别 | 数量 | 文件 |
|------|------|------|
| Service 层修复 | 4 | CoursePackageService, CourseCollectionService, CourseServiceImpl, TenantServiceImpl |
| 删除操作优化 | 4 | deletePackage, deleteCollection, deleteCourse, deleteTenant |
| 关联数据清理 | 4 | tenant_package, course_collection_package, course_package_course |
### 编译验证
```bash
mvn clean compile -DskipTests
[INFO] BUILD SUCCESS
[INFO] Total time: 3.735 s
```
### 遗留问题
- **P2 优先级**: 3 个辅助功能 Controller低优先级
- **业务确认**: 教师端课程列表是否应通过套餐过滤
### 审查文档
- ✅ 审查规划: `docs/dev-logs/2026-03-18-code-audit-plan.md`
- ✅ 完成进度: P0 (100%), P1 (100%), P2 (0%)
- ✅ 总体进度: 73%
---
**晚间工作时间**: 16:30 - 18:00
**完成任务**: 8 项审查与修复
**代码质量**: 编译通过,删除操作已正确处理关联数据
---
## 最终修复:教师端课程过滤 (18:30)
### 用户确认
**问题**: 教师端课程列表返回所有系统课程
**用户确认**: "是的,教师应该只看到租户购买的套餐下的课程"
### 修复内容
#### CourseServiceImpl.getCoursesByTenantId()
**修复前**: 返回租户课程 + 所有系统课程
**修复后**: 调用 `getTenantPackageCourses()` 使用三层查询
```java
public List<Course> getCoursesByTenantId(Long tenantId) {
// 使用三层架构查询:租户 → 课程套餐 → 课程包 → 课程
return getTenantPackageCourses(tenantId);
}
```
#### CourseServiceImpl.getCoursePage()
**修复前**: 返回租户课程 + 所有系统课程(分页)
**修复后**: 实现支持分页的三层查询
```java
// 三层查询 + 分页 + 关键词/分类/状态过滤
// 1. tenant_package → collectionIds
// 2. course_collection_package → packageIds
// 3. course_package_course → courseIds
// 4. course → 分页查询(应用过滤条件)
```
### 最终状态
- ✅ P0 优先级: 100% 完成
- ✅ P1 优先级: 100% 完成
- ⏸️ P2 优先级: 0% (低优先级)
### 编译验证
```bash
mvn clean compile -DskipTests
[INFO] BUILD SUCCESS
[INFO] Total time: 3.952 s
```
---
**最终工作时间**: 16:30 - 18:30
**完成任务**: 9 项审查与修复(含教师端课程过滤)
**代码质量**: 编译通过,所有核心业务逻辑已修复
---
## 配置变更
### 后端端口修改 (2026-03-18)
**变更内容**:后端服务端口从 8080 改为 8480
**配置文件**
- `reading-platform-java/src/main/resources/application.yml`
- 修改:`server.port: ${SERVER_PORT:8080}` → `server.port: ${SERVER_PORT:8480}`
**影响**
- 后端 API 访问地址:`http://localhost:8480`
- API 文档地址:`http://localhost:8480/doc.html`
- Swagger UI`http://localhost:8480/swagger-ui.html`
**记录位置**:已更新 `.claude/CLAUDE.md` 文档
---

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
*/
/**
*
*
*/
export interface Course {
/** 主键 ID */

View File

@ -5,6 +5,7 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { LessonStepResponse } from './lessonStepResponse';
/**
*
@ -48,6 +49,8 @@ export interface CourseLessonResponse {
useTemplate?: boolean;
/** 排序号 */
sortOrder?: number;
/** 教学环节列表 */
steps?: LessonStepResponse[];
/** 创建时间 */
createdAt?: string;
/** 更新时间 */

View File

@ -18,4 +18,8 @@ export interface CoursePackageCourseItem {
gradeLevel?: string;
/** 排序号 */
sortOrder?: number;
/** 排课计划参考数据JSON */
scheduleRefData?: string;
/** 课程类型 */
lessonType?: string;
}

View File

@ -53,4 +53,6 @@ export interface CoursePackageResponse {
startDate?: string;
/** 结束日期(租户套餐) */
endDate?: string;
/** 排序号(在课程套餐中的顺序) */
sortOrder?: number;
}

View File

@ -5,7 +5,8 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { ActiveTenantsQueryRequest } from './activeTenantsQueryRequest';
export type GetActiveTenantsParams = {
limit?: number;
request: ActiveTenantsQueryRequest;
};

View File

@ -5,10 +5,8 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { CoursePageQueryRequest } from './coursePageQueryRequest';
export type GetCoursePage1Params = {
pageNum?: number;
pageSize?: number;
keyword?: string;
category?: string;
request: CoursePageQueryRequest;
};

View File

@ -9,5 +9,4 @@
export type GetFeedbacksParams = {
pageNum?: number;
pageSize?: number;
type?: string;
};

View File

@ -5,7 +5,8 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { PopularCoursesQueryRequest } from './popularCoursesQueryRequest';
export type GetPopularCoursesParams = {
limit?: number;
request: PopularCoursesQueryRequest;
};

View File

@ -5,7 +5,8 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { RecentActivitiesQueryRequest } from './recentActivitiesQueryRequest';
export type GetRecentActivities1Params = {
limit?: number;
request: RecentActivitiesQueryRequest;
};

View File

@ -7,8 +7,11 @@
*/
export type GetSchedules1Params = {
pageNum?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
classId?: number;
teacherId?: number;
status?: string;
};

View File

@ -7,6 +7,8 @@
*/
export type GetSchedulesParams = {
pageNum?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
};

View File

@ -6,24 +6,35 @@
* OpenAPI spec version: 1.0.0
*/
export * from './activeTenantItemResponse';
export * from './activeTenantsQueryRequest';
export * from './addClassTeacherDto';
export * from './adminStatsControllerGetActiveTenantsParams';
export * from './adminStatsControllerGetPopularCoursesParams';
export * from './adminStatsControllerGetRecentActivitiesParams';
export * from './approveCourseDto';
export * from './approveCourseDtoChecklist';
export * from './basicSettingsResponse';
export * from './basicSettingsUpdateRequest';
export * from './batchCreateSchedulesBody';
export * from './batchStudentRecordsDto';
export * from './batchStudentRecordsDtoRecordsItem';
export * from './bindStudentParams';
export * from './calendarViewResponse';
export * from './calendarViewResponseSchedules';
export * from './changePasswordParams';
export * from './checkConflictParams';
export * from './classCreateRequest';
export * from './classResponse';
export * from './classTeacherResponse';
export * from './classUpdateRequest';
export * from './clazz';
export * from './completeTaskParams';
export * from './conflictCheckResult';
export * from './conflictInfo';
export * from './course';
export * from './courseCollectionPageQueryRequest';
export * from './courseCollectionResponse';
export * from './courseControllerFindAllParams';
export * from './courseControllerGetReviewListParams';
export * from './courseCreateRequest';
@ -33,10 +44,14 @@ export * from './courseLessonResponse';
export * from './coursePackage';
export * from './coursePackageControllerFindAllParams';
export * from './coursePackageCourseItem';
export * from './coursePackageItem';
export * from './coursePackageResponse';
export * from './coursePageQueryRequest';
export * from './courseReportResponse';
export * from './courseResponse';
export * from './courseUpdateRequest';
export * from './createClassDto';
export * from './createCollectionRequest';
export * from './createFromSourceDto';
export * from './createFromSourceDtoSaveLocation';
export * from './createFromTemplateBody';
@ -52,12 +67,14 @@ export * from './createScheduleDto';
export * from './createSchoolCourseDto';
export * from './createStudentDto';
export * from './createTaskDto';
export * from './createTaskFromTemplateRequest';
export * from './createTaskTemplateDto';
export * from './createTeacherDto';
export * from './createTemplate1Body';
export * from './createTemplateBody';
export * from './createTenantDto';
export * from './createTenantDtoPackageType';
export * from './dayScheduleItem';
export * from './deleteFileBody';
export * from './directPublishDto';
export * from './exportControllerExportGrowthRecordsParams';
@ -69,11 +86,14 @@ export * from './findAll1Params';
export * from './findAllItemsParams';
export * from './findAllLibrariesParams';
export * from './finishLessonDto';
export * from './generateEditTokenParams';
export * from './generateReadOnlyTokenParams';
export * from './getActiveTeachersParams';
export * from './getActiveTenants200';
export * from './getActiveTenants200DataItem';
export * from './getActiveTenantsParams';
export * from './getAllStudentsParams';
export * from './getCalendarViewDataParams';
export * from './getClassPageParams';
export * from './getClassStudents1Params';
export * from './getClassStudentsParams';
@ -91,6 +111,7 @@ export * from './getMyLessonsParams';
export * from './getMyNotifications1Params';
export * from './getMyNotificationsParams';
export * from './getMyTasksParams';
export * from './getOssTokenParams';
export * from './getParentPageParams';
export * from './getPopularCourses200';
export * from './getPopularCourses200DataItem';
@ -117,20 +138,31 @@ export * from './growthRecord';
export * from './growthRecordCreateRequest';
export * from './growthRecordResponse';
export * from './growthRecordUpdateRequest';
export * from './immTokenVo';
export * from './itemCreateRequest';
export * from './itemUpdateRequest';
export * from './lesson';
export * from './lessonControllerFindAllParams';
export * from './lessonCreateRequest';
export * from './lessonDetailResponse';
export * from './lessonFeedback';
export * from './lessonFeedbackDto';
export * from './lessonFeedbackDtoActivitiesDone';
export * from './lessonFeedbackDtoStepFeedbacks';
export * from './lessonFeedbackRequest';
export * from './lessonFeedbackResponse';
export * from './lessonProgressDto';
export * from './lessonProgressDtoProgressData';
export * from './lessonProgressRequest';
export * from './lessonProgressRequestProgressData';
export * from './lessonResponse';
export * from './lessonStep';
export * from './lessonStepCreateRequest';
export * from './lessonStepResponse';
export * from './lessonTypeInfo';
export * from './lessonUpdateRequest';
export * from './libraryCreateRequest';
export * from './librarySummary';
export * from './libraryUpdateRequest';
export * from './localTime';
export * from './loginDto';
@ -138,31 +170,47 @@ export * from './loginRequest';
export * from './loginResponse';
export * from './notification';
export * from './notificationResponse';
export * from './notificationSettingsResponse';
export * from './notificationSettingsUpdateRequest';
export * from './object';
export * from './operationLogResponse';
export * from './orderItem';
export * from './ossTokenVo';
export * from './packageCreateRequest';
export * from './packageGrantRequest';
export * from './packageInfoResponse';
export * from './packageReviewRequest';
export * from './packageUsageResponse';
export * from './pageCoursePackage';
export * from './pageParams';
export * from './pageResourceItem';
export * from './pageResourceLibrary';
export * from './pageResultClassResponse';
export * from './pageResultClazz';
export * from './pageResultCourse';
export * from './pageResultCourseCollectionResponse';
export * from './pageResultCoursePackageResponse';
export * from './pageResultCourseResponse';
export * from './pageResultGrowthRecord';
export * from './pageResultGrowthRecordResponse';
export * from './pageResultLesson';
export * from './pageResultLessonFeedbackResponse';
export * from './pageResultLessonResponse';
export * from './pageResultNotification';
export * from './pageResultNotificationResponse';
export * from './pageResultOperationLogResponse';
export * from './pageResultParent';
export * from './pageResultParentResponse';
export * from './pageResultResourceItem';
export * from './pageResultResourceItemResponse';
export * from './pageResultResourceLibrary';
export * from './pageResultResourceLibraryResponse';
export * from './pageResultSchedulePlanResponse';
export * from './pageResultStudent';
export * from './pageResultStudentResponse';
export * from './pageResultTask';
export * from './pageResultTaskResponse';
export * from './pageResultTaskTemplateResponse';
export * from './pageResultTeacher';
export * from './pageResultTeacherResponse';
export * from './pageResultTenant';
@ -172,17 +220,35 @@ export * from './parentCreateRequest';
export * from './parentResponse';
export * from './parentStudentResponse';
export * from './parentUpdateRequest';
export * from './popularCourseItemResponse';
export * from './popularCoursesQueryRequest';
export * from './recentActivitiesQueryRequest';
export * from './recentActivityItemResponse';
export * from './refreshTokenRequest';
export * from './rejectCourseDto';
export * from './rejectCourseDtoChecklist';
export * from './renewRequest';
export * from './reportOverviewResponse';
export * from './reportOverviewResponseCourseStats';
export * from './resetPassword1Params';
export * from './resetPasswordParams';
export * from './resourceItem';
export * from './resourceItemCreateRequest';
export * from './resourceItemResponse';
export * from './resourceItemUpdateRequest';
export * from './resourceLibrary';
export * from './resourceLibraryCreateRequest';
export * from './resourceLibraryResponse';
export * from './resourceLibraryUpdateRequest';
export * from './resultBasicSettingsResponse';
export * from './resultCalendarViewResponse';
export * from './resultClassResponse';
export * from './resultClazz';
export * from './resultConflictCheckResult';
export * from './resultCourse';
export * from './resultCourseCollectionResponse';
export * from './resultCourseLesson';
export * from './resultCourseLessonResponse';
export * from './resultCoursePackage';
export * from './resultCoursePackageResponse';
export * from './resultCourseResponse';
@ -190,98 +256,156 @@ export * from './resultDto';
export * from './resultDtoData';
export * from './resultGrowthRecord';
export * from './resultGrowthRecordResponse';
export * from './resultImmTokenVo';
export * from './resultLesson';
export * from './resultLessonDetailResponse';
export * from './resultLessonFeedback';
export * from './resultLessonResponse';
export * from './resultLessonStep';
export * from './resultLessonStepResponse';
export * from './resultListActiveTenantItemResponse';
export * from './resultListClassResponse';
export * from './resultListClassTeacherResponse';
export * from './resultListClazz';
export * from './resultListCourse';
export * from './resultListCourseCollectionResponse';
export * from './resultListCourseLesson';
export * from './resultListCourseLessonResponse';
export * from './resultListCoursePackageResponse';
export * from './resultListCourseReportResponse';
export * from './resultListCourseResponse';
export * from './resultListGrowthRecord';
export * from './resultListGrowthRecordResponse';
export * from './resultListLesson';
export * from './resultListLessonResponse';
export * from './resultListLessonStep';
export * from './resultListLessonStepResponse';
export * from './resultListLessonTypeInfo';
export * from './resultListMapStringObject';
export * from './resultListMapStringObjectDataItem';
export * from './resultListParentStudentResponse';
export * from './resultListPopularCourseItemResponse';
export * from './resultListRecentActivityItemResponse';
export * from './resultListSchedulePlanResponse';
export * from './resultListStudent';
export * from './resultListStudentRecordResponse';
export * from './resultListStudentReportResponse';
export * from './resultListStudentResponse';
export * from './resultListTeacherReportResponse';
export * from './resultListTeacherResponse';
export * from './resultListTenantPackage';
export * from './resultListTenantResponse';
export * from './resultListTheme';
export * from './resultListThemeResponse';
export * from './resultListTimetableResponse';
export * from './resultLoginResponse';
export * from './resultLong';
export * from './resultMapStringObject';
export * from './resultMapStringObjectData';
export * from './resultNotification';
export * from './resultNotificationResponse';
export * from './resultNotificationSettingsResponse';
export * from './resultObject';
export * from './resultObjectData';
export * from './resultOperationLogResponse';
export * from './resultOssTokenVo';
export * from './resultPackageInfoResponse';
export * from './resultPackageUsageResponse';
export * from './resultPageCoursePackage';
export * from './resultPageResourceItem';
export * from './resultPageResourceLibrary';
export * from './resultPageResultClassResponse';
export * from './resultPageResultClazz';
export * from './resultPageResultCourse';
export * from './resultPageResultCourseCollectionResponse';
export * from './resultPageResultCoursePackageResponse';
export * from './resultPageResultCourseResponse';
export * from './resultPageResultGrowthRecord';
export * from './resultPageResultGrowthRecordResponse';
export * from './resultPageResultLesson';
export * from './resultPageResultLessonFeedbackResponse';
export * from './resultPageResultLessonResponse';
export * from './resultPageResultNotification';
export * from './resultPageResultNotificationResponse';
export * from './resultPageResultOperationLogResponse';
export * from './resultPageResultParent';
export * from './resultPageResultParentResponse';
export * from './resultPageResultResourceItem';
export * from './resultPageResultResourceItemResponse';
export * from './resultPageResultResourceLibrary';
export * from './resultPageResultResourceLibraryResponse';
export * from './resultPageResultSchedulePlanResponse';
export * from './resultPageResultStudent';
export * from './resultPageResultStudentResponse';
export * from './resultPageResultTask';
export * from './resultPageResultTaskResponse';
export * from './resultPageResultTaskTemplateResponse';
export * from './resultPageResultTeacher';
export * from './resultPageResultTeacherResponse';
export * from './resultPageResultTenant';
export * from './resultPageResultTenantResponse';
export * from './resultParent';
export * from './resultParentResponse';
export * from './resultReportOverviewResponse';
export * from './resultResourceItem';
export * from './resultResourceItemResponse';
export * from './resultResourceLibrary';
export * from './resultResourceLibraryResponse';
export * from './resultSchedulePlanResponse';
export * from './resultSchoolSettingsResponse';
export * from './resultSecuritySettingsResponse';
export * from './resultStatsResponse';
export * from './resultStatsTrendResponse';
export * from './resultStudent';
export * from './resultStudentRecordResponse';
export * from './resultStudentResponse';
export * from './resultTask';
export * from './resultTaskResponse';
export * from './resultTaskTemplateResponse';
export * from './resultTeacher';
export * from './resultTeacherResponse';
export * from './resultTenant';
export * from './resultTenantResponse';
export * from './resultTheme';
export * from './resultThemeResponse';
export * from './resultTimetableResponse';
export * from './resultTokenResponse';
export * from './resultUserInfoResponse';
export * from './resultVoid';
export * from './resultVoidData';
export * from './reviewDto';
export * from './reviewRequest';
export * from './scheduleCreateByClassesRequest';
export * from './schedulePlanCreateRequest';
export * from './schedulePlanResponse';
export * from './schedulePlanUpdateRequest';
export * from './schoolControllerImportStudentsParams';
export * from './schoolFeedbackControllerFindAllParams';
export * from './schoolSettingsResponse';
export * from './schoolSettingsUpdateRequest';
export * from './schoolTaskControllerGetMonthlyStatsParams';
export * from './securitySettingsResponse';
export * from './securitySettingsUpdateRequest';
export * from './statsControllerGetActiveTeachersParams';
export * from './statsControllerGetLessonTrendParams';
export * from './statsControllerGetRecentActivitiesParams';
export * from './statsResponse';
export * from './statsTrendResponse';
export * from './stepCreateRequest';
export * from './student';
export * from './studentCreateRequest';
export * from './studentRecordDto';
export * from './studentRecordRequest';
export * from './studentRecordResponse';
export * from './studentReportResponse';
export * from './studentResponse';
export * from './studentUpdateRequest';
export * from './submitCourseDto';
export * from './task';
export * from './taskCreateRequest';
export * from './taskResponse';
export * from './taskTemplateCreateRequest';
export * from './taskTemplateResponse';
export * from './taskUpdateRequest';
export * from './teacher';
export * from './teacherCourseControllerFindAllParams';
@ -292,6 +416,7 @@ export * from './teacherCourseControllerGetTeacherSchedulesParams';
export * from './teacherCourseControllerGetTeacherTimetableParams';
export * from './teacherCreateRequest';
export * from './teacherFeedbackControllerFindAllParams';
export * from './teacherReportResponse';
export * from './teacherResponse';
export * from './teacherTaskControllerGetMonthlyStatsParams';
export * from './teacherUpdateRequest';
@ -305,6 +430,8 @@ export * from './tenantResponse';
export * from './tenantUpdateRequest';
export * from './theme';
export * from './themeCreateRequest';
export * from './themeResponse';
export * from './timetableResponse';
export * from './tokenResponse';
export * from './transferStudentDto';
export * from './updateBasicSettings1Body';
@ -345,4 +472,5 @@ export * from './updateTenantStatusDto';
export * from './updateTenantStatusDtoStatus';
export * from './uploadFileBody';
export * from './uploadFileParams';
export * from './usageInfo';
export * from './userInfoResponse';

View File

@ -29,16 +29,42 @@ export interface Lesson {
classId?: number;
/** 教师 ID */
teacherId?: number;
/** 排课计划 ID */
schedulePlanId?: number;
/** 课程标题 */
title?: string;
/** 上课日期 */
lessonDate?: string;
startTime?: LocalTime;
endTime?: LocalTime;
/** 计划上课时间 */
plannedDatetime?: string;
/** 实际上课开始时间 */
startDatetime?: string;
/** 实际上课结束时间 */
endDatetime?: string;
/** 实际时长 (分钟) */
actualDuration?: number;
/** 上课地点 */
location?: string;
/** 状态 */
status?: string;
/** 整体评价 */
overallRating?: string;
/** 参与度评价 */
participationRating?: string;
/** 完成说明 */
completionNote?: string;
/** 进度数据 (JSON) */
progressData?: string;
/** 当前课程 ID */
currentLessonId?: number;
/** 当前步骤 ID */
currentStepId?: number;
/** 课程 ID 列表 (JSON) */
lessonIds?: string;
/** 已完成课程 ID 列表 (JSON) */
completedLessonIds?: string;
/** 备注 */
notes?: string;
}

View File

@ -19,6 +19,10 @@ export interface LessonResponse {
courseId?: number;
/** 班级 ID */
classId?: number;
/** 课程名称(用于列表展示) */
courseName?: string;
/** 班级名称(用于列表展示) */
className?: string;
/** 教师 ID */
teacherId?: number;
/** 标题 */

View File

@ -6,7 +6,12 @@
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface RenewRequest {
/** 到期日期 */
endDate?: string;
/** 支付金额 */
pricePaid?: number;
}

View File

@ -6,20 +6,40 @@
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface Task {
/** 主键 ID */
id?: number;
tenantId?: number;
title?: string;
description?: string;
type?: string;
courseId?: number;
creatorId?: number;
creatorRole?: string;
startDate?: string;
dueDate?: string;
status?: string;
attachments?: string;
/** 创建人 */
createBy?: string;
/** 创建时间 */
createdAt?: string;
/** 更新人 */
updateBy?: string;
/** 更新时间 */
updatedAt?: string;
deleted?: number;
/** 租户 ID */
tenantId?: number;
/** 任务标题 */
title?: string;
/** 任务描述 */
description?: string;
/** 任务类型 */
type?: string;
/** 课程 ID */
courseId?: number;
/** 创建人 ID */
creatorId?: number;
/** 创建人角色 */
creatorRole?: string;
/** 开始日期 */
startDate?: string;
/** 截止日期 */
dueDate?: string;
/** 状态 */
status?: string;
/** 附件JSON 数组) */
attachments?: string;
}

View File

@ -34,6 +34,8 @@ export interface TenantCreateRequest {
startDate?: string;
/** 结束日期 */
expireDate?: string;
/** 课程套餐 ID可选 */
packageId?: number;
/**
*
* @deprecated

View File

@ -113,17 +113,12 @@ const api = getReadingPlatformAPI();
// 获取校本课程包列表
export function getSchoolCourseList() {
return api.schoolCourseControllerFindAll() as any;
}
// 获取可创建校本课程包的源课程列表
export function getSourceCourses() {
return api.schoolCourseControllerGetSourceCourses() as any;
return api.getSchoolCourses() as any;
}
// 获取校本课程包详情
export function getSchoolCourseDetail(id: number) {
return api.schoolCourseControllerFindOne(id) as any;
return api.getSchoolCourse(id) as any;
}
// 创建校本课程包

View File

@ -36,7 +36,7 @@
</a-select>
<!-- 选择课程包 -->
<div v-if="selectedCollection && selectedCollection.packages" class="packages-section">
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
<h4>选择课程包</h4>
<div class="packages-grid">
<div
@ -46,26 +46,15 @@
@click="selectPackage(pkg.id)"
>
<div class="package-name">{{ pkg.name }}</div>
<div class="package-grade">{{ pkg.gradeLevels?.join(', ') }}</div>
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') : pkg.gradeLevels }}</div>
<div class="package-count">{{ pkg.courseCount }} 门课程</div>
</div>
</div>
</div>
<!-- 选择课程 -->
<div v-if="selectedPackage && selectedPackage.courses" class="courses-section">
<h4>选择课程</h4>
<div class="courses-grid">
<div
v-for="course in selectedPackage.courses"
:key="course.id"
:class="['course-card', { active: formData.courseId === course.id }]"
@click="selectCourse(course.id)"
>
<div class="course-name">{{ course.name }}</div>
<div class="course-grade">{{ course.gradeLevel }}</div>
</div>
</div>
<!-- 调试信息如果没有课程包显示提示 -->
<div v-else-if="selectedCollection" class="packages-section">
<h4>选择课程包</h4>
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
</div>
<!-- 排课计划参考 -->
@ -298,7 +287,7 @@ const weekDayNames: Record<number, string> = {
interface FormData {
collectionId?: number;
packageId?: number;
courseId?: number;
courseId?: number; // 使
lessonType?: LessonType;
classIds: number[];
scheduledDate?: Dayjs;
@ -318,13 +307,19 @@ const filteredClasses = computed(() => {
//
const selectedCollection = computed(() => {
if (!formData.collectionId) return null;
return collections.value.find(c => c.id === formData.collectionId) || null;
const collection = collections.value.find(c => c.id === formData.collectionId) || null;
console.log('🎯 selectedCollection:', collection);
console.log('📦 selectedCollection.packages:', collection?.packages);
return collection;
});
//
const selectedPackage = computed(() => {
if (!formData.packageId || !selectedCollection.value?.packages) return null;
return selectedCollection.value.packages.find(p => p.id === formData.packageId) || null;
const pkg = selectedCollection.value.packages.find(p => p.id === formData.packageId) || null;
console.log('📦 selectedPackage:', pkg);
console.log('📚 selectedPackage.courses:', pkg?.courses);
return pkg;
});
//
@ -356,7 +351,21 @@ const resetForm = () => {
const loadCollections = async () => {
try {
collections.value = await getCourseCollections();
console.log('📚 课程套餐列表 (API返回):', collections.value);
console.log('📚 套餐数量:', collections.value?.length);
// packages
collections.value.forEach((coll, idx) => {
console.log(` 📚 套餐[${idx}]:`, {
id: coll.id,
name: coll.name,
hasPackages: !!coll.packages,
packagesCount: coll.packages?.length || 0,
packages: coll.packages
});
});
} catch (error) {
console.error('❌ 加载课程套餐失败:', error);
message.error('加载课程套餐失败');
}
};
@ -390,27 +399,59 @@ const handleCollectionChange = async (collectionId: number) => {
if (!collectionId) return;
try {
// API: GET /v1/school/packages/{collectionId}/packages
const packages = await getCourseCollectionPackages(collectionId);
console.log('📦 API返回的课程包列表:', packages);
console.log('📦 课程包数量:', packages?.length);
if (!packages || packages.length === 0) {
message.warning('该套餐暂无课程包');
return;
}
//
packages.forEach((pkg, idx) => {
console.log(` 📦 课程包[${idx}]:`, {
id: pkg.id,
name: pkg.name,
courseCount: pkg.courseCount,
hasCourses: !!pkg.courses,
coursesCount: pkg.courses?.length || 0
});
});
//
const collection = collections.value.find(c => c.id === collectionId);
if (collection) {
collection.packages = packages;
console.log('✅ 已更新套餐的课程包列表');
}
} catch (error) {
console.error('❌ 加载课程包失败:', error);
message.error('加载课程包失败');
}
};
//
const selectPackage = async (packageId: number) => {
formData.packageId = packageId;
formData.courseId = undefined;
console.log('🎯 点击选择课程包packageId:', packageId);
console.log('📦 当前 packages 数组:', selectedCollection.value?.packages);
//
formData.packageId = packageId;
//
const foundPkg = selectedCollection.value?.packages?.find((p: any) => p.id === packageId);
console.log('🔍 找到的课程包:', foundPkg);
// API
if (selectedCollection.value?.packages) {
const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id === packageId);
if (selectedPkg?.courses && selectedPkg.courses.length > 0) {
//
//
formData.courseId = selectedPkg.courses[0].id;
console.log('✅ 自动选择第一门课程:', formData.courseId);
//
const firstCourse = selectedPkg.courses[0];
if (firstCourse.scheduleRefData) {
try {
@ -425,6 +466,7 @@ const selectPackage = async (packageId: number) => {
scheduleRefData.value = [];
}
} else {
formData.courseId = undefined;
scheduleRefData.value = [];
}
}
@ -433,11 +475,6 @@ const selectPackage = async (packageId: number) => {
await loadLessonTypes(packageId);
};
//
const selectCourse = (courseId: number) => {
formData.courseId = courseId;
};
//
const loadLessonTypes = async (packageId: number) => {
loadingLessonTypes.value = true;
@ -538,7 +575,7 @@ const validateStep = (): boolean => {
return false;
}
if (!formData.courseId) {
message.warning('请选择课程');
message.warning('课程包数据异常,请联系管理员');
return false;
}
break;
@ -604,7 +641,7 @@ const handleSubmit = async () => {
const promises = formData.classIds.map(classId => {
return createSchedulesByClasses({
coursePackageId: formData.packageId!,
courseId: formData.courseId!,
courseId: formData.courseId!, //
lessonType: formData.lessonType!,
classIds: [classId],
teacherId: classTeacherMap.value[classId],
@ -716,45 +753,6 @@ defineExpose({ open });
}
}
.courses-section {
margin-top: 24px;
}
.courses-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.course-card {
padding: 12px;
background: white;
border: 2px solid #E0E0E0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #BDBDBD;
}
&.active {
border-color: #FF8C42;
background: #FFF0E6;
}
.course-name {
font-weight: 500;
color: #2D3436;
margin-bottom: 4px;
}
.course-grade {
font-size: 12px;
color: #999;
}
}
.schedule-ref-card {
margin-top: 24px;
padding: 16px;

View File

@ -56,11 +56,11 @@ export default defineConfig({
host: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
target: 'http://localhost:8480',
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:8080',
target: 'http://localhost:8480',
changeOrigin: true,
},
},

View File

@ -8,6 +8,8 @@ import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.CourseCollectionPageQueryRequest;
import com.reading.platform.dto.response.CourseCollectionResponse;
import com.reading.platform.service.CourseCollectionService;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -106,6 +108,37 @@ public class AdminCourseCollectionController {
return Result.success();
}
@PostMapping("/{id}/grant")
@Operation(summary = "授权课程套餐给租户")
@RequireRole(UserRole.ADMIN)
public Result<Void> grantToTenant(
@PathVariable Long id,
@Valid @RequestBody GrantCollectionRequest request) {
LocalDate endDate = LocalDate.parse(request.getEndDate(), DateTimeFormatter.ISO_DATE);
collectionService.renewTenantCollection(
request.getTenantId(),
id,
endDate,
request.getPricePaid()
);
return Result.success();
}
/**
* 授权课程套餐请求
*/
@Data
public static class GrantCollectionRequest {
@Schema(description = "租户ID")
private Long tenantId;
@Schema(description = "结束日期ISO格式2024-12-31")
private String endDate;
@Schema(description = "支付价格(分)")
private Long pricePaid;
}
/**
* 创建课程套餐请求
*/

View File

@ -23,18 +23,18 @@ import java.util.List;
import java.util.stream.Collectors;
/**
* 课程套餐控制器超管端
* 课程控制器超管端
*/
@RestController
@RequestMapping("/api/v1/admin/packages")
@RequiredArgsConstructor
@Tag(name = "超管端 - 课程套餐")
@Tag(name = "超管端 - 课程包管理")
public class AdminPackageController {
private final CoursePackageService packageService;
@GetMapping
@Operation(summary = "分页查询套餐")
@Operation(summary = "分页查询课程包")
public Result<PageResult<CoursePackageResponse>> findAll(
@RequestParam(required = false) String status,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@ -43,13 +43,13 @@ public class AdminPackageController {
}
@GetMapping("/{id}")
@Operation(summary = "查询套餐详情")
@Operation(summary = "查询课程包详情")
public Result<CoursePackageResponse> findOne(@PathVariable Long id) {
return Result.success(packageService.findOnePackage(id));
}
@PostMapping
@Operation(summary = "创建套餐")
@Operation(summary = "创建课程包")
@RequireRole(UserRole.ADMIN)
public Result<CoursePackageResponse> create(@Valid @RequestBody PackageCreateRequest request) {
CoursePackage pkg = packageService.createPackage(
@ -64,7 +64,7 @@ public class AdminPackageController {
}
@PutMapping("/{id}")
@Operation(summary = "更新套餐")
@Operation(summary = "更新课程包")
@RequireRole(UserRole.ADMIN)
public Result<CoursePackageResponse> update(
@PathVariable Long id,
@ -82,7 +82,7 @@ public class AdminPackageController {
}
@DeleteMapping("/{id}")
@Operation(summary = "删除套餐")
@Operation(summary = "删除课程包")
@RequireRole(UserRole.ADMIN)
public Result<Void> delete(@PathVariable Long id) {
packageService.deletePackage(id);
@ -90,7 +90,7 @@ public class AdminPackageController {
}
@PutMapping("/{id}/courses")
@Operation(summary = "设置套餐课程")
@Operation(summary = "设置课程包课程")
@RequireRole(UserRole.ADMIN)
public Result<Void> setCourses(
@PathVariable Long id,
@ -108,7 +108,7 @@ public class AdminPackageController {
}
@PostMapping("/{id}/review")
@Operation(summary = "审核套餐")
@Operation(summary = "审核课程包")
@RequireRole(UserRole.ADMIN)
public Result<Void> review(
@PathVariable Long id,
@ -124,7 +124,7 @@ public class AdminPackageController {
}
@PostMapping("/{id}/publish")
@Operation(summary = "发布套餐")
@Operation(summary = "发布课程包")
@RequireRole(UserRole.ADMIN)
public Result<Void> publish(@PathVariable Long id) {
packageService.publishPackage(id);
@ -132,7 +132,7 @@ public class AdminPackageController {
}
@PostMapping("/{id}/offline")
@Operation(summary = "下线套餐")
@Operation(summary = "下线课程包")
@RequireRole(UserRole.ADMIN)
public Result<Void> offline(@PathVariable Long id) {
packageService.offlinePackage(id);
@ -140,7 +140,7 @@ public class AdminPackageController {
}
@GetMapping("/all")
@Operation(summary = "查询所有已发布的套餐列表")
@Operation(summary = "查询所有已发布的课程包列表")
public Result<List<CoursePackageResponse>> getPublishedPackages() {
List<CoursePackage> packages = packageService.findPublishedPackages();
List<CoursePackageResponse> responses = packages.stream()
@ -150,7 +150,8 @@ public class AdminPackageController {
}
@PostMapping("/{id}/grant")
@Operation(summary = "授权套餐给租户")
@Operation(summary = "授权课程包给租户(已废弃,请使用课程套餐授权)")
@Deprecated
@RequireRole(UserRole.ADMIN)
public Result<Void> grantToTenant(
@PathVariable Long id,

View File

@ -14,13 +14,13 @@ import java.util.List;
import java.util.stream.Collectors;
/**
* 课程管理控制器学校端
* 课程管理控制器学校端
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/school/courses")
@RequiredArgsConstructor
@Tag(name = "学校端 - 课程管理")
@Tag(name = "学校端 - 课程管理")
public class SchoolCourseController {
private final CourseService courseService;
@ -37,9 +37,9 @@ public class SchoolCourseController {
}
@GetMapping("/{id}")
@Operation(summary = "获取课程详情")
@Operation(summary = "获取课程详情")
public Result<Course> getSchoolCourse(@PathVariable Long id) {
log.info("获取课程详情id={}", id);
log.info("获取课程详情id={}", id);
Long tenantId = SecurityUtils.getCurrentTenantId();
Course course = courseService.getCourseByIdWithTenantCheck(id, tenantId);
return Result.success(course);

View File

@ -48,6 +48,17 @@ public class SchoolPackageController {
return Result.success(collectionService.getPackagesByCollection(collectionId));
}
@PostMapping("/{collectionId}/renew")
@Operation(summary = "续费课程套餐")
@RequireRole(UserRole.SCHOOL)
public Result<Void> renewCollection(
@PathVariable Long collectionId,
@RequestBody RenewRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId();
collectionService.renewTenantCollection(tenantId, collectionId, request.getEndDate(), request.getPricePaid());
return Result.success();
}
@GetMapping("/packages/{packageId}/courses")
@Operation(summary = "获取课程包下的课程列表(包含排课计划参考)")
@RequireRole(UserRole.SCHOOL)
@ -65,7 +76,8 @@ public class SchoolPackageController {
}
@PostMapping("/{id}/renew")
@Operation(summary = "续费套餐")
@Operation(summary = "续费套餐(已废弃,请使用课程套餐续费)")
@Deprecated
@RequireRole(UserRole.SCHOOL)
public Result<Void> renewPackage(
@PathVariable Long id,

View File

@ -50,7 +50,7 @@ public class TenantCreateRequest {
private LocalDate expireDate;
@Schema(description = "课程套餐 ID可选")
private Long packageId;
private Long collectionId;
@Schema(description = "过期时间(兼容旧字段)")
@Deprecated

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.reading.platform.common.enums.CourseStatus;
import com.reading.platform.common.enums.TenantPackageStatus;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.dto.response.CourseCollectionResponse;
import com.reading.platform.dto.response.CoursePackageResponse;
@ -155,7 +156,8 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
.eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode())
);
return packages.stream()
// 转换为响应对象并设置排序号
List<CoursePackageResponse> result = packages.stream()
.map(pkg -> {
CoursePackageResponse response = toPackageResponse(pkg);
// 设置排序号
@ -166,6 +168,9 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
return response;
})
.collect(Collectors.toList());
log.info("查询到{}个课程包", result.size());
return result;
}
/**
@ -257,13 +262,47 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
public void deleteCollection(Long id) {
log.info("删除课程套餐id={}", id);
// 删除关联关系
collectionPackageMapper.delete(
// 检查是否有租户正在使用此课程套餐
Long tenantCount = tenantPackageMapper.selectCount(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getCollectionId, id)
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
);
if (tenantCount > 0) {
log.warn("删除课程套餐失败,有 {} 个租户正在使用此套餐id={}", tenantCount, id);
throw new BusinessException("" + tenantCount + " 个租户正在使用此课程套餐,无法删除");
}
// 清理所有状态的租户套餐关联记录包括非活跃状态
List<TenantPackage> allTenantPackages = tenantPackageMapper.selectList(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getCollectionId, id)
);
if (!allTenantPackages.isEmpty()) {
tenantPackageMapper.delete(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getCollectionId, id)
);
log.info("已清理 {} 条租户套餐关联记录", allTenantPackages.size());
}
// 删除课程套餐与课程包的关联关系
List<CourseCollectionPackage> collectionPackages = collectionPackageMapper.selectList(
new LambdaQueryWrapper<CourseCollectionPackage>()
.eq(CourseCollectionPackage::getCollectionId, id)
);
// 删除套餐
if (!collectionPackages.isEmpty()) {
collectionPackageMapper.delete(
new LambdaQueryWrapper<CourseCollectionPackage>()
.eq(CourseCollectionPackage::getCollectionId, id)
);
log.info("已清理 {} 条课程包关联记录", collectionPackages.size());
}
// 删除课程套餐
collectionMapper.deleteById(id);
log.info("课程套餐删除成功id={}", id);
@ -288,6 +327,53 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
log.info("课程套餐发布成功id={}", id);
}
/**
* 续费租户课程套餐
*/
@Transactional(rollbackFor = Exception.class)
public void renewTenantCollection(Long tenantId, Long collectionId, java.time.LocalDate endDate, Long pricePaid) {
log.info("续费租户课程套餐tenantId={}, collectionId={}, endDate={}, pricePaid={}", tenantId, collectionId, endDate, pricePaid);
// 查询现有租户套餐关联
List<TenantPackage> existingPackages = tenantPackageMapper.selectList(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getTenantId, tenantId)
.eq(TenantPackage::getCollectionId, collectionId)
);
if (!existingPackages.isEmpty()) {
// 更新现有记录
TenantPackage existing = existingPackages.get(0);
existing.setEndDate(endDate);
existing.setStatus(TenantPackageStatus.ACTIVE);
if (pricePaid != null) {
existing.setPricePaid(pricePaid);
}
existing.setUpdatedAt(LocalDateTime.now());
tenantPackageMapper.updateById(existing);
log.info("租户课程套餐续费成功tenantId={}, collectionId={}", tenantId, collectionId);
} else {
// 查询课程套餐信息
CourseCollection collection = collectionMapper.selectById(collectionId);
if (collection == null) {
throw new IllegalArgumentException("课程套餐不存在");
}
// 创建新记录
TenantPackage tp = new TenantPackage();
tp.setTenantId(tenantId);
tp.setCollectionId(collectionId);
tp.setStartDate(java.time.LocalDate.now());
tp.setEndDate(endDate);
tp.setStatus(TenantPackageStatus.ACTIVE);
tp.setPricePaid(pricePaid != null ? pricePaid :
(collection.getDiscountPrice() != null ? collection.getDiscountPrice() : collection.getPrice()));
tp.setCreatedAt(LocalDateTime.now());
tenantPackageMapper.insert(tp);
log.info("租户课程套餐新办成功tenantId={}, collectionId={}", tenantId, collectionId);
}
}
/**
* 转换为响应对象
*/

View File

@ -1,6 +1,8 @@
package com.reading.platform.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -12,10 +14,12 @@ import com.reading.platform.dto.response.CoursePackageResponse;
import com.reading.platform.entity.Course;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.entity.CoursePackageCourse;
import com.reading.platform.entity.CourseCollectionPackage;
import com.reading.platform.entity.TenantPackage;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.mapper.CoursePackageCourseMapper;
import com.reading.platform.mapper.CoursePackageMapper;
import com.reading.platform.mapper.CourseCollectionPackageMapper;
import com.reading.platform.mapper.TenantPackageMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -41,6 +45,7 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
private final CoursePackageCourseMapper packageCourseMapper;
private final TenantPackageMapper tenantPackageMapper;
private final CourseMapper courseMapper;
private final CourseCollectionPackageMapper collectionPackageMapper;
/**
* 分页查询套餐
@ -110,13 +115,29 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
.updatedAt(pkg.getUpdatedAt())
.build();
// 计算使用该套餐的租户数量
Long tenantCount = tenantPackageMapper.selectCount(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getPackageId, pkg.getId())
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
// 计算使用包含此课程包的课程套餐的租户数量三层架构
// 1. 查询包含此课程包的课程套餐
List<CourseCollectionPackage> collectionPackages = collectionPackageMapper.selectList(
new LambdaQueryWrapper<CourseCollectionPackage>()
.eq(CourseCollectionPackage::getPackageId, pkg.getId())
);
response.setTenantCount(tenantCount.intValue());
Integer tenantCount = 0;
if (!collectionPackages.isEmpty()) {
// 2. 获取课程套餐 ID 列表
List<Long> collectionIds = collectionPackages.stream()
.map(CourseCollectionPackage::getCollectionId)
.distinct()
.collect(Collectors.toList());
// 3. 统计使用这些套餐的租户数量去重
tenantCount = tenantPackageMapper.selectCount(
new LambdaQueryWrapper<TenantPackage>()
.in(TenantPackage::getCollectionId, collectionIds)
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
).intValue();
}
response.setTenantCount(tenantCount);
// 查询套餐包含的课程包过滤下架状态
List<CoursePackageCourse> packageCourses = packageCourseMapper.selectList(
@ -154,6 +175,8 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
item.setGradeLevel(pkc.getGradeLevel());
item.setSortOrder(pkc.getSortOrder());
item.setScheduleRefData(course.getScheduleRefData());
// schedule_ref_data 解析 lessonType
item.setLessonType(parseLessonType(course.getScheduleRefData()));
return item;
})
.filter(Objects::nonNull)
@ -245,21 +268,48 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
*/
@Transactional(rollbackFor = Exception.class)
public void deletePackage(Long id) {
log.info("删除套餐id={}", id);
// 检查是否有租户正在使用
Long tenantCount = tenantPackageMapper.selectCount(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getPackageId, id)
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
log.info("删除课程包id={}", id);
// 三层架构下检查查询包含此课程包的课程套餐
List<CourseCollectionPackage> collectionPackages = collectionPackageMapper.selectList(
new LambdaQueryWrapper<CourseCollectionPackage>()
.eq(CourseCollectionPackage::getPackageId, id)
);
if (tenantCount > 0) {
log.warn("删除套餐失败,有 {} 个租户正在使用该套餐id={}", tenantCount, id);
throw new BusinessException("" + tenantCount + " 个租户正在使用该套餐,无法删除");
if (!collectionPackages.isEmpty()) {
// 检查这些套餐是否被租户使用
List<Long> collectionIds = collectionPackages.stream()
.map(CourseCollectionPackage::getCollectionId)
.distinct()
.collect(Collectors.toList());
Long tenantCount = tenantPackageMapper.selectCount(
new LambdaQueryWrapper<TenantPackage>()
.in(TenantPackage::getCollectionId, collectionIds)
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
);
if (tenantCount > 0) {
log.warn("删除课程包失败,有 {} 个租户正在使用包含此课程包的套餐id={}", tenantCount, id);
throw new BusinessException("" + tenantCount + " 个租户正在使用包含此课程包的课程套餐,无法删除");
}
// 清理课程套餐与课程包的关联
collectionPackageMapper.delete(
new LambdaQueryWrapper<CourseCollectionPackage>()
.eq(CourseCollectionPackage::getPackageId, id)
);
log.info("已清理课程包与课程套餐的关联packageId={}", id);
}
// 删除课程包与课程的关联
packageCourseMapper.delete(
new LambdaQueryWrapper<CoursePackageCourse>()
.eq(CoursePackageCourse::getPackageId, id)
);
packageMapper.deleteById(id);
log.info("套餐删除成功id={}", id);
log.info("课程包删除成功id={}", id);
}
/**
@ -404,8 +454,10 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
}
/**
* 查询租户套餐
* 查询租户套餐已废弃 - 三层架构下应使用 CourseCollectionService.findTenantCollections
* @deprecated 三层架构下租户关联的是课程套餐而非课程包请使用 {@link com.reading.platform.service.CourseCollectionService#findTenantCollections}
*/
@Deprecated
public List<CoursePackageResponse> findTenantPackages(Long tenantId) {
log.info("查询租户套餐tenantId={}", tenantId);
// 查询租户的套餐关联
@ -437,8 +489,10 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
}
/**
* 续费套餐
* 续费套餐已废弃 - 三层架构下应使用 CourseCollectionService.renewTenantCollection
* @deprecated 三层架构下租户应关联课程套餐而非直接关联课程包请使用 {@link com.reading.platform.service.CourseCollectionService#renewTenantCollection}
*/
@Deprecated
@Transactional(rollbackFor = Exception.class)
public void renewTenantPackage(Long tenantId, Long packageId, LocalDate endDate, Long pricePaid) {
log.info("续费套餐tenantId={}, packageId={}, endDate={}, pricePaid={}", tenantId, packageId, endDate, pricePaid);
@ -484,4 +538,49 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
log.info("查询所有已发布的套餐列表成功count={}", packages.size());
return packages;
}
/**
* schedule_ref_data 解析 lessonType
* 将中文课程类型映射到英文枚举值
*/
private String parseLessonType(String scheduleRefData) {
if (scheduleRefData == null || scheduleRefData.isEmpty()) {
return null;
}
try {
JSONArray array = JSON.parseArray(scheduleRefData);
if (array != null && !array.isEmpty()) {
JSONObject firstItem = array.getJSONObject(0);
if (firstItem != null) {
String lessonType = firstItem.getString("lessonType");
return mapLessonTypeToEnum(lessonType);
}
}
} catch (Exception e) {
log.debug("解析 schedule_ref_data 失败: {}", e.getMessage());
}
return null;
}
/**
* 将中文课程类型映射到英文枚举值
*/
private String mapLessonTypeToEnum(String chineseType) {
if (chineseType == null) {
return null;
}
return switch (chineseType) {
case "导入课" -> "INTRODUCTION";
case "集体课" -> "COLLECTIVE";
case "五大领域语言课" -> "LANGUAGE";
case "五大领域艺术课" -> "ART";
case "五大领域科学课" -> "SCIENCE";
case "五大领域健康课" -> "HEALTH";
case "五大领域社会课" -> "SOCIETY";
default -> null;
};
}
}

View File

@ -18,12 +18,14 @@ import com.reading.platform.entity.CourseLesson;
import com.alibaba.fastjson2.JSON;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.entity.CoursePackageCourse;
import com.reading.platform.entity.CourseCollectionPackage;
import com.reading.platform.entity.LessonStep;
import com.reading.platform.entity.TenantPackage;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.mapper.CoursePackageCourseMapper;
import com.reading.platform.mapper.CoursePackageMapper;
import com.reading.platform.mapper.TenantPackageMapper;
import com.reading.platform.mapper.CourseCollectionPackageMapper;
import com.reading.platform.service.CourseLessonService;
import com.reading.platform.service.CourseService;
import lombok.RequiredArgsConstructor;
@ -49,6 +51,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
private final TenantPackageMapper tenantPackageMapper;
private final CoursePackageCourseMapper packageCourseMapper;
private final CoursePackageMapper packageMapper;
private final CourseCollectionPackageMapper collectionPackageMapper;
@Override
@Transactional
@ -358,16 +361,85 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
@Override
public Page<Course> getCoursePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String category, String status) {
Page<Course> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
log.info("查询租户课程分页tenantId={}, pageNum={}, pageSize={}, keyword={}, category={}, status={}",
tenantId, pageNum, pageSize, keyword, category, status);
// Include both tenant courses and system courses
wrapper.and(w -> w
.eq(Course::getTenantId, tenantId)
.or()
.eq(Course::getIsSystem, YesNo.YES.getCode())
// 使用三层架构查询租户 课程套餐 课程包 课程
// 1. 查询租户的课程套餐关联
List<TenantPackage> tenantPackages = tenantPackageMapper.selectList(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getTenantId, tenantId)
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
.isNotNull(TenantPackage::getCollectionId)
);
if (tenantPackages.isEmpty()) {
log.info("租户没有关联课程套餐tenantId={}", tenantId);
return new Page<>(pageNum, pageSize);
}
// 2. 获取课程套餐 ID 列表
List<Long> collectionIds = tenantPackages.stream()
.map(TenantPackage::getCollectionId)
.distinct()
.collect(Collectors.toList());
// 3. 查询课程套餐下的课程包关联
List<CourseCollectionPackage> collectionPackages = collectionPackageMapper.selectList(
new LambdaQueryWrapper<CourseCollectionPackage>()
.in(CourseCollectionPackage::getCollectionId, collectionIds)
);
if (collectionPackages.isEmpty()) {
log.info("课程套餐下没有关联课程包tenantId={}", tenantId);
return new Page<>(pageNum, pageSize);
}
// 4. 获取课程包 ID 列表
List<Long> packageIds = collectionPackages.stream()
.map(CourseCollectionPackage::getPackageId)
.collect(Collectors.toList());
// 5. 查询课程包列表并过滤下架状态
List<CoursePackage> coursePackages = packageMapper.selectList(
new LambdaQueryWrapper<CoursePackage>()
.in(CoursePackage::getId, packageIds)
.ne(CoursePackage::getStatus, CourseStatus.ARCHIVED.getCode())
);
Set<Long> validPackageIds = coursePackages.stream()
.map(CoursePackage::getId)
.collect(Collectors.toSet());
if (validPackageIds.isEmpty()) {
log.info("课程包均为下架状态tenantId={}", tenantId);
return new Page<>(pageNum, pageSize);
}
// 6. 查询课程包包含的课程 ID
List<CoursePackageCourse> packageCourses = packageCourseMapper.selectList(
new LambdaQueryWrapper<CoursePackageCourse>()
.in(CoursePackageCourse::getPackageId, validPackageIds)
);
if (packageCourses.isEmpty()) {
log.info("课程包下没有关联的课程tenantId={}", tenantId);
return new Page<>(pageNum, pageSize);
}
// 7. 获取课程 ID 列表
List<Long> courseIds = packageCourses.stream()
.map(CoursePackageCourse::getCourseId)
.distinct()
.collect(Collectors.toList());
// 8. 分页查询课程详情应用过滤条件
Page<Course> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<Course>()
.in(Course::getId, courseIds)
.ne(Course::getStatus, CourseStatus.ARCHIVED.getCode());
// 应用过滤条件
if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w
.like(Course::getName, keyword)
@ -383,7 +455,9 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
}
wrapper.orderByDesc(Course::getCreatedAt);
return courseMapper.selectPage(page, wrapper);
Page<Course> result = courseMapper.selectPage(page, wrapper);
log.info("查询租户课程分页成功tenantId={}, total={}", tenantId, result.getTotal());
return result;
}
@Override
@ -427,6 +501,14 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
@Transactional
public void deleteCourse(Long id) {
getCourseById(id);
// 清理课程包与课程的关联记录
int deletedCount = packageCourseMapper.delete(
new LambdaQueryWrapper<CoursePackageCourse>()
.eq(CoursePackageCourse::getCourseId, id)
);
log.info("已清理课程包关联记录courseId={}, count={}", id, deletedCount);
courseMapper.deleteById(id);
log.info("课程删除成功id={}", id);
}
@ -467,40 +549,51 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
@Override
public List<Course> getCoursesByTenantId(Long tenantId) {
return courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.and(w -> w
.eq(Course::getTenantId, tenantId)
.or()
.eq(Course::getIsSystem, YesNo.YES.getCode())
)
.eq(Course::getStatus, CourseStatus.PUBLISHED.getCode())
.orderByAsc(Course::getName)
);
log.info("查询租户课程列表tenantId={}", tenantId);
// 使用三层架构查询租户 课程套餐 课程包 课程
return getTenantPackageCourses(tenantId);
}
@Override
public List<Course> getTenantPackageCourses(Long tenantId, String keyword, String grade) {
log.info("查询租户套餐下的课程tenantId={}, keyword={}, grade={}", tenantId, keyword, grade);
log.info("查询租户套餐下的课程三层架构tenantId={}, keyword={}, grade={}", tenantId, keyword, grade);
// 1. 查询租户的套餐关联
// 1. 查询租户的课程套餐关联
List<TenantPackage> tenantPackages = tenantPackageMapper.selectList(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getTenantId, tenantId)
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE)
.isNotNull(TenantPackage::getCollectionId) // 使用新的 collectionId 字段
);
if (tenantPackages.isEmpty()) {
log.info("租户套餐下的课程查询结果为空tenantId={}", tenantId);
log.info("租户没有关联课程套餐tenantId={}", tenantId);
return List.of();
}
// 2. 获取套餐 ID 列表
List<Long> packageIds = tenantPackages.stream()
.map(TenantPackage::getPackageId)
// 2. 获取课程套餐 ID 列表
List<Long> collectionIds = tenantPackages.stream()
.map(TenantPackage::getCollectionId)
.distinct()
.collect(Collectors.toList());
// 3. 查询课程包列表并过滤下架状态
// 3. 查询课程套餐下的课程包关联
List<CourseCollectionPackage> collectionPackages = collectionPackageMapper.selectList(
new LambdaQueryWrapper<CourseCollectionPackage>()
.in(CourseCollectionPackage::getCollectionId, collectionIds)
);
if (collectionPackages.isEmpty()) {
log.info("课程套餐下没有关联课程包tenantId={}", tenantId);
return List.of();
}
// 4. 获取课程包 ID 列表
List<Long> packageIds = collectionPackages.stream()
.map(CourseCollectionPackage::getPackageId)
.collect(Collectors.toList());
// 5. 查询课程包列表并过滤下架状态
List<CoursePackage> coursePackages = packageMapper.selectList(
new LambdaQueryWrapper<CoursePackage>()
.in(CoursePackage::getId, packageIds)
@ -511,35 +604,35 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
.collect(Collectors.toSet());
if (validPackageIds.isEmpty()) {
log.info("租户套餐下的课程包均为下架状态tenantId={}", tenantId);
log.info("课程包均为下架状态tenantId={}", tenantId);
return List.of();
}
// 4. 查询套餐包含的课程 ID只包含有效课程包
// 6. 查询课程包包含的课程 ID
List<CoursePackageCourse> packageCourses = packageCourseMapper.selectList(
new LambdaQueryWrapper<CoursePackageCourse>()
.in(CoursePackageCourse::getPackageId, validPackageIds)
);
if (packageCourses.isEmpty()) {
log.info("租户套餐下没有关联的课程tenantId={}", tenantId);
log.info("课程包下没有关联的课程tenantId={}", tenantId);
return List.of();
}
// 5. 获取课程 ID 列表
// 7. 获取课程 ID 列表
List<Long> courseIds = packageCourses.stream()
.map(CoursePackageCourse::getCourseId)
.distinct()
.collect(Collectors.toList());
// 6. 查询课程包详情过滤下架状态
// 8. 查询课程详情过滤下架状态
List<Course> courses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.in(Course::getId, courseIds)
.ne(Course::getStatus, CourseStatus.ARCHIVED.getCode())
);
// 7. 按关键词和年级筛选
// 9. 按关键词和年级筛选
String kw = StringUtils.hasText(keyword) ? keyword.trim().toLowerCase() : null;
List<String> gradeKeys = parseGradeFilter(grade);
if (kw != null || !gradeKeys.isEmpty()) {
@ -549,7 +642,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
.collect(Collectors.toList());
}
log.info("查询租户套餐下的课程成功tenantId={}, count={}", tenantId, courses.size());
log.info("查询租户套餐下的课程成功(三层架构:套餐→课程包→课程)tenantId={}, count={}", tenantId, courses.size());
return courses;
}

View File

@ -1,5 +1,8 @@
package com.reading.platform.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -11,7 +14,11 @@ import com.reading.platform.dto.request.ScheduleCreateByClassesRequest;
import com.reading.platform.dto.response.CalendarViewResponse;
import com.reading.platform.dto.response.ConflictCheckResult;
import com.reading.platform.dto.response.LessonTypeInfo;
import com.reading.platform.entity.Course;
import com.reading.platform.entity.CoursePackageCourse;
import com.reading.platform.entity.SchedulePlan;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.mapper.CoursePackageCourseMapper;
import com.reading.platform.mapper.SchedulePlanMapper;
import com.reading.platform.service.ScheduleConflictService;
import com.reading.platform.service.SchoolScheduleService;
@ -37,6 +44,8 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
private final SchedulePlanMapper schedulePlanMapper;
private final ScheduleConflictService scheduleConflictService;
private final CoursePackageCourseMapper coursePackageCourseMapper;
private final CourseMapper courseMapper;
/**
* 最大重复周数限制
@ -287,39 +296,97 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
public List<LessonTypeInfo> getCoursePackageLessonTypes(Long tenantId, Long coursePackageId) {
log.info("获取课程包的课程类型列表: tenantId={}, coursePackageId={}", tenantId, coursePackageId);
// 查询课程包下的课程ID列表
// 这里需要查询 course_package_course 表获取课程包关联的课程
// 然后查询 course_lesson 表按 lesson_type 分组统计
// TODO: 实现查询课程包下的课程类型统计
// 1. 根据 coursePackageId 查询 course_package_course 获取 courseId 列表
// 2. 根据 courseId 列表查询 course_lesson lesson_type 分组
// 3. 统计每个 lesson_type 的数量
List<CoursePackageCourse> packageCourses = coursePackageCourseMapper.selectList(
new LambdaQueryWrapper<CoursePackageCourse>()
.eq(CoursePackageCourse::getPackageId, coursePackageId)
.orderByAsc(CoursePackageCourse::getSortOrder)
);
// 临时返回示例数据
List<LessonTypeInfo> types = new ArrayList<>();
types.add(LessonTypeInfo.builder()
.lessonType("INTRODUCTION")
.lessonTypeName("导入课")
.count(1L)
.build());
types.add(LessonTypeInfo.builder()
.lessonType("COLLECTIVE")
.lessonTypeName("集体课")
.count(1L)
.build());
types.add(LessonTypeInfo.builder()
.lessonType("LANGUAGE")
.lessonTypeName("语言课")
.count(1L)
.build());
types.add(LessonTypeInfo.builder()
.lessonType("ART")
.lessonTypeName("艺术课")
.count(1L)
.build());
if (packageCourses.isEmpty()) {
log.warn("课程包下没有课程: coursePackageId={}", coursePackageId);
return new ArrayList<>();
}
return types;
List<Long> courseIds = packageCourses.stream()
.map(CoursePackageCourse::getCourseId)
.collect(Collectors.toList());
// 2. 根据 courseId 列表查询 course
List<Course> courses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.in(Course::getId, courseIds)
.eq(Course::getStatus, "PUBLISHED")
);
if (courses.isEmpty()) {
log.warn("没有找到已发布的课程: courseIds={}", courseIds);
return new ArrayList<>();
}
// 3. schedule_ref_data 中提取 lessonType 并统计
Map<String, LessonTypeInfo> lessonTypeMap = new LinkedHashMap<>();
for (Course course : courses) {
String scheduleRefData = course.getScheduleRefData();
if (!StringUtils.hasText(scheduleRefData)) {
continue;
}
try {
// 解析 JSON 数组
JSONArray jsonArray = JSON.parseArray(scheduleRefData);
if (jsonArray != null && !jsonArray.isEmpty()) {
JSONObject firstItem = jsonArray.getJSONObject(0);
if (firstItem != null && firstItem.containsKey("lessonType")) {
String lessonType = firstItem.getString("lessonType");
if (StringUtils.hasText(lessonType)) {
// 转换为英文代码
String lessonTypeCode = convertLessonTypeNameToCode(lessonType);
lessonTypeMap.compute(lessonTypeCode, (k, v) -> {
if (v == null) {
return LessonTypeInfo.builder()
.lessonType(lessonTypeCode)
.lessonTypeName(lessonType)
.count(1L)
.build();
} else {
v.setCount(v.getCount() + 1);
return v;
}
});
}
}
}
} catch (Exception e) {
log.warn("解析课程排课参考数据失败: courseId={}, error={}", course.getId(), e.getMessage());
}
}
List<LessonTypeInfo> result = new ArrayList<>(lessonTypeMap.values());
log.info("课程包课程类型统计完成: coursePackageId={}, types={}", coursePackageId, result.size());
return result;
}
/**
* 将中文课程类型名称转换为英文代码
*/
private String convertLessonTypeNameToCode(String lessonTypeName) {
if (lessonTypeName == null) {
return "UNKNOWN";
}
return switch (lessonTypeName) {
case "导入课" -> "INTRODUCTION";
case "集体课" -> "COLLECTIVE";
case "五大领域语言课" -> "LANGUAGE";
case "五大领域艺术课" -> "ART";
case "五大领域科学课" -> "SCIENCE";
case "五大领域社会课" -> "SOCIETY";
case "五大领域健康课" -> "HEALTH";
default -> "UNKNOWN";
};
}
@Override

View File

@ -17,6 +17,8 @@ import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.mapper.TenantMapper;
import com.reading.platform.mapper.TenantPackageMapper;
import com.reading.platform.mapper.CourseCollectionMapper;
import com.reading.platform.entity.CourseCollection;
import com.reading.platform.service.CoursePackageService;
import com.reading.platform.service.TenantService;
import lombok.RequiredArgsConstructor;
@ -41,6 +43,7 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
private final TenantMapper tenantMapper;
private final TenantPackageMapper tenantPackageMapper;
private final CoursePackageService coursePackageService;
private final CourseCollectionMapper collectionMapper;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
@ -81,16 +84,16 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
tenant.setStatus("ACTIVE");
// 如果传入了 packageId查询套餐信息并填充相关字段
if (request.getPackageId() != null) {
CoursePackage coursePackage = coursePackageService.getById(request.getPackageId());
if (coursePackage == null) {
log.warn("课程套餐不存在,packageId: {}", request.getPackageId());
// 如果传入了 collectionId查询课程套餐信息并填充相关字段
if (request.getCollectionId() != null) {
CourseCollection collection = collectionMapper.selectById(request.getCollectionId());
if (collection == null) {
log.warn("课程套餐不存在,collectionId: {}", request.getCollectionId());
throw new BusinessException(ErrorCode.PACKAGE_NOT_FOUND, "课程套餐不存在");
}
// 根据套餐信息填充租户字段
tenant.setPackageType(coursePackage.getName());
tenant.setPackageType(collection.getName());
tenant.setTeacherQuota(request.getTeacherQuota() != null ? request.getTeacherQuota() : 20);
tenant.setStudentQuota(request.getStudentQuota() != null ? request.getStudentQuota() : 200);
tenant.setStartDate(request.getStartDate());
@ -99,19 +102,19 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
// 使用 MP insert 方法
tenantMapper.insert(tenant);
// 创建租户套餐关联记录
// 创建租户套餐关联记录使用三层架构的 collectionId
TenantPackage tenantPackage = new TenantPackage();
tenantPackage.setTenantId(tenant.getId());
tenantPackage.setPackageId(request.getPackageId());
tenantPackage.setCollectionId(request.getCollectionId());
tenantPackage.setStartDate(request.getStartDate() != null ? request.getStartDate() : LocalDate.now());
tenantPackage.setEndDate(request.getExpireDate());
tenantPackage.setPricePaid(coursePackage.getDiscountPrice() != null ? coursePackage.getDiscountPrice() : coursePackage.getPrice());
tenantPackage.setPricePaid(collection.getDiscountPrice() != null ? collection.getDiscountPrice() : collection.getPrice());
tenantPackage.setStatus(TenantPackageStatus.ACTIVE);
tenantPackageMapper.insert(tenantPackage);
log.info("租户创建成功并关联套餐ID: {}, packageId: {}", tenant.getId(), request.getPackageId());
log.info("租户创建成功并关联课程套餐ID: {}, collectionId: {}", tenant.getId(), request.getCollectionId());
} else {
// 没有传入 packageId使用原有逻辑
// 没有传入 collectionId使用原有逻辑
tenant.setPackageType(request.getPackageType() != null ? request.getPackageType() : "STANDARD");
tenant.setTeacherQuota(request.getTeacherQuota() != null ? request.getTeacherQuota() : 20);
tenant.setStudentQuota(request.getStudentQuota() != null ? request.getStudentQuota() : 200);
@ -231,6 +234,14 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
log.info("开始删除租户ID: {}", id);
Tenant tenant = getTenantById(id);
// 清理租户套餐关联记录
int deletedCount = tenantPackageMapper.delete(
new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getTenantId, id)
);
log.info("已清理租户套餐关联记录tenantId={}, count={}", id, deletedCount);
tenantMapper.deleteById(id);
log.info("租户删除成功ID: {}", id);

View File

@ -33,7 +33,7 @@ spring:
validate-on-migrate: false
baseline-on-migrate: true
baseline-version: 0
repair-on-migrate: true
clean-on-validation-error: true
out-of-order: true
# Druid 连接池配置(开发环境)

View File

@ -10,7 +10,7 @@ spring:
active: ${SPRING_PROFILES_ACTIVE:dev}
server:
port: ${SERVER_PORT:8080}
port: ${SERVER_PORT:8480}
# MyBatis-Plus 配置(共用)
mybatis-plus: