feat(backend): 补全所有未提交的 Java 源码文件

新增 44 个此前仅在本地存在、从未提交到 git 的源码文件:
- Controllers: FileUpload, AdminStats, AdminTheme, AdminCoursePackage,
  AdminCourseLesson, AdminResource, AdminOperationLog,
  School(Course/Schedule/Settings/Stats/Export/OperationLog/CoursePackage),
  Teacher(Dashboard/Schedule/SchoolCourse/CourseLesson)
- Entities: CourseLesson, CoursePackage, SchoolCourse, Theme
- Mappers: CourseLesson, CoursePackage, SchoolCourse, Theme
- Services: All 13 service classes
- Config: WebMvcConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tonytech 2026-03-03 13:51:33 +08:00
parent 3f3ed084f7
commit e9dff31242
53 changed files with 2146 additions and 51 deletions

View File

@ -0,0 +1,13 @@
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -q
COPY src ./src
RUN mvn package -DskipTests -q
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
RUN mkdir -p /app/uploads
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]

View File

@ -24,6 +24,8 @@
<jjwt.version>0.12.5</jjwt.version> <jjwt.version>0.12.5</jjwt.version>
<knife4j.version>4.4.0</knife4j.version> <knife4j.version>4.4.0</knife4j.version>
<hutool.version>5.8.26</hutool.version> <hutool.version>5.8.26</hutool.version>
<poi.version>5.2.5</poi.version>
<flyway.version>10.8.1</flyway.version>
</properties> </properties>
<dependencies> <dependencies>
@ -32,6 +34,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
@ -95,6 +101,25 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- Apache POI (Excel export) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- Flyway Database Migration -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<!-- Test --> <!-- Test -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -40,11 +40,11 @@ public class SecurityConfig {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// Public endpoints // Public endpoints
.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/v1/auth/**").permitAll()
// Swagger/Knife4j endpoints // Swagger/Knife4j endpoints
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll() .requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll()
// Static resources // Static resources - uploads
.requestMatchers("/static/**", "/favicon.ico").permitAll() .requestMatchers("/uploads/**", "/static/**", "/favicon.ico").permitAll()
// OPTIONS requests // OPTIONS requests
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// All other requests require authentication // All other requests require authentication

View File

@ -0,0 +1,19 @@
package com.reading.platform.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${file.upload.path:/app/uploads/}")
private String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadPath);
}
}

View File

@ -1,5 +1,6 @@
package com.reading.platform.common.response; package com.reading.platform.common.response;
import com.reading.platform.common.enums.ErrorCode;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@ -47,4 +48,8 @@ public class Result<T> implements Serializable {
return error(500, message); return error(500, message);
} }
public static <T> Result<T> error(ErrorCode errorCode) {
return error(errorCode.getCode(), errorCode.getMessage());
}
} }

View File

@ -0,0 +1,38 @@
package com.reading.platform.controller;
import com.reading.platform.common.response.Result;
import com.reading.platform.service.FileUploadService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
@Tag(name = "File Upload", description = "File Upload APIs")
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor
public class FileUploadController {
private final FileUploadService fileUploadService;
@Operation(summary = "Upload file")
@PostMapping("/upload")
public Result<Map<String, String>> uploadFile(@RequestParam("file") MultipartFile file) {
String url = fileUploadService.uploadFile(file);
Map<String, String> result = new HashMap<>();
result.put("url", url);
result.put("filename", file.getOriginalFilename());
return Result.success(result);
}
@Operation(summary = "Delete file")
@DeleteMapping("/delete")
public Result<Void> deleteFile(@RequestParam("filePath") String filePath) {
fileUploadService.deleteFile(filePath);
return Result.success();
}
}

View File

@ -0,0 +1,55 @@
package com.reading.platform.controller.admin;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.service.CourseLessonService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Admin - Course Lessons", description = "Course Lesson Management for Admin")
@RestController
@RequestMapping("/api/v1/admin/courses/{courseId}/lessons")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminCourseLessonController {
private final CourseLessonService courseLessonService;
@Operation(summary = "Get lessons for a course")
@GetMapping
public Result<List<CourseLesson>> getLessons(@PathVariable Long courseId) {
return Result.success(courseLessonService.getLessonsByCourse(courseId));
}
@Operation(summary = "Get lesson by ID")
@GetMapping("/{id}")
public Result<CourseLesson> getLesson(@PathVariable Long courseId, @PathVariable Long id) {
return Result.success(courseLessonService.getLessonById(id));
}
@Operation(summary = "Create course lesson")
@PostMapping
public Result<CourseLesson> createLesson(@PathVariable Long courseId, @RequestBody CourseLesson lesson) {
lesson.setCourseId(courseId);
return Result.success(courseLessonService.createLesson(lesson));
}
@Operation(summary = "Update course lesson")
@PutMapping("/{id}")
public Result<CourseLesson> updateLesson(@PathVariable Long courseId, @PathVariable Long id, @RequestBody CourseLesson lesson) {
return Result.success(courseLessonService.updateLesson(id, lesson));
}
@Operation(summary = "Delete course lesson")
@DeleteMapping("/{id}")
public Result<Void> deleteLesson(@PathVariable Long courseId, @PathVariable Long id) {
courseLessonService.deleteLesson(id);
return Result.success();
}
}

View File

@ -0,0 +1,59 @@
package com.reading.platform.controller.admin;
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.entity.CoursePackage;
import com.reading.platform.service.CoursePackageService;
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 = "Admin - Course Packages", description = "Course Package Management for Admin")
@RestController
@RequestMapping("/api/v1/admin/course-packages")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminCoursePackageController {
private final CoursePackageService coursePackageService;
@Operation(summary = "Get course packages")
@GetMapping
public Result<PageResult<CoursePackage>> getPackages(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) {
Page<CoursePackage> page = coursePackageService.getPackages(pageNum, pageSize, keyword, status);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get course package by ID")
@GetMapping("/{id}")
public Result<CoursePackage> getPackage(@PathVariable Long id) {
return Result.success(coursePackageService.getPackageById(id));
}
@Operation(summary = "Create course package")
@PostMapping
public Result<CoursePackage> createPackage(@RequestBody CoursePackage pkg) {
return Result.success(coursePackageService.createPackage(pkg));
}
@Operation(summary = "Update course package")
@PutMapping("/{id}")
public Result<CoursePackage> updatePackage(@PathVariable Long id, @RequestBody CoursePackage pkg) {
return Result.success(coursePackageService.updatePackage(id, pkg));
}
@Operation(summary = "Delete course package")
@DeleteMapping("/{id}")
public Result<Void> deletePackage(@PathVariable Long id) {
coursePackageService.deletePackage(id);
return Result.success();
}
}

View File

@ -0,0 +1,34 @@
package com.reading.platform.controller.admin;
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.entity.OperationLog;
import com.reading.platform.service.OperationLogService;
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 = "Admin - Operation Logs", description = "Operation Log Management for Admin")
@RestController
@RequestMapping("/api/v1/admin/operation-logs")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminOperationLogController {
private final OperationLogService operationLogService;
@Operation(summary = "Get operation logs")
@GetMapping
public Result<PageResult<OperationLog>> getLogs(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) Long tenantId,
@RequestParam(required = false) String module) {
Page<OperationLog> page = operationLogService.getLogs(pageNum, pageSize, tenantId, module);
return Result.success(PageResult.of(page));
}
}

View File

@ -0,0 +1,81 @@
package com.reading.platform.controller.admin;
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.entity.ResourceItem;
import com.reading.platform.entity.ResourceLibrary;
import com.reading.platform.service.ResourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Admin - Resources", description = "Resource Library Management for Admin")
@RestController
@RequestMapping("/api/v1/admin/resources")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminResourceController {
private final ResourceService resourceService;
@Operation(summary = "Get all resource libraries")
@GetMapping("/libraries")
public Result<List<ResourceLibrary>> getLibraries(@RequestParam(required = false) Long tenantId) {
return Result.success(resourceService.getLibraries(tenantId));
}
@Operation(summary = "Create resource library")
@PostMapping("/libraries")
public Result<ResourceLibrary> createLibrary(@RequestBody ResourceLibrary library) {
return Result.success(resourceService.createLibrary(library));
}
@Operation(summary = "Update resource library")
@PutMapping("/libraries/{id}")
public Result<ResourceLibrary> updateLibrary(@PathVariable Long id, @RequestBody ResourceLibrary library) {
return Result.success(resourceService.updateLibrary(id, library));
}
@Operation(summary = "Delete resource library")
@DeleteMapping("/libraries/{id}")
public Result<Void> deleteLibrary(@PathVariable Long id) {
resourceService.deleteLibrary(id);
return Result.success();
}
@Operation(summary = "Get resource items")
@GetMapping("/items")
public Result<PageResult<ResourceItem>> getItems(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) Long libraryId,
@RequestParam(required = false) String keyword) {
Page<ResourceItem> page = resourceService.getItems(pageNum, pageSize, libraryId, keyword);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Create resource item")
@PostMapping("/items")
public Result<ResourceItem> createItem(@RequestBody ResourceItem item) {
return Result.success(resourceService.createItem(item));
}
@Operation(summary = "Update resource item")
@PutMapping("/items/{id}")
public Result<ResourceItem> updateItem(@PathVariable Long id, @RequestBody ResourceItem item) {
return Result.success(resourceService.updateItem(id, item));
}
@Operation(summary = "Delete resource item")
@DeleteMapping("/items/{id}")
public Result<Void> deleteItem(@PathVariable Long id) {
resourceService.deleteItem(id);
return Result.success();
}
}

View File

@ -0,0 +1,56 @@
package com.reading.platform.controller.admin;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.service.AdminStatsService;
import io.swagger.v3.oas.annotations.Operation;
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 = "Admin - Stats", description = "Admin Statistics Dashboard")
@RestController
@RequestMapping("/api/v1/admin/stats")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminStatsController {
private final AdminStatsService adminStatsService;
@Operation(summary = "Get overall statistics")
@GetMapping
public Result<Map<String, Object>> getStats() {
return Result.success(adminStatsService.getStats());
}
@Operation(summary = "Get trend data (last 6 months)")
@GetMapping("/trend")
public Result<List<Map<String, Object>>> getTrendData() {
return Result.success(adminStatsService.getTrendData());
}
@Operation(summary = "Get active tenants")
@GetMapping("/tenants/active")
public Result<List<Map<String, Object>>> getActiveTenants(
@RequestParam(defaultValue = "10") int limit) {
return Result.success(adminStatsService.getActiveTenants(limit));
}
@Operation(summary = "Get popular courses")
@GetMapping("/courses/popular")
public Result<List<Map<String, Object>>> getPopularCourses(
@RequestParam(defaultValue = "10") int limit) {
return Result.success(adminStatsService.getPopularCourses(limit));
}
@Operation(summary = "Get recent activities")
@GetMapping("/activities")
public Result<List<Map<String, Object>>> getActivities(
@RequestParam(defaultValue = "10") int limit) {
return Result.success(adminStatsService.getRecentActivities(limit));
}
}

View File

@ -0,0 +1,54 @@
package com.reading.platform.controller.admin;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.entity.Theme;
import com.reading.platform.service.ThemeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Admin - Themes", description = "Theme Management for Admin")
@RestController
@RequestMapping("/api/v1/admin/themes")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminThemeController {
private final ThemeService themeService;
@Operation(summary = "Get all themes")
@GetMapping
public Result<List<Theme>> getThemes(@RequestParam(required = false) Boolean enabledOnly) {
return Result.success(themeService.getAllThemes(enabledOnly));
}
@Operation(summary = "Get theme by ID")
@GetMapping("/{id}")
public Result<Theme> getTheme(@PathVariable Long id) {
return Result.success(themeService.getThemeById(id));
}
@Operation(summary = "Create theme")
@PostMapping
public Result<Theme> createTheme(@RequestBody Theme theme) {
return Result.success(themeService.createTheme(theme));
}
@Operation(summary = "Update theme")
@PutMapping("/{id}")
public Result<Theme> updateTheme(@PathVariable Long id, @RequestBody Theme theme) {
return Result.success(themeService.updateTheme(id, theme));
}
@Operation(summary = "Delete theme")
@DeleteMapping("/{id}")
public Result<Void> deleteTheme(@PathVariable Long id) {
themeService.deleteTheme(id);
return Result.success();
}
}

View File

@ -13,7 +13,7 @@ import java.util.List;
@Tag(name = "Parent - Child", description = "Child Information APIs for Parent") @Tag(name = "Parent - Child", description = "Child Information APIs for Parent")
@RestController @RestController
@RequestMapping("/api/parent/children") @RequestMapping("/api/v1/parent/children")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ParentChildController { public class ParentChildController {

View File

@ -0,0 +1,62 @@
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.SchoolCourse;
import com.reading.platform.service.SchoolCourseService;
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 = "School - School Courses", description = "School Custom Course Management")
@RestController
@RequestMapping("/api/v1/school/school-courses")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolCourseController {
private final SchoolCourseService schoolCourseService;
@Operation(summary = "Get school courses")
@GetMapping
public Result<PageResult<SchoolCourse>> getCourses(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String keyword) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<SchoolCourse> page = schoolCourseService.getCourses(pageNum, pageSize, tenantId, keyword);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get school course by ID")
@GetMapping("/{id}")
public Result<SchoolCourse> getCourse(@PathVariable Long id) {
return Result.success(schoolCourseService.getCourseById(id));
}
@Operation(summary = "Create school course")
@PostMapping
public Result<SchoolCourse> createCourse(@RequestBody SchoolCourse course) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Long userId = SecurityUtils.getCurrentUserId();
return Result.success(schoolCourseService.createCourse(tenantId, userId, course));
}
@Operation(summary = "Update school course")
@PutMapping("/{id}")
public Result<SchoolCourse> updateCourse(@PathVariable Long id, @RequestBody SchoolCourse course) {
return Result.success(schoolCourseService.updateCourse(id, course));
}
@Operation(summary = "Delete school course")
@DeleteMapping("/{id}")
public Result<Void> deleteCourse(@PathVariable Long id) {
schoolCourseService.deleteCourse(id);
return Result.success();
}
}

View File

@ -0,0 +1,39 @@
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.entity.CoursePackage;
import com.reading.platform.service.CoursePackageService;
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 = "School - Course Packages", description = "Course Packages for School")
@RestController
@RequestMapping("/api/v1/school/course-packages")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolCoursePackageController {
private final CoursePackageService coursePackageService;
@Operation(summary = "Get available course packages")
@GetMapping
public Result<PageResult<CoursePackage>> getPackages(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String keyword) {
Page<CoursePackage> page = coursePackageService.getPackages(pageNum, pageSize, keyword, "published");
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get course package by ID")
@GetMapping("/{id}")
public Result<CoursePackage> getPackage(@PathVariable Long id) {
return Result.success(coursePackageService.getPackageById(id));
}
}

View File

@ -0,0 +1,65 @@
package com.reading.platform.controller.school;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.service.ExportService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
@Tag(name = "School - Export", description = "School Data Export")
@RestController
@RequestMapping("/api/v1/school/export")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolExportController {
private final ExportService exportService;
@Operation(summary = "Export teachers to Excel")
@GetMapping("/teachers")
public ResponseEntity<byte[]> exportTeachers() throws IOException {
Long tenantId = SecurityUtils.getCurrentTenantId();
byte[] data = exportService.exportTeachers(tenantId);
return buildResponse(data, "teachers_" + LocalDate.now() + ".xlsx");
}
@Operation(summary = "Export students to Excel")
@GetMapping("/students")
public ResponseEntity<byte[]> exportStudents() throws IOException {
Long tenantId = SecurityUtils.getCurrentTenantId();
byte[] data = exportService.exportStudents(tenantId);
return buildResponse(data, "students_" + LocalDate.now() + ".xlsx");
}
@Operation(summary = "Export lessons to Excel")
@GetMapping("/lessons")
public ResponseEntity<byte[]> exportLessons() throws IOException {
Long tenantId = SecurityUtils.getCurrentTenantId();
byte[] data = exportService.exportLessons(tenantId);
return buildResponse(data, "lessons_" + LocalDate.now() + ".xlsx");
}
@Operation(summary = "Export growth records to Excel")
@GetMapping("/growth-records")
public ResponseEntity<byte[]> exportGrowthRecords() throws IOException {
Long tenantId = SecurityUtils.getCurrentTenantId();
byte[] data = exportService.exportGrowthRecords(tenantId);
return buildResponse(data, "growth_records_" + LocalDate.now() + ".xlsx");
}
private ResponseEntity<byte[]> buildResponse(byte[] data, String filename) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.body(data);
}
}

View File

@ -0,0 +1,35 @@
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.OperationLog;
import com.reading.platform.service.OperationLogService;
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 = "School - Operation Logs", description = "Operation Log for School")
@RestController
@RequestMapping("/api/v1/school/operation-logs")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolOperationLogController {
private final OperationLogService operationLogService;
@Operation(summary = "Get school operation logs")
@GetMapping
public Result<PageResult<OperationLog>> getLogs(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String module) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<OperationLog> page = operationLogService.getLogs(pageNum, pageSize, tenantId, module);
return Result.success(PageResult.of(page));
}
}

View File

@ -0,0 +1,86 @@
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.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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "School - Schedule", description = "School Schedule Management")
@RestController
@RequestMapping("/api/v1/school/schedules")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolScheduleController {
private final ScheduleService scheduleService;
@Operation(summary = "Get schedule plans")
@GetMapping
public Result<PageResult<SchedulePlan>> getSchedulePlans(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) Long classId) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<SchedulePlan> page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, classId);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get schedule plan by ID")
@GetMapping("/{id}")
public Result<SchedulePlan> getSchedulePlan(@PathVariable Long id) {
return Result.success(scheduleService.getSchedulePlanById(id));
}
@Operation(summary = "Create schedule plan")
@PostMapping
public Result<SchedulePlan> createSchedulePlan(@RequestBody SchedulePlan plan) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(scheduleService.createSchedulePlan(tenantId, plan));
}
@Operation(summary = "Update schedule plan")
@PutMapping("/{id}")
public Result<SchedulePlan> updateSchedulePlan(@PathVariable Long id, @RequestBody SchedulePlan plan) {
return Result.success(scheduleService.updateSchedulePlan(id, plan));
}
@Operation(summary = "Delete schedule plan")
@DeleteMapping("/{id}")
public Result<Void> deleteSchedulePlan(@PathVariable Long id) {
scheduleService.deleteSchedulePlan(id);
return Result.success();
}
@Operation(summary = "Get schedule templates")
@GetMapping("/templates")
public Result<PageResult<ScheduleTemplate>> getScheduleTemplates(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<ScheduleTemplate> page = scheduleService.getScheduleTemplates(pageNum, pageSize, tenantId);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Create schedule template")
@PostMapping("/templates")
public Result<ScheduleTemplate> createScheduleTemplate(@RequestBody ScheduleTemplate template) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(scheduleService.createScheduleTemplate(tenantId, template));
}
@Operation(summary = "Delete schedule template")
@DeleteMapping("/templates/{id}")
public Result<Void> deleteScheduleTemplate(@PathVariable Long id) {
scheduleService.deleteScheduleTemplate(id);
return Result.success();
}
}

View File

@ -0,0 +1,38 @@
package com.reading.platform.controller.school;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.service.SystemSettingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "School - Settings", description = "School Settings Management")
@RestController
@RequestMapping("/api/v1/school/settings")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolSettingsController {
private final SystemSettingService systemSettingService;
@Operation(summary = "Get school settings")
@GetMapping
public Result<Map<String, String>> getSettings() {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(systemSettingService.getSettings(tenantId));
}
@Operation(summary = "Update school settings")
@PutMapping
public Result<Void> updateSettings(@RequestBody Map<String, String> settings) {
Long tenantId = SecurityUtils.getCurrentTenantId();
systemSettingService.updateSettings(tenantId, settings);
return Result.success();
}
}

View File

@ -0,0 +1,30 @@
package com.reading.platform.controller.school;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.service.SchoolStatsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "School - Stats", description = "School Statistics Dashboard")
@RestController
@RequestMapping("/api/v1/school/stats")
@RequiredArgsConstructor
@RequireRole(UserRole.SCHOOL)
public class SchoolStatsController {
private final SchoolStatsService schoolStatsService;
@Operation(summary = "Get school statistics")
@GetMapping
public Result<Map<String, Object>> getStats() {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getStats(tenantId));
}
}

View File

@ -0,0 +1,35 @@
package com.reading.platform.controller.teacher;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.service.CourseLessonService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Teacher - Course Lessons", description = "Course Lessons for Teacher")
@RestController
@RequestMapping("/api/v1/teacher/courses/{courseId}/lessons")
@RequiredArgsConstructor
@RequireRole(UserRole.TEACHER)
public class TeacherCourseLessonController {
private final CourseLessonService courseLessonService;
@Operation(summary = "Get lessons for a course")
@GetMapping
public Result<List<CourseLesson>> getLessons(@PathVariable Long courseId) {
return Result.success(courseLessonService.getLessonsByCourse(courseId));
}
@Operation(summary = "Get lesson by ID")
@GetMapping("/{id}")
public Result<CourseLesson> getLesson(@PathVariable Long courseId, @PathVariable Long id) {
return Result.success(courseLessonService.getLessonById(id));
}
}

View File

@ -0,0 +1,48 @@
package com.reading.platform.controller.teacher;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.service.TeacherDashboardService;
import io.swagger.v3.oas.annotations.Operation;
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 = "Teacher - Dashboard", description = "Teacher Dashboard")
@RestController
@RequestMapping("/api/v1/teacher/dashboard")
@RequiredArgsConstructor
@RequireRole(UserRole.TEACHER)
public class TeacherDashboardController {
private final TeacherDashboardService teacherDashboardService;
@Operation(summary = "Get teacher dashboard overview")
@GetMapping
public Result<Map<String, Object>> getDashboard() {
Long teacherId = SecurityUtils.getCurrentUserId();
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(teacherDashboardService.getDashboard(teacherId, tenantId));
}
@Operation(summary = "Get today's lessons")
@GetMapping("/today")
public Result<List<Map<String, Object>>> getTodayLessons() {
Long teacherId = SecurityUtils.getCurrentUserId();
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(teacherDashboardService.getTodayLessons(teacherId, tenantId));
}
@Operation(summary = "Get weekly lessons")
@GetMapping("/weekly")
public Result<List<Map<String, Object>>> getWeeklyLessons() {
Long teacherId = SecurityUtils.getCurrentUserId();
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(teacherDashboardService.getWeeklyLessons(teacherId, tenantId));
}
}

View File

@ -0,0 +1,40 @@
package com.reading.platform.controller.teacher;
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.SchedulePlan;
import com.reading.platform.service.ScheduleService;
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 = "Teacher - Schedule", description = "Teacher Schedule View")
@RestController
@RequestMapping("/api/v1/teacher/schedules")
@RequiredArgsConstructor
@RequireRole(UserRole.TEACHER)
public class TeacherScheduleController {
private final ScheduleService scheduleService;
@Operation(summary = "Get teacher schedule plans")
@GetMapping
public Result<PageResult<SchedulePlan>> getSchedulePlans(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<SchedulePlan> page = scheduleService.getSchedulePlans(pageNum, pageSize, tenantId, null);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get schedule plan by ID")
@GetMapping("/{id}")
public Result<SchedulePlan> getSchedulePlan(@PathVariable Long id) {
return Result.success(scheduleService.getSchedulePlanById(id));
}
}

View File

@ -0,0 +1,41 @@
package com.reading.platform.controller.teacher;
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.SchoolCourse;
import com.reading.platform.service.SchoolCourseService;
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 = "Teacher - School Courses", description = "School Courses for Teacher")
@RestController
@RequestMapping("/api/v1/teacher/school-courses")
@RequiredArgsConstructor
@RequireRole(UserRole.TEACHER)
public class TeacherSchoolCourseController {
private final SchoolCourseService schoolCourseService;
@Operation(summary = "Get school courses")
@GetMapping
public Result<PageResult<SchoolCourse>> getCourses(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String keyword) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<SchoolCourse> page = schoolCourseService.getCourses(pageNum, pageSize, tenantId, keyword);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get school course by ID")
@GetMapping("/{id}")
public Result<SchoolCourse> getCourse(@PathVariable Long id) {
return Result.success(schoolCourseService.getCourseById(id));
}
}

View File

@ -0,0 +1,42 @@
package com.reading.platform.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Course Lesson Entity (chapters/sections within a course)
*/
@Data
@TableName("course_lessons")
public class CourseLesson {
@TableId(type = IdType.AUTO)
private Long id;
private Long courseId;
private String title;
private String description;
private String content;
private Integer sortOrder;
private Integer durationMinutes;
private String videoUrl;
private String status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@ -0,0 +1,41 @@
package com.reading.platform.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* Course Package Entity
*/
@Data
@TableName("course_packages")
public class CoursePackage {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private String coverUrl;
private Integer courseCount;
private BigDecimal price;
private String status;
private Integer isSystem;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@ -0,0 +1,42 @@
package com.reading.platform.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* School Course Entity (tenant-customized courses)
*/
@Data
@TableName("school_courses")
public class SchoolCourse {
@TableId(type = IdType.AUTO)
private Long id;
private Long tenantId;
private String name;
private String description;
private String coverUrl;
private String category;
private String ageRange;
private String status;
private Long createdBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@ -0,0 +1,38 @@
package com.reading.platform.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Theme Entity
*/
@Data
@TableName("themes")
public class Theme {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String displayName;
private String color;
private String icon;
private Integer sortOrder;
private Integer isEnabled;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@ -0,0 +1,9 @@
package com.reading.platform.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reading.platform.entity.CourseLesson;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CourseLessonMapper extends BaseMapper<CourseLesson> {
}

View File

@ -0,0 +1,9 @@
package com.reading.platform.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reading.platform.entity.CoursePackage;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CoursePackageMapper extends BaseMapper<CoursePackage> {
}

View File

@ -0,0 +1,9 @@
package com.reading.platform.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reading.platform.entity.SchoolCourse;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SchoolCourseMapper extends BaseMapper<SchoolCourse> {
}

View File

@ -0,0 +1,9 @@
package com.reading.platform.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reading.platform.entity.Theme;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ThemeMapper extends BaseMapper<Theme> {
}

View File

@ -0,0 +1,49 @@
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 {
private final CourseLessonMapper courseLessonMapper;
public List<CourseLesson> getLessonsByCourse(Long courseId) {
LambdaQueryWrapper<CourseLesson> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CourseLesson::getCourseId, courseId)
.orderByAsc(CourseLesson::getSortOrder);
return courseLessonMapper.selectList(wrapper);
}
public CourseLesson getLessonById(Long id) {
CourseLesson lesson = courseLessonMapper.selectById(id);
if (lesson == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "Course lesson not found");
}
return lesson;
}
public CourseLesson createLesson(CourseLesson lesson) {
courseLessonMapper.insert(lesson);
return 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);
}
}

View File

@ -0,0 +1,54 @@
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 {
private final CoursePackageMapper coursePackageMapper;
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);
}
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;
}
public CoursePackage createPackage(CoursePackage pkg) {
coursePackageMapper.insert(pkg);
return pkg;
}
public CoursePackage updatePackage(Long id, CoursePackage pkg) {
getPackageById(id);
pkg.setId(id);
coursePackageMapper.updateById(pkg);
return coursePackageMapper.selectById(id);
}
public void deletePackage(Long id) {
coursePackageMapper.deleteById(id);
}
}

