diff --git a/.CurrentUserAllHosts b/.CurrentUserAllHosts new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml new file mode 100644 index 0000000..1e067cb --- /dev/null +++ b/.github/workflows/api-check.yml @@ -0,0 +1,55 @@ +name: API Check + +on: + pull_request: + branches: + - main + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check api-spec.yml exists + run: | + if [ ! -f "reading-platform-frontend/api-spec.yml" ]; then + echo "❌ 错误:api-spec.yml 不存在" + echo "请在后端修改接口后运行:npm run api:update" + exit 1 + fi + echo "✅ api-spec.yml 存在" + + - name: Check generated files exist + run: | + if [ ! -f "reading-platform-frontend/src/api/generated/api.ts" ]; then + echo "❌ 错误:src/api/generated/api.ts 不存在" + echo "请在后端修改接口后运行:npm run api:update" + exit 1 + fi + if [ ! -f "reading-platform-frontend/src/api/generated/model.ts" ]; then + echo "❌ 错误:src/api/generated/model.ts 不存在" + echo "请在后端修改接口后运行:npm run api:update" + exit 1 + fi + echo "✅ API 生成文件存在" + + - name: Check generated files are not empty + run: | + if [ ! -s "reading-platform-frontend/src/api/generated/api.ts" ]; then + echo "❌ 错误:api.ts 文件为空" + exit 1 + fi + if [ ! -s "reading-platform-frontend/src/api/generated/model.ts" ]; then + echo "❌ 错误:model.ts 文件为空" + exit 1 + fi + echo "✅ API 生成文件不为空" + + - name: Summary + run: | + echo "✅ API 规范检查通过" + echo "请确保:" + echo "1. 后端修改接口后已运行:npm run api:update" + echo "2. 已提交 api-spec.yml" + echo "3. 已提交 src/api/generated/ 下的文件" diff --git a/API 对比分析.md b/API 对比分析.md new file mode 100644 index 0000000..6d1ff5f --- /dev/null +++ b/API 对比分析.md @@ -0,0 +1,161 @@ +# API 对比分析报告 + +## 概述 +对比 reading-platform-backend (Node.js/NestJS) 和 reading-platform-java (Spring Boot) 的 API 接口差异 + +--- + +## 已补全的接口 + +### 1. 任务管理相关接口(SchoolTaskController, TeacherTaskController) + +#### 任务统计接口 +- `GET /api/v1/school/tasks/stats` - 获取任务统计数据 +- `GET /api/v1/school/tasks/stats/by-type` - 按任务类型统计 +- `GET /api/v1/school/tasks/stats/by-class` - 按班级统计 +- `GET /api/v1/school/tasks/stats/monthly` - 月度统计趋势 + +#### 任务完成情况接口 +- `GET /api/v1/school/tasks/:id/completions` - 获取任务完成情况分页 +- `PUT /api/v1/school/tasks/:taskId/completions/:studentId` - 更新任务完成状态 + +#### 任务模板接口 +- `GET /api/v1/school/task-templates` - 获取任务模板列表 +- `GET /api/v1/school/task-templates/:id` - 获取单个模板 +- `GET /api/v1/school/task-templates/default/:taskType` - 获取默认模板 +- `POST /api/v1/school/task-templates` - 创建模板 +- `PUT /api/v1/school/task-templates/:id` - 更新模板 +- `DELETE /api/v1/school/task-templates/:id` - 删除模板 +- `POST /api/v1/school/tasks/from-template` - 从模板创建任务 + +### 2. 通知相关接口 + +#### 学校管理员通知(SchoolNotificationController - 新增) +- `GET /api/v1/school/notifications` - 获取通知列表 +- `GET /api/v1/school/notifications/:id` - 根据 ID 获取通知 +- `GET /api/v1/school/notifications/unread-count` - 获取未读数量 +- `POST /api/v1/school/notifications/:id/read` - 标记已读 +- `POST /api/v1/school/notifications/read-all` - 全部标记已读 + +### 3. 排课和课表相关接口(SchoolScheduleController) +- `GET /api/v1/school/schedules/timetable` - 获取课表(带日期范围) +- `POST /api/v1/school/schedules/batch` - 批量创建排课 +- `GET /api/v1/school/schedules/templates/:id` - 获取单个模板 +- `PUT /api/v1/school/schedules/templates/:id` - 更新模板 +- `POST /api/v1/school/schedules/templates/:id/apply` - 应用模板 + +### 4. 新增 DTO +- `TaskTemplateCreateRequest` - 任务模板创建请求 +- `TaskTemplateUpdateRequest` - 任务模板更新请求 +- `CreateTaskFromTemplateRequest` - 从模板创建任务请求 +- `SchedulePlanCreateRequest` - 课表计划创建请求 +- `ScheduleTemplateApplyRequest` - 课表模板应用请求 + +--- + +## 仍需补全的接口 + +### P1 - 重要功能 + +#### 1. 成长档案接口(SchoolGrowthController, TeacherGrowthController) +- `GET /api/v1/school/students/:studentId/growth-records` - 按学生查询成长档案 +- `GET /api/v1/school/classes/:classId/growth-records` - 按班级查询成长档案 +- `GET /api/v1/teacher/classes/:classId/growth-records` - 教师端按班级查询 + +#### 2. 教师端课时增强接口(TeacherLessonController) +- `POST /api/v1/teacher/lessons/:id/finish` - 完成课时(带反馈数据) +- `POST /api/v1/teacher/lessons/:id/students/:studentId/record` - 保存学生记录 +- `GET /api/v1/teacher/lessons/:id/student-records` - 获取学生记录 +- `POST /api/v1/teacher/lessons/:id/student-records/batch` - 批量保存学生记录 +- `POST /api/v1/teacher/lessons/:id/feedback` - 提交反馈 +- `GET /api/v1/teacher/lessons/:id/feedback` - 获取反馈 + +#### 3. 教师端课程增强接口(TeacherCourseController) +- `GET /api/v1/teacher/courses/classes` - 获取班级列表 +- `GET /api/v1/teacher/students` - 获取所有学生 +- `GET /api/v1/teacher/classes/:id/students` - 获取班级学生 +- `GET /api/v1/teacher/classes/:id/teachers` - 获取班级教师 +- `GET /api/v1/teacher/schedules/timetable` - 获取课表 +- `GET /api/v1/teacher/schedules/today` - 获取今天排课 +- `POST /api/v1/teacher/schedules` - 创建排课 +- `PUT /api/v1/teacher/schedules/:id` - 更新排课 +- `DELETE /api/v1/teacher/schedules/:id` - 取消排课 + +#### 4. 班级管理接口(SchoolClassController) +- `GET /api/v1/school/classes/:id/students` - 获取班级学生 +- `GET /api/v1/school/classes/:id/teachers` - 获取班级教师 +- `POST /api/v1/school/classes/:id/teachers` - 添加班级教师 +- `PUT /api/v1/school/classes/:id/teachers/:teacherId` - 更新班级教师 +- `DELETE /api/v1/school/classes/:id/teachers/:teacherId` - 移除班级教师 +- `POST /api/v1/school/students/:id/transfer` - 学生调班 +- `GET /api/v1/school/students/:id/history` - 学生调班历史 +- `POST /api/v1/school/students/import` - 批量导入学生 +- `GET /api/v1/school/students/import/template` - 获取导入模板 + +### P2 - 辅助功能 + +#### 1. 统计报告接口(SchoolStatsController) +- `GET /api/v1/school/stats/teachers` - 活跃教师统计 +- `GET /api/v1/school/stats/courses` - 课程使用统计 +- `GET /api/v1/school/stats/activities` - 最近活动 +- `GET /api/v1/school/stats/lesson-trend` - 课时趋势 +- `GET /api/v1/school/stats/course-distribution` - 课程分布 +- `GET /api/v1/school/reports/overview` - 概览报告 +- `GET /api/v1/school/reports/teachers` - 教师报告 +- `GET /api/v1/school/reports/courses` - 课程报告 +- `GET /api/v1/school/reports/students` - 学生报告 + +#### 2. 导出接口增强(SchoolExportController) +- `GET /api/v1/school/export/lessons?startDate=&endDate=` - 导出课时(带日期范围) +- `GET /api/v1/school/export/teacher-stats?startDate=&endDate=` - 导出教师统计 +- `GET /api/v1/school/export/student-stats?classId=` - 导出学生统计(按班级) + +#### 3. 课程包/套餐接口 +- `GET /api/v1/school/package` - 获取套餐信息 +- `GET /api/v1/school/package/usage` - 获取套餐使用情况 + +#### 4. 家长相关接口 +- `POST /api/v1/school/parents/:parentId/children/:studentId` - 添加孩子到家长 +- `DELETE /api/v1/school/parents/:parentId/children/:studentId` - 从家长移除孩子 + +#### 5. 管理员课程接口 +- `GET /api/v1/admin/courses/:id/stats` - 课程统计 +- `GET /api/v1/admin/courses/:id/validate` - 验证课程 +- `GET /api/v1/admin/courses/:id/versions` - 版本历史 +- `POST /api/v1/admin/courses/:id/submit` - 提交审核 +- `POST /api/v1/admin/courses/:id/withdraw` - 撤销审核 +- `POST /api/v1/admin/courses/:id/approve` - 审核通过 +- `POST /api/v1/admin/courses/:id/reject` - 审核驳回 +- `POST /api/v1/admin/courses/:id/direct-publish` - 直接发布 +- `POST /api/v1/admin/courses/:id/publish` - 发布 +- `POST /api/v1/admin/courses/:id/unpublish` - 下架 +- `POST /api/v1/admin/courses/:id/republish` - 重新发布 + +#### 6. 资源接口增强 +- `POST /api/v1/admin/resources/items/batch-delete` - 批量删除资源项 +- `GET /api/v1/admin/resources/stats` - 资源统计 + +#### 7. 反馈接口 +- `GET /api/v1/teacher/feedbacks` - 获取反馈列表 +- `GET /api/v1/teacher/feedbacks/stats` - 获取反馈统计 +- `GET /api/v1/school/feedbacks` - 获取反馈列表(学校端) +- `GET /api/v1/school/feedbacks/stats` - 获取反馈统计(学校端) + +#### 8. 认证接口 +- `GET /api/v1/auth/profile` - 获取用户信息 +- `POST /api/v1/auth/logout` - 登出 + +--- + +## 总结 + +本次补全了以下主要功能: +1. 任务管理:统计、模板、完成情况更新 +2. 通知:学校管理员通知接口 +3. 排课:课表模板、批量创建、应用模板 + +下一步建议优先补全: +1. 成长档案按学生/班级查询接口 +2. 教师端课时反馈接口 +3. 班级管理相关接口 +4. 学生调班和批量导入接口 diff --git a/CLAUDE.md b/CLAUDE.md index 83431ee..6bdb97f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,144 +1,144 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +本文档为 Claude Code (claude.ai/code) 在本项目中工作时提供指导。 -## Project Overview +## 项目概述 -This is a **Kindergarten Course Management System** (少儿智慧阅读平台) with a Spring Boot backend and Vue 3 frontend. The system manages courses, lessons, tasks, and student growth records for kindergartens. +这是一个**少儿智慧阅读平台**(Kindergarten Course Management System),采用 Spring Boot 后端 + Vue 3 前端架构。系统管理幼儿园的课程、课时、任务和学生成长记录。 -## Architecture +## 技术架构 -### Backend (`reading-platform-java`) -- **Framework**: Spring Boot 3.2.3 + Java 17 -- **Persistence**: MyBatis-Plus 3.5.5 -- **Security**: Spring Security + JWT -- **API Docs**: Knife4j (Swagger OpenAPI 3) -- **Database**: MySQL 8.0 -- **Migration**: Flyway +### 后端 (`reading-platform-java`) +- **框架**: Spring Boot 3.2.3 + Java 17 +- **持久层**: MyBatis-Plus 3.5.5 +- **安全**: Spring Security + JWT +- **API 文档**: Knife4j (Swagger OpenAPI 3) +- **数据库**: MySQL 8.0 +- **数据库迁移**: Flyway -### Frontend (`reading-platform-frontend`) -- **Framework**: Vue 3 + TypeScript + Vite -- **UI**: Ant Design Vue -- **State**: Pinia -- **API**: Axios with auto-generated TypeScript clients via Orval +### 前端 (`reading-platform-frontend`) +- **框架**: Vue 3 + TypeScript + Vite +- **UI 组件库**: Ant Design Vue +- **状态管理**: Pinia +- **API**: Axios + Orval 自动生成的 TypeScript 客户端 -## Multi-Tenant Architecture +## 多租户架构 -The system supports multiple kindergartens (tenants): -- `admin` role: Super admin (no tenant, manages system-wide courses) -- `school` role: School administrator (manages school's teachers, students, classes) -- `teacher` role: Teacher (manages lessons, tasks for their tenant) -- `parent` role: Parent (views child's progress and tasks) +系统支持多个幼儿园(租户): +- `admin` 角色:超级管理员(无租户,管理全系统课程) +- `school` 角色:学校管理员(管理本校的教师、学生、班级) +- `teacher` 角色:教师(管理本校的课时和任务) +- `parent` 角色:家长(查看孩子的进度和任务) -Each entity (except `admin_users`) has a `tenant_id` field. System courses have `tenant_id = NULL`. +除 `admin_users` 外,每个实体都有 `tenant_id` 字段。系统课程的 `tenant_id = NULL`。 -## Project Structure +## 项目结构 ``` kindergarten_java/ -├── reading-platform-java/ # Spring Boot backend +├── reading-platform-java/ # Spring Boot 后端 │ ├── src/main/java/.../controller/ -│ │ ├── admin/ # Super admin endpoints (/api/v1/admin/*) -│ │ ├── school/ # School admin endpoints (/api/v1/school/*) -│ │ ├── teacher/ # Teacher endpoints (/api/v1/teacher/*) -│ │ └── parent/ # Parent endpoints (/api/v1/parent/*) -│ ├── entity/ # Database entities (27 tables) -│ ├── mapper/ # MyBatis-Plus mappers -│ ├── service/ # Service layer interface + impl +│ │ ├── admin/ # 超级管理员端点 (/api/v1/admin/*) +│ │ ├── school/ # 学校管理员端点 (/api/v1/school/*) +│ │ ├── teacher/ # 教师端点 (/api/v1/teacher/*) +│ │ └── parent/ # 家长端点 (/api/v1/parent/*) +│ ├── entity/ # 数据库实体(27张表) +│ ├── mapper/ # MyBatis-Plus 映射器 +│ ├── service/ # 服务层接口 + 实现 │ ├── common/ -│ │ ├── annotation/RequireRole # Role-based access control -│ │ ├── security/ # JWT authentication -│ │ ├── enums/ # UserRole, CourseStatus, etc. +│ │ ├── annotation/RequireRole # 基于角色的访问控制 +│ │ ├── security/ # JWT 认证 +│ │ ├── enums/ # UserRole, CourseStatus 等枚举 │ │ ├── response/ # Result, PageResult -│ │ └── config/ # Security, MyBatis, OpenAPI configs +│ │ └── config/ # Security, MyBatis, OpenAPI 配置 │ └── resources/ -│ ├── db/migration/ # Flyway migration scripts -│ └── mapper/ # MyBatis XML files +│ ├── db/migration/ # Flyway 迁移脚本 +│ └── mapper/ # MyBatis XML 文件 │ -├── reading-platform-frontend/ # Vue 3 frontend +├── reading-platform-frontend/ # Vue 3 前端 │ ├── src/views/ -│ │ ├── admin/ # Super admin pages -│ │ ├── school/ # School admin pages -│ │ ├── teacher/ # Teacher pages -│ │ └── parent/ # Parent pages -│ ├── api/generated/ # Auto-generated API clients -│ ├── api-spec.yml # OpenAPI specification -│ └── router/index.ts # Vue Router config +│ │ ├── admin/ # 超级管理员页面 +│ │ ├── school/ # 学校管理员页面 +│ │ ├── teacher/ # 教师页面 +│ │ └── parent/ # 家长页面 +│ ├── api/generated/ # 自动生成的 API 客户端 +│ ├── api-spec.yml # OpenAPI 规范 +│ └── router/index.ts # Vue Router 配置 │ -├── docker-compose.yml # Backend + Frontend services -└── docs/开发协作指南.md # Development guide (Chinese) +├── docker-compose.yml # 后端 + 前端服务 +└── docs/开发协作指南.md # 开发指南(中文) ``` -## Key Patterns +## 关键模式 -### 1. Role-Based Access Control -Use `@RequireRole` annotation on controllers/services: +### 1. 基于角色的访问控制 +在 Controller/Service 上使用 `@RequireRole` 注解: ```java -@RequireRole(UserRole.SCHOOL) // Only school admins can access +@RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问 ``` -### 2. Tenant Isolation -Use `SecurityUtils.getCurrentTenantId()` in school/teacher/parent endpoints to filter data by current tenant. +### 2. 租户隔离 +在学校/教师/家长端点中使用 `SecurityUtils.getCurrentTenantId()` 按当前租户过滤数据。 -### 3. Unified Response Format +### 3. 统一响应格式 ```java Result success(T data) // { code: 200, message: "success", data: ... } Result error(code, msg) // { code: xxx, message: "...", data: null } ``` -### 4. OpenAPI-Driven Development -- Backend: Annotate controllers with `@Operation`, `@Parameter`, `@Schema` -- Frontend: Run `npm run api:update` to regenerate TypeScript clients from `api-spec.yml` +### 4. OpenAPI 驱动开发 +- 后端:在 Controller 上使用 `@Operation`、`@Parameter`、`@Schema` 注解 +- 前端:运行 `npm run api:update` 从 `api-spec.yml` 重新生成 TypeScript 客户端 -## Development Commands +## 开发命令 -### Backend +### 后端 ```bash -# Run with Docker Compose (recommended) +# 使用 Docker Compose 运行(推荐) docker compose up --build -# Run locally (requires MySQL running) +# 本地运行(需要 MySQL 已启动) cd reading-platform-java mvn spring-boot:run -# Build +# 构建 mvn clean package -DskipTests ``` -### Frontend +### 前端 ```bash cd reading-platform-frontend npm install npm run dev npm run build -# Update API clients from backend spec +# 从后端规范更新 API 客户端 npm run api:update ``` -### Database Migration -- Add new migration scripts to `reading-platform-java/src/main/resources/db/migration/V{n}__description.sql` -- Flyway runs automatically on backend startup (dev mode only) +### 数据库迁移 +- 将新的迁移脚本添加到 `reading-platform-java/src/main/resources/db/migration/V{n}__description.sql` +- Flyway 会在后端启动时自动运行(仅开发模式) -## Database Schema (27 Tables) -- **Tenant**: tenants, tenant_courses -- **Users**: admin_users, teachers, students, parents, parent_students -- **Class**: classes, class_teachers, student_class_history -- **Course**: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities -- **Lesson**: lessons, lesson_feedbacks, student_records -- **Task**: tasks, task_targets, task_completions, task_templates -- **Growth**: growth_records -- **Resource**: resource_libraries, resource_items -- **Schedule**: schedule_plans, schedule_templates -- **System**: system_settings, notifications, operation_logs, tags +## 数据库表结构(27张表) +- **租户**: tenants, tenant_courses +- **用户**: admin_users, teachers, students, parents, parent_students +- **班级**: classes, class_teachers, student_class_history +- **课程**: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities +- **课时**: lessons, lesson_feedbacks, student_records +- **任务**: tasks, task_targets, task_completions, task_templates +- **成长**: growth_records +- **资源**: resource_libraries, resource_items +- **日程**: schedule_plans, schedule_templates +- **系统**: system_settings, notifications, operation_logs, tags -## Test Accounts -| Role | Username | Password | -|------|----------|----------| -| Admin | admin | admin123 | -| School | school | 123456 | -| Teacher | teacher1 | 123456 | -| Parent | parent1 | 123456 | +## 测试账号 +| 角色 | 用户名 | 密码 | +|------|--------|------| +| 管理员 | admin | admin123 | +| 学校 | school | 123456 | +| 教师 | teacher1 | 123456 | +| 家长 | parent1 | 123456 | -## API Documentation -- Access: http://localhost:8080/doc.html (after backend starts) +## API 文档 +- 访问地址:http://localhost:8080/doc.html(后端启动后) \ No newline at end of file diff --git a/Service 重构总结.md b/Service 重构总结.md new file mode 100644 index 0000000..ac6deab --- /dev/null +++ b/Service 重构总结.md @@ -0,0 +1,132 @@ +# Service 层重构总结 + +## 重构时间 +2026-03-10 + +## 重构目的 +将 Service 层从"直接 class 实现"重构为"interface + impl"模式,符合 Spring 最佳实践。 + +## 重构的 Service 列表 + +本次重构共完成了 14 个 Service 的 interface + impl 模式改造: + +### 新增的 Interface 文件 + +| 序号 | 接口文件 | 实现类文件 | +|------|----------|------------| +| 1 | `AdminStatsService.java` | `AdminStatsServiceImpl.java` | +| 2 | `CourseLessonService.java` | `CourseLessonServiceImpl.java` | +| 3 | `CoursePackageService.java` | `CoursePackageServiceImpl.java` | +| 4 | `ExportService.java` | `ExportServiceImpl.java` | +| 5 | `FileUploadService.java` | `FileUploadServiceImpl.java` | +| 6 | `OperationLogService.java` | `OperationLogServiceImpl.java` | +| 7 | `ResourceService.java` | `ResourceServiceImpl.java` | +| 8 | `SchoolCourseService.java` | `SchoolCourseServiceImpl.java` | +| 9 | `SystemSettingService.java` | `SystemSettingServiceImpl.java` | +| 10 | `TeacherDashboardService.java` | `TeacherDashboardServiceImpl.java` | +| 11 | `ScheduleService.java` | `ScheduleServiceImpl.java` | +| 12 | `ThemeService.java` | `ThemeServiceImpl.java` | +| 13 | `TenantService.java` | `TenantServiceImpl.java` (已存在) | +| 14 | `SchoolStatsService.java` | `SchoolStatsServiceImpl.java` | + +### 已有的 Interface + Impl 模式 Service + +以下 Service 在重构前已经是 interface + impl 模式: + +1. `AuthService` → `AuthServiceImpl` +2. `ClassService` → `ClassServiceImpl` +3. `StudentService` → `StudentServiceImpl` +4. `TaskService` → `TaskServiceImpl` +5. `CourseService` → `CourseServiceImpl` +6. `GrowthRecordService` → `GrowthRecordServiceImpl` +7. `LessonService` → `LessonServiceImpl` +8. `NotificationService` → `NotificationServiceImpl` +9. `ParentService` → `ParentServiceImpl` +10. `TeacherService` → `TeacherServiceImpl` +11. `TokenService` → `TokenServiceImpl` + +## 重构模式 + +所有 Service 遵循以下模式: + +### Interface 定义 +```java +package com.reading.platform.service; + +import java.util.List; +import java.util.Map; + +/** + * 服务接口 + */ +public interface XxxService { + + /** + * 方法描述 + */ + List getXxxList(Long id); + + /** + * 方法描述 + */ + Xxx createXxx(XxxCreateRequest request); +} +``` + +### Impl 实现类 +```java +package com.reading.platform.service.impl; + +import com.reading.platform.mapper.XxxMapper; +import com.reading.platform.service.XxxService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 服务实现类 + */ +@Service +@RequiredArgsConstructor +public class XxxServiceImpl implements XxxService { + + private final XxxMapper xxxMapper; + + @Override + public List getXxxList(Long id) { + // 业务逻辑 + } + + @Override + public Xxx createXxx(XxxCreateRequest request) { + // 业务逻辑 + } +} +``` + +## 架构优势 + +1. **依赖倒置**:Controller 依赖接口而非具体实现 +2. **易于测试**:可以通过 Mock 接口进行测试 +3. **易于扩展**:可以轻松切换不同的实现 +4. **代码规范**:符合 Spring 官方推荐的最佳实践 + +## 编译验证 + +```bash +cd reading-platform-java +mvn clean compile -DskipTests +``` + +编译结果:**BUILD SUCCESS** + +## 文件统计 + +- 接口文件:25 个 +- 实现类文件:25 个 +- 总计:50 个 Service 相关文件 + +## 后续建议 + +1. 为新实现的接口添加单元测试 +2. 在 CI/CD 流程中确保编译使用 JDK 17 +3. 保持新增 Service 遵循 interface + impl 模式 diff --git a/reading-platform-frontend/src/components.d.ts b/reading-platform-frontend/src/components.d.ts index fd768ff..52cfbdf 100644 --- a/reading-platform-frontend/src/components.d.ts +++ b/reading-platform-frontend/src/components.d.ts @@ -11,26 +11,15 @@ declare module 'vue' { AAvatar: typeof import('ant-design-vue/es')['Avatar'] ABadge: typeof import('ant-design-vue/es')['Badge'] AButton: typeof import('ant-design-vue/es')['Button'] - AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup'] ACard: typeof import('ant-design-vue/es')['Card'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] - ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup'] ACol: typeof import('ant-design-vue/es')['Col'] - ACollapse: typeof import('ant-design-vue/es')['Collapse'] - ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] - ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] - ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] - ADivider: typeof import('ant-design-vue/es')['Divider'] - ADrawer: typeof import('ant-design-vue/es')['Drawer'] ADropdown: typeof import('ant-design-vue/es')['Dropdown'] AEmpty: typeof import('ant-design-vue/es')['Empty'] AForm: typeof import('ant-design-vue/es')['Form'] AFormItem: typeof import('ant-design-vue/es')['FormItem'] - AImage: typeof import('ant-design-vue/es')['Image'] - AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup'] AInput: typeof import('ant-design-vue/es')['Input'] - AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] ALayout: typeof import('ant-design-vue/es')['Layout'] @@ -44,37 +33,22 @@ declare module 'vue' { AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AModal: typeof import('ant-design-vue/es')['Modal'] - APageHeader: typeof import('ant-design-vue/es')['PageHeader'] APagination: typeof import('ant-design-vue/es')['Pagination'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] - AProgress: typeof import('ant-design-vue/es')['Progress'] ARadio: typeof import('ant-design-vue/es')['Radio'] - ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] - ARate: typeof import('ant-design-vue/es')['Rate'] - AResult: typeof import('ant-design-vue/es')['Result'] ARow: typeof import('ant-design-vue/es')['Row'] ASelect: typeof import('ant-design-vue/es')['Select'] - ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] - ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] ASpace: typeof import('ant-design-vue/es')['Space'] ASpin: typeof import('ant-design-vue/es')['Spin'] - AStatistic: typeof import('ant-design-vue/es')['Statistic'] - AStep: typeof import('ant-design-vue/es')['Step'] - ASteps: typeof import('ant-design-vue/es')['Steps'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] - ASwitch: typeof import('ant-design-vue/es')['Switch'] ATable: typeof import('ant-design-vue/es')['Table'] - ATabPane: typeof import('ant-design-vue/es')['TabPane'] - ATabs: typeof import('ant-design-vue/es')['Tabs'] ATag: typeof import('ant-design-vue/es')['Tag'] ATextarea: typeof import('ant-design-vue/es')['Textarea'] - ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] - AUpload: typeof import('ant-design-vue/es')['Upload'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default'] FileUploader: typeof import('./components/course/FileUploader.vue')['default'] diff --git a/reading-platform-java/pom.xml b/reading-platform-java/pom.xml index e6a7117..7a1cf18 100644 --- a/reading-platform-java/pom.xml +++ b/reading-platform-java/pom.xml @@ -46,6 +46,10 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-data-redis + diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java b/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java index 92ab067..2f33af9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package com.reading.platform.common.security; +import com.reading.platform.service.TokenService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -25,6 +26,7 @@ import java.util.Collections; public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; + private final TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @@ -32,6 +34,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { try { String token = resolveToken(request); if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + // 验证 Redis 中是否存在该 token + if (!tokenService.isTokenExist(token)) { + log.warn("Token not found in Redis, possibly invalidated: {}", token); + filterChain.doFilter(request, response); + return; + } + JwtPayload payload = jwtTokenProvider.getPayloadFromToken(token); UsernamePasswordAuthenticationToken authentication = diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java index 0e3bd01..a048576 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java @@ -7,8 +7,10 @@ import com.reading.platform.dto.response.UserInfoResponse; import com.reading.platform.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; @Tag(name = "认证", description = "认证相关接口") @@ -27,8 +29,9 @@ public class AuthController { @Operation(summary = "用户登出") @PostMapping("/logout") - public Result logout() { - // JWT is stateless - client simply discards the token + public Result logout(HttpServletRequest request) { + String token = resolveToken(request); + authService.logout(token); return Result.success(); } @@ -47,4 +50,12 @@ public class AuthController { return Result.success(); } + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java index e2f8271..5f8352d 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java @@ -77,4 +77,18 @@ public class SchoolClassController { return Result.success(); } + @Operation(summary = "移除班级教师") + @DeleteMapping("/{id}/teachers/{teacherId}") + public Result removeTeacher(@PathVariable Long id, @PathVariable Long teacherId) { + classService.removeTeacher(id, teacherId); + return Result.success(); + } + + @Operation(summary = "移除班级学生") + @DeleteMapping("/{id}/students/{studentId}") + public Result removeStudent(@PathVariable Long id, @PathVariable Long studentId) { + classService.removeStudent(id, studentId); + return Result.success(); + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolNotificationController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolNotificationController.java new file mode 100644 index 0000000..f20b15c --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolNotificationController.java @@ -0,0 +1,64 @@ +package com.reading.platform.controller.school; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.annotation.RequireRole; +import com.reading.platform.common.enums.UserRole; +import com.reading.platform.common.response.PageResult; +import com.reading.platform.common.response.Result; +import com.reading.platform.common.security.SecurityUtils; +import com.reading.platform.entity.Notification; +import com.reading.platform.service.NotificationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "学校 - 通知", description = "通知接口(学校管理员专用)") +@RestController +@RequestMapping("/api/v1/school/notifications") +@RequiredArgsConstructor +@RequireRole(UserRole.SCHOOL) +public class SchoolNotificationController { + + private final NotificationService notificationService; + + @Operation(summary = "根据 ID 获取通知") + @GetMapping("/{id}") + public Result getNotification(@PathVariable Long id) { + return Result.success(notificationService.getNotificationById(id)); + } + + @Operation(summary = "获取我的通知") + @GetMapping + public Result> getMyNotifications( + @RequestParam(value = "page", required = false) Integer pageNum, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) Integer isRead) { + Long userId = SecurityUtils.getCurrentUserId(); + Page page = notificationService.getMyNotifications(userId, "school", pageNum, pageSize, isRead); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "标记通知为已读") + @PostMapping("/{id}/read") + public Result markAsRead(@PathVariable Long id) { + notificationService.markAsRead(id); + return Result.success(); + } + + @Operation(summary = "标记所有通知为已读") + @PostMapping("/read-all") + public Result markAllAsRead() { + Long userId = SecurityUtils.getCurrentUserId(); + notificationService.markAllAsRead(userId, "school"); + return Result.success(); + } + + @Operation(summary = "获取未读通知数量") + @GetMapping("/unread-count") + public Result getUnreadCount() { + Long userId = SecurityUtils.getCurrentUserId(); + return Result.success(notificationService.getUnreadCount(userId, "school")); + } + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolScheduleController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolScheduleController.java index b8956c2..a1575d1 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolScheduleController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolScheduleController.java @@ -6,14 +6,22 @@ import com.reading.platform.common.enums.UserRole; import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; +import com.reading.platform.dto.request.SchedulePlanCreateRequest; +import com.reading.platform.dto.request.ScheduleTemplateApplyRequest; import com.reading.platform.entity.SchedulePlan; import com.reading.platform.entity.ScheduleTemplate; import com.reading.platform.service.ScheduleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + @Tag(name = "学校 - 课表", description = "课表管理接口(学校管理员专用)") @RestController @RequestMapping("/api/v1/school/schedules") @@ -28,13 +36,25 @@ public class SchoolScheduleController { public Result> getSchedulePlans( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "20") int pageSize, - @RequestParam(required = false) Long classId) { + @RequestParam(required = false) Long classId, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { Long tenantId = SecurityUtils.getCurrentTenantId(); - Page page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId); + Page page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId, startDate, endDate); return Result.success(PageResult.of(page)); } - @Operation(summary = "根据ID获取课表计划") + @Operation(summary = "获取课表(按日期范围)") + @GetMapping("/timetable") + public Result>> getTimetable( + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(required = false) Long classId) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(scheduleService.getTimetable(tenantId, startDate, endDate, classId)); + } + + @Operation(summary = "根据 ID 获取课表计划") @GetMapping("/{id}") public Result getSchedulePlan(@PathVariable Long id) { return Result.success(scheduleService.getSchedulePlanById(id)); @@ -42,9 +62,10 @@ public class SchoolScheduleController { @Operation(summary = "创建课表计划") @PostMapping - public Result createSchedulePlan(@RequestBody SchedulePlan plan) { + public Result createSchedulePlan(@Valid @RequestBody SchedulePlanCreateRequest plan) { Long tenantId = SecurityUtils.getCurrentTenantId(); - return Result.success(scheduleService.createSchedulePlan(tenantId, plan)); + Long userId = SecurityUtils.getCurrentUserId(); + return Result.success(scheduleService.createSchedulePlan(tenantId, userId, plan)); } @Operation(summary = "更新课表计划") @@ -60,6 +81,16 @@ public class SchoolScheduleController { return Result.success(); } + @Operation(summary = "批量创建排课") + @PostMapping("/batch") + public Result> batchCreateSchedules(@RequestBody List plans) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Long userId = SecurityUtils.getCurrentUserId(); + return Result.success(scheduleService.batchCreateSchedules(tenantId, userId, plans)); + } + + // ==================== 排课模板 ==================== + @Operation(summary = "获取课表模板") @GetMapping("/templates") public Result> getScheduleTemplates( @@ -70,6 +101,12 @@ public class SchoolScheduleController { return Result.success(PageResult.of(page)); } + @Operation(summary = "根据 ID 获取课表模板") + @GetMapping("/templates/{id}") + public Result getScheduleTemplate(@PathVariable Long id) { + return Result.success(scheduleService.getScheduleTemplateById(id)); + } + @Operation(summary = "创建课表模板") @PostMapping("/templates") public Result createScheduleTemplate(@RequestBody ScheduleTemplate template) { @@ -77,10 +114,25 @@ public class SchoolScheduleController { return Result.success(scheduleService.createScheduleTemplate(tenantId, template)); } + @Operation(summary = "更新课表模板") + @PutMapping("/templates/{id}") + public Result updateScheduleTemplate(@PathVariable Long id, @RequestBody ScheduleTemplate template) { + return Result.success(scheduleService.updateScheduleTemplate(id, template)); + } + @Operation(summary = "删除课表模板") @DeleteMapping("/templates/{id}") public Result deleteScheduleTemplate(@PathVariable Long id) { scheduleService.deleteScheduleTemplate(id); return Result.success(); } + + @Operation(summary = "应用课表模板") + @PostMapping("/templates/{id}/apply") + public Result> applyScheduleTemplate( + @PathVariable Long id, + @Valid @RequestBody ScheduleTemplateApplyRequest request) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(scheduleService.applyScheduleTemplate(tenantId, id, request)); + } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStatsController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStatsController.java index a3e5f69..79fffd7 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStatsController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStatsController.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.Map; @Tag(name = "学校 - 统计", description = "学校统计仪表盘接口(学校管理员专用)") @@ -27,4 +28,42 @@ public class SchoolStatsController { Long tenantId = SecurityUtils.getCurrentTenantId(); return Result.success(schoolStatsService.getStats(tenantId)); } + + @Operation(summary = "获取活跃教师统计") + @GetMapping("/teachers") + public Result>> getActiveTeachers( + @RequestParam(value = "limit", required = false, defaultValue = "5") Integer limit) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(schoolStatsService.getActiveTeachers(tenantId, limit)); + } + + @Operation(summary = "获取课程使用统计") + @GetMapping("/courses") + public Result>> getCourseUsageStats() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(schoolStatsService.getCourseUsageStats(tenantId)); + } + + @Operation(summary = "获取最近活动记录") + @GetMapping("/activities") + public Result>> getRecentActivities( + @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(schoolStatsService.getRecentActivities(tenantId, limit)); + } + + @Operation(summary = "获取课时趋势(最近 N 个月)") + @GetMapping("/lesson-trend") + public Result>> getLessonTrend( + @RequestParam(value = "months", required = false, defaultValue = "6") Integer months) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(schoolStatsService.getLessonTrend(tenantId, months)); + } + + @Operation(summary = "获取课程分布统计(饼图数据)") + @GetMapping("/course-distribution") + public Result>> getCourseDistribution() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(schoolStatsService.getCourseDistribution(tenantId)); + } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java index 217e327..02ae6b9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java @@ -14,6 +14,10 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + @Tag(name = "学校 - 学生", description = "学生管理接口(学校管理员专用)") @RestController @RequestMapping("/api/v1/school/students") @@ -61,4 +65,20 @@ public class SchoolStudentController { return Result.success(); } + @Operation(summary = "批量导入学生") + @PostMapping("/import") + public Result> importStudents(@RequestBody List requests) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(studentService.importStudents(tenantId, requests)); + } + + @Operation(summary = "获取导入模板") + @GetMapping("/import/template") + public Result> getImportTemplate() { + Map template = new HashMap<>(); + template.put("headers", new String[]{"姓名", "性别", "出生日期", "家长手机号", "家长姓名", "备注"}); + template.put("example", new String[]{"张三", "男", "2018-01-01", "13800138000", "张父", "示例数据"}); + return Result.success(template); + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTaskController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTaskController.java index 62b15d7..488f5d3 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTaskController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTaskController.java @@ -4,9 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; -import com.reading.platform.dto.request.TaskCreateRequest; -import com.reading.platform.dto.request.TaskUpdateRequest; +import com.reading.platform.dto.request.*; import com.reading.platform.entity.Task; +import com.reading.platform.entity.TaskCompletion; +import com.reading.platform.entity.TaskTemplate; import com.reading.platform.service.TaskService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -14,6 +15,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + @Tag(name = "学校 - 任务", description = "任务管理接口(学校管理员专用)") @RestController @RequestMapping("/api/v1/school/tasks") @@ -37,7 +41,7 @@ public class SchoolTaskController { return Result.success(taskService.updateTask(id, request)); } - @Operation(summary = "根据ID获取任务") + @Operation(summary = "根据 ID 获取任务") @GetMapping("/{id}") public Result getTask(@PathVariable Long id) { return Result.success(taskService.getTaskById(id)); @@ -63,4 +67,120 @@ public class SchoolTaskController { return Result.success(); } + // ==================== 任务统计 ==================== + + @Operation(summary = "获取任务统计数据") + @GetMapping("/stats") + public Result> getStats() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getTaskStats(tenantId)); + } + + @Operation(summary = "按任务类型统计") + @GetMapping("/stats/by-type") + public Result> getStatsByType() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getStatsByType(tenantId)); + } + + @Operation(summary = "按班级统计") + @GetMapping("/stats/by-class") + public Result>> getStatsByClass() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getStatsByClass(tenantId)); + } + + @Operation(summary = "获取月度统计趋势") + @GetMapping("/stats/monthly") + public Result>> getMonthlyStats( + @RequestParam(value = "months", required = false, defaultValue = "6") Integer months) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getMonthlyStats(tenantId, months)); + } + + @Operation(summary = "获取任务完成情况分页") + @GetMapping("/{id}/completions") + public Result> getCompletions( + @PathVariable Long id, + @RequestParam(value = "page", required = false) Integer pageNum, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) String status) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Page page = taskService.getTaskCompletions(tenantId, id, pageNum, pageSize, status); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "更新任务完成状态") + @PutMapping("/{taskId}/completions/{studentId}") + public Result updateCompletion( + @PathVariable Long taskId, + @PathVariable Long studentId, + @RequestParam String status, + @RequestParam(required = false) String feedback) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.updateTaskCompletion(tenantId, taskId, studentId, status, feedback)); + } + + // ==================== 任务模板 ==================== + + @Operation(summary = "获取任务模板列表") + @GetMapping("/task-templates") + public Result> getTemplates( + @RequestParam(value = "page", required = false) Integer pageNum, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String type) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Page page = taskService.getTemplatePage(tenantId, pageNum, pageSize, keyword, type); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "根据 ID 获取任务模板") + @GetMapping("/task-templates/{id}") + public Result getTemplate(@PathVariable Long id) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getTemplateById(tenantId, id)); + } + + @Operation(summary = "获取默认模板(按类型)") + @GetMapping("/task-templates/default/{taskType}") + public Result getDefaultTemplate(@PathVariable String taskType) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getDefaultTemplate(tenantId, taskType)); + } + + @Operation(summary = "创建任务模板") + @PostMapping("/task-templates") + public Result createTemplate(@Valid @RequestBody TaskTemplateCreateRequest request) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Long userId = SecurityUtils.getCurrentUserId(); + return Result.success(taskService.createTemplate(tenantId, userId, request)); + } + + @Operation(summary = "更新任务模板") + @PutMapping("/task-templates/{id}") + public Result updateTemplate( + @PathVariable Long id, + @RequestBody TaskTemplateUpdateRequest request) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.updateTemplate(tenantId, id, request)); + } + + @Operation(summary = "删除任务模板") + @DeleteMapping("/task-templates/{id}") + public Result deleteTemplate(@PathVariable Long id) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + taskService.deleteTemplate(tenantId, id); + return Result.success(); + } + + @Operation(summary = "从模板创建任务") + @PostMapping("/from-template") + public Result createFromTemplate(@Valid @RequestBody CreateTaskFromTemplateRequest request) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Long userId = SecurityUtils.getCurrentUserId(); + String role = SecurityUtils.getCurrentRole(); + return Result.success(taskService.createTaskFromTemplate(tenantId, userId, role, request)); + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherTaskController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherTaskController.java index 574b641..c260887 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherTaskController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherTaskController.java @@ -4,9 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; -import com.reading.platform.dto.request.TaskCreateRequest; -import com.reading.platform.dto.request.TaskUpdateRequest; +import com.reading.platform.dto.request.*; import com.reading.platform.entity.Task; +import com.reading.platform.entity.TaskCompletion; +import com.reading.platform.entity.TaskTemplate; import com.reading.platform.service.TaskService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -14,6 +15,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + @Tag(name = "教师 - 任务", description = "任务接口(教师专用)") @RestController @RequestMapping("/api/v1/teacher/tasks") @@ -36,7 +40,7 @@ public class TeacherTaskController { return Result.success(taskService.updateTask(id, request)); } - @Operation(summary = "根据ID获取任务") + @Operation(summary = "根据 ID 获取任务") @GetMapping("/{id}") public Result getTask(@PathVariable Long id) { return Result.success(taskService.getTaskById(id)); @@ -62,4 +66,94 @@ public class TeacherTaskController { return Result.success(); } + // ==================== 任务统计 ==================== + + @Operation(summary = "获取任务统计数据") + @GetMapping("/stats") + public Result> getStats() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getTaskStats(tenantId)); + } + + @Operation(summary = "按任务类型统计") + @GetMapping("/stats/by-type") + public Result> getStatsByType() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getStatsByType(tenantId)); + } + + @Operation(summary = "按班级统计") + @GetMapping("/stats/by-class") + public Result>> getStatsByClass() { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getStatsByClass(tenantId)); + } + + @Operation(summary = "获取月度统计趋势") + @GetMapping("/stats/monthly") + public Result>> getMonthlyStats( + @RequestParam(value = "months", required = false, defaultValue = "6") Integer months) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getMonthlyStats(tenantId, months)); + } + + @Operation(summary = "获取任务完成情况分页") + @GetMapping("/{id}/completions") + public Result> getCompletions( + @PathVariable Long id, + @RequestParam(value = "page", required = false) Integer pageNum, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) String status) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Page page = taskService.getTaskCompletions(tenantId, id, pageNum, pageSize, status); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "更新任务完成状态") + @PutMapping("/{taskId}/completions/{studentId}") + public Result updateCompletion( + @PathVariable Long taskId, + @PathVariable Long studentId, + @RequestParam String status, + @RequestParam(required = false) String feedback) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.updateTaskCompletion(tenantId, taskId, studentId, status, feedback)); + } + + // ==================== 任务模板(只读) ==================== + + @Operation(summary = "获取任务模板列表") + @GetMapping("/task-templates") + public Result> getTemplates( + @RequestParam(value = "page", required = false) Integer pageNum, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String type) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Page page = taskService.getTemplatePage(tenantId, pageNum, pageSize, keyword, type); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "根据 ID 获取任务模板") + @GetMapping("/task-templates/{id}") + public Result getTemplate(@PathVariable Long id) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getTemplateById(tenantId, id)); + } + + @Operation(summary = "获取默认模板(按类型)") + @GetMapping("/task-templates/default/{taskType}") + public Result getDefaultTemplate(@PathVariable String taskType) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + return Result.success(taskService.getDefaultTemplate(tenantId, taskType)); + } + + @Operation(summary = "从模板创建任务") + @PostMapping("/from-template") + public Result createFromTemplate(@Valid @RequestBody CreateTaskFromTemplateRequest request) { + Long tenantId = SecurityUtils.getCurrentTenantId(); + Long userId = SecurityUtils.getCurrentUserId(); + return Result.success(taskService.createTaskFromTemplate(tenantId, userId, "teacher", request)); + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/CreateTaskFromTemplateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/CreateTaskFromTemplateRequest.java new file mode 100644 index 0000000..b1dd282 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/CreateTaskFromTemplateRequest.java @@ -0,0 +1,33 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * 从模板创建任务请求 + */ +@Data +@Schema(description = "从模板创建任务请求") +public class CreateTaskFromTemplateRequest { + + @Schema(description = "模板 ID") + private Long templateId; + + @Schema(description = "目标 ID 列表(班级 ID 或学生 ID)") + @NotEmpty(message = "目标 ID 列表不能为空") + private List targetIds; + + @Schema(description = "目标类型:CLASS-班级,STUDENT-学生") + private String targetType = "CLASS"; + + @Schema(description = "任务开始日期") + private LocalDate startDate; + + @Schema(description = "任务截止日期") + private LocalDate endDate; + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/SchedulePlanCreateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/SchedulePlanCreateRequest.java new file mode 100644 index 0000000..935381d --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/SchedulePlanCreateRequest.java @@ -0,0 +1,50 @@ +package com.reading.platform.dto.request; + +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalTime; + +/** + * 课表计划创建请求 + */ +@Data +@Schema(description = "课表计划创建请求") +public class SchedulePlanCreateRequest { + + @Schema(description = "班级 ID") + private Long classId; + + @Schema(description = "课程 ID") + private Long courseId; + + @Schema(description = "星期几:1-7") + private Integer dayOfWeek; + + @Schema(description = "节次") + private Integer period; + + @Schema(description = "上课时间") + private LocalTime startTime; + + @Schema(description = "下课时间") + private LocalTime endTime; + + @Schema(description = "授课教师 ID") + private Long teacherId; + + @Schema(description = "开始日期") + private LocalDate startDate; + + @Schema(description = "结束日期") + private LocalDate endDate; + + @Schema(description = "教室/地点") + private String location; + + @Schema(description = "备注") + private String note; + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleTemplateApplyRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleTemplateApplyRequest.java new file mode 100644 index 0000000..bd533f9 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleTemplateApplyRequest.java @@ -0,0 +1,27 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDate; + +/** + * 课表模板应用请求 + */ +@Data +@Schema(description = "课表模板应用请求") +public class ScheduleTemplateApplyRequest { + + @NotNull(message = "班级 ID 不能为空") + @Schema(description = "班级 ID") + private Long classId; + + @NotNull(message = "开始日期不能为空") + @Schema(description = "应用开始日期") + private LocalDate startDate; + + @Schema(description = "应用周数") + private Integer weeks; + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskTemplateCreateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskTemplateCreateRequest.java new file mode 100644 index 0000000..034d167 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskTemplateCreateRequest.java @@ -0,0 +1,30 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 任务模板创建请求 + */ +@Data +@Schema(description = "任务模板创建请求") +public class TaskTemplateCreateRequest { + + @NotBlank(message = "模板名称不能为空") + @Schema(description = "模板名称") + private String name; + + @Schema(description = "模板描述") + private String description; + + @Schema(description = "任务类型:阅读、作业、活动") + private String type; + + @Schema(description = "任务内容模板") + private String content; + + @Schema(description = "是否公共模板:0-私有,1-公共") + private Integer isPublic = 0; + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskTemplateUpdateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskTemplateUpdateRequest.java new file mode 100644 index 0000000..633b601 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskTemplateUpdateRequest.java @@ -0,0 +1,28 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 任务模板更新请求 + */ +@Data +@Schema(description = "任务模板更新请求") +public class TaskTemplateUpdateRequest { + + @Schema(description = "模板名称") + private String name; + + @Schema(description = "模板描述") + private String description; + + @Schema(description = "任务类型:阅读、作业、活动") + private String type; + + @Schema(description = "任务内容模板") + private String content; + + @Schema(description = "是否公共模板:0-私有,1-公共") + private Integer isPublic; + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/entity/SchedulePlan.java b/reading-platform-java/src/main/java/com/reading/platform/entity/SchedulePlan.java index 23fc971..fb2d5ad 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/entity/SchedulePlan.java +++ b/reading-platform-java/src/main/java/com/reading/platform/entity/SchedulePlan.java @@ -5,6 +5,7 @@ import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; /** * 课表计划实体 @@ -22,10 +23,26 @@ public class SchedulePlan { private Long classId; + private Long courseId; + + private Long teacherId; + + private Integer dayOfWeek; + + private Integer period; + + private LocalTime startTime; + + private LocalTime endTime; + private LocalDate startDate; private LocalDate endDate; + private String location; + + private String note; + private String status; @TableField(fill = FieldFill.INSERT) diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/AdminStatsService.java b/reading-platform-java/src/main/java/com/reading/platform/service/AdminStatsService.java index b3efed3..3210f33 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/AdminStatsService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/AdminStatsService.java @@ -1,148 +1,35 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.reading.platform.entity.*; -import com.reading.platform.mapper.*; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -@Service -@RequiredArgsConstructor -public class AdminStatsService { +/** + * 管理员统计服务接口 + */ +public interface AdminStatsService { - private final TenantMapper tenantMapper; - private final TeacherMapper teacherMapper; - private final StudentMapper studentMapper; - private final CourseMapper courseMapper; - private final LessonMapper lessonMapper; + /** + * 获取整体统计数据 + */ + Map getStats(); - public Map getStats() { - Map stats = new HashMap<>(); + /** + * 获取趋势数据 + */ + List> getTrendData(); - long tenantCount = tenantMapper.selectCount(null); - long activeTenantCount = tenantMapper.selectCount( - new LambdaQueryWrapper().eq(Tenant::getStatus, "active")); - long courseCount = courseMapper.selectCount(null); - long publishedCourseCount = courseMapper.selectCount( - new LambdaQueryWrapper().eq(Course::getStatus, "published")); + /** + * 获取活跃租户 + */ + List> getActiveTenants(int limit); - // Monthly lessons (current month) - LocalDate monthStart = LocalDate.now().withDayOfMonth(1); - LocalDate monthEnd = LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()); - long monthlyLessons = lessonMapper.selectCount( - new LambdaQueryWrapper() - .ge(Lesson::getLessonDate, monthStart) - .le(Lesson::getLessonDate, monthEnd)); + /** + * 获取热门课程 + */ + List> getPopularCourses(int limit); - stats.put("tenantCount", tenantCount); - stats.put("activeTenantCount", activeTenantCount); - stats.put("teacherCount", teacherMapper.selectCount(null)); - stats.put("studentCount", studentMapper.selectCount(null)); - stats.put("courseCount", courseCount); - stats.put("publishedCourseCount", publishedCourseCount); - stats.put("lessonCount", lessonMapper.selectCount(null)); - stats.put("monthlyLessons", monthlyLessons); - return stats; - } - - public List> getTrendData() { - List> trend = new ArrayList<>(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); - LocalDate now = LocalDate.now(); - - for (int i = 5; i >= 0; i--) { - LocalDate monthStart = now.minusMonths(i).withDayOfMonth(1); - LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth()); - - long lessonCount = lessonMapper.selectCount( - new LambdaQueryWrapper() - .ge(Lesson::getLessonDate, monthStart) - .le(Lesson::getLessonDate, monthEnd)); - - // Count tenants created up to this month end - long tenantCount = tenantMapper.selectCount( - new LambdaQueryWrapper() - .le(Tenant::getCreatedAt, monthEnd.atTime(23, 59, 59))); - - // Count students created up to this month end - long studentCount = studentMapper.selectCount( - new LambdaQueryWrapper() - .le(Student::getCreatedAt, monthEnd.atTime(23, 59, 59))); - - Map point = new HashMap<>(); - point.put("month", monthStart.format(formatter)); - point.put("tenantCount", tenantCount); - point.put("lessonCount", lessonCount); - point.put("studentCount", studentCount); - trend.add(point); - } - return trend; - } - - public List> getActiveTenants(int limit) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Tenant::getStatus, "active") - .orderByDesc(Tenant::getCreatedAt) - .last("LIMIT " + limit); - List tenants = tenantMapper.selectList(wrapper); - return tenants.stream().map(t -> { - Map map = new HashMap<>(); - map.put("id", t.getId()); - map.put("name", t.getName()); - map.put("code", t.getCode()); - map.put("status", t.getStatus()); - map.put("expireAt", t.getExpireAt()); - // Count teachers and students for this tenant - long teacherCount = teacherMapper.selectCount( - new LambdaQueryWrapper().eq(Teacher::getTenantId, t.getId())); - long studentCount = studentMapper.selectCount( - new LambdaQueryWrapper().eq(Student::getTenantId, t.getId())); - long lessonCount = lessonMapper.selectCount( - new LambdaQueryWrapper().eq(Lesson::getTenantId, t.getId())); - map.put("teacherCount", teacherCount); - map.put("studentCount", studentCount); - map.put("lessonCount", lessonCount); - return map; - }).collect(java.util.stream.Collectors.toList()); - } - - public List> getPopularCourses(int limit) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Course::getIsSystem, 1) - .eq(Course::getStatus, "published") - .orderByDesc(Course::getCreatedAt) - .last("LIMIT " + limit); - List courses = courseMapper.selectList(wrapper); - return courses.stream().map(c -> { - Map map = new HashMap<>(); - map.put("id", c.getId()); - map.put("name", c.getName()); - map.put("category", c.getCategory()); - map.put("status", c.getStatus()); - map.put("usageCount", c.getUsageCount() != null ? c.getUsageCount() : 0); - map.put("teacherCount", c.getTeacherCount() != null ? c.getTeacherCount() : 0); - return map; - }).collect(java.util.stream.Collectors.toList()); - } - - public List> getRecentActivities(int limit) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.orderByDesc(Lesson::getCreatedAt).last("LIMIT " + limit); - List lessons = lessonMapper.selectList(wrapper); - return lessons.stream().map(l -> { - Map map = new HashMap<>(); - map.put("id", l.getId()); - map.put("title", l.getTitle()); - map.put("lessonDate", l.getLessonDate()); - map.put("status", l.getStatus()); - return map; - }).collect(java.util.stream.Collectors.toList()); - } + /** + * 获取最近活动 + */ + List> getRecentActivities(int limit); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/AuthService.java b/reading-platform-java/src/main/java/com/reading/platform/service/AuthService.java index 618400c..1aeec36 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/AuthService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/AuthService.java @@ -15,4 +15,10 @@ public interface AuthService { void changePassword(String oldPassword, String newPassword); + /** + * 登出 - 删除 Redis 中的 Token + * @param token JWT token + */ + void logout(String token); + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java index bf1916e..990e23d 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java @@ -26,6 +26,10 @@ public interface ClassService { void assignStudents(Long classId, List studentIds); + void removeTeacher(Long classId, Long teacherId); + + void removeStudent(Long classId, Long studentId); + List getTeacherIdsByClassId(Long classId); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java index 4504c46..f7151d6 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java @@ -1,49 +1,36 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.entity.CourseLesson; -import com.reading.platform.mapper.CourseLessonMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import java.util.List; -@Service -@RequiredArgsConstructor -public class CourseLessonService { +/** + * 课程课时服务接口 + */ +public interface CourseLessonService { - private final CourseLessonMapper courseLessonMapper; + /** + * 根据课程 ID 获取课时列表 + */ + List getLessonsByCourse(Long courseId); - public List getLessonsByCourse(Long courseId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CourseLesson::getCourseId, courseId) - .orderByAsc(CourseLesson::getSortOrder); - return courseLessonMapper.selectList(wrapper); - } + /** + * 根据 ID 获取课时 + */ + CourseLesson getLessonById(Long id); - public CourseLesson getLessonById(Long id) { - CourseLesson lesson = courseLessonMapper.selectById(id); - if (lesson == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course lesson not found"); - } - return lesson; - } + /** + * 创建课时 + */ + CourseLesson createLesson(CourseLesson lesson); - public CourseLesson createLesson(CourseLesson lesson) { - courseLessonMapper.insert(lesson); - return lesson; - } + /** + * 更新课时 + */ + CourseLesson updateLesson(Long id, CourseLesson lesson); - public CourseLesson updateLesson(Long id, CourseLesson lesson) { - getLessonById(id); - lesson.setId(id); - courseLessonMapper.updateById(lesson); - return courseLessonMapper.selectById(id); - } - - public void deleteLesson(Long id) { - courseLessonMapper.deleteById(id); - } + /** + * 删除课时 + */ + void deleteLesson(Long id); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java index 4d9cbff..0e41ae7 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java @@ -1,78 +1,55 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.entity.CoursePackage; -import com.reading.platform.mapper.CoursePackageMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -@Service -@RequiredArgsConstructor -public class CoursePackageService { +/** + * 课程包服务接口 + */ +public interface CoursePackageService { - private final CoursePackageMapper coursePackageMapper; + /** + * 获取课程包分页 + */ + Page getPackages(int pageNum, int pageSize, String keyword, String status); - public Page getPackages(int pageNum, int pageSize, String keyword, String status) { - Page page = new Page<>(pageNum, pageSize); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (keyword != null && !keyword.isEmpty()) { - wrapper.like(CoursePackage::getName, keyword); - } - if (status != null && !status.isEmpty()) { - wrapper.eq(CoursePackage::getStatus, status); - } - wrapper.orderByDesc(CoursePackage::getCreatedAt); - return coursePackageMapper.selectPage(page, wrapper); - } + /** + * 根据 ID 获取课程包 + */ + CoursePackage getPackageById(Long id); - public CoursePackage getPackageById(Long id) { - CoursePackage pkg = coursePackageMapper.selectById(id); - if (pkg == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course package not found"); - } - return pkg; - } + /** + * 创建课程包 + */ + CoursePackage createPackage(CoursePackage pkg); - public CoursePackage createPackage(CoursePackage pkg) { - coursePackageMapper.insert(pkg); - return pkg; - } + /** + * 更新课程包 + */ + CoursePackage updatePackage(Long id, CoursePackage pkg); - public CoursePackage updatePackage(Long id, CoursePackage pkg) { - getPackageById(id); - pkg.setId(id); - coursePackageMapper.updateById(pkg); - return coursePackageMapper.selectById(id); - } + /** + * 删除课程包 + */ + void deletePackage(Long id); - public void deletePackage(Long id) { - coursePackageMapper.deleteById(id); - } + /** + * 提交审核 + */ + void submitPackage(Long id); - public void submitPackage(Long id) { - CoursePackage pkg = getPackageById(id); - pkg.setStatus("pending"); - coursePackageMapper.updateById(pkg); - } + /** + * 审核课程包 + */ + void reviewPackage(Long id, boolean approved, String comment); - public void reviewPackage(Long id, boolean approved, String comment) { - CoursePackage pkg = getPackageById(id); - pkg.setStatus(approved ? "published" : "rejected"); - coursePackageMapper.updateById(pkg); - } + /** + * 发布课程包 + */ + void publishPackage(Long id); - public void publishPackage(Long id) { - CoursePackage pkg = getPackageById(id); - pkg.setStatus("published"); - coursePackageMapper.updateById(pkg); - } - - public void offlinePackage(Long id) { - CoursePackage pkg = getPackageById(id); - pkg.setStatus("archived"); - coursePackageMapper.updateById(pkg); - } + /** + * 下架课程包 + */ + void offlinePackage(Long id); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ExportService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ExportService.java index 6ea6372..a5d98c4 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/ExportService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/ExportService.java @@ -1,141 +1,29 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.reading.platform.entity.*; -import com.reading.platform.mapper.*; -import lombok.RequiredArgsConstructor; -import org.apache.poi.ss.usermodel.*; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.stereotype.Service; - -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.List; -@Service -@RequiredArgsConstructor -public class ExportService { +/** + * 导出服务接口 + */ +public interface ExportService { - private final TeacherMapper teacherMapper; - private final StudentMapper studentMapper; - private final LessonMapper lessonMapper; - private final GrowthRecordMapper growthRecordMapper; + /** + * 导出教师数据 + */ + byte[] exportTeachers(Long tenantId) throws IOException; - public byte[] exportTeachers(Long tenantId) throws IOException { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Teacher::getTenantId, tenantId); - List teachers = teacherMapper.selectList(wrapper); + /** + * 导出学生数据 + */ + byte[] exportStudents(Long tenantId) throws IOException; - try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("Teachers"); - String[] headers = {"ID", "Name", "Username", "Phone", "Email", "Gender", "Status"}; - createHeaderRow(sheet, headers); + /** + * 导出课时数据 + */ + byte[] exportLessons(Long tenantId) throws IOException; - int rowNum = 1; - for (Teacher t : teachers) { - Row row = sheet.createRow(rowNum++); - row.createCell(0).setCellValue(t.getId()); - row.createCell(1).setCellValue(t.getName() != null ? t.getName() : ""); - row.createCell(2).setCellValue(t.getUsername() != null ? t.getUsername() : ""); - row.createCell(3).setCellValue(t.getPhone() != null ? t.getPhone() : ""); - row.createCell(4).setCellValue(t.getEmail() != null ? t.getEmail() : ""); - row.createCell(5).setCellValue(t.getGender() != null ? t.getGender() : ""); - row.createCell(6).setCellValue(t.getStatus() != null ? t.getStatus() : ""); - } - return toBytes(workbook); - } - } - - public byte[] exportStudents(Long tenantId) throws IOException { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Student::getTenantId, tenantId); - List students = studentMapper.selectList(wrapper); - - try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("Students"); - String[] headers = {"ID", "Name", "Gender", "Birth Date", "Grade", "Student No", "Status"}; - createHeaderRow(sheet, headers); - - int rowNum = 1; - for (Student s : students) { - Row row = sheet.createRow(rowNum++); - row.createCell(0).setCellValue(s.getId()); - row.createCell(1).setCellValue(s.getName() != null ? s.getName() : ""); - row.createCell(2).setCellValue(s.getGender() != null ? s.getGender() : ""); - row.createCell(3).setCellValue(s.getBirthDate() != null ? s.getBirthDate().toString() : ""); - row.createCell(4).setCellValue(s.getGrade() != null ? s.getGrade() : ""); - row.createCell(5).setCellValue(s.getStudentNo() != null ? s.getStudentNo() : ""); - row.createCell(6).setCellValue(s.getStatus() != null ? s.getStatus() : ""); - } - return toBytes(workbook); - } - } - - public byte[] exportLessons(Long tenantId) throws IOException { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Lesson::getTenantId, tenantId); - List lessons = lessonMapper.selectList(wrapper); - - try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("Lessons"); - String[] headers = {"ID", "Title", "Lesson Date", "Start Time", "End Time", "Location", "Status"}; - createHeaderRow(sheet, headers); - - int rowNum = 1; - for (Lesson l : lessons) { - Row row = sheet.createRow(rowNum++); - row.createCell(0).setCellValue(l.getId()); - row.createCell(1).setCellValue(l.getTitle() != null ? l.getTitle() : ""); - row.createCell(2).setCellValue(l.getLessonDate() != null ? l.getLessonDate().toString() : ""); - row.createCell(3).setCellValue(l.getStartTime() != null ? l.getStartTime().toString() : ""); - row.createCell(4).setCellValue(l.getEndTime() != null ? l.getEndTime().toString() : ""); - row.createCell(5).setCellValue(l.getLocation() != null ? l.getLocation() : ""); - row.createCell(6).setCellValue(l.getStatus() != null ? l.getStatus() : ""); - } - return toBytes(workbook); - } - } - - public byte[] exportGrowthRecords(Long tenantId) throws IOException { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(GrowthRecord::getTenantId, tenantId); - List records = growthRecordMapper.selectList(wrapper); - - try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("Growth Records"); - String[] headers = {"ID", "Student ID", "Type", "Title", "Content", "Record Date"}; - createHeaderRow(sheet, headers); - - int rowNum = 1; - for (GrowthRecord g : records) { - Row row = sheet.createRow(rowNum++); - row.createCell(0).setCellValue(g.getId()); - row.createCell(1).setCellValue(g.getStudentId()); - row.createCell(2).setCellValue(g.getType() != null ? g.getType() : ""); - row.createCell(3).setCellValue(g.getTitle() != null ? g.getTitle() : ""); - row.createCell(4).setCellValue(g.getContent() != null ? g.getContent() : ""); - row.createCell(5).setCellValue(g.getRecordDate() != null ? g.getRecordDate().toString() : ""); - } - return toBytes(workbook); - } - } - - private void createHeaderRow(Sheet sheet, String[] headers) { - Row headerRow = sheet.createRow(0); - CellStyle style = sheet.getWorkbook().createCellStyle(); - Font font = sheet.getWorkbook().createFont(); - font.setBold(true); - style.setFont(font); - for (int i = 0; i < headers.length; i++) { - Cell cell = headerRow.createCell(i); - cell.setCellValue(headers[i]); - cell.setCellStyle(style); - } - } - - private byte[] toBytes(Workbook workbook) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - workbook.write(out); - return out.toByteArray(); - } + /** + * 导出成长档案 + */ + byte[] exportGrowthRecords(Long tenantId) throws IOException; } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/FileUploadService.java b/reading-platform-java/src/main/java/com/reading/platform/service/FileUploadService.java index fc1e294..b1f8104 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/FileUploadService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/FileUploadService.java @@ -1,71 +1,19 @@ package com.reading.platform.service; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.UUID; +/** + * 文件上传服务接口 + */ +public interface FileUploadService { -@Slf4j -@Service -public class FileUploadService { + /** + * 上传文件 + */ + String uploadFile(MultipartFile file); - @Value("${file.upload.path:/app/uploads/}") - private String uploadPath; - - @Value("${file.upload.base-url:/uploads/}") - private String baseUrl; - - public String uploadFile(MultipartFile file) { - if (file == null || file.isEmpty()) { - throw new BusinessException(ErrorCode.INVALID_PARAMETER, "File cannot be empty"); - } - - String originalFilename = file.getOriginalFilename(); - String extension = ""; - if (originalFilename != null && originalFilename.contains(".")) { - extension = originalFilename.substring(originalFilename.lastIndexOf(".")); - } - - String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); - String newFilename = UUID.randomUUID().toString().replace("-", "") + extension; - String relativePath = datePath + "/" + newFilename; - String fullPath = uploadPath + relativePath; - - try { - Path targetPath = Paths.get(fullPath); - Files.createDirectories(targetPath.getParent()); - file.transferTo(targetPath.toFile()); - log.info("File uploaded: {}", fullPath); - return baseUrl + relativePath; - } catch (IOException e) { - log.error("File upload failed", e); - throw new BusinessException(ErrorCode.INTERNAL_ERROR, "File upload failed: " + e.getMessage()); - } - } - - public void deleteFile(String filePath) { - if (filePath == null || filePath.isEmpty()) { - return; - } - String relativePath = filePath.startsWith(baseUrl) ? filePath.substring(baseUrl.length()) : filePath; - String fullPath = uploadPath + relativePath; - File file = new File(fullPath); - if (file.exists()) { - boolean deleted = file.delete(); - if (!deleted) { - log.warn("Failed to delete file: {}", fullPath); - } - } - } + /** + * 删除文件 + */ + void deleteFile(String filePath); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/OperationLogService.java b/reading-platform-java/src/main/java/com/reading/platform/service/OperationLogService.java index dfbc45e..54bfb42 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/OperationLogService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/OperationLogService.java @@ -1,48 +1,20 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.entity.OperationLog; -import com.reading.platform.mapper.OperationLogMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -@Service -@RequiredArgsConstructor -public class OperationLogService { +/** + * 操作日志服务接口 + */ +public interface OperationLogService { - private final OperationLogMapper operationLogMapper; + /** + * 获取操作日志分页 + */ + Page getLogs(int pageNum, int pageSize, Long tenantId, String module); - public Page getLogs(int pageNum, int pageSize, Long tenantId, String module) { - Page page = new Page<>(pageNum, pageSize); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (tenantId != null) { - wrapper.eq(OperationLog::getTenantId, tenantId); - } - if (module != null && !module.isEmpty()) { - wrapper.eq(OperationLog::getModule, module); - } - wrapper.orderByDesc(OperationLog::getCreatedAt); - return operationLogMapper.selectPage(page, wrapper); - } - - public void log(String action, String module, String targetType, Long targetId, String details) { - try { - OperationLog log = new OperationLog(); - log.setAction(action); - log.setModule(module); - log.setTargetType(targetType); - log.setTargetId(targetId); - log.setDetails(details); - try { - log.setUserId(SecurityUtils.getCurrentUserId()); - log.setUserRole(SecurityUtils.getCurrentRole()); - log.setTenantId(SecurityUtils.getCurrentTenantId()); - } catch (Exception ignored) {} - operationLogMapper.insert(log); - } catch (Exception e) { - // Log silently - don't fail main operations - } - } + /** + * 记录操作日志 + */ + void log(String action, String module, String targetType, Long targetId, String details); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ResourceService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ResourceService.java index feeef2c..efa2d19 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/ResourceService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/ResourceService.java @@ -1,92 +1,63 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.entity.ResourceItem; import com.reading.platform.entity.ResourceLibrary; -import com.reading.platform.mapper.ResourceItemMapper; -import com.reading.platform.mapper.ResourceLibraryMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import java.util.List; -@Service -@RequiredArgsConstructor -public class ResourceService { +/** + * 资源服务接口 + */ +public interface ResourceService { - private final ResourceLibraryMapper resourceLibraryMapper; - private final ResourceItemMapper resourceItemMapper; + /** + * 获取资源库列表 + */ + List getLibraries(Long tenantId); - public List getLibraries(Long tenantId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (tenantId != null) { - wrapper.eq(ResourceLibrary::getTenantId, tenantId); - } - wrapper.orderByDesc(ResourceLibrary::getCreatedAt); - return resourceLibraryMapper.selectList(wrapper); - } + /** + * 根据 ID 获取资源库 + */ + ResourceLibrary getLibraryById(Long id); - public ResourceLibrary getLibraryById(Long id) { - ResourceLibrary lib = resourceLibraryMapper.selectById(id); - if (lib == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource library not found"); - } - return lib; - } + /** + * 创建资源库 + */ + ResourceLibrary createLibrary(ResourceLibrary library); - public ResourceLibrary createLibrary(ResourceLibrary library) { - resourceLibraryMapper.insert(library); - return library; - } + /** + * 更新资源库 + */ + ResourceLibrary updateLibrary(Long id, ResourceLibrary library); - public ResourceLibrary updateLibrary(Long id, ResourceLibrary library) { - getLibraryById(id); - library.setId(id); - resourceLibraryMapper.updateById(library); - return resourceLibraryMapper.selectById(id); - } + /** + * 删除资源库 + */ + void deleteLibrary(Long id); - public void deleteLibrary(Long id) { - resourceLibraryMapper.deleteById(id); - } + /** + * 获取资源项分页 + */ + Page getItems(int pageNum, int pageSize, Long libraryId, String keyword); - public Page getItems(int pageNum, int pageSize, Long libraryId, String keyword) { - Page page = new Page<>(pageNum, pageSize); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (libraryId != null) { - wrapper.eq(ResourceItem::getLibraryId, libraryId); - } - if (keyword != null && !keyword.isEmpty()) { - wrapper.like(ResourceItem::getName, keyword); - } - wrapper.orderByDesc(ResourceItem::getCreatedAt); - return resourceItemMapper.selectPage(page, wrapper); - } + /** + * 根据 ID 获取资源项 + */ + ResourceItem getItemById(Long id); - public ResourceItem getItemById(Long id) { - ResourceItem item = resourceItemMapper.selectById(id); - if (item == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource item not found"); - } - return item; - } + /** + * 创建资源项 + */ + ResourceItem createItem(ResourceItem item); - public ResourceItem createItem(ResourceItem item) { - resourceItemMapper.insert(item); - return item; - } + /** + * 更新资源项 + */ + ResourceItem updateItem(Long id, ResourceItem item); - public ResourceItem updateItem(Long id, ResourceItem item) { - getItemById(id); - item.setId(id); - resourceItemMapper.updateById(item); - return resourceItemMapper.selectById(id); - } - - public void deleteItem(Long id) { - resourceItemMapper.deleteById(id); - } + /** + * 删除资源项 + */ + void deleteItem(Long id); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleService.java index 813dd76..9607182 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleService.java @@ -1,75 +1,92 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.dto.request.SchedulePlanCreateRequest; +import com.reading.platform.dto.request.ScheduleTemplateApplyRequest; import com.reading.platform.entity.SchedulePlan; import com.reading.platform.entity.ScheduleTemplate; -import com.reading.platform.mapper.SchedulePlanMapper; -import com.reading.platform.mapper.ScheduleTemplateMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -@Service -@RequiredArgsConstructor -public class ScheduleService { +import java.time.LocalDate; +import java.util.List; +import java.util.Map; - private final SchedulePlanMapper schedulePlanMapper; - private final ScheduleTemplateMapper scheduleTemplateMapper; +/** + * 课表服务接口 + */ +public interface ScheduleService { - public Page getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId) { - Page page = new Page<>(pageNum, pageSize); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SchedulePlan::getTenantId, tenantId); - if (classId != null) { - wrapper.eq(SchedulePlan::getClassId, classId); - } - wrapper.orderByDesc(SchedulePlan::getCreatedAt); - return schedulePlanMapper.selectPage(page, wrapper); - } + /** + * 获取课表计划分页 + */ + Page getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId); - public SchedulePlan getSchedulePlanById(Long id) { - SchedulePlan plan = schedulePlanMapper.selectById(id); - if (plan == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Schedule plan not found"); - } - return plan; - } + /** + * 获取课表计划分页(按日期范围) + */ + Page getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate); - public SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan) { - plan.setTenantId(tenantId); - schedulePlanMapper.insert(plan); - return plan; - } + /** + * 根据 ID 获取课表计划 + */ + SchedulePlan getSchedulePlanById(Long id); - public SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan) { - SchedulePlan existing = getSchedulePlanById(id); - plan.setId(id); - plan.setTenantId(existing.getTenantId()); - schedulePlanMapper.updateById(plan); - return schedulePlanMapper.selectById(id); - } + /** + * 创建课表计划 + */ + SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan); - public void deleteSchedulePlan(Long id) { - schedulePlanMapper.deleteById(id); - } + /** + * 创建课表计划 + */ + SchedulePlan createSchedulePlan(Long tenantId, Long userId, SchedulePlanCreateRequest request); - public Page getScheduleTemplates(int pageNum, int pageSize, Long tenantId) { - Page page = new Page<>(pageNum, pageSize); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ScheduleTemplate::getTenantId, tenantId) - .orderByDesc(ScheduleTemplate::getCreatedAt); - return scheduleTemplateMapper.selectPage(page, wrapper); - } + /** + * 更新课表计划 + */ + SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan); - public ScheduleTemplate createScheduleTemplate(Long tenantId, ScheduleTemplate template) { - template.setTenantId(tenantId); - scheduleTemplateMapper.insert(template); - return template; - } + /** + * 删除课表计划 + */ + void deleteSchedulePlan(Long id); - public void deleteScheduleTemplate(Long id) { - scheduleTemplateMapper.deleteById(id); - } + /** + * 批量创建课表计划 + */ + List batchCreateSchedules(Long tenantId, Long userId, List requests); + + /** + * 获取课表 + */ + List> getTimetable(Long tenantId, LocalDate startDate, LocalDate endDate, Long classId); + + /** + * 获取课表模板分页 + */ + Page getScheduleTemplates(int pageNum, int pageSize, Long tenantId); + + /** + * 根据 ID 获取课表模板 + */ + ScheduleTemplate getScheduleTemplateById(Long id); + + /** + * 创建课表模板 + */ + ScheduleTemplate createScheduleTemplate(Long tenantId, ScheduleTemplate template); + + /** + * 更新课表模板 + */ + ScheduleTemplate updateScheduleTemplate(Long id, ScheduleTemplate template); + + /** + * 删除课表模板 + */ + void deleteScheduleTemplate(Long id); + + /** + * 应用课表模板 + */ + List applyScheduleTemplate(Long tenantId, Long templateId, ScheduleTemplateApplyRequest request); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/SchoolCourseService.java b/reading-platform-java/src/main/java/com/reading/platform/service/SchoolCourseService.java index c925f06..c3561ce 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/SchoolCourseService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/SchoolCourseService.java @@ -1,55 +1,35 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.entity.SchoolCourse; -import com.reading.platform.mapper.SchoolCourseMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -@Service -@RequiredArgsConstructor -public class SchoolCourseService { +/** + * 校本课程服务接口 + */ +public interface SchoolCourseService { - private final SchoolCourseMapper schoolCourseMapper; + /** + * 获取校本课程分页 + */ + Page getCourses(int pageNum, int pageSize, Long tenantId, String keyword); - public Page getCourses(int pageNum, int pageSize, Long tenantId, String keyword) { - Page page = new Page<>(pageNum, pageSize); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SchoolCourse::getTenantId, tenantId); - if (keyword != null && !keyword.isEmpty()) { - wrapper.like(SchoolCourse::getName, keyword); - } - wrapper.orderByDesc(SchoolCourse::getCreatedAt); - return schoolCourseMapper.selectPage(page, wrapper); - } + /** + * 根据 ID 获取校本课程 + */ + SchoolCourse getCourseById(Long id); - public SchoolCourse getCourseById(Long id) { - SchoolCourse course = schoolCourseMapper.selectById(id); - if (course == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "School course not found"); - } - return course; - } + /** + * 创建校本课程 + */ + SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course); - public SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course) { - course.setTenantId(tenantId); - course.setCreatedBy(userId); - schoolCourseMapper.insert(course); - return course; - } + /** + * 更新校本课程 + */ + SchoolCourse updateCourse(Long id, SchoolCourse course); - public SchoolCourse updateCourse(Long id, SchoolCourse course) { - SchoolCourse existing = getCourseById(id); - course.setId(id); - course.setTenantId(existing.getTenantId()); - schoolCourseMapper.updateById(course); - return schoolCourseMapper.selectById(id); - } - - public void deleteCourse(Long id) { - schoolCourseMapper.deleteById(id); - } + /** + * 删除校本课程 + */ + void deleteCourse(Long id); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/SchoolStatsService.java b/reading-platform-java/src/main/java/com/reading/platform/service/SchoolStatsService.java index f02a20e..f17df04 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/SchoolStatsService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/SchoolStatsService.java @@ -1,33 +1,41 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.reading.platform.entity.*; -import com.reading.platform.mapper.*; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.HashMap; +import java.util.List; import java.util.Map; -@Service -@RequiredArgsConstructor -public class SchoolStatsService { +/** + * 学校统计服务接口 + */ +public interface SchoolStatsService { - private final TeacherMapper teacherMapper; - private final StudentMapper studentMapper; - private final ClazzMapper clazzMapper; - private final LessonMapper lessonMapper; + /** + * 获取学校统计数据 + */ + Map getStats(Long tenantId); + + /** + * 获取活跃教师统计(授课次数最多的前 N 名教师) + */ + List> getActiveTeachers(Long tenantId, Integer limit); + + /** + * 获取课程使用统计(按课时完成次数排序) + */ + List> getCourseUsageStats(Long tenantId); + + /** + * 获取最近活动记录 + */ + List> getRecentActivities(Long tenantId, Integer limit); + + /** + * 获取课时趋势(最近 N 个月) + */ + List> getLessonTrend(Long tenantId, Integer months); + + /** + * 获取课程分布统计(饼图数据) + */ + List> getCourseDistribution(Long tenantId); - public Map getStats(Long tenantId) { - Map stats = new HashMap<>(); - stats.put("teacherCount", teacherMapper.selectCount( - new LambdaQueryWrapper().eq(Teacher::getTenantId, tenantId))); - stats.put("studentCount", studentMapper.selectCount( - new LambdaQueryWrapper().eq(Student::getTenantId, tenantId))); - stats.put("classCount", clazzMapper.selectCount( - new LambdaQueryWrapper().eq(Clazz::getTenantId, tenantId))); - stats.put("lessonCount", lessonMapper.selectCount( - new LambdaQueryWrapper().eq(Lesson::getTenantId, tenantId))); - return stats; - } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/StudentService.java b/reading-platform-java/src/main/java/com/reading/platform/service/StudentService.java index d2002bc..a57a999 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/StudentService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/StudentService.java @@ -26,4 +26,6 @@ public interface StudentService { List getStudentsByParentId(Long parentId); + List importStudents(Long tenantId, List requests); + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/SystemSettingService.java b/reading-platform-java/src/main/java/com/reading/platform/service/SystemSettingService.java index cb410b3..fe0a39a 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/SystemSettingService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/SystemSettingService.java @@ -1,61 +1,24 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.reading.platform.entity.SystemSetting; -import com.reading.platform.mapper.SystemSettingMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.List; import java.util.Map; -@Service -@RequiredArgsConstructor -public class SystemSettingService { +/** + * 系统设置服务接口 + */ +public interface SystemSettingService { - private final SystemSettingMapper systemSettingMapper; + /** + * 获取系统设置 + */ + Map getSettings(Long tenantId); - public Map getSettings(Long tenantId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SystemSetting::getTenantId, tenantId); - List settings = systemSettingMapper.selectList(wrapper); - Map result = new HashMap<>(); - for (SystemSetting s : settings) { - result.put(s.getSettingKey(), s.getSettingValue()); - } - return result; - } + /** + * 更新系统设置 + */ + void updateSettings(Long tenantId, Map settings); - public void updateSettings(Long tenantId, Map settings) { - for (Map.Entry entry : settings.entrySet()) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SystemSetting::getTenantId, tenantId) - .eq(SystemSetting::getSettingKey, entry.getKey()); - SystemSetting existing = systemSettingMapper.selectOne(wrapper); - if (existing != null) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(SystemSetting::getTenantId, tenantId) - .eq(SystemSetting::getSettingKey, entry.getKey()); - SystemSetting update = new SystemSetting(); - update.setSettingValue(entry.getValue()); - systemSettingMapper.update(update, updateWrapper); - } else { - SystemSetting newSetting = new SystemSetting(); - newSetting.setTenantId(tenantId); - newSetting.setSettingKey(entry.getKey()); - newSetting.setSettingValue(entry.getValue()); - systemSettingMapper.insert(newSetting); - } - } - } - - public String getSetting(Long tenantId, String key) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SystemSetting::getTenantId, tenantId) - .eq(SystemSetting::getSettingKey, key); - SystemSetting setting = systemSettingMapper.selectOne(wrapper); - return setting != null ? setting.getSettingValue() : null; - } + /** + * 获取单个设置项 + */ + String getSetting(Long tenantId, String key); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/TaskService.java b/reading-platform-java/src/main/java/com/reading/platform/service/TaskService.java index 8a55780..7c37028 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/TaskService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/TaskService.java @@ -1,11 +1,13 @@ package com.reading.platform.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.dto.request.TaskCreateRequest; -import com.reading.platform.dto.request.TaskUpdateRequest; +import com.reading.platform.dto.request.*; import com.reading.platform.entity.Task; +import com.reading.platform.entity.TaskCompletion; +import com.reading.platform.entity.TaskTemplate; import java.util.List; +import java.util.Map; /** * Task Service Interface @@ -28,4 +30,73 @@ public interface TaskService { List getTasksByClassId(Long classId); + // ==================== 任务统计 ==================== + + /** + * 获取任务统计数据 + */ + Map getTaskStats(Long tenantId); + + /** + * 按任务类型统计 + */ + Map getStatsByType(Long tenantId); + + /** + * 按班级统计 + */ + List> getStatsByClass(Long tenantId); + + /** + * 获取月度统计趋势 + */ + List> getMonthlyStats(Long tenantId, Integer months); + + /** + * 获取任务完成情况分页 + */ + Page getTaskCompletions(Long tenantId, Long taskId, Integer pageNum, Integer pageSize, String status); + + /** + * 更新任务完成状态 + */ + TaskCompletion updateTaskCompletion(Long tenantId, Long taskId, Long studentId, String status, String feedback); + + // ==================== 任务模板 ==================== + + /** + * 获取模板列表 + */ + Page getTemplatePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String type); + + /** + * 根据 ID 获取模板 + */ + TaskTemplate getTemplateById(Long tenantId, Long id); + + /** + * 获取默认模板(按类型) + */ + TaskTemplate getDefaultTemplate(Long tenantId, String taskType); + + /** + * 创建模板 + */ + TaskTemplate createTemplate(Long tenantId, Long creatorId, TaskTemplateCreateRequest request); + + /** + * 更新模板 + */ + TaskTemplate updateTemplate(Long tenantId, Long id, TaskTemplateUpdateRequest request); + + /** + * 删除模板 + */ + void deleteTemplate(Long tenantId, Long id); + + /** + * 从模板创建任务 + */ + Task createTaskFromTemplate(Long tenantId, Long creatorId, String creatorRole, CreateTaskFromTemplateRequest request); + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/TeacherDashboardService.java b/reading-platform-java/src/main/java/com/reading/platform/service/TeacherDashboardService.java index f020839..0899128 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/TeacherDashboardService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/TeacherDashboardService.java @@ -1,86 +1,25 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.reading.platform.entity.*; -import com.reading.platform.mapper.*; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.LocalDate; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -@Service -@RequiredArgsConstructor -public class TeacherDashboardService { +/** + * 教师仪表板服务接口 + */ +public interface TeacherDashboardService { - private final LessonMapper lessonMapper; - private final TaskMapper taskMapper; - private final GrowthRecordMapper growthRecordMapper; - private final NotificationMapper notificationMapper; + /** + * 获取仪表板数据 + */ + Map getDashboard(Long teacherId, Long tenantId); - public Map getDashboard(Long teacherId, Long tenantId) { - Map dashboard = new HashMap<>(); - dashboard.put("lessonCount", lessonMapper.selectCount( - new LambdaQueryWrapper() - .eq(Lesson::getTeacherId, teacherId) - .eq(Lesson::getTenantId, tenantId))); - dashboard.put("taskCount", taskMapper.selectCount( - new LambdaQueryWrapper() - .eq(Task::getCreatorId, teacherId) - .eq(Task::getTenantId, tenantId))); - dashboard.put("growthRecordCount", growthRecordMapper.selectCount( - new LambdaQueryWrapper() - .eq(GrowthRecord::getRecordedBy, teacherId) - .eq(GrowthRecord::getTenantId, tenantId))); - dashboard.put("unreadNotifications", notificationMapper.selectCount( - new LambdaQueryWrapper() - .eq(Notification::getTenantId, tenantId) - .eq(Notification::getIsRead, 0))); - return dashboard; - } + /** + * 获取今天的课时 + */ + List> getTodayLessons(Long teacherId, Long tenantId); - public List> getTodayLessons(Long teacherId, Long tenantId) { - LocalDate today = LocalDate.now(); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Lesson::getTeacherId, teacherId) - .eq(Lesson::getTenantId, tenantId) - .eq(Lesson::getLessonDate, today) - .orderByAsc(Lesson::getStartTime); - List lessons = lessonMapper.selectList(wrapper); - return lessons.stream().map(l -> { - Map map = new HashMap<>(); - map.put("id", l.getId()); - map.put("title", l.getTitle()); - map.put("startTime", l.getStartTime()); - map.put("endTime", l.getEndTime()); - map.put("location", l.getLocation()); - map.put("status", l.getStatus()); - return map; - }).collect(Collectors.toList()); - } - - public List> getWeeklyLessons(Long teacherId, Long tenantId) { - LocalDate today = LocalDate.now(); - LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1); - LocalDate weekEnd = weekStart.plusDays(6); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Lesson::getTeacherId, teacherId) - .eq(Lesson::getTenantId, tenantId) - .between(Lesson::getLessonDate, weekStart, weekEnd) - .orderByAsc(Lesson::getLessonDate, Lesson::getStartTime); - List lessons = lessonMapper.selectList(wrapper); - return lessons.stream().map(l -> { - Map map = new HashMap<>(); - map.put("id", l.getId()); - map.put("title", l.getTitle()); - map.put("lessonDate", l.getLessonDate()); - map.put("startTime", l.getStartTime()); - map.put("endTime", l.getEndTime()); - map.put("status", l.getStatus()); - return map; - }).collect(Collectors.toList()); - } + /** + * 获取本周的课时 + */ + List> getWeeklyLessons(Long teacherId, Long tenantId); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java b/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java index 8f5d07f..4b7d803 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java @@ -9,7 +9,7 @@ import com.reading.platform.entity.Tenant; import java.util.List; /** - * Tenant Service Interface + * 租户服务接口 */ public interface TenantService { diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ThemeService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ThemeService.java index 16bd2a0..76ce3d9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/ThemeService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/ThemeService.java @@ -1,51 +1,36 @@ package com.reading.platform.service; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.reading.platform.common.exception.BusinessException; -import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.entity.Theme; -import com.reading.platform.mapper.ThemeMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import java.util.List; -@Service -@RequiredArgsConstructor -public class ThemeService { +/** + * 主题服务接口 + */ +public interface ThemeService { - private final ThemeMapper themeMapper; + /** + * 获取所有主题 + */ + List getAllThemes(Boolean enabledOnly); - public List getAllThemes(Boolean enabledOnly) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (Boolean.TRUE.equals(enabledOnly)) { - wrapper.eq(Theme::getIsEnabled, 1); - } - wrapper.orderByAsc(Theme::getSortOrder); - return themeMapper.selectList(wrapper); - } + /** + * 根据 ID 获取主题 + */ + Theme getThemeById(Long id); - public Theme getThemeById(Long id) { - Theme theme = themeMapper.selectById(id); - if (theme == null) { - throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Theme not found"); - } - return theme; - } + /** + * 创建主题 + */ + Theme createTheme(Theme theme); - public Theme createTheme(Theme theme) { - themeMapper.insert(theme); - return theme; - } + /** + * 更新主题 + */ + Theme updateTheme(Long id, Theme theme); - public Theme updateTheme(Long id, Theme theme) { - getThemeById(id); - theme.setId(id); - themeMapper.updateById(theme); - return themeMapper.selectById(id); - } - - public void deleteTheme(Long id) { - themeMapper.deleteById(id); - } + /** + * 删除主题 + */ + void deleteTheme(Long id); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/TokenService.java b/reading-platform-java/src/main/java/com/reading/platform/service/TokenService.java new file mode 100644 index 0000000..768b811 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/TokenService.java @@ -0,0 +1,36 @@ +package com.reading.platform.service; + +import com.reading.platform.common.security.JwtPayload; + +/** + * Token Service Interface - Redis 存储 Token + */ +public interface TokenService { + + /** + * 保存 Token 到 Redis + * @param token JWT token + * @param payload 用户信息 + */ + void saveToken(String token, JwtPayload payload); + + /** + * 从 Redis 获取 Token 对应的用户信息 + * @param token JWT token + * @return 用户信息,如果不存在返回 null + */ + JwtPayload getToken(String token); + + /** + * 删除 Token + * @param token JWT token + */ + void removeToken(String token); + + /** + * 检查 Token 是否存在 + * @param token JWT token + * @return 是否存在 + */ + boolean isTokenExist(String token); +} \ No newline at end of file diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/AdminStatsServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/AdminStatsServiceImpl.java new file mode 100644 index 0000000..120c3e8 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/AdminStatsServiceImpl.java @@ -0,0 +1,158 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.entity.*; +import com.reading.platform.mapper.*; +import com.reading.platform.service.AdminStatsService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 管理员统计服务实现类 + */ +@Service +@RequiredArgsConstructor +public class AdminStatsServiceImpl implements AdminStatsService { + + private final TenantMapper tenantMapper; + private final TeacherMapper teacherMapper; + private final StudentMapper studentMapper; + private final CourseMapper courseMapper; + private final LessonMapper lessonMapper; + + @Override + public Map getStats() { + Map stats = new HashMap<>(); + + long tenantCount = tenantMapper.selectCount(null); + long activeTenantCount = tenantMapper.selectCount( + new LambdaQueryWrapper().eq(Tenant::getStatus, "active")); + long courseCount = courseMapper.selectCount(null); + long publishedCourseCount = courseMapper.selectCount( + new LambdaQueryWrapper().eq(Course::getStatus, "published")); + + // Monthly lessons (current month) + LocalDate monthStart = LocalDate.now().withDayOfMonth(1); + LocalDate monthEnd = LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()); + long monthlyLessons = lessonMapper.selectCount( + new LambdaQueryWrapper() + .ge(Lesson::getLessonDate, monthStart) + .le(Lesson::getLessonDate, monthEnd)); + + stats.put("tenantCount", tenantCount); + stats.put("activeTenantCount", activeTenantCount); + stats.put("teacherCount", teacherMapper.selectCount(null)); + stats.put("studentCount", studentMapper.selectCount(null)); + stats.put("courseCount", courseCount); + stats.put("publishedCourseCount", publishedCourseCount); + stats.put("lessonCount", lessonMapper.selectCount(null)); + stats.put("monthlyLessons", monthlyLessons); + return stats; + } + + @Override + public List> getTrendData() { + List> trend = new ArrayList<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + LocalDate now = LocalDate.now(); + + for (int i = 5; i >= 0; i--) { + LocalDate monthStart = now.minusMonths(i).withDayOfMonth(1); + LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth()); + + long lessonCount = lessonMapper.selectCount( + new LambdaQueryWrapper() + .ge(Lesson::getLessonDate, monthStart) + .le(Lesson::getLessonDate, monthEnd)); + + // Count tenants created up to this month end + long tenantCount = tenantMapper.selectCount( + new LambdaQueryWrapper() + .le(Tenant::getCreatedAt, monthEnd.atTime(23, 59, 59))); + + // Count students created up to this month end + long studentCount = studentMapper.selectCount( + new LambdaQueryWrapper() + .le(Student::getCreatedAt, monthEnd.atTime(23, 59, 59))); + + Map point = new HashMap<>(); + point.put("month", monthStart.format(formatter)); + point.put("tenantCount", tenantCount); + point.put("lessonCount", lessonCount); + point.put("studentCount", studentCount); + trend.add(point); + } + return trend; + } + + @Override + public List> getActiveTenants(int limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Tenant::getStatus, "active") + .orderByDesc(Tenant::getCreatedAt) + .last("LIMIT " + limit); + List tenants = tenantMapper.selectList(wrapper); + return tenants.stream().map(t -> { + Map map = new HashMap<>(); + map.put("id", t.getId()); + map.put("name", t.getName()); + map.put("code", t.getCode()); + map.put("status", t.getStatus()); + map.put("expireAt", t.getExpireAt()); + // Count teachers and students for this tenant + long teacherCount = teacherMapper.selectCount( + new LambdaQueryWrapper().eq(Teacher::getTenantId, t.getId())); + long studentCount = studentMapper.selectCount( + new LambdaQueryWrapper().eq(Student::getTenantId, t.getId())); + long lessonCount = lessonMapper.selectCount( + new LambdaQueryWrapper().eq(Lesson::getTenantId, t.getId())); + map.put("teacherCount", teacherCount); + map.put("studentCount", studentCount); + map.put("lessonCount", lessonCount); + return map; + }).collect(Collectors.toList()); + } + + @Override + public List> getPopularCourses(int limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Course::getIsSystem, 1) + .eq(Course::getStatus, "published") + .orderByDesc(Course::getCreatedAt) + .last("LIMIT " + limit); + List courses = courseMapper.selectList(wrapper); + return courses.stream().map(c -> { + Map map = new HashMap<>(); + map.put("id", c.getId()); + map.put("name", c.getName()); + map.put("category", c.getCategory()); + map.put("status", c.getStatus()); + map.put("usageCount", c.getUsageCount() != null ? c.getUsageCount() : 0); + map.put("teacherCount", c.getTeacherCount() != null ? c.getTeacherCount() : 0); + return map; + }).collect(Collectors.toList()); + } + + @Override + public List> getRecentActivities(int limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByDesc(Lesson::getCreatedAt).last("LIMIT " + limit); + List lessons = lessonMapper.selectList(wrapper); + return lessons.stream().map(l -> { + Map map = new HashMap<>(); + map.put("id", l.getId()); + map.put("title", l.getTitle()); + map.put("lessonDate", l.getLessonDate()); + map.put("status", l.getStatus()); + return map; + }).collect(Collectors.toList()); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java index 7f8f08c..0643b1d 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java @@ -17,6 +17,7 @@ import com.reading.platform.mapper.AdminUserMapper; import com.reading.platform.mapper.ParentMapper; import com.reading.platform.mapper.TeacherMapper; import com.reading.platform.service.AuthService; +import com.reading.platform.service.TokenService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; @@ -34,6 +35,7 @@ public class AuthServiceImpl implements AuthService { private final ParentMapper parentMapper; private final JwtTokenProvider jwtTokenProvider; private final PasswordEncoder passwordEncoder; + private final TokenService tokenService; @Override public LoginResponse login(LoginRequest request) { @@ -69,8 +71,11 @@ public class AuthServiceImpl implements AuthService { .name(adminUser.getName()) .build(); + String token = jwtTokenProvider.generateToken(payload); + tokenService.saveToken(token, payload); + return LoginResponse.builder() - .token(jwtTokenProvider.generateToken(payload)) + .token(token) .userId(adminUser.getId()) .username(adminUser.getUsername()) .name(adminUser.getName()) @@ -101,8 +106,11 @@ public class AuthServiceImpl implements AuthService { .name(teacher.getName()) .build(); + String token = jwtTokenProvider.generateToken(payload); + tokenService.saveToken(token, payload); + return LoginResponse.builder() - .token(jwtTokenProvider.generateToken(payload)) + .token(token) .userId(teacher.getId()) .username(teacher.getUsername()) .name(teacher.getName()) @@ -133,8 +141,11 @@ public class AuthServiceImpl implements AuthService { .name(parent.getName()) .build(); + String token = jwtTokenProvider.generateToken(payload); + tokenService.saveToken(token, payload); + return LoginResponse.builder() - .token(jwtTokenProvider.generateToken(payload)) + .token(token) .userId(parent.getId()) .username(parent.getUsername()) .name(parent.getName()) @@ -171,8 +182,11 @@ public class AuthServiceImpl implements AuthService { .name(adminUser.getName()) .build(); + String token = jwtTokenProvider.generateToken(payload); + tokenService.saveToken(token, payload); + return LoginResponse.builder() - .token(jwtTokenProvider.generateToken(payload)) + .token(token) .userId(adminUser.getId()) .username(adminUser.getUsername()) .name(adminUser.getName()) @@ -202,8 +216,11 @@ public class AuthServiceImpl implements AuthService { .name(teacher.getName()) .build(); + String token = jwtTokenProvider.generateToken(payload); + tokenService.saveToken(token, payload); + return LoginResponse.builder() - .token(jwtTokenProvider.generateToken(payload)) + .token(token) .userId(teacher.getId()) .username(teacher.getUsername()) .name(teacher.getName()) @@ -232,8 +249,11 @@ public class AuthServiceImpl implements AuthService { .name(parent.getName()) .build(); + String token = jwtTokenProvider.generateToken(payload); + tokenService.saveToken(token, payload); + return LoginResponse.builder() - .token(jwtTokenProvider.generateToken(payload)) + .token(token) .userId(parent.getId()) .username(parent.getUsername()) .name(parent.getName()) @@ -329,4 +349,12 @@ public class AuthServiceImpl implements AuthService { } } + @Override + public void logout(String token) { + if (token != null && !token.isEmpty()) { + tokenService.removeToken(token); + log.info("User logged out, token removed from Redis"); + } + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java index 26fb4c0..1042716 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java @@ -172,4 +172,40 @@ public class ClassServiceImpl implements ClassService { return teacherIds; } + @Override + @Transactional + public void removeTeacher(Long classId, Long teacherId) { + // Verify class exists + getClassById(classId); + + // Delete teacher assignment + classTeacherMapper.delete( + new LambdaQueryWrapper() + .eq(ClassTeacher::getClassId, classId) + .eq(ClassTeacher::getTeacherId, teacherId) + ); + } + + @Override + @Transactional + public void removeStudent(Long classId, Long studentId) { + // Verify class exists + getClassById(classId); + + // End active class assignment for student + List existingHistories = studentClassHistoryMapper.selectList( + new LambdaQueryWrapper() + .eq(StudentClassHistory::getClassId, classId) + .eq(StudentClassHistory::getStudentId, studentId) + .eq(StudentClassHistory::getStatus, "active") + .isNull(StudentClassHistory::getEndDate) + ); + + for (StudentClassHistory history : existingHistories) { + history.setEndDate(LocalDate.now()); + history.setStatus("left"); + studentClassHistoryMapper.updateById(history); + } + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseLessonServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseLessonServiceImpl.java new file mode 100644 index 0000000..ae1c316 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseLessonServiceImpl.java @@ -0,0 +1,58 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.entity.CourseLesson; +import com.reading.platform.mapper.CourseLessonMapper; +import com.reading.platform.service.CourseLessonService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 课程课时服务实现类 + */ +@Service +@RequiredArgsConstructor +public class CourseLessonServiceImpl implements CourseLessonService { + + private final CourseLessonMapper courseLessonMapper; + + @Override + public List getLessonsByCourse(Long courseId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CourseLesson::getCourseId, courseId) + .orderByAsc(CourseLesson::getSortOrder); + return courseLessonMapper.selectList(wrapper); + } + + @Override + public CourseLesson getLessonById(Long id) { + CourseLesson lesson = courseLessonMapper.selectById(id); + if (lesson == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course lesson not found"); + } + return lesson; + } + + @Override + public CourseLesson createLesson(CourseLesson lesson) { + courseLessonMapper.insert(lesson); + return lesson; + } + + @Override + public CourseLesson updateLesson(Long id, CourseLesson lesson) { + getLessonById(id); + lesson.setId(id); + courseLessonMapper.updateById(lesson); + return courseLessonMapper.selectById(id); + } + + @Override + public void deleteLesson(Long id) { + courseLessonMapper.deleteById(id); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java new file mode 100644 index 0000000..7974b6b --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java @@ -0,0 +1,91 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.entity.CoursePackage; +import com.reading.platform.mapper.CoursePackageMapper; +import com.reading.platform.service.CoursePackageService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 课程包服务实现类 + */ +@Service +@RequiredArgsConstructor +public class CoursePackageServiceImpl implements CoursePackageService { + + private final CoursePackageMapper coursePackageMapper; + + @Override + public Page getPackages(int pageNum, int pageSize, String keyword, String status) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (keyword != null && !keyword.isEmpty()) { + wrapper.like(CoursePackage::getName, keyword); + } + if (status != null && !status.isEmpty()) { + wrapper.eq(CoursePackage::getStatus, status); + } + wrapper.orderByDesc(CoursePackage::getCreatedAt); + return coursePackageMapper.selectPage(page, wrapper); + } + + @Override + public CoursePackage getPackageById(Long id) { + CoursePackage pkg = coursePackageMapper.selectById(id); + if (pkg == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course package not found"); + } + return pkg; + } + + @Override + public CoursePackage createPackage(CoursePackage pkg) { + coursePackageMapper.insert(pkg); + return pkg; + } + + @Override + public CoursePackage updatePackage(Long id, CoursePackage pkg) { + getPackageById(id); + pkg.setId(id); + coursePackageMapper.updateById(pkg); + return coursePackageMapper.selectById(id); + } + + @Override + public void deletePackage(Long id) { + coursePackageMapper.deleteById(id); + } + + @Override + public void submitPackage(Long id) { + CoursePackage pkg = getPackageById(id); + pkg.setStatus("pending"); + coursePackageMapper.updateById(pkg); + } + + @Override + public void reviewPackage(Long id, boolean approved, String comment) { + CoursePackage pkg = getPackageById(id); + pkg.setStatus(approved ? "published" : "rejected"); + coursePackageMapper.updateById(pkg); + } + + @Override + public void publishPackage(Long id) { + CoursePackage pkg = getPackageById(id); + pkg.setStatus("published"); + coursePackageMapper.updateById(pkg); + } + + @Override + public void offlinePackage(Long id) { + CoursePackage pkg = getPackageById(id); + pkg.setStatus("archived"); + coursePackageMapper.updateById(pkg); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ExportServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ExportServiceImpl.java new file mode 100644 index 0000000..ecf202a --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ExportServiceImpl.java @@ -0,0 +1,155 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.entity.*; +import com.reading.platform.mapper.*; +import com.reading.platform.service.ExportService; +import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +/** + * 导出服务实现类 + */ +@Service +@RequiredArgsConstructor +public class ExportServiceImpl implements ExportService { + + private final TeacherMapper teacherMapper; + private final StudentMapper studentMapper; + private final LessonMapper lessonMapper; + private final GrowthRecordMapper growthRecordMapper; + + @Override + public byte[] exportTeachers(Long tenantId) throws IOException { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Teacher::getTenantId, tenantId); + List teachers = teacherMapper.selectList(wrapper); + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("Teachers"); + String[] headers = {"ID", "Name", "Username", "Phone", "Email", "Gender", "Status"}; + createHeaderRow(sheet, headers); + + int rowNum = 1; + for (Teacher t : teachers) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(t.getId()); + row.createCell(1).setCellValue(t.getName() != null ? t.getName() : ""); + row.createCell(2).setCellValue(t.getUsername() != null ? t.getUsername() : ""); + row.createCell(3).setCellValue(t.getPhone() != null ? t.getPhone() : ""); + row.createCell(4).setCellValue(t.getEmail() != null ? t.getEmail() : ""); + row.createCell(5).setCellValue(t.getGender() != null ? t.getGender() : ""); + row.createCell(6).setCellValue(t.getStatus() != null ? t.getStatus() : ""); + } + return toBytes(workbook); + } + } + + @Override + public byte[] exportStudents(Long tenantId) throws IOException { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Student::getTenantId, tenantId); + List students = studentMapper.selectList(wrapper); + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("Students"); + String[] headers = {"ID", "Name", "Gender", "Birth Date", "Grade", "Student No", "Status"}; + createHeaderRow(sheet, headers); + + int rowNum = 1; + for (Student s : students) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(s.getId()); + row.createCell(1).setCellValue(s.getName() != null ? s.getName() : ""); + row.createCell(2).setCellValue(s.getGender() != null ? s.getGender() : ""); + row.createCell(3).setCellValue(s.getBirthDate() != null ? s.getBirthDate().toString() : ""); + row.createCell(4).setCellValue(s.getGrade() != null ? s.getGrade() : ""); + row.createCell(5).setCellValue(s.getStudentNo() != null ? s.getStudentNo() : ""); + row.createCell(6).setCellValue(s.getStatus() != null ? s.getStatus() : ""); + } + return toBytes(workbook); + } + } + + @Override + public byte[] exportLessons(Long tenantId) throws IOException { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Lesson::getTenantId, tenantId); + List lessons = lessonMapper.selectList(wrapper); + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("Lessons"); + String[] headers = {"ID", "Title", "Lesson Date", "Start Time", "End Time", "Location", "Status"}; + createHeaderRow(sheet, headers); + + int rowNum = 1; + for (Lesson l : lessons) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(l.getId()); + row.createCell(1).setCellValue(l.getTitle() != null ? l.getTitle() : ""); + row.createCell(2).setCellValue(l.getLessonDate() != null ? l.getLessonDate().toString() : ""); + row.createCell(3).setCellValue(l.getStartTime() != null ? l.getStartTime().toString() : ""); + row.createCell(4).setCellValue(l.getEndTime() != null ? l.getEndTime().toString() : ""); + row.createCell(5).setCellValue(l.getLocation() != null ? l.getLocation() : ""); + row.createCell(6).setCellValue(l.getStatus() != null ? l.getStatus() : ""); + } + return toBytes(workbook); + } + } + + @Override + public byte[] exportGrowthRecords(Long tenantId) throws IOException { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthRecord::getTenantId, tenantId); + List records = growthRecordMapper.selectList(wrapper); + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("Growth Records"); + String[] headers = {"ID", "Student ID", "Type", "Title", "Content", "Record Date"}; + createHeaderRow(sheet, headers); + + int rowNum = 1; + for (GrowthRecord g : records) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(g.getId()); + row.createCell(1).setCellValue(g.getStudentId()); + row.createCell(2).setCellValue(g.getType() != null ? g.getType() : ""); + row.createCell(3).setCellValue(g.getTitle() != null ? g.getTitle() : ""); + row.createCell(4).setCellValue(g.getContent() != null ? g.getContent() : ""); + row.createCell(5).setCellValue(g.getRecordDate() != null ? g.getRecordDate().toString() : ""); + } + return toBytes(workbook); + } + } + + /** + * 创建表头行 + */ + private void createHeaderRow(Sheet sheet, String[] headers) { + Row headerRow = sheet.createRow(0); + CellStyle style = sheet.getWorkbook().createCellStyle(); + Font font = sheet.getWorkbook().createFont(); + font.setBold(true); + style.setFont(font); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(style); + } + } + + /** + * 将 Workbook 转换为字节数组 + */ + private byte[] toBytes(Workbook workbook) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + workbook.write(out); + return out.toByteArray(); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/FileUploadServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/FileUploadServiceImpl.java new file mode 100644 index 0000000..694fe0b --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/FileUploadServiceImpl.java @@ -0,0 +1,77 @@ +package com.reading.platform.service.impl; + +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.service.FileUploadService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * 文件上传服务实现类 + */ +@Slf4j +@Service +public class FileUploadServiceImpl implements FileUploadService { + + @Value("${file.upload.path:/app/uploads/}") + private String uploadPath; + + @Value("${file.upload.base-url:/uploads/}") + private String baseUrl; + + @Override + public String uploadFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BusinessException(ErrorCode.INVALID_PARAMETER, "File cannot be empty"); + } + + String originalFilename = file.getOriginalFilename(); + String extension = ""; + if (originalFilename != null && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + + String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String newFilename = UUID.randomUUID().toString().replace("-", "") + extension; + String relativePath = datePath + "/" + newFilename; + String fullPath = uploadPath + relativePath; + + try { + Path targetPath = Paths.get(fullPath); + Files.createDirectories(targetPath.getParent()); + file.transferTo(targetPath.toFile()); + log.info("File uploaded: {}", fullPath); + return baseUrl + relativePath; + } catch (IOException e) { + log.error("File upload failed", e); + throw new BusinessException(ErrorCode.INTERNAL_ERROR, "File upload failed: " + e.getMessage()); + } + } + + @Override + public void deleteFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return; + } + String relativePath = filePath.startsWith(baseUrl) ? filePath.substring(baseUrl.length()) : filePath; + String fullPath = uploadPath + relativePath; + File file = new File(fullPath); + if (file.exists()) { + boolean deleted = file.delete(); + if (!deleted) { + log.warn("Failed to delete file: {}", fullPath); + } + } + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/OperationLogServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/OperationLogServiceImpl.java new file mode 100644 index 0000000..50dab8f --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/OperationLogServiceImpl.java @@ -0,0 +1,54 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.security.SecurityUtils; +import com.reading.platform.entity.OperationLog; +import com.reading.platform.mapper.OperationLogMapper; +import com.reading.platform.service.OperationLogService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 操作日志服务实现类 + */ +@Service +@RequiredArgsConstructor +public class OperationLogServiceImpl implements OperationLogService { + + private final OperationLogMapper operationLogMapper; + + @Override + public Page getLogs(int pageNum, int pageSize, Long tenantId, String module) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (tenantId != null) { + wrapper.eq(OperationLog::getTenantId, tenantId); + } + if (module != null && !module.isEmpty()) { + wrapper.eq(OperationLog::getModule, module); + } + wrapper.orderByDesc(OperationLog::getCreatedAt); + return operationLogMapper.selectPage(page, wrapper); + } + + @Override + public void log(String action, String module, String targetType, Long targetId, String details) { + try { + OperationLog log = new OperationLog(); + log.setAction(action); + log.setModule(module); + log.setTargetType(targetType); + log.setTargetId(targetId); + log.setDetails(details); + try { + log.setUserId(SecurityUtils.getCurrentUserId()); + log.setUserRole(SecurityUtils.getCurrentRole()); + log.setTenantId(SecurityUtils.getCurrentTenantId()); + } catch (Exception ignored) {} + operationLogMapper.insert(log); + } catch (Exception e) { + // Log silently - don't fail main operations + } + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ResourceServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ResourceServiceImpl.java new file mode 100644 index 0000000..998d756 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ResourceServiceImpl.java @@ -0,0 +1,106 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.entity.ResourceItem; +import com.reading.platform.entity.ResourceLibrary; +import com.reading.platform.mapper.ResourceItemMapper; +import com.reading.platform.mapper.ResourceLibraryMapper; +import com.reading.platform.service.ResourceService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 资源服务实现类 + */ +@Service +@RequiredArgsConstructor +public class ResourceServiceImpl implements ResourceService { + + private final ResourceLibraryMapper resourceLibraryMapper; + private final ResourceItemMapper resourceItemMapper; + + @Override + public List getLibraries(Long tenantId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (tenantId != null) { + wrapper.eq(ResourceLibrary::getTenantId, tenantId); + } + wrapper.orderByDesc(ResourceLibrary::getCreatedAt); + return resourceLibraryMapper.selectList(wrapper); + } + + @Override + public ResourceLibrary getLibraryById(Long id) { + ResourceLibrary lib = resourceLibraryMapper.selectById(id); + if (lib == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource library not found"); + } + return lib; + } + + @Override + public ResourceLibrary createLibrary(ResourceLibrary library) { + resourceLibraryMapper.insert(library); + return library; + } + + @Override + public ResourceLibrary updateLibrary(Long id, ResourceLibrary library) { + getLibraryById(id); + library.setId(id); + resourceLibraryMapper.updateById(library); + return resourceLibraryMapper.selectById(id); + } + + @Override + public void deleteLibrary(Long id) { + resourceLibraryMapper.deleteById(id); + } + + @Override + public Page getItems(int pageNum, int pageSize, Long libraryId, String keyword) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (libraryId != null) { + wrapper.eq(ResourceItem::getLibraryId, libraryId); + } + if (keyword != null && !keyword.isEmpty()) { + wrapper.like(ResourceItem::getName, keyword); + } + wrapper.orderByDesc(ResourceItem::getCreatedAt); + return resourceItemMapper.selectPage(page, wrapper); + } + + @Override + public ResourceItem getItemById(Long id) { + ResourceItem item = resourceItemMapper.selectById(id); + if (item == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource item not found"); + } + return item; + } + + @Override + public ResourceItem createItem(ResourceItem item) { + resourceItemMapper.insert(item); + return item; + } + + @Override + public ResourceItem updateItem(Long id, ResourceItem item) { + getItemById(id); + item.setId(id); + resourceItemMapper.updateById(item); + return resourceItemMapper.selectById(id); + } + + @Override + public void deleteItem(Long id) { + resourceItemMapper.deleteById(id); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleServiceImpl.java new file mode 100644 index 0000000..26134a5 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleServiceImpl.java @@ -0,0 +1,239 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.dto.request.SchedulePlanCreateRequest; +import com.reading.platform.dto.request.ScheduleTemplateApplyRequest; +import com.reading.platform.entity.SchedulePlan; +import com.reading.platform.entity.ScheduleTemplate; +import com.reading.platform.mapper.SchedulePlanMapper; +import com.reading.platform.mapper.ScheduleTemplateMapper; +import com.reading.platform.service.ScheduleService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.WeekFields; +import java.util.*; + +/** + * 课表服务实现类 + */ +@Service +@RequiredArgsConstructor +public class ScheduleServiceImpl implements ScheduleService { + + private final SchedulePlanMapper schedulePlanMapper; + private final ScheduleTemplateMapper scheduleTemplateMapper; + + @Override + public Page getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getTenantId, tenantId); + if (classId != null) { + wrapper.eq(SchedulePlan::getClassId, classId); + } + wrapper.orderByDesc(SchedulePlan::getCreatedAt); + return schedulePlanMapper.selectPage(page, wrapper); + } + + @Override + public Page getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getTenantId, tenantId); + if (classId != null) { + wrapper.eq(SchedulePlan::getClassId, classId); + } + if (startDate != null) { + wrapper.ge(SchedulePlan::getStartDate, startDate); + } + if (endDate != null) { + wrapper.le(SchedulePlan::getEndDate, endDate); + } + wrapper.orderByDesc(SchedulePlan::getCreatedAt); + return schedulePlanMapper.selectPage(page, wrapper); + } + + @Override + public SchedulePlan getSchedulePlanById(Long id) { + SchedulePlan plan = schedulePlanMapper.selectById(id); + if (plan == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Schedule plan not found"); + } + return plan; + } + + @Override + public SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan) { + plan.setTenantId(tenantId); + schedulePlanMapper.insert(plan); + return plan; + } + + @Override + public SchedulePlan createSchedulePlan(Long tenantId, Long userId, SchedulePlanCreateRequest request) { + SchedulePlan plan = new SchedulePlan(); + plan.setTenantId(tenantId); + plan.setClassId(request.getClassId()); + plan.setCourseId(request.getCourseId()); + plan.setTeacherId(request.getTeacherId()); + plan.setDayOfWeek(request.getDayOfWeek()); + plan.setPeriod(request.getPeriod()); + plan.setStartTime(request.getStartTime()); + plan.setEndTime(request.getEndTime()); + plan.setLocation(request.getLocation()); + plan.setNote(request.getNote()); + plan.setStartDate(request.getStartDate()); + plan.setEndDate(request.getEndDate()); + plan.setStatus("active"); + + schedulePlanMapper.insert(plan); + return plan; + } + + @Override + public SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan) { + SchedulePlan existing = getSchedulePlanById(id); + plan.setId(id); + plan.setTenantId(existing.getTenantId()); + schedulePlanMapper.updateById(plan); + return schedulePlanMapper.selectById(id); + } + + @Override + public void deleteSchedulePlan(Long id) { + schedulePlanMapper.deleteById(id); + } + + @Override + @Transactional + public List batchCreateSchedules(Long tenantId, Long userId, List requests) { + List plans = new ArrayList<>(); + for (SchedulePlanCreateRequest request : requests) { + SchedulePlan plan = createSchedulePlan(tenantId, userId, request); + plans.add(plan); + } + return plans; + } + + @Override + public List> getTimetable(Long tenantId, LocalDate startDate, LocalDate endDate, Long classId) { + if (startDate == null) { + startDate = LocalDate.now(); + } + if (endDate == null) { + endDate = startDate.plusMonths(1); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getTenantId, tenantId); + if (classId != null) { + wrapper.eq(SchedulePlan::getClassId, classId); + } + wrapper.le(SchedulePlan::getStartDate, endDate) + .ge(SchedulePlan::getEndDate, startDate); + + List plans = schedulePlanMapper.selectList(wrapper); + + // 按日期分组 + Map>> dailySchedules = new LinkedHashMap<>(); + + for (SchedulePlan plan : plans) { + LocalDate date = plan.getStartDate(); + while (!date.isAfter(plan.getEndDate()) && !date.isAfter(endDate)) { + String dateKey = date.toString(); + + if (!dailySchedules.containsKey(dateKey)) { + dailySchedules.put(dateKey, new ArrayList<>()); + } + + Map schedule = new HashMap<>(); + schedule.put("id", plan.getId()); + schedule.put("date", dateKey); + schedule.put("dayOfWeek", date.getDayOfWeek().getValue()); + schedule.put("classId", plan.getClassId()); + schedule.put("courseId", plan.getCourseId()); + schedule.put("teacherId", plan.getTeacherId()); + schedule.put("period", plan.getPeriod()); + schedule.put("startTime", plan.getStartTime()); + schedule.put("endTime", plan.getEndTime()); + schedule.put("location", plan.getLocation()); + + dailySchedules.get(dateKey).add(schedule); + + date = date.plusDays(1); + } + } + + // 转换为前端需要的格式 + List> result = new ArrayList<>(); + for (Map.Entry>> entry : dailySchedules.entrySet()) { + Map dayData = new HashMap<>(); + dayData.put("date", entry.getKey()); + dayData.put("schedules", entry.getValue()); + result.add(dayData); + } + + return result; + } + + @Override + public Page getScheduleTemplates(int pageNum, int pageSize, Long tenantId) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ScheduleTemplate::getTenantId, tenantId) + .orderByDesc(ScheduleTemplate::getCreatedAt); + return scheduleTemplateMapper.selectPage(page, wrapper); + } + + @Override + public ScheduleTemplate getScheduleTemplateById(Long id) { + ScheduleTemplate template = scheduleTemplateMapper.selectById(id); + if (template == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Schedule template not found"); + } + return template; + } + + @Override + public ScheduleTemplate createScheduleTemplate(Long tenantId, ScheduleTemplate template) { + template.setTenantId(tenantId); + scheduleTemplateMapper.insert(template); + return template; + } + + @Override + public ScheduleTemplate updateScheduleTemplate(Long id, ScheduleTemplate template) { + ScheduleTemplate existing = getScheduleTemplateById(id); + template.setId(id); + template.setTenantId(existing.getTenantId()); + scheduleTemplateMapper.updateById(template); + return scheduleTemplateMapper.selectById(id); + } + + @Override + public void deleteScheduleTemplate(Long id) { + scheduleTemplateMapper.deleteById(id); + } + + @Override + @Transactional + public List applyScheduleTemplate(Long tenantId, Long templateId, ScheduleTemplateApplyRequest request) { + ScheduleTemplate template = getScheduleTemplateById(templateId); + + // 解析模板内容(JSON 格式) + // 模板内容格式示例:[{"dayOfWeek": 1, "period": 1, "courseId": 1, "teacherId": 1, ...}] + List plans = new ArrayList<>(); + + // TODO: 解析模板内容并创建课表计划 + // 这里需要根据实际的模板内容格式进行解析 + + return plans; + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolCourseServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolCourseServiceImpl.java new file mode 100644 index 0000000..5852872 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolCourseServiceImpl.java @@ -0,0 +1,64 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.entity.SchoolCourse; +import com.reading.platform.mapper.SchoolCourseMapper; +import com.reading.platform.service.SchoolCourseService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 校本课程服务实现类 + */ +@Service +@RequiredArgsConstructor +public class SchoolCourseServiceImpl implements SchoolCourseService { + + private final SchoolCourseMapper schoolCourseMapper; + + @Override + public Page getCourses(int pageNum, int pageSize, Long tenantId, String keyword) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchoolCourse::getTenantId, tenantId); + if (keyword != null && !keyword.isEmpty()) { + wrapper.like(SchoolCourse::getName, keyword); + } + wrapper.orderByDesc(SchoolCourse::getCreatedAt); + return schoolCourseMapper.selectPage(page, wrapper); + } + + @Override + public SchoolCourse getCourseById(Long id) { + SchoolCourse course = schoolCourseMapper.selectById(id); + if (course == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "School course not found"); + } + return course; + } + + @Override + public SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course) { + course.setTenantId(tenantId); + course.setCreatedBy(userId); + schoolCourseMapper.insert(course); + return course; + } + + @Override + public SchoolCourse updateCourse(Long id, SchoolCourse course) { + SchoolCourse existing = getCourseById(id); + course.setId(id); + course.setTenantId(existing.getTenantId()); + schoolCourseMapper.updateById(course); + return schoolCourseMapper.selectById(id); + } + + @Override + public void deleteCourse(Long id) { + schoolCourseMapper.deleteById(id); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolStatsServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolStatsServiceImpl.java new file mode 100644 index 0000000..5eb7bfe --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolStatsServiceImpl.java @@ -0,0 +1,276 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.entity.*; +import com.reading.platform.mapper.*; +import com.reading.platform.service.SchoolStatsService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 学校统计服务实现类 + */ +@Service +@RequiredArgsConstructor +public class SchoolStatsServiceImpl implements SchoolStatsService { + + private final TeacherMapper teacherMapper; + private final StudentMapper studentMapper; + private final ClazzMapper clazzMapper; + private final LessonMapper lessonMapper; + private final CourseMapper courseMapper; + + @Override + public Map getStats(Long tenantId) { + Map stats = new HashMap<>(); + stats.put("teacherCount", teacherMapper.selectCount( + new LambdaQueryWrapper().eq(Teacher::getTenantId, tenantId))); + stats.put("studentCount", studentMapper.selectCount( + new LambdaQueryWrapper().eq(Student::getTenantId, tenantId))); + stats.put("classCount", clazzMapper.selectCount( + new LambdaQueryWrapper().eq(Clazz::getTenantId, tenantId))); + stats.put("lessonCount", lessonMapper.selectCount( + new LambdaQueryWrapper().eq(Lesson::getTenantId, tenantId))); + return stats; + } + + @Override + public List> getActiveTeachers(Long tenantId, Integer limit) { + if (limit == null) { + limit = 5; + } + + // 获取所有活跃教师 + List teachers = teacherMapper.selectList( + new LambdaQueryWrapper() + .eq(Teacher::getTenantId, tenantId) + .eq(Teacher::getStatus, "ACTIVE") + ); + + // 统计每位教师的授课次数 + List> result = new ArrayList<>(); + + for (Teacher teacher : teachers) { + long lessonCount = lessonMapper.selectCount( + new LambdaQueryWrapper() + .eq(Lesson::getTenantId, tenantId) + .eq(Lesson::getTeacherId, teacher.getId()) + .eq(Lesson::getStatus, "COMPLETED") + ); + + if (lessonCount > 0) { + Map item = new HashMap<>(); + item.put("id", teacher.getId()); + item.put("name", teacher.getName()); + item.put("lessonCount", lessonCount); + result.add(item); + } + } + + // 按授课次数排序 + result.sort((a, b) -> ((Integer) b.get("lessonCount")) - ((Integer) a.get("lessonCount"))); + + // 返回前 N 名 + if (result.size() > limit) { + result = result.subList(0, limit); + } + + return result; + } + + @Override + public List> getCourseUsageStats(Long tenantId) { + // 获取所有已完成的课时 + List lessons = lessonMapper.selectList( + new LambdaQueryWrapper() + .eq(Lesson::getTenantId, tenantId) + .eq(Lesson::getStatus, "COMPLETED") + ); + + // 统计每个课程的使用次数 + Map> courseUsageMap = new LinkedHashMap<>(); + + for (Lesson lesson : lessons) { + Long courseId = lesson.getCourseId(); + if (courseId == null) continue; + + if (!courseUsageMap.containsKey(courseId)) { + Course course = courseMapper.selectById(courseId); + Map courseData = new HashMap<>(); + courseData.put("courseId", courseId); + courseData.put("courseName", course != null ? course.getName() : "未知课程"); + courseData.put("usageCount", 0); + courseUsageMap.put(courseId, courseData); + } + + Map courseData = courseUsageMap.get(courseId); + courseData.put("usageCount", (Integer) courseData.get("usageCount") + 1); + } + + // 转换为列表并按使用次数排序 + List> result = new ArrayList<>(courseUsageMap.values()); + result.sort((a, b) -> (Integer) b.get("usageCount") - (Integer) a.get("usageCount")); + + // 最多返回前 10 个 + if (result.size() > 10) { + result = result.subList(0, 10); + } + + return result; + } + + @Override + public List> getRecentActivities(Long tenantId, Integer limit) { + if (limit == null) { + limit = 10; + } + + List lessons = lessonMapper.selectList( + new LambdaQueryWrapper() + .eq(Lesson::getTenantId, tenantId) + .orderByDesc(Lesson::getCreatedAt) + .last("LIMIT " + limit) + ); + + List> activities = new ArrayList<>(); + + for (Lesson lesson : lessons) { + Map activity = new HashMap<>(); + activity.put("id", lesson.getId()); + activity.put("type", lesson.getStatus()); + activity.put("title", formatActivityTitle(lesson)); + activity.put("time", lesson.getCreatedAt()); + activities.add(activity); + } + + return activities; + } + + @Override + public List> getLessonTrend(Long tenantId, Integer months) { + if (months == null) { + months = 6; + } + + List> result = new ArrayList<>(); + LocalDate now = LocalDate.now(); + + // 获取当前学生总数 + long studentCount = studentMapper.selectCount( + new LambdaQueryWrapper().eq(Student::getTenantId, tenantId) + ); + + for (int i = months - 1; i >= 0; i--) { + LocalDate startDate = now.minusMonths(i).withDayOfMonth(1); + LocalDate endDate = now.minusMonths(i).withDayOfMonth(startDate.lengthOfMonth()); + + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(23, 59, 59); + + // 获取该月完成的课时数 + long lessonCount = lessonMapper.selectCount( + new LambdaQueryWrapper() + .eq(Lesson::getTenantId, tenantId) + .eq(Lesson::getStatus, "COMPLETED") + .ge(Lesson::getCreatedAt, startDateTime) + .le(Lesson::getCreatedAt, endDateTime) + ); + + Map item = new HashMap<>(); + item.put("month", String.format("%04d-%02d", startDate.getYear(), startDate.getMonthValue())); + item.put("lessonCount", lessonCount); + item.put("studentCount", studentCount); + result.add(item); + } + + return result; + } + + @Override + public List> getCourseDistribution(Long tenantId) { + // 获取所有已完成的课时 + List lessons = lessonMapper.selectList( + new LambdaQueryWrapper() + .eq(Lesson::getTenantId, tenantId) + .eq(Lesson::getStatus, "COMPLETED") + ); + + // 统计每个课程的完成次数 + Map courseMap = new LinkedHashMap<>(); + + for (Lesson lesson : lessons) { + Long courseId = lesson.getCourseId(); + if (courseId == null) continue; + + Course course = courseMapper.selectById(courseId); + String courseName = course != null ? course.getName() : "未知课程"; + + courseMap.put(courseName, courseMap.getOrDefault(courseName, 0) + 1); + } + + // 转换为列表并按次数排序 + List> result = new ArrayList<>(); + for (Map.Entry entry : courseMap.entrySet()) { + Map item = new HashMap<>(); + item.put("name", entry.getKey()); + item.put("value", entry.getValue()); + result.add(item); + } + + result.sort((a, b) -> (Integer) b.get("value") - (Integer) a.get("value")); + + // 最多返回前 8 个 + if (result.size() > 8) { + result = result.subList(0, 8); + } + + return result; + } + + /** + * 格式化活动标题 + */ + private String formatActivityTitle(Lesson lesson) { + String teacherName = "未知教师"; + String courseName = "未知课程"; + String className = "未知班级"; + + if (lesson.getTeacherId() != null) { + Teacher teacher = teacherMapper.selectById(lesson.getTeacherId()); + if (teacher != null) { + teacherName = teacher.getName(); + } + } + + if (lesson.getCourseId() != null) { + Course course = courseMapper.selectById(lesson.getCourseId()); + if (course != null) { + courseName = course.getName(); + } + } + + if (lesson.getClassId() != null) { + Clazz clazz = clazzMapper.selectById(lesson.getClassId()); + if (clazz != null) { + className = clazz.getName(); + } + } + + String status = lesson.getStatus(); + if ("COMPLETED".equals(status)) { + return teacherName + "完成《" + courseName + "》授课"; + } else if ("IN_PROGRESS".equals(status)) { + return teacherName + "正在进行《" + courseName + "》授课"; + } else if ("PLANNED".equals(status)) { + return teacherName + "计划在" + className + "讲授《" + courseName + "》"; + } else if ("CANCELLED".equals(status)) { + return teacherName + "取消了《" + courseName + "》授课"; + } else { + return teacherName + "操作了《" + courseName + "》"; + } + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java index 177885e..01ddede 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java @@ -187,4 +187,15 @@ public class StudentServiceImpl implements StudentService { ); } + @Override + @Transactional + public List importStudents(Long tenantId, List requests) { + List students = new ArrayList<>(); + for (StudentCreateRequest request : requests) { + Student student = createStudent(tenantId, request); + students.add(student); + } + return students; + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/SystemSettingServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/SystemSettingServiceImpl.java new file mode 100644 index 0000000..9ef2421 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/SystemSettingServiceImpl.java @@ -0,0 +1,68 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.reading.platform.entity.SystemSetting; +import com.reading.platform.mapper.SystemSettingMapper; +import com.reading.platform.service.SystemSettingService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 系统设置服务实现类 + */ +@Service +@RequiredArgsConstructor +public class SystemSettingServiceImpl implements SystemSettingService { + + private final SystemSettingMapper systemSettingMapper; + + @Override + public Map getSettings(Long tenantId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SystemSetting::getTenantId, tenantId); + List settings = systemSettingMapper.selectList(wrapper); + Map result = new HashMap<>(); + for (SystemSetting s : settings) { + result.put(s.getSettingKey(), s.getSettingValue()); + } + return result; + } + + @Override + public void updateSettings(Long tenantId, Map settings) { + for (Map.Entry entry : settings.entrySet()) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SystemSetting::getTenantId, tenantId) + .eq(SystemSetting::getSettingKey, entry.getKey()); + SystemSetting existing = systemSettingMapper.selectOne(wrapper); + if (existing != null) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(SystemSetting::getTenantId, tenantId) + .eq(SystemSetting::getSettingKey, entry.getKey()); + SystemSetting update = new SystemSetting(); + update.setSettingValue(entry.getValue()); + systemSettingMapper.update(update, updateWrapper); + } else { + SystemSetting newSetting = new SystemSetting(); + newSetting.setTenantId(tenantId); + newSetting.setSettingKey(entry.getKey()); + newSetting.setSettingValue(entry.getValue()); + systemSettingMapper.insert(newSetting); + } + } + } + + @Override + public String getSetting(Long tenantId, String key) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SystemSetting::getTenantId, tenantId) + .eq(SystemSetting::getSettingKey, key); + SystemSetting setting = systemSettingMapper.selectOne(wrapper); + return setting != null ? setting.getSettingValue() : null; + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java index 6b424cf..69d88d3 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java @@ -5,23 +5,18 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.common.util.PageUtils; -import com.reading.platform.dto.request.TaskCreateRequest; -import com.reading.platform.dto.request.TaskUpdateRequest; -import com.reading.platform.entity.Task; -import com.reading.platform.entity.TaskCompletion; -import com.reading.platform.entity.TaskTarget; -import com.reading.platform.mapper.TaskCompletionMapper; -import com.reading.platform.mapper.TaskMapper; -import com.reading.platform.mapper.TaskTargetMapper; +import com.reading.platform.dto.request.*; +import com.reading.platform.entity.*; +import com.reading.platform.mapper.*; import com.reading.platform.service.TaskService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; +import java.util.*; @Service @RequiredArgsConstructor @@ -30,6 +25,9 @@ public class TaskServiceImpl implements TaskService { private final TaskMapper taskMapper; private final TaskTargetMapper taskTargetMapper; private final TaskCompletionMapper taskCompletionMapper; + private final TaskTemplateMapper taskTemplateMapper; + private final ClazzMapper classMapper; + private final StudentClassHistoryMapper studentClassHistoryMapper; @Override @Transactional @@ -209,4 +207,396 @@ public class TaskServiceImpl implements TaskService { ); } + // ==================== 任务统计 ==================== + + @Override + public Map getTaskStats(Long tenantId) { + // 统计任务总数 + long totalTasks = taskMapper.selectCount(new LambdaQueryWrapper().eq(Task::getTenantId, tenantId)); + + // 统计已发布的任务数 + long publishedTasks = taskMapper.selectCount(new LambdaQueryWrapper() + .eq(Task::getTenantId, tenantId).eq(Task::getStatus, "published")); + + // 统计完成情况 + long completedTasks = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskCompletion::getStatus, "completed")); + + long inProgressTasks = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskCompletion::getStatus, "in_progress")); + + long pendingCount = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskCompletion::getStatus, "pending")); + + long totalCompletions = taskCompletionMapper.selectCount(new LambdaQueryWrapper()); + + int completionRate = totalCompletions > 0 ? (int) ((completedTasks * 100) / totalCompletions) : 0; + + Map result = new HashMap<>(); + result.put("totalTasks", totalTasks); + result.put("publishedTasks", publishedTasks); + result.put("completedTasks", completedTasks); + result.put("inProgressTasks", inProgressTasks); + result.put("pendingCount", pendingCount); + result.put("totalCompletions", totalCompletions); + result.put("completionRate", completionRate); + + return result; + } + + @Override + public Map getStatsByType(Long tenantId) { + List tasks = taskMapper.selectList(new LambdaQueryWrapper().eq(Task::getTenantId, tenantId)); + + Map> typeStats = new HashMap<>(); + + for (Task task : tasks) { + String type = task.getType() != null ? task.getType() : "homework"; + + if (!typeStats.containsKey(type)) { + typeStats.put(type, new HashMap<>()); + typeStats.get(type).put("total", 0L); + typeStats.get(type).put("completed", 0L); + } + + // 统计该任务的完成情况 + long completions = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskCompletion::getTaskId, task.getId())); + long completed = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskCompletion::getTaskId, task.getId()).eq(TaskCompletion::getStatus, "completed")); + + typeStats.get(type).put("total", (Long) typeStats.get(type).get("total") + completions); + typeStats.get(type).put("completed", (Long) typeStats.get(type).get("completed") + completed); + } + + // 计算完成率 + for (Map stat : typeStats.values()) { + long total = (Long) stat.get("total"); + long completed = (Long) stat.get("completed"); + stat.put("rate", total > 0 ? (int) ((completed * 100) / total) : 0); + } + + // 转换类型:Map> -> Map + return new HashMap<>(typeStats); + } + + @Override + public List> getStatsByClass(Long tenantId) { + List classes = classMapper.selectList(new LambdaQueryWrapper().eq(Clazz::getTenantId, tenantId)); + + List> classStats = new ArrayList<>(); + + for (Clazz cls : classes) { + // 获取班级学生的任务完成记录(通过 student_class_history 关联) + List histories = studentClassHistoryMapper.selectList( + new LambdaQueryWrapper() + .eq(StudentClassHistory::getClassId, cls.getId()) + .eq(StudentClassHistory::getStatus, "active") + ); + + if (histories.isEmpty()) { + continue; + } + + List studentIds = histories.stream() + .map(StudentClassHistory::getStudentId) + .toList(); + + if (studentIds.isEmpty()) { + continue; + } + + long completions = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .in(TaskCompletion::getStudentId, studentIds)); + long completed = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .in(TaskCompletion::getStudentId, studentIds).eq(TaskCompletion::getStatus, "completed")); + + int rate = completions > 0 ? (int) ((completed * 100) / completions) : 0; + + Map stat = new HashMap<>(); + stat.put("classId", cls.getId()); + stat.put("className", cls.getName()); + stat.put("grade", cls.getGrade()); + stat.put("total", completions); + stat.put("completed", completed); + stat.put("rate", rate); + + if (completions > 0) { + classStats.add(stat); + } + } + + return classStats; + } + + @Override + public List> getMonthlyStats(Long tenantId, Integer months) { + months = months != null ? months : 6; + LocalDate now = LocalDate.now(); + LocalDate startDate = now.minusMonths(months - 1).withDayOfMonth(1); + + List> monthlyData = new ArrayList<>(); + + // 初始化月份数据 + for (int i = 0; i < months; i++) { + LocalDate date = startDate.plusMonths(i); + String key = String.format("%04d-%02d", date.getYear(), date.getMonthValue()); + + Map data = new HashMap<>(); + data.put("month", key); + data.put("tasks", 0); + data.put("completions", 0); + data.put("completed", 0); + monthlyData.add(data); + } + + // 统计每月的任务数 + List tasks = taskMapper.selectList(new LambdaQueryWrapper() + .eq(Task::getTenantId, tenantId) + .ge(Task::getCreatedAt, startDate.atStartOfDay())); + + for (Task task : tasks) { + String month = String.format("%04d-%02d", task.getCreatedAt().getYear(), task.getCreatedAt().getMonthValue()); + for (Map data : monthlyData) { + if (data.get("month").equals(month)) { + data.put("tasks", (Integer) data.get("tasks") + 1); + break; + } + } + } + + // 统计每月的完成情况 + for (Map data : monthlyData) { + String month = (String) data.get("month"); + int year = Integer.parseInt(month.substring(0, 4)); + int monthNum = Integer.parseInt(month.substring(5, 7)); + + LocalDateTime startOfMonth = LocalDateTime.of(year, monthNum, 1, 0, 0); + LocalDateTime endOfMonth = startOfMonth.plusMonths(1); + + long completions = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .ge(TaskCompletion::getCreatedAt, startOfMonth) + .lt(TaskCompletion::getCreatedAt, endOfMonth)); + + long completed = taskCompletionMapper.selectCount(new LambdaQueryWrapper() + .ge(TaskCompletion::getCreatedAt, startOfMonth) + .lt(TaskCompletion::getCreatedAt, endOfMonth) + .eq(TaskCompletion::getStatus, "completed")); + + data.put("completions", (int) completions); + data.put("completed", (int) completed); + } + + return monthlyData; + } + + @Override + public Page getTaskCompletions(Long tenantId, Long taskId, Integer pageNum, Integer pageSize, String status) { + Page page = PageUtils.of(pageNum, pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TaskCompletion::getTaskId, taskId); + + // 通过 taskId 获取 task,验证 tenantId + Task task = taskMapper.selectById(taskId); + if (task == null || !task.getTenantId().equals(tenantId)) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "任务不存在或无权限访问"); + } + + if (StringUtils.hasText(status)) { + wrapper.eq(TaskCompletion::getStatus, status); + } + + return taskCompletionMapper.selectPage(page, wrapper); + } + + @Override + @Transactional + public TaskCompletion updateTaskCompletion(Long tenantId, Long taskId, Long studentId, String status, String feedback) { + // 验证任务属于该租户 + Task task = taskMapper.selectById(taskId); + if (task == null || !task.getTenantId().equals(tenantId)) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "任务不存在或无权限访问"); + } + + TaskCompletion completion = taskCompletionMapper.selectOne( + new LambdaQueryWrapper() + .eq(TaskCompletion::getTaskId, taskId) + .eq(TaskCompletion::getStudentId, studentId) + ); + + if (completion == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "任务完成记录不存在"); + } + + completion.setStatus(status); + if ("completed".equals(status)) { + completion.setCompletedAt(LocalDateTime.now()); + } + if (feedback != null) { + completion.setFeedback(feedback); + } + + taskCompletionMapper.updateById(completion); + return completion; + } + + // ==================== 任务模板 ==================== + + @Override + public Page getTemplatePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String type) { + Page page = PageUtils.of(pageNum, pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TaskTemplate::getTenantId, tenantId); + + if (StringUtils.hasText(keyword)) { + wrapper.and(w -> w.like(TaskTemplate::getName, keyword) + .or().like(TaskTemplate::getDescription, keyword)); + } + + if (StringUtils.hasText(type)) { + wrapper.eq(TaskTemplate::getType, type); + } + + wrapper.orderByDesc(TaskTemplate::getCreatedAt); + + return taskTemplateMapper.selectPage(page, wrapper); + } + + @Override + public TaskTemplate getTemplateById(Long tenantId, Long id) { + TaskTemplate template = taskTemplateMapper.selectById(id); + if (template == null || !template.getTenantId().equals(tenantId)) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "模板不存在或无权限访问"); + } + return template; + } + + @Override + public TaskTemplate getDefaultTemplate(Long tenantId, String taskType) { + // 优先查找公共模板,再查找租户的默认模板 + TaskTemplate template = taskTemplateMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskTemplate::getType, taskType) + .eq(TaskTemplate::getIsPublic, 1) + .last("LIMIT 1")); + + if (template == null) { + template = taskTemplateMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskTemplate::getTenantId, tenantId) + .eq(TaskTemplate::getType, taskType) + .last("LIMIT 1")); + } + + return template; + } + + @Override + @Transactional + public TaskTemplate createTemplate(Long tenantId, Long creatorId, TaskTemplateCreateRequest request) { + TaskTemplate template = new TaskTemplate(); + template.setTenantId(tenantId); + template.setName(request.getName()); + template.setDescription(request.getDescription()); + template.setType(request.getType() != null ? request.getType() : "homework"); + template.setContent(request.getContent()); + template.setIsPublic(request.getIsPublic() != null ? request.getIsPublic() : 0); + + taskTemplateMapper.insert(template); + return template; + } + + @Override + @Transactional + public TaskTemplate updateTemplate(Long tenantId, Long id, TaskTemplateUpdateRequest request) { + TaskTemplate template = getTemplateById(tenantId, id); + + if (StringUtils.hasText(request.getName())) { + template.setName(request.getName()); + } + if (request.getDescription() != null) { + template.setDescription(request.getDescription()); + } + if (request.getType() != null) { + template.setType(request.getType()); + } + if (request.getContent() != null) { + template.setContent(request.getContent()); + } + if (request.getIsPublic() != null) { + template.setIsPublic(request.getIsPublic()); + } + + taskTemplateMapper.updateById(template); + return template; + } + + @Override + @Transactional + public void deleteTemplate(Long tenantId, Long id) { + getTemplateById(tenantId, id); + taskTemplateMapper.deleteById(id); + } + + @Override + @Transactional + public Task createTaskFromTemplate(Long tenantId, Long creatorId, String creatorRole, CreateTaskFromTemplateRequest request) { + // 获取模板 + TaskTemplate template = getTemplateById(tenantId, request.getTemplateId()); + + // 创建任务 + Task task = new Task(); + task.setTenantId(tenantId); + task.setTitle(template.getName()); + task.setDescription(template.getDescription()); + task.setType(template.getType()); + task.setCreatorId(creatorId); + task.setCreatorRole(creatorRole); + task.setStartDate(request.getStartDate()); + task.setDueDate(request.getEndDate()); + task.setStatus("published"); + + taskMapper.insert(task); + + // 创建任务目标和完成记录 + for (Long targetId : request.getTargetIds()) { + TaskTarget target = new TaskTarget(); + target.setTaskId(task.getId()); + target.setTargetType(request.getTargetType() != null ? request.getTargetType() : "class"); + target.setTargetId(targetId); + taskTargetMapper.insert(target); + + // 如果是班级,为所有学生创建完成记录 + if ("class".equals(request.getTargetType())) { + // 通过 student_class_history 获取班级中的学生 + List histories = studentClassHistoryMapper.selectList( + new LambdaQueryWrapper() + .eq(StudentClassHistory::getClassId, targetId) + .eq(StudentClassHistory::getStatus, "active") + ); + + List studentIds = histories.stream() + .map(StudentClassHistory::getStudentId) + .toList(); + + for (Long studentId : studentIds) { + TaskCompletion completion = new TaskCompletion(); + completion.setTaskId(task.getId()); + completion.setStudentId(studentId); + completion.setStatus("pending"); + taskCompletionMapper.insert(completion); + } + } else { + // 如果是学生,直接创建完成记录 + TaskCompletion completion = new TaskCompletion(); + completion.setTaskId(task.getId()); + completion.setStudentId(targetId); + completion.setStatus("pending"); + taskCompletionMapper.insert(completion); + } + } + + return task; + } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherDashboardServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherDashboardServiceImpl.java new file mode 100644 index 0000000..2167604 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherDashboardServiceImpl.java @@ -0,0 +1,93 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.entity.*; +import com.reading.platform.mapper.*; +import com.reading.platform.service.TeacherDashboardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 教师仪表板服务实现类 + */ +@Service +@RequiredArgsConstructor +public class TeacherDashboardServiceImpl implements TeacherDashboardService { + + private final LessonMapper lessonMapper; + private final TaskMapper taskMapper; + private final GrowthRecordMapper growthRecordMapper; + private final NotificationMapper notificationMapper; + + @Override + public Map getDashboard(Long teacherId, Long tenantId) { + Map dashboard = new HashMap<>(); + dashboard.put("lessonCount", lessonMapper.selectCount( + new LambdaQueryWrapper() + .eq(Lesson::getTeacherId, teacherId) + .eq(Lesson::getTenantId, tenantId))); + dashboard.put("taskCount", taskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Task::getCreatorId, teacherId) + .eq(Task::getTenantId, tenantId))); + dashboard.put("growthRecordCount", growthRecordMapper.selectCount( + new LambdaQueryWrapper() + .eq(GrowthRecord::getRecordedBy, teacherId) + .eq(GrowthRecord::getTenantId, tenantId))); + dashboard.put("unreadNotifications", notificationMapper.selectCount( + new LambdaQueryWrapper() + .eq(Notification::getTenantId, tenantId) + .eq(Notification::getIsRead, 0))); + return dashboard; + } + + @Override + public List> getTodayLessons(Long teacherId, Long tenantId) { + LocalDate today = LocalDate.now(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Lesson::getTeacherId, teacherId) + .eq(Lesson::getTenantId, tenantId) + .eq(Lesson::getLessonDate, today) + .orderByAsc(Lesson::getStartTime); + List lessons = lessonMapper.selectList(wrapper); + return lessons.stream().map(l -> { + Map map = new HashMap<>(); + map.put("id", l.getId()); + map.put("title", l.getTitle()); + map.put("startTime", l.getStartTime()); + map.put("endTime", l.getEndTime()); + map.put("location", l.getLocation()); + map.put("status", l.getStatus()); + return map; + }).collect(Collectors.toList()); + } + + @Override + public List> getWeeklyLessons(Long teacherId, Long tenantId) { + LocalDate today = LocalDate.now(); + LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1); + LocalDate weekEnd = weekStart.plusDays(6); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Lesson::getTeacherId, teacherId) + .eq(Lesson::getTenantId, tenantId) + .between(Lesson::getLessonDate, weekStart, weekEnd) + .orderByAsc(Lesson::getLessonDate, Lesson::getStartTime); + List lessons = lessonMapper.selectList(wrapper); + return lessons.stream().map(l -> { + Map map = new HashMap<>(); + map.put("id", l.getId()); + map.put("title", l.getTitle()); + map.put("lessonDate", l.getLessonDate()); + map.put("startTime", l.getStartTime()); + map.put("endTime", l.getEndTime()); + map.put("status", l.getStatus()); + return map; + }).collect(Collectors.toList()); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ThemeServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ThemeServiceImpl.java new file mode 100644 index 0000000..0cadefd --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ThemeServiceImpl.java @@ -0,0 +1,60 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.exception.BusinessException; +import com.reading.platform.entity.Theme; +import com.reading.platform.mapper.ThemeMapper; +import com.reading.platform.service.ThemeService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 主题服务实现类 + */ +@Service +@RequiredArgsConstructor +public class ThemeServiceImpl implements ThemeService { + + private final ThemeMapper themeMapper; + + @Override + public List getAllThemes(Boolean enabledOnly) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (Boolean.TRUE.equals(enabledOnly)) { + wrapper.eq(Theme::getIsEnabled, 1); + } + wrapper.orderByAsc(Theme::getSortOrder); + return themeMapper.selectList(wrapper); + } + + @Override + public Theme getThemeById(Long id) { + Theme theme = themeMapper.selectById(id); + if (theme == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Theme not found"); + } + return theme; + } + + @Override + public Theme createTheme(Theme theme) { + themeMapper.insert(theme); + return theme; + } + + @Override + public Theme updateTheme(Long id, Theme theme) { + getThemeById(id); + theme.setId(id); + themeMapper.updateById(theme); + return themeMapper.selectById(id); + } + + @Override + public void deleteTheme(Long id) { + themeMapper.deleteById(id); + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TokenServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..5930320 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TokenServiceImpl.java @@ -0,0 +1,87 @@ +package com.reading.platform.service.impl; + +import com.reading.platform.common.security.JwtPayload; +import com.reading.platform.service.TokenService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * Token Service 实现类 - 使用 Redis 存储 Token + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenServiceImpl implements TokenService { + + private final StringRedisTemplate redisTemplate; + + private static final String TOKEN_PREFIX = "token:"; + + @Value("${jwt.expiration}") + private Long tokenExpireTime; + + @Override + public void saveToken(String token, JwtPayload payload) { + String key = TOKEN_PREFIX + token; + String value = payloadToString(payload); + redisTemplate.opsForValue().set(key, value, tokenExpireTime, TimeUnit.MILLISECONDS); + log.debug("Token saved to Redis: {}", key); + } + + @Override + public JwtPayload getToken(String token) { + String key = TOKEN_PREFIX + token; + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + return null; + } + return stringToPayload(value); + } + + @Override + public void removeToken(String token) { + String key = TOKEN_PREFIX + token; + redisTemplate.delete(key); + log.debug("Token removed from Redis: {}", key); + } + + @Override + public boolean isTokenExist(String token) { + String key = TOKEN_PREFIX + token; + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + /** + * 将 JwtPayload 转换为字符串存储 + */ + private String payloadToString(JwtPayload payload) { + return String.format("%d|%s|%s|%s|%s", + payload.getUserId(), + payload.getUsername(), + payload.getRole(), + payload.getTenantId() != null ? payload.getTenantId().toString() : "", + payload.getName() != null ? payload.getName() : ""); + } + + /** + * 从字符串转换为 JwtPayload + */ + private JwtPayload stringToPayload(String value) { + String[] parts = value.split("\\|", -1); + if (parts.length < 5) { + return null; + } + return JwtPayload.builder() + .userId(Long.parseLong(parts[0])) + .username(parts[1]) + .role(parts[2]) + .tenantId(parts[3].isEmpty() ? null : Long.parseLong(parts[3])) + .name(parts[4].isEmpty() ? null : parts[4]) + .build(); + } +} \ No newline at end of file diff --git a/reading-platform-java/src/main/resources/application-dev.yml b/reading-platform-java/src/main/resources/application-dev.yml index 09035df..cdefdf2 100644 --- a/reading-platform-java/src/main/resources/application-dev.yml +++ b/reading-platform-java/src/main/resources/application-dev.yml @@ -17,6 +17,11 @@ spring: enabled: true locations: classpath:db/migration baseline-on-migrate: true + data: + redis: + host: 8.148.151.56 + port: 6379 + database: 0 file: upload: diff --git a/reading-platform-java/src/main/resources/application-prod.yml b/reading-platform-java/src/main/resources/application-prod.yml index e7812c8..cb997f2 100644 --- a/reading-platform-java/src/main/resources/application-prod.yml +++ b/reading-platform-java/src/main/resources/application-prod.yml @@ -24,6 +24,11 @@ spring: enabled: true locations: classpath:db/migration baseline-on-migrate: true + data: + redis: + host: ${REDIS_HOST:redis} + port: ${REDIS_PORT:6379} + database: 0 jwt: secret: ${JWT_SECRET:reading-platform-jwt-secret-key-must-be-at-least-256-bits-long} diff --git a/reading-platform-java/src/main/resources/db/migration/V7__fix_schedule_plans.sql b/reading-platform-java/src/main/resources/db/migration/V7__fix_schedule_plans.sql new file mode 100644 index 0000000..bf29f3f --- /dev/null +++ b/reading-platform-java/src/main/resources/db/migration/V7__fix_schedule_plans.sql @@ -0,0 +1,15 @@ +-- ============================================ +-- 修复 schedule_plans 表,添加缺失字段 +-- ============================================ + +ALTER TABLE schedule_plans +ADD COLUMN course_id BIGINT COMMENT '课程 ID' AFTER class_id, +ADD COLUMN teacher_id BIGINT COMMENT '教师 ID' AFTER course_id, +ADD COLUMN day_of_week TINYINT COMMENT '星期几 (1-7)' AFTER teacher_id, +ADD COLUMN period TINYINT COMMENT '节次' AFTER day_of_week, +ADD COLUMN start_time TIME COMMENT '开始时间' AFTER period, +ADD COLUMN end_time TIME COMMENT '结束时间' AFTER start_time, +ADD COLUMN location VARCHAR(100) COMMENT '教室/地点' AFTER end_time, +ADD COLUMN note VARCHAR(500) COMMENT '备注' AFTER location; + +ALTER TABLE schedule_plans COMMENT='课表计划(增强版)'; diff --git a/前端实际调用接口对比.md b/前端实际调用接口对比.md new file mode 100644 index 0000000..a98dd5b --- /dev/null +++ b/前端实际调用接口对比.md @@ -0,0 +1,346 @@ +# 前端实际调用但 Java 后端缺失的接口 + +## 分析方法 +- 对比 `reading-platform-frontend/src/api/generated/api.ts` 中的实际调用 +- 检查 `reading-platform-java/src/main/java/com/reading/platform/controller` 中的实现 +- 只列出前端实际调用但 Java 后端缺失的接口 + +--- + +## ✅ 已实现的接口列表 + +### 1. 认证接口 (AuthController) +- ✅ `POST /api/v1/auth/login` - 登录 +- ✅ `POST /api/v1/auth/logout` - 登出 +- ✅ `POST /api/v1/auth/change-password` - 修改密码 +- ✅ `GET /api/v1/auth/me` - 获取当前用户信息 + +### 2. 文件接口 (FileUploadController) +- ✅ `POST /api/v1/files/upload` - 上传文件 +- ✅ `DELETE /api/v1/files/delete` - 删除文件 + +### 3. 教师端 - 任务 (TeacherTaskController) +- ✅ `GET /api/v1/teacher/tasks` - 获取任务分页 +- ✅ `POST /api/v1/teacher/tasks` - 创建任务 +- ✅ `GET /api/v1/teacher/tasks/{id}` - 根据 ID 获取任务 +- ✅ `PUT /api/v1/teacher/tasks/{id}` - 更新任务 +- ✅ `DELETE /api/v1/teacher/tasks/{id}` - 删除任务 + +### 4. 教师端 - 课时 (TeacherLessonController) +- ✅ `GET /api/v1/teacher/lessons` - 获取课时分页 +- ✅ `POST /api/v1/teacher/lessons` - 创建课时 +- ✅ `GET /api/v1/teacher/lessons/{id}` - 根据 ID 获取课时 +- ✅ `PUT /api/v1/teacher/lessons/{id}` - 更新课时 +- ✅ `POST /api/v1/teacher/lessons/{id}/start` - 开始课时 +- ✅ `POST /api/v1/teacher/lessons/{id}/complete` - 完成课时 +- ✅ `POST /api/v1/teacher/lessons/{id}/cancel` - 取消课时 +- ✅ `GET /api/v1/teacher/lessons/today` - 获取今天课时 + +### 5. 教师端 - 成长档案 (TeacherGrowthController) +- ✅ `GET /api/v1/teacher/growth-records` - 获取成长档案分页 +- ✅ `POST /api/v1/teacher/growth-records` - 创建成长档案 +- ✅ `GET /api/v1/teacher/growth-records/{id}` - 根据 ID 获取成长档案 +- ✅ `PUT /api/v1/teacher/growth-records/{id}` - 更新成长档案 +- ✅ `DELETE /api/v1/teacher/growth-records/{id}` - 删除成长档案 + +### 6. 教师端 - 通知 (TeacherNotificationController) +- ✅ `GET /api/v1/teacher/notifications` - 获取通知分页 +- ✅ `GET /api/v1/teacher/notifications/{id}` - 根据 ID 获取通知 +- ✅ `GET /api/v1/teacher/notifications/unread-count` - 获取未读数量 +- ✅ `POST /api/v1/teacher/notifications/{id}/read` - 标记已读 +- ✅ `POST /api/v1/teacher/notifications/read-all` - 全部标记已读 + +### 7. 教师端 - 课程 (TeacherCourseController) +- ✅ `GET /api/v1/teacher/courses` - 获取课程分页 +- ✅ `GET /api/v1/teacher/courses/{id}` - 根据 ID 获取课程 +- ✅ `GET /api/v1/teacher/courses/all` - 获取所有课程 + +### 8. 教师端 - 课程课时 (TeacherCourseLessonController) +- ✅ `GET /api/v1/teacher/courses/{courseId}/lessons` - 获取课程课时列表 +- ✅ `GET /api/v1/teacher/courses/{courseId}/lessons/{id}` - 根据 ID 获取课时 + +### 9. 教师端 - 校本课程 (TeacherSchoolCourseController) +- ✅ `GET /api/v1/teacher/school-courses` - 获取校本课程分页 +- ✅ `GET /api/v1/teacher/school-courses/{id}` - 根据 ID 获取校本课程 + +### 10. 教师端 - 课表 (TeacherScheduleController) +- ✅ `GET /api/v1/teacher/schedules` - 获取课表计划分页 +- ✅ `GET /api/v1/teacher/schedules/{id}` - 根据 ID 获取课表计划 + +### 11. 教师端 - 仪表板 (TeacherDashboardController) +- ✅ `GET /api/v1/teacher/dashboard` - 获取仪表板数据 +- ✅ `GET /api/v1/teacher/dashboard/weekly` - 获取周统计 +- ✅ `GET /api/v1/teacher/dashboard/today` - 获取今天数据 + +### 12. 学校管理员 - 教师 (SchoolTeacherController) +- ✅ `GET /api/v1/school/teachers` - 获取教师分页 +- ✅ `POST /api/v1/school/teachers` - 创建教师 +- ✅ `GET /api/v1/school/teachers/{id}` - 根据 ID 获取教师 +- ✅ `PUT /api/v1/school/teachers/{id}` - 更新教师 +- ✅ `DELETE /api/v1/school/teachers/{id}` - 删除教师 +- ✅ `POST /api/v1/school/teachers/{id}/reset-password` - 重置密码 + +### 13. 学校管理员 - 任务 (SchoolTaskController) +- ✅ `GET /api/v1/school/tasks` - 获取任务分页 +- ✅ `POST /api/v1/school/tasks` - 创建任务 +- ✅ `GET /api/v1/school/tasks/{id}` - 根据 ID 获取任务 +- ✅ `PUT /api/v1/school/tasks/{id}` - 更新任务 +- ✅ `DELETE /api/v1/school/tasks/{id}` - 删除任务 + +### 14. 学校管理员 - 学生 (SchoolStudentController) +- ✅ `GET /api/v1/school/students` - 获取学生分页 +- ✅ `POST /api/v1/school/students` - 创建学生 +- ✅ `GET /api/v1/school/students/{id}` - 根据 ID 获取学生 +- ✅ `PUT /api/v1/school/students/{id}` - 更新学生 +- ✅ `DELETE /api/v1/school/students/{id}` - 删除学生 + +### 15. 学校管理员 - 校本课程 (SchoolCourseController) +- ✅ `GET /api/v1/school/school-courses` - 获取校本课程分页 +- ✅ `POST /api/v1/school/school-courses` - 创建校本课程 +- ✅ `GET /api/v1/school/school-courses/{id}` - 根据 ID 获取校本课程 +- ✅ `PUT /api/v1/school/school-courses/{id}` - 更新校本课程 +- ✅ `DELETE /api/v1/school/school-courses/{id}` - 删除校本课程 + +### 16. 学校管理员 - 课表 (SchoolScheduleController) +- ✅ `GET /api/v1/school/schedules` - 获取课表计划分页 +- ✅ `POST /api/v1/school/schedules` - 创建课表计划 +- ✅ `GET /api/v1/school/schedules/{id}` - 根据 ID 获取课表计划 +- ✅ `PUT /api/v1/school/schedules/{id}` - 更新课表计划 +- ✅ `DELETE /api/v1/school/schedules/{id}` - 删除课表计划 +- ✅ `GET /api/v1/school/schedules/templates` - 获取课表模板分页 +- ✅ `POST /api/v1/school/schedules/templates` - 创建课表模板 +- ✅ `GET /api/v1/school/schedules/templates/{id}` - 根据 ID 获取课表模板 +- ✅ `PUT /api/v1/school/schedules/templates/{id}` - 更新课表模板 +- ✅ `DELETE /api/v1/school/schedules/templates/{id}` - 删除课表模板 +- ✅ `POST /api/v1/school/schedules/templates/{id}/apply` - 应用课表模板 + +### 17. 学校管理员 - 家长 (SchoolParentController) +- ✅ `GET /api/v1/school/parents` - 获取家长分页 +- ✅ `POST /api/v1/school/parents` - 创建家长 +- ✅ `GET /api/v1/school/parents/{id}` - 根据 ID 获取家长 +- ✅ `PUT /api/v1/school/parents/{id}` - 更新家长 +- ✅ `DELETE /api/v1/school/parents/{id}` - 删除家长 +- ✅ `POST /api/v1/school/parents/{id}/reset-password` - 重置密码 +- ✅ `POST /api/v1/school/parents/{parentId}/students/{studentId}` - 绑定学生 +- ✅ `DELETE /api/v1/school/parents/{parentId}/students/{studentId}` - 解绑学生 + +### 18. 学校管理员 - 成长档案 (SchoolGrowthController) +- ✅ `GET /api/v1/school/growth-records` - 获取成长档案分页 +- ✅ `POST /api/v1/school/growth-records` - 创建成长档案 +- ✅ `GET /api/v1/school/growth-records/{id}` - 根据 ID 获取成长档案 +- ✅ `PUT /api/v1/school/growth-records/{id}` - 更新成长档案 +- ✅ `DELETE /api/v1/school/growth-records/{id}` - 删除成长档案 + +### 19. 学校管理员 - 班级 (SchoolClassController) +- ✅ `GET /api/v1/school/classes` - 获取班级分页 +- ✅ `POST /api/v1/school/classes` - 创建班级 +- ✅ `GET /api/v1/school/classes/{id}` - 根据 ID 获取班级 +- ✅ `PUT /api/v1/school/classes/{id}` - 更新班级 +- ✅ `DELETE /api/v1/school/classes/{id}` - 删除班级 +- ✅ `POST /api/v1/school/classes/{id}/teachers` - 分配教师到班级 +- ✅ `POST /api/v1/school/classes/{id}/students` - 分配学生到班级 + +### 20. 学校管理员 - 通知 (SchoolNotificationController) +- ✅ `GET /api/v1/school/notifications` - 获取通知分页 +- ✅ `GET /api/v1/school/notifications/{id}` - 根据 ID 获取通知 +- ✅ `GET /api/v1/school/notifications/unread-count` - 获取未读数量 +- ✅ `POST /api/v1/school/notifications/{id}/read` - 标记已读 +- ✅ `POST /api/v1/school/notifications/read-all` - 全部标记已读 + +### 21. 学校管理员 - 统计 (SchoolStatsController) +- ✅ `GET /api/v1/school/stats` - 获取统计数据 + +### 22. 学校管理员 - 操作日志 (SchoolOperationLogController) +- ✅ `GET /api/v1/school/operation-logs` - 获取操作日志分页 + +### 23. 学校管理员 - 导出 (SchoolExportController) +- ✅ `GET /api/v1/school/export/teachers` - 导出教师数据 +- ✅ `GET /api/v1/school/export/students` - 导出学生数据 +- ✅ `GET /api/v1/school/export/lessons` - 导出课时数据 +- ✅ `GET /api/v1/school/export/growth-records` - 导出成长档案 + +### 24. 学校管理员 - 课程包 (SchoolCoursePackageController) +- ✅ `GET /api/v1/school/course-packages` - 获取课程包分页 +- ✅ `GET /api/v1/school/course-packages/{id}` - 根据 ID 获取课程包 + +### 25. 家长端 - 孩子 (ParentChildController) +- ✅ `GET /api/v1/parent/children` - 获取我的孩子 +- ✅ `GET /api/v1/parent/children/{id}` - 根据 ID 获取孩子 + +### 26. 家长端 - 任务 (ParentTaskController) +- ✅ `GET /api/v1/parent/tasks/{id}` - 根据 ID 获取任务 +- ✅ `GET /api/v1/parent/tasks/student/{studentId}` - 根据学生 ID 获取任务 +- ✅ `POST /api/v1/parent/tasks/{taskId}/complete` - 完成任务 + +### 27. 家长端 - 通知 (ParentNotificationController) +- ✅ `GET /api/v1/parent/notifications` - 获取通知分页 +- ✅ `GET /api/v1/parent/notifications/{id}` - 根据 ID 获取通知 +- ✅ `GET /api/v1/parent/notifications/unread-count` - 获取未读数量 +- ✅ `POST /api/v1/parent/notifications/{id}/read` - 标记已读 +- ✅ `POST /api/v1/parent/notifications/read-all` - 全部标记已读 + +### 28. 家长端 - 成长档案 (ParentGrowthController) +- ✅ `GET /api/v1/parent/growth-records` - 获取成长档案分页 +- ✅ `POST /api/v1/parent/growth-records` - 创建成长档案 +- ✅ `GET /api/v1/parent/growth-records/{id}` - 根据 ID 获取成长档案 +- ✅ `PUT /api/v1/parent/growth-records/{id}` - 更新成长档案 +- ✅ `DELETE /api/v1/parent/growth-records/{id}` - 删除成长档案 +- ✅ `GET /api/v1/parent/growth-records/student/{studentId}` - 根据学生 ID 获取成长档案 +- ✅ `GET /api/v1/parent/growth-records/student/{studentId}/recent` - 获取最近成长档案 + +### 29. 管理员端 - 租户 (AdminTenantController) +- ✅ `GET /api/v1/admin/tenants` - 获取租户分页 +- ✅ `POST /api/v1/admin/tenants` - 创建租户 +- ✅ `GET /api/v1/admin/tenants/{id}` - 根据 ID 获取租户 +- ✅ `PUT /api/v1/admin/tenants/{id}` - 更新租户 +- ✅ `DELETE /api/v1/admin/tenants/{id}` - 删除租户 +- ✅ `GET /api/v1/admin/tenants/active` - 获取活跃租户 +- ✅ `PUT /api/v1/admin/tenants/{id}/status` - 更新租户状态 +- ✅ `PUT /api/v1/admin/tenants/{id}/quota` - 更新租户配额 +- ✅ `POST /api/v1/admin/tenants/{id}/reset-password` - 重置密码 + +### 30. 管理员端 - 主题 (AdminThemeController) +- ✅ `GET /api/v1/admin/themes` - 获取主题分页 +- ✅ `POST /api/v1/admin/themes` - 创建主题 +- ✅ `GET /api/v1/admin/themes/{id}` - 根据 ID 获取主题 +- ✅ `PUT /api/v1/admin/themes/{id}` - 更新主题 +- ✅ `DELETE /api/v1/admin/themes/{id}` - 删除主题 + +### 31. 管理员端 - 资源 (AdminResourceController) +- ✅ `GET /api/v1/admin/resources/libraries` - 获取资源库分页 +- ✅ `POST /api/v1/admin/resources/libraries` - 创建资源库 +- ✅ `GET /api/v1/admin/resources/libraries/{id}` - 根据 ID 获取资源库 +- ✅ `PUT /api/v1/admin/resources/libraries/{id}` - 更新资源库 +- ✅ `DELETE /api/v1/admin/resources/libraries/{id}` - 删除资源库 +- ✅ `GET /api/v1/admin/resources/items` - 获取资源项分页 +- ✅ `POST /api/v1/admin/resources/items` - 创建资源项 +- ✅ `GET /api/v1/admin/resources/items/{id}` - 根据 ID 获取资源项 +- ✅ `PUT /api/v1/admin/resources/items/{id}` - 更新资源项 +- ✅ `DELETE /api/v1/admin/resources/items/{id}` - 删除资源项 + +### 32. 管理员端 - 课程包 (AdminCoursePackageController) +- ✅ `GET /api/v1/admin/packages` - 获取课程包分页 +- ✅ `POST /api/v1/admin/packages` - 创建课程包 +- ✅ `GET /api/v1/admin/packages/{id}` - 根据 ID 获取课程包 +- ✅ `PUT /api/v1/admin/packages/{id}` - 更新课程包 +- ✅ `DELETE /api/v1/admin/packages/{id}` - 删除课程包 +- ✅ `POST /api/v1/admin/packages/{id}/submit` - 提交审核 +- ✅ `POST /api/v1/admin/packages/{id}/review` - 审核 +- ✅ `POST /api/v1/admin/packages/{id}/publish` - 发布 +- ✅ `POST /api/v1/admin/packages/{id}/offline` - 下架 + +### 33. 管理员端 - 课程 (AdminCourseController) +- ✅ `GET /api/v1/admin/courses` - 获取课程分页 +- ✅ `POST /api/v1/admin/courses` - 创建课程 +- ✅ `GET /api/v1/admin/courses/{id}` - 根据 ID 获取课程 +- ✅ `PUT /api/v1/admin/courses/{id}` - 更新课程 +- ✅ `DELETE /api/v1/admin/courses/{id}` - 删除课程 +- ✅ `GET /api/v1/admin/courses/review` - 获取待审核课程 +- ✅ `POST /api/v1/admin/courses/{id}/submit` - 提交审核 +- ✅ `POST /api/v1/admin/courses/{id}/approve` - 审核通过 +- ✅ `POST /api/v1/admin/courses/{id}/reject` - 审核驳回 +- ✅ `POST /api/v1/admin/courses/{id}/publish` - 发布 +- ✅ `POST /api/v1/admin/courses/{id}/direct-publish` - 直接发布 +- ✅ `POST /api/v1/admin/courses/{id}/withdraw` - 撤销审核 +- ✅ `POST /api/v1/admin/courses/{id}/unpublish` - 下架 +- ✅ `POST /api/v1/admin/courses/{id}/republish` - 重新发布 +- ✅ `POST /api/v1/admin/courses/{id}/archive` - 归档 + +### 34. 管理员端 - 课程课时 (AdminCourseLessonController) +- ✅ `GET /api/v1/admin/courses/{courseId}/lessons` - 获取课程课时列表 +- ✅ `POST /api/v1/admin/courses/{courseId}/lessons` - 创建课程课时 +- ✅ `GET /api/v1/admin/courses/{courseId}/lessons/{id}` - 根据 ID 获取课时 +- ✅ `PUT /api/v1/admin/courses/{courseId}/lessons/{id}` - 更新课程课时 +- ✅ `DELETE /api/v1/admin/courses/{courseId}/lessons/{id}` - 删除课程课时 + +### 35. 管理员端 - 统计 (AdminStatsController) +- ✅ `GET /api/v1/admin/stats` - 获取整体统计数据 +- ✅ `GET /api/v1/admin/stats/trend` - 获取趋势数据 +- ✅ `GET /api/v1/admin/stats/tenants/active` - 获取活跃租户 +- ✅ `GET /api/v1/admin/stats/courses/popular` - 获取热门课程 +- ✅ `GET /api/v1/admin/stats/activities` - 获取最近活动 + +### 36. 管理员端 - 操作日志 (AdminOperationLogController) +- ✅ `GET /api/v1/admin/operation-logs` - 获取操作日志分页 + +### 37. 管理员端 - 设置 (AdminSettingsController) +- ✅ `GET /api/v1/admin/settings` - 获取系统设置 +- ✅ `PUT /api/v1/admin/settings` - 更新系统设置 + +--- + +## ❌ 缺失接口列表 + +### 1. 学校管理员端 - 班级 (SchoolClassController) + +前端调用但 Java 后端缺失: +- ❌ `DELETE /api/v1/school/classes/{id}/teachers/{teacherId}` - 移除班级教师 +- ❌ `DELETE /api/v1/school/classes/{id}/students/{studentId}` - 移除班级学生 + +Java 后端已有: +- ✅ `POST /api/v1/school/classes/{id}/teachers` - 分配教师到班级 +- ✅ `POST /api/v1/school/classes/{id}/students` - 分配学生到班级 + +### 2. 学校管理员端 - 学生 (SchoolStudentController) + +前端调用但 Java 后端缺失: +- ❌ `POST /api/v1/school/students/import` - 批量导入学生 +- ❌ `GET /api/v1/school/students/import/template` - 获取导入模板 + +--- + +## 总结 + +绝大部分接口已经在 Java 后端实现,仅剩以下缺失接口需要补全: + +### P0 - 核心功能缺失(必须补全) +暂无 + +### P1 - 重要功能缺失 +暂无 + +### P2 - 辅助功能缺失 +暂无 + +--- + +## 本次补全的接口 + +### 1. 学校管理员端 - 班级 (SchoolClassController) +新增接口: +- ✅ `DELETE /api/v1/school/classes/{id}/teachers/{teacherId}` - 移除班级教师 +- ✅ `DELETE /api/v1/school/classes/{id}/students/{studentId}` - 移除班级学生 + +新增 Service 方法 (ClassService): +- `void removeTeacher(Long classId, Long teacherId)` +- `void removeStudent(Long classId, Long studentId)` + +### 2. 学校管理员端 - 学生 (SchoolStudentController) +新增接口: +- ✅ `POST /api/v1/school/students/import` - 批量导入学生 +- ✅ `GET /api/v1/school/students/import/template` - 获取导入模板 + +新增 Service 方法 (StudentService): +- `List importStudents(Long tenantId, List requests)` + +### 3. 学校统计接口 (SchoolStatsController + SchoolStatsService) +新增接口: +- ✅ `GET /api/v1/school/stats/teachers` - 获取活跃教师统计 +- ✅ `GET /api/v1/school/stats/courses` - 获取课程使用统计 +- ✅ `GET /api/v1/school/stats/activities` - 获取最近活动记录 +- ✅ `GET /api/v1/school/stats/lesson-trend` - 获取课时趋势(最近 N 个月) +- ✅ `GET /api/v1/school/stats/course-distribution` - 获取课程分布统计(饼图数据) + +新增 Service 方法 (SchoolStatsService): +- `List> getActiveTeachers(Long tenantId, Integer limit)` +- `List> getCourseUsageStats(Long tenantId)` +- `List> getRecentActivities(Long tenantId, Integer limit)` +- `List> getLessonTrend(Long tenantId, Integer months)` +- `List> getCourseDistribution(Long tenantId)` +- `String formatActivityTitle(Lesson lesson)` - 私有方法,格式化活动标题 + +--- + +## 最终结论 + +**前端实际调用的所有接口已在 Java 后端全部实现。** diff --git a/补全接口修复总结.md b/补全接口修复总结.md new file mode 100644 index 0000000..992dec6 --- /dev/null +++ b/补全接口修复总结.md @@ -0,0 +1,228 @@ +# API 接口补全修复总结 + +## 修复日期 +2026-03-09 + +## 问题修复 + +### Bug 修复 +修复了 `TaskServiceImpl.java` 中使用 `ClassEntity` 的错误,改为使用 `Clazz`(因为 `class` 是 Java 关键字,项目中使用 `Clazz` 作为班级实体名称)。 + +**修改位置**: `reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java` +- 第 284 行:`List classes` → `List classes` +- 第 288 行:`for (ClassEntity cls : classes)` → `for (Clazz cls : classes)` + +### 数据库迁移 +新增数据库迁移脚本 `V7__fix_schedule_plans.sql`,为 `schedule_plans` 表添加以下字段: +- `course_id` - 课程 ID +- `teacher_id` - 教师 ID +- `day_of_week` - 星期几 (1-7) +- `period` - 节次 +- `start_time` - 开始时间 +- `end_time` - 结束时间 +- `location` - 教室/地点 +- `note` - 备注 + +--- + +## 已补全的 API 接口 + +### 1. 任务管理接口 + +#### 新增 Service 方法 (TaskService) +```java +// 任务统计 +Map getTaskStats(Long tenantId); +Map getStatsByType(Long tenantId); +List> getStatsByClass(Long tenantId); +List> getMonthlyStats(Long tenantId, Integer months); + +// 任务完成情况 +Page getTaskCompletions(Long tenantId, Long taskId, Integer pageNum, Integer pageSize, String status); +TaskCompletion updateTaskCompletion(Long tenantId, Long taskId, Long studentId, String status, String feedback); + +// 任务模板 +Page getTemplatePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String type); +TaskTemplate getTemplateById(Long tenantId, Long id); +TaskTemplate getDefaultTemplate(Long tenantId, String taskType); +TaskTemplate createTemplate(Long tenantId, Long creatorId, TaskTemplateCreateRequest request); +TaskTemplate updateTemplate(Long tenantId, Long id, TaskTemplateUpdateRequest request); +void deleteTemplate(Long tenantId, Long id); +Task createTaskFromTemplate(Long tenantId, Long creatorId, String creatorRole, CreateTaskFromTemplateRequest request); +``` + +#### 新增 Controller 端点 + +**SchoolTaskController** (学校管理员 - 任务管理): +- `GET /api/v1/school/tasks/stats` - 获取任务统计数据 +- `GET /api/v1/school/tasks/stats/by-type` - 按任务类型统计 +- `GET /api/v1/school/tasks/stats/by-class` - 按班级统计 +- `GET /api/v1/school/tasks/stats/monthly` - 月度统计趋势 +- `GET /api/v1/school/tasks/:id/completions` - 获取任务完成情况分页 +- `PUT /api/v1/school/tasks/:taskId/completions/:studentId` - 更新任务完成状态 +- `GET /api/v1/school/task-templates` - 获取任务模板列表 +- `GET /api/v1/school/task-templates/:id` - 获取单个模板 +- `GET /api/v1/school/task-templates/default/:taskType` - 获取默认模板 +- `POST /api/v1/school/task-templates` - 创建任务模板 +- `PUT /api/v1/school/task-templates/:id` - 更新任务模板 +- `DELETE /api/v1/school/task-templates/:id` - 删除任务模板 +- `POST /api/v1/school/tasks/from-template` - 从模板创建任务 + +**TeacherTaskController** (教师端 - 任务管理): +- `GET /api/v1/teacher/tasks/stats` - 获取任务统计数据 +- `GET /api/v1/teacher/tasks/stats/by-type` - 按任务类型统计 +- `GET /api/v1/teacher/tasks/stats/by-class` - 按班级统计 +- `GET /api/v1/teacher/tasks/stats/monthly` - 月度统计趋势 +- `GET /api/v1/teacher/tasks/:id/completions` - 获取任务完成情况分页 +- `PUT /api/v1/teacher/tasks/:taskId/completions/:studentId` - 更新任务完成状态 +- `GET /api/v1/teacher/task-templates` - 获取任务模板列表 +- `GET /api/v1/teacher/task-templates/:id` - 获取单个模板 +- `GET /api/v1/teacher/task-templates/default/:taskType` - 获取默认模板 +- `POST /api/v1/teacher/tasks/from-template` - 从模板创建任务 + +--- + +### 2. 通知接口 + +**新增 SchoolNotificationController** (学校管理员 - 通知): +- `GET /api/v1/school/notifications` - 获取通知列表 +- `GET /api/v1/school/notifications/:id` - 根据 ID 获取通知 +- `GET /api/v1/school/notifications/unread-count` - 获取未读数量 +- `POST /api/v1/school/notifications/:id/read` - 标记已读 +- `POST /api/v1/school/notifications/read-all` - 全部标记已读 + +--- + +### 3. 排课和课表接口 + +#### 新增 Service 方法 (ScheduleService) +```java +Page getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate); +List> getTimetable(Long tenantId, LocalDate startDate, LocalDate endDate, Long classId); +List batchCreateSchedules(Long tenantId, Long userId, List requests); +ScheduleTemplate updateScheduleTemplate(Long id, ScheduleTemplate template); +List applyScheduleTemplate(Long tenantId, Long templateId, ScheduleTemplateApplyRequest request); +``` + +#### 新增 Controller 端点 (SchoolScheduleController) +- `GET /api/v1/school/schedules/timetable` - 获取课表(带日期范围) +- `POST /api/v1/school/schedules/batch` - 批量创建排课 +- `GET /api/v1/school/schedules/templates/:id` - 获取单个模板 +- `PUT /api/v1/school/schedules/templates/:id` - 更新模板 +- `POST /api/v1/school/schedules/templates/:id/apply` - 应用模板 + +--- + +### 4. 实体类增强 + +**SchedulePlan** (新增字段): +- `courseId` - 课程 ID +- `teacherId` - 教师 ID +- `dayOfWeek` - 星期几 (1-7) +- `period` - 节次 +- `startTime` - 开始时间 +- `endTime` - 结束时间 +- `location` - 教室/地点 +- `note` - 备注 + +--- + +### 5. 新增 DTO + +**TaskTemplateCreateRequest** - 任务模板创建请求 +```java +- name: 模板名称 +- description: 模板描述 +- type: 任务类型 +- content: 任务内容模板 +- isPublic: 是否公共模板 +``` + +**TaskTemplateUpdateRequest** - 任务模板更新请求 + +**CreateTaskFromTemplateRequest** - 从模板创建任务请求 +```java +- templateId: 模板 ID +- targetIds: 目标 ID 列表 +- targetType: 目标类型 (CLASS/STUDENT) +- startDate: 开始日期 +- endDate: 截止日期 +``` + +**SchedulePlanCreateRequest** - 课表计划创建请求 +```java +- classId: 班级 ID +- courseId: 课程 ID +- teacherId: 授课教师 ID +- dayOfWeek: 星期几 +- period: 节次 +- startTime: 开始时间 +- endTime: 结束时间 +- location: 教室/地点 +- note: 备注 +- startDate: 开始日期 +- endDate: 结束日期 +``` + +**ScheduleTemplateApplyRequest** - 课表模板应用请求 +```java +- classId: 班级 ID +- startDate: 应用开始日期 +- weeks: 应用周数 +``` + +--- + +## 文件变更列表 + +### 新增文件 (7 个) +1. `dto/request/TaskTemplateCreateRequest.java` +2. `dto/request/TaskTemplateUpdateRequest.java` +3. `dto/request/CreateTaskFromTemplateRequest.java` +4. `dto/request/SchedulePlanCreateRequest.java` +5. `dto/request/ScheduleTemplateApplyRequest.java` +6. `controller/school/SchoolNotificationController.java` +7. `resources/db/migration/V7__fix_schedule_plans.sql` + +### 修改文件 (7 个) +1. `service/TaskService.java` - 添加统计和模板方法接口 +2. `service/impl/TaskServiceImpl.java` - 实现统计和模板逻辑 + 修复 ClassEntity bug +3. `service/ScheduleService.java` - 添加课表相关方法 +4. `entity/SchedulePlan.java` - 添加课程、教师等字段 +5. `controller/school/SchoolTaskController.java` - 添加统计和模板端点 +6. `controller/teacher/TeacherTaskController.java` - 添加统计和模板端点 +7. `controller/school/SchoolScheduleController.java` - 添加课表和模板端点 + +--- + +## 仍需补全的接口(下一步建议) + +### 高优先级 +1. **成长档案接口** - 按学生/班级查询 +2. **教师端课时反馈** - 完成课时、学生记录、反馈接口 +3. **班级管理** - 班级教师/学生管理、学生调班 + +### 中优先级 +1. **统计报告** - 学校统计、报告导出 +2. **导出接口增强** - 带日期范围参数 +3. **管理员课程审核** - 提交、审核、发布流程 + +--- + +## 使用说明 + +1. 运行数据库迁移: +```bash +# Flyway 会在应用启动时自动运行 V7 迁移脚本 +``` + +2. 编译项目: +```bash +cd reading-platform-java +mvn clean compile +``` + +3. 启动应用后,访问 API 文档: +``` +http://localhost:8080/doc.html +```