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:
En 2026-03-10 01:06:03 +08:00
parent 70e9683506
commit 583b47c430
64 changed files with 4215 additions and 990 deletions

0
.CurrentUserAllHosts Normal file
View File

55
.github/workflows/api-check.yml vendored Normal file
View 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
View 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
View File

@ -1,144 +1,144 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
本文档为 Claude Code (claude.ai/code) 在本项目中工作时提供指导。
## Project Overview
## 项目概述
This is a **Kindergarten Course Management System** (少儿智慧阅读平台) with a Spring Boot backend and Vue 3 frontend. The system manages courses, lessons, tasks, and student growth records for kindergartens.
这是一个**少儿智慧阅读平台**Kindergarten Course Management System采用 Spring Boot 后端 + Vue 3 前端架构。系统管理幼儿园的课程、课时、任务和学生成长记录。
## Architecture
## 技术架构
### Backend (`reading-platform-java`)
- **Framework**: Spring Boot 3.2.3 + Java 17
- **Persistence**: MyBatis-Plus 3.5.5
- **Security**: Spring Security + JWT
- **API Docs**: Knife4j (Swagger OpenAPI 3)
- **Database**: MySQL 8.0
- **Migration**: Flyway
### 后端 (`reading-platform-java`)
- **框架**: Spring Boot 3.2.3 + Java 17
- **持久层**: MyBatis-Plus 3.5.5
- **安全**: Spring Security + JWT
- **API 文档**: Knife4j (Swagger OpenAPI 3)
- **数据库**: MySQL 8.0
- **数据库迁移**: Flyway
### Frontend (`reading-platform-frontend`)
- **Framework**: Vue 3 + TypeScript + Vite
- **UI**: Ant Design Vue
- **State**: Pinia
- **API**: Axios with auto-generated TypeScript clients via Orval
### 前端 (`reading-platform-frontend`)
- **框架**: Vue 3 + TypeScript + Vite
- **UI 组件库**: Ant Design Vue
- **状态管理**: Pinia
- **API**: Axios + Orval 自动生成的 TypeScript 客户端
## Multi-Tenant Architecture
## 多租户架构
The system supports multiple kindergartens (tenants):
- `admin` role: Super admin (no tenant, manages system-wide courses)
- `school` role: School administrator (manages school's teachers, students, classes)
- `teacher` role: Teacher (manages lessons, tasks for their tenant)
- `parent` role: Parent (views child's progress and tasks)
系统支持多个幼儿园(租户):
- `admin` 角色:超级管理员(无租户,管理全系统课程)
- `school` 角色:学校管理员(管理本校的教师、学生、班级)
- `teacher` 角色:教师(管理本校的课时和任务)
- `parent` 角色:家长(查看孩子的进度和任务)
Each entity (except `admin_users`) has a `tenant_id` field. System courses have `tenant_id = NULL`.
`admin_users` 外,每个实体都有 `tenant_id` 字段。系统课程的 `tenant_id = NULL`
## Project Structure
## 项目结构
```
kindergarten_java/
├── reading-platform-java/ # Spring Boot backend
├── reading-platform-java/ # Spring Boot 后端
│ ├── src/main/java/.../controller/
│ │ ├── admin/ # Super admin endpoints (/api/v1/admin/*)
│ │ ├── school/ # School admin endpoints (/api/v1/school/*)
│ │ ├── teacher/ # Teacher endpoints (/api/v1/teacher/*)
│ │ └── parent/ # Parent endpoints (/api/v1/parent/*)
│ ├── entity/ # Database entities (27 tables)
│ ├── mapper/ # MyBatis-Plus mappers
│ ├── service/ # Service layer interface + impl
│ │ ├── admin/ # 超级管理员端点 (/api/v1/admin/*)
│ │ ├── school/ # 学校管理员端点 (/api/v1/school/*)
│ │ ├── teacher/ # 教师端点 (/api/v1/teacher/*)
│ │ └── parent/ # 家长端点 (/api/v1/parent/*)
│ ├── entity/ # 数据库实体27张表
│ ├── mapper/ # MyBatis-Plus 映射器
│ ├── service/ # 服务层接口 + 实现
│ ├── common/
│ │ ├── annotation/RequireRole # Role-based access control
│ │ ├── security/ # JWT authentication
│ │ ├── enums/ # UserRole, CourseStatus, etc.
│ │ ├── annotation/RequireRole # 基于角色的访问控制
│ │ ├── security/ # JWT 认证
│ │ ├── enums/ # UserRole, CourseStatus 等枚举
│ │ ├── response/ # Result<T>, PageResult<T>
│ │ └── config/ # Security, MyBatis, OpenAPI configs
│ │ └── config/ # Security, MyBatis, OpenAPI 配置
│ └── resources/
│ ├── db/migration/ # Flyway migration scripts
│ └── mapper/ # MyBatis XML files
│ ├── db/migration/ # Flyway 迁移脚本
│ └── mapper/ # MyBatis XML 文件
├── reading-platform-frontend/ # Vue 3 frontend
├── reading-platform-frontend/ # Vue 3 前端
│ ├── src/views/
│ │ ├── admin/ # Super admin pages
│ │ ├── school/ # School admin pages
│ │ ├── teacher/ # Teacher pages
│ │ └── parent/ # Parent pages
│ ├── api/generated/ # Auto-generated API clients
│ ├── api-spec.yml # OpenAPI specification
│ └── router/index.ts # Vue Router config
│ │ ├── admin/ # 超级管理员页面
│ │ ├── school/ # 学校管理员页面
│ │ ├── teacher/ # 教师页面
│ │ └── parent/ # 家长页面
│ ├── api/generated/ # 自动生成的 API 客户端
│ ├── api-spec.yml # OpenAPI 规范
│ └── router/index.ts # Vue Router 配置
├── docker-compose.yml # Backend + Frontend services
└── docs/开发协作指南.md # Development guide (Chinese)
├── docker-compose.yml # 后端 + 前端服务
└── docs/开发协作指南.md # 开发指南(中文)
```
## Key Patterns
## 关键模式
### 1. Role-Based Access Control
Use `@RequireRole` annotation on controllers/services:
### 1. 基于角色的访问控制
在 Controller/Service 上使用 `@RequireRole` 注解:
```java
@RequireRole(UserRole.SCHOOL) // Only school admins can access
@RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问
```
### 2. Tenant Isolation
Use `SecurityUtils.getCurrentTenantId()` in school/teacher/parent endpoints to filter data by current tenant.
### 2. 租户隔离
在学校/教师/家长端点中使用 `SecurityUtils.getCurrentTenantId()` 按当前租户过滤数据。
### 3. Unified Response Format
### 3. 统一响应格式
```java
Result<T> success(T data) // { code: 200, message: "success", data: ... }
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
```
### 4. OpenAPI-Driven Development
- Backend: Annotate controllers with `@Operation`, `@Parameter`, `@Schema`
- Frontend: Run `npm run api:update` to regenerate TypeScript clients from `api-spec.yml`
### 4. OpenAPI 驱动开发
- 后端:在 Controller 上使用 `@Operation`、`@Parameter`、`@Schema` 注解
- 前端:运行 `npm run api:update``api-spec.yml` 重新生成 TypeScript 客户端
## Development Commands
## 开发命令
### Backend
### 后端
```bash
# Run with Docker Compose (recommended)
# 使用 Docker Compose 运行(推荐)
docker compose up --build
# Run locally (requires MySQL running)
# 本地运行(需要 MySQL 已启动)
cd reading-platform-java
mvn spring-boot:run
# Build
# 构建
mvn clean package -DskipTests
```
### Frontend
### 前端
```bash
cd reading-platform-frontend
npm install
npm run dev
npm run build
# Update API clients from backend spec
# 从后端规范更新 API 客户端
npm run api:update
```
### Database Migration
- Add new migration scripts to `reading-platform-java/src/main/resources/db/migration/V{n}__description.sql`
- Flyway runs automatically on backend startup (dev mode only)
### 数据库迁移
- 将新的迁移脚本添加到 `reading-platform-java/src/main/resources/db/migration/V{n}__description.sql`
- Flyway 会在后端启动时自动运行(仅开发模式)
## Database Schema (27 Tables)
- **Tenant**: tenants, tenant_courses
- **Users**: admin_users, teachers, students, parents, parent_students
- **Class**: classes, class_teachers, student_class_history
- **Course**: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities
- **Lesson**: lessons, lesson_feedbacks, student_records
- **Task**: tasks, task_targets, task_completions, task_templates
- **Growth**: growth_records
- **Resource**: resource_libraries, resource_items
- **Schedule**: schedule_plans, schedule_templates
- **System**: system_settings, notifications, operation_logs, tags
## 数据库表结构27张表
- **租户**: tenants, tenant_courses
- **用户**: admin_users, teachers, students, parents, parent_students
- **班级**: classes, class_teachers, student_class_history
- **课程**: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities
- **课时**: lessons, lesson_feedbacks, student_records
- **任务**: tasks, task_targets, task_completions, task_templates
- **成长**: growth_records
- **资源**: resource_libraries, resource_items
- **日程**: schedule_plans, schedule_templates
- **系统**: system_settings, notifications, operation_logs, tags
## Test Accounts
| Role | Username | Password |
|------|----------|----------|
| Admin | admin | admin123 |
| School | school | 123456 |
| Teacher | teacher1 | 123456 |
| Parent | parent1 | 123456 |
## 测试账号
| 角色 | 用户名 | 密码 |
|------|--------|------|
| 管理员 | admin | admin123 |
| 学校 | school | 123456 |
| 教师 | teacher1 | 123456 |
| 家长 | parent1 | 123456 |
## API Documentation
- Access: http://localhost:8080/doc.html (after backend starts)
## API 文档
- 访问地址http://localhost:8080/doc.html后端启动后

132
Service 重构总结.md Normal file
View 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 模式

View File

@ -11,26 +11,15 @@ declare module 'vue' {
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
@ -44,37 +33,22 @@ declare module 'vue' {
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARate: typeof import('ant-design-vue/es')['Rate']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default']

View File

@ -46,6 +46,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>

View File

@ -1,5 +1,6 @@
package com.reading.platform.common.security;
import com.reading.platform.service.TokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -25,6 +26,7 @@ import java.util.Collections;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
@ -32,6 +34,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
try {
String token = resolveToken(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
// 验证 Redis 中是否存在该 token
if (!tokenService.isTokenExist(token)) {
log.warn("Token not found in Redis, possibly invalidated: {}", token);
filterChain.doFilter(request, response);
return;
}
JwtPayload payload = jwtTokenProvider.getPayloadFromToken(token);
UsernamePasswordAuthenticationToken authentication =

View File

@ -7,8 +7,10 @@ import com.reading.platform.dto.response.UserInfoResponse;
import com.reading.platform.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@Tag(name = "认证", description = "认证相关接口")
@ -27,8 +29,9 @@ public class AuthController {
@Operation(summary = "用户登出")
@PostMapping("/logout")
public Result<Void> logout() {
// JWT is stateless - client simply discards the token
public Result<Void> logout(HttpServletRequest request) {
String token = resolveToken(request);
authService.logout(token);
return Result.success();
}
@ -47,4 +50,12 @@ public class AuthController {
return Result.success();
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

View File

@ -77,4 +77,18 @@ public class SchoolClassController {
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();
}
}

View File

@ -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"));
}
}

View File

@ -6,14 +6,22 @@ import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.request.SchedulePlanCreateRequest;
import com.reading.platform.dto.request.ScheduleTemplateApplyRequest;
import com.reading.platform.entity.SchedulePlan;
import com.reading.platform.entity.ScheduleTemplate;
import com.reading.platform.service.ScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Tag(name = "学校 - 课表", description = "课表管理接口(学校管理员专用)")
@RestController
@RequestMapping("/api/v1/school/schedules")
@ -28,10 +36,22 @@ public class SchoolScheduleController {
public Result<PageResult<SchedulePlan>> getSchedulePlans(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) Long classId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<SchedulePlan> page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId, startDate, endDate);
return Result.success(PageResult.of(page));
}
@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();
Page<SchedulePlan> page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId);
return Result.success(PageResult.of(page));
return Result.success(scheduleService.getTimetable(tenantId, startDate, endDate, classId));
}
@Operation(summary = "根据 ID 获取课表计划")
@ -42,9 +62,10 @@ public class SchoolScheduleController {
@Operation(summary = "创建课表计划")
@PostMapping
public Result<SchedulePlan> createSchedulePlan(@RequestBody SchedulePlan plan) {
public Result<SchedulePlan> createSchedulePlan(@Valid @RequestBody SchedulePlanCreateRequest plan) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(scheduleService.createSchedulePlan(tenantId, plan));
Long userId = SecurityUtils.getCurrentUserId();
return Result.success(scheduleService.createSchedulePlan(tenantId, userId, plan));
}
@Operation(summary = "更新课表计划")
@ -60,6 +81,16 @@ public class SchoolScheduleController {
return Result.success();
}
@Operation(summary = "批量创建排课")
@PostMapping("/batch")
public Result<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 = "获取课表模板")
@GetMapping("/templates")
public Result<PageResult<ScheduleTemplate>> getScheduleTemplates(
@ -70,6 +101,12 @@ public class SchoolScheduleController {
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 = "创建课表模板")
@PostMapping("/templates")
public Result<ScheduleTemplate> createScheduleTemplate(@RequestBody ScheduleTemplate template) {
@ -77,10 +114,25 @@ public class SchoolScheduleController {
return Result.success(scheduleService.createScheduleTemplate(tenantId, template));
}
@Operation(summary = "更新课表模板")
@PutMapping("/templates/{id}")
public Result<ScheduleTemplate> updateScheduleTemplate(@PathVariable Long id, @RequestBody ScheduleTemplate template) {
return Result.success(scheduleService.updateScheduleTemplate(id, template));
}
@Operation(summary = "删除课表模板")
@DeleteMapping("/templates/{id}")
public Result<Void> deleteScheduleTemplate(@PathVariable Long id) {
scheduleService.deleteScheduleTemplate(id);
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));
}
}

View File

@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "学校 - 统计", description = "学校统计仪表盘接口(学校管理员专用)")
@ -27,4 +28,42 @@ public class SchoolStatsController {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getStats(tenantId));
}
@Operation(summary = "获取活跃教师统计")
@GetMapping("/teachers")
public Result<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));
}
}