View File

@ -0,0 +1,141 @@
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 {
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final LessonMapper lessonMapper;
private final GrowthRecordMapper growthRecordMapper;
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);
}
}
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();
}
}

View File

@ -0,0 +1,71 @@
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;
@Slf4j
@Service
public class FileUploadService {
@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);
}
}
}
}

View File

@ -0,0 +1,48 @@
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 {
private final OperationLogMapper operationLogMapper;
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
}
}
}

View File

@ -0,0 +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.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 {
private final ResourceLibraryMapper resourceLibraryMapper;
private final ResourceItemMapper resourceItemMapper;
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);
}
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;
}
public ResourceLibrary createLibrary(ResourceLibrary library) {
resourceLibraryMapper.insert(library);
return library;
}
public ResourceLibrary updateLibrary(Long id, ResourceLibrary library) {
getLibraryById(id);
library.setId(id);
resourceLibraryMapper.updateById(library);
return resourceLibraryMapper.selectById(id);
}
public void deleteLibrary(Long id) {
resourceLibraryMapper.deleteById(id);
}
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);
}
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;
}
public ResourceItem createItem(ResourceItem item) {
resourceItemMapper.insert(item);
return 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);
}
}

View File

@ -0,0 +1,75 @@
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.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 {
private final SchedulePlanMapper schedulePlanMapper;
private final ScheduleTemplateMapper scheduleTemplateMapper;
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);
}
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;
}
public SchedulePlan createSchedulePlan(Long tenantId, SchedulePlan plan) {
plan.setTenantId(tenantId);
schedulePlanMapper.insert(plan);
return plan;
}
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);
}
public void deleteSchedulePlan(Long id) {
schedulePlanMapper.deleteById(id);
}
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);
}
public ScheduleTemplate createScheduleTemplate(Long tenantId, ScheduleTemplate template) {
template.setTenantId(tenantId);
scheduleTemplateMapper.insert(template);
return template;
}
public void deleteScheduleTemplate(Long id) {
scheduleTemplateMapper.deleteById(id);
}
}

