Merge branch 'original' of http://8.148.151.56:3000/tonytech/kindergarten into dev_unocss

This commit is contained in:
zhonghua 2026-03-04 15:30:46 +08:00
commit 327fc9fe8d
28 changed files with 401 additions and 56 deletions

View File

@ -1,6 +1,7 @@
package com.reading.platform.common.response; package com.reading.platform.common.response;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@ -15,10 +16,13 @@ public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@JsonProperty("items")
private List<T> list; private List<T> list;
private Long total; private Long total;
@JsonProperty("page")
private Long pageNum; private Long pageNum;
private Long pageSize; private Long pageSize;
@JsonProperty("totalPages")
private Long pages; private Long pages;
public static <T> PageResult<T> of(List<T> list, Long total, Long pageNum, Long pageSize) { public static <T> PageResult<T> of(List<T> list, Long total, Long pageNum, Long pageSize) {

View File

@ -0,0 +1,19 @@
package com.reading.platform.common.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
* Jackson serializer that converts string values to uppercase.
* Used to normalize status fields from DB (lowercase) to JSON output (uppercase).
*/
public class UpperCaseSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value != null ? value.toUpperCase() : null);
}
}

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Auth", description = "Authentication APIs") @Tag(name = "Auth", description = "Authentication APIs")
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/v1/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthController { public class AuthController {
@ -25,6 +25,13 @@ public class AuthController {
return Result.success(authService.login(request)); return Result.success(authService.login(request));
} }
@Operation(summary = "User logout")
@PostMapping("/logout")
public Result<Void> logout() {
// JWT is stateless - client simply discards the token
return Result.success();
}
@Operation(summary = "Get current user info") @Operation(summary = "Get current user info")
@GetMapping("/me") @GetMapping("/me")
public Result<UserInfoResponse> getCurrentUser() { public Result<UserInfoResponse> getCurrentUser() {

View File

@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Admin - Course", description = "System Course Management APIs for Admin") @Tag(name = "Admin - Course", description = "System Course Management APIs for Admin")
@RestController @RestController
@RequestMapping("/api/admin/courses") @RequestMapping("/api/v1/admin/courses")
@RequiredArgsConstructor @RequiredArgsConstructor
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public class AdminCourseController { public class AdminCourseController {
@ -48,7 +48,7 @@ public class AdminCourseController {
@Operation(summary = "Get system course page") @Operation(summary = "Get system course page")
@GetMapping @GetMapping
public Result<PageResult<Course>> getCoursePage( public Result<PageResult<Course>> getCoursePage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String category) { @RequestParam(required = false) String category) {

View File

@ -0,0 +1,44 @@
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.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 = "Admin - Settings", description = "Admin System Settings Management")
@RestController
@RequestMapping("/api/v1/admin/settings")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminSettingsController {
// Use tenantId=0 for global admin settings
private static final Long ADMIN_TENANT_ID = 0L;
private final SystemSettingService systemSettingService;
@Operation(summary = "Get admin system settings")
@GetMapping
public Result<Map<String, String>> getSettings() {
return Result.success(systemSettingService.getSettings(ADMIN_TENANT_ID));
}
@Operation(summary = "Update admin system settings")
@PutMapping
public Result<Void> updateSettings(@RequestBody Map<String, Object> settings) {
Map<String, String> stringSettings = new java.util.HashMap<>();
settings.forEach((k, v) -> {
if (v != null) {
stringSettings.put(k, v.toString());
}
});
systemSettingService.updateSettings(ADMIN_TENANT_ID, stringSettings);
return Result.success();
}
}

View File

@ -16,11 +16,13 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Tag(name = "Admin - Tenant", description = "Tenant Management APIs for Admin") @Tag(name = "Admin - Tenant", description = "Tenant Management APIs for Admin")
@RestController @RestController
@RequestMapping("/api/admin/tenants") @RequestMapping("/api/v1/admin/tenants")
@RequiredArgsConstructor @RequiredArgsConstructor
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public class AdminTenantController { public class AdminTenantController {
@ -48,10 +50,11 @@ public class AdminTenantController {
@Operation(summary = "Get tenant page") @Operation(summary = "Get tenant page")
@GetMapping @GetMapping
public Result<PageResult<Tenant>> getTenantPage( public Result<PageResult<Tenant>> getTenantPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) { @RequestParam(required = false) String status,
@RequestParam(required = false) String packageType) {
Page<Tenant> page = tenantService.getTenantPage(pageNum, pageSize, keyword, status); Page<Tenant> page = tenantService.getTenantPage(pageNum, pageSize, keyword, status);
return Result.success(PageResult.of(page)); return Result.success(PageResult.of(page));
} }
@ -69,4 +72,45 @@ public class AdminTenantController {
return Result.success(tenantService.getAllActiveTenants()); return Result.success(tenantService.getAllActiveTenants());
} }
@Operation(summary = "Update tenant status")
@PutMapping("/{id}/status")
public Result<Map<String, Object>> updateTenantStatus(@PathVariable Long id, @RequestBody Map<String, String> body) {
String status = body.get("status");
// Normalize status to lowercase for DB
String dbStatus = status != null ? status.toLowerCase() : "active";
TenantUpdateRequest req = new TenantUpdateRequest();
req.setStatus(dbStatus);
Tenant tenant = tenantService.updateTenant(id, req);
Map<String, Object> result = new HashMap<>();
result.put("id", tenant.getId());
result.put("name", tenant.getName());
result.put("status", tenant.getStatus());
return Result.success(result);
}
@Operation(summary = "Reset tenant school account password")
@PostMapping("/{id}/reset-password")
public Result<Map<String, String>> resetTenantPassword(@PathVariable Long id) {
String tempPassword = tenantService.resetSchoolAccountPassword(id);
Map<String, String> result = new HashMap<>();
result.put("tempPassword", tempPassword);
return Result.success(result);
}
@Operation(summary = "Update tenant quota")
@PutMapping("/{id}/quota")
public Result<Tenant> updateTenantQuota(@PathVariable Long id, @RequestBody Map<String, Object> body) {
TenantUpdateRequest req = new TenantUpdateRequest();
if (body.get("teacherQuota") != null) {
req.setMaxTeachers(Integer.parseInt(body.get("teacherQuota").toString()));
}
if (body.get("studentQuota") != null) {
req.setMaxStudents(Integer.parseInt(body.get("studentQuota").toString()));
}
if (body.get("packageType") != null) {
req.setPackageType(body.get("packageType").toString());
}
return Result.success(tenantService.updateTenant(id, req));
}
} }

View File

@ -18,7 +18,7 @@ import java.util.List;
@Tag(name = "Parent - Growth Record", description = "Growth Record APIs for Parent") @Tag(name = "Parent - Growth Record", description = "Growth Record APIs for Parent")
@RestController @RestController
@RequestMapping("/api/parent/growth-records") @RequestMapping("/api/v1/parent/growth-records")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ParentGrowthController { public class ParentGrowthController {
@ -48,7 +48,7 @@ public class ParentGrowthController {
@GetMapping("/student/{studentId}") @GetMapping("/student/{studentId}")
public Result<PageResult<GrowthRecord>> getGrowthRecordsByStudent( public Result<PageResult<GrowthRecord>> getGrowthRecordsByStudent(
@PathVariable Long studentId, @PathVariable Long studentId,
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String type) { @RequestParam(required = false) String type) {
Page<GrowthRecord> page = growthRecordService.getGrowthRecordsByStudentId(studentId, pageNum, pageSize, type); Page<GrowthRecord> page = growthRecordService.getGrowthRecordsByStudentId(studentId, pageNum, pageSize, type);

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Parent - Notification", description = "Notification APIs for Parent") @Tag(name = "Parent - Notification", description = "Notification APIs for Parent")
@RestController @RestController
@RequestMapping("/api/parent/notifications") @RequestMapping("/api/v1/parent/notifications")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ParentNotificationController { public class ParentNotificationController {
@ -28,7 +28,7 @@ public class ParentNotificationController {
@Operation(summary = "Get my notifications") @Operation(summary = "Get my notifications")
@GetMapping @GetMapping
public Result<PageResult<Notification>> getMyNotifications( public Result<PageResult<Notification>> getMyNotifications(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) Integer isRead) { @RequestParam(required = false) Integer isRead) {
Long userId = SecurityUtils.getCurrentUserId(); Long userId = SecurityUtils.getCurrentUserId();

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Parent - Task", description = "Task APIs for Parent") @Tag(name = "Parent - Task", description = "Task APIs for Parent")
@RestController @RestController
@RequestMapping("/api/parent/tasks") @RequestMapping("/api/v1/parent/tasks")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ParentTaskController { public class ParentTaskController {
@ -29,7 +29,7 @@ public class ParentTaskController {
@GetMapping("/student/{studentId}") @GetMapping("/student/{studentId}")
public Result<PageResult<Task>> getTasksByStudent( public Result<PageResult<Task>> getTasksByStudent(
@PathVariable Long studentId, @PathVariable Long studentId,
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {
Page<Task> page = taskService.getTasksByStudentId(studentId, pageNum, pageSize, status); Page<Task> page = taskService.getTasksByStudentId(studentId, pageNum, pageSize, status);

View File

@ -18,7 +18,7 @@ import java.util.List;
@Tag(name = "School - Class", description = "Class Management APIs for School") @Tag(name = "School - Class", description = "Class Management APIs for School")
@RestController @RestController
@RequestMapping("/api/school/classes") @RequestMapping("/api/v1/school/classes")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SchoolClassController { public class SchoolClassController {
@ -46,7 +46,7 @@ public class SchoolClassController {
@Operation(summary = "Get class page") @Operation(summary = "Get class page")
@GetMapping @GetMapping
public Result<PageResult<Clazz>> getClassPage( public Result<PageResult<Clazz>> getClassPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String grade, @RequestParam(required = false) String grade,

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "School - Growth Record", description = "Growth Record Management APIs for School") @Tag(name = "School - Growth Record", description = "Growth Record Management APIs for School")
@RestController @RestController
@RequestMapping("/api/school/growth-records") @RequestMapping("/api/v1/school/growth-records")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SchoolGrowthController { public class SchoolGrowthController {
@ -46,7 +46,7 @@ public class SchoolGrowthController {
@Operation(summary = "Get growth record page") @Operation(summary = "Get growth record page")
@GetMapping @GetMapping
public Result<PageResult<GrowthRecord>> getGrowthRecordPage( public Result<PageResult<GrowthRecord>> getGrowthRecordPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) Long studentId, @RequestParam(required = false) Long studentId,
@RequestParam(required = false) String type) { @RequestParam(required = false) String type) {

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "School - Parent", description = "Parent Management APIs for School") @Tag(name = "School - Parent", description = "Parent Management APIs for School")
@RestController @RestController
@RequestMapping("/api/school/parents") @RequestMapping("/api/v1/school/parents")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SchoolParentController { public class SchoolParentController {
@ -44,7 +44,7 @@ public class SchoolParentController {
@Operation(summary = "Get parent page") @Operation(summary = "Get parent page")
@GetMapping @GetMapping
public Result<PageResult<Parent>> getParentPage( public Result<PageResult<Parent>> getParentPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "School - Student", description = "Student Management APIs for School") @Tag(name = "School - Student", description = "Student Management APIs for School")
@RestController @RestController
@RequestMapping("/api/school/students") @RequestMapping("/api/v1/school/students")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SchoolStudentController { public class SchoolStudentController {
@ -44,7 +44,7 @@ public class SchoolStudentController {
@Operation(summary = "Get student page") @Operation(summary = "Get student page")
@GetMapping @GetMapping
public Result<PageResult<Student>> getStudentPage( public Result<PageResult<Student>> getStudentPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String grade, @RequestParam(required = false) String grade,

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "School - Task", description = "Task Management APIs for School") @Tag(name = "School - Task", description = "Task Management APIs for School")
@RestController @RestController
@RequestMapping("/api/school/tasks") @RequestMapping("/api/v1/school/tasks")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SchoolTaskController { public class SchoolTaskController {
@ -46,7 +46,7 @@ public class SchoolTaskController {
@Operation(summary = "Get task page") @Operation(summary = "Get task page")
@GetMapping @GetMapping
public Result<PageResult<Task>> getTaskPage( public Result<PageResult<Task>> getTaskPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String type, @RequestParam(required = false) String type,

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "School - Teacher", description = "Teacher Management APIs for School") @Tag(name = "School - Teacher", description = "Teacher Management APIs for School")
@RestController @RestController
@RequestMapping("/api/school/teachers") @RequestMapping("/api/v1/school/teachers")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SchoolTeacherController { public class SchoolTeacherController {
@ -44,7 +44,7 @@ public class SchoolTeacherController {
@Operation(summary = "Get teacher page") @Operation(summary = "Get teacher page")
@GetMapping @GetMapping
public Result<PageResult<Teacher>> getTeacherPage( public Result<PageResult<Teacher>> getTeacherPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {

View File

@ -15,7 +15,7 @@ import java.util.List;
@Tag(name = "Teacher - Course", description = "Course APIs for Teacher") @Tag(name = "Teacher - Course", description = "Course APIs for Teacher")
@RestController @RestController
@RequestMapping("/api/teacher/courses") @RequestMapping("/api/v1/teacher/courses")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeacherCourseController { public class TeacherCourseController {
@ -30,7 +30,7 @@ public class TeacherCourseController {
@Operation(summary = "Get course page") @Operation(summary = "Get course page")
@GetMapping @GetMapping
public Result<PageResult<Course>> getCoursePage( public Result<PageResult<Course>> getCoursePage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String category) { @RequestParam(required = false) String category) {

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Teacher - Growth Record", description = "Growth Record APIs for Teacher") @Tag(name = "Teacher - Growth Record", description = "Growth Record APIs for Teacher")
@RestController @RestController
@RequestMapping("/api/teacher/growth-records") @RequestMapping("/api/v1/teacher/growth-records")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeacherGrowthController { public class TeacherGrowthController {
@ -45,7 +45,7 @@ public class TeacherGrowthController {
@Operation(summary = "Get growth record page") @Operation(summary = "Get growth record page")
@GetMapping @GetMapping
public Result<PageResult<GrowthRecord>> getGrowthRecordPage( public Result<PageResult<GrowthRecord>> getGrowthRecordPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) Long studentId, @RequestParam(required = false) Long studentId,
@RequestParam(required = false) String type) { @RequestParam(required = false) String type) {

View File

@ -20,7 +20,7 @@ import java.util.List;
@Tag(name = "Teacher - Lesson", description = "Lesson APIs for Teacher") @Tag(name = "Teacher - Lesson", description = "Lesson APIs for Teacher")
@RestController @RestController
@RequestMapping("/api/teacher/lessons") @RequestMapping("/api/v1/teacher/lessons")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeacherLessonController { public class TeacherLessonController {
@ -48,7 +48,7 @@ public class TeacherLessonController {
@Operation(summary = "Get my lessons") @Operation(summary = "Get my lessons")
@GetMapping @GetMapping
public Result<PageResult<Lesson>> getMyLessons( public Result<PageResult<Lesson>> getMyLessons(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Teacher - Notification", description = "Notification APIs for Teacher") @Tag(name = "Teacher - Notification", description = "Notification APIs for Teacher")
@RestController @RestController
@RequestMapping("/api/teacher/notifications") @RequestMapping("/api/v1/teacher/notifications")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeacherNotificationController { public class TeacherNotificationController {
@ -28,7 +28,7 @@ public class TeacherNotificationController {
@Operation(summary = "Get my notifications") @Operation(summary = "Get my notifications")
@GetMapping @GetMapping
public Result<PageResult<Notification>> getMyNotifications( public Result<PageResult<Notification>> getMyNotifications(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) Integer isRead) { @RequestParam(required = false) Integer isRead) {
Long userId = SecurityUtils.getCurrentUserId(); Long userId = SecurityUtils.getCurrentUserId();

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Teacher - Task", description = "Task APIs for Teacher") @Tag(name = "Teacher - Task", description = "Task APIs for Teacher")
@RestController @RestController
@RequestMapping("/api/teacher/tasks") @RequestMapping("/api/v1/teacher/tasks")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeacherTaskController { public class TeacherTaskController {
@ -45,7 +45,7 @@ public class TeacherTaskController {
@Operation(summary = "Get task page") @Operation(summary = "Get task page")
@GetMapping @GetMapping
public Result<PageResult<Task>> getTaskPage( public Result<PageResult<Task>> getTaskPage(
@RequestParam(required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String type, @RequestParam(required = false) String type,

View File

@ -1,5 +1,6 @@
package com.reading.platform.dto.request; package com.reading.platform.dto.request;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
@ -14,10 +15,12 @@ public class TenantCreateRequest {
@Schema(description = "Tenant name") @Schema(description = "Tenant name")
private String name; private String name;
@NotBlank(message = "Tenant code is required") @NotBlank(message = "Login account is required")
@Schema(description = "Tenant code") @JsonAlias("loginAccount")
@Schema(description = "Tenant code / login account")
private String code; private String code;
@JsonAlias("contactPerson")
@Schema(description = "Contact person") @Schema(description = "Contact person")
private String contactName; private String contactName;
@ -33,13 +36,25 @@ public class TenantCreateRequest {
@Schema(description = "Logo URL") @Schema(description = "Logo URL")
private String logoUrl; private String logoUrl;
@JsonAlias("expireDate")
@Schema(description = "Expiration date") @Schema(description = "Expiration date")
private LocalDateTime expireAt; private LocalDateTime expireAt;
@JsonAlias("studentQuota")
@Schema(description = "Max students") @Schema(description = "Max students")
private Integer maxStudents; private Integer maxStudents;
@JsonAlias("teacherQuota")
@Schema(description = "Max teachers") @Schema(description = "Max teachers")
private Integer maxTeachers; private Integer maxTeachers;
@Schema(description = "Initial password (default: 123456)")
private String password;
@Schema(description = "Package type (optional)")
private String packageType;
@Schema(description = "Start date (optional)")
private String startDate;
} }

View File

@ -1,5 +1,6 @@
package com.reading.platform.dto.request; package com.reading.platform.dto.request;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@ -12,6 +13,7 @@ public class TenantUpdateRequest {
@Schema(description = "Tenant name") @Schema(description = "Tenant name")
private String name; private String name;
@JsonAlias("contactPerson")
@Schema(description = "Contact person") @Schema(description = "Contact person")
private String contactName; private String contactName;
@ -30,13 +32,22 @@ public class TenantUpdateRequest {
@Schema(description = "Status") @Schema(description = "Status")
private String status; private String status;
@JsonAlias("expireDate")
@Schema(description = "Expiration date") @Schema(description = "Expiration date")
private LocalDateTime expireAt; private LocalDateTime expireAt;
@JsonAlias("studentQuota")
@Schema(description = "Max students") @Schema(description = "Max students")
private Integer maxStudents; private Integer maxStudents;
@JsonAlias("teacherQuota")
@Schema(description = "Max teachers") @Schema(description = "Max teachers")
private Integer maxTeachers; private Integer maxTeachers;
@Schema(description = "Package type (optional)")
private String packageType;
@Schema(description = "Start date (optional)")
private String startDate;
} }

View File

@ -17,11 +17,11 @@ public class TenantResponse {
@Schema(description = "Tenant name") @Schema(description = "Tenant name")
private String name; private String name;
@Schema(description = "Tenant code") @Schema(description = "Login account (tenant code)")
private String code; private String loginAccount;
@Schema(description = "Contact person") @Schema(description = "Contact person")
private String contactName; private String contactPerson;
@Schema(description = "Contact phone") @Schema(description = "Contact phone")
private String contactPhone; private String contactPhone;
@ -39,13 +39,13 @@ public class TenantResponse {
private String status; private String status;
@Schema(description = "Expiration date") @Schema(description = "Expiration date")
private LocalDateTime expireAt; private LocalDateTime expireDate;
@Schema(description = "Max students") @Schema(description = "Max students / student quota")
private Integer maxStudents; private Integer studentQuota;
@Schema(description = "Max teachers") @Schema(description = "Max teachers / teacher quota")
private Integer maxTeachers; private Integer teacherQuota;
@Schema(description = "Created at") @Schema(description = "Created at")
private LocalDateTime createdAt; private LocalDateTime createdAt;

View File

@ -1,6 +1,9 @@
package com.reading.platform.entity; package com.reading.platform.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.reading.platform.common.serializer.UpperCaseSerializer;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -17,8 +20,10 @@ public class Tenant {
private String name; private String name;
@JsonProperty("loginAccount")
private String code; private String code;
@JsonProperty("contactPerson")
private String contactName; private String contactName;
private String contactPhone; private String contactPhone;
@ -29,12 +34,16 @@ public class Tenant {
private String logoUrl; private String logoUrl;
@JsonSerialize(using = UpperCaseSerializer.class)
private String status; private String status;
@JsonProperty("expireDate")
private LocalDateTime expireAt; private LocalDateTime expireAt;
@JsonProperty("studentQuota")
private Integer maxStudents; private Integer maxStudents;
@JsonProperty("teacherQuota")
private Integer maxTeachers; private Integer maxTeachers;
@TableField(fill = FieldFill.INSERT) @TableField(fill = FieldFill.INSERT)
@ -46,4 +55,8 @@ public class Tenant {
@TableLogic @TableLogic
private Integer deleted; private Integer deleted;
// Not persisted - used in create response to return temp password
@TableField(exist = false)
private String tempPassword;
} }

View File

@ -0,0 +1,148 @@
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 {
private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final CourseMapper courseMapper;
private final LessonMapper lessonMapper;
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;
}
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());
}
}

View File

@ -25,4 +25,6 @@ public interface TenantService {
List<TenantResponse> getAllActiveTenants(); List<TenantResponse> getAllActiveTenants();
String resetSchoolAccountPassword(Long tenantId);
} }

View File

@ -193,10 +193,11 @@ public class AuthServiceImpl implements AuthService {
teacher.setLastLoginAt(LocalDateTime.now()); teacher.setLastLoginAt(LocalDateTime.now());
teacherMapper.updateById(teacher); teacherMapper.updateById(teacher);
// Use the requested role (school or teacher) from the login request
JwtPayload payload = JwtPayload.builder() JwtPayload payload = JwtPayload.builder()
.userId(teacher.getId()) .userId(teacher.getId())
.username(teacher.getUsername()) .username(teacher.getUsername())
.role("teacher") .role(role)
.tenantId(teacher.getTenantId()) .tenantId(teacher.getTenantId())
.name(teacher.getName()) .name(teacher.getName())
.build(); .build();
@ -206,7 +207,7 @@ public class AuthServiceImpl implements AuthService {
.userId(teacher.getId()) .userId(teacher.getId())
.username(teacher.getUsername()) .username(teacher.getUsername())
.name(teacher.getName()) .name(teacher.getName())
.role("teacher") .role(role)
.tenantId(teacher.getTenantId()) .tenantId(teacher.getTenantId())
.build(); .build();
} }
@ -263,7 +264,7 @@ public class AuthServiceImpl implements AuthService {
.tenantId(null) .tenantId(null)
.build(); .build();
} }
case "teacher" -> { case "teacher", "school" -> {
Teacher teacher = teacherMapper.selectById(payload.getUserId()); Teacher teacher = teacherMapper.selectById(payload.getUserId());
yield UserInfoResponse.builder() yield UserInfoResponse.builder()
.id(teacher.getId()) .id(teacher.getId())
@ -272,7 +273,7 @@ public class AuthServiceImpl implements AuthService {
.email(teacher.getEmail()) .email(teacher.getEmail())
.phone(teacher.getPhone()) .phone(teacher.getPhone())
.avatarUrl(teacher.getAvatarUrl()) .avatarUrl(teacher.getAvatarUrl())
.role("teacher") .role(role)
.tenantId(teacher.getTenantId()) .tenantId(teacher.getTenantId())
.build(); .build();
} }
@ -308,7 +309,7 @@ public class AuthServiceImpl implements AuthService {
adminUser.setPassword(passwordEncoder.encode(newPassword)); adminUser.setPassword(passwordEncoder.encode(newPassword));
adminUserMapper.updateById(adminUser); adminUserMapper.updateById(adminUser);
} }
case "teacher" -> { case "teacher", "school" -> {
Teacher teacher = teacherMapper.selectById(userId); Teacher teacher = teacherMapper.selectById(userId);
if (!passwordEncoder.matches(oldPassword, teacher.getPassword())) { if (!passwordEncoder.matches(oldPassword, teacher.getPassword())) {
throw new BusinessException(ErrorCode.OLD_PASSWORD_ERROR); throw new BusinessException(ErrorCode.OLD_PASSWORD_ERROR);

View File

@ -8,10 +8,13 @@ import com.reading.platform.common.util.PageUtils;
import com.reading.platform.dto.request.TenantCreateRequest; import com.reading.platform.dto.request.TenantCreateRequest;
import com.reading.platform.dto.request.TenantUpdateRequest; import com.reading.platform.dto.request.TenantUpdateRequest;
import com.reading.platform.dto.response.TenantResponse; import com.reading.platform.dto.response.TenantResponse;
import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.Tenant; import com.reading.platform.entity.Tenant;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.mapper.TenantMapper; import com.reading.platform.mapper.TenantMapper;
import com.reading.platform.service.TenantService; import com.reading.platform.service.TenantService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -24,6 +27,8 @@ import java.util.stream.Collectors;
public class TenantServiceImpl implements TenantService { public class TenantServiceImpl implements TenantService {
private final TenantMapper tenantMapper; private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final PasswordEncoder passwordEncoder;
@Override @Override
@Transactional @Transactional
@ -33,7 +38,7 @@ public class TenantServiceImpl implements TenantService {
new LambdaQueryWrapper<Tenant>().eq(Tenant::getCode, request.getCode()) new LambdaQueryWrapper<Tenant>().eq(Tenant::getCode, request.getCode())
); );
if (existing != null) { if (existing != null) {
throw new BusinessException(ErrorCode.DATA_ALREADY_EXISTS, "Tenant code already exists"); throw new BusinessException(ErrorCode.DATA_ALREADY_EXISTS, "Login account already exists");
} }
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
@ -50,6 +55,19 @@ public class TenantServiceImpl implements TenantService {
tenant.setStatus("active"); tenant.setStatus("active");
tenantMapper.insert(tenant); tenantMapper.insert(tenant);
// Create school login account in teachers table
String defaultPassword = StringUtils.hasText(request.getPassword()) ? request.getPassword() : "123456";
Teacher schoolAccount = new Teacher();
schoolAccount.setTenantId(tenant.getId());
schoolAccount.setUsername(request.getCode());
schoolAccount.setPassword(passwordEncoder.encode(defaultPassword));
schoolAccount.setName(request.getName());
schoolAccount.setStatus("active");
teacherMapper.insert(schoolAccount);
// Include temp password in response (cleared text for admin to share)
tenant.setTempPassword(defaultPassword);
return tenant; return tenant;
} }
@ -117,7 +135,8 @@ public class TenantServiceImpl implements TenantService {
); );
} }
if (StringUtils.hasText(status)) { if (StringUtils.hasText(status)) {
wrapper.eq(Tenant::getStatus, status); // Frontend sends uppercase (ACTIVE/SUSPENDED), DB stores lowercase - normalize
wrapper.eq(Tenant::getStatus, status.toLowerCase());
} }
wrapper.orderByDesc(Tenant::getCreatedAt); wrapper.orderByDesc(Tenant::getCreatedAt);
@ -131,6 +150,24 @@ public class TenantServiceImpl implements TenantService {
tenantMapper.deleteById(id); tenantMapper.deleteById(id);
} }
@Override
@Transactional
public String resetSchoolAccountPassword(Long tenantId) {
Tenant tenant = getTenantById(tenantId);
// Find the school account (username = tenant code)
Teacher schoolAccount = teacherMapper.selectOne(
new LambdaQueryWrapper<Teacher>()
.eq(Teacher::getTenantId, tenantId)
.eq(Teacher::getUsername, tenant.getCode())
);
String newPassword = "123456";
if (schoolAccount != null) {
schoolAccount.setPassword(passwordEncoder.encode(newPassword));
teacherMapper.updateById(schoolAccount);
}
return newPassword;
}
@Override @Override
public List<TenantResponse> getAllActiveTenants() { public List<TenantResponse> getAllActiveTenants() {
List<Tenant> tenants = tenantMapper.selectList( List<Tenant> tenants = tenantMapper.selectList(
@ -147,16 +184,16 @@ public class TenantServiceImpl implements TenantService {
return TenantResponse.builder() return TenantResponse.builder()
.id(tenant.getId()) .id(tenant.getId())
.name(tenant.getName()) .name(tenant.getName())
.code(tenant.getCode()) .loginAccount(tenant.getCode())
.contactName(tenant.getContactName()) .contactPerson(tenant.getContactName())
.contactPhone(tenant.getContactPhone()) .contactPhone(tenant.getContactPhone())
.contactEmail(tenant.getContactEmail()) .contactEmail(tenant.getContactEmail())
.address(tenant.getAddress()) .address(tenant.getAddress())
.logoUrl(tenant.getLogoUrl()) .logoUrl(tenant.getLogoUrl())
.status(tenant.getStatus()) .status(tenant.getStatus())
.expireAt(tenant.getExpireAt()) .expireDate(tenant.getExpireAt())
.maxStudents(tenant.getMaxStudents()) .studentQuota(tenant.getMaxStudents())
.maxTeachers(tenant.getMaxTeachers()) .teacherQuota(tenant.getMaxTeachers())
.createdAt(tenant.getCreatedAt()) .createdAt(tenant.getCreatedAt())
.build(); .build();
} }