View File

@ -14,6 +14,10 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Tag(name = "学校 - 学生", description = "学生管理接口(学校管理员专用)")
@RestController
@RequestMapping("/api/v1/school/students")
@ -61,4 +65,20 @@ public class SchoolStudentController {
return Result.success();
}
@Operation(summary = "批量导入学生")
@PostMapping("/import")
public Result<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);
}
}

View File

@ -4,9 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.request.TaskCreateRequest;
import com.reading.platform.dto.request.TaskUpdateRequest;
import com.reading.platform.dto.request.*;
import com.reading.platform.entity.Task;
import com.reading.platform.entity.TaskCompletion;
import com.reading.platform.entity.TaskTemplate;
import com.reading.platform.service.TaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -14,6 +15,9 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "学校 - 任务", description = "任务管理接口(学校管理员专用)")
@RestController
@RequestMapping("/api/v1/school/tasks")
@ -63,4 +67,120 @@ public class SchoolTaskController {
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));
}
}

View File

@ -4,9 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.request.TaskCreateRequest;
import com.reading.platform.dto.request.TaskUpdateRequest;
import com.reading.platform.dto.request.*;
import com.reading.platform.entity.Task;
import com.reading.platform.entity.TaskCompletion;
import com.reading.platform.entity.TaskTemplate;
import com.reading.platform.service.TaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -14,6 +15,9 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "教师 - 任务", description = "任务接口(教师专用)")
@RestController
@RequestMapping("/api/v1/teacher/tasks")
@ -62,4 +66,94 @@ public class TeacherTaskController {
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));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -5,6 +5,7 @@ import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 课表计划实体
@ -22,10 +23,26 @@ public class SchedulePlan {
private Long classId;
private Long courseId;
private Long teacherId;
private Integer dayOfWeek;
private Integer period;
private LocalTime startTime;
private LocalTime endTime;
private LocalDate startDate;
private LocalDate endDate;
private String location;
private String note;
private String status;
@TableField(fill = FieldFill.INSERT)

View File

@ -1,148 +1,35 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.*;
import com.reading.platform.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AdminStatsService {
/**
* 管理员统计服务接口
*/
public interface AdminStatsService {
private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final CourseMapper courseMapper;
private final LessonMapper lessonMapper;
/**
* 获取整体统计数据
*/
Map<String, Object> getStats();
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);
long publishedCourseCount = courseMapper.selectCount(
new LambdaQueryWrapper<Course>().eq(Course::getStatus, "published"));
/**
* 获取活跃租户
*/
List<Map<String, Object>> getActiveTenants(int limit);
// Monthly lessons (current month)
LocalDate monthStart = LocalDate.now().withDayOfMonth(1);
LocalDate monthEnd = LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth());
long monthlyLessons = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.ge(Lesson::getLessonDate, monthStart)
.le(Lesson::getLessonDate, monthEnd));
/**
* 获取热门课程
*/
List<Map<String, Object>> getPopularCourses(int limit);
stats.put("tenantCount", tenantCount);
stats.put("activeTenantCount", activeTenantCount);
stats.put("teacherCount", teacherMapper.selectCount(null));
stats.put("studentCount", studentMapper.selectCount(null));
stats.put("courseCount", courseCount);
stats.put("publishedCourseCount", publishedCourseCount);
stats.put("lessonCount", lessonMapper.selectCount(null));
stats.put("monthlyLessons", monthlyLessons);
return stats;
}
public List<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());
}
/**
* 获取最近活动
*/
List<Map<String, Object>> getRecentActivities(int limit);
}