View File

@ -0,0 +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.SchoolCourse;
import com.reading.platform.mapper.SchoolCourseMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class SchoolCourseService {
private final SchoolCourseMapper schoolCourseMapper;
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);
}
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;
}
public SchoolCourse createCourse(Long tenantId, Long userId, SchoolCourse course) {
course.setTenantId(tenantId);
course.setCreatedBy(userId);
schoolCourseMapper.insert(course);
return 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);
}
}

View File

@ -0,0 +1,33 @@
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.Map;
@Service
@RequiredArgsConstructor
public class SchoolStatsService {
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final ClazzMapper clazzMapper;
private final LessonMapper lessonMapper;
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

@ -0,0 +1,61 @@
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 {
private final SystemSettingMapper systemSettingMapper;
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;
}
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;
}
}

View File

@ -0,0 +1,86 @@
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 {
private final LessonMapper lessonMapper;
private final TaskMapper taskMapper;
private final GrowthRecordMapper growthRecordMapper;
private final NotificationMapper notificationMapper;
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());
}
}

View File

@ -0,0 +1,51 @@
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 {
private final ThemeMapper themeMapper;
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);
}
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;
}
public Theme createTheme(Theme theme) {
themeMapper.insert(theme);
return 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);
}
}

View File

@ -13,6 +13,16 @@ spring:
serialization: serialization:
write-dates-as-timestamps: false write-dates-as-timestamps: false
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
file:
upload:
path: ./uploads/
base-url: /uploads/
logging: logging:
level: level:
com.reading.platform: debug com.reading.platform: debug

