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;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
@ -15,10 +16,13 @@ public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("items")
private List<T> list;
private Long total;
@JsonProperty("page")
private Long pageNum;
private Long pageSize;
@JsonProperty("totalPages")
private Long pages;
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")
@RestController
@RequestMapping("/api/auth")
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
@ -25,6 +25,13 @@ public class AuthController {
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")
@GetMapping("/me")
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")
@RestController
@RequestMapping("/api/admin/courses")
@RequestMapping("/api/v1/admin/courses")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminCourseController {
@ -48,7 +48,7 @@ public class AdminCourseController {
@Operation(summary = "Get system course page")
@GetMapping
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) String keyword,
@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 org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Tag(name = "Admin - Tenant", description = "Tenant Management APIs for Admin")
@RestController
@RequestMapping("/api/admin/tenants")
@RequestMapping("/api/v1/admin/tenants")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminTenantController {
@ -48,10 +50,11 @@ public class AdminTenantController {
@Operation(summary = "Get tenant page")
@GetMapping
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) 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);
return Result.success(PageResult.of(page));
}
@ -69,4 +72,45 @@ public class AdminTenantController {
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")
@RestController
@RequestMapping("/api/parent/growth-records")
@RequestMapping("/api/v1/parent/growth-records")
@RequiredArgsConstructor
public class ParentGrowthController {
@ -48,7 +48,7 @@ public class ParentGrowthController {
@GetMapping("/student/{studentId}")
public Result<PageResult<GrowthRecord>> getGrowthRecordsByStudent(
@PathVariable Long studentId,
@RequestParam(required = false) Integer pageNum,
@RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String 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")
@RestController
@RequestMapping("/api/parent/notifications")
@RequestMapping("/api/v1/parent/notifications")
@RequiredArgsConstructor
public class ParentNotificationController {
@ -28,7 +28,7 @@ public class ParentNotificationController {
@Operation(summary = "Get my notifications")
@GetMapping
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 isRead) {
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")
@RestController
@RequestMapping("/api/parent/tasks")
@RequestMapping("/api/v1/parent/tasks")
@RequiredArgsConstructor
public class ParentTaskController {
@ -29,7 +29,7 @@ public class ParentTaskController {
@GetMapping("/student/{studentId}")
public Result<PageResult<Task>> getTasksByStudent(
@PathVariable Long studentId,
@RequestParam(required = false) Integer pageNum,
@RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String 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")
@RestController
@RequestMapping("/api/school/classes")
@RequestMapping("/api/v1/school/classes")
@RequiredArgsConstructor
public class SchoolClassController {
@ -46,7 +46,7 @@ public class SchoolClassController {
@Operation(summary = "Get class page")
@GetMapping
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) String keyword,
@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")
@RestController
@RequestMapping("/api/school/growth-records")
@RequestMapping("/api/v1/school/growth-records")
@RequiredArgsConstructor
public class SchoolGrowthController {
@ -46,7 +46,7 @@ public class SchoolGrowthController {
@Operation(summary = "Get growth record page")
@GetMapping
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) Long studentId,
@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")
@RestController
@RequestMapping("/api/school/parents")
@RequestMapping("/api/v1/school/parents")
@RequiredArgsConstructor
public class SchoolParentController {
@ -44,7 +44,7 @@ public class SchoolParentController {
@Operation(summary = "Get parent page")
@GetMapping
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) String keyword,
@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")
@RestController
@RequestMapping("/api/school/students")
@RequestMapping("/api/v1/school/students")
@RequiredArgsConstructor
public class SchoolStudentController {
@ -44,7 +44,7 @@ public class SchoolStudentController {
@Operation(summary = "Get student page")
@GetMapping
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) String keyword,
@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")
@RestController
@RequestMapping("/api/school/tasks")
@RequestMapping("/api/v1/school/tasks")
@RequiredArgsConstructor
public class SchoolTaskController {
@ -46,7 +46,7 @@ public class SchoolTaskController {
@Operation(summary = "Get task page")
@GetMapping
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) String keyword,
@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")
@RestController
@RequestMapping("/api/school/teachers")
@RequestMapping("/api/v1/school/teachers")
@RequiredArgsConstructor
public class SchoolTeacherController {
@ -44,7 +44,7 @@ public class SchoolTeacherController {
@Operation(summary = "Get teacher page")
@GetMapping
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) String keyword,
@RequestParam(required = false) String status) {

View File

@ -15,7 +15,7 @@ import java.util.List;
@Tag(name = "Teacher - Course", description = "Course APIs for Teacher")
@RestController
@RequestMapping("/api/teacher/courses")
@RequestMapping("/api/v1/teacher/courses")
@RequiredArgsConstructor
public class TeacherCourseController {
@ -30,7 +30,7 @@ public class TeacherCourseController {
@Operation(summary = "Get course page")
@GetMapping
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) String keyword,
@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")
@RestController
@RequestMapping("/api/teacher/growth-records")
@RequestMapping("/api/v1/teacher/growth-records")
@RequiredArgsConstructor
public class TeacherGrowthController {
@ -45,7 +45,7 @@ public class TeacherGrowthController {
@Operation(summary = "Get growth record page")
@GetMapping
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) Long studentId,
@RequestParam(required = false) String type) {

View File

@ -20,7 +20,7 @@ import java.util.List;
@Tag(name = "Teacher - Lesson", description = "Lesson APIs for Teacher")
@RestController
@RequestMapping("/api/teacher/lessons")
@RequestMapping("/api/v1/teacher/lessons")
@RequiredArgsConstructor
public class TeacherLessonController {
@ -48,7 +48,7 @@ public class TeacherLessonController {
@Operation(summary = "Get my lessons")
@GetMapping
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) String status,
@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")
@RestController
@RequestMapping("/api/teacher/notifications")
@RequestMapping("/api/v1/teacher/notifications")
@RequiredArgsConstructor
public class TeacherNotificationController {
@ -28,7 +28,7 @@ public class TeacherNotificationController {
@Operation(summary = "Get my notifications")
@GetMapping
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 isRead) {
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")
@RestController
@RequestMapping("/api/teacher/tasks")
@RequestMapping("/api/v1/teacher/tasks")
@RequiredArgsConstructor
public class TeacherTaskController {
@ -45,7 +45,7 @@ public class TeacherTaskController {
@Operation(summary = "Get task page")
@GetMapping
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) String keyword,
@RequestParam(required = false) String type,

View File

@ -1,5 +1,6 @@
package com.reading.platform.dto.request;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@ -14,10 +15,12 @@ public class TenantCreateRequest {
@Schema(description = "Tenant name")
private String name;
@NotBlank(message = "Tenant code is required")
@Schema(description = "Tenant code")
@NotBlank(message = "Login account is required")
@JsonAlias("loginAccount")
@Schema(description = "Tenant code / login account")
private String code;
@JsonAlias("contactPerson")
@Schema(description = "Contact person")
private String contactName;
@ -33,13 +36,25 @@ public class TenantCreateRequest {
@Schema(description = "Logo URL")
private String logoUrl;
@JsonAlias("expireDate")
@Schema(description = "Expiration date")
private LocalDateTime expireAt;
@JsonAlias("studentQuota")
@Schema(description = "Max students")
private Integer maxStudents;
@JsonAlias("teacherQuota")
@Schema(description = "Max teachers")
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;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -12,6 +13,7 @@ public class TenantUpdateRequest {
@Schema(description = "Tenant name")
private String name;
@JsonAlias("contactPerson")
@Schema(description = "Contact person")
private String contactName;
@ -30,13 +32,22 @@ public class TenantUpdateRequest {
@Schema(description = "Status")
private String status;
@JsonAlias("expireDate")
@Schema(description = "Expiration date")
private LocalDateTime expireAt;
@JsonAlias("studentQuota")
@Schema(description = "Max students")
private Integer maxStudents;
@JsonAlias("teacherQuota")
@Schema(description = "Max teachers")
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")
private String name;
@Schema(description = "Tenant code")
private String code;
@Schema(description = "Login account (tenant code)")
private String loginAccount;
@Schema(description = "Contact person")
private String contactName;
private String contactPerson;
@Schema(description = "Contact phone")
private String contactPhone;
@ -39,13 +39,13 @@ public class TenantResponse {
private String status;
@Schema(description = "Expiration date")
private LocalDateTime expireAt;
private LocalDateTime expireDate;
@Schema(description = "Max students")
private Integer maxStudents;
@Schema(description = "Max students / student quota")
private Integer studentQuota;
@Schema(description = "Max teachers")
private Integer maxTeachers;
@Schema(description = "Max teachers / teacher quota")
private Integer teacherQuota;
@Schema(description = "Created at")
private LocalDateTime createdAt;

View File

@ -1,6 +1,9 @@
package com.reading.platform.entity;
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 java.time.LocalDateTime;
@ -17,8 +20,10 @@ public class Tenant {
private String name;
@JsonProperty("loginAccount")
private String code;
@JsonProperty("contactPerson")
private String contactName;
private String contactPhone;
@ -29,12 +34,16 @@ public class Tenant {
private String logoUrl;
@JsonSerialize(using = UpperCaseSerializer.class)
private String status;
@JsonProperty("expireDate")
private LocalDateTime expireAt;
@JsonProperty("studentQuota")
private Integer maxStudents;
@JsonProperty("teacherQuota")
private Integer maxTeachers;
@TableField(fill = FieldFill.INSERT)
@ -46,4 +55,8 @@ public class Tenant {
@TableLogic
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();
String resetSchoolAccountPassword(Long tenantId);
}

View File

@ -193,10 +193,11 @@ public class AuthServiceImpl implements AuthService {
teacher.setLastLoginAt(LocalDateTime.now());
teacherMapper.updateById(teacher);
// Use the requested role (school or teacher) from the login request
JwtPayload payload = JwtPayload.builder()
.userId(teacher.getId())
.username(teacher.getUsername())
.role("teacher")
.role(role)
.tenantId(teacher.getTenantId())
.name(teacher.getName())
.build();
@ -206,7 +207,7 @@ public class AuthServiceImpl implements AuthService {
.userId(teacher.getId())
.username(teacher.getUsername())
.name(teacher.getName())
.role("teacher")
.role(role)
.tenantId(teacher.getTenantId())
.build();
}
@ -263,7 +264,7 @@ public class AuthServiceImpl implements AuthService {
.tenantId(null)
.build();
}
case "teacher" -> {
case "teacher", "school" -> {
Teacher teacher = teacherMapper.selectById(payload.getUserId());
yield UserInfoResponse.builder()
.id(teacher.getId())
@ -272,7 +273,7 @@ public class AuthServiceImpl implements AuthService {
.email(teacher.getEmail())
.phone(teacher.getPhone())
.avatarUrl(teacher.getAvatarUrl())
.role("teacher")
.role(role)
.tenantId(teacher.getTenantId())
.build();
}
@ -308,7 +309,7 @@ public class AuthServiceImpl implements AuthService {
adminUser.setPassword(passwordEncoder.encode(newPassword));
adminUserMapper.updateById(adminUser);
}
case "teacher" -> {
case "teacher", "school" -> {
Teacher teacher = teacherMapper.selectById(userId);
if (!passwordEncoder.matches(oldPassword, teacher.getPassword())) {
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.TenantUpdateRequest;
import com.reading.platform.dto.response.TenantResponse;
import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.Tenant;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.mapper.TenantMapper;
import com.reading.platform.service.TenantService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -24,6 +27,8 @@ import java.util.stream.Collectors;
public class TenantServiceImpl implements TenantService {
private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final PasswordEncoder passwordEncoder;
@Override
@Transactional
@ -33,7 +38,7 @@ public class TenantServiceImpl implements TenantService {
new LambdaQueryWrapper<Tenant>().eq(Tenant::getCode, request.getCode())
);
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();
@ -50,6 +55,19 @@ public class TenantServiceImpl implements TenantService {
tenant.setStatus("active");
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;
}
@ -117,7 +135,8 @@ public class TenantServiceImpl implements TenantService {
);
}
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);
@ -131,6 +150,24 @@ public class TenantServiceImpl implements TenantService {
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
public List<TenantResponse> getAllActiveTenants() {
List<Tenant> tenants = tenantMapper.selectList(
@ -147,16 +184,16 @@ public class TenantServiceImpl implements TenantService {
return TenantResponse.builder()
.id(tenant.getId())
.name(tenant.getName())
.code(tenant.getCode())
.contactName(tenant.getContactName())
.loginAccount(tenant.getCode())
.contactPerson(tenant.getContactName())
.contactPhone(tenant.getContactPhone())
.contactEmail(tenant.getContactEmail())
.address(tenant.getAddress())
.logoUrl(tenant.getLogoUrl())
.status(tenant.getStatus())
.expireAt(tenant.getExpireAt())
.maxStudents(tenant.getMaxStudents())
.maxTeachers(tenant.getMaxTeachers())
.expireDate(tenant.getExpireAt())
.studentQuota(tenant.getMaxStudents())
.teacherQuota(tenant.getMaxTeachers())
.createdAt(tenant.getCreatedAt())
.build();
}