View File

@ -15,4 +15,10 @@ public interface AuthService {
void changePassword(String oldPassword, String newPassword);
/**
* 登出 - 删除 Redis 中的 Token
* @param token JWT token
*/
void logout(String token);
}

View File

@ -26,6 +26,10 @@ public interface ClassService {
void assignStudents(Long classId, List<Long> studentIds);
void removeTeacher(Long classId, Long teacherId);
void removeStudent(Long classId, Long studentId);
List<Long> getTeacherIdsByClassId(Long classId);
}

View File

@ -1,49 +1,36 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.mapper.CourseLessonMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CourseLessonService {
/**
* 课程课时服务接口
*/
public interface CourseLessonService {
private final CourseLessonMapper courseLessonMapper;
/**
* 根据课程 ID 获取课时列表
*/
List<CourseLesson> getLessonsByCourse(Long courseId);
public List<CourseLesson> getLessonsByCourse(Long courseId) {
LambdaQueryWrapper<CourseLesson> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CourseLesson::getCourseId, courseId)
.orderByAsc(CourseLesson::getSortOrder);
return courseLessonMapper.selectList(wrapper);
}
/**
* 根据 ID 获取课时
*/
CourseLesson getLessonById(Long id);
public CourseLesson getLessonById(Long id) {
CourseLesson lesson = courseLessonMapper.selectById(id);
if (lesson == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course lesson not found");
}
return lesson;
}
/**
* 创建课时
*/
CourseLesson createLesson(CourseLesson lesson);
public CourseLesson createLesson(CourseLesson lesson) {
courseLessonMapper.insert(lesson);
return lesson;
}
/**
* 更新课时
*/
CourseLesson updateLesson(Long id, CourseLesson lesson);
public CourseLesson updateLesson(Long id, CourseLesson lesson) {
getLessonById(id);
lesson.setId(id);
courseLessonMapper.updateById(lesson);
return courseLessonMapper.selectById(id);
}
public void deleteLesson(Long id) {
courseLessonMapper.deleteById(id);
}
/**
* 删除课时
*/
void deleteLesson(Long id);
}