View File

@ -0,0 +1,39 @@
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql:3306/reading_platform?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: reading_platform_pwd
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
serialization:
write-dates-as-timestamps: false
servlet:
multipart:
max-file-size: 300MB
max-request-size: 1500MB
web:
resources:
static-locations: file:/app/uploads/,classpath:/static/
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
jwt:
secret: ${JWT_SECRET:reading-platform-jwt-secret-key-must-be-at-least-256-bits-long}
expiration: 604800000
file:
upload:
path: /app/uploads/
base-url: /uploads/
logging:
level:
com.reading.platform: info

View File

@ -6,6 +6,10 @@ spring:
name: reading-platform name: reading-platform
profiles: profiles:
active: dev active: dev
servlet:
multipart:
max-file-size: 300MB
max-request-size: 1500MB
# MyBatis-Plus Configuration # MyBatis-Plus Configuration
mybatis-plus: mybatis-plus:

View File

@ -519,4 +519,4 @@ CREATE INDEX idx_notifications_tenant ON notifications(tenant_id);
-- Insert default admin user (password: admin123) -- Insert default admin user (password: admin123)
INSERT INTO admin_users (username, password, name, status) VALUES INSERT INTO admin_users (username, password, name, status) VALUES
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'Super Admin', 'active'); ('admin', '$2b$10$VSes.57X35WZov4LdyJ.eu1pll7qnt7tGuq/u3iXOrYawHTebe.Eu', 'Super Admin', 'active');

