feat: 新增学校通知、任务模板和日程管理功能
- 新增学校通知控制器 (SchoolNotificationController) - 新增任务模板创建/更新请求 DTO - 新增日程计划创建和模板应用请求 DTO - 新增 TokenService 服务实现 - 新增多个服务实现类 (AdminStats, CourseLesson, CoursePackage 等) - 添加数据库迁移脚本 V7__fix_schedule_plans.sql - 更新配置文件和依赖 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
70e9683506
commit
583b47c430
0
.CurrentUserAllHosts
Normal file
0
.CurrentUserAllHosts
Normal file
55
.github/workflows/api-check.yml
vendored
Normal file
55
.github/workflows/api-check.yml
vendored
Normal file
@ -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/ 下的文件"
|
||||||
161
API 对比分析.md
Normal file
161
API 对比分析.md
Normal file
@ -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. 学生调班和批量导入接口
|
||||||
176
CLAUDE.md
176
CLAUDE.md
@ -1,144 +1,144 @@
|
|||||||
# CLAUDE.md
|
# 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`)
|
### 后端 (`reading-platform-java`)
|
||||||
- **Framework**: Spring Boot 3.2.3 + Java 17
|
- **框架**: Spring Boot 3.2.3 + Java 17
|
||||||
- **Persistence**: MyBatis-Plus 3.5.5
|
- **持久层**: MyBatis-Plus 3.5.5
|
||||||
- **Security**: Spring Security + JWT
|
- **安全**: Spring Security + JWT
|
||||||
- **API Docs**: Knife4j (Swagger OpenAPI 3)
|
- **API 文档**: Knife4j (Swagger OpenAPI 3)
|
||||||
- **Database**: MySQL 8.0
|
- **数据库**: MySQL 8.0
|
||||||
- **Migration**: Flyway
|
- **数据库迁移**: Flyway
|
||||||
|
|
||||||
### Frontend (`reading-platform-frontend`)
|
### 前端 (`reading-platform-frontend`)
|
||||||
- **Framework**: Vue 3 + TypeScript + Vite
|
- **框架**: Vue 3 + TypeScript + Vite
|
||||||
- **UI**: Ant Design Vue
|
- **UI 组件库**: Ant Design Vue
|
||||||
- **State**: Pinia
|
- **状态管理**: Pinia
|
||||||
- **API**: Axios with auto-generated TypeScript clients via Orval
|
- **API**: Axios + Orval 自动生成的 TypeScript 客户端
|
||||||
|
|
||||||
## Multi-Tenant Architecture
|
## 多租户架构
|
||||||
|
|
||||||
The system supports multiple kindergartens (tenants):
|
系统支持多个幼儿园(租户):
|
||||||
- `admin` role: Super admin (no tenant, manages system-wide courses)
|
- `admin` 角色:超级管理员(无租户,管理全系统课程)
|
||||||
- `school` role: School administrator (manages school's teachers, students, classes)
|
- `school` 角色:学校管理员(管理本校的教师、学生、班级)
|
||||||
- `teacher` role: Teacher (manages lessons, tasks for their tenant)
|
- `teacher` 角色:教师(管理本校的课时和任务)
|
||||||
- `parent` role: Parent (views child's progress and tasks)
|
- `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/
|
kindergarten_java/
|
||||||
├── reading-platform-java/ # Spring Boot backend
|
├── reading-platform-java/ # Spring Boot 后端
|
||||||
│ ├── src/main/java/.../controller/
|
│ ├── src/main/java/.../controller/
|
||||||
│ │ ├── admin/ # Super admin endpoints (/api/v1/admin/*)
|
│ │ ├── admin/ # 超级管理员端点 (/api/v1/admin/*)
|
||||||
│ │ ├── school/ # School admin endpoints (/api/v1/school/*)
|
│ │ ├── school/ # 学校管理员端点 (/api/v1/school/*)
|
||||||
│ │ ├── teacher/ # Teacher endpoints (/api/v1/teacher/*)
|
│ │ ├── teacher/ # 教师端点 (/api/v1/teacher/*)
|
||||||
│ │ └── parent/ # Parent endpoints (/api/v1/parent/*)
|
│ │ └── parent/ # 家长端点 (/api/v1/parent/*)
|
||||||
│ ├── entity/ # Database entities (27 tables)
|
│ ├── entity/ # 数据库实体(27张表)
|
||||||
│ ├── mapper/ # MyBatis-Plus mappers
|
│ ├── mapper/ # MyBatis-Plus 映射器
|
||||||
│ ├── service/ # Service layer interface + impl
|
│ ├── service/ # 服务层接口 + 实现
|
||||||
│ ├── common/
|
│ ├── common/
|
||||||
│ │ ├── annotation/RequireRole # Role-based access control
|
│ │ ├── annotation/RequireRole # 基于角色的访问控制
|
||||||
│ │ ├── security/ # JWT authentication
|
│ │ ├── security/ # JWT 认证
|
||||||
│ │ ├── enums/ # UserRole, CourseStatus, etc.
|
│ │ ├── enums/ # UserRole, CourseStatus 等枚举
|
||||||
│ │ ├── response/ # Result<T>, PageResult<T>
|
│ │ ├── response/ # Result<T>, PageResult<T>
|
||||||
│ │ └── config/ # Security, MyBatis, OpenAPI configs
|
│ │ └── config/ # Security, MyBatis, OpenAPI 配置
|
||||||
│ └── resources/
|
│ └── resources/
|
||||||
│ ├── db/migration/ # Flyway migration scripts
|
│ ├── db/migration/ # Flyway 迁移脚本
|
||||||
│ └── mapper/ # MyBatis XML files
|
│ └── mapper/ # MyBatis XML 文件
|
||||||
│
|
│
|
||||||
├── reading-platform-frontend/ # Vue 3 frontend
|
├── reading-platform-frontend/ # Vue 3 前端
|
||||||
│ ├── src/views/
|
│ ├── src/views/
|
||||||
│ │ ├── admin/ # Super admin pages
|
│ │ ├── admin/ # 超级管理员页面
|
||||||
│ │ ├── school/ # School admin pages
|
│ │ ├── school/ # 学校管理员页面
|
||||||
│ │ ├── teacher/ # Teacher pages
|
│ │ ├── teacher/ # 教师页面
|
||||||
│ │ └── parent/ # Parent pages
|
│ │ └── parent/ # 家长页面
|
||||||
│ ├── api/generated/ # Auto-generated API clients
|
│ ├── api/generated/ # 自动生成的 API 客户端
|
||||||
│ ├── api-spec.yml # OpenAPI specification
|
│ ├── api-spec.yml # OpenAPI 规范
|
||||||
│ └── router/index.ts # Vue Router config
|
│ └── router/index.ts # Vue Router 配置
|
||||||
│
|
│
|
||||||
├── docker-compose.yml # Backend + Frontend services
|
├── docker-compose.yml # 后端 + 前端服务
|
||||||
└── docs/开发协作指南.md # Development guide (Chinese)
|
└── docs/开发协作指南.md # 开发指南(中文)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Patterns
|
## 关键模式
|
||||||
|
|
||||||
### 1. Role-Based Access Control
|
### 1. 基于角色的访问控制
|
||||||
Use `@RequireRole` annotation on controllers/services:
|
在 Controller/Service 上使用 `@RequireRole` 注解:
|
||||||
```java
|
```java
|
||||||
@RequireRole(UserRole.SCHOOL) // Only school admins can access
|
@RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Tenant Isolation
|
### 2. 租户隔离
|
||||||
Use `SecurityUtils.getCurrentTenantId()` in school/teacher/parent endpoints to filter data by current tenant.
|
在学校/教师/家长端点中使用 `SecurityUtils.getCurrentTenantId()` 按当前租户过滤数据。
|
||||||
|
|
||||||
### 3. Unified Response Format
|
### 3. 统一响应格式
|
||||||
```java
|
```java
|
||||||
Result<T> success(T data) // { code: 200, message: "success", data: ... }
|
Result<T> success(T data) // { code: 200, message: "success", data: ... }
|
||||||
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
|
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. OpenAPI-Driven Development
|
### 4. OpenAPI 驱动开发
|
||||||
- Backend: Annotate controllers with `@Operation`, `@Parameter`, `@Schema`
|
- 后端:在 Controller 上使用 `@Operation`、`@Parameter`、`@Schema` 注解
|
||||||
- Frontend: Run `npm run api:update` to regenerate TypeScript clients from `api-spec.yml`
|
- 前端:运行 `npm run api:update` 从 `api-spec.yml` 重新生成 TypeScript 客户端
|
||||||
|
|
||||||
## Development Commands
|
## 开发命令
|
||||||
|
|
||||||
### Backend
|
### 后端
|
||||||
```bash
|
```bash
|
||||||
# Run with Docker Compose (recommended)
|
# 使用 Docker Compose 运行(推荐)
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
|
|
||||||
# Run locally (requires MySQL running)
|
# 本地运行(需要 MySQL 已启动)
|
||||||
cd reading-platform-java
|
cd reading-platform-java
|
||||||
mvn spring-boot:run
|
mvn spring-boot:run
|
||||||
|
|
||||||
# Build
|
# 构建
|
||||||
mvn clean package -DskipTests
|
mvn clean package -DskipTests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend
|
### 前端
|
||||||
```bash
|
```bash
|
||||||
cd reading-platform-frontend
|
cd reading-platform-frontend
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Update API clients from backend spec
|
# 从后端规范更新 API 客户端
|
||||||
npm run api:update
|
npm run api:update
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Migration
|
### 数据库迁移
|
||||||
- Add new migration scripts to `reading-platform-java/src/main/resources/db/migration/V{n}__description.sql`
|
- 将新的迁移脚本添加到 `reading-platform-java/src/main/resources/db/migration/V{n}__description.sql`
|
||||||
- Flyway runs automatically on backend startup (dev mode only)
|
- Flyway 会在后端启动时自动运行(仅开发模式)
|
||||||
|
|
||||||
## Database Schema (27 Tables)
|
## 数据库表结构(27张表)
|
||||||
- **Tenant**: tenants, tenant_courses
|
- **租户**: tenants, tenant_courses
|
||||||
- **Users**: admin_users, teachers, students, parents, parent_students
|
- **用户**: admin_users, teachers, students, parents, parent_students
|
||||||
- **Class**: classes, class_teachers, student_class_history
|
- **班级**: classes, class_teachers, student_class_history
|
||||||
- **Course**: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities
|
- **课程**: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities
|
||||||
- **Lesson**: lessons, lesson_feedbacks, student_records
|
- **课时**: lessons, lesson_feedbacks, student_records
|
||||||
- **Task**: tasks, task_targets, task_completions, task_templates
|
- **任务**: tasks, task_targets, task_completions, task_templates
|
||||||
- **Growth**: growth_records
|
- **成长**: growth_records
|
||||||
- **Resource**: resource_libraries, resource_items
|
- **资源**: resource_libraries, resource_items
|
||||||
- **Schedule**: schedule_plans, schedule_templates
|
- **日程**: schedule_plans, schedule_templates
|
||||||
- **System**: system_settings, notifications, operation_logs, tags
|
- **系统**: system_settings, notifications, operation_logs, tags
|
||||||
|
|
||||||
## Test Accounts
|
## 测试账号
|
||||||
| Role | Username | Password |
|
| 角色 | 用户名 | 密码 |
|
||||||
|------|----------|----------|
|
|------|--------|------|
|
||||||
| Admin | admin | admin123 |
|
| 管理员 | admin | admin123 |
|
||||||
| School | school | 123456 |
|
| 学校 | school | 123456 |
|
||||||
| Teacher | teacher1 | 123456 |
|
| 教师 | teacher1 | 123456 |
|
||||||
| Parent | parent1 | 123456 |
|
| 家长 | parent1 | 123456 |
|
||||||
|
|
||||||
## API Documentation
|
## API 文档
|
||||||
- Access: http://localhost:8080/doc.html (after backend starts)
|
- 访问地址:http://localhost:8080/doc.html(后端启动后)
|
||||||
132
Service 重构总结.md
Normal file
132
Service 重构总结.md
Normal file
@ -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<Xxx> 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<Xxx> 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 模式
|
||||||
26
reading-platform-frontend/src/components.d.ts
vendored
26
reading-platform-frontend/src/components.d.ts
vendored
@ -11,26 +11,15 @@ declare module 'vue' {
|
|||||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
|
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
|
||||||
ACol: typeof import('ant-design-vue/es')['Col']
|
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']
|
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']
|
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||||
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||||
AForm: typeof import('ant-design-vue/es')['Form']
|
AForm: typeof import('ant-design-vue/es')['Form']
|
||||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
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']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
||||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||||
@ -44,37 +33,22 @@ declare module 'vue' {
|
|||||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
|
||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
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']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
|
||||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
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']
|
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
||||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
|
||||||
ATable: typeof import('ant-design-vue/es')['Table']
|
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']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
|
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
|
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
|
||||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||||
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
|
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
|
||||||
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
|
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
|
||||||
|
|||||||
@ -46,6 +46,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- MyBatis-Plus -->
|
<!-- MyBatis-Plus -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.reading.platform.common.security;
|
package com.reading.platform.common.security;
|
||||||
|
|
||||||
|
import com.reading.platform.service.TokenService;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@ -25,6 +26,7 @@ import java.util.Collections;
|
|||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
private final TokenService tokenService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||||
@ -32,6 +34,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
try {
|
try {
|
||||||
String token = resolveToken(request);
|
String token = resolveToken(request);
|
||||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
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);
|
JwtPayload payload = jwtTokenProvider.getPayloadFromToken(token);
|
||||||
|
|
||||||
UsernamePasswordAuthenticationToken authentication =
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
|||||||
@ -7,8 +7,10 @@ import com.reading.platform.dto.response.UserInfoResponse;
|
|||||||
import com.reading.platform.service.AuthService;
|
import com.reading.platform.service.AuthService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@Tag(name = "认证", description = "认证相关接口")
|
@Tag(name = "认证", description = "认证相关接口")
|
||||||
@ -27,8 +29,9 @@ public class AuthController {
|
|||||||
|
|
||||||
@Operation(summary = "用户登出")
|
@Operation(summary = "用户登出")
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
public Result<Void> logout() {
|
public Result<Void> logout(HttpServletRequest request) {
|
||||||
// JWT is stateless - client simply discards the token
|
String token = resolveToken(request);
|
||||||
|
authService.logout(token);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,4 +50,12 @@ public class AuthController {
|
|||||||
return Result.success();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,4 +77,18 @@ public class SchoolClassController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "移除班级教师")
|
||||||
|
@DeleteMapping("/{id}/teachers/{teacherId}")
|
||||||
|
public Result<Void> removeTeacher(@PathVariable Long id, @PathVariable Long teacherId) {
|
||||||
|
classService.removeTeacher(id, teacherId);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "移除班级学生")
|
||||||
|
@DeleteMapping("/{id}/students/{studentId}")
|
||||||
|
public Result<Void> removeStudent(@PathVariable Long id, @PathVariable Long studentId) {
|
||||||
|
classService.removeStudent(id, studentId);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<Notification> getNotification(@PathVariable Long id) {
|
||||||
|
return Result.success(notificationService.getNotificationById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取我的通知")
|
||||||
|
@GetMapping
|
||||||
|
public Result<PageResult<Notification>> getMyNotifications(
|
||||||
|
@RequestParam(value = "page", required = false) Integer pageNum,
|
||||||
|
@RequestParam(required = false) Integer pageSize,
|
||||||
|
@RequestParam(required = false) Integer isRead) {
|
||||||
|
Long userId = SecurityUtils.getCurrentUserId();
|
||||||
|
Page<Notification> page = notificationService.getMyNotifications(userId, "school", pageNum, pageSize, isRead);
|
||||||
|
return Result.success(PageResult.of(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "标记通知为已读")
|
||||||
|
@PostMapping("/{id}/read")
|
||||||
|
public Result<Void> markAsRead(@PathVariable Long id) {
|
||||||
|
notificationService.markAsRead(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "标记所有通知为已读")
|
||||||
|
@PostMapping("/read-all")
|
||||||
|
public Result<Void> markAllAsRead() {
|
||||||
|
Long userId = SecurityUtils.getCurrentUserId();
|
||||||
|
notificationService.markAllAsRead(userId, "school");
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取未读通知数量")
|
||||||
|
@GetMapping("/unread-count")
|
||||||
|
public Result<Long> getUnreadCount() {
|
||||||
|
Long userId = SecurityUtils.getCurrentUserId();
|
||||||
|
return Result.success(notificationService.getUnreadCount(userId, "school"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -6,14 +6,22 @@ import com.reading.platform.common.enums.UserRole;
|
|||||||
import com.reading.platform.common.response.PageResult;
|
import com.reading.platform.common.response.PageResult;
|
||||||
import com.reading.platform.common.response.Result;
|
import com.reading.platform.common.response.Result;
|
||||||
import com.reading.platform.common.security.SecurityUtils;
|
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.SchedulePlan;
|
||||||
import com.reading.platform.entity.ScheduleTemplate;
|
import com.reading.platform.entity.ScheduleTemplate;
|
||||||
import com.reading.platform.service.ScheduleService;
|
import com.reading.platform.service.ScheduleService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "学校 - 课表", description = "课表管理接口(学校管理员专用)")
|
@Tag(name = "学校 - 课表", description = "课表管理接口(学校管理员专用)")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/school/schedules")
|
@RequestMapping("/api/v1/school/schedules")
|
||||||
@ -28,13 +36,25 @@ public class SchoolScheduleController {
|
|||||||
public Result<PageResult<SchedulePlan>> getSchedulePlans(
|
public Result<PageResult<SchedulePlan>> getSchedulePlans(
|
||||||
@RequestParam(defaultValue = "1") int pageNum,
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@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();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
Page<SchedulePlan> page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId);
|
Page<SchedulePlan> page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId, startDate, endDate);
|
||||||
return Result.success(PageResult.of(page));
|
return Result.success(PageResult.of(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "根据ID获取课表计划")
|
@Operation(summary = "获取课表(按日期范围)")
|
||||||
|
@GetMapping("/timetable")
|
||||||
|
public Result<List<Map<String, Object>>> 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}")
|
@GetMapping("/{id}")
|
||||||
public Result<SchedulePlan> getSchedulePlan(@PathVariable Long id) {
|
public Result<SchedulePlan> getSchedulePlan(@PathVariable Long id) {
|
||||||
return Result.success(scheduleService.getSchedulePlanById(id));
|
return Result.success(scheduleService.getSchedulePlanById(id));
|
||||||
@ -42,9 +62,10 @@ public class SchoolScheduleController {
|
|||||||
|
|
||||||
@Operation(summary = "创建课表计划")
|
@Operation(summary = "创建课表计划")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public Result<SchedulePlan> createSchedulePlan(@RequestBody SchedulePlan plan) {
|
public Result<SchedulePlan> createSchedulePlan(@Valid @RequestBody SchedulePlanCreateRequest plan) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
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 = "更新课表计划")
|
@Operation(summary = "更新课表计划")
|
||||||
@ -60,6 +81,16 @@ public class SchoolScheduleController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "批量创建排课")
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public Result<List<SchedulePlan>> batchCreateSchedules(@RequestBody List<SchedulePlanCreateRequest> plans) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
Long userId = SecurityUtils.getCurrentUserId();
|
||||||
|
return Result.success(scheduleService.batchCreateSchedules(tenantId, userId, plans));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 排课模板 ====================
|
||||||
|
|
||||||
@Operation(summary = "获取课表模板")
|
@Operation(summary = "获取课表模板")
|
||||||
@GetMapping("/templates")
|
@GetMapping("/templates")
|
||||||
public Result<PageResult<ScheduleTemplate>> getScheduleTemplates(
|
public Result<PageResult<ScheduleTemplate>> getScheduleTemplates(
|
||||||
@ -70,6 +101,12 @@ public class SchoolScheduleController {
|
|||||||
return Result.success(PageResult.of(page));
|
return Result.success(PageResult.of(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据 ID 获取课表模板")
|
||||||
|
@GetMapping("/templates/{id}")
|
||||||
|
public Result<ScheduleTemplate> getScheduleTemplate(@PathVariable Long id) {
|
||||||
|
return Result.success(scheduleService.getScheduleTemplateById(id));
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "创建课表模板")
|
@Operation(summary = "创建课表模板")
|
||||||
@PostMapping("/templates")
|
@PostMapping("/templates")
|
||||||
public Result<ScheduleTemplate> createScheduleTemplate(@RequestBody ScheduleTemplate template) {
|
public Result<ScheduleTemplate> createScheduleTemplate(@RequestBody ScheduleTemplate template) {
|
||||||
@ -77,10 +114,25 @@ public class SchoolScheduleController {
|
|||||||
return Result.success(scheduleService.createScheduleTemplate(tenantId, template));
|
return Result.success(scheduleService.createScheduleTemplate(tenantId, template));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新课表模板")
|
||||||
|
@PutMapping("/templates/{id}")
|
||||||
|
public Result<ScheduleTemplate> updateScheduleTemplate(@PathVariable Long id, @RequestBody ScheduleTemplate template) {
|
||||||
|
return Result.success(scheduleService.updateScheduleTemplate(id, template));
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "删除课表模板")
|
@Operation(summary = "删除课表模板")
|
||||||
@DeleteMapping("/templates/{id}")
|
@DeleteMapping("/templates/{id}")
|
||||||
public Result<Void> deleteScheduleTemplate(@PathVariable Long id) {
|
public Result<Void> deleteScheduleTemplate(@PathVariable Long id) {
|
||||||
scheduleService.deleteScheduleTemplate(id);
|
scheduleService.deleteScheduleTemplate(id);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "应用课表模板")
|
||||||
|
@PostMapping("/templates/{id}/apply")
|
||||||
|
public Result<List<SchedulePlan>> applyScheduleTemplate(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody ScheduleTemplateApplyRequest request) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(scheduleService.applyScheduleTemplate(tenantId, id, request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "学校 - 统计", description = "学校统计仪表盘接口(学校管理员专用)")
|
@Tag(name = "学校 - 统计", description = "学校统计仪表盘接口(学校管理员专用)")
|
||||||
@ -27,4 +28,42 @@ public class SchoolStatsController {
|
|||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
return Result.success(schoolStatsService.getStats(tenantId));
|
return Result.success(schoolStatsService.getStats(tenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取活跃教师统计")
|
||||||
|
@GetMapping("/teachers")
|
||||||
|
public Result<List<Map<String, Object>>> 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<List<Map<String, Object>>> getCourseUsageStats() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(schoolStatsService.getCourseUsageStats(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取最近活动记录")
|
||||||
|
@GetMapping("/activities")
|
||||||
|
public Result<List<Map<String, Object>>> 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<List<Map<String, Object>>> 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<List<Map<String, Object>>> getCourseDistribution() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(schoolStatsService.getCourseDistribution(tenantId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,10 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "学校 - 学生", description = "学生管理接口(学校管理员专用)")
|
@Tag(name = "学校 - 学生", description = "学生管理接口(学校管理员专用)")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/school/students")
|
@RequestMapping("/api/v1/school/students")
|
||||||
@ -61,4 +65,20 @@ public class SchoolStudentController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "批量导入学生")
|
||||||
|
@PostMapping("/import")
|
||||||
|
public Result<List<Student>> importStudents(@RequestBody List<StudentCreateRequest> requests) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(studentService.importStudents(tenantId, requests));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取导入模板")
|
||||||
|
@GetMapping("/import/template")
|
||||||
|
public Result<Map<String, Object>> getImportTemplate() {
|
||||||
|
Map<String, Object> template = new HashMap<>();
|
||||||
|
template.put("headers", new String[]{"姓名", "性别", "出生日期", "家长手机号", "家长姓名", "备注"});
|
||||||
|
template.put("example", new String[]{"张三", "男", "2018-01-01", "13800138000", "张父", "示例数据"});
|
||||||
|
return Result.success(template);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.PageResult;
|
||||||
import com.reading.platform.common.response.Result;
|
import com.reading.platform.common.response.Result;
|
||||||
import com.reading.platform.common.security.SecurityUtils;
|
import com.reading.platform.common.security.SecurityUtils;
|
||||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
import com.reading.platform.dto.request.*;
|
||||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
|
||||||
import com.reading.platform.entity.Task;
|
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 com.reading.platform.service.TaskService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@ -14,6 +15,9 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "学校 - 任务", description = "任务管理接口(学校管理员专用)")
|
@Tag(name = "学校 - 任务", description = "任务管理接口(学校管理员专用)")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/school/tasks")
|
@RequestMapping("/api/v1/school/tasks")
|
||||||
@ -37,7 +41,7 @@ public class SchoolTaskController {
|
|||||||
return Result.success(taskService.updateTask(id, request));
|
return Result.success(taskService.updateTask(id, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "根据ID获取任务")
|
@Operation(summary = "根据 ID 获取任务")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public Result<Task> getTask(@PathVariable Long id) {
|
public Result<Task> getTask(@PathVariable Long id) {
|
||||||
return Result.success(taskService.getTaskById(id));
|
return Result.success(taskService.getTaskById(id));
|
||||||
@ -63,4 +67,120 @@ public class SchoolTaskController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 任务统计 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "获取任务统计数据")
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public Result<Map<String, Object>> getStats() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getTaskStats(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "按任务类型统计")
|
||||||
|
@GetMapping("/stats/by-type")
|
||||||
|
public Result<Map<String, Object>> getStatsByType() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getStatsByType(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "按班级统计")
|
||||||
|
@GetMapping("/stats/by-class")
|
||||||
|
public Result<List<Map<String, Object>>> getStatsByClass() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getStatsByClass(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取月度统计趋势")
|
||||||
|
@GetMapping("/stats/monthly")
|
||||||
|
public Result<List<Map<String, Object>>> 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<PageResult<TaskCompletion>> 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<TaskCompletion> page = taskService.getTaskCompletions(tenantId, id, pageNum, pageSize, status);
|
||||||
|
return Result.success(PageResult.of(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新任务完成状态")
|
||||||
|
@PutMapping("/{taskId}/completions/{studentId}")
|
||||||
|
public Result<TaskCompletion> 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<PageResult<TaskTemplate>> 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<TaskTemplate> page = taskService.getTemplatePage(tenantId, pageNum, pageSize, keyword, type);
|
||||||
|
return Result.success(PageResult.of(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据 ID 获取任务模板")
|
||||||
|
@GetMapping("/task-templates/{id}")
|
||||||
|
public Result<TaskTemplate> getTemplate(@PathVariable Long id) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getTemplateById(tenantId, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取默认模板(按类型)")
|
||||||
|
@GetMapping("/task-templates/default/{taskType}")
|
||||||
|
public Result<TaskTemplate> getDefaultTemplate(@PathVariable String taskType) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getDefaultTemplate(tenantId, taskType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建任务模板")
|
||||||
|
@PostMapping("/task-templates")
|
||||||
|
public Result<TaskTemplate> 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<TaskTemplate> 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<Void> deleteTemplate(@PathVariable Long id) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
taskService.deleteTemplate(tenantId, id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "从模板创建任务")
|
||||||
|
@PostMapping("/from-template")
|
||||||
|
public Result<Task> 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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.PageResult;
|
||||||
import com.reading.platform.common.response.Result;
|
import com.reading.platform.common.response.Result;
|
||||||
import com.reading.platform.common.security.SecurityUtils;
|
import com.reading.platform.common.security.SecurityUtils;
|
||||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
import com.reading.platform.dto.request.*;
|
||||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
|
||||||
import com.reading.platform.entity.Task;
|
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 com.reading.platform.service.TaskService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@ -14,6 +15,9 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "教师 - 任务", description = "任务接口(教师专用)")
|
@Tag(name = "教师 - 任务", description = "任务接口(教师专用)")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/teacher/tasks")
|
@RequestMapping("/api/v1/teacher/tasks")
|
||||||
@ -36,7 +40,7 @@ public class TeacherTaskController {
|
|||||||
return Result.success(taskService.updateTask(id, request));
|
return Result.success(taskService.updateTask(id, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "根据ID获取任务")
|
@Operation(summary = "根据 ID 获取任务")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public Result<Task> getTask(@PathVariable Long id) {
|
public Result<Task> getTask(@PathVariable Long id) {
|
||||||
return Result.success(taskService.getTaskById(id));
|
return Result.success(taskService.getTaskById(id));
|
||||||
@ -62,4 +66,94 @@ public class TeacherTaskController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 任务统计 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "获取任务统计数据")
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public Result<Map<String, Object>> getStats() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getTaskStats(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "按任务类型统计")
|
||||||
|
@GetMapping("/stats/by-type")
|
||||||
|
public Result<Map<String, Object>> getStatsByType() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getStatsByType(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "按班级统计")
|
||||||
|
@GetMapping("/stats/by-class")
|
||||||
|
public Result<List<Map<String, Object>>> getStatsByClass() {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getStatsByClass(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取月度统计趋势")
|
||||||
|
@GetMapping("/stats/monthly")
|
||||||
|
public Result<List<Map<String, Object>>> 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<PageResult<TaskCompletion>> 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<TaskCompletion> page = taskService.getTaskCompletions(tenantId, id, pageNum, pageSize, status);
|
||||||
|
return Result.success(PageResult.of(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新任务完成状态")
|
||||||
|
@PutMapping("/{taskId}/completions/{studentId}")
|
||||||
|
public Result<TaskCompletion> 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<PageResult<TaskTemplate>> 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<TaskTemplate> page = taskService.getTemplatePage(tenantId, pageNum, pageSize, keyword, type);
|
||||||
|
return Result.success(PageResult.of(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据 ID 获取任务模板")
|
||||||
|
@GetMapping("/task-templates/{id}")
|
||||||
|
public Result<TaskTemplate> getTemplate(@PathVariable Long id) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getTemplateById(tenantId, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取默认模板(按类型)")
|
||||||
|
@GetMapping("/task-templates/default/{taskType}")
|
||||||
|
public Result<TaskTemplate> getDefaultTemplate(@PathVariable String taskType) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
return Result.success(taskService.getDefaultTemplate(tenantId, taskType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "从模板创建任务")
|
||||||
|
@PostMapping("/from-template")
|
||||||
|
public Result<Task> createFromTemplate(@Valid @RequestBody CreateTaskFromTemplateRequest request) {
|
||||||
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
|
Long userId = SecurityUtils.getCurrentUserId();
|
||||||
|
return Result.success(taskService.createTaskFromTemplate(tenantId, userId, "teacher", request));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<Long> targetIds;
|
||||||
|
|
||||||
|
@Schema(description = "目标类型:CLASS-班级,STUDENT-学生")
|
||||||
|
private String targetType = "CLASS";
|
||||||
|
|
||||||
|
@Schema(description = "任务开始日期")
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
@Schema(description = "任务截止日期")
|
||||||
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 课表计划实体
|
* 课表计划实体
|
||||||
@ -22,10 +23,26 @@ public class SchedulePlan {
|
|||||||
|
|
||||||
private Long classId;
|
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 startDate;
|
||||||
|
|
||||||
private LocalDate endDate;
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
private String note;
|
||||||
|
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
|||||||
@ -1,148 +1,35 @@
|
|||||||
package com.reading.platform.service;
|
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.List;
|
||||||
import java.util.Map;
|
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;
|
Map<String, Object> getStats();
|
||||||
private final LessonMapper lessonMapper;
|
|
||||||
|
|
||||||
public Map<String, Object> getStats() {
|
/**
|
||||||
Map<String, Object> stats = new HashMap<>();
|
* 获取趋势数据
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getTrendData();
|
||||||
|
|
||||||
long tenantCount = tenantMapper.selectCount(null);
|
/**
|
||||||
long activeTenantCount = tenantMapper.selectCount(
|
* 获取活跃租户
|
||||||
new LambdaQueryWrapper<Tenant>().eq(Tenant::getStatus, "active"));
|
*/
|
||||||
long courseCount = courseMapper.selectCount(null);
|
List<Map<String, Object>> getActiveTenants(int limit);
|
||||||
long publishedCourseCount = courseMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Course>().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(
|
List<Map<String, Object>> getPopularCourses(int limit);
|
||||||
new LambdaQueryWrapper<Lesson>()
|
|
||||||
.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));
|
List<Map<String, Object>> getRecentActivities(int limit);
|
||||||
stats.put("courseCount", courseCount);
|
|
||||||
stats.put("publishedCourseCount", publishedCourseCount);
|
|
||||||
stats.put("lessonCount", lessonMapper.selectCount(null));
|
|
||||||
stats.put("monthlyLessons", monthlyLessons);
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Map<String, Object>> getTrendData() {
|
|
||||||
List<Map<String, Object>> 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<Lesson>()
|
|
||||||
.ge(Lesson::getLessonDate, monthStart)
|
|
||||||
.le(Lesson::getLessonDate, monthEnd));
|
|
||||||
|
|
||||||
// Count tenants created up to this month end
|
|
||||||
long tenantCount = tenantMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Tenant>()
|
|
||||||
.le(Tenant::getCreatedAt, monthEnd.atTime(23, 59, 59)));
|
|
||||||
|
|
||||||
// Count students created up to this month end
|
|
||||||
long studentCount = studentMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Student>()
|
|
||||||
.le(Student::getCreatedAt, monthEnd.atTime(23, 59, 59)));
|
|
||||||
|
|
||||||
Map<String, Object> 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<Map<String, Object>> getActiveTenants(int limit) {
|
|
||||||
LambdaQueryWrapper<Tenant> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(Tenant::getStatus, "active")
|
|
||||||
.orderByDesc(Tenant::getCreatedAt)
|
|
||||||
.last("LIMIT " + limit);
|
|
||||||
List<Tenant> tenants = tenantMapper.selectList(wrapper);
|
|
||||||
return tenants.stream().map(t -> {
|
|
||||||
Map<String, Object> 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<Teacher>().eq(Teacher::getTenantId, t.getId()));
|
|
||||||
long studentCount = studentMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, t.getId()));
|
|
||||||
long lessonCount = lessonMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Lesson>().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<Map<String, Object>> getPopularCourses(int limit) {
|
|
||||||
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(Course::getIsSystem, 1)
|
|
||||||
.eq(Course::getStatus, "published")
|
|
||||||
.orderByDesc(Course::getCreatedAt)
|
|
||||||
.last("LIMIT " + limit);
|
|
||||||
List<Course> courses = courseMapper.selectList(wrapper);
|
|
||||||
return courses.stream().map(c -> {
|
|
||||||
Map<String, Object> 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<Map<String, Object>> getRecentActivities(int limit) {
|
|
||||||
LambdaQueryWrapper<Lesson> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.orderByDesc(Lesson::getCreatedAt).last("LIMIT " + limit);
|
|
||||||
List<Lesson> lessons = lessonMapper.selectList(wrapper);
|
|
||||||
return lessons.stream().map(l -> {
|
|
||||||
Map<String, Object> 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,4 +15,10 @@ public interface AuthService {
|
|||||||
|
|
||||||
void changePassword(String oldPassword, String newPassword);
|
void changePassword(String oldPassword, String newPassword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出 - 删除 Redis 中的 Token
|
||||||
|
* @param token JWT token
|
||||||
|
*/
|
||||||
|
void logout(String token);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,10 @@ public interface ClassService {
|
|||||||
|
|
||||||
void assignStudents(Long classId, List<Long> studentIds);
|
void assignStudents(Long classId, List<Long> studentIds);
|
||||||
|
|
||||||
|
void removeTeacher(Long classId, Long teacherId);
|
||||||
|
|
||||||
|
void removeStudent(Long classId, Long studentId);
|
||||||
|
|
||||||
List<Long> getTeacherIdsByClassId(Long classId);
|
List<Long> getTeacherIdsByClassId(Long classId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,36 @@
|
|||||||
package com.reading.platform.service;
|
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.entity.CourseLesson;
|
||||||
import com.reading.platform.mapper.CourseLessonMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
/**
|
||||||
@RequiredArgsConstructor
|
* 课程课时服务接口
|
||||||
public class CourseLessonService {
|
*/
|
||||||
|
public interface CourseLessonService {
|
||||||
|
|
||||||
private final CourseLessonMapper courseLessonMapper;
|
/**
|
||||||
|
* 根据课程 ID 获取课时列表
|
||||||
|
*/
|
||||||
|
List<CourseLesson> getLessonsByCourse(Long courseId);
|
||||||
|
|
||||||
public List<CourseLesson> getLessonsByCourse(Long courseId) {
|
/**
|
||||||
LambdaQueryWrapper<CourseLesson> wrapper = new LambdaQueryWrapper<>();
|
* 根据 ID 获取课时
|
||||||
wrapper.eq(CourseLesson::getCourseId, courseId)
|
*/
|
||||||
.orderByAsc(CourseLesson::getSortOrder);
|
CourseLesson getLessonById(Long id);
|
||||||
return courseLessonMapper.selectList(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CourseLesson getLessonById(Long id) {
|
/**
|
||||||
CourseLesson lesson = courseLessonMapper.selectById(id);
|
* 创建课时
|
||||||
if (lesson == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course lesson not found");
|
CourseLesson createLesson(CourseLesson lesson);
|
||||||
}
|
|
||||||
return 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);
|
void deleteLesson(Long id);
|
||||||
return courseLessonMapper.selectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteLesson(Long id) {
|
|
||||||
courseLessonMapper.deleteById(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,78 +1,55 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.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<CoursePackage> getPackages(int pageNum, int pageSize, String keyword, String status);
|
||||||
|
|
||||||
public Page<CoursePackage> getPackages(int pageNum, int pageSize, String keyword, String status) {
|
/**
|
||||||
Page<CoursePackage> page = new Page<>(pageNum, pageSize);
|
* 根据 ID 获取课程包
|
||||||
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
if (keyword != null && !keyword.isEmpty()) {
|
CoursePackage getPackageById(Long id);
|
||||||
wrapper.like(CoursePackage::getName, keyword);
|
|
||||||
}
|
|
||||||
if (status != null && !status.isEmpty()) {
|
|
||||||
wrapper.eq(CoursePackage::getStatus, status);
|
|
||||||
}
|
|
||||||
wrapper.orderByDesc(CoursePackage::getCreatedAt);
|
|
||||||
return coursePackageMapper.selectPage(page, wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoursePackage getPackageById(Long id) {
|
/**
|
||||||
CoursePackage pkg = coursePackageMapper.selectById(id);
|
* 创建课程包
|
||||||
if (pkg == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course package not found");
|
CoursePackage createPackage(CoursePackage pkg);
|
||||||
}
|
|
||||||
return 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);
|
void deletePackage(Long id);
|
||||||
return coursePackageMapper.selectById(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);
|
void offlinePackage(Long id);
|
||||||
}
|
|
||||||
|
|
||||||
public void offlinePackage(Long id) {
|
|
||||||
CoursePackage pkg = getPackageById(id);
|
|
||||||
pkg.setStatus("archived");
|
|
||||||
coursePackageMapper.updateById(pkg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,141 +1,29 @@
|
|||||||
package com.reading.platform.service;
|
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.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<Teacher> wrapper = new LambdaQueryWrapper<>();
|
* 导出学生数据
|
||||||
wrapper.eq(Teacher::getTenantId, tenantId);
|
*/
|
||||||
List<Teacher> 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());
|
byte[] exportGrowthRecords(Long tenantId) throws IOException;
|
||||||
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<Student> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(Student::getTenantId, tenantId);
|
|
||||||
List<Student> 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<Lesson> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(Lesson::getTenantId, tenantId);
|
|
||||||
List<Lesson> 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<GrowthRecord> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(GrowthRecord::getTenantId, tenantId);
|
|
||||||
List<GrowthRecord> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,71 +1,19 @@
|
|||||||
package com.reading.platform.service;
|
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 org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
/**
|
||||||
import java.io.IOException;
|
* 文件上传服务接口
|
||||||
import java.nio.file.Files;
|
*/
|
||||||
import java.nio.file.Path;
|
public interface FileUploadService {
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Slf4j
|
/**
|
||||||
@Service
|
* 上传文件
|
||||||
public class FileUploadService {
|
*/
|
||||||
|
String uploadFile(MultipartFile file);
|
||||||
|
|
||||||
@Value("${file.upload.path:/app/uploads/}")
|
/**
|
||||||
private String uploadPath;
|
* 删除文件
|
||||||
|
*/
|
||||||
@Value("${file.upload.base-url:/uploads/}")
|
void deleteFile(String filePath);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,20 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.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<OperationLog> getLogs(int pageNum, int pageSize, Long tenantId, String module);
|
||||||
|
|
||||||
public Page<OperationLog> getLogs(int pageNum, int pageSize, Long tenantId, String module) {
|
/**
|
||||||
Page<OperationLog> page = new Page<>(pageNum, pageSize);
|
* 记录操作日志
|
||||||
LambdaQueryWrapper<OperationLog> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
if (tenantId != null) {
|
void log(String action, String module, String targetType, Long targetId, String details);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,92 +1,63 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.ResourceItem;
|
||||||
import com.reading.platform.entity.ResourceLibrary;
|
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;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
/**
|
||||||
@RequiredArgsConstructor
|
* 资源服务接口
|
||||||
public class ResourceService {
|
*/
|
||||||
|
public interface ResourceService {
|
||||||
|
|
||||||
private final ResourceLibraryMapper resourceLibraryMapper;
|
/**
|
||||||
private final ResourceItemMapper resourceItemMapper;
|
* 获取资源库列表
|
||||||
|
*/
|
||||||
|
List<ResourceLibrary> getLibraries(Long tenantId);
|
||||||
|
|
||||||
public List<ResourceLibrary> getLibraries(Long tenantId) {
|
/**
|
||||||
LambdaQueryWrapper<ResourceLibrary> wrapper = new LambdaQueryWrapper<>();
|
* 根据 ID 获取资源库
|
||||||
if (tenantId != null) {
|
*/
|
||||||
wrapper.eq(ResourceLibrary::getTenantId, tenantId);
|
ResourceLibrary getLibraryById(Long id);
|
||||||
}
|
|
||||||
wrapper.orderByDesc(ResourceLibrary::getCreatedAt);
|
|
||||||
return resourceLibraryMapper.selectList(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResourceLibrary getLibraryById(Long id) {
|
/**
|
||||||
ResourceLibrary lib = resourceLibraryMapper.selectById(id);
|
* 创建资源库
|
||||||
if (lib == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource library not found");
|
ResourceLibrary createLibrary(ResourceLibrary library);
|
||||||
}
|
|
||||||
return lib;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
void deleteLibrary(Long id);
|
||||||
return resourceLibraryMapper.selectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteLibrary(Long id) {
|
/**
|
||||||
resourceLibraryMapper.deleteById(id);
|
* 获取资源项分页
|
||||||
}
|
*/
|
||||||
|
Page<ResourceItem> getItems(int pageNum, int pageSize, Long libraryId, String keyword);
|
||||||
|
|
||||||
public Page<ResourceItem> getItems(int pageNum, int pageSize, Long libraryId, String keyword) {
|
/**
|
||||||
Page<ResourceItem> page = new Page<>(pageNum, pageSize);
|
* 根据 ID 获取资源项
|
||||||
LambdaQueryWrapper<ResourceItem> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
if (libraryId != null) {
|
ResourceItem getItemById(Long id);
|
||||||
wrapper.eq(ResourceItem::getLibraryId, libraryId);
|
|
||||||
}
|
|
||||||
if (keyword != null && !keyword.isEmpty()) {
|
|
||||||
wrapper.like(ResourceItem::getName, keyword);
|
|
||||||
}
|
|
||||||
wrapper.orderByDesc(ResourceItem::getCreatedAt);
|
|
||||||
return resourceItemMapper.selectPage(page, wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResourceItem getItemById(Long id) {
|
/**
|
||||||
ResourceItem item = resourceItemMapper.selectById(id);
|
* 创建资源项
|
||||||
if (item == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource item not found");
|
ResourceItem createItem(ResourceItem item);
|
||||||
}
|
|
||||||
return 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);
|
void deleteItem(Long id);
|
||||||
return resourceItemMapper.selectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteItem(Long id) {
|
|
||||||
resourceItemMapper.deleteById(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +1,92 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.reading.platform.common.exception.BusinessException;
|
import com.reading.platform.dto.request.SchedulePlanCreateRequest;
|
||||||
import com.reading.platform.common.enums.ErrorCode;
|
import com.reading.platform.dto.request.ScheduleTemplateApplyRequest;
|
||||||
import com.reading.platform.entity.SchedulePlan;
|
import com.reading.platform.entity.SchedulePlan;
|
||||||
import com.reading.platform.entity.ScheduleTemplate;
|
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
|
import java.time.LocalDate;
|
||||||
@RequiredArgsConstructor
|
import java.util.List;
|
||||||
public class ScheduleService {
|
import java.util.Map;
|
||||||
|
|
||||||
private final SchedulePlanMapper schedulePlanMapper;
|
/**
|
||||||
private final ScheduleTemplateMapper scheduleTemplateMapper;
|
* 课表服务接口
|
||||||
|
*/
|
||||||
|
public interface ScheduleService {
|
||||||
|
|
||||||
public Page<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId) {
|
/**
|
||||||
Page<SchedulePlan> page = new Page<>(pageNum, pageSize);
|
* 获取课表计划分页
|
||||||
LambdaQueryWrapper<SchedulePlan> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
wrapper.eq(SchedulePlan::getTenantId, tenantId);
|
Page<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId);
|
||||||
if (classId != null) {
|
|
||||||
wrapper.eq(SchedulePlan::getClassId, classId);
|
|
||||||
}
|
|
||||||
wrapper.orderByDesc(SchedulePlan::getCreatedAt);
|
|
||||||
return schedulePlanMapper.selectPage(page, wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SchedulePlan getSchedulePlanById(Long id) {
|
/**
|
||||||
SchedulePlan plan = schedulePlanMapper.selectById(id);
|
* 获取课表计划分页(按日期范围)
|
||||||
if (plan == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Schedule plan not found");
|
Page<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate);
|
||||||
}
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan) {
|
/**
|
||||||
plan.setTenantId(tenantId);
|
* 根据 ID 获取课表计划
|
||||||
schedulePlanMapper.insert(plan);
|
*/
|
||||||
return plan;
|
SchedulePlan getSchedulePlanById(Long id);
|
||||||
}
|
|
||||||
|
|
||||||
public SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan) {
|
/**
|
||||||
SchedulePlan existing = getSchedulePlanById(id);
|
* 创建课表计划
|
||||||
plan.setId(id);
|
*/
|
||||||
plan.setTenantId(existing.getTenantId());
|
SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan);
|
||||||
schedulePlanMapper.updateById(plan);
|
|
||||||
return schedulePlanMapper.selectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteSchedulePlan(Long id) {
|
/**
|
||||||
schedulePlanMapper.deleteById(id);
|
* 创建课表计划
|
||||||
}
|
*/
|
||||||
|
SchedulePlan createSchedulePlan(Long tenantId, Long userId, SchedulePlanCreateRequest request);
|
||||||
|
|
||||||
public Page<ScheduleTemplate> getScheduleTemplates(int pageNum, int pageSize, Long tenantId) {
|
/**
|
||||||
Page<ScheduleTemplate> page = new Page<>(pageNum, pageSize);
|
* 更新课表计划
|
||||||
LambdaQueryWrapper<ScheduleTemplate> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
wrapper.eq(ScheduleTemplate::getTenantId, tenantId)
|
SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan);
|
||||||
.orderByDesc(ScheduleTemplate::getCreatedAt);
|
|
||||||
return scheduleTemplateMapper.selectPage(page, wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<SchedulePlan> batchCreateSchedules(Long tenantId, Long userId, List<SchedulePlanCreateRequest> requests);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课表
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getTimetable(Long tenantId, LocalDate startDate, LocalDate endDate, Long classId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课表模板分页
|
||||||
|
*/
|
||||||
|
Page<ScheduleTemplate> 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<SchedulePlan> applyScheduleTemplate(Long tenantId, Long templateId, ScheduleTemplateApplyRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +1,35 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.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<SchoolCourse> getCourses(int pageNum, int pageSize, Long tenantId, String keyword);
|
||||||
|
|
||||||
public Page<SchoolCourse> getCourses(int pageNum, int pageSize, Long tenantId, String keyword) {
|
/**
|
||||||
Page<SchoolCourse> page = new Page<>(pageNum, pageSize);
|
* 根据 ID 获取校本课程
|
||||||
LambdaQueryWrapper<SchoolCourse> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
wrapper.eq(SchoolCourse::getTenantId, tenantId);
|
SchoolCourse getCourseById(Long id);
|
||||||
if (keyword != null && !keyword.isEmpty()) {
|
|
||||||
wrapper.like(SchoolCourse::getName, keyword);
|
|
||||||
}
|
|
||||||
wrapper.orderByDesc(SchoolCourse::getCreatedAt);
|
|
||||||
return schoolCourseMapper.selectPage(page, wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SchoolCourse getCourseById(Long id) {
|
/**
|
||||||
SchoolCourse course = schoolCourseMapper.selectById(id);
|
* 创建校本课程
|
||||||
if (course == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "School course not found");
|
SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course);
|
||||||
}
|
|
||||||
return course;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course) {
|
/**
|
||||||
course.setTenantId(tenantId);
|
* 更新校本课程
|
||||||
course.setCreatedBy(userId);
|
*/
|
||||||
schoolCourseMapper.insert(course);
|
SchoolCourse updateCourse(Long id, SchoolCourse course);
|
||||||
return course;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SchoolCourse updateCourse(Long id, SchoolCourse course) {
|
/**
|
||||||
SchoolCourse existing = getCourseById(id);
|
* 删除校本课程
|
||||||
course.setId(id);
|
*/
|
||||||
course.setTenantId(existing.getTenantId());
|
void deleteCourse(Long id);
|
||||||
schoolCourseMapper.updateById(course);
|
|
||||||
return schoolCourseMapper.selectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteCourse(Long id) {
|
|
||||||
schoolCourseMapper.deleteById(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,41 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import java.util.List;
|
||||||
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.Map;
|
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<String, Object> getStats(Long tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活跃教师统计(授课次数最多的前 N 名教师)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getActiveTeachers(Long tenantId, Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课程使用统计(按课时完成次数排序)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getCourseUsageStats(Long tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近活动记录
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getRecentActivities(Long tenantId, Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课时趋势(最近 N 个月)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getLessonTrend(Long tenantId, Integer months);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课程分布统计(饼图数据)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getCourseDistribution(Long tenantId);
|
||||||
|
|
||||||
public Map<String, Object> getStats(Long tenantId) {
|
|
||||||
Map<String, Object> stats = new HashMap<>();
|
|
||||||
stats.put("teacherCount", teacherMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenantId)));
|
|
||||||
stats.put("studentCount", studentMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId)));
|
|
||||||
stats.put("classCount", clazzMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId)));
|
|
||||||
stats.put("lessonCount", lessonMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Lesson>().eq(Lesson::getTenantId, tenantId)));
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,4 +26,6 @@ public interface StudentService {
|
|||||||
|
|
||||||
List<Student> getStudentsByParentId(Long parentId);
|
List<Student> getStudentsByParentId(Long parentId);
|
||||||
|
|
||||||
|
List<Student> importStudents(Long tenantId, List<StudentCreateRequest> requests);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,61 +1,24 @@
|
|||||||
package com.reading.platform.service;
|
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;
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
/**
|
||||||
@RequiredArgsConstructor
|
* 系统设置服务接口
|
||||||
public class SystemSettingService {
|
*/
|
||||||
|
public interface SystemSettingService {
|
||||||
|
|
||||||
private final SystemSettingMapper systemSettingMapper;
|
/**
|
||||||
|
* 获取系统设置
|
||||||
|
*/
|
||||||
|
Map<String, String> getSettings(Long tenantId);
|
||||||
|
|
||||||
public Map<String, String> getSettings(Long tenantId) {
|
/**
|
||||||
LambdaQueryWrapper<SystemSetting> wrapper = new LambdaQueryWrapper<>();
|
* 更新系统设置
|
||||||
wrapper.eq(SystemSetting::getTenantId, tenantId);
|
*/
|
||||||
List<SystemSetting> settings = systemSettingMapper.selectList(wrapper);
|
void updateSettings(Long tenantId, Map<String, String> settings);
|
||||||
Map<String, String> result = new HashMap<>();
|
|
||||||
for (SystemSetting s : settings) {
|
|
||||||
result.put(s.getSettingKey(), s.getSettingValue());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateSettings(Long tenantId, Map<String, String> settings) {
|
/**
|
||||||
for (Map.Entry<String, String> entry : settings.entrySet()) {
|
* 获取单个设置项
|
||||||
LambdaQueryWrapper<SystemSetting> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
wrapper.eq(SystemSetting::getTenantId, tenantId)
|
String getSetting(Long tenantId, String key);
|
||||||
.eq(SystemSetting::getSettingKey, entry.getKey());
|
|
||||||
SystemSetting existing = systemSettingMapper.selectOne(wrapper);
|
|
||||||
if (existing != null) {
|
|
||||||
LambdaUpdateWrapper<SystemSetting> 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<SystemSetting> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(SystemSetting::getTenantId, tenantId)
|
|
||||||
.eq(SystemSetting::getSettingKey, key);
|
|
||||||
SystemSetting setting = systemSettingMapper.selectOne(wrapper);
|
|
||||||
return setting != null ? setting.getSettingValue() : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package com.reading.platform.service;
|
package com.reading.platform.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
import com.reading.platform.dto.request.*;
|
||||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
|
||||||
import com.reading.platform.entity.Task;
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task Service Interface
|
* Task Service Interface
|
||||||
@ -28,4 +30,73 @@ public interface TaskService {
|
|||||||
|
|
||||||
List<Task> getTasksByClassId(Long classId);
|
List<Task> getTasksByClassId(Long classId);
|
||||||
|
|
||||||
|
// ==================== 任务统计 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务统计数据
|
||||||
|
*/
|
||||||
|
Map<String, Object> getTaskStats(Long tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按任务类型统计
|
||||||
|
*/
|
||||||
|
Map<String, Object> getStatsByType(Long tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按班级统计
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getStatsByClass(Long tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取月度统计趋势
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getMonthlyStats(Long tenantId, Integer months);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务完成情况分页
|
||||||
|
*/
|
||||||
|
Page<TaskCompletion> getTaskCompletions(Long tenantId, Long taskId, Integer pageNum, Integer pageSize, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新任务完成状态
|
||||||
|
*/
|
||||||
|
TaskCompletion updateTaskCompletion(Long tenantId, Long taskId, Long studentId, String status, String feedback);
|
||||||
|
|
||||||
|
// ==================== 任务模板 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模板列表
|
||||||
|
*/
|
||||||
|
Page<TaskTemplate> 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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,86 +1,25 @@
|
|||||||
package com.reading.platform.service;
|
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.List;
|
||||||
import java.util.Map;
|
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<String, Object> getDashboard(Long teacherId, Long tenantId);
|
||||||
|
|
||||||
public Map<String, Object> getDashboard(Long teacherId, Long tenantId) {
|
/**
|
||||||
Map<String, Object> dashboard = new HashMap<>();
|
* 获取今天的课时
|
||||||
dashboard.put("lessonCount", lessonMapper.selectCount(
|
*/
|
||||||
new LambdaQueryWrapper<Lesson>()
|
List<Map<String, Object>> getTodayLessons(Long teacherId, Long tenantId);
|
||||||
.eq(Lesson::getTeacherId, teacherId)
|
|
||||||
.eq(Lesson::getTenantId, tenantId)));
|
|
||||||
dashboard.put("taskCount", taskMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Task>()
|
|
||||||
.eq(Task::getCreatorId, teacherId)
|
|
||||||
.eq(Task::getTenantId, tenantId)));
|
|
||||||
dashboard.put("growthRecordCount", growthRecordMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<GrowthRecord>()
|
|
||||||
.eq(GrowthRecord::getRecordedBy, teacherId)
|
|
||||||
.eq(GrowthRecord::getTenantId, tenantId)));
|
|
||||||
dashboard.put("unreadNotifications", notificationMapper.selectCount(
|
|
||||||
new LambdaQueryWrapper<Notification>()
|
|
||||||
.eq(Notification::getTenantId, tenantId)
|
|
||||||
.eq(Notification::getIsRead, 0)));
|
|
||||||
return dashboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Map<String, Object>> getTodayLessons(Long teacherId, Long tenantId) {
|
/**
|
||||||
LocalDate today = LocalDate.now();
|
* 获取本周的课时
|
||||||
LambdaQueryWrapper<Lesson> wrapper = new LambdaQueryWrapper<>();
|
*/
|
||||||
wrapper.eq(Lesson::getTeacherId, teacherId)
|
List<Map<String, Object>> getWeeklyLessons(Long teacherId, Long tenantId);
|
||||||
.eq(Lesson::getTenantId, tenantId)
|
|
||||||
.eq(Lesson::getLessonDate, today)
|
|
||||||
.orderByAsc(Lesson::getStartTime);
|
|
||||||
List<Lesson> lessons = lessonMapper.selectList(wrapper);
|
|
||||||
return lessons.stream().map(l -> {
|
|
||||||
Map<String, Object> 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<Map<String, Object>> getWeeklyLessons(Long teacherId, Long tenantId) {
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1);
|
|
||||||
LocalDate weekEnd = weekStart.plusDays(6);
|
|
||||||
LambdaQueryWrapper<Lesson> wrapper = new LambdaQueryWrapper<>();
|
|
||||||
wrapper.eq(Lesson::getTeacherId, teacherId)
|
|
||||||
.eq(Lesson::getTenantId, tenantId)
|
|
||||||
.between(Lesson::getLessonDate, weekStart, weekEnd)
|
|
||||||
.orderByAsc(Lesson::getLessonDate, Lesson::getStartTime);
|
|
||||||
List<Lesson> lessons = lessonMapper.selectList(wrapper);
|
|
||||||
return lessons.stream().map(l -> {
|
|
||||||
Map<String, Object> 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import com.reading.platform.entity.Tenant;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenant Service Interface
|
* 租户服务接口
|
||||||
*/
|
*/
|
||||||
public interface TenantService {
|
public interface TenantService {
|
||||||
|
|
||||||
|
|||||||
@ -1,51 +1,36 @@
|
|||||||
package com.reading.platform.service;
|
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.entity.Theme;
|
||||||
import com.reading.platform.mapper.ThemeMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
/**
|
||||||
@RequiredArgsConstructor
|
* 主题服务接口
|
||||||
public class ThemeService {
|
*/
|
||||||
|
public interface ThemeService {
|
||||||
|
|
||||||
private final ThemeMapper themeMapper;
|
/**
|
||||||
|
* 获取所有主题
|
||||||
|
*/
|
||||||
|
List<Theme> getAllThemes(Boolean enabledOnly);
|
||||||
|
|
||||||
public List<Theme> getAllThemes(Boolean enabledOnly) {
|
/**
|
||||||
LambdaQueryWrapper<Theme> wrapper = new LambdaQueryWrapper<>();
|
* 根据 ID 获取主题
|
||||||
if (Boolean.TRUE.equals(enabledOnly)) {
|
*/
|
||||||
wrapper.eq(Theme::getIsEnabled, 1);
|
Theme getThemeById(Long id);
|
||||||
}
|
|
||||||
wrapper.orderByAsc(Theme::getSortOrder);
|
|
||||||
return themeMapper.selectList(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Theme getThemeById(Long id) {
|
/**
|
||||||
Theme theme = themeMapper.selectById(id);
|
* 创建主题
|
||||||
if (theme == null) {
|
*/
|
||||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Theme not found");
|
Theme createTheme(Theme theme);
|
||||||
}
|
|
||||||
return 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);
|
void deleteTheme(Long id);
|
||||||
return themeMapper.selectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteTheme(Long id) {
|
|
||||||
themeMapper.deleteById(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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<String, Object> getStats() {
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
|
||||||
|
long tenantCount = tenantMapper.selectCount(null);
|
||||||
|
long activeTenantCount = tenantMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Tenant>().eq(Tenant::getStatus, "active"));
|
||||||
|
long courseCount = courseMapper.selectCount(null);
|
||||||
|
long publishedCourseCount = courseMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Course>().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<Lesson>()
|
||||||
|
.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<Map<String, Object>> getTrendData() {
|
||||||
|
List<Map<String, Object>> 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<Lesson>()
|
||||||
|
.ge(Lesson::getLessonDate, monthStart)
|
||||||
|
.le(Lesson::getLessonDate, monthEnd));
|
||||||
|
|
||||||
|
// Count tenants created up to this month end
|
||||||
|
long tenantCount = tenantMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Tenant>()
|
||||||
|
.le(Tenant::getCreatedAt, monthEnd.atTime(23, 59, 59)));
|
||||||
|
|
||||||
|
// Count students created up to this month end
|
||||||
|
long studentCount = studentMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Student>()
|
||||||
|
.le(Student::getCreatedAt, monthEnd.atTime(23, 59, 59)));
|
||||||
|
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getActiveTenants(int limit) {
|
||||||
|
LambdaQueryWrapper<Tenant> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Tenant::getStatus, "active")
|
||||||
|
.orderByDesc(Tenant::getCreatedAt)
|
||||||
|
.last("LIMIT " + limit);
|
||||||
|
List<Tenant> tenants = tenantMapper.selectList(wrapper);
|
||||||
|
return tenants.stream().map(t -> {
|
||||||
|
Map<String, Object> 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<Teacher>().eq(Teacher::getTenantId, t.getId()));
|
||||||
|
long studentCount = studentMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, t.getId()));
|
||||||
|
long lessonCount = lessonMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Lesson>().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<Map<String, Object>> getPopularCourses(int limit) {
|
||||||
|
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Course::getIsSystem, 1)
|
||||||
|
.eq(Course::getStatus, "published")
|
||||||
|
.orderByDesc(Course::getCreatedAt)
|
||||||
|
.last("LIMIT " + limit);
|
||||||
|
List<Course> courses = courseMapper.selectList(wrapper);
|
||||||
|
return courses.stream().map(c -> {
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getRecentActivities(int limit) {
|
||||||
|
LambdaQueryWrapper<Lesson> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.orderByDesc(Lesson::getCreatedAt).last("LIMIT " + limit);
|
||||||
|
List<Lesson> lessons = lessonMapper.selectList(wrapper);
|
||||||
|
return lessons.stream().map(l -> {
|
||||||
|
Map<String, Object> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import com.reading.platform.mapper.AdminUserMapper;
|
|||||||
import com.reading.platform.mapper.ParentMapper;
|
import com.reading.platform.mapper.ParentMapper;
|
||||||
import com.reading.platform.mapper.TeacherMapper;
|
import com.reading.platform.mapper.TeacherMapper;
|
||||||
import com.reading.platform.service.AuthService;
|
import com.reading.platform.service.AuthService;
|
||||||
|
import com.reading.platform.service.TokenService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@ -34,6 +35,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
private final ParentMapper parentMapper;
|
private final ParentMapper parentMapper;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final TokenService tokenService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginResponse login(LoginRequest request) {
|
public LoginResponse login(LoginRequest request) {
|
||||||
@ -69,8 +71,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.name(adminUser.getName())
|
.name(adminUser.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken(payload);
|
||||||
|
tokenService.saveToken(token, payload);
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
.token(jwtTokenProvider.generateToken(payload))
|
.token(token)
|
||||||
.userId(adminUser.getId())
|
.userId(adminUser.getId())
|
||||||
.username(adminUser.getUsername())
|
.username(adminUser.getUsername())
|
||||||
.name(adminUser.getName())
|
.name(adminUser.getName())
|
||||||
@ -101,8 +106,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.name(teacher.getName())
|
.name(teacher.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken(payload);
|
||||||
|
tokenService.saveToken(token, payload);
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
.token(jwtTokenProvider.generateToken(payload))
|
.token(token)
|
||||||
.userId(teacher.getId())
|
.userId(teacher.getId())
|
||||||
.username(teacher.getUsername())
|
.username(teacher.getUsername())
|
||||||
.name(teacher.getName())
|
.name(teacher.getName())
|
||||||
@ -133,8 +141,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.name(parent.getName())
|
.name(parent.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken(payload);
|
||||||
|
tokenService.saveToken(token, payload);
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
.token(jwtTokenProvider.generateToken(payload))
|
.token(token)
|
||||||
.userId(parent.getId())
|
.userId(parent.getId())
|
||||||
.username(parent.getUsername())
|
.username(parent.getUsername())
|
||||||
.name(parent.getName())
|
.name(parent.getName())
|
||||||
@ -171,8 +182,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.name(adminUser.getName())
|
.name(adminUser.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken(payload);
|
||||||
|
tokenService.saveToken(token, payload);
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
.token(jwtTokenProvider.generateToken(payload))
|
.token(token)
|
||||||
.userId(adminUser.getId())
|
.userId(adminUser.getId())
|
||||||
.username(adminUser.getUsername())
|
.username(adminUser.getUsername())
|
||||||
.name(adminUser.getName())
|
.name(adminUser.getName())
|
||||||
@ -202,8 +216,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.name(teacher.getName())
|
.name(teacher.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken(payload);
|
||||||
|
tokenService.saveToken(token, payload);
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
.token(jwtTokenProvider.generateToken(payload))
|
.token(token)
|
||||||
.userId(teacher.getId())
|
.userId(teacher.getId())
|
||||||
.username(teacher.getUsername())
|
.username(teacher.getUsername())
|
||||||
.name(teacher.getName())
|
.name(teacher.getName())
|
||||||
@ -232,8 +249,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.name(parent.getName())
|
.name(parent.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken(payload);
|
||||||
|
tokenService.saveToken(token, payload);
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
.token(jwtTokenProvider.generateToken(payload))
|
.token(token)
|
||||||
.userId(parent.getId())
|
.userId(parent.getId())
|
||||||
.username(parent.getUsername())
|
.username(parent.getUsername())
|
||||||
.name(parent.getName())
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -172,4 +172,40 @@ public class ClassServiceImpl implements ClassService {
|
|||||||
return teacherIds;
|
return teacherIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void removeTeacher(Long classId, Long teacherId) {
|
||||||
|
// Verify class exists
|
||||||
|
getClassById(classId);
|
||||||
|
|
||||||
|
// Delete teacher assignment
|
||||||
|
classTeacherMapper.delete(
|
||||||
|
new LambdaQueryWrapper<ClassTeacher>()
|
||||||
|
.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<StudentClassHistory> existingHistories = studentClassHistoryMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<StudentClassHistory>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<CourseLesson> getLessonsByCourse(Long courseId) {
|
||||||
|
LambdaQueryWrapper<CourseLesson> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<CoursePackage> getPackages(int pageNum, int pageSize, String keyword, String status) {
|
||||||
|
Page<CoursePackage> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<CoursePackage> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Teacher> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Teacher::getTenantId, tenantId);
|
||||||
|
List<Teacher> 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<Student> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Student::getTenantId, tenantId);
|
||||||
|
List<Student> 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<Lesson> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Lesson::getTenantId, tenantId);
|
||||||
|
List<Lesson> 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<GrowthRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(GrowthRecord::getTenantId, tenantId);
|
||||||
|
List<GrowthRecord> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<OperationLog> getLogs(int pageNum, int pageSize, Long tenantId, String module) {
|
||||||
|
Page<OperationLog> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<OperationLog> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ResourceLibrary> getLibraries(Long tenantId) {
|
||||||
|
LambdaQueryWrapper<ResourceLibrary> 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<ResourceItem> getItems(int pageNum, int pageSize, Long libraryId, String keyword) {
|
||||||
|
Page<ResourceItem> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<ResourceItem> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId) {
|
||||||
|
Page<SchedulePlan> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<SchedulePlan> 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<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate) {
|
||||||
|
Page<SchedulePlan> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<SchedulePlan> 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<SchedulePlan> batchCreateSchedules(Long tenantId, Long userId, List<SchedulePlanCreateRequest> requests) {
|
||||||
|
List<SchedulePlan> plans = new ArrayList<>();
|
||||||
|
for (SchedulePlanCreateRequest request : requests) {
|
||||||
|
SchedulePlan plan = createSchedulePlan(tenantId, userId, request);
|
||||||
|
plans.add(plan);
|
||||||
|
}
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getTimetable(Long tenantId, LocalDate startDate, LocalDate endDate, Long classId) {
|
||||||
|
if (startDate == null) {
|
||||||
|
startDate = LocalDate.now();
|
||||||
|
}
|
||||||
|
if (endDate == null) {
|
||||||
|
endDate = startDate.plusMonths(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<SchedulePlan> 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<SchedulePlan> plans = schedulePlanMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
// 按日期分组
|
||||||
|
Map<String, List<Map<String, Object>>> 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<String, Object> 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<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<Map<String, Object>>> entry : dailySchedules.entrySet()) {
|
||||||
|
Map<String, Object> dayData = new HashMap<>();
|
||||||
|
dayData.put("date", entry.getKey());
|
||||||
|
dayData.put("schedules", entry.getValue());
|
||||||
|
result.add(dayData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<ScheduleTemplate> getScheduleTemplates(int pageNum, int pageSize, Long tenantId) {
|
||||||
|
Page<ScheduleTemplate> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<ScheduleTemplate> 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<SchedulePlan> applyScheduleTemplate(Long tenantId, Long templateId, ScheduleTemplateApplyRequest request) {
|
||||||
|
ScheduleTemplate template = getScheduleTemplateById(templateId);
|
||||||
|
|
||||||
|
// 解析模板内容(JSON 格式)
|
||||||
|
// 模板内容格式示例:[{"dayOfWeek": 1, "period": 1, "courseId": 1, "teacherId": 1, ...}]
|
||||||
|
List<SchedulePlan> plans = new ArrayList<>();
|
||||||
|
|
||||||
|
// TODO: 解析模板内容并创建课表计划
|
||||||
|
// 这里需要根据实际的模板内容格式进行解析
|
||||||
|
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<SchoolCourse> getCourses(int pageNum, int pageSize, Long tenantId, String keyword) {
|
||||||
|
Page<SchoolCourse> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<SchoolCourse> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<String, Object> getStats(Long tenantId) {
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
stats.put("teacherCount", teacherMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenantId)));
|
||||||
|
stats.put("studentCount", studentMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId)));
|
||||||
|
stats.put("classCount", clazzMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId)));
|
||||||
|
stats.put("lessonCount", lessonMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Lesson>().eq(Lesson::getTenantId, tenantId)));
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getActiveTeachers(Long tenantId, Integer limit) {
|
||||||
|
if (limit == null) {
|
||||||
|
limit = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有活跃教师
|
||||||
|
List<Teacher> teachers = teacherMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<Teacher>()
|
||||||
|
.eq(Teacher::getTenantId, tenantId)
|
||||||
|
.eq(Teacher::getStatus, "ACTIVE")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 统计每位教师的授课次数
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Teacher teacher : teachers) {
|
||||||
|
long lessonCount = lessonMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Lesson>()
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.eq(Lesson::getTeacherId, teacher.getId())
|
||||||
|
.eq(Lesson::getStatus, "COMPLETED")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lessonCount > 0) {
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getCourseUsageStats(Long tenantId) {
|
||||||
|
// 获取所有已完成的课时
|
||||||
|
List<Lesson> lessons = lessonMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<Lesson>()
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.eq(Lesson::getStatus, "COMPLETED")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 统计每个课程的使用次数
|
||||||
|
Map<Long, Map<String, Object>> 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<String, Object> courseData = new HashMap<>();
|
||||||
|
courseData.put("courseId", courseId);
|
||||||
|
courseData.put("courseName", course != null ? course.getName() : "未知课程");
|
||||||
|
courseData.put("usageCount", 0);
|
||||||
|
courseUsageMap.put(courseId, courseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> courseData = courseUsageMap.get(courseId);
|
||||||
|
courseData.put("usageCount", (Integer) courseData.get("usageCount") + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为列表并按使用次数排序
|
||||||
|
List<Map<String, Object>> 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<Map<String, Object>> getRecentActivities(Long tenantId, Integer limit) {
|
||||||
|
if (limit == null) {
|
||||||
|
limit = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Lesson> lessons = lessonMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<Lesson>()
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.orderByDesc(Lesson::getCreatedAt)
|
||||||
|
.last("LIMIT " + limit)
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Map<String, Object>> activities = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Lesson lesson : lessons) {
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getLessonTrend(Long tenantId, Integer months) {
|
||||||
|
if (months == null) {
|
||||||
|
months = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
|
||||||
|
// 获取当前学生总数
|
||||||
|
long studentCount = studentMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Student>().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<Lesson>()
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.eq(Lesson::getStatus, "COMPLETED")
|
||||||
|
.ge(Lesson::getCreatedAt, startDateTime)
|
||||||
|
.le(Lesson::getCreatedAt, endDateTime)
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getCourseDistribution(Long tenantId) {
|
||||||
|
// 获取所有已完成的课时
|
||||||
|
List<Lesson> lessons = lessonMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<Lesson>()
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.eq(Lesson::getStatus, "COMPLETED")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 统计每个课程的完成次数
|
||||||
|
Map<String, Integer> 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<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Integer> entry : courseMap.entrySet()) {
|
||||||
|
Map<String, Object> 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 + "》";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -187,4 +187,15 @@ public class StudentServiceImpl implements StudentService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public List<Student> importStudents(Long tenantId, List<StudentCreateRequest> requests) {
|
||||||
|
List<Student> students = new ArrayList<>();
|
||||||
|
for (StudentCreateRequest request : requests) {
|
||||||
|
Student student = createStudent(tenantId, request);
|
||||||
|
students.add(student);
|
||||||
|
}
|
||||||
|
return students;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String, String> getSettings(Long tenantId) {
|
||||||
|
LambdaQueryWrapper<SystemSetting> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SystemSetting::getTenantId, tenantId);
|
||||||
|
List<SystemSetting> settings = systemSettingMapper.selectList(wrapper);
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
for (SystemSetting s : settings) {
|
||||||
|
result.put(s.getSettingKey(), s.getSettingValue());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateSettings(Long tenantId, Map<String, String> settings) {
|
||||||
|
for (Map.Entry<String, String> entry : settings.entrySet()) {
|
||||||
|
LambdaQueryWrapper<SystemSetting> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SystemSetting::getTenantId, tenantId)
|
||||||
|
.eq(SystemSetting::getSettingKey, entry.getKey());
|
||||||
|
SystemSetting existing = systemSettingMapper.selectOne(wrapper);
|
||||||
|
if (existing != null) {
|
||||||
|
LambdaUpdateWrapper<SystemSetting> 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<SystemSetting> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SystemSetting::getTenantId, tenantId)
|
||||||
|
.eq(SystemSetting::getSettingKey, key);
|
||||||
|
SystemSetting setting = systemSettingMapper.selectOne(wrapper);
|
||||||
|
return setting != null ? setting.getSettingValue() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,23 +5,18 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.reading.platform.common.enums.ErrorCode;
|
import com.reading.platform.common.enums.ErrorCode;
|
||||||
import com.reading.platform.common.exception.BusinessException;
|
import com.reading.platform.common.exception.BusinessException;
|
||||||
import com.reading.platform.common.util.PageUtils;
|
import com.reading.platform.common.util.PageUtils;
|
||||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
import com.reading.platform.dto.request.*;
|
||||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
import com.reading.platform.entity.*;
|
||||||
import com.reading.platform.entity.Task;
|
import com.reading.platform.mapper.*;
|
||||||
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.service.TaskService;
|
import com.reading.platform.service.TaskService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -30,6 +25,9 @@ public class TaskServiceImpl implements TaskService {
|
|||||||
private final TaskMapper taskMapper;
|
private final TaskMapper taskMapper;
|
||||||
private final TaskTargetMapper taskTargetMapper;
|
private final TaskTargetMapper taskTargetMapper;
|
||||||
private final TaskCompletionMapper taskCompletionMapper;
|
private final TaskCompletionMapper taskCompletionMapper;
|
||||||
|
private final TaskTemplateMapper taskTemplateMapper;
|
||||||
|
private final ClazzMapper classMapper;
|
||||||
|
private final StudentClassHistoryMapper studentClassHistoryMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@ -209,4 +207,396 @@ public class TaskServiceImpl implements TaskService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 任务统计 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getTaskStats(Long tenantId) {
|
||||||
|
// 统计任务总数
|
||||||
|
long totalTasks = taskMapper.selectCount(new LambdaQueryWrapper<Task>().eq(Task::getTenantId, tenantId));
|
||||||
|
|
||||||
|
// 统计已发布的任务数
|
||||||
|
long publishedTasks = taskMapper.selectCount(new LambdaQueryWrapper<Task>()
|
||||||
|
.eq(Task::getTenantId, tenantId).eq(Task::getStatus, "published"));
|
||||||
|
|
||||||
|
// 统计完成情况
|
||||||
|
long completedTasks = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.eq(TaskCompletion::getStatus, "completed"));
|
||||||
|
|
||||||
|
long inProgressTasks = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.eq(TaskCompletion::getStatus, "in_progress"));
|
||||||
|
|
||||||
|
long pendingCount = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.eq(TaskCompletion::getStatus, "pending"));
|
||||||
|
|
||||||
|
long totalCompletions = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>());
|
||||||
|
|
||||||
|
int completionRate = totalCompletions > 0 ? (int) ((completedTasks * 100) / totalCompletions) : 0;
|
||||||
|
|
||||||
|
Map<String, Object> 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<String, Object> getStatsByType(Long tenantId) {
|
||||||
|
List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>().eq(Task::getTenantId, tenantId));
|
||||||
|
|
||||||
|
Map<String, Map<String, Object>> 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<TaskCompletion>()
|
||||||
|
.eq(TaskCompletion::getTaskId, task.getId()));
|
||||||
|
long completed = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.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<String, Object> 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<String, Map<String, Object>> -> Map<String, Object>
|
||||||
|
return new HashMap<>(typeStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getStatsByClass(Long tenantId) {
|
||||||
|
List<Clazz> classes = classMapper.selectList(new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId));
|
||||||
|
|
||||||
|
List<Map<String, Object>> classStats = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Clazz cls : classes) {
|
||||||
|
// 获取班级学生的任务完成记录(通过 student_class_history 关联)
|
||||||
|
List<StudentClassHistory> histories = studentClassHistoryMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<StudentClassHistory>()
|
||||||
|
.eq(StudentClassHistory::getClassId, cls.getId())
|
||||||
|
.eq(StudentClassHistory::getStatus, "active")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (histories.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> studentIds = histories.stream()
|
||||||
|
.map(StudentClassHistory::getStudentId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (studentIds.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long completions = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.in(TaskCompletion::getStudentId, studentIds));
|
||||||
|
long completed = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.in(TaskCompletion::getStudentId, studentIds).eq(TaskCompletion::getStatus, "completed"));
|
||||||
|
|
||||||
|
int rate = completions > 0 ? (int) ((completed * 100) / completions) : 0;
|
||||||
|
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getMonthlyStats(Long tenantId, Integer months) {
|
||||||
|
months = months != null ? months : 6;
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
LocalDate startDate = now.minusMonths(months - 1).withDayOfMonth(1);
|
||||||
|
|
||||||
|
List<Map<String, Object>> 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<String, Object> data = new HashMap<>();
|
||||||
|
data.put("month", key);
|
||||||
|
data.put("tasks", 0);
|
||||||
|
data.put("completions", 0);
|
||||||
|
data.put("completed", 0);
|
||||||
|
monthlyData.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计每月的任务数
|
||||||
|
List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
|
||||||
|
.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<String, Object> data : monthlyData) {
|
||||||
|
if (data.get("month").equals(month)) {
|
||||||
|
data.put("tasks", (Integer) data.get("tasks") + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计每月的完成情况
|
||||||
|
for (Map<String, Object> 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<TaskCompletion>()
|
||||||
|
.ge(TaskCompletion::getCreatedAt, startOfMonth)
|
||||||
|
.lt(TaskCompletion::getCreatedAt, endOfMonth));
|
||||||
|
|
||||||
|
long completed = taskCompletionMapper.selectCount(new LambdaQueryWrapper<TaskCompletion>()
|
||||||
|
.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<TaskCompletion> getTaskCompletions(Long tenantId, Long taskId, Integer pageNum, Integer pageSize, String status) {
|
||||||
|
Page<TaskCompletion> page = PageUtils.of(pageNum, pageSize);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<TaskCompletion> 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<TaskCompletion>()
|
||||||
|
.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<TaskTemplate> getTemplatePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String type) {
|
||||||
|
Page<TaskTemplate> page = PageUtils.of(pageNum, pageSize);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<TaskTemplate> 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<TaskTemplate>()
|
||||||
|
.eq(TaskTemplate::getType, taskType)
|
||||||
|
.eq(TaskTemplate::getIsPublic, 1)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
|
||||||
|
if (template == null) {
|
||||||
|
template = taskTemplateMapper.selectOne(new LambdaQueryWrapper<TaskTemplate>()
|
||||||
|
.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<StudentClassHistory> histories = studentClassHistoryMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<StudentClassHistory>()
|
||||||
|
.eq(StudentClassHistory::getClassId, targetId)
|
||||||
|
.eq(StudentClassHistory::getStatus, "active")
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Long> 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String, Object> getDashboard(Long teacherId, Long tenantId) {
|
||||||
|
Map<String, Object> dashboard = new HashMap<>();
|
||||||
|
dashboard.put("lessonCount", lessonMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Lesson>()
|
||||||
|
.eq(Lesson::getTeacherId, teacherId)
|
||||||
|
.eq(Lesson::getTenantId, tenantId)));
|
||||||
|
dashboard.put("taskCount", taskMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Task>()
|
||||||
|
.eq(Task::getCreatorId, teacherId)
|
||||||
|
.eq(Task::getTenantId, tenantId)));
|
||||||
|
dashboard.put("growthRecordCount", growthRecordMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<GrowthRecord>()
|
||||||
|
.eq(GrowthRecord::getRecordedBy, teacherId)
|
||||||
|
.eq(GrowthRecord::getTenantId, tenantId)));
|
||||||
|
dashboard.put("unreadNotifications", notificationMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Notification>()
|
||||||
|
.eq(Notification::getTenantId, tenantId)
|
||||||
|
.eq(Notification::getIsRead, 0)));
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getTodayLessons(Long teacherId, Long tenantId) {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LambdaQueryWrapper<Lesson> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Lesson::getTeacherId, teacherId)
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.eq(Lesson::getLessonDate, today)
|
||||||
|
.orderByAsc(Lesson::getStartTime);
|
||||||
|
List<Lesson> lessons = lessonMapper.selectList(wrapper);
|
||||||
|
return lessons.stream().map(l -> {
|
||||||
|
Map<String, Object> 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<Map<String, Object>> getWeeklyLessons(Long teacherId, Long tenantId) {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1);
|
||||||
|
LocalDate weekEnd = weekStart.plusDays(6);
|
||||||
|
LambdaQueryWrapper<Lesson> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Lesson::getTeacherId, teacherId)
|
||||||
|
.eq(Lesson::getTenantId, tenantId)
|
||||||
|
.between(Lesson::getLessonDate, weekStart, weekEnd)
|
||||||
|
.orderByAsc(Lesson::getLessonDate, Lesson::getStartTime);
|
||||||
|
List<Lesson> lessons = lessonMapper.selectList(wrapper);
|
||||||
|
return lessons.stream().map(l -> {
|
||||||
|
Map<String, Object> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Theme> getAllThemes(Boolean enabledOnly) {
|
||||||
|
LambdaQueryWrapper<Theme> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,11 @@ spring:
|
|||||||
enabled: true
|
enabled: true
|
||||||
locations: classpath:db/migration
|
locations: classpath:db/migration
|
||||||
baseline-on-migrate: true
|
baseline-on-migrate: true
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: 8.148.151.56
|
||||||
|
port: 6379
|
||||||
|
database: 0
|
||||||
|
|
||||||
file:
|
file:
|
||||||
upload:
|
upload:
|
||||||
|
|||||||
@ -24,6 +24,11 @@ spring:
|
|||||||
enabled: true
|
enabled: true
|
||||||
locations: classpath:db/migration
|
locations: classpath:db/migration
|
||||||
baseline-on-migrate: true
|
baseline-on-migrate: true
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:redis}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
database: 0
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET:reading-platform-jwt-secret-key-must-be-at-least-256-bits-long}
|
secret: ${JWT_SECRET:reading-platform-jwt-secret-key-must-be-at-least-256-bits-long}
|
||||||
|
|||||||
@ -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='课表计划(增强版)';
|
||||||
346
前端实际调用接口对比.md
Normal file
346
前端实际调用接口对比.md
Normal file
@ -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<Student> importStudents(Long tenantId, List<StudentCreateRequest> 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<Map<String, Object>> getActiveTeachers(Long tenantId, Integer limit)`
|
||||||
|
- `List<Map<String, Object>> getCourseUsageStats(Long tenantId)`
|
||||||
|
- `List<Map<String, Object>> getRecentActivities(Long tenantId, Integer limit)`
|
||||||
|
- `List<Map<String, Object>> getLessonTrend(Long tenantId, Integer months)`
|
||||||
|
- `List<Map<String, Object>> getCourseDistribution(Long tenantId)`
|
||||||
|
- `String formatActivityTitle(Lesson lesson)` - 私有方法,格式化活动标题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最终结论
|
||||||
|
|
||||||
|
**前端实际调用的所有接口已在 Java 后端全部实现。**
|
||||||
228
补全接口修复总结.md
Normal file
228
补全接口修复总结.md
Normal file
@ -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<ClassEntity> classes` → `List<Clazz> 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<String, Object> getTaskStats(Long tenantId);
|
||||||
|
Map<String, Object> getStatsByType(Long tenantId);
|
||||||
|
List<Map<String, Object>> getStatsByClass(Long tenantId);
|
||||||
|
List<Map<String, Object>> getMonthlyStats(Long tenantId, Integer months);
|
||||||
|
|
||||||
|
// 任务完成情况
|
||||||
|
Page<TaskCompletion> getTaskCompletions(Long tenantId, Long taskId, Integer pageNum, Integer pageSize, String status);
|
||||||
|
TaskCompletion updateTaskCompletion(Long tenantId, Long taskId, Long studentId, String status, String feedback);
|
||||||
|
|
||||||
|
// 任务模板
|
||||||
|
Page<TaskTemplate> 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<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate);
|
||||||
|
List<Map<String, Object>> getTimetable(Long tenantId, LocalDate startDate, LocalDate endDate, Long classId);
|
||||||
|
List<SchedulePlan> batchCreateSchedules(Long tenantId, Long userId, List<SchedulePlanCreateRequest> requests);
|
||||||
|
ScheduleTemplate updateScheduleTemplate(Long id, ScheduleTemplate template);
|
||||||
|
List<SchedulePlan> 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
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user