View File

@ -1,78 +1,55 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.mapper.CoursePackageMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CoursePackageService {
/**
* 课程包服务接口
*/
public interface CoursePackageService {
private final CoursePackageMapper coursePackageMapper;
/**
* 获取课程包分页
*/
Page<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);
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);
}
/**
* 根据 ID 获取课程包
*/
CoursePackage getPackageById(Long id);
public CoursePackage getPackageById(Long id) {
CoursePackage pkg = coursePackageMapper.selectById(id);
if (pkg == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course package not found");
}
return pkg;
}
/**
* 创建课程包
*/
CoursePackage createPackage(CoursePackage pkg);
public CoursePackage createPackage(CoursePackage pkg) {
coursePackageMapper.insert(pkg);
return pkg;
}
/**
* 更新课程包
*/
CoursePackage updatePackage(Long id, CoursePackage pkg);
public CoursePackage updatePackage(Long id, CoursePackage pkg) {
getPackageById(id);
pkg.setId(id);
coursePackageMapper.updateById(pkg);
return coursePackageMapper.selectById(id);
}
/**
* 删除课程包
*/
void deletePackage(Long id);
public void deletePackage(Long id) {
coursePackageMapper.deleteById(id);
}
/**
* 提交审核
*/
void submitPackage(Long id);
public void submitPackage(Long id) {
CoursePackage pkg = getPackageById(id);
pkg.setStatus("pending");
coursePackageMapper.updateById(pkg);
}
/**
* 审核课程包
*/
void reviewPackage(Long id, boolean approved, String comment);
public void reviewPackage(Long id, boolean approved, String comment) {
CoursePackage pkg = getPackageById(id);
pkg.setStatus(approved ? "published" : "rejected");
coursePackageMapper.updateById(pkg);
}
/**
* 发布课程包
*/
void publishPackage(Long id);
public void publishPackage(Long id) {
CoursePackage pkg = getPackageById(id);
pkg.setStatus("published");
coursePackageMapper.updateById(pkg);
}
public void offlinePackage(Long id) {
CoursePackage pkg = getPackageById(id);
pkg.setStatus("archived");
coursePackageMapper.updateById(pkg);
}
/**
* 下架课程包
*/
void offlinePackage(Long id);
}