View File

@ -4,75 +4,75 @@
-- Add new fields to courses table for course package refactoring -- Add new fields to courses table for course package refactoring
-- Core content -- Core content
ALTER TABLE courses ADD COLUMN IF NOT EXISTS core_content TEXT COMMENT 'Core content summary'; ALTER TABLE courses ADD COLUMN core_content TEXT COMMENT 'Core content summary';
-- Course introduction fields (8 fields) -- Course introduction fields (8 fields)
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_summary TEXT COMMENT 'Course summary'; ALTER TABLE courses ADD COLUMN intro_summary TEXT COMMENT 'Course summary';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_highlights TEXT COMMENT 'Course highlights'; ALTER TABLE courses ADD COLUMN intro_highlights TEXT COMMENT 'Course highlights';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_goals TEXT COMMENT 'Course goals'; ALTER TABLE courses ADD COLUMN intro_goals TEXT COMMENT 'Course goals';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_schedule TEXT COMMENT 'Content schedule'; ALTER TABLE courses ADD COLUMN intro_schedule TEXT COMMENT 'Content schedule';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_key_points TEXT COMMENT 'Key points and difficulties'; ALTER TABLE courses ADD COLUMN intro_key_points TEXT COMMENT 'Key points and difficulties';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_methods TEXT COMMENT 'Teaching methods'; ALTER TABLE courses ADD COLUMN intro_methods TEXT COMMENT 'Teaching methods';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_evaluation TEXT COMMENT 'Evaluation methods'; ALTER TABLE courses ADD COLUMN intro_evaluation TEXT COMMENT 'Evaluation methods';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS intro_notes TEXT COMMENT 'Notes and precautions'; ALTER TABLE courses ADD COLUMN intro_notes TEXT COMMENT 'Notes and precautions';
-- Schedule reference data -- Schedule reference data
ALTER TABLE courses ADD COLUMN IF NOT EXISTS schedule_ref_data TEXT COMMENT 'Schedule reference data (JSON)'; ALTER TABLE courses ADD COLUMN schedule_ref_data TEXT COMMENT 'Schedule reference data (JSON)';
-- Environment construction (Step 7) -- Environment construction (Step 7)
ALTER TABLE courses ADD COLUMN IF NOT EXISTS environment_construction TEXT COMMENT 'Environment construction content'; ALTER TABLE courses ADD COLUMN environment_construction TEXT COMMENT 'Environment construction content';
-- Theme and picture book relation -- Theme and picture book relation
ALTER TABLE courses ADD COLUMN IF NOT EXISTS theme_id BIGINT COMMENT 'Theme ID'; ALTER TABLE courses ADD COLUMN theme_id BIGINT COMMENT 'Theme ID';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS picture_book_name VARCHAR(200) COMMENT 'Picture book name'; ALTER TABLE courses ADD COLUMN picture_book_name VARCHAR(200) COMMENT 'Picture book name';
-- Cover image path -- Cover image path
ALTER TABLE courses ADD COLUMN IF NOT EXISTS cover_image_path VARCHAR(500) COMMENT 'Cover image file path'; ALTER TABLE courses ADD COLUMN cover_image_path VARCHAR(500) COMMENT 'Cover image file path';
-- Digital resources (JSON arrays) -- Digital resources (JSON arrays)
ALTER TABLE courses ADD COLUMN IF NOT EXISTS ebook_paths TEXT COMMENT 'Ebook paths (JSON array)'; ALTER TABLE courses ADD COLUMN ebook_paths TEXT COMMENT 'Ebook paths (JSON array)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS audio_paths TEXT COMMENT 'Audio paths (JSON array)'; ALTER TABLE courses ADD COLUMN audio_paths TEXT COMMENT 'Audio paths (JSON array)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS video_paths TEXT COMMENT 'Video paths (JSON array)'; ALTER TABLE courses ADD COLUMN video_paths TEXT COMMENT 'Video paths (JSON array)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS other_resources TEXT COMMENT 'Other resources (JSON array)'; ALTER TABLE courses ADD COLUMN other_resources TEXT COMMENT 'Other resources (JSON array)';
-- Teaching materials -- Teaching materials
ALTER TABLE courses ADD COLUMN IF NOT EXISTS ppt_path VARCHAR(500) COMMENT 'PPT file path'; ALTER TABLE courses ADD COLUMN ppt_path VARCHAR(500) COMMENT 'PPT file path';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS ppt_name VARCHAR(200) COMMENT 'PPT file name'; ALTER TABLE courses ADD COLUMN ppt_name VARCHAR(200) COMMENT 'PPT file name';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS poster_paths TEXT COMMENT 'Poster paths (JSON array)'; ALTER TABLE courses ADD COLUMN poster_paths TEXT COMMENT 'Poster paths (JSON array)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS tools TEXT COMMENT 'Teaching tools (JSON array)'; ALTER TABLE courses ADD COLUMN tools TEXT COMMENT 'Teaching tools (JSON array)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS student_materials TEXT COMMENT 'Student materials'; ALTER TABLE courses ADD COLUMN student_materials TEXT COMMENT 'Student materials';
-- Lesson plan and activities -- Lesson plan and activities
ALTER TABLE courses ADD COLUMN IF NOT EXISTS lesson_plan_data TEXT COMMENT 'Lesson plan data (JSON)'; ALTER TABLE courses ADD COLUMN lesson_plan_data TEXT COMMENT 'Lesson plan data (JSON)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS activities_data TEXT COMMENT 'Activities data (JSON)'; ALTER TABLE courses ADD COLUMN activities_data TEXT COMMENT 'Activities data (JSON)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS assessment_data TEXT COMMENT 'Assessment data (JSON)'; ALTER TABLE courses ADD COLUMN assessment_data TEXT COMMENT 'Assessment data (JSON)';
-- Grade and domain tags -- Grade and domain tags
ALTER TABLE courses ADD COLUMN IF NOT EXISTS grade_tags TEXT COMMENT 'Grade tags (JSON array)'; ALTER TABLE courses ADD COLUMN grade_tags TEXT COMMENT 'Grade tags (JSON array)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS domain_tags TEXT COMMENT 'Domain tags (JSON array)'; ALTER TABLE courses ADD COLUMN domain_tags TEXT COMMENT 'Domain tags (JSON array)';
-- Collective lesson flag -- Collective lesson flag
ALTER TABLE courses ADD COLUMN IF NOT EXISTS has_collective_lesson TINYINT DEFAULT 0 COMMENT 'Has collective lesson'; ALTER TABLE courses ADD COLUMN has_collective_lesson TINYINT DEFAULT 0 COMMENT 'Has collective lesson';
-- Version and review fields -- Version and review fields
ALTER TABLE courses ADD COLUMN IF NOT EXISTS version VARCHAR(20) DEFAULT '1.0' COMMENT 'Version number'; ALTER TABLE courses ADD COLUMN version VARCHAR(20) DEFAULT '1.0' COMMENT 'Version number';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS parent_id BIGINT COMMENT 'Parent course ID for versions'; ALTER TABLE courses ADD COLUMN parent_id BIGINT COMMENT 'Parent course ID for versions';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS is_latest TINYINT DEFAULT 1 COMMENT 'Is latest version'; ALTER TABLE courses ADD COLUMN is_latest TINYINT DEFAULT 1 COMMENT 'Is latest version';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS submitted_at DATETIME COMMENT 'Submitted for review at'; ALTER TABLE courses ADD COLUMN submitted_at DATETIME COMMENT 'Submitted for review at';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS submitted_by BIGINT COMMENT 'Submitted by user ID'; ALTER TABLE courses ADD COLUMN submitted_by BIGINT COMMENT 'Submitted by user ID';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS reviewed_at DATETIME COMMENT 'Reviewed at'; ALTER TABLE courses ADD COLUMN reviewed_at DATETIME COMMENT 'Reviewed at';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS reviewed_by BIGINT COMMENT 'Reviewed by user ID'; ALTER TABLE courses ADD COLUMN reviewed_by BIGINT COMMENT 'Reviewed by user ID';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS review_comment TEXT COMMENT 'Review comment'; ALTER TABLE courses ADD COLUMN review_comment TEXT COMMENT 'Review comment';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS review_checklist TEXT COMMENT 'Review checklist (JSON)'; ALTER TABLE courses ADD COLUMN review_checklist TEXT COMMENT 'Review checklist (JSON)';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS published_at DATETIME COMMENT 'Published at'; ALTER TABLE courses ADD COLUMN published_at DATETIME COMMENT 'Published at';
-- Usage statistics -- Usage statistics
ALTER TABLE courses ADD COLUMN IF NOT EXISTS usage_count INT DEFAULT 0 COMMENT 'Usage count'; ALTER TABLE courses ADD COLUMN usage_count INT DEFAULT 0 COMMENT 'Usage count';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS teacher_count INT DEFAULT 0 COMMENT 'Teacher count'; ALTER TABLE courses ADD COLUMN teacher_count INT DEFAULT 0 COMMENT 'Teacher count';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS avg_rating DECIMAL(3,2) DEFAULT 0 COMMENT 'Average rating'; ALTER TABLE courses ADD COLUMN avg_rating DECIMAL(3,2) DEFAULT 0 COMMENT 'Average rating';
ALTER TABLE courses ADD COLUMN IF NOT EXISTS created_by BIGINT COMMENT 'Created by user ID'; ALTER TABLE courses ADD COLUMN created_by BIGINT COMMENT 'Created by user ID';
-- Create indexes for better query performance -- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_courses_theme ON courses(theme_id); CREATE INDEX idx_courses_theme ON courses(theme_id);
CREATE INDEX IF NOT EXISTS idx_courses_status ON courses(status); CREATE INDEX idx_courses_status ON courses(status);
CREATE INDEX IF NOT EXISTS idx_courses_parent ON courses(parent_id); CREATE INDEX idx_courses_parent ON courses(parent_id);