View File

@ -1,141 +1,29 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.*;
import com.reading.platform.mapper.*;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ExportService {
/**
* 导出服务接口
*/
public interface ExportService {
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final LessonMapper lessonMapper;
private final GrowthRecordMapper growthRecordMapper;
/**
* 导出教师数据
*/
byte[] exportTeachers(Long tenantId) throws IOException;
public byte[] exportTeachers(Long tenantId) throws IOException {
LambdaQueryWrapper<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());
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();
}
/**
* 导出成长档案
*/
byte[] exportGrowthRecords(Long tenantId) throws IOException;
}

View File

@ -1,71 +1,19 @@
package com.reading.platform.service;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* 文件上传服务接口
*/
public interface FileUploadService {
@Slf4j
@Service
public class FileUploadService {
/**
* 上传文件
*/
String uploadFile(MultipartFile file);
@Value("${file.upload.path:/app/uploads/}")
private String uploadPath;
@Value("${file.upload.base-url:/uploads/}")
private String baseUrl;
public String uploadFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "File cannot be empty");
}
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String newFilename = UUID.randomUUID().toString().replace("-", "") + extension;
String relativePath = datePath + "/" + newFilename;
String fullPath = uploadPath + relativePath;
try {
Path targetPath = Paths.get(fullPath);
Files.createDirectories(targetPath.getParent());
file.transferTo(targetPath.toFile());
log.info("File uploaded: {}", fullPath);
return baseUrl + relativePath;
} catch (IOException e) {
log.error("File upload failed", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "File upload failed: " + e.getMessage());
}
}
public void deleteFile(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return;
}
String relativePath = filePath.startsWith(baseUrl) ? filePath.substring(baseUrl.length()) : filePath;
String fullPath = uploadPath + relativePath;
File file = new File(fullPath);
if (file.exists()) {
boolean deleted = file.delete();
if (!deleted) {
log.warn("Failed to delete file: {}", fullPath);
}
}
}
/**
* 删除文件
*/
void deleteFile(String filePath);
}

View File

@ -1,48 +1,20 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.entity.OperationLog;
import com.reading.platform.mapper.OperationLogMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OperationLogService {
/**
* 操作日志服务接口
*/
public interface OperationLogService {
private final OperationLogMapper operationLogMapper;
/**
* 获取操作日志分页
*/
Page<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) {
wrapper.eq(OperationLog::getTenantId, tenantId);
}
if (module != null && !module.isEmpty()) {
wrapper.eq(OperationLog::getModule, module);
}
wrapper.orderByDesc(OperationLog::getCreatedAt);
return operationLogMapper.selectPage(page, wrapper);
}
public void log(String action, String module, String targetType, Long targetId, String details) {
try {
OperationLog log = new OperationLog();
log.setAction(action);
log.setModule(module);
log.setTargetType(targetType);
log.setTargetId(targetId);
log.setDetails(details);
try {
log.setUserId(SecurityUtils.getCurrentUserId());
log.setUserRole(SecurityUtils.getCurrentRole());
log.setTenantId(SecurityUtils.getCurrentTenantId());
} catch (Exception ignored) {}
operationLogMapper.insert(log);
} catch (Exception e) {
// Log silently - don't fail main operations
}
}
/**
* 记录操作日志
*/
void log(String action, String module, String targetType, Long targetId, String details);
}

View File

@ -1,92 +1,63 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.entity.ResourceItem;
import com.reading.platform.entity.ResourceLibrary;
import com.reading.platform.mapper.ResourceItemMapper;
import com.reading.platform.mapper.ResourceLibraryMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ResourceService {
/**
* 资源服务接口
*/
public interface ResourceService {
private final ResourceLibraryMapper resourceLibraryMapper;
private final ResourceItemMapper resourceItemMapper;
/**
* 获取资源库列表
*/
List<ResourceLibrary> getLibraries(Long tenantId);
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);
}
/**
* 根据 ID 获取资源库
*/
ResourceLibrary getLibraryById(Long id);
public ResourceLibrary getLibraryById(Long id) {
ResourceLibrary lib = resourceLibraryMapper.selectById(id);
if (lib == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource library not found");
}
return lib;
}
/**
* 创建资源库
*/
ResourceLibrary createLibrary(ResourceLibrary library);
public ResourceLibrary createLibrary(ResourceLibrary library) {
resourceLibraryMapper.insert(library);
return library;
}
/**
* 更新资源库
*/
ResourceLibrary updateLibrary(Long id, ResourceLibrary library);
public ResourceLibrary updateLibrary(Long id, ResourceLibrary library) {
getLibraryById(id);
library.setId(id);
resourceLibraryMapper.updateById(library);
return resourceLibraryMapper.selectById(id);
}
/**
* 删除资源库
*/
void deleteLibrary(Long id);
public void deleteLibrary(Long id) {
resourceLibraryMapper.deleteById(id);
}
/**
* 获取资源项分页
*/
Page<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);
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);
}
/**
* 根据 ID 获取资源项
*/
ResourceItem getItemById(Long id);
public ResourceItem getItemById(Long id) {
ResourceItem item = resourceItemMapper.selectById(id);
if (item == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Resource item not found");
}
return item;
}
/**
* 创建资源项
*/
ResourceItem createItem(ResourceItem item);
public ResourceItem createItem(ResourceItem item) {
resourceItemMapper.insert(item);
return item;
}
/**
* 更新资源项
*/
ResourceItem updateItem(Long id, ResourceItem item);
public ResourceItem updateItem(Long id, ResourceItem item) {
getItemById(id);
item.setId(id);
resourceItemMapper.updateById(item);
return resourceItemMapper.selectById(id);
}
public void deleteItem(Long id) {
resourceItemMapper.deleteById(id);
}
/**
* 删除资源项
*/
void deleteItem(Long id);
}

View File

@ -1,75 +1,92 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.dto.request.SchedulePlanCreateRequest;
import com.reading.platform.dto.request.ScheduleTemplateApplyRequest;
import com.reading.platform.entity.SchedulePlan;
import com.reading.platform.entity.ScheduleTemplate;
import com.reading.platform.mapper.SchedulePlanMapper;
import com.reading.platform.mapper.ScheduleTemplateMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ScheduleService {
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
private final SchedulePlanMapper schedulePlanMapper;
private final ScheduleTemplateMapper scheduleTemplateMapper;
/**
* 课表服务接口
*/
public interface ScheduleService {
public Page<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);
}
/**
* 获取课表计划分页
*/
Page<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId);
public SchedulePlan getSchedulePlanById(Long id) {
SchedulePlan plan = schedulePlanMapper.selectById(id);
if (plan == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Schedule plan not found");
}
return plan;
}
/**
* 获取课表计划分页按日期范围
*/
Page<SchedulePlan> getSchedulePlans(int pageNum, int pageSize, Long tenantId, Long classId, LocalDate startDate, LocalDate endDate);
public SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan) {
plan.setTenantId(tenantId);
schedulePlanMapper.insert(plan);
return plan;
}
/**
* 根据 ID 获取课表计划
*/
SchedulePlan getSchedulePlanById(Long id);
public SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan) {
SchedulePlan existing = getSchedulePlanById(id);
plan.setId(id);
plan.setTenantId(existing.getTenantId());
schedulePlanMapper.updateById(plan);
return schedulePlanMapper.selectById(id);
}
/**
* 创建课表计划
*/
SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan);
public void deleteSchedulePlan(Long id) {
schedulePlanMapper.deleteById(id);
}
/**
* 创建课表计划
*/
SchedulePlan createSchedulePlan(Long tenantId, Long userId, SchedulePlanCreateRequest request);
public Page<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);
}
/**
* 更新课表计划
*/
SchedulePlan updateSchedulePlan(Long id, SchedulePlan plan);
public ScheduleTemplate createScheduleTemplate(Long tenantId, ScheduleTemplate template) {
template.setTenantId(tenantId);
scheduleTemplateMapper.insert(template);
return template;
}
/**
* 删除课表计划
*/
void deleteSchedulePlan(Long id);
public void deleteScheduleTemplate(Long id) {
scheduleTemplateMapper.deleteById(id);
}
/**
* 批量创建课表计划
*/
List<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);
}

View File