View File

@ -0,0 +1,21 @@
-- Add themes table
CREATE TABLE IF NOT EXISTS themes (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE COMMENT 'Theme technical name',
display_name VARCHAR(100) NOT NULL COMMENT 'Display name',
color VARCHAR(20) COMMENT 'Theme color (hex)',
icon VARCHAR(100) COMMENT 'Theme icon',
sort_order INT DEFAULT 0 COMMENT 'Sort order',
is_enabled TINYINT DEFAULT 1 COMMENT 'Is enabled',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Themes';
-- Insert default themes
INSERT INTO themes (name, display_name, color, sort_order, is_enabled) VALUES
('nature', '自然探索', '#4CAF50', 1, 1),
('ocean', '海洋世界', '#2196F3', 2, 1),
('space', '太空冒险', '#9C27B0', 3, 1),
('animals', '动物王国', '#FF9800', 4, 1),
('fairytale', '童话故事', '#E91E63', 5, 1);

View File

@ -0,0 +1,14 @@
-- Add course packages table
CREATE TABLE IF NOT EXISTS course_packages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL COMMENT 'Package name',
description TEXT COMMENT 'Package description',
cover_url VARCHAR(500) COMMENT 'Cover image URL',
course_count INT DEFAULT 0 COMMENT 'Number of courses',
price DECIMAL(10, 2) DEFAULT 0 COMMENT 'Price',
status VARCHAR(20) DEFAULT 'draft' COMMENT 'Status: draft, published, archived',
is_system TINYINT DEFAULT 1 COMMENT 'Is system package',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Course Packages';

View File

@ -0,0 +1,17 @@
-- Add school courses table (tenant-customized courses)
CREATE TABLE IF NOT EXISTS school_courses (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL COMMENT 'Tenant ID',
name VARCHAR(200) NOT NULL COMMENT 'Course name',
description TEXT COMMENT 'Course description',
cover_url VARCHAR(500) COMMENT 'Cover URL',
category VARCHAR(50) COMMENT 'Course category',
age_range VARCHAR(50) COMMENT 'Age range',
status VARCHAR(20) DEFAULT 'draft' COMMENT 'Status: draft, published, archived',
created_by BIGINT COMMENT 'Created by user ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
INDEX idx_tenant (tenant_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='School Custom Courses';

View File

@ -0,0 +1,17 @@
-- Add course lessons table (chapters/sections within a course)
CREATE TABLE IF NOT EXISTS course_lessons (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
course_id BIGINT NOT NULL COMMENT 'Course ID',
title VARCHAR(200) NOT NULL COMMENT 'Lesson title',
description TEXT COMMENT 'Lesson description',
content TEXT COMMENT 'Lesson content',
sort_order INT DEFAULT 0 COMMENT 'Sort order',
duration_minutes INT COMMENT 'Duration in minutes',
video_url VARCHAR(1000) COMMENT 'Video URL',
status VARCHAR(20) DEFAULT 'draft' COMMENT 'Status: draft, published',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
INDEX idx_course (course_id),
INDEX idx_sort (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Course Lessons (chapters/sections)';