@ -1,55 +1,35 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.entity.SchoolCourse;
import com.reading.platform.mapper.SchoolCourseMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class SchoolCourseService {
/**
* 校本课程服务接口
*/
public interface SchoolCourseService {
private final SchoolCourseMapper schoolCourseMapper;
/**
* 获取校本课程分页
*/
Page<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);
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);
}
/**
* 根据 ID 获取校本课程
*/
SchoolCourse getCourseById(Long id);
public SchoolCourse getCourseById(Long id) {
SchoolCourse course = schoolCourseMapper.selectById(id);
if (course == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "School course not found");
}
return course;
}
/**
* 创建校本课程
*/
SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course);
public SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course) {
course.setTenantId(tenantId);
course.setCreatedBy(userId);
schoolCourseMapper.insert(course);
return course;
}
/**
* 更新校本课程
*/
SchoolCourse updateCourse(Long id, SchoolCourse course);
public SchoolCourse updateCourse(Long id, SchoolCourse course) {
SchoolCourse existing = getCourseById(id);
course.setId(id);
course.setTenantId(existing.getTenantId());
schoolCourseMapper.updateById(course);
return schoolCourseMapper.selectById(id);
}
public void deleteCourse(Long id) {
schoolCourseMapper.deleteById(id);
}
/**
* 删除校本课程
*/
void deleteCourse(Long id);
}

View File

@ -1,33 +1,41 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.*;
import com.reading.platform.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class SchoolStatsService {
/**
* 学校统计服务接口
*/
public interface SchoolStatsService {
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final ClazzMapper clazzMapper;
private final LessonMapper lessonMapper;
/**
* 获取学校统计数据
*/
Map<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;
}
}

View File

@ -26,4 +26,6 @@ public interface StudentService {
List<Student> getStudentsByParentId(Long parentId);
List<Student> importStudents(Long tenantId, List<StudentCreateRequest> requests);
}

View File

@ -1,61 +1,24 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.reading.platform.entity.SystemSetting;
import com.reading.platform.mapper.SystemSettingMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class SystemSettingService {
/**
* 系统设置服务接口
*/
public interface SystemSettingService {
private final SystemSettingMapper systemSettingMapper;
/**
* 获取系统设置
*/
Map<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);
Map<String, String> result = new HashMap<>();
for (SystemSetting s : settings) {
result.put(s.getSettingKey(), s.getSettingValue());
}
return result;
}
/**
* 更新系统设置
*/
void updateSettings(Long tenantId, Map<String, String> settings);
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);
}
}
}
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;
}
/**
* 获取单个设置项
*/
String getSetting(Long tenantId, String key);
}

View File

@ -1,11 +1,13 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.dto.request.TaskCreateRequest;
import com.reading.platform.dto.request.TaskUpdateRequest;
import com.reading.platform.dto.request.*;
import com.reading.platform.entity.Task;
import com.reading.platform.entity.TaskCompletion;
import com.reading.platform.entity.TaskTemplate;
import java.util.List;
import java.util.Map;
/**
* Task Service Interface
@ -28,4 +30,73 @@ public interface TaskService {
List<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);
}

View File

@ -1,86 +1,25 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.*;
import com.reading.platform.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TeacherDashboardService {
/**
* 教师仪表板服务接口
*/
public interface TeacherDashboardService {
private final LessonMapper lessonMapper;
private final TaskMapper taskMapper;
private final GrowthRecordMapper growthRecordMapper;
private final NotificationMapper notificationMapper;
/**
* 获取仪表板数据
*/
Map<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>()
.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)
.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());
}
/**
* 获取今天的课时
*/
List<Map<String, Object>> getTodayLessons(Long teacherId, Long tenantId);
/**
* 获取本周的课时
*/
List<Map<String, Object>> getWeeklyLessons(Long teacherId, Long tenantId);
}

View File

@ -9,7 +9,7 @@ import com.reading.platform.entity.Tenant;
import java.util.List;
/**
* Tenant Service Interface
* 租户服务接口
*/
public interface TenantService {

View File

@ -1,51 +1,36 @@
package com.reading.platform.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.entity.Theme;
import com.reading.platform.mapper.ThemeMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ThemeService {
/**
* 主题服务接口
*/
public interface ThemeService {
private final ThemeMapper themeMapper;
/**
* 获取所有主题
*/
List<Theme> getAllThemes(Boolean enabledOnly);
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);
}
/**
* 根据 ID 获取主题
*/
Theme getThemeById(Long id);
public Theme getThemeById(Long id) {
Theme theme = themeMapper.selectById(id);
if (theme == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Theme not found");
}
return theme;
}
/**
* 创建主题
*/
Theme createTheme(Theme theme);
public Theme createTheme(Theme theme) {
themeMapper.insert(theme);
return theme;
}
/**
* 更新主题
*/
Theme updateTheme(Long id, Theme theme);
public Theme updateTheme(Long id, Theme theme) {
getThemeById(id);
theme.setId(id);
themeMapper.updateById(theme);
return themeMapper.selectById(id);
}
public void deleteTheme(Long id) {
themeMapper.deleteById(id);
}
/**
* 删除主题
*/
void deleteTheme(Long id);
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -17,6 +17,7 @@ import com.reading.platform.mapper.AdminUserMapper;
import com.reading.platform.mapper.ParentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.service.AuthService;
import com.reading.platform.service.TokenService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -34,6 +35,7 @@ public class AuthServiceImpl implements AuthService {
private final ParentMapper parentMapper;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
private final TokenService tokenService;
@Override
public LoginResponse login(LoginRequest request) {
@ -69,8 +71,11 @@ public class AuthServiceImpl implements AuthService {
.name(adminUser.getName())
.build();
String token = jwtTokenProvider.generateToken(payload);
tokenService.saveToken(token, payload);
return LoginResponse.builder()
.token(jwtTokenProvider.generateToken(payload))
.token(token)
.userId(adminUser.getId())
.username(adminUser.getUsername())
.name(adminUser.getName())
@ -101,8 +106,11 @@ public class AuthServiceImpl implements AuthService {
.name(teacher.getName())
.build();
String token = jwtTokenProvider.generateToken(payload);
tokenService.saveToken(token, payload);
return LoginResponse.builder()
.token(jwtTokenProvider.generateToken(payload))
.token(token)
.userId(teacher.getId())
.username(teacher.getUsername())
.name(teacher.getName())
@ -133,8 +141,11 @@ public class AuthServiceImpl implements AuthService {
.name(parent.getName())
.build();
String token = jwtTokenProvider.generateToken(payload);
tokenService.saveToken(token, payload);
return LoginResponse.builder()
.token(jwtTokenProvider.generateToken(payload))
.token(token)
.userId(parent.getId())
.username(parent.getUsername())
.name(parent.getName())
@ -171,8 +182,11 @@ public class AuthServiceImpl implements AuthService {
.name(adminUser.getName())
.build();
String token = jwtTokenProvider.generateToken(payload);
tokenService.saveToken(token, payload);
return LoginResponse.builder()
.token(jwtTokenProvider.generateToken(payload))
.token(token)
.userId(adminUser.getId())
.username(adminUser.getUsername())
.name(adminUser.getName())
@ -202,8 +216,11 @@ public class AuthServiceImpl implements AuthService {
.name(teacher.getName())
.build();
String token = jwtTokenProvider.generateToken(payload);
tokenService.saveToken(token, payload);
return LoginResponse.builder()
.token(jwtTokenProvider.generateToken(payload))
.token(token)
.userId(teacher.getId())
.username(teacher.getUsername())
.name(teacher.getName())
@ -232,8 +249,11 @@ public class AuthServiceImpl implements AuthService {
.name(parent.getName())
.build();
String token = jwtTokenProvider.generateToken(payload);
tokenService.saveToken(token, payload);
return LoginResponse.builder()
.token(jwtTokenProvider.generateToken(payload))
.token(token)
.userId(parent.getId())
.username(parent.getUsername())
.name(parent.getName())
@ -329,4 +349,12 @@ public class AuthServiceImpl implements AuthService {
}
}
@Override
public void logout(String token) {
if (token != null && !token.isEmpty()) {
tokenService.removeToken(token);
log.info("User logged out, token removed from Redis");
}
}
}

View File

@ -172,4 +172,40 @@ public class ClassServiceImpl implements ClassService {
return teacherIds;
}
@Override
@Transactional
public void removeTeacher(Long classId, Long teacherId) {
// Verify class exists
getClassById(classId);
// Delete teacher assignment
classTeacherMapper.delete(
new LambdaQueryWrapper<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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 + "";
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -5,23 +5,18 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.common.util.PageUtils;
import com.reading.platform.dto.request.TaskCreateRequest;
import com.reading.platform.dto.request.TaskUpdateRequest;
import com.reading.platform.entity.Task;
import com.reading.platform.entity.TaskCompletion;
import com.reading.platform.entity.TaskTarget;
import com.reading.platform.mapper.TaskCompletionMapper;
import com.reading.platform.mapper.TaskMapper;
import com.reading.platform.mapper.TaskTargetMapper;
import com.reading.platform.dto.request.*;
import com.reading.platform.entity.*;
import com.reading.platform.mapper.*;
import com.reading.platform.service.TaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
@Service
@RequiredArgsConstructor
@ -30,6 +25,9 @@ public class TaskServiceImpl implements TaskService {
private final TaskMapper taskMapper;
private final TaskTargetMapper taskTargetMapper;
private final TaskCompletionMapper taskCompletionMapper;
private final TaskTemplateMapper taskTemplateMapper;
private final ClazzMapper classMapper;
private final StudentClassHistoryMapper studentClassHistoryMapper;
@Override
@Transactional
@ -209,4 +207,396 @@ public class TaskServiceImpl implements TaskService {
);
}
// ==================== 任务统计 ====================
@Override
public Map<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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -17,6 +17,11 @@ spring:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
data:
redis:
host: 8.148.151.56
port: 6379
database: 0
file:
upload:

View File

@ -24,6 +24,11 @@ spring:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
data:
redis:
host: ${REDIS_HOST:redis}
port: ${REDIS_PORT:6379}
database: 0
jwt:
secret: ${JWT_SECRET:reading-platform-jwt-secret-key-must-be-at-least-256-bits-long}

View File

@ -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='课表计划(增强版)';

View 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
View 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
```