This commit is contained in:
zhonghua 2026-04-02 14:22:56 +08:00
parent 746f5d85ec
commit f40b0e7c94
110 changed files with 677 additions and 12517 deletions

View File

@ -61,10 +61,6 @@ SELECT
FROM t_sys_tenant t
JOIN (
SELECT 'activity:read' AS code UNION ALL
SELECT 'ai-3d:create' UNION ALL
SELECT 'class:create' UNION ALL
SELECT 'class:delete' UNION ALL
SELECT 'class:update' UNION ALL
SELECT 'config:create' UNION ALL
SELECT 'config:delete' UNION ALL
SELECT 'config:update' UNION ALL
@ -73,19 +69,9 @@ JOIN (
SELECT 'contest:publish' UNION ALL
SELECT 'contest:read' UNION ALL
SELECT 'contest:update' UNION ALL
SELECT 'department:create' UNION ALL
SELECT 'department:delete' UNION ALL
SELECT 'department:update' UNION ALL
SELECT 'dict:create' UNION ALL
SELECT 'dict:delete' UNION ALL
SELECT 'dict:update' UNION ALL
SELECT 'grade:create' UNION ALL
SELECT 'grade:delete' UNION ALL
SELECT 'grade:update' UNION ALL
SELECT 'homework:create' UNION ALL
SELECT 'homework:delete' UNION ALL
SELECT 'homework:read' UNION ALL
SELECT 'homework:update' UNION ALL
SELECT 'judge:create' UNION ALL
SELECT 'judge:delete' UNION ALL
SELECT 'judge:read' UNION ALL
@ -105,14 +91,6 @@ JOIN (
SELECT 'role:delete' UNION ALL
SELECT 'role:read' UNION ALL
SELECT 'role:update' UNION ALL
SELECT 'school:create' UNION ALL
SELECT 'school:update' UNION ALL
SELECT 'student:create' UNION ALL
SELECT 'student:delete' UNION ALL
SELECT 'student:update' UNION ALL
SELECT 'teacher:create' UNION ALL
SELECT 'teacher:delete' UNION ALL
SELECT 'teacher:update' UNION ALL
SELECT 'tenant:create' UNION ALL
SELECT 'tenant:delete' UNION ALL
SELECT 'tenant:update' UNION ALL

View File

@ -1,30 +0,0 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 作业状态枚举
*/
@Getter
@AllArgsConstructor
public enum HomeworkStatusEnum {
UNPUBLISHED("unpublished", "未发布"),
PUBLISHED("published", "已发布");
private final String code;
private final String desc;
public static HomeworkStatusEnum getByCode(String code) {
if (code == null) {
return UNPUBLISHED;
}
for (HomeworkStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return UNPUBLISHED;
}
}

View File

@ -1,31 +0,0 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 作业提交状态枚举
*/
@Getter
@AllArgsConstructor
public enum HomeworkSubmissionStatusEnum {
PENDING("pending", "待评审"),
REVIEWED("reviewed", "已评审"),
REJECTED("rejected", "已拒绝");
private final String code;
private final String desc;
public static HomeworkSubmissionStatusEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (HomeworkSubmissionStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return PENDING;
}
}

View File

@ -5,8 +5,10 @@ import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.registration.*;
import com.lesingle.creation.service.ContestRegistrationService;
import com.lesingle.creation.service.UserService;
import com.lesingle.creation.vo.registration.RegistrationStatsVO;
import com.lesingle.creation.vo.registration.RegistrationVO;
import com.lesingle.creation.vo.user.UserListVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.*;
public class ContestRegistrationController {
private final ContestRegistrationService registrationService;
private final UserService userService;
@PostMapping
@Operation(summary = "创建报名")
@ -57,6 +60,21 @@ public class ContestRegistrationController {
return Result.success(result);
}
@GetMapping("/candidate-users")
@Operation(summary = "报名场景按角色分页查询可选用户teacher/student")
@PreAuthorize("hasAnyAuthority('contest:read','contest:register','contest:update','registration:read','user:read')")
public Result<Page<UserListVO>> pageCandidateUsers(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam String roleCode,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
Long tenantId = userPrincipal.getTenantId();
Page<UserListVO> result =
userService.pageCandidateUsersByRoleCode(tenantId, roleCode, keyword, page, pageSize);
return Result.success(result);
}
@GetMapping("/my/{contestId}")
@Operation(summary = "获取用户在某活动中的报名记录")
@PreAuthorize("hasAuthority('contest:read')")

View File

@ -1,97 +0,0 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.department.CreateDepartmentDTO;
import com.lesingle.creation.dto.department.UpdateDepartmentDTO;
import com.lesingle.creation.service.DepartmentService;
import com.lesingle.creation.vo.department.DepartmentVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 部门管理控制器
*/
@Tag(name = "部门管理", description = "部门 CRUD 和树形结构接口")
@RestController
@RequestMapping("/api/departments")
@RequiredArgsConstructor
public class DepartmentController {
private final DepartmentService departmentService;
@PostMapping
@Operation(summary = "创建部门")
@PreAuthorize("hasAuthority('department:create')")
public Result<DepartmentVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateDepartmentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
DepartmentVO result = departmentService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "查询部门列表")
@PreAuthorize("hasAuthority('department:read')")
public Result<List<DepartmentVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) Long parentId) {
Long tenantId = userPrincipal.getTenantId();
List<DepartmentVO> result = departmentService.list(tenantId, parentId);
return Result.success(result);
}
@GetMapping("/tree")
@Operation(summary = "查询部门树形结构")
@PreAuthorize("hasAuthority('department:read')")
public Result<List<DepartmentVO>> tree(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<DepartmentVO> result = departmentService.tree(tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取部门详情")
@PreAuthorize("hasAuthority('department:read')")
public Result<DepartmentVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
DepartmentVO result = departmentService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新部门")
@PreAuthorize("hasAuthority('department:update')")
public Result<DepartmentVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateDepartmentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
DepartmentVO result = departmentService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除部门")
@PreAuthorize("hasAuthority('department:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
departmentService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -1,87 +0,0 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.grade.CreateGradeDTO;
import com.lesingle.creation.dto.grade.UpdateGradeDTO;
import com.lesingle.creation.service.GradeService;
import com.lesingle.creation.vo.grade.GradeVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
* 年级管理控制器
*/
@Tag(name = "年级管理", description = "年级 CRUD 接口")
@RestController
@RequestMapping("/api/grades")
@RequiredArgsConstructor
public class GradeController {
private final GradeService gradeService;
@PostMapping
@Operation(summary = "创建年级")
@PreAuthorize("hasAuthority('grade:create')")
public Result<GradeVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateGradeDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
GradeVO result = gradeService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "分页查询年级列表")
@PreAuthorize("hasAuthority('grade:read')")
public Result<Page<GradeVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) {
Long tenantId = userPrincipal.getTenantId();
Page<GradeVO> result = gradeService.pageQuery(pageNum, pageSize, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取年级详情")
@PreAuthorize("hasAuthority('grade:read')")
public Result<GradeVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
GradeVO result = gradeService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新年级")
@PreAuthorize("hasAuthority('grade:update')")
public Result<GradeVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateGradeDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
GradeVO result = gradeService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除年级")
@PreAuthorize("hasAuthority('grade:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
gradeService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -1,292 +0,0 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.dto.homework.*;
import com.lesingle.creation.vo.school.ClassTreeNodeVO;
import com.lesingle.creation.service.HomeworkService;
import com.lesingle.creation.vo.homework.HomeworkDetailVO;
import com.lesingle.creation.vo.homework.HomeworkListVO;
import com.lesingle.creation.vo.homework.ReviewRuleVO;
import com.lesingle.creation.vo.homework.SubmissionVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import com.lesingle.creation.common.security.UserPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 作业管理控制器
*/
@Tag(name = "作业管理")
@RestController
@RequestMapping("/api/homeworks")
@RequiredArgsConstructor
public class HomeworkController {
private final HomeworkService homeworkService;
@PostMapping
@Operation(summary = "创建作业")
@PreAuthorize("hasAuthority('homework:create')")
public Result<HomeworkDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
HomeworkDetailVO result = homeworkService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新作业")
@PreAuthorize("hasAuthority('homework:update')")
public Result<HomeworkDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.update(id, dto, tenantId);
return Result.success(result);
}
@PostMapping("/{id}/publish")
@Operation(summary = "发布作业")
@PreAuthorize("hasAuthority('homework:publish')")
public Result<HomeworkDetailVO> publish(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.publish(id, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除作业")
@PreAuthorize("hasAuthority('homework:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.delete(id, tenantId);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取作业详情")
@PreAuthorize("hasAuthority('homework:read')")
public Result<HomeworkDetailVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/page")
@Operation(summary = "分页查询作业列表")
@PreAuthorize("hasAuthority('homework:read')")
public Result<Page<HomeworkListVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute HomeworkQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<HomeworkListVO> result = homeworkService.pageQuery(queryDTO, tenantId);
return Result.success(result);
}
@PostMapping("/submit")
@Operation(summary = "提交作业")
public Result<SubmissionVO> submit(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated SubmitHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long studentId = userPrincipal.getUserId();
SubmissionVO result = homeworkService.submit(dto, tenantId, studentId);
return Result.success(result);
}
@PostMapping("/review")
@Operation(summary = "批改作业")
@PreAuthorize("hasAuthority('homework:review')")
public Result<SubmissionVO> review(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated ReviewHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long reviewerId = userPrincipal.getUserId();
SubmissionVO result = homeworkService.review(dto, tenantId, reviewerId);
return Result.success(result);
}
@GetMapping("/{homeworkId}/submissions")
@Operation(summary = "获取作业提交列表")
@PreAuthorize("hasAuthority('homework:review')")
public Result<List<SubmissionVO>> getSubmissions(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long homeworkId) {
Long tenantId = userPrincipal.getTenantId();
List<SubmissionVO> result = homeworkService.getSubmissions(homeworkId, tenantId);
return Result.success(result);
}
@PostMapping("/review-rules")
@Operation(summary = "创建评审规则")
@PreAuthorize("hasAuthority('homework:review-rule:create')")
public Result<ReviewRuleVO> createReviewRule(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam String ruleName,
@RequestParam(required = false, defaultValue = "custom") String ruleType,
@RequestParam(required = false) java.math.BigDecimal totalScore,
@RequestParam(required = false) String description) {
Long tenantId = userPrincipal.getTenantId();
ReviewRuleVO result = homeworkService.createReviewRule(
ruleName, ruleType, totalScore, description, tenantId);
return Result.success(result);
}
@GetMapping("/review-rules")
@Operation(summary = "获取评审规则列表")
public Result<List<ReviewRuleVO>> getReviewRules(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewRuleVO> result = homeworkService.getReviewRules(tenantId);
return Result.success(result);
}
@GetMapping("/review-rules/select")
@Operation(summary = "获取可选的评审规则")
public Result<List<ReviewRuleVO>> getReviewRulesForSelect(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewRuleVO> result = homeworkService.getReviewRulesForSelect(tenantId);
return Result.success(result);
}
@PutMapping("/review-rules/{id}")
@Operation(summary = "更新评审规则")
@PreAuthorize("hasAuthority('homework:review-rule:update')")
public Result<ReviewRuleVO> updateReviewRule(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateReviewRuleDTO dto) {
Long tenantId = userPrincipal.getTenantId();
ReviewRuleVO result = homeworkService.updateReviewRule(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/review-rules/{id}")
@Operation(summary = "删除评审规则")
@PreAuthorize("hasAuthority('homework:review-rule:delete')")
public Result<Void> deleteReviewRule(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.deleteReviewRule(id, tenantId);
return Result.success(null);
}
@GetMapping("/my")
@Operation(summary = "我的作业列表(学生端)")
public Result<Page<HomeworkListVO>> getMyHomeworks(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute HomeworkQueryDTO queryDTO) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
Page<HomeworkListVO> result = homeworkService.getMyHomeworks(queryDTO, userId, tenantId);
return Result.success(result);
}
@PostMapping("/{id}/unpublish")
@Operation(summary = "取消发布作业")
@PreAuthorize("hasAuthority('homework:publish')")
public Result<HomeworkDetailVO> unpublish(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.unpublish(id, tenantId);
return Result.success(result);
}
@GetMapping("/submissions")
@Operation(summary = "获取提交记录列表")
@PreAuthorize("hasAuthority('homework:review')")
public Result<Page<SubmissionVO>> getSubmissionsList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute SubmissionQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<SubmissionVO> result = homeworkService.getSubmissionsList(queryDTO, tenantId);
return Result.success(result);
}
@GetMapping("/submissions/{id}")
@Operation(summary = "获取提交记录详情")
@PreAuthorize("hasAuthority('homework:review')")
public Result<SubmissionVO> getSubmissionDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
SubmissionVO result = homeworkService.getSubmissionDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/submissions/class-tree")
@Operation(summary = "获取班级树结构")
@PreAuthorize("hasAuthority('homework:review')")
public Result<List<ClassTreeNodeVO>> getClassTree(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ClassTreeNodeVO> result = homeworkService.getClassTree(tenantId);
return Result.success(result);
}
@GetMapping("/submissions/my/{homeworkId}")
@Operation(summary = "获取我的提交记录")
public Result<SubmissionVO> getMySubmission(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long homeworkId) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
SubmissionVO result = homeworkService.getMySubmission(homeworkId, userId, tenantId);
return result != null ? Result.success(result) : Result.success(null);
}
@PostMapping("/scores")
@Operation(summary = "提交评分")
@PreAuthorize("hasAuthority('homework:review')")
public Result<SubmissionVO> createScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateScoreDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long reviewerId = userPrincipal.getUserId();
SubmissionVO result = homeworkService.createScore(dto, tenantId, reviewerId);
return Result.success(result);
}
@PostMapping("/scores/{submissionId}/violation")
@Operation(summary = "标记作品违规")
@PreAuthorize("hasAuthority('homework:review')")
public Result<Void> markViolation(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long submissionId,
@RequestParam(required = false) String reason) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.markViolation(submissionId, reason, tenantId);
return Result.success(null);
}
@PostMapping("/scores/{submissionId}/reset")
@Operation(summary = "重置评分")
@PreAuthorize("hasAuthority('homework:review')")
public Result<Void> resetScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long submissionId) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.resetScore(submissionId, tenantId);
return Result.success(null);
}
}

View File

@ -1,89 +0,0 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.schoolclass.CreateClassDTO;
import com.lesingle.creation.dto.schoolclass.UpdateClassDTO;
import com.lesingle.creation.service.SchoolClassService;
import com.lesingle.creation.vo.schoolclass.ClassVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
* 班级管理控制器
*/
@Tag(name = "班级管理", description = "班级 CRUD 接口")
@RestController
@RequestMapping("/api/classes")
@RequiredArgsConstructor
public class SchoolClassController {
private final SchoolClassService schoolClassService;
@PostMapping
@Operation(summary = "创建班级")
@PreAuthorize("hasAuthority('class:create')")
public Result<ClassVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateClassDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
ClassVO result = schoolClassService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "分页查询班级列表")
@PreAuthorize("hasAuthority('class:read')")
public Result<Page<ClassVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long gradeId,
@RequestParam(required = false) Integer type) {
Long tenantId = userPrincipal.getTenantId();
Page<ClassVO> result = schoolClassService.pageQuery(pageNum, pageSize, tenantId, gradeId, type);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取班级详情")
@PreAuthorize("hasAuthority('class:read')")
public Result<ClassVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ClassVO result = schoolClassService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新班级")
@PreAuthorize("hasAuthority('class:update')")
public Result<ClassVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateClassDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
ClassVO result = schoolClassService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除班级")
@PreAuthorize("hasAuthority('class:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
schoolClassService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -1,71 +0,0 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.school.CreateSchoolDTO;
import com.lesingle.creation.dto.school.UpdateSchoolDTO;
import com.lesingle.creation.service.SchoolService;
import com.lesingle.creation.vo.school.SchoolVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 学校管理控制器
*/
@Tag(name = "学校管理")
@RestController
@RequestMapping("/api/schools")
@RequiredArgsConstructor
public class SchoolController {
private final SchoolService schoolService;
@PostMapping
@Operation(summary = "创建学校")
@PreAuthorize("hasAuthority('school:create')")
public Result<SchoolVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateSchoolDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
SchoolVO result = schoolService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "获取学校信息(未创建时 data 为 null")
@PreAuthorize("hasAuthority('school:read')")
public Result<SchoolVO> get(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
SchoolVO result = schoolService.getByTenantId(tenantId);
return Result.success(result);
}
@PutMapping
@Operation(summary = "更新学校")
@PreAuthorize("hasAuthority('school:update')")
public Result<SchoolVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated UpdateSchoolDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
SchoolVO result = schoolService.update(dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping
@Operation(summary = "删除学校")
@PreAuthorize("hasAuthority('school:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
schoolService.delete(tenantId);
return Result.success();
}
}

View File

@ -1,100 +0,0 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.student.CreateStudentDTO;
import com.lesingle.creation.dto.student.UpdateStudentDTO;
import com.lesingle.creation.service.StudentService;
import com.lesingle.creation.vo.student.StudentVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 学生管理控制器
*/
@Tag(name = "学生管理", description = "学生 CRUD 和查询接口")
@RestController
@RequestMapping("/api/students")
@RequiredArgsConstructor
public class StudentController {
private final StudentService studentService;
@PostMapping
@Operation(summary = "创建学生")
@PreAuthorize("hasAuthority('student:create')")
public Result<StudentVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateStudentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
StudentVO result = studentService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "查询学生列表")
@PreAuthorize("hasAuthority('student:read')")
public Result<List<StudentVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long classId) {
Long tenantId = userPrincipal.getTenantId();
List<StudentVO> result = studentService.list(tenantId, page, pageSize, classId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取学生详情")
@PreAuthorize("hasAuthority('student:read')")
public Result<StudentVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
StudentVO result = studentService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/user/{userId}")
@Operation(summary = "根据用户 ID 获取学生信息")
@PreAuthorize("hasAuthority('student:read')")
public Result<StudentVO> getByUserId(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long userId) {
Long tenantId = userPrincipal.getTenantId();
StudentVO result = studentService.getByUserId(userId, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新学生")
@PreAuthorize("hasAuthority('student:update')")
public Result<StudentVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateStudentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
StudentVO result = studentService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除学生")
@PreAuthorize("hasAuthority('student:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
studentService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -1,100 +0,0 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.teacher.CreateTeacherDTO;
import com.lesingle.creation.dto.teacher.UpdateTeacherDTO;
import com.lesingle.creation.service.TeacherService;
import com.lesingle.creation.vo.teacher.TeacherVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 教师管理控制器
*/
@Tag(name = "教师管理", description = "教师 CRUD 和查询接口")
@RestController
@RequestMapping("/api/teachers")
@RequiredArgsConstructor
public class TeacherController {
private final TeacherService teacherService;
@PostMapping
@Operation(summary = "创建教师")
@PreAuthorize("hasAuthority('teacher:create')")
public Result<TeacherVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateTeacherDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
TeacherVO result = teacherService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "查询教师列表")
@PreAuthorize("hasAuthority('teacher:read')")
public Result<List<TeacherVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) Long departmentId,
@RequestParam(required = false) String nickname,
@RequestParam(required = false) String username) {
Long tenantId = userPrincipal.getTenantId();
List<TeacherVO> result = teacherService.list(tenantId, departmentId, nickname, username);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取教师详情")
@PreAuthorize("hasAuthority('teacher:read')")
public Result<TeacherVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
TeacherVO result = teacherService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/user/{userId}")
@Operation(summary = "根据用户 ID 获取教师信息")
@PreAuthorize("hasAuthority('teacher:read')")
public Result<TeacherVO> getByUserId(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long userId) {
Long tenantId = userPrincipal.getTenantId();
TeacherVO result = teacherService.getByUserId(userId, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新教师")
@PreAuthorize("hasAuthority('teacher:update')")
public Result<TeacherVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateTeacherDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
TeacherVO result = teacherService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除教师")
@PreAuthorize("hasAuthority('teacher:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
teacherService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -1,30 +0,0 @@
package com.lesingle.creation.dto.department;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 创建部门 DTO
*/
@Data
@Schema(description = "创建部门请求")
public class CreateDepartmentDTO {
@NotBlank(message = "部门名称不能为空")
@Schema(description = "部门名称")
private String name;
@NotBlank(message = "部门编码不能为空")
@Schema(description = "部门编码")
private String code;
@Schema(description = "父部门 ID")
private Long parentId;
@Schema(description = "部门描述")
private String description;
@Schema(description = "排序")
private Integer sort = 0;
}

View File

@ -1,30 +0,0 @@
package com.lesingle.creation.dto.department;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 更新部门 DTO
*/
@Data
@Schema(description = "更新部门请求")
public class UpdateDepartmentDTO {
@Schema(description = "部门名称")
private String name;
@Schema(description = "部门编码")
private String code;
@Schema(description = "父部门 ID")
private Long parentId;
@Schema(description = "部门描述")
private String description;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "有效状态1-有效2-失效")
private Integer validState;
}

View File

@ -1,29 +0,0 @@
package com.lesingle.creation.dto.grade;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 创建年级 DTO
*/
@Data
@Schema(description = "创建年级请求")
public class CreateGradeDTO {
@NotBlank(message = "年级名称不能为空")
@Schema(description = "年级名称")
private String name;
@NotBlank(message = "年级编码不能为空")
@Schema(description = "年级编码")
private String code;
@NotNull(message = "年级级别不能为空")
@Schema(description = "年级级别")
private Integer level;
@Schema(description = "年级描述")
private String description;
}

View File

@ -1,24 +0,0 @@
package com.lesingle.creation.dto.grade;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 更新年级 DTO
*/
@Data
@Schema(description = "更新年级请求")
public class UpdateGradeDTO {
@Schema(description = "年级名称")
private String name;
@Schema(description = "年级编码")
private String code;
@Schema(description = "年级级别")
private Integer level;
@Schema(description = "年级描述")
private String description;
}

View File

@ -1,40 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 创建作业请求 DTO
*/
@Data
@Schema(description = "创建作业请求")
public class CreateHomeworkDTO {
@NotBlank(message = "作业名称不能为空")
@Schema(description = "作业名称", example = "创意绘画练习")
private String name;
@Schema(description = "作业内容(富文本)")
private String content;
@NotNull(message = "提交开始时间不能为空")
@Schema(description = "提交开始时间")
private LocalDateTime submitStartTime;
@NotNull(message = "提交结束时间不能为空")
@Schema(description = "提交结束时间")
private LocalDateTime submitEndTime;
@Schema(description = "附件列表JSON 数组)")
private String attachments;
@Schema(description = "公开范围(班级 ID 数组 JSON")
private String publishScope;
@Schema(description = "评审规则 ID")
private Long reviewRuleId;
}

View File

@ -1,27 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
/**
* 创建评分请求 DTO
*/
@Data
@Schema(description = "创建评分请求")
public class CreateScoreDTO {
@NotNull(message = "提交 ID 不能为空")
@Schema(description = "提交 ID")
private Long submissionId;
@Schema(description = "维度评分列表")
private List<DimensionScoreDTO> dimensionScores;
@Schema(description = "评语")
private String comments;
}

View File

@ -1,27 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 维度评分 DTO
*/
@Data
@Schema(description = "维度评分")
public class DimensionScoreDTO {
@NotBlank(message = "维度名称不能为空")
@Schema(description = "维度名称")
private String name;
@NotNull(message = "得分不能为空")
@Schema(description = "得分")
private BigDecimal score;
@Schema(description = "满分")
private BigDecimal maxScore;
}

View File

@ -1,27 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 作业查询 DTO
*/
@Data
@Schema(description = "作业查询参数")
public class HomeworkQueryDTO {
@Schema(description = "页码", example = "1")
private Integer pageNum = 1;
@Schema(description = "每页数量", example = "10")
private Integer pageSize = 10;
@Schema(description = "作业名称关键字")
private String nameKeyword;
@Schema(description = "作业状态unpublished/published")
private String status;
@Schema(description = "有效状态1-有效2-失效")
private Integer validState;
}

View File

@ -1,27 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 评审维度 DTO
*/
@Data
@Schema(description = "评审维度")
public class ReviewDimensionDTO {
@Schema(description = "维度名称")
@NotBlank(message = "维度名称不能为空")
private String name;
@Schema(description = "满分")
@NotNull(message = "满分不能为空")
private BigDecimal maxScore;
@Schema(description = "维度描述")
private String description;
}

View File

@ -1,26 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 批改作业请求 DTO
*/
@Data
@Schema(description = "批改作业请求")
public class ReviewHomeworkDTO {
@NotNull(message = "提交 ID 不能为空")
@Schema(description = "提交 ID")
private Long submissionId;
@Schema(description = "批改分数")
private BigDecimal score;
@Schema(description = "批改评语")
private String comment;
}

View File

@ -1,42 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 提交记录查询 DTO
*/
@Data
@Schema(description = "提交记录查询参数")
public class SubmissionQueryDTO {
@Schema(description = "页码", example = "1")
private Integer pageNum = 1;
@Schema(description = "每页大小", example = "10")
private Integer pageSize = 10;
@Schema(description = "作业 ID")
private Long homeworkId;
@Schema(description = "作品编号")
private String workNo;
@Schema(description = "作品名称")
private String workName;
@Schema(description = "学生账号")
private String studentAccount;
@Schema(description = "学生姓名")
private String studentName;
@Schema(description = "提交状态")
private String status;
@Schema(description = "班级 ID 列表")
private java.util.List<Long> classIds;
@Schema(description = "年级 ID")
private Long gradeId;
}

View File

@ -1,26 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 提交作业请求 DTO
*/
@Data
@Schema(description = "提交作业请求")
public class SubmitHomeworkDTO {
@NotNull(message = "作业 ID 不能为空")
@Schema(description = "作业 ID")
private Long homeworkId;
@Schema(description = "提交内容")
private String content;
@Schema(description = "提交附件列表JSON 数组)")
private String attachments;
}

View File

@ -1,37 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
/**
* 更新作业请求 DTO
*/
@Data
@Schema(description = "更新作业请求")
public class UpdateHomeworkDTO {
@NotBlank(message = "作业名称不能为空")
@Schema(description = "作业名称", example = "创意绘画练习")
private String name;
@Schema(description = "作业内容(富文本)")
private String content;
@Schema(description = "提交开始时间")
private LocalDateTime submitStartTime;
@Schema(description = "提交结束时间")
private LocalDateTime submitEndTime;
@Schema(description = "附件列表JSON 数组)")
private String attachments;
@Schema(description = "公开范围(班级 ID 数组 JSON")
private String publishScope;
@Schema(description = "评审规则 ID")
private Long reviewRuleId;
}

View File

@ -1,26 +0,0 @@
package com.lesingle.creation.dto.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
/**
* 更新评审规则请求 DTO
*/
@Data
@Schema(description = "更新评审规则请求")
public class UpdateReviewRuleDTO {
@Schema(description = "规则名称")
private String ruleName;
@Schema(description = "规则描述")
private String description;
@Schema(description = "评审维度列表")
private List<ReviewDimensionDTO> dimensions;
}

View File

@ -1,36 +0,0 @@
package com.lesingle.creation.dto.school;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDate;
/**
* 创建学校 DTO
*/
@Data
@Schema(description = "创建学校请求")
public class CreateSchoolDTO {
@Schema(description = "学校地址")
private String address;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "校长姓名")
private String principal;
@Schema(description = "建校时间")
private LocalDate established;
@Schema(description = "学校描述")
private String description;
@Schema(description = "学校 Logo URL")
private String logo;
@Schema(description = "学校网站")
private String website;
}

View File

@ -1,35 +0,0 @@
package com.lesingle.creation.dto.school;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
/**
* 更新学校 DTO
*/
@Data
@Schema(description = "更新学校请求")
public class UpdateSchoolDTO {
@Schema(description = "学校地址")
private String address;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "校长姓名")
private String principal;
@Schema(description = "建校时间")
private LocalDate established;
@Schema(description = "学校描述")
private String description;
@Schema(description = "学校 Logo URL")
private String logo;
@Schema(description = "学校网站")
private String website;
}

View File

@ -1,68 +0,0 @@
package com.lesingle.creation.dto.student;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
* 创建学生 DTO
*/
@Data
@Schema(description = "创建学生请求")
public class CreateStudentDTO {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码")
private String password;
@NotBlank(message = "昵称不能为空")
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "头像 URL")
private String avatar;
@NotNull(message = "行政班级 ID 不能为空")
@Schema(description = "行政班级 ID")
private Long classId;
@Schema(description = "学号")
private String studentNo;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "性别1-男2-女")
private Integer gender;
@Schema(description = "出生日期")
private LocalDate birthDate;
@Schema(description = "入学日期")
private LocalDate enrollmentDate;
@Schema(description = "家长姓名")
private String parentName;
@Schema(description = "家长电话")
private String parentPhone;
@Schema(description = "家庭地址")
private String address;
@Schema(description = "学生描述")
private String description;
}

View File

@ -1,59 +0,0 @@
package com.lesingle.creation.dto.student;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
/**
* 更新学生 DTO
*/
@Data
@Schema(description = "更新学生请求")
public class UpdateStudentDTO {
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "头像 URL")
private String avatar;
@Schema(description = "行政班级 ID")
private Long classId;
@Schema(description = "学号")
private String studentNo;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "性别1-男2-女")
private Integer gender;
@Schema(description = "出生日期")
private LocalDate birthDate;
@Schema(description = "入学日期")
private LocalDate enrollmentDate;
@Schema(description = "家长姓名")
private String parentName;
@Schema(description = "家长电话")
private String parentPhone;
@Schema(description = "家庭地址")
private String address;
@Schema(description = "学生描述")
private String description;
@Schema(description = "有效状态1-有效2-失效")
private Integer validState;
}

View File

@ -1,65 +0,0 @@
package com.lesingle.creation.dto.teacher;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
* 创建教师 DTO
*/
@Data
@Schema(description = "创建教师请求")
public class CreateTeacherDTO {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码")
private String password;
@NotBlank(message = "昵称不能为空")
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "头像 URL")
private String avatar;
@NotNull(message = "部门 ID 不能为空")
@Schema(description = "部门 ID")
private Long departmentId;
@Schema(description = "工号")
private String employeeNo;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "性别1-男2-女")
private Integer gender;
@Schema(description = "出生日期")
private LocalDate birthDate;
@Schema(description = "入职日期")
private LocalDate hireDate;
@Schema(description = "任教科目")
private String subject;
@Schema(description = "职称")
private String title;
@Schema(description = "教师描述")
private String description;
}

View File

@ -1,56 +0,0 @@
package com.lesingle.creation.dto.teacher;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
/**
* 更新教师 DTO
*/
@Data
@Schema(description = "更新教师请求")
public class UpdateTeacherDTO {
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "头像 URL")
private String avatar;
@Schema(description = "部门 ID")
private Long departmentId;
@Schema(description = "工号")
private String employeeNo;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "性别1-男2-女")
private Integer gender;
@Schema(description = "出生日期")
private LocalDate birthDate;
@Schema(description = "入职日期")
private LocalDate hireDate;
@Schema(description = "任教科目")
private String subject;
@Schema(description = "职称")
private String title;
@Schema(description = "教师描述")
private String description;
@Schema(description = "有效状态1-有效2-失效")
private Integer validState;
}

View File

@ -1,66 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 部门表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_department")
public class Department extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 部门名称
*/
@TableField("name")
private String name;
/**
* 部门编码在租户内唯一
*/
@TableField("code")
private String code;
/**
* 父部门 ID支持树形结构
*/
@TableField("parent_id")
private Long parentId;
/**
* 部门描述
*/
@TableField("description")
private String description;
/**
* 排序
*/
@TableField("sort")
private Integer sort;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
/**
* 子部门列表非数据库字段
*/
@TableField(exist = false)
private List<Department> children;
}

View File

@ -1,52 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 年级表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_grade")
public class Grade extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 年级名称一年级二年级
*/
@TableField("name")
private String name;
/**
* 年级编码在租户内唯一grade_1, grade_2
*/
@TableField("code")
private String code;
/**
* 年级级别用于排序1, 2, 3
*/
@TableField("level")
private Integer level;
/**
* 年级描述
*/
@TableField("description")
private String description;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
}

View File

@ -1,82 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 作业表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_homework")
public class Homework extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 作业名称
*/
@TableField("name")
private String name;
/**
* 作业内容富文本
*/
@TableField("content")
private String content;
/**
* 作业状态unpublished/published
*/
@TableField("status")
private String status;
/**
* 发布时间
*/
@TableField("publish_time")
private java.time.LocalDateTime publishTime;
/**
* 提交开始时间
*/
@TableField("submit_start_time")
private java.time.LocalDateTime submitStartTime;
/**
* 提交结束时间
*/
@TableField("submit_end_time")
private java.time.LocalDateTime submitEndTime;
/**
* 附件列表JSON 格式
*/
@TableField("attachments")
private String attachments;
/**
* 公开范围班级 ID 数组 JSON 格式
*/
@TableField("publish_scope")
private String publishScope;
/**
* 评审规则 ID
*/
@TableField("review_rule_id")
private Long reviewRuleId;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
}

View File

@ -1,54 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 作业评审维度实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_homework_review_dimension")
public class HomeworkReviewDimension extends BaseEntity {
/**
* 规则 ID
*/
@TableField("rule_id")
private Long ruleId;
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 维度名称
*/
@TableField("name")
private String name;
/**
* 满分
*/
@TableField("max_score")
private BigDecimal maxScore;
/**
* 维度描述
*/
@TableField("description")
private String description;
/**
* 排序顺序
*/
@TableField("sort_order")
private Integer sortOrder;
}

View File

@ -1,48 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 作业评审规则表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_homework_review_rule")
public class HomeworkReviewRule extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 规则名称
*/
@TableField("rule_name")
private String ruleName;
/**
* 规则类型default/custom
*/
@TableField("rule_type")
private String ruleType;
/**
* 总分
*/
@TableField("total_score")
private BigDecimal totalScore;
/**
* 规则描述
*/
@TableField("description")
private String description;
}

View File

@ -1,54 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 作业评分表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_homework_score")
public class HomeworkScore extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 提交 ID
*/
@TableField("submission_id")
private Long submissionId;
/**
* 作业 ID
*/
@TableField("homework_id")
private Long homeworkId;
/**
* 评分人 ID
*/
@TableField("reviewer_id")
private Long reviewerId;
/**
* 得分
*/
@TableField("score")
private BigDecimal score;
/**
* 评语
*/
@TableField("comment")
private String comment;
}

View File

@ -1,90 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 作业提交表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_homework_submission")
public class HomeworkSubmission extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 作业 ID
*/
@TableField("homework_id")
private Long homeworkId;
/**
* 学生用户 ID
*/
@TableField("student_id")
private Long studentId;
/**
* 学生姓名
*/
@TableField("student_name")
private String studentName;
/**
* 提交内容
*/
@TableField("content")
private String content;
/**
* 提交附件列表JSON 格式
*/
@TableField("attachments")
private String attachments;
/**
* 提交状态submitted/reviewing/returned
*/
@TableField("status")
private String status;
/**
* 提交时间
*/
@TableField("submit_time")
private java.time.LocalDateTime submitTime;
/**
* 批改时间
*/
@TableField("review_time")
private java.time.LocalDateTime reviewTime;
/**
* 批改人 ID
*/
@TableField("reviewer_id")
private Long reviewerId;
/**
* 批改评语
*/
@TableField("review_comment")
private String reviewComment;
/**
* 批改分数
*/
@TableField("review_score")
private BigDecimal reviewScore;
}

View File

@ -1,72 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
* 学校信息表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_school")
public class School extends BaseEntity {
/**
* 租户 ID一对一
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 学校地址
*/
@TableField("address")
private String address;
/**
* 联系电话
*/
@TableField("phone")
private String phone;
/**
* 校长姓名
*/
@TableField("principal")
private String principal;
/**
* 建校时间
*/
@TableField("established")
private LocalDate established;
/**
* 学校描述
*/
@TableField("description")
private String description;
/**
* 学校 Logo URL
*/
@TableField("logo")
private String logo;
/**
* 学校网站
*/
@TableField("website")
private String website;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
}

View File

@ -1,64 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 班级表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_class")
public class SchoolClass extends BaseEntity {
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 年级 ID
*/
@TableField("grade_id")
private Long gradeId;
/**
* 班级名称一年级 1 二年级 2
*/
@TableField("name")
private String name;
/**
* 班级编码在租户内唯一
*/
@TableField("code")
private String code;
/**
* 班级类型1-行政班级教学班级2-兴趣班
*/
@TableField("type")
private Integer type;
/**
* 班级容量可选
*/
@TableField("capacity")
private Integer capacity;
/**
* 班级描述
*/
@TableField("description")
private String description;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
}

View File

@ -1,102 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
* 学生表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_student")
public class Student extends BaseEntity {
/**
* 用户 ID一对一
*/
@TableField("user_id")
private Long userId;
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 行政班级 ID
*/
@TableField("class_id")
private Long classId;
/**
* 学号在租户内唯一
*/
@TableField("student_no")
private String studentNo;
/**
* 联系电话
*/
@TableField("phone")
private String phone;
/**
* 身份证号
*/
@TableField("id_card")
private String idCard;
/**
* 性别1-2-
*/
@TableField("gender")
private Integer gender;
/**
* 出生日期
*/
@TableField("birth_date")
private LocalDate birthDate;
/**
* 入学日期
*/
@TableField("enrollment_date")
private LocalDate enrollmentDate;
/**
* 家长姓名
*/
@TableField("parent_name")
private String parentName;
/**
* 家长电话
*/
@TableField("parent_phone")
private String parentPhone;
/**
* 家庭地址
*/
@TableField("address")
private String address;
/**
* 学生描述
*/
@TableField("description")
private String description;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
}

View File

@ -1,96 +0,0 @@
package com.lesingle.creation.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lesingle.creation.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
* 教师表实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_teacher")
public class Teacher extends BaseEntity {
/**
* 用户 ID一对一
*/
@TableField("user_id")
private Long userId;
/**
* 租户 ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 部门 ID
*/
@TableField("department_id")
private Long departmentId;
/**
* 工号在租户内唯一
*/
@TableField("employee_no")
private String employeeNo;
/**
* 联系电话
*/
@TableField("phone")
private String phone;
/**
* 身份证号
*/
@TableField("id_card")
private String idCard;
/**
* 性别1-2-
*/
@TableField("gender")
private Integer gender;
/**
* 出生日期
*/
@TableField("birth_date")
private LocalDate birthDate;
/**
* 入职日期
*/
@TableField("hire_date")
private LocalDate hireDate;
/**
* 任教科目可选语文数学
*/
@TableField("subject")
private String subject;
/**
* 职称可选高级教师一级教师
*/
@TableField("title")
private String title;
/**
* 教师描述
*/
@TableField("description")
private String description;
/**
* 有效状态1-有效2-失效
*/
@TableField("valid_state")
private Integer validState;
}

View File

@ -1,35 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.Department;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 部门 Mapper 接口
*/
@Mapper
public interface DepartmentMapper extends BaseMapper<Department> {
/**
* 根据租户 ID 和部门编码查询部门
*
* @param tenantId 租户 ID
* @param code 部门编码
* @return 部门实体
*/
@Select("SELECT * FROM t_biz_department WHERE tenant_id = #{tenantId} AND code = #{code} AND deleted = 0")
Department selectByCode(@Param("tenantId") Long tenantId, @Param("code") String code);
/**
* 根据租户 ID 查询部门列表
*
* @param tenantId 租户 ID
* @return 部门列表
*/
@Select("SELECT * FROM t_biz_department WHERE tenant_id = #{tenantId} AND deleted = 0 ORDER BY sort ASC")
List<Department> selectByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -1,35 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.Grade;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 年级 Mapper 接口
*/
@Mapper
public interface GradeMapper extends BaseMapper<Grade> {
/**
* 根据租户 ID 和年级编码查询年级
*
* @param tenantId 租户 ID
* @param code 年级编码
* @return 年级实体
*/
@Select("SELECT * FROM t_biz_grade WHERE tenant_id = #{tenantId} AND code = #{code} AND deleted = 0")
Grade selectByCode(@Param("tenantId") Long tenantId, @Param("code") String code);
/**
* 根据租户 ID 查询年级列表
*
* @param tenantId 租户 ID
* @return 年级列表
*/
@Select("SELECT * FROM t_biz_grade WHERE tenant_id = #{tenantId} AND deleted = 0 ORDER BY level ASC")
List<Grade> selectByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -1,13 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.Homework;
import org.apache.ibatis.annotations.Mapper;
/**
* 作业 Mapper 接口
*/
@Mapper
public interface HomeworkMapper extends BaseMapper<Homework> {
}

View File

@ -1,26 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.HomeworkReviewDimension;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* 作业评审维度 Mapper 接口
*/
@Mapper
public interface HomeworkReviewDimensionMapper extends BaseMapper<HomeworkReviewDimension> {
/**
* 根据规则 ID 删除维度
*/
void deleteByRuleId(@Param("ruleId") Long ruleId);
/**
* 根据规则 ID 查询维度列表
*/
List<HomeworkReviewDimension> selectByRuleId(@Param("ruleId") Long ruleId);
}

View File

@ -1,13 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.HomeworkReviewRule;
import org.apache.ibatis.annotations.Mapper;
/**
* 作业评审规则 Mapper 接口
*/
@Mapper
public interface HomeworkReviewRuleMapper extends BaseMapper<HomeworkReviewRule> {
}

View File

@ -1,13 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.HomeworkScore;
import org.apache.ibatis.annotations.Mapper;
/**
* 作业评分 Mapper 接口
*/
@Mapper
public interface HomeworkScoreMapper extends BaseMapper<HomeworkScore> {
}

View File

@ -1,13 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.HomeworkSubmission;
import org.apache.ibatis.annotations.Mapper;
/**
* 作业提交 Mapper 接口
*/
@Mapper
public interface HomeworkSubmissionMapper extends BaseMapper<HomeworkSubmission> {
}

View File

@ -1,35 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.SchoolClass;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 班级 Mapper 接口
*/
@Mapper
public interface SchoolClassMapper extends BaseMapper<SchoolClass> {
/**
* 根据租户 ID 和班级编码查询班级
*
* @param tenantId 租户 ID
* @param code 班级编码
* @return 班级实体
*/
@Select("SELECT * FROM t_biz_class WHERE tenant_id = #{tenantId} AND code = #{code} AND deleted = 0")
SchoolClass selectByCode(@Param("tenantId") Long tenantId, @Param("code") String code);
/**
* 根据年级 ID 查询班级列表
*
* @param gradeId 年级 ID
* @return 班级列表
*/
@Select("SELECT * FROM t_biz_class WHERE grade_id = #{gradeId} AND deleted = 0 ORDER BY name ASC")
List<SchoolClass> selectByGradeId(@Param("gradeId") Long gradeId);
}

View File

@ -1,23 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.School;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 学校信息 Mapper 接口
*/
@Mapper
public interface SchoolMapper extends BaseMapper<School> {
/**
* 根据租户 ID 查询学校
*
* @param tenantId 租户 ID
* @return 学校实体
*/
@Select("SELECT * FROM t_biz_school WHERE tenant_id = #{tenantId} AND deleted = 0")
School selectByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -1,33 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.Student;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 学生 Mapper 接口
*/
@Mapper
public interface StudentMapper extends BaseMapper<Student> {
/**
* 根据用户 ID 查询学生
*
* @param userId 用户 ID
* @return 学生实体
*/
@Select("SELECT * FROM t_biz_student WHERE user_id = #{userId} AND deleted = 0")
Student selectByUserId(@Param("userId") Long userId);
/**
* 根据租户 ID 和学号查询学生
*
* @param tenantId 租户 ID
* @param studentNo 学号
* @return 学生实体
*/
@Select("SELECT * FROM t_biz_student WHERE tenant_id = #{tenantId} AND student_no = #{studentNo} AND deleted = 0")
Student selectByStudentNo(@Param("tenantId") Long tenantId, @Param("studentNo") String studentNo);
}

View File

@ -1,33 +0,0 @@
package com.lesingle.creation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lesingle.creation.entity.Teacher;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 教师 Mapper 接口
*/
@Mapper
public interface TeacherMapper extends BaseMapper<Teacher> {
/**
* 根据用户 ID 查询教师
*
* @param userId 用户 ID
* @return 教师实体
*/
@Select("SELECT * FROM t_biz_teacher WHERE user_id = #{userId} AND deleted = 0")
Teacher selectByUserId(@Param("userId") Long userId);
/**
* 根据租户 ID 和工号查询教师
*
* @param tenantId 租户 ID
* @param employeeNo 工号
* @return 教师实体
*/
@Select("SELECT * FROM t_biz_teacher WHERE tenant_id = #{tenantId} AND employee_no = #{employeeNo} AND deleted = 0")
Teacher selectByEmployeeNo(@Param("tenantId") Long tenantId, @Param("employeeNo") String employeeNo);
}

View File

@ -1,45 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lesingle.creation.entity.Department;
import com.lesingle.creation.dto.department.CreateDepartmentDTO;
import com.lesingle.creation.dto.department.UpdateDepartmentDTO;
import com.lesingle.creation.vo.department.DepartmentVO;
import java.util.List;
/**
* 部门服务接口
*/
public interface DepartmentService extends IService<Department> {
/**
* 创建部门
*/
DepartmentVO create(CreateDepartmentDTO dto, Long tenantId, Long creatorId);
/**
* 查询部门列表分页
*/
List<DepartmentVO> list(Long tenantId, Long parentId);
/**
* 查询部门树形结构
*/
List<DepartmentVO> tree(Long tenantId);
/**
* 获取部门详情
*/
DepartmentVO getDetail(Long id, Long tenantId);
/**
* 更新部门
*/
DepartmentVO update(Long id, UpdateDepartmentDTO dto, Long tenantId, Long modifierId);
/**
* 删除部门
*/
void delete(Long id, Long tenantId);
}

View File

@ -1,37 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.dto.grade.CreateGradeDTO;
import com.lesingle.creation.dto.grade.UpdateGradeDTO;
import com.lesingle.creation.vo.grade.GradeVO;
/**
* 年级服务接口
*/
public interface GradeService {
/**
* 创建年级
*/
GradeVO create(CreateGradeDTO dto, Long tenantId, Long creatorId);
/**
* 分页查询年级列表
*/
Page<GradeVO> pageQuery(Integer pageNum, Integer pageSize, Long tenantId);
/**
* 获取年级详情
*/
GradeVO getDetail(Long id, Long tenantId);
/**
* 更新年级
*/
GradeVO update(Long id, UpdateGradeDTO dto, Long tenantId, Long modifierId);
/**
* 删除年级
*/
void delete(Long id, Long tenantId);
}

View File

@ -1,208 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lesingle.creation.dto.homework.*;
import com.lesingle.creation.entity.Homework;
import com.lesingle.creation.vo.homework.HomeworkDetailVO;
import com.lesingle.creation.vo.homework.HomeworkListVO;
import com.lesingle.creation.vo.homework.ReviewRuleVO;
import com.lesingle.creation.vo.homework.SubmissionVO;
import java.util.List;
/**
* 作业服务接口
*/
public interface HomeworkService extends IService<Homework> {
/**
* 创建作业
* @param dto 创建作业请求
* @param tenantId 租户 ID
* @param creatorId 创建人 ID
* @return 作业详情
*/
HomeworkDetailVO create(CreateHomeworkDTO dto, Long tenantId, Long creatorId);
/**
* 更新作业
* @param id 作业 ID
* @param dto 更新作业请求
* @param tenantId 租户 ID
* @return 作业详情
*/
HomeworkDetailVO update(Long id, UpdateHomeworkDTO dto, Long tenantId);
/**
* 发布作业
* @param id 作业 ID
* @param tenantId 租户 ID
* @return 作业详情
*/
HomeworkDetailVO publish(Long id, Long tenantId);
/**
* 删除作业
* @param id 作业 ID
* @param tenantId 租户 ID
*/
void delete(Long id, Long tenantId);
/**
* 获取作业详情
* @param id 作业 ID
* @param tenantId 租户 ID
* @return 作业详情
*/
HomeworkDetailVO getDetail(Long id, Long tenantId);
/**
* 分页查询作业列表
* @param queryDTO 查询参数
* @param tenantId 租户 ID
* @return 作业列表
*/
Page<HomeworkListVO> pageQuery(HomeworkQueryDTO queryDTO, Long tenantId);
/**
* 提交作业
* @param dto 提交作业请求
* @param tenantId 租户 ID
* @param studentId 学生 ID
* @return 提交详情
*/
SubmissionVO submit(SubmitHomeworkDTO dto, Long tenantId, Long studentId);
/**
* 批改作业
* @param dto 批改作业请求
* @param tenantId 租户 ID
* @param reviewerId 批改人 ID
* @return 提交详情
*/
SubmissionVO review(ReviewHomeworkDTO dto, Long tenantId, Long reviewerId);
/**
* 获取作业提交列表
* @param homeworkId 作业 ID
* @param tenantId 租户 ID
* @return 提交列表
*/
List<SubmissionVO> getSubmissions(Long homeworkId, Long tenantId);
/**
* 创建评审规则
* @param ruleName 规则名称
* @param ruleType 规则类型
* @param totalScore 总分
* @param description 规则描述
* @param tenantId 租户 ID
* @return 评审规则
*/
ReviewRuleVO createReviewRule(String ruleName, String ruleType,
java.math.BigDecimal totalScore, String description, Long tenantId);
/**
* 获取评审规则列表
* @param tenantId 租户 ID
* @return 评审规则列表
*/
List<ReviewRuleVO> getReviewRules(Long tenantId);
/**
* 获取可选的评审规则
* @param tenantId 租户 ID
* @return 评审规则列表
*/
List<ReviewRuleVO> getReviewRulesForSelect(Long tenantId);
/**
* 更新评审规则
* @param id 规则 ID
* @param dto 更新请求
* @param tenantId 租户 ID
* @return 评审规则
*/
ReviewRuleVO updateReviewRule(Long id, UpdateReviewRuleDTO dto, Long tenantId);
/**
* 删除评审规则
* @param id 规则 ID
* @param tenantId 租户 ID
*/
void deleteReviewRule(Long id, Long tenantId);
/**
* 我的作业列表学生端
* @param queryDTO 查询参数
* @param userId 用户 ID
* @param tenantId 租户 ID
* @return 作业列表
*/
Page<HomeworkListVO> getMyHomeworks(HomeworkQueryDTO queryDTO, Long userId, Long tenantId);
/**
* 取消发布作业
* @param id 作业 ID
* @param tenantId 租户 ID
* @return 作业详情
*/
HomeworkDetailVO unpublish(Long id, Long tenantId);
/**
* 获取提交记录列表
* @param queryDTO 查询参数
* @param tenantId 租户 ID
* @return 提交记录列表
*/
Page<SubmissionVO> getSubmissionsList(SubmissionQueryDTO queryDTO, Long tenantId);
/**
* 获取提交记录详情
* @param id 提交 ID
* @param tenantId 租户 ID
* @return 提交详情
*/
SubmissionVO getSubmissionDetail(Long id, Long tenantId);
/**
* 获取班级树结构
* @param tenantId 租户 ID
* @return 班级树
*/
List<com.lesingle.creation.vo.school.ClassTreeNodeVO> getClassTree(Long tenantId);
/**
* 获取我的提交记录
* @param homeworkId 作业 ID
* @param userId 用户 ID
* @param tenantId 租户 ID
* @return 提交详情
*/
SubmissionVO getMySubmission(Long homeworkId, Long userId, Long tenantId);
/**
* 提交评分
* @param dto 评分请求
* @param tenantId 租户 ID
* @param reviewerId 批改人 ID
* @return 提交详情
*/
SubmissionVO createScore(CreateScoreDTO dto, Long tenantId, Long reviewerId);
/**
* 标记作品违规
* @param submissionId 提交 ID
* @param reason 原因
* @param tenantId 租户 ID
*/
void markViolation(Long submissionId, String reason, Long tenantId);
/**
* 重置评分
* @param submissionId 提交 ID
* @param tenantId 租户 ID
*/
void resetScore(Long submissionId, Long tenantId);
}

View File

@ -1,37 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.dto.schoolclass.CreateClassDTO;
import com.lesingle.creation.dto.schoolclass.UpdateClassDTO;
import com.lesingle.creation.vo.schoolclass.ClassVO;
/**
* 班级服务接口
*/
public interface SchoolClassService {
/**
* 创建班级
*/
ClassVO create(CreateClassDTO dto, Long tenantId, Long creatorId);
/**
* 分页查询班级列表
*/
Page<ClassVO> pageQuery(Integer pageNum, Integer pageSize, Long tenantId, Long gradeId, Integer type);
/**
* 获取班级详情
*/
ClassVO getDetail(Long id, Long tenantId);
/**
* 更新班级
*/
ClassVO update(Long id, UpdateClassDTO dto, Long tenantId, Long modifierId);
/**
* 删除班级
*/
void delete(Long id, Long tenantId);
}

View File

@ -1,48 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lesingle.creation.dto.school.CreateSchoolDTO;
import com.lesingle.creation.dto.school.UpdateSchoolDTO;
import com.lesingle.creation.entity.School;
import com.lesingle.creation.vo.school.SchoolVO;
/**
* 学校服务接口
*/
public interface SchoolService extends IService<School> {
/**
* 创建学校
*
* @param dto 创建学校 DTO
* @param tenantId 租户 ID
* @param creatorId 创建人 ID
* @return 学校 VO
*/
SchoolVO create(CreateSchoolDTO dto, Long tenantId, Long creatorId);
/**
* 根据租户 ID 查询学校
*
* @param tenantId 租户 ID
* @return 学校 VO尚未创建时返回 {@code null}
*/
SchoolVO getByTenantId(Long tenantId);
/**
* 更新学校
*
* @param dto 更新学校 DTO
* @param tenantId 租户 ID
* @param modifierId 修改人 ID
* @return 学校 VO
*/
SchoolVO update(UpdateSchoolDTO dto, Long tenantId, Long modifierId);
/**
* 删除学校
*
* @param tenantId 租户 ID
*/
void delete(Long tenantId);
}

View File

@ -1,73 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lesingle.creation.dto.student.CreateStudentDTO;
import com.lesingle.creation.dto.student.UpdateStudentDTO;
import com.lesingle.creation.entity.Student;
import com.lesingle.creation.vo.student.StudentVO;
import java.util.List;
/**
* 学生服务接口
*/
public interface StudentService extends IService<Student> {
/**
* 创建学生
*
* @param dto 创建学生请求
* @param tenantId 租户 ID
* @param creatorId 创建人 ID
* @return 学生 VO
*/
StudentVO create(CreateStudentDTO dto, Long tenantId, Long creatorId);
/**
* 查询学生列表分页
*
* @param tenantId 租户 ID
* @param page 页码
* @param pageSize 每页大小
* @param classId 班级 ID可选
* @return 学生 VO 列表
*/
List<StudentVO> list(Long tenantId, Integer page, Integer pageSize, Long classId);
/**
* 获取学生详情
*
* @param id 学生 ID
* @param tenantId 租户 ID
* @return 学生 VO
*/
StudentVO getDetail(Long id, Long tenantId);
/**
* 根据用户 ID 获取学生信息
*
* @param userId 用户 ID
* @param tenantId 租户 ID
* @return 学生 VO
*/
StudentVO getByUserId(Long userId, Long tenantId);
/**
* 更新学生
*
* @param id 学生 ID
* @param dto 更新学生请求
* @param tenantId 租户 ID
* @param modifierId 修改人 ID
* @return 学生 VO
*/
StudentVO update(Long id, UpdateStudentDTO dto, Long tenantId, Long modifierId);
/**
* 删除学生
*
* @param id 学生 ID
* @param tenantId 租户 ID
*/
void delete(Long id, Long tenantId);
}

View File

@ -1,73 +0,0 @@
package com.lesingle.creation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lesingle.creation.dto.teacher.CreateTeacherDTO;
import com.lesingle.creation.dto.teacher.UpdateTeacherDTO;
import com.lesingle.creation.entity.Teacher;
import com.lesingle.creation.vo.teacher.TeacherVO;
import java.util.List;
/**
* 教师服务接口
*/
public interface TeacherService extends IService<Teacher> {
/**
* 创建教师
*
* @param dto 创建教师请求
* @param tenantId 租户 ID
* @param creatorId 创建人 ID
* @return 教师 VO
*/
TeacherVO create(CreateTeacherDTO dto, Long tenantId, Long creatorId);
/**
* 查询教师列表
*
* @param tenantId 租户 ID
* @param departmentId 部门 ID可选
* @param nickname 昵称可选
* @param username 用户名可选
* @return 教师 VO 列表
*/
List<TeacherVO> list(Long tenantId, Long departmentId, String nickname, String username);
/**
* 获取教师详情
*
* @param id 教师 ID
* @param tenantId 租户 ID
* @return 教师 VO
*/
TeacherVO getDetail(Long id, Long tenantId);
/**
* 根据用户 ID 获取教师信息
*
* @param userId 用户 ID
* @param tenantId 租户 ID
* @return 教师 VO
*/
TeacherVO getByUserId(Long userId, Long tenantId);
/**
* 更新教师
*
* @param id 教师 ID
* @param dto 更新教师请求
* @param tenantId 租户 ID
* @param modifierId 修改人 ID
* @return 教师 VO
*/
TeacherVO update(Long id, UpdateTeacherDTO dto, Long tenantId, Long modifierId);
/**
* 删除教师
*
* @param id 教师 ID
* @param tenantId 租户 ID
*/
void delete(Long id, Long tenantId);
}

View File

@ -79,4 +79,17 @@ public interface UserService extends IService<User> {
* @return 用户统计 VO
*/
UserStatsVO getStats();
/**
* 报名/组队场景按租户角色编码分页查询可选用户仅允许 teacherstudent
*
* @param tenantId 租户 ID
* @param roleCode 角色编码 teacherstudent
* @param keyword 用户名/昵称/邮箱/手机号关键字
* @param page 页码
* @param pageSize 每页条数
* @return 分页列表
*/
Page<UserListVO> pageCandidateUsersByRoleCode(
Long tenantId, String roleCode, String keyword, int page, int pageSize);
}

View File

@ -1,189 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.lesingle.creation.dto.department.CreateDepartmentDTO;
import com.lesingle.creation.dto.department.UpdateDepartmentDTO;
import com.lesingle.creation.entity.Department;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.mapper.DepartmentMapper;
import com.lesingle.creation.service.DepartmentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.vo.department.DepartmentVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 部门服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DepartmentServiceImpl extends ServiceImpl<DepartmentMapper, Department> implements DepartmentService {
private final DepartmentMapper departmentMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public DepartmentVO create(CreateDepartmentDTO dto, Long tenantId, Long creatorId) {
// 检查部门编码是否已存在
Department existing = departmentMapper.selectByCode(tenantId, dto.getCode());
if (existing != null) {
throw new BusinessException("部门编码已存在");
}
Department department = new Department();
department.setTenantId(tenantId);
department.setName(dto.getName());
department.setCode(dto.getCode());
department.setParentId(dto.getParentId());
department.setDescription(dto.getDescription());
department.setSort(dto.getSort() != null ? dto.getSort() : 0);
department.setValidState(1);
departmentMapper.insert(department);
log.info("创建部门成功ID={}, 名称={}", department.getId(), dto.getName());
return convertToVO(department);
}
@Override
public List<DepartmentVO> list(Long tenantId, Long parentId) {
LambdaQueryWrapper<Department> wrapper = new LambdaQueryWrapper<Department>()
.eq(Department::getTenantId, tenantId)
.eq(Department::getDeleted, 0)
.eq(parentId != null, Department::getParentId, parentId)
.orderByAsc(Department::getSort);
List<Department> departments = this.list(wrapper);
return departments.stream().map(this::convertToVO).collect(Collectors.toList());
}
@Override
public List<DepartmentVO> tree(Long tenantId) {
// 查询所有顶级部门parentId null 0
List<Department> allDepartments = this.list(new LambdaQueryWrapper<Department>()
.eq(Department::getTenantId, tenantId)
.eq(Department::getDeleted, 0)
.orderByAsc(Department::getSort));
// 构建树形结构
return buildDepartmentTree(allDepartments, null);
}
@Override
public DepartmentVO getDetail(Long id, Long tenantId) {
Department department = this.getById(id);
if (department == null || !department.getTenantId().equals(tenantId)) {
throw new BusinessException("部门不存在");
}
return convertToVO(department);
}
@Override
@Transactional(rollbackFor = Exception.class)
public DepartmentVO update(Long id, UpdateDepartmentDTO dto, Long tenantId, Long modifierId) {
Department department = this.getById(id);
if (department == null || !department.getTenantId().equals(tenantId)) {
throw new BusinessException("部门不存在");
}
if (dto.getCode() != null) {
// 检查新编码是否已被其他部门使用
Department existing = departmentMapper.selectByCode(tenantId, dto.getCode());
if (existing != null && !existing.getId().equals(id)) {
throw new BusinessException("部门编码已存在");
}
department.setCode(dto.getCode());
}
if (dto.getName() != null) {
department.setName(dto.getName());
}
if (dto.getParentId() != null) {
// 不能将自己设置为父部门
if (dto.getParentId().equals(id)) {
throw new BusinessException("不能将自己设置为父部门");
}
department.setParentId(dto.getParentId());
}
if (dto.getDescription() != null) {
department.setDescription(dto.getDescription());
}
if (dto.getSort() != null) {
department.setSort(dto.getSort());
}
if (dto.getValidState() != null) {
department.setValidState(dto.getValidState());
}
this.updateById(department);
log.info("更新部门成功ID={}", id);
return convertToVO(department);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long tenantId) {
Department department = this.getById(id);
if (department == null || !department.getTenantId().equals(tenantId)) {
throw new BusinessException("部门不存在");
}
// 检查是否有子部门
Long childCount = this.count(new LambdaQueryWrapper<Department>()
.eq(Department::getParentId, id)
.eq(Department::getDeleted, 0));
if (childCount > 0) {
throw new BusinessException("存在子部门,无法删除");
}
department.setDeleted(1);
this.updateById(department);
log.info("删除部门成功ID={}", id);
}
private DepartmentVO convertToVO(Department department) {
DepartmentVO vo = new DepartmentVO();
vo.setId(department.getId());
vo.setTenantId(department.getTenantId());
vo.setName(department.getName());
vo.setCode(department.getCode());
vo.setParentId(department.getParentId());
vo.setDescription(department.getDescription());
vo.setSort(department.getSort());
vo.setValidState(department.getValidState());
vo.setCreateTime(department.getCreateTime());
vo.setUpdateTime(department.getUpdateTime());
return vo;
}
private List<DepartmentVO> buildDepartmentTree(List<Department> allDepartments, Long parentId) {
List<DepartmentVO> tree = allDepartments.stream()
.filter(dept -> {
if (parentId == null) {
return dept.getParentId() == null || dept.getParentId() == 0;
}
return parentId.equals(dept.getParentId());
})
.map(this::convertToVO)
.sorted(Comparator.comparing(DepartmentVO::getSort))
.collect(Collectors.toList());
// 递归构建子部门
for (DepartmentVO node : tree) {
List<DepartmentVO> children = buildDepartmentTree(allDepartments, node.getId());
if (!children.isEmpty()) {
node.setChildren(children);
}
}
return tree;
}
}

View File

@ -1,152 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.dto.grade.CreateGradeDTO;
import com.lesingle.creation.dto.grade.UpdateGradeDTO;
import com.lesingle.creation.entity.Grade;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.mapper.GradeMapper;
import com.lesingle.creation.service.GradeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.vo.grade.GradeVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 年级服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GradeServiceImpl extends ServiceImpl<GradeMapper, Grade> implements GradeService {
private final GradeMapper gradeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public GradeVO create(CreateGradeDTO dto, Long tenantId, Long creatorId) {
// 检查年级编码是否已存在
Grade existing = gradeMapper.selectByCode(tenantId, dto.getCode());
if (existing != null) {
throw new BusinessException("年级编码已存在");
}
// 检查年级级别是否已存在
Grade existingByLevel = this.getOne(new LambdaQueryWrapper<Grade>()
.eq(Grade::getTenantId, tenantId)
.eq(Grade::getLevel, dto.getLevel())
.eq(Grade::getDeleted, 0));
if (existingByLevel != null) {
throw new BusinessException("年级级别已存在");
}
Grade grade = new Grade();
grade.setTenantId(tenantId);
grade.setName(dto.getName());
grade.setCode(dto.getCode());
grade.setLevel(dto.getLevel());
grade.setDescription(dto.getDescription());
grade.setValidState(1);
gradeMapper.insert(grade);
log.info("创建年级成功ID={}, 名称={}", grade.getId(), dto.getName());
return convertToVO(grade);
}
@Override
public Page<GradeVO> pageQuery(Integer pageNum, Integer pageSize, Long tenantId) {
Page<Grade> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Grade> wrapper = new LambdaQueryWrapper<Grade>()
.eq(Grade::getTenantId, tenantId)
.eq(Grade::getDeleted, 0)
.orderByAsc(Grade::getLevel);
Page<Grade> resultPage = this.page(page, wrapper);
Page<GradeVO> voPage = new Page<>(resultPage.getCurrent(), resultPage.getSize(), resultPage.getTotal());
voPage.setRecords(resultPage.getRecords().stream().map(this::convertToVO).toList());
return voPage;
}
@Override
public GradeVO getDetail(Long id, Long tenantId) {
Grade grade = this.getById(id);
if (grade == null || !grade.getTenantId().equals(tenantId)) {
throw new BusinessException("年级不存在");
}
return convertToVO(grade);
}
@Override
@Transactional(rollbackFor = Exception.class)
public GradeVO update(Long id, UpdateGradeDTO dto, Long tenantId, Long modifierId) {
Grade grade = this.getById(id);
if (grade == null || !grade.getTenantId().equals(tenantId)) {
throw new BusinessException("年级不存在");
}
if (dto.getName() != null) {
grade.setName(dto.getName());
}
if (dto.getCode() != null) {
// 检查新编码是否已被其他年级使用
Grade existing = gradeMapper.selectByCode(tenantId, dto.getCode());
if (existing != null && !existing.getId().equals(id)) {
throw new BusinessException("年级编码已存在");
}
grade.setCode(dto.getCode());
}
if (dto.getLevel() != null) {
// 检查新级别是否已被其他年级使用
Grade existing = this.getOne(new LambdaQueryWrapper<Grade>()
.eq(Grade::getTenantId, tenantId)
.eq(Grade::getLevel, dto.getLevel())
.eq(Grade::getDeleted, 0)
.ne(Grade::getId, id));
if (existing != null) {
throw new BusinessException("年级级别已存在");
}
grade.setLevel(dto.getLevel());
}
if (dto.getDescription() != null) {
grade.setDescription(dto.getDescription());
}
this.updateById(grade);
log.info("更新年级成功ID={}", id);
return convertToVO(grade);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long tenantId) {
Grade grade = this.getById(id);
if (grade == null || !grade.getTenantId().equals(tenantId)) {
throw new BusinessException("年级不存在");
}
grade.setDeleted(1);
this.updateById(grade);
log.info("删除年级成功ID={}", id);
}
private GradeVO convertToVO(Grade grade) {
GradeVO vo = new GradeVO();
vo.setId(grade.getId());
vo.setTenantId(grade.getTenantId());
vo.setName(grade.getName());
vo.setCode(grade.getCode());
vo.setLevel(grade.getLevel());
vo.setDescription(grade.getDescription());
vo.setValidState(grade.getValidState());
vo.setCreateTime(grade.getCreateTime());
vo.setUpdateTime(grade.getUpdateTime());
return vo;
}
}

View File

@ -1,633 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.lesingle.creation.common.enums.ValidStateEnum;
import com.lesingle.creation.dto.homework.*;
import com.lesingle.creation.entity.Homework;
import com.lesingle.creation.entity.HomeworkReviewDimension;
import com.lesingle.creation.entity.HomeworkReviewRule;
import com.lesingle.creation.entity.HomeworkSubmission;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.mapper.HomeworkMapper;
import com.lesingle.creation.mapper.HomeworkReviewDimensionMapper;
import com.lesingle.creation.mapper.HomeworkReviewRuleMapper;
import com.lesingle.creation.mapper.HomeworkSubmissionMapper;
import com.lesingle.creation.service.HomeworkService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.vo.homework.HomeworkDetailVO;
import com.lesingle.creation.vo.homework.HomeworkListVO;
import com.lesingle.creation.vo.homework.ReviewDimensionVO;
import com.lesingle.creation.vo.homework.ReviewRuleVO;
import com.lesingle.creation.vo.homework.SubmissionVO;
import com.lesingle.creation.vo.school.ClassTreeNodeVO;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 作业服务实现类
*/
@Slf4j
@Service
public class HomeworkServiceImpl extends ServiceImpl<HomeworkMapper, Homework>
implements HomeworkService {
private final HomeworkSubmissionMapper homeworkSubmissionMapper;
private final HomeworkReviewRuleMapper homeworkReviewRuleMapper;
private final HomeworkReviewDimensionMapper homeworkReviewDimensionMapper;
public HomeworkServiceImpl(HomeworkSubmissionMapper homeworkSubmissionMapper,
HomeworkReviewRuleMapper homeworkReviewRuleMapper,
HomeworkReviewDimensionMapper homeworkReviewDimensionMapper) {
this.homeworkSubmissionMapper = homeworkSubmissionMapper;
this.homeworkReviewRuleMapper = homeworkReviewRuleMapper;
this.homeworkReviewDimensionMapper = homeworkReviewDimensionMapper;
}
@Override
@Transactional(rollbackFor = Exception.class)
public HomeworkDetailVO create(CreateHomeworkDTO dto, Long tenantId, Long creatorId) {
// 验证时间顺序
if (dto.getSubmitStartTime().isAfter(dto.getSubmitEndTime())) {
throw new BusinessException("提交开始时间不能晚于结束时间");
}
// 检查作业名称是否已存在
Homework existing = this.getOne(new LambdaQueryWrapper<Homework>()
.eq(Homework::getTenantId, tenantId)
.eq(Homework::getName, dto.getName())
.eq(Homework::getDeleted, 0));
if (existing != null) {
throw new BusinessException("作业名称已存在");
}
// 创建作业
Homework homework = new Homework();
homework.setTenantId(tenantId);
homework.setName(dto.getName());
homework.setContent(dto.getContent());
homework.setStatus("unpublished");
homework.setSubmitStartTime(dto.getSubmitStartTime());
homework.setSubmitEndTime(dto.getSubmitEndTime());
homework.setAttachments(dto.getAttachments());
homework.setPublishScope(dto.getPublishScope());
homework.setReviewRuleId(dto.getReviewRuleId());
homework.setValidState(ValidStateEnum.VALID.getCode());
this.save(homework);
log.info("创建作业成功ID={}, 名称={}", homework.getId(), dto.getName());
return convertToDetailVO(homework);
}
@Override
@Transactional(rollbackFor = Exception.class)
public HomeworkDetailVO update(Long id, UpdateHomeworkDTO dto, Long tenantId) {
Homework homework = this.getById(id);
if (homework == null || !homework.getTenantId().equals(tenantId)) {
throw new BusinessException("作业不存在");
}
// 已发布的作业不能修改
if ("published".equals(homework.getStatus())) {
throw new BusinessException("已发布的作业不能修改");
}
homework.setName(dto.getName());
homework.setContent(dto.getContent());
if (dto.getSubmitStartTime() != null) {
homework.setSubmitStartTime(dto.getSubmitStartTime());
}
if (dto.getSubmitEndTime() != null) {
homework.setSubmitEndTime(dto.getSubmitEndTime());
}
homework.setAttachments(dto.getAttachments());
homework.setPublishScope(dto.getPublishScope());
if (dto.getReviewRuleId() != null) {
homework.setReviewRuleId(dto.getReviewRuleId());
}
this.updateById(homework);
log.info("更新作业成功ID={}", id);
return convertToDetailVO(homework);
}
@Override
@Transactional(rollbackFor = Exception.class)
public HomeworkDetailVO publish(Long id, Long tenantId) {
Homework homework = this.getById(id);
if (homework == null || !homework.getTenantId().equals(tenantId)) {
throw new BusinessException("作业不存在");
}
homework.setStatus("published");
homework.setPublishTime(LocalDateTime.now());
this.updateById(homework);
log.info("发布作业成功ID={}", id);
return convertToDetailVO(homework);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long tenantId) {
Homework homework = this.getById(id);
if (homework == null || !homework.getTenantId().equals(tenantId)) {
throw new BusinessException("作业不存在");
}
this.removeById(id);
log.info("删除作业成功ID={}", id);
}
@Override
public HomeworkDetailVO getDetail(Long id, Long tenantId) {
Homework homework = this.getById(id);
if (homework == null || !homework.getTenantId().equals(tenantId)) {
throw new BusinessException("作业不存在");
}
return convertToDetailVO(homework);
}
@Override
public Page<HomeworkListVO> pageQuery(HomeworkQueryDTO queryDTO, Long tenantId) {
Page<Homework> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
LambdaQueryWrapper<Homework> wrapper = new LambdaQueryWrapper<Homework>()
.eq(Homework::getTenantId, tenantId)
.eq(Homework::getDeleted, 0)
.like(queryDTO.getNameKeyword() != null, Homework::getName, queryDTO.getNameKeyword())
.eq(queryDTO.getStatus() != null, Homework::getStatus, queryDTO.getStatus())
.eq(queryDTO.getValidState() != null, Homework::getValidState, queryDTO.getValidState())
.orderByDesc(Homework::getCreateTime);
Page<Homework> resultPage = this.page(page, wrapper);
// 转换为 VO
List<HomeworkListVO> voList = resultPage.getRecords().stream()
.map(this::convertToListVO)
.collect(Collectors.toList());
Page<HomeworkListVO> voPage = new Page<>(
resultPage.getCurrent(),
resultPage.getSize(),
resultPage.getTotal()
);
voPage.setRecords(voList);
return voPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public SubmissionVO submit(SubmitHomeworkDTO dto, Long tenantId, Long studentId) {
Homework homework = this.getById(dto.getHomeworkId());
if (homework == null || !homework.getTenantId().equals(tenantId)) {
throw new BusinessException("作业不存在");
}
// 检查是否已提交
HomeworkSubmission existing = homeworkSubmissionMapper.selectOne(new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getHomeworkId, dto.getHomeworkId())
.eq(HomeworkSubmission::getStudentId, studentId)
.eq(HomeworkSubmission::getDeleted, 0));
if (existing != null) {
throw new BusinessException("您已经提交过该作业");
}
// 检查提交时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(homework.getSubmitStartTime())) {
throw new BusinessException("还未到提交开始时间");
}
if (now.isAfter(homework.getSubmitEndTime())) {
throw new BusinessException("已错过提交截止时间");
}
HomeworkSubmission submission = new HomeworkSubmission();
submission.setTenantId(tenantId);
submission.setHomeworkId(dto.getHomeworkId());
submission.setStudentId(studentId);
submission.setContent(dto.getContent());
submission.setAttachments(dto.getAttachments());
submission.setStatus("submitted");
submission.setSubmitTime(LocalDateTime.now());
homeworkSubmissionMapper.insert(submission);
log.info("提交作业成功ID={}, 学生 ID={}", submission.getId(), studentId);
return convertToSubmissionVO(submission);
}
@Override
@Transactional(rollbackFor = Exception.class)
public SubmissionVO review(ReviewHomeworkDTO dto, Long tenantId, Long reviewerId) {
HomeworkSubmission submission = homeworkSubmissionMapper.selectById(dto.getSubmissionId());
if (submission == null || !submission.getTenantId().equals(tenantId)) {
throw new BusinessException("提交记录不存在");
}
submission.setReviewerId(reviewerId);
submission.setReviewComment(dto.getComment());
submission.setReviewScore(dto.getScore());
submission.setReviewTime(LocalDateTime.now());
submission.setStatus("reviewing");
homeworkSubmissionMapper.updateById(submission);
log.info("批改作业成功,提交 ID={}, 分数={}", dto.getSubmissionId(), dto.getScore());
return convertToSubmissionVO(submission);
}
@Override
public List<SubmissionVO> getSubmissions(Long homeworkId, Long tenantId) {
List<HomeworkSubmission> submissions = homeworkSubmissionMapper.selectList(
new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getHomeworkId, homeworkId)
.eq(HomeworkSubmission::getTenantId, tenantId)
.eq(HomeworkSubmission::getDeleted, 0)
.orderByDesc(HomeworkSubmission::getSubmitTime)
);
return submissions.stream()
.map(this::convertToSubmissionVO)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReviewRuleVO createReviewRule(String ruleName, String ruleType,
java.math.BigDecimal totalScore, String description, Long tenantId) {
HomeworkReviewRule rule = new HomeworkReviewRule();
rule.setTenantId(tenantId);
rule.setRuleName(ruleName);
rule.setRuleType(ruleType);
rule.setTotalScore(totalScore);
rule.setDescription(description);
homeworkReviewRuleMapper.insert(rule);
log.info("创建评审规则成功ID={}", rule.getId());
return convertToReviewRuleVO(rule);
}
@Override
public List<ReviewRuleVO> getReviewRules(Long tenantId) {
List<HomeworkReviewRule> rules = homeworkReviewRuleMapper.selectList(
new LambdaQueryWrapper<HomeworkReviewRule>()
.eq(HomeworkReviewRule::getTenantId, tenantId)
.eq(HomeworkReviewRule::getDeleted, 0)
.orderByDesc(HomeworkReviewRule::getCreateTime)
);
return rules.stream()
.map(this::convertToReviewRuleVO)
.collect(Collectors.toList());
}
// ========== 转换方法 ==========
private HomeworkDetailVO convertToDetailVO(Homework homework) {
HomeworkDetailVO vo = new HomeworkDetailVO();
vo.setId(homework.getId());
vo.setName(homework.getName());
vo.setContent(homework.getContent());
vo.setStatus(homework.getStatus());
vo.setPublishTime(homework.getPublishTime());
vo.setSubmitStartTime(homework.getSubmitStartTime());
vo.setSubmitEndTime(homework.getSubmitEndTime());
vo.setAttachments(homework.getAttachments());
vo.setPublishScope(homework.getPublishScope());
vo.setReviewRuleId(homework.getReviewRuleId());
vo.setCreateTime(homework.getCreateTime());
// 统计提交人数和批改人数
Long submissionCount = homeworkSubmissionMapper.selectCount(
new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getHomeworkId, homework.getId())
.eq(HomeworkSubmission::getDeleted, 0)
);
Long reviewedCount = homeworkSubmissionMapper.selectCount(
new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getHomeworkId, homework.getId())
.eq(HomeworkSubmission::getStatus, "reviewing")
.eq(HomeworkSubmission::getDeleted, 0)
);
vo.setSubmissionCount(submissionCount.intValue());
vo.setReviewedCount(reviewedCount.intValue());
return vo;
}
private HomeworkListVO convertToListVO(Homework homework) {
HomeworkListVO vo = new HomeworkListVO();
vo.setId(homework.getId());
vo.setName(homework.getName());
vo.setStatus(homework.getStatus());
vo.setPublishTime(homework.getPublishTime());
vo.setSubmitStartTime(homework.getSubmitStartTime());
vo.setSubmitEndTime(homework.getSubmitEndTime());
vo.setCreateTime(homework.getCreateTime());
// 统计提交人数和批改人数
Long submissionCount = homeworkSubmissionMapper.selectCount(
new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getHomeworkId, homework.getId())
.eq(HomeworkSubmission::getDeleted, 0)
);
Long reviewedCount = homeworkSubmissionMapper.selectCount(
new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getHomeworkId, homework.getId())
.eq(HomeworkSubmission::getStatus, "reviewing")
.eq(HomeworkSubmission::getDeleted, 0)
);
vo.setSubmissionCount(submissionCount.intValue());
vo.setReviewedCount(reviewedCount.intValue());
return vo;
}
private SubmissionVO convertToSubmissionVO(HomeworkSubmission submission) {
SubmissionVO vo = new SubmissionVO();
vo.setId(submission.getId());
vo.setHomeworkId(submission.getHomeworkId());
vo.setStudentId(submission.getStudentId());
vo.setStudentName(submission.getStudentName());
vo.setContent(submission.getContent());
vo.setAttachments(submission.getAttachments());
vo.setStatus(submission.getStatus());
vo.setSubmitTime(submission.getSubmitTime());
vo.setReviewTime(submission.getReviewTime());
vo.setReviewerId(submission.getReviewerId());
vo.setReviewComment(submission.getReviewComment());
vo.setReviewScore(submission.getReviewScore());
return vo;
}
private ReviewRuleVO convertToReviewRuleVO(HomeworkReviewRule rule) {
ReviewRuleVO vo = new ReviewRuleVO();
vo.setId(rule.getId());
vo.setRuleName(rule.getRuleName());
vo.setRuleType(rule.getRuleType());
vo.setTotalScore(rule.getTotalScore());
vo.setDescription(rule.getDescription());
// 查询关联的维度列表
List<HomeworkReviewDimension> dimensions = homeworkReviewDimensionMapper.selectByRuleId(rule.getId());
if (dimensions != null && !dimensions.isEmpty()) {
vo.setDimensions(dimensions.stream().map(this::convertToReviewDimensionVO).collect(Collectors.toList()));
}
return vo;
}
private ReviewDimensionVO convertToReviewDimensionVO(HomeworkReviewDimension dimension) {
ReviewDimensionVO vo = new ReviewDimensionVO();
vo.setId(dimension.getId());
vo.setName(dimension.getName());
vo.setMaxScore(dimension.getMaxScore());
vo.setDescription(dimension.getDescription());
vo.setSortOrder(dimension.getSortOrder());
return vo;
}
// ========== 新增方法实现 ==========
@Override
public List<ReviewRuleVO> getReviewRulesForSelect(Long tenantId) {
// 获取所有有效的评审规则用于下拉选择
List<HomeworkReviewRule> rules = homeworkReviewRuleMapper.selectList(
new LambdaQueryWrapper<HomeworkReviewRule>()
.eq(HomeworkReviewRule::getTenantId, tenantId)
.eq(HomeworkReviewRule::getDeleted, 0)
.orderByDesc(HomeworkReviewRule::getCreateTime)
);
return rules.stream()
.map(this::convertToReviewRuleVO)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReviewRuleVO updateReviewRule(Long id, UpdateReviewRuleDTO dto, Long tenantId) {
HomeworkReviewRule rule = homeworkReviewRuleMapper.selectById(id);
if (rule == null || !rule.getTenantId().equals(tenantId)) {
throw new BusinessException("评审规则不存在");
}
if (dto.getRuleName() != null) {
rule.setRuleName(dto.getRuleName());
}
if (dto.getDescription() != null) {
rule.setDescription(dto.getDescription());
}
// 注意维度列表需要单独处理
if (dto.getDimensions() != null && !dto.getDimensions().isEmpty()) {
// 删除原有维度
homeworkReviewDimensionMapper.deleteByRuleId(id);
// 插入新维度
int sortOrder = 0;
for (ReviewDimensionDTO dim : dto.getDimensions()) {
HomeworkReviewDimension dimension = new HomeworkReviewDimension();
dimension.setRuleId(id);
dimension.setTenantId(tenantId);
dimension.setName(dim.getName());
dimension.setMaxScore(dim.getMaxScore());
dimension.setDescription(dim.getDescription());
dimension.setSortOrder(sortOrder++);
homeworkReviewDimensionMapper.insert(dimension);
}
}
homeworkReviewRuleMapper.updateById(rule);
log.info("更新评审规则成功ID={}", id);
return convertToReviewRuleVO(rule);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteReviewRule(Long id, Long tenantId) {
HomeworkReviewRule rule = homeworkReviewRuleMapper.selectById(id);
if (rule == null || !rule.getTenantId().equals(tenantId)) {
throw new BusinessException("评审规则不存在");
}
// 删除关联的维度
homeworkReviewDimensionMapper.deleteByRuleId(id);
// 逻辑删除规则
rule.setDeleted(1);
homeworkReviewRuleMapper.updateById(rule);
log.info("删除评审规则成功ID={}", id);
}
@Override
public Page<HomeworkListVO> getMyHomeworks(HomeworkQueryDTO queryDTO, Long userId, Long tenantId) {
Page<Homework> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
// TODO: 需要根据用户 ID 查询关联的班级/年级进而查询作业
// 这里简化处理查询所有已发布的作业
LambdaQueryWrapper<Homework> wrapper = new LambdaQueryWrapper<Homework>()
.eq(Homework::getTenantId, tenantId)
.eq(Homework::getDeleted, 0)
.eq(Homework::getStatus, "published")
.like(queryDTO.getNameKeyword() != null, Homework::getName, queryDTO.getNameKeyword())
.orderByDesc(Homework::getCreateTime);
Page<Homework> resultPage = this.page(page, wrapper);
List<HomeworkListVO> voList = resultPage.getRecords().stream()
.map(this::convertToListVO)
.collect(Collectors.toList());
Page<HomeworkListVO> voPage = new Page<>(
resultPage.getCurrent(),
resultPage.getSize(),
resultPage.getTotal()
);
voPage.setRecords(voList);
return voPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public HomeworkDetailVO unpublish(Long id, Long tenantId) {
Homework homework = this.getById(id);
if (homework == null || !homework.getTenantId().equals(tenantId)) {
throw new BusinessException("作业不存在");
}
homework.setStatus("unpublished");
this.updateById(homework);
log.info("撤销发布作业成功ID={}", id);
return convertToDetailVO(homework);
}
@Override
public Page<SubmissionVO> getSubmissionsList(SubmissionQueryDTO queryDTO, Long tenantId) {
Page<HomeworkSubmission> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
LambdaQueryWrapper<HomeworkSubmission> wrapper = new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getTenantId, tenantId)
.eq(HomeworkSubmission::getDeleted, 0)
.eq(queryDTO.getHomeworkId() != null, HomeworkSubmission::getHomeworkId, queryDTO.getHomeworkId())
.like(queryDTO.getStudentName() != null, HomeworkSubmission::getStudentName, queryDTO.getStudentName())
.eq(queryDTO.getStatus() != null, HomeworkSubmission::getStatus, queryDTO.getStatus())
.orderByDesc(HomeworkSubmission::getSubmitTime);
Page<HomeworkSubmission> resultPage = homeworkSubmissionMapper.selectPage(page, wrapper);
List<SubmissionVO> voList = resultPage.getRecords().stream()
.map(this::convertToSubmissionVO)
.collect(Collectors.toList());
Page<SubmissionVO> voPage = new Page<>(
resultPage.getCurrent(),
resultPage.getSize(),
resultPage.getTotal()
);
voPage.setRecords(voList);
return voPage;
}
@Override
public SubmissionVO getSubmissionDetail(Long id, Long tenantId) {
HomeworkSubmission submission = homeworkSubmissionMapper.selectById(id);
if (submission == null || !submission.getTenantId().equals(tenantId)) {
throw new BusinessException("提交记录不存在");
}
return convertToSubmissionVO(submission);
}
@Override
public List<com.lesingle.creation.vo.school.ClassTreeNodeVO> getClassTree(Long tenantId) {
// TODO: 需要实现班级树查询逻辑
// 这里返回空列表后续需要根据实际的班级/年级表实现
log.warn("getClassTree 方法暂未实现tenantId={}", tenantId);
return List.of();
}
@Override
public SubmissionVO getMySubmission(Long homeworkId, Long userId, Long tenantId) {
HomeworkSubmission submission = homeworkSubmissionMapper.selectOne(
new LambdaQueryWrapper<HomeworkSubmission>()
.eq(HomeworkSubmission::getTenantId, tenantId)
.eq(HomeworkSubmission::getHomeworkId, homeworkId)
.eq(HomeworkSubmission::getStudentId, userId)
.eq(HomeworkSubmission::getDeleted, 0)
);
if (submission == null) {
return null;
}
return convertToSubmissionVO(submission);
}
@Override
@Transactional(rollbackFor = Exception.class)
public SubmissionVO createScore(CreateScoreDTO dto, Long tenantId, Long reviewerId) {
HomeworkSubmission submission = homeworkSubmissionMapper.selectById(dto.getSubmissionId());
if (submission == null || !submission.getTenantId().equals(tenantId)) {
throw new BusinessException("提交记录不存在");
}
// TODO: 保存维度评分到 homework_dimension_scores
// 这里先更新总分和评语
if (dto.getDimensionScores() != null && !dto.getDimensionScores().isEmpty()) {
java.math.BigDecimal totalScore = dto.getDimensionScores().stream()
.map(com.lesingle.creation.dto.homework.DimensionScoreDTO::getScore)
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
submission.setReviewScore(totalScore);
}
submission.setReviewComment(dto.getComments());
submission.setReviewerId(reviewerId);
submission.setStatus("reviewing");
submission.setReviewTime(LocalDateTime.now());
homeworkSubmissionMapper.updateById(submission);
log.info("创建评分成功,提交 ID={}, 评审人 ID={}", dto.getSubmissionId(), reviewerId);
return convertToSubmissionVO(submission);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void markViolation(Long submissionId, String reason, Long tenantId) {
HomeworkSubmission submission = homeworkSubmissionMapper.selectById(submissionId);
if (submission == null || !submission.getTenantId().equals(tenantId)) {
throw new BusinessException("提交记录不存在");
}
submission.setStatus("violation");
submission.setReviewComment("违规:" + reason);
homeworkSubmissionMapper.updateById(submission);
log.info("标记违规成功,提交 ID={}, 原因={}", submissionId, reason);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void resetScore(Long submissionId, Long tenantId) {
HomeworkSubmission submission = homeworkSubmissionMapper.selectById(submissionId);
if (submission == null || !submission.getTenantId().equals(tenantId)) {
throw new BusinessException("提交记录不存在");
}
submission.setReviewScore(null);
submission.setReviewComment(null);
submission.setReviewerId(null);
submission.setStatus("submitted");
homeworkSubmissionMapper.updateById(submission);
log.info("重置评分成功,提交 ID={}", submissionId);
}
}

View File

@ -1,173 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.dto.schoolclass.CreateClassDTO;
import com.lesingle.creation.dto.schoolclass.UpdateClassDTO;
import com.lesingle.creation.entity.Grade;
import com.lesingle.creation.entity.SchoolClass;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.mapper.GradeMapper;
import com.lesingle.creation.mapper.SchoolClassMapper;
import com.lesingle.creation.service.SchoolClassService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.vo.grade.GradeVO;
import com.lesingle.creation.vo.schoolclass.ClassVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 班级服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SchoolClassServiceImpl extends ServiceImpl<SchoolClassMapper, SchoolClass> implements SchoolClassService {
private final SchoolClassMapper schoolClassMapper;
private final GradeMapper gradeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public ClassVO create(CreateClassDTO dto, Long tenantId, Long creatorId) {
// 检查班级编码是否已存在
SchoolClass existing = schoolClassMapper.selectByCode(tenantId, dto.getCode());
if (existing != null) {
throw new BusinessException("班级编码已存在");
}
// 验证年级是否存在
Grade grade = gradeMapper.selectById(dto.getGradeId());
if (grade == null) {
throw new BusinessException("年级不存在");
}
SchoolClass schoolClass = new SchoolClass();
schoolClass.setTenantId(tenantId);
schoolClass.setGradeId(dto.getGradeId());
schoolClass.setName(dto.getName());
schoolClass.setCode(dto.getCode());
schoolClass.setType(dto.getType());
schoolClass.setCapacity(dto.getCapacity());
schoolClass.setDescription(dto.getDescription());
schoolClass.setValidState(1);
schoolClassMapper.insert(schoolClass);
log.info("创建班级成功ID={}, 名称={}", schoolClass.getId(), dto.getName());
return convertToVO(schoolClass);
}
@Override
public Page<ClassVO> pageQuery(Integer pageNum, Integer pageSize, Long tenantId, Long gradeId, Integer type) {
Page<SchoolClass> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SchoolClass> wrapper = new LambdaQueryWrapper<SchoolClass>()
.eq(SchoolClass::getTenantId, tenantId)
.eq(SchoolClass::getDeleted, 0)
.eq(gradeId != null, SchoolClass::getGradeId, gradeId)
.eq(type != null, SchoolClass::getType, type)
.orderByAsc(SchoolClass::getName);
Page<SchoolClass> resultPage = this.page(page, wrapper);
Page<ClassVO> voPage = new Page<>(resultPage.getCurrent(), resultPage.getSize(), resultPage.getTotal());
voPage.setRecords(resultPage.getRecords().stream().map(this::convertToVO).toList());
return voPage;
}
@Override
public ClassVO getDetail(Long id, Long tenantId) {
SchoolClass schoolClass = this.getById(id);
if (schoolClass == null || !schoolClass.getTenantId().equals(tenantId)) {
throw new BusinessException("班级不存在");
}
return convertToVO(schoolClass);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ClassVO update(Long id, UpdateClassDTO dto, Long tenantId, Long modifierId) {
SchoolClass schoolClass = this.getById(id);
if (schoolClass == null || !schoolClass.getTenantId().equals(tenantId)) {
throw new BusinessException("班级不存在");
}
if (dto.getGradeId() != null) {
// 验证年级是否存在
Grade grade = gradeMapper.selectById(dto.getGradeId());
if (grade == null) {
throw new BusinessException("年级不存在");
}
schoolClass.setGradeId(dto.getGradeId());
}
if (dto.getName() != null) {
schoolClass.setName(dto.getName());
}
if (dto.getCode() != null) {
// 检查新编码是否已被其他班级使用
SchoolClass existing = schoolClassMapper.selectByCode(tenantId, dto.getCode());
if (existing != null && !existing.getId().equals(id)) {
throw new BusinessException("班级编码已存在");
}
schoolClass.setCode(dto.getCode());
}
if (dto.getType() != null) {
schoolClass.setType(dto.getType());
}
if (dto.getCapacity() != null) {
schoolClass.setCapacity(dto.getCapacity());
}
if (dto.getDescription() != null) {
schoolClass.setDescription(dto.getDescription());
}
this.updateById(schoolClass);
log.info("更新班级成功ID={}", id);
return convertToVO(schoolClass);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long tenantId) {
SchoolClass schoolClass = this.getById(id);
if (schoolClass == null || !schoolClass.getTenantId().equals(tenantId)) {
throw new BusinessException("班级不存在");
}
schoolClass.setDeleted(1);
this.updateById(schoolClass);
log.info("删除班级成功ID={}", id);
}
private ClassVO convertToVO(SchoolClass schoolClass) {
ClassVO vo = new ClassVO();
vo.setId(schoolClass.getId());
vo.setTenantId(schoolClass.getTenantId());
vo.setGradeId(schoolClass.getGradeId());
vo.setName(schoolClass.getName());
vo.setCode(schoolClass.getCode());
vo.setType(schoolClass.getType());
vo.setCapacity(schoolClass.getCapacity());
vo.setDescription(schoolClass.getDescription());
vo.setValidState(schoolClass.getValidState());
vo.setCreateTime(schoolClass.getCreateTime());
vo.setUpdateTime(schoolClass.getUpdateTime());
// 关联查询年级信息
Grade grade = gradeMapper.selectById(schoolClass.getGradeId());
if (grade != null) {
GradeVO gradeVO = new GradeVO();
gradeVO.setId(grade.getId());
gradeVO.setName(grade.getName());
gradeVO.setCode(grade.getCode());
gradeVO.setLevel(grade.getLevel());
vo.setGrade(gradeVO);
}
return vo;
}
}

View File

@ -1,133 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.dto.school.CreateSchoolDTO;
import com.lesingle.creation.dto.school.UpdateSchoolDTO;
import com.lesingle.creation.entity.School;
import com.lesingle.creation.mapper.SchoolMapper;
import com.lesingle.creation.service.SchoolService;
import com.lesingle.creation.vo.school.SchoolVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 学校服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SchoolServiceImpl extends ServiceImpl<SchoolMapper, School> implements SchoolService {
private final SchoolMapper schoolMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public SchoolVO create(CreateSchoolDTO dto, Long tenantId, Long creatorId) {
log.info("开始创建学校,租户 ID: {}", tenantId);
// 检查租户是否已有学校信息
School existingSchool = schoolMapper.selectByTenantId(tenantId);
if (existingSchool != null) {
throw new BusinessException("该租户已存在学校信息");
}
// 创建学校
School school = new School();
school.setTenantId(tenantId);
school.setAddress(dto.getAddress());
school.setPhone(dto.getPhone());
school.setPrincipal(dto.getPrincipal());
school.setEstablished(dto.getEstablished());
school.setDescription(dto.getDescription());
school.setLogo(dto.getLogo());
school.setWebsite(dto.getWebsite());
school.setValidState(1);
school.setCreateBy(String.valueOf(creatorId));
schoolMapper.insert(school);
log.info("学校创建成功,学校 ID: {}", school.getId());
return convertToVO(school);
}
@Override
public SchoolVO getByTenantId(Long tenantId) {
log.info("查询学校信息,租户 ID: {}", tenantId);
School school = schoolMapper.selectByTenantId(tenantId);
if (school == null) {
log.debug("当前租户尚未创建学校,租户 ID: {}", tenantId);
return null;
}
return convertToVO(school);
}
@Override
@Transactional(rollbackFor = Exception.class)
public SchoolVO update(UpdateSchoolDTO dto, Long tenantId, Long modifierId) {
log.info("更新学校信息,租户 ID: {}", tenantId);
School existingSchool = schoolMapper.selectByTenantId(tenantId);
if (existingSchool == null) {
throw new BusinessException("学校信息不存在");
}
// 更新学校信息
School school = new School();
school.setId(existingSchool.getId());
school.setAddress(dto.getAddress());
school.setPhone(dto.getPhone());
school.setPrincipal(dto.getPrincipal());
school.setEstablished(dto.getEstablished());
school.setDescription(dto.getDescription());
school.setLogo(dto.getLogo());
school.setWebsite(dto.getWebsite());
school.setUpdateBy(String.valueOf(modifierId));
schoolMapper.updateById(school);
log.info("学校更新成功,学校 ID: {}", school.getId());
return convertToVO(schoolMapper.selectById(school.getId()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long tenantId) {
log.info("删除学校信息,租户 ID: {}", tenantId);
School school = schoolMapper.selectByTenantId(tenantId);
if (school == null) {
throw new BusinessException("学校信息不存在");
}
// 删除学校逻辑删除
schoolMapper.deleteById(school.getId());
log.info("学校删除成功,学校 ID: {}", school.getId());
}
/**
* 转换为 VO
*/
private SchoolVO convertToVO(School school) {
SchoolVO vo = new SchoolVO();
vo.setId(school.getId());
vo.setTenantId(school.getTenantId());
vo.setAddress(school.getAddress());
vo.setPhone(school.getPhone());
vo.setPrincipal(school.getPrincipal());
vo.setEstablished(school.getEstablished());
vo.setDescription(school.getDescription());
vo.setLogo(school.getLogo());
vo.setWebsite(school.getWebsite());
vo.setValidState(school.getValidState());
vo.setCreateBy(school.getCreateBy());
vo.setCreateTime(school.getCreateTime());
vo.setUpdateBy(school.getUpdateBy());
vo.setUpdateTime(school.getUpdateTime());
return vo;
}
}

View File

@ -1,293 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.dto.student.CreateStudentDTO;
import com.lesingle.creation.dto.student.UpdateStudentDTO;
import com.lesingle.creation.entity.SchoolClass;
import com.lesingle.creation.entity.Student;
import com.lesingle.creation.entity.User;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.mapper.StudentMapper;
import com.lesingle.creation.mapper.UserMapper;
import com.lesingle.creation.mapper.SchoolClassMapper;
import com.lesingle.creation.service.SchoolClassService;
import com.lesingle.creation.service.StudentService;
import com.lesingle.creation.vo.student.StudentVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 学生服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
private final StudentMapper studentMapper;
private final UserMapper userMapper;
private final SchoolClassService schoolClassService;
private final SchoolClassMapper schoolClassMapper;
private final PasswordEncoder passwordEncoder;
@Override
@Transactional(rollbackFor = Exception.class)
public StudentVO create(CreateStudentDTO dto, Long tenantId, Long creatorId) {
// 检查用户名是否已存在
User existingUser = userMapper.selectByUsername(dto.getUsername());
if (existingUser != null) {
throw new BusinessException("用户名已存在");
}
// 检查学号是否已存在
Student existingStudent = studentMapper.selectByStudentNo(tenantId, dto.getStudentNo());
if (existingStudent != null) {
throw new BusinessException("学号已存在");
}
// 检查班级是否存在
SchoolClass schoolClass = schoolClassMapper.selectById(dto.getClassId());
if (schoolClass == null || !schoolClass.getTenantId().equals(tenantId)) {
throw new BusinessException("班级不存在");
}
// 创建用户账号
User user = new User();
user.setTenantId(tenantId);
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setNickname(dto.getNickname());
user.setEmail(dto.getEmail());
user.setAvatar(dto.getAvatar());
user.setUserType("student"); // 学生用户
user.setStatus("active");
user.setUserSource("admin_created");
userMapper.insert(user);
log.info("创建用户账号成功ID={}, 用户名={}", user.getId(), dto.getUsername());
// 创建学生信息
Student student = new Student();
student.setTenantId(tenantId);
student.setUserId(user.getId());
student.setClassId(dto.getClassId());
student.setStudentNo(dto.getStudentNo());
student.setPhone(dto.getPhone());
student.setIdCard(dto.getIdCard());
student.setGender(dto.getGender());
student.setBirthDate(dto.getBirthDate());
student.setEnrollmentDate(dto.getEnrollmentDate());
student.setParentName(dto.getParentName());
student.setParentPhone(dto.getParentPhone());
student.setAddress(dto.getAddress());
student.setDescription(dto.getDescription());
student.setValidState(1);
studentMapper.insert(student);
log.info("创建学生成功ID={}, 学号={}", student.getId(), dto.getStudentNo());
return convertToVO(student, user, schoolClass);
}
@Override
public List<StudentVO> list(Long tenantId, Integer page, Integer pageSize, Long classId) {
LambdaQueryWrapper<Student> wrapper = new LambdaQueryWrapper<Student>()
.eq(Student::getTenantId, tenantId)
.eq(Student::getDeleted, 0)
.eq(classId != null, Student::getClassId, classId)
.orderByDesc(Student::getCreateTime);
Page<Student> studentPage = this.page(new Page<>(page, pageSize), wrapper);
// 转换为 VO
return studentPage.getRecords().stream()
.map(student -> {
User user = userMapper.selectById(student.getUserId());
SchoolClass schoolClass = schoolClassMapper.selectById(student.getClassId());
return convertToVO(student, user, schoolClass);
})
.collect(Collectors.toList());
}
@Override
public StudentVO getDetail(Long id, Long tenantId) {
Student student = this.getById(id);
if (student == null || !student.getTenantId().equals(tenantId)) {
throw new BusinessException("学生不存在");
}
User user = userMapper.selectById(student.getUserId());
SchoolClass schoolClass = schoolClassMapper.selectById(student.getClassId());
return convertToVO(student, user, schoolClass);
}
@Override
public StudentVO getByUserId(Long userId, Long tenantId) {
Student student = studentMapper.selectByUserId(userId);
if (student == null || !student.getTenantId().equals(tenantId)) {
throw new BusinessException("学生不存在");
}
User user = userMapper.selectById(student.getUserId());
SchoolClass schoolClass = schoolClassMapper.selectById(student.getClassId());
return convertToVO(student, user, schoolClass);
}
@Override
@Transactional(rollbackFor = Exception.class)
public StudentVO update(Long id, UpdateStudentDTO dto, Long tenantId, Long modifierId) {
Student student = this.getById(id);
if (student == null || !student.getTenantId().equals(tenantId)) {
throw new BusinessException("学生不存在");
}
// 检查学号是否已被其他学生使用
if (dto.getStudentNo() != null) {
Student existingStudent = studentMapper.selectByStudentNo(tenantId, dto.getStudentNo());
if (existingStudent != null && !existingStudent.getId().equals(id)) {
throw new BusinessException("学号已存在");
}
student.setStudentNo(dto.getStudentNo());
}
// 更新学生信息
if (dto.getClassId() != null) {
SchoolClass schoolClass = schoolClassMapper.selectById(dto.getClassId());
if (schoolClass == null || !schoolClass.getTenantId().equals(tenantId)) {
throw new BusinessException("班级不存在");
}
student.setClassId(dto.getClassId());
}
if (dto.getPhone() != null) {
student.setPhone(dto.getPhone());
}
if (dto.getIdCard() != null) {
student.setIdCard(dto.getIdCard());
}
if (dto.getGender() != null) {
student.setGender(dto.getGender());
}
if (dto.getBirthDate() != null) {
student.setBirthDate(dto.getBirthDate());
}
if (dto.getEnrollmentDate() != null) {
student.setEnrollmentDate(dto.getEnrollmentDate());
}
if (dto.getParentName() != null) {
student.setParentName(dto.getParentName());
}
if (dto.getParentPhone() != null) {
student.setParentPhone(dto.getParentPhone());
}
if (dto.getAddress() != null) {
student.setAddress(dto.getAddress());
}
if (dto.getDescription() != null) {
student.setDescription(dto.getDescription());
}
if (dto.getValidState() != null) {
student.setValidState(dto.getValidState());
}
// 更新用户信息如果有
if (dto.getNickname() != null || dto.getEmail() != null || dto.getAvatar() != null) {
User user = userMapper.selectById(student.getUserId());
if (user != null) {
if (dto.getNickname() != null) {
user.setNickname(dto.getNickname());
}
if (dto.getEmail() != null) {
user.setEmail(dto.getEmail());
}
if (dto.getAvatar() != null) {
user.setAvatar(dto.getAvatar());
}
userMapper.updateById(user);
}
}
this.updateById(student);
log.info("更新学生成功ID={}", id);
User user = userMapper.selectById(student.getUserId());
SchoolClass schoolClass = schoolClassMapper.selectById(student.getClassId());
return convertToVO(student, user, schoolClass);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long tenantId) {
Student student = this.getById(id);
if (student == null || !student.getTenantId().equals(tenantId)) {
throw new BusinessException("学生不存在");
}
// 逻辑删除学生
student.setDeleted(1);
this.updateById(student);
// 同时禁用关联的用户账号
User user = userMapper.selectById(student.getUserId());
if (user != null) {
user.setStatus("disabled"); // 禁用状态
userMapper.updateById(user);
}
log.info("删除学生成功ID={}, 用户 ID={}", id, student.getUserId());
}
/**
* 转换为 VO
*/
private StudentVO convertToVO(Student student, User user, SchoolClass schoolClass) {
StudentVO vo = new StudentVO();
vo.setId(student.getId());
vo.setUserId(student.getUserId());
vo.setTenantId(student.getTenantId());
vo.setClassId(student.getClassId());
vo.setStudentNo(student.getStudentNo());
vo.setPhone(student.getPhone());
vo.setIdCard(student.getIdCard());
vo.setGender(student.getGender());
vo.setBirthDate(student.getBirthDate());
vo.setEnrollmentDate(student.getEnrollmentDate());
vo.setParentName(student.getParentName());
vo.setParentPhone(student.getParentPhone());
vo.setAddress(student.getAddress());
vo.setDescription(student.getDescription());
vo.setValidState(student.getValidState());
vo.setCreateTime(student.getCreateTime());
vo.setUpdateTime(student.getUpdateTime());
// 用户信息
if (user != null) {
StudentVO.UserInfo userInfo = new StudentVO.UserInfo();
userInfo.setUsername(user.getUsername());
userInfo.setNickname(user.getNickname());
userInfo.setEmail(user.getEmail());
userInfo.setAvatar(user.getAvatar());
vo.setUserInfo(userInfo);
}
// 班级信息
if (schoolClass != null) {
StudentVO.ClassInfo classInfo = new StudentVO.ClassInfo();
classInfo.setId(schoolClass.getId());
classInfo.setName(schoolClass.getName());
classInfo.setCode(schoolClass.getCode());
vo.setClassInfo(classInfo);
}
return vo;
}
}

View File

@ -1,307 +0,0 @@
package com.lesingle.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lesingle.creation.dto.teacher.CreateTeacherDTO;
import com.lesingle.creation.dto.teacher.UpdateTeacherDTO;
import com.lesingle.creation.entity.Department;
import com.lesingle.creation.entity.Teacher;
import com.lesingle.creation.entity.User;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.mapper.TeacherMapper;
import com.lesingle.creation.mapper.UserMapper;
import com.lesingle.creation.service.DepartmentService;
import com.lesingle.creation.service.TeacherService;
import com.lesingle.creation.vo.teacher.TeacherVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 教师服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> implements TeacherService {
private final TeacherMapper teacherMapper;
private final UserMapper userMapper;
private final DepartmentService departmentService;
private final PasswordEncoder passwordEncoder;
@Override
@Transactional(rollbackFor = Exception.class)
public TeacherVO create(CreateTeacherDTO dto, Long tenantId, Long creatorId) {
// 检查用户名是否已存在
User existingUser = userMapper.selectByUsername(dto.getUsername());
if (existingUser != null) {
throw new BusinessException("用户名已存在");
}
// 检查工号是否已存在
Teacher existingTeacher = teacherMapper.selectByEmployeeNo(tenantId, dto.getEmployeeNo());
if (existingTeacher != null) {
throw new BusinessException("工号已存在");
}
// 检查部门是否存在
Department department = departmentService.getById(dto.getDepartmentId());
if (department == null || !department.getTenantId().equals(tenantId)) {
throw new BusinessException("部门不存在");
}
// 创建用户账号
User user = new User();
user.setTenantId(tenantId);
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setNickname(dto.getNickname());
user.setEmail(dto.getEmail());
user.setAvatar(dto.getAvatar());
user.setUserType("teacher"); // 教师用户
user.setStatus("active");
user.setUserSource("admin_created");
userMapper.insert(user);
log.info("创建用户账号成功ID={}, 用户名={}", user.getId(), dto.getUsername());
// 创建教师信息
Teacher teacher = new Teacher();
teacher.setTenantId(tenantId);
teacher.setUserId(user.getId());
teacher.setDepartmentId(dto.getDepartmentId());
teacher.setEmployeeNo(dto.getEmployeeNo());
teacher.setPhone(dto.getPhone());
teacher.setIdCard(dto.getIdCard());
teacher.setGender(dto.getGender());
teacher.setBirthDate(dto.getBirthDate());
teacher.setHireDate(dto.getHireDate());
teacher.setSubject(dto.getSubject());
teacher.setTitle(dto.getTitle());
teacher.setDescription(dto.getDescription());
teacher.setValidState(1);
teacherMapper.insert(teacher);
log.info("创建教师成功ID={}, 工号={}", teacher.getId(), dto.getEmployeeNo());
return convertToVO(teacher, user, department);
}
@Override
public List<TeacherVO> list(Long tenantId, Long departmentId, String nickname, String username) {
LambdaQueryWrapper<Teacher> wrapper = new LambdaQueryWrapper<Teacher>()
.eq(Teacher::getTenantId, tenantId)
.eq(Teacher::getDeleted, 0)
.eq(departmentId != null, Teacher::getDepartmentId, departmentId)
.orderByDesc(Teacher::getCreateTime);
List<Teacher> teachers = this.list(wrapper);
// 过滤用户名和昵称
if (username != null || nickname != null) {
List<Long> userIds = teachers.stream().map(Teacher::getUserId).collect(Collectors.toList());
LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<User>()
.in(User::getId, userIds)
.eq(User::getDeleted, 0);
if (username != null && !username.isEmpty()) {
userWrapper.like(User::getUsername, username);
}
if (nickname != null && !nickname.isEmpty()) {
userWrapper.like(User::getNickname, nickname);
}
List<User> users = userMapper.selectList(userWrapper);
List<Long> validUserIds = users.stream().map(User::getId).collect(Collectors.toList());
teachers = teachers.stream()
.filter(t -> validUserIds.contains(t.getUserId()))
.collect(Collectors.toList());
}
// 转换为 VO
return teachers.stream()
.map(teacher -> {
User user = userMapper.selectById(teacher.getUserId());
Department dept = departmentService.getById(teacher.getDepartmentId());
return convertToVO(teacher, user, dept);
})
.collect(Collectors.toList());
}
@Override
public TeacherVO getDetail(Long id, Long tenantId) {
Teacher teacher = this.getById(id);
if (teacher == null || !teacher.getTenantId().equals(tenantId)) {
throw new BusinessException("教师不存在");
}
User user = userMapper.selectById(teacher.getUserId());
Department department = departmentService.getById(teacher.getDepartmentId());
return convertToVO(teacher, user, department);
}
@Override
public TeacherVO getByUserId(Long userId, Long tenantId) {
Teacher teacher = teacherMapper.selectByUserId(userId);
if (teacher == null || !teacher.getTenantId().equals(tenantId)) {
throw new BusinessException("教师不存在");
}
User user = userMapper.selectById(teacher.getUserId());
Department department = departmentService.getById(teacher.getDepartmentId());
return convertToVO(teacher, user, department);
}
@Override
@Transactional(rollbackFor = Exception.class)
public TeacherVO update(Long id, UpdateTeacherDTO dto, Long tenantId, Long modifierId) {
Teacher teacher = this.getById(id);
if (teacher == null || !teacher.getTenantId().equals(tenantId)) {
throw new BusinessException("教师不存在");
}
// 检查工号是否已被其他教师使用
if (dto.getEmployeeNo() != null) {
Teacher existingTeacher = teacherMapper.selectByEmployeeNo(tenantId, dto.getEmployeeNo());
if (existingTeacher != null && !existingTeacher.getId().equals(id)) {
throw new BusinessException("工号已存在");
}
teacher.setEmployeeNo(dto.getEmployeeNo());
}
// 更新教师信息
if (dto.getDepartmentId() != null) {
Department department = departmentService.getById(dto.getDepartmentId());
if (department == null || !department.getTenantId().equals(tenantId)) {
throw new BusinessException("部门不存在");
}
teacher.setDepartmentId(dto.getDepartmentId());
}
if (dto.getPhone() != null) {
teacher.setPhone(dto.getPhone());
}
if (dto.getIdCard() != null) {
teacher.setIdCard(dto.getIdCard());
}
if (dto.getGender() != null) {
teacher.setGender(dto.getGender());
}
if (dto.getBirthDate() != null) {
teacher.setBirthDate(dto.getBirthDate());
}
if (dto.getHireDate() != null) {
teacher.setHireDate(dto.getHireDate());
}
if (dto.getSubject() != null) {
teacher.setSubject(dto.getSubject());
}
if (dto.getTitle() != null) {
teacher.setTitle(dto.getTitle());
}
if (dto.getDescription() != null) {
teacher.setDescription(dto.getDescription());
}
if (dto.getValidState() != null) {
teacher.setValidState(dto.getValidState());
}
// 更新用户信息如果有
if (dto.getNickname() != null || dto.getEmail() != null || dto.getAvatar() != null) {
User user = userMapper.selectById(teacher.getUserId());
if (user != null) {
if (dto.getNickname() != null) {
user.setNickname(dto.getNickname());
}
if (dto.getEmail() != null) {
user.setEmail(dto.getEmail());
}
if (dto.getAvatar() != null) {
user.setAvatar(dto.getAvatar());
}
userMapper.updateById(user);
}
}
this.updateById(teacher);
log.info("更新教师成功ID={}", id);
User user = userMapper.selectById(teacher.getUserId());
Department department = departmentService.getById(teacher.getDepartmentId());
return convertToVO(teacher, user, department);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long tenantId) {
Teacher teacher = this.getById(id);
if (teacher == null || !teacher.getTenantId().equals(tenantId)) {
throw new BusinessException("教师不存在");
}
// 逻辑删除教师
teacher.setDeleted(1);
this.updateById(teacher);
// 同时禁用关联的用户账号
User user = userMapper.selectById(teacher.getUserId());
if (user != null) {
user.setStatus("disabled"); // 禁用状态
userMapper.updateById(user);
}
log.info("删除教师成功ID={}, 用户 ID={}", id, teacher.getUserId());
}
/**
* 转换为 VO
*/
private TeacherVO convertToVO(Teacher teacher, User user, Department department) {
TeacherVO vo = new TeacherVO();
vo.setId(teacher.getId());
vo.setUserId(teacher.getUserId());
vo.setTenantId(teacher.getTenantId());
vo.setDepartmentId(teacher.getDepartmentId());
vo.setEmployeeNo(teacher.getEmployeeNo());
vo.setPhone(teacher.getPhone());
vo.setIdCard(teacher.getIdCard());
vo.setGender(teacher.getGender());
vo.setBirthDate(teacher.getBirthDate());
vo.setHireDate(teacher.getHireDate());
vo.setSubject(teacher.getSubject());
vo.setTitle(teacher.getTitle());
vo.setDescription(teacher.getDescription());
vo.setValidState(teacher.getValidState());
vo.setCreateTime(teacher.getCreateTime());
vo.setUpdateTime(teacher.getUpdateTime());
// 用户信息
if (user != null) {
TeacherVO.UserInfo userInfo = new TeacherVO.UserInfo();
userInfo.setUsername(user.getUsername());
userInfo.setNickname(user.getNickname());
userInfo.setEmail(user.getEmail());
userInfo.setAvatar(user.getAvatar());
vo.setUserInfo(userInfo);
}
// 部门信息
if (department != null) {
TeacherVO.DepartmentInfo deptInfo = new TeacherVO.DepartmentInfo();
deptInfo.setId(department.getId());
deptInfo.setName(department.getName());
deptInfo.setCode(department.getCode());
vo.setDepartment(deptInfo);
}
return vo;
}
}

View File

@ -288,6 +288,49 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return stats;
}
@Override
public Page<UserListVO> pageCandidateUsersByRoleCode(
Long tenantId, String roleCode, String keyword, int page, int pageSize) {
if (!"teacher".equals(roleCode) && !"student".equals(roleCode)) {
throw new BusinessException("不支持的角色类型,仅允许 teacher 或 student");
}
log.info("报名场景分页查询用户,租户:{},角色:{}", tenantId, roleCode);
Page<User> userPage = new Page<>(page, pageSize);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getTenantId, tenantId);
wrapper.eq(User::getValidState, 1);
wrapper.eq(User::getStatus, "enabled");
wrapper.apply(
"EXISTS (SELECT 1 FROM t_auth_user_role ur "
+ "INNER JOIN t_auth_role r ON ur.role_id = r.id "
+ "WHERE ur.user_id = t_sys_user.id AND r.tenant_id = {0} AND r.code = {1} "
+ "AND r.deleted = 0 AND r.valid_state = 1)",
tenantId, roleCode);
if (StringUtils.hasText(keyword)) {
String kw = keyword.trim();
wrapper.and(w -> w
.like(User::getUsername, kw)
.or()
.like(User::getNickname, kw)
.or()
.like(User::getEmail, kw)
.or()
.like(User::getPhone, kw));
}
wrapper.orderByDesc(User::getCreateTime);
Page<User> resultPage = userMapper.selectPage(userPage, wrapper);
List<UserListVO> voList = resultPage.getRecords().stream()
.map(this::convertToListVO)
.collect(Collectors.toList());
Page<UserListVO> voPage = new Page<>(resultPage.getCurrent(), resultPage.getSize(), resultPage.getTotal());
voPage.setRecords(voList);
return voPage;
}
/**
* 转换为详情 VO
*/

View File

@ -1,50 +0,0 @@
package com.lesingle.creation.vo.department;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 部门响应 VO
*/
@Data
@Schema(description = "部门响应")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DepartmentVO {
@Schema(description = "部门 ID")
private Long id;
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "部门名称")
private String name;
@Schema(description = "部门编码")
private String code;
@Schema(description = "父部门 ID")
private Long parentId;
@Schema(description = "部门描述")
private String description;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "有效状态")
private Integer validState;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "子部门列表")
private List<DepartmentVO> children;
}

View File

@ -1,41 +0,0 @@
package com.lesingle.creation.vo.grade;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 年级响应 VO
*/
@Data
@Schema(description = "年级响应")
public class GradeVO {
@Schema(description = "年级 ID")
private Long id;
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "年级名称")
private String name;
@Schema(description = "年级编码")
private String code;
@Schema(description = "年级级别")
private Integer level;
@Schema(description = "年级描述")
private String description;
@Schema(description = "有效状态")
private Integer validState;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@ -1,53 +0,0 @@
package com.lesingle.creation.vo.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 作业详情 VO
*/
@Data
@Schema(description = "作业详情响应")
public class HomeworkDetailVO {
@Schema(description = "作业 ID")
private Long id;
@Schema(description = "作业名称")
private String name;
@Schema(description = "作业内容(富文本)")
private String content;
@Schema(description = "作业状态unpublished/published")
private String status;
@Schema(description = "发布时间")
private LocalDateTime publishTime;
@Schema(description = "提交开始时间")
private LocalDateTime submitStartTime;
@Schema(description = "提交结束时间")
private LocalDateTime submitEndTime;
@Schema(description = "附件列表JSON 数组)")
private String attachments;
@Schema(description = "公开范围(班级 ID 数组 JSON")
private String publishScope;
@Schema(description = "评审规则 ID")
private Long reviewRuleId;
@Schema(description = "提交人数")
private Integer submissionCount;
@Schema(description = "已批改人数")
private Integer reviewedCount;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@ -1,41 +0,0 @@
package com.lesingle.creation.vo.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 作业列表 VO
*/
@Data
@Schema(description = "作业列表响应")
public class HomeworkListVO {
@Schema(description = "作业 ID")
private Long id;
@Schema(description = "作业名称")
private String name;
@Schema(description = "作业状态unpublished/published")
private String status;
@Schema(description = "发布时间")
private LocalDateTime publishTime;
@Schema(description = "提交开始时间")
private LocalDateTime submitStartTime;
@Schema(description = "提交结束时间")
private LocalDateTime submitEndTime;
@Schema(description = "提交人数")
private Integer submissionCount;
@Schema(description = "已批改人数")
private Integer reviewedCount;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@ -1,29 +0,0 @@
package com.lesingle.creation.vo.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 作业评审维度 VO
*/
@Data
@Schema(description = "作业评审维度响应")
public class ReviewDimensionVO {
@Schema(description = "维度 ID")
private Long id;
@Schema(description = "维度名称")
private String name;
@Schema(description = "满分")
private BigDecimal maxScore;
@Schema(description = "维度描述")
private String description;
@Schema(description = "排序顺序")
private Integer sortOrder;
}

View File

@ -1,33 +0,0 @@
package com.lesingle.creation.vo.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 作业评审规则 VO
*/
@Data
@Schema(description = "作业评审规则响应")
public class ReviewRuleVO {
@Schema(description = "规则 ID")
private Long id;
@Schema(description = "规则名称")
private String ruleName;
@Schema(description = "规则类型default/custom")
private String ruleType;
@Schema(description = "总分")
private BigDecimal totalScore;
@Schema(description = "规则描述")
private String description;
@Schema(description = "评审维度列表")
private List<ReviewDimensionVO> dimensions;
}

View File

@ -1,51 +0,0 @@
package com.lesingle.creation.vo.homework;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 作业提交 VO
*/
@Data
@Schema(description = "作业提交响应")
public class SubmissionVO {
@Schema(description = "提交 ID")
private Long id;
@Schema(description = "作业 ID")
private Long homeworkId;
@Schema(description = "学生 ID")
private Long studentId;
@Schema(description = "学生姓名")
private String studentName;
@Schema(description = "提交内容")
private String content;
@Schema(description = "提交附件列表JSON 数组)")
private String attachments;
@Schema(description = "提交状态submitted/reviewing/returned")
private String status;
@Schema(description = "提交时间")
private LocalDateTime submitTime;
@Schema(description = "批改时间")
private LocalDateTime reviewTime;
@Schema(description = "批改人 ID")
private Long reviewerId;
@Schema(description = "批改评语")
private String reviewComment;
@Schema(description = "批改分数")
private BigDecimal reviewScore;
}

View File

@ -1,32 +0,0 @@
package com.lesingle.creation.vo.school;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 班级树节点 VO
*/
@Data
@Schema(description = "班级树节点")
public class ClassTreeNodeVO {
@Schema(description = "班级 ID")
private Long id;
@Schema(description = "班级名称")
private String name;
@Schema(description = "班级编码")
private String code;
@Schema(description = "年级 ID")
private Long gradeId;
@Schema(description = "年级名称")
private String gradeName;
@Schema(description = "子节点(小班、中班、大班等)")
private List<ClassTreeNodeVO> children;
}

View File

@ -1,57 +0,0 @@
package com.lesingle.creation.vo.school;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 学校信息 VO
*/
@Data
@Schema(description = "学校信息响应")
public class SchoolVO {
@Schema(description = "学校 ID")
private Long id;
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "学校地址")
private String address;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "校长姓名")
private String principal;
@Schema(description = "建校时间")
private LocalDate established;
@Schema(description = "学校描述")
private String description;
@Schema(description = "学校 Logo URL")
private String logo;
@Schema(description = "学校网站")
private String website;
@Schema(description = "有效状态1-有效2-失效")
private Integer validState;
@Schema(description = "创建人")
private String createBy;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新人")
private String updateBy;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@ -1,51 +0,0 @@
package com.lesingle.creation.vo.schoolclass;
import com.lesingle.creation.vo.grade.GradeVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 班级响应 VO
*/
@Data
@Schema(description = "班级响应")
public class ClassVO {
@Schema(description = "班级 ID")
private Long id;
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "年级 ID")
private Long gradeId;
@Schema(description = "班级名称")
private String name;
@Schema(description = "班级编码")
private String code;
@Schema(description = "班级类型1-行政班级2-兴趣班")
private Integer type;
@Schema(description = "班级容量")
private Integer capacity;
@Schema(description = "班级描述")
private String description;
@Schema(description = "有效状态")
private Integer validState;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "关联年级信息")
private GradeVO grade;
}

View File

@ -1,102 +0,0 @@
package com.lesingle.creation.vo.student;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 学生响应 VO
*/
@Data
@Schema(description = "学生响应")
public class StudentVO {
@Schema(description = "学生 ID")
private Long id;
@Schema(description = "用户 ID")
private Long userId;
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "行政班级 ID")
private Long classId;
@Schema(description = "学号")
private String studentNo;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "性别1-男2-女")
private Integer gender;
@Schema(description = "出生日期")
private LocalDate birthDate;
@Schema(description = "入学日期")
private LocalDate enrollmentDate;
@Schema(description = "家长姓名")
private String parentName;
@Schema(description = "家长电话")
private String parentPhone;
@Schema(description = "家庭地址")
private String address;
@Schema(description = "学生描述")
private String description;
@Schema(description = "有效状态")
private Integer validState;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "用户信息")
private UserInfo userInfo;
@Schema(description = "班级信息")
private ClassInfo classInfo;
/**
* 用户信息嵌套 VO
*/
@Data
@Schema(description = "用户信息")
public static class UserInfo {
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "头像 URL")
private String avatar;
}
/**
* 班级信息嵌套 VO
*/
@Data
@Schema(description = "班级信息")
public static class ClassInfo {
@Schema(description = "班级 ID")
private Long id;
@Schema(description = "班级名称")
private String name;
@Schema(description = "班级编码")
private String code;
}
}

View File

@ -1,99 +0,0 @@
package com.lesingle.creation.vo.teacher;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 教师响应 VO
*/
@Data
@Schema(description = "教师响应")
public class TeacherVO {
@Schema(description = "教师 ID")
private Long id;
@Schema(description = "用户 ID")
private Long userId;
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "部门 ID")
private Long departmentId;
@Schema(description = "工号")
private String employeeNo;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "性别1-男2-女")
private Integer gender;
@Schema(description = "出生日期")
private LocalDate birthDate;
@Schema(description = "入职日期")
private LocalDate hireDate;
@Schema(description = "任教科目")
private String subject;
@Schema(description = "职称")
private String title;
@Schema(description = "教师描述")
private String description;
@Schema(description = "有效状态")
private Integer validState;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "用户信息")
private UserInfo userInfo;
@Schema(description = "部门信息")
private DepartmentInfo department;
/**
* 用户信息嵌套 VO
*/
@Data
@Schema(description = "用户信息")
public static class UserInfo {
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "头像 URL")
private String avatar;
}
/**
* 部门信息嵌套 VO
*/
@Data
@Schema(description = "部门信息")
public static class DepartmentInfo {
@Schema(description = "部门 ID")
private Long id;
@Schema(description = "部门名称")
private String name;
@Schema(description = "部门编码")
private String code;
}
}

View File

@ -4,7 +4,7 @@ spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.250:3306/lesingle-creation-test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
url: jdbc:mysql://192.168.1.250:3306/lesingle-creation-test-2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: lesingle-creation-test
password: 8ErFZiPBGbyrTHsy
druid:

View File

@ -0,0 +1,68 @@
-- =========================================================
-- 下线学校组织、作业模块、AI 3D 任务表及相关 RBAC
-- 说明:不修改历史脚本;幂等尽量通过 DELETE/UPDATE 可重复执行
-- =========================================================
-- 1) 角色-菜单:学校管理树(id=12)、作业中心树(id=31)
DELETE rm FROM t_auth_role_menu rm
INNER JOIN t_auth_menu m ON rm.menu_id = m.id
WHERE m.id IN (12, 31) OR m.parent_id IN (12, 31);
-- 2) 租户-菜单:同上
DELETE tm FROM t_auth_tenant_menu tm
INNER JOIN t_auth_menu m ON tm.menu_id = m.id
WHERE m.id IN (12, 31) OR m.parent_id IN (12, 31);
-- 3) 角色-权限:移除待下线权限码(全租户 permission 行)
DELETE rp FROM t_auth_role_permission rp
INNER JOIN t_auth_permission p ON rp.permission_id = p.id
WHERE p.code IN (
'school:create', 'school:read', 'school:update', 'school:delete',
'department:create', 'department:read', 'department:update', 'department:delete',
'grade:create', 'grade:read', 'grade:update', 'grade:delete',
'class:create', 'class:read', 'class:update', 'class:delete',
'teacher:create', 'teacher:read', 'teacher:update', 'teacher:delete',
'student:create', 'student:read', 'student:update', 'student:delete',
'homework:create', 'homework:read', 'homework:update', 'homework:publish', 'homework:delete',
'homework:review',
'homework:review-rule:create', 'homework:review-rule:update', 'homework:review-rule:delete',
'ai-3d:delete'
);
-- 4) 权限行标记失效
UPDATE t_auth_permission
SET valid_state = 2, deleted = 1, update_time = CURRENT_TIMESTAMP
WHERE code IN (
'school:create', 'school:read', 'school:update', 'school:delete',
'department:create', 'department:read', 'department:update', 'department:delete',
'grade:create', 'grade:read', 'grade:update', 'grade:delete',
'class:create', 'class:read', 'class:update', 'class:delete',
'teacher:create', 'teacher:read', 'teacher:update', 'teacher:delete',
'student:create', 'student:read', 'student:update', 'student:delete',
'homework:create', 'homework:read', 'homework:update', 'homework:publish', 'homework:delete',
'homework:review',
'homework:review-rule:create', 'homework:review-rule:update', 'homework:review-rule:delete',
'ai-3d:delete'
);
-- 5) 菜单标记删除
UPDATE t_auth_menu
SET valid_state = 2, deleted = 1, update_time = CURRENT_TIMESTAMP
WHERE id IN (12, 31) OR parent_id IN (12, 31);
-- 6) 业务表(先子后父)
DROP TABLE IF EXISTS t_biz_homework_score;
DROP TABLE IF EXISTS t_biz_homework_review_dimension;
DROP TABLE IF EXISTS t_biz_homework_submission;
DROP TABLE IF EXISTS t_biz_homework;
DROP TABLE IF EXISTS t_biz_homework_review_rule;
DROP TABLE IF EXISTS t_biz_student_interest_class;
DROP TABLE IF EXISTS t_biz_student;
DROP TABLE IF EXISTS t_biz_teacher;
DROP TABLE IF EXISTS t_biz_class;
DROP TABLE IF EXISTS t_biz_grade;
DROP TABLE IF EXISTS t_biz_department;
DROP TABLE IF EXISTS t_biz_school;
DROP TABLE IF EXISTS t_biz_ai3d_task;

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lesingle.creation.mapper.HomeworkReviewDimensionMapper">
<resultMap id="BaseResultMap" type="com.lesingle.creation.entity.HomeworkReviewDimension">
<id column="id" property="id"/>
<result column="rule_id" property="ruleId"/>
<result column="tenant_id" property="tenantId"/>
<result column="name" property="name"/>
<result column="max_score" property="maxScore"/>
<result column="description" property="description"/>
<result column="sort_order" property="sortOrder"/>
<result column="create_time" property="createTime"/>
<result column="deleted" property="deleted"/>
</resultMap>
<!-- 根据规则 ID 删除维度 -->
<delete id="deleteByRuleId" parameterType="java.lang.Long">
UPDATE t_biz_homework_review_dimension SET deleted = 1 WHERE rule_id = #{ruleId}
</delete>
<!-- 根据规则 ID 查询维度列表 -->
<select id="selectByRuleId" resultMap="BaseResultMap">
SELECT * FROM t_biz_homework_review_dimension
WHERE rule_id = #{ruleId} AND deleted = 0
ORDER BY sort_order ASC
</select>
</mapper>

View File

@ -1,91 +0,0 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
export interface Class {
id: number;
tenantId: number;
gradeId: number;
name: string;
code: string;
type: number; // 1-行政班级2-兴趣班
capacity?: number;
description?: string;
validState: number;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
grade?: {
id: number;
name: string;
code: string;
level: number;
};
_count?: {
students: number;
studentInterestClasses: number;
};
}
export interface CreateClassForm {
gradeId: number;
name: string;
code: string;
type: number;
capacity?: number;
description?: string;
}
export interface UpdateClassForm {
gradeId?: number;
name?: string;
code?: string;
type?: number;
capacity?: number;
description?: string;
}
// 获取班级列表
export async function getClassesList(
params: PaginationParams & { gradeId?: number; type?: number }
): Promise<PaginationResponse<Class>> {
const response = await request.get<any, PaginationResponse<Class>>("/api/classes", {
params,
});
return response;
}
// 获取单个班级详情
export async function getClassDetail(id: number): Promise<Class> {
const response = await request.get<any, Class>(`/api/classes/${id}`);
return response;
}
// 创建班级
export async function createClass(data: CreateClassForm): Promise<Class> {
const response = await request.post<any, Class>("/api/classes", data);
return response;
}
// 更新班级
export async function updateClass(
id: number,
data: UpdateClassForm
): Promise<Class> {
const response = await request.put<any, Class>(`/api/classes/${id}`, data);
return response;
}
// 删除班级
export async function deleteClass(id: number): Promise<void> {
return await request.delete<any, void>(`/api/classes/${id}`);
}
export const classesApi = {
getList: getClassesList,
getDetail: getClassDetail,
create: createClass,
update: updateClass,
delete: deleteClass,
};

View File

@ -754,8 +754,38 @@ export const reviewRulesApi = {
},
};
/** 报名场景选人(后端 UserListVOid 即用户 ID */
export interface RegistrationCandidateUser {
id: number;
tenantId?: number;
username: string;
nickname: string;
email?: string;
phone?: string;
gender?: string;
avatar?: string;
userType?: string;
status?: string;
createTime?: string;
roleNames?: string[];
}
// 报名管理
export const registrationsApi = {
/** 按角色teacher/student分页查询本租户可选用户 */
getCandidateUsers: async (params: {
roleCode: "teacher" | "student";
keyword?: string;
page?: number;
pageSize?: number;
}): Promise<PaginationResponse<RegistrationCandidateUser>> => {
const response = await request.get<
any,
PaginationResponse<RegistrationCandidateUser>
>("/api/contests/registrations/candidate-users", { params });
return response;
},
// 报名统计
getStats: async (contestId?: number): Promise<RegistrationStats> => {
const params: any = {};

View File

@ -1,91 +0,0 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
export interface Department {
id: number;
tenantId: number;
name: string;
code: string;
parentId?: number | null;
description?: string;
sort: number;
validState: number;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
parent?: Department;
children?: Department[];
_count?: {
teachers: number;
children: number;
};
}
export interface CreateDepartmentForm {
name: string;
code: string;
parentId?: number;
description?: string;
sort?: number;
}
export interface UpdateDepartmentForm {
name?: string;
code?: string;
parentId?: number | null;
description?: string;
sort?: number;
}
// 获取部门列表
export async function getDepartmentsList(
params: PaginationParams & { parentId?: number }
): Promise<PaginationResponse<Department>> {
const response = await request.get<any, PaginationResponse<Department>>("/api/departments", {
params,
});
return response;
}
// 获取部门树
export async function getDepartmentsTree(): Promise<Department[]> {
const response = await request.get<any, Department[]>("/api/departments/tree");
return response;
}
// 获取单个部门详情
export async function getDepartmentDetail(id: number): Promise<Department> {
const response = await request.get<any, Department>(`/api/departments/${id}`);
return response;
}
// 创建部门
export async function createDepartment(data: CreateDepartmentForm): Promise<Department> {
const response = await request.post<any, Department>("/api/departments", data);
return response;
}
// 更新部门
export async function updateDepartment(
id: number,
data: UpdateDepartmentForm
): Promise<Department> {
const response = await request.put<any, Department>(`/api/departments/${id}`, data);
return response;
}
// 删除部门
export async function deleteDepartment(id: number): Promise<void> {
return await request.delete<any, void>(`/api/departments/${id}`);
}
export const departmentsApi = {
getList: getDepartmentsList,
getTree: getDepartmentsTree,
getDetail: getDepartmentDetail,
create: createDepartment,
update: updateDepartment,
delete: deleteDepartment,
};

View File

@ -1,78 +0,0 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
export interface Grade {
id: number;
tenantId: number;
name: string;
code: string;
level: number;
description?: string;
validState: number;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
_count?: {
classes: number;
};
}
export interface CreateGradeForm {
name: string;
code: string;
level: number;
description?: string;
}
export interface UpdateGradeForm {
name?: string;
code?: string;
level?: number;
description?: string;
}
// 获取年级列表
export async function getGradesList(
params: PaginationParams
): Promise<PaginationResponse<Grade>> {
const response = await request.get<any, PaginationResponse<Grade>>("/api/grades", {
params,
});
return response;
}
// 获取单个年级详情
export async function getGradeDetail(id: number): Promise<Grade> {
const response = await request.get<any, Grade>(`/api/grades/${id}`);
return response;
}
// 创建年级
export async function createGrade(data: CreateGradeForm): Promise<Grade> {
const response = await request.post<any, Grade>("/api/grades", data);
return response;
}
// 更新年级
export async function updateGrade(
id: number,
data: UpdateGradeForm
): Promise<Grade> {
const response = await request.put<any, Grade>(`/api/grades/${id}`, data);
return response;
}
// 删除年级
export async function deleteGrade(id: number): Promise<void> {
return await request.delete<any, void>(`/api/grades/${id}`);
}
export const gradesApi = {
getList: getGradesList,
getDetail: getGradeDetail,
create: createGrade,
update: updateGrade,
delete: deleteGrade,
};

View File

@ -1,415 +0,0 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
// ==================== 作业相关类型 ====================
export interface Homework {
id: number;
tenantId: number;
name: string;
content?: string;
status: "unpublished" | "published";
publishTime?: string;
submitStartTime: string;
submitEndTime: string;
attachments?: HomeworkAttachment[];
publishScope?: number[];
publishScopeNames?: string[];
reviewRuleId?: number;
reviewRule?: HomeworkReviewRule;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
validState?: number;
_count?: {
submissions: number;
};
// 学生端:我的提交记录
submission?: {
id: number;
workName: string;
submitTime: string;
totalScore?: number;
} | null;
}
export interface HomeworkAttachment {
fileName: string;
fileUrl: string;
size?: string;
}
export interface CreateHomeworkForm {
name: string;
content?: string;
submitStartTime: string;
submitEndTime: string;
attachments?: HomeworkAttachment[];
publishScope?: number[];
reviewRuleId?: number;
}
export interface UpdateHomeworkForm extends Partial<CreateHomeworkForm> {}
export interface QueryHomeworkParams extends PaginationParams {
name?: string;
status?: string;
submitStartTime?: string;
submitEndTime?: string;
}
// ==================== 提交记录相关类型 ====================
export interface HomeworkSubmission {
id: number;
tenantId: number;
homeworkId: number;
studentId: number;
workNo?: string;
workName: string;
workDescription?: string;
files?: any[];
attachments?: any[];
submitTime: string;
status: "pending" | "reviewed" | "rejected";
totalScore?: number;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
validState?: number;
homework?: {
id: number;
name: string;
reviewRuleId?: number;
reviewRule?: HomeworkReviewRule;
};
student?: {
id: number;
username: string;
nickname: string;
student?: {
studentNo?: string;
class?: {
id: number;
name: string;
grade?: {
id: number;
name: string;
};
};
};
};
scores?: HomeworkScore[];
_count?: {
scores: number;
};
}
export interface QuerySubmissionParams extends PaginationParams {
homeworkId?: number;
workNo?: string;
workName?: string;
studentAccount?: string;
studentName?: string;
status?: string;
classIds?: number[];
gradeId?: number;
}
// ==================== 评审规则相关类型 ====================
export interface HomeworkReviewRule {
id: number;
tenantId: number;
name: string;
description?: string;
criteria: ReviewCriterion[];
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
validState?: number;
homeworks?: Array<{
id: number;
name: string;
}>;
}
export interface ReviewCriterion {
name: string;
maxScore: number;
description?: string;
}
export interface CreateReviewRuleForm {
name: string;
description?: string;
criteria: ReviewCriterion[];
}
// ==================== 评分相关类型 ====================
export interface HomeworkScore {
id: number;
tenantId: number;
submissionId: number;
reviewerId: number;
dimensionScores: DimensionScore[];
totalScore: number;
comments?: string;
scoreTime: string;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
validState?: number;
reviewer?: {
id: number;
nickname: string;
};
}
export interface DimensionScore {
name: string;
score: number;
maxScore: number;
}
export interface CreateScoreForm {
submissionId: number;
dimensionScores: DimensionScore[];
comments?: string;
}
// ==================== 班级树相关类型 ====================
export interface ClassTreeNode {
id: string | number;
name: string;
type: "grade" | "class";
gradeId?: number;
classId?: number;
children?: ClassTreeNode[];
}
// ==================== API 函数 ====================
// 作业管理
export const homeworksApi = {
// 获取作业列表(教师端)
getList: async (
params: QueryHomeworkParams
): Promise<PaginationResponse<Homework>> => {
const response = await request.get<any, PaginationResponse<Homework>>(
"/api/homeworks/page",
{ params }
);
return response;
},
// 获取我的作业列表(学生端)
getMyList: async (
params: QueryHomeworkParams
): Promise<PaginationResponse<Homework>> => {
const response = await request.get<any, PaginationResponse<Homework>>(
"/api/homeworks/my",
{ params }
);
return response;
},
// 获取作业详情
getDetail: async (id: number): Promise<Homework> => {
const response = await request.get<any, Homework>(
`/api/homeworks/${id}`
);
return response;
},
// 创建作业
create: async (data: CreateHomeworkForm): Promise<Homework> => {
const response = await request.post<any, Homework>(
"/api/homeworks",
data
);
return response;
},
// 更新作业
update: async (id: number, data: UpdateHomeworkForm): Promise<Homework> => {
const response = await request.put<any, Homework>(
`/api/homeworks/${id}`,
data
);
return response;
},
// 发布作业
publish: async (id: number, publishScope: number[]): Promise<Homework> => {
const response = await request.post<any, Homework>(
`/api/homeworks/${id}/publish`,
{ publishScope }
);
return response;
},
// 取消发布作业
unpublish: async (id: number): Promise<Homework> => {
const response = await request.post<any, Homework>(
`/api/homeworks/${id}/unpublish`
);
return response;
},
// 删除作业
delete: async (id: number): Promise<void> => {
return await request.delete<any, void>(`/api/homeworks/${id}`);
},
};
// 学生提交作业表单
export interface SubmitHomeworkForm {
homeworkId: number;
workName: string;
workDescription?: string;
files?: HomeworkAttachment[];
}
// 提交记录管理
export const submissionsApi = {
// 获取提交记录列表
getList: async (
params: QuerySubmissionParams
): Promise<PaginationResponse<HomeworkSubmission>> => {
const response = await request.get<
any,
PaginationResponse<HomeworkSubmission>
>("/api/homeworks/submissions", { params });
return response;
},
// 获取提交记录详情
getDetail: async (id: number): Promise<HomeworkSubmission> => {
const response = await request.get<any, HomeworkSubmission>(
`/api/homeworks/submissions/${id}`
);
return response;
},
// 获取班级树结构
getClassTree: async (): Promise<ClassTreeNode[]> => {
const response = await request.get<any, ClassTreeNode[]>(
"/api/homeworks/submissions/class-tree"
);
return response;
},
// 获取当前用户对某作业的提交记录
getMySubmission: async (homeworkId: number): Promise<HomeworkSubmission> => {
const response = await request.get<any, HomeworkSubmission>(
`/api/homeworks/submissions/my/${homeworkId}`
);
return response;
},
// 提交作业
submit: async (data: SubmitHomeworkForm): Promise<HomeworkSubmission> => {
const response = await request.post<any, HomeworkSubmission>(
"/api/homeworks/submissions",
data
);
return response;
},
};
// 评审规则管理
export const reviewRulesApi = {
// 获取评审规则列表
getList: async (params?: {
name?: string;
page?: number;
pageSize?: number;
}): Promise<{
list: HomeworkReviewRule[];
total: number;
page: number;
pageSize: number;
}> => {
const response = await request.get<
any,
{
list: HomeworkReviewRule[];
total: number;
page: number;
pageSize: number;
}
>("/api/homeworks/review-rules", { params });
return response;
},
// 获取所有可用的评审规则(用于选择)
getForSelect: async (): Promise<HomeworkReviewRule[]> => {
const response = await request.get<any, HomeworkReviewRule[]>(
"/api/homeworks/review-rules/select"
);
return response;
},
// 获取评审规则详情
getDetail: async (id: number): Promise<HomeworkReviewRule> => {
const response = await request.get<any, HomeworkReviewRule>(
`/api/homeworks/review-rules/${id}`
);
return response;
},
// 创建评审规则
create: async (data: CreateReviewRuleForm): Promise<HomeworkReviewRule> => {
const response = await request.post<any, HomeworkReviewRule>(
"/api/homeworks/review-rules",
data
);
return response;
},
// 更新评审规则
update: async (
id: number,
data: Partial<CreateReviewRuleForm>
): Promise<HomeworkReviewRule> => {
const response = await request.put<any, HomeworkReviewRule>(
`/api/homeworks/review-rules/${id}`,
data
);
return response;
},
// 删除评审规则
delete: async (id: number): Promise<void> => {
await request.delete(`/api/homeworks/review-rules/${id}`);
},
};
// 评分管理
export const scoresApi = {
// 提交评分
create: async (data: CreateScoreForm): Promise<HomeworkScore> => {
const response = await request.post<any, HomeworkScore>(
"/api/homeworks/scores",
data
);
return response;
},
// 标记作品违规
markViolation: async (
submissionId: number,
reason?: string
): Promise<void> => {
await request.post(`/api/homeworks/scores/${submissionId}/violation`, {
reason,
});
},
// 重置评分
resetScore: async (submissionId: number): Promise<void> => {
await request.post(`/api/homeworks/scores/${submissionId}/reset`);
},
};

View File

@ -1,73 +0,0 @@
import request from "@/utils/request";
export interface School {
id: number;
tenantId: number;
address?: string;
phone?: string;
principal?: string;
established?: string;
description?: string;
logo?: string;
website?: string;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
tenant?: {
id: number;
name: string;
code: string;
};
}
export interface CreateSchoolForm {
address?: string;
phone?: string;
principal?: string;
established?: string;
description?: string;
logo?: string;
website?: string;
}
export interface UpdateSchoolForm {
address?: string;
phone?: string;
principal?: string;
established?: string;
description?: string;
logo?: string;
website?: string;
}
// 获取学校信息
export async function getSchool(): Promise<School> {
const response = await request.get<any, School>("/api/schools");
return response;
}
// 创建学校信息
export async function createSchool(data: CreateSchoolForm): Promise<School> {
const response = await request.post<any, School>("/api/schools", data);
return response;
}
// 更新学校信息
export async function updateSchool(data: UpdateSchoolForm): Promise<School> {
const response = await request.put<any, School>("/api/schools", data);
return response;
}
// 删除学校信息
export async function deleteSchool(): Promise<void> {
return await request.delete<any, void>("/api/schools");
}
export const schoolsApi = {
get: getSchool,
create: createSchool,
update: updateSchool,
delete: deleteSchool,
};

View File

@ -1,150 +0,0 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
export interface Student {
id: number;
userId: number;
tenantId: number;
classId: number;
studentNo?: string;
phone?: string;
idCard?: string;
gender?: number; // 1-男2-女
birthDate?: string;
enrollmentDate?: string;
parentName?: string;
parentPhone?: string;
address?: string;
description?: string;
validState: number;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
user?: {
id: number;
username: string;
nickname: string;
email?: string;
avatar?: string;
validState: number;
};
class?: {
id: number;
name: string;
code: string;
type: number;
grade?: {
id: number;
name: string;
code: string;
level: number;
};
};
interestClasses?: Array<{
id: number;
class: {
id: number;
name: string;
code: string;
type: number;
grade?: {
id: number;
name: string;
code: string;
level: number;
};
};
}>;
}
export interface CreateStudentForm {
username: string;
password: string;
nickname: string;
email?: string;
avatar?: string;
classId: number;
studentNo?: string;
phone?: string;
idCard?: string;
gender?: number;
birthDate?: string;
enrollmentDate?: string;
parentName?: string;
parentPhone?: string;
address?: string;
description?: string;
interestClassIds?: number[];
}
export interface UpdateStudentForm {
nickname?: string;
email?: string;
avatar?: string;
classId?: number;
studentNo?: string;
phone?: string;
idCard?: string;
gender?: number;
birthDate?: string;
enrollmentDate?: string;
parentName?: string;
parentPhone?: string;
address?: string;
description?: string;
validState?: number;
interestClassIds?: number[];
}
// 获取学生列表
export async function getStudentsList(
params: PaginationParams & { classId?: number }
): Promise<PaginationResponse<Student>> {
const response = await request.get<any, PaginationResponse<Student>>("/api/students", {
params,
});
return response;
}
// 获取单个学生详情
export async function getStudentDetail(id: number): Promise<Student> {
const response = await request.get<any, Student>(`/api/students/${id}`);
return response;
}
// 根据用户ID获取学生信息
export async function getStudentByUserId(userId: number): Promise<Student> {
const response = await request.get<any, Student>(`/api/students/user/${userId}`);
return response;
}
// 创建学生
export async function createStudent(data: CreateStudentForm): Promise<Student> {
const response = await request.post<any, Student>("/api/students", data);
return response;
}
// 更新学生
export async function updateStudent(
id: number,
data: UpdateStudentForm
): Promise<Student> {
const response = await request.put<any, Student>(`/api/students/${id}`, data);
return response;
}
// 删除学生
export async function deleteStudent(id: number): Promise<void> {
return await request.delete<any, void>(`/api/students/${id}`);
}
export const studentsApi = {
getList: getStudentsList,
getDetail: getStudentDetail,
getByUserId: getStudentByUserId,
create: createStudent,
update: updateStudent,
delete: deleteStudent,
};

View File

@ -1,123 +0,0 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
export interface Teacher {
id: number;
userId: number;
tenantId: number;
departmentId: number;
employeeNo?: string;
phone?: string;
idCard?: string;
gender?: number; // 1-男2-女
birthDate?: string;
hireDate?: string;
subject?: string;
title?: string;
description?: string;
validState: number;
createBy?: number;
updateBy?: number;
createTime?: string;
updateTime?: string;
user?: {
id: number;
username: string;
nickname: string;
email?: string;
avatar?: string;
validState: number;
};
department?: {
id: number;
name: string;
code: string;
};
}
export interface CreateTeacherForm {
username: string;
password: string;
nickname: string;
email?: string;
avatar?: string;
departmentId: number;
employeeNo?: string;
phone?: string;
idCard?: string;
gender?: number;
birthDate?: string;
hireDate?: string;
subject?: string;
title?: string;
description?: string;
}
export interface UpdateTeacherForm {
nickname?: string;
email?: string;
avatar?: string;
departmentId?: number;
employeeNo?: string;
phone?: string;
idCard?: string;
gender?: number;
birthDate?: string;
hireDate?: string;
subject?: string;
title?: string;
description?: string;
validState?: number;
}
// 获取教师列表
export async function getTeachersList(
params: PaginationParams & { departmentId?: number; nickname?: string; username?: string }
): Promise<PaginationResponse<Teacher>> {
const response = await request.get<any, PaginationResponse<Teacher>>("/api/teachers", {
params,
});
return response;
}
// 获取单个教师详情
export async function getTeacherDetail(id: number): Promise<Teacher> {
const response = await request.get<any, Teacher>(`/api/teachers/${id}`);
return response;
}
// 根据用户ID获取教师信息
export async function getTeacherByUserId(userId: number): Promise<Teacher> {
const response = await request.get<any, Teacher>(`/api/teachers/user/${userId}`);
return response;
}
// 创建教师
export async function createTeacher(data: CreateTeacherForm): Promise<Teacher> {
const response = await request.post<any, Teacher>("/api/teachers", data);
return response;
}
// 更新教师
export async function updateTeacher(
id: number,
data: UpdateTeacherForm
): Promise<Teacher> {
const response = await request.put<any, Teacher>(`/api/teachers/${id}`, data);
return response;
}
// 删除教师
export async function deleteTeacher(id: number): Promise<void> {
return await request.delete<any, void>(`/api/teachers/${id}`);
}
export const teachersApi = {
getList: getTeachersList,
getDetail: getTeacherDetail,
getByUserId: getTeacherByUserId,
create: createTeacher,
update: updateTeacher,
delete: deleteTeacher,
};

View File

@ -1,11 +1,10 @@
import { createRouter, createWebHistory } from "vue-router"
import type { RouteRecordRaw } from "vue-router"
import { nextTick } from "vue"
import { useAuthStore } from "@/stores/auth"
import { convertMenusToRoutes } from "@/utils/menu"
import "@/types/router"
import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
import { nextTick } from "vue";
import { useAuthStore } from "@/stores/auth";
import { convertMenusToRoutes } from "@/utils/menu";
import "@/types/router";
// 基础路由(不需要动态加载的)
const baseRoutes: RouteRecordRaw[] = [
{
path: "/:tenantCode/login",
@ -19,7 +18,6 @@ const baseRoutes: RouteRecordRaw[] = [
component: () => import("@/views/auth/Login.vue"),
meta: { requiresAuth: false },
},
// ========== 公众端路由 ==========
{
path: "/p/login",
name: "PublicLogin",
@ -78,7 +76,6 @@ const baseRoutes: RouteRecordRaw[] = [
component: () => import("@/views/public/mine/Favorites.vue"),
meta: { title: "我的收藏" },
},
// ========== 创作与作品库 ==========
{
path: "create",
name: "PublicCreate",
@ -111,15 +108,12 @@ const baseRoutes: RouteRecordRaw[] = [
},
],
},
// ========== 管理端路由 ==========
{
path: "/:tenantCode",
name: "Main",
component: () => import("@/layouts/BasicLayout.vue"),
// 不设置固定redirect由路由守卫根据用户菜单动态跳转到第一个可见菜单
meta: {},
children: [
// 创建活动路由(不需要在菜单中显示)
{
path: "contests/create",
name: "ContestsCreate",
@ -130,7 +124,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["contest:create"],
},
},
// 超管活动详情路由
{
path: "contests/:id/overview",
name: "SuperContestOverview",
@ -140,7 +133,6 @@ const baseRoutes: RouteRecordRaw[] = [
requiresAuth: true,
},
},
// 活动详情路由(不需要在菜单中显示)
{
path: "contests/:id",
name: "ContestsDetail",
@ -151,7 +143,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["contest:read", "activity:read"],
},
},
// 编辑活动路由(不需要在菜单中显示)
{
path: "contests/:id/edit",
name: "ContestsEdit",
@ -162,7 +153,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["contest:update"],
},
},
// 活动评委管理路由(不需要在菜单中显示)
{
path: "contests/:id/judges",
name: "ContestsJudges",
@ -173,7 +163,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["contest:read"],
},
},
// 个人参与报名路由
{
path: "contests/:id/register/individual",
name: "ContestsRegisterIndividual",
@ -183,7 +172,6 @@ const baseRoutes: RouteRecordRaw[] = [
requiresAuth: true,
},
},
// 团队参与报名路由
{
path: "contests/:id/register/team",
name: "ContestsRegisterTeam",
@ -193,7 +181,6 @@ const baseRoutes: RouteRecordRaw[] = [
requiresAuth: true,
},
},
// 报名管理列表路由
{
path: "contests/registrations",
name: "ContestsRegistrations",
@ -204,7 +191,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["registration:read", "contest:read"],
},
},
// 报名记录路由
{
path: "contests/registrations/:id/records",
name: "RegistrationRecords",
@ -215,7 +201,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["registration:read", "contest:read"],
},
},
// 评委评审详情路由(从评审任务列表进入)
{
path: "activities/review/:id",
name: "ActivitiesReviewDetail",
@ -226,7 +211,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["review:score"],
},
},
// 评审进度详情路由
{
path: "contests/reviews/:id/progress",
name: "ReviewProgressDetail",
@ -236,7 +220,6 @@ const baseRoutes: RouteRecordRaw[] = [
requiresAuth: true,
},
},
// 成果发布详情路由
{
path: "contests/results/:id",
name: "ContestsResultsDetail",
@ -246,7 +229,6 @@ const baseRoutes: RouteRecordRaw[] = [
requiresAuth: true,
},
},
// 参赛作品详情列表路由
{
path: "contests/works/:id/list",
name: "WorksDetail",
@ -257,29 +239,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["work:read"],
},
},
// 作业提交记录路由
{
path: "homework/submissions",
name: "HomeworkSubmissions",
component: () => import("@/views/homework/Submissions.vue"),
meta: {
title: "作业提交记录",
requiresAuth: true,
permissions: ["homework:read"],
},
},
// 学生作业详情路由
{
path: "homework/detail/:id",
name: "HomeworkDetail",
component: () => import("@/views/homework/StudentDetail.vue"),
meta: {
title: "作业详情",
requiresAuth: true,
permissions: ["homework:read"],
},
},
// 教师我的指导路由
{
path: "student-activities/guidance",
name: "TeacherGuidance",
@ -290,17 +249,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["activity:read"],
},
},
// 评委评审详情页
{
path: "activities/review/:id",
name: "ReviewDetail",
component: () => import("@/views/activities/ReviewDetail.vue"),
meta: {
title: "作品评审",
requiresAuth: true,
},
},
// 预设评语页面
{
path: "activities/preset-comments",
name: "PresetComments",
@ -310,7 +258,6 @@ const baseRoutes: RouteRecordRaw[] = [
requiresAuth: true,
},
},
// ========== 数据统计 ==========
{
path: "analytics/overview",
name: "AnalyticsOverview",
@ -331,7 +278,6 @@ const baseRoutes: RouteRecordRaw[] = [
permissions: ["contest:read"],
},
},
// 动态路由将在这里添加
],
},
{
@ -345,523 +291,461 @@ const baseRoutes: RouteRecordRaw[] = [
name: "NotFound",
component: () => import("@/views/error/404.vue"),
},
]
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: baseRoutes,
})
});
// 标记是否已经添加了动态路由
let dynamicRoutesAdded = false
// 保存已添加的路由名称,用于清理
let addedRouteNames: string[] = []
// 保存上次的菜单数据,用于检测变化
let lastMenusHash: string = ""
let dynamicRoutesAdded = false;
let addedRouteNames: string[] = [];
let lastMenusHash = "";
/**
*
*/
export function resetDynamicRoutes(): void {
dynamicRoutesAdded = false
addedRouteNames = []
lastMenusHash = ""
dynamicRoutesAdded = false;
addedRouteNames = [];
lastMenusHash = "";
}
/**
*
* @returns Promise resolve
*/
export async function addDynamicRoutes(): Promise<void> {
const authStore = useAuthStore()
const authStore = useAuthStore();
if (!authStore.menus || authStore.menus.length === 0) {
// 如果没有菜单,重置标记
if (dynamicRoutesAdded) {
resetDynamicRoutes()
resetDynamicRoutes();
}
return
return;
}
// 计算菜单数据的哈希值,用于检测变化
const menusHash = JSON.stringify(
authStore.menus.map((m) => ({ id: m.id, path: m.path }))
)
authStore.menus.map((m) => ({ id: m.id, path: m.path })),
);
// 如果菜单数据没有变化且已经添加过路由,直接返回
if (dynamicRoutesAdded && menusHash === lastMenusHash) {
return
return;
}
// 如果已经添加过路由,先移除旧路由
if (dynamicRoutesAdded && addedRouteNames.length > 0) {
addedRouteNames.forEach((routeName) => {
if (router.hasRoute(routeName)) {
router.removeRoute(routeName)
router.removeRoute(routeName);
}
})
addedRouteNames = []
});
addedRouteNames = [];
}
// 将菜单转换为路由
const dynamicRoutes = convertMenusToRoutes(authStore.menus)
const dynamicRoutes = convertMenusToRoutes(authStore.menus);
if (dynamicRoutes.length === 0) {
return
return;
}
// 添加动态路由到根路由下
dynamicRoutes.forEach((route) => {
router.addRoute("Main", route)
router.addRoute("Main", route);
if (route.name) {
addedRouteNames.push(route.name as string)
addedRouteNames.push(route.name as string);
}
})
});
dynamicRoutesAdded = true
lastMenusHash = menusHash
dynamicRoutesAdded = true;
lastMenusHash = menusHash;
// 等待多个 tick确保路由已完全注册
await nextTick()
await nextTick()
await nextTick()
// 额外等待一小段时间,确保路由系统完全更新
await new Promise((resolve) => setTimeout(resolve, 50))
await nextTick();
await nextTick();
await nextTick();
await new Promise<void>((resolve) => setTimeout(resolve, 50));
}
/**
*
*/
function extractTenantCodeFromPath(path: string): string | null {
const match = path.match(/^\/([^/]+)/)
return match ? match[1] : null
const match = path.match(/^\/([^/]+)/);
return match ? match[1] : null;
}
/**
*
*/
function buildPathWithTenantCode(tenantCode: string, path: string): string {
// 如果路径已经包含租户编码,直接返回
if (path.startsWith(`/${tenantCode}/`)) {
return path
return path;
}
// 移除开头的斜杠(如果有)
const cleanPath = path.startsWith("/") ? path.slice(1) : path
// 如果路径是根路径,返回租户编码根路径(路由守卫会处理跳转到第一个菜单)
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
if (cleanPath === "" || cleanPath === tenantCode) {
return `/${tenantCode}`
return `/${tenantCode}`;
}
return `/${tenantCode}/${cleanPath}`
return `/${tenantCode}/${cleanPath}`;
}
router.beforeEach(async (to, _from, next) => {
// 公众端路由不走管理端认证逻辑,直接放行
if (to.path.startsWith("/p/") || to.path === "/p") {
next()
return
next();
return;
}
const authStore = useAuthStore()
const authStore = useAuthStore();
const tenantCodeFromUrl = extractTenantCodeFromPath(to.path);
// 从URL中提取租户编码
const tenantCodeFromUrl = extractTenantCodeFromPath(to.path)
// 如果 token 存在但用户信息不存在,先获取用户信息
if (authStore.token && !authStore.user) {
try {
const userInfo = await authStore.fetchUserInfo()
const userInfo = await authStore.fetchUserInfo();
// 如果获取用户信息失败或用户信息为空,跳转到登录页
if (!userInfo) {
authStore.logout()
authStore.logout();
const tenantCode =
tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
tenantCodeFromUrl || extractTenantCodeFromPath(to.path);
if (tenantCode) {
next({
path: `/${tenantCode}/login`,
query: { redirect: to.fullPath },
})
});
} else {
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
next({ name: "LoginFallback", query: { redirect: to.fullPath } });
}
return
return;
}
// 获取用户信息后,检查租户编码一致性
const userTenantCode = userInfo?.tenantCode
const userTenantCode = userInfo?.tenantCode;
if (userTenantCode) {
// 如果URL中的租户编码与用户信息不一致更正URL
if (tenantCodeFromUrl && tenantCodeFromUrl !== userTenantCode) {
const correctedPath = buildPathWithTenantCode(
userTenantCode,
to.path.replace(`/${tenantCodeFromUrl}`, "")
)
next({ path: correctedPath, query: to.query, replace: true })
return
to.path.replace(`/${tenantCodeFromUrl}`, ""),
);
next({ path: correctedPath, query: to.query, replace: true });
return;
}
// 如果URL中没有租户编码添加租户编码
if (!tenantCodeFromUrl) {
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
next({ path: correctedPath, query: to.query, replace: true })
return
const correctedPath = buildPathWithTenantCode(
userTenantCode,
to.path,
);
next({ path: correctedPath, query: to.query, replace: true });
return;
}
}
// 获取用户信息后,添加动态路由并等待生效
await addDynamicRoutes()
// 保存原始目标路径
const targetPath = to.fullPath
// 路由已生效,重新解析目标路由
const resolved = router.resolve(targetPath)
// 如果目标是租户根路径(如 /judge、/super直接跳转到第一个菜单
const isRootPath = to.matched.length === 1 && to.matched[0].name === "Main"
await addDynamicRoutes();
const targetPath = to.fullPath;
const resolved = router.resolve(targetPath);
const isRootPath =
to.matched.length === 1 && to.matched[0].name === "Main";
if (isRootPath && authStore.menus?.length) {
const findFirst = (menus: any[]): string | null => {
const findFirst = (menus: { path?: string; component?: string; children?: typeof menus }[]): string | null => {
for (const m of menus) {
if (m.path && m.component) return m.path.startsWith("/") ? m.path.slice(1) : m.path
if (m.children?.length) { const c = findFirst(m.children); if (c) return c }
if (m.path && m.component)
return m.path.startsWith("/") ? m.path.slice(1) : m.path;
if (m.children?.length) {
const c = findFirst(m.children);
if (c) return c;
}
}
return null
}
const first = findFirst(authStore.menus)
return null;
};
const first = findFirst(authStore.menus);
if (first) {
const tc = tenantCodeFromUrl || authStore.user?.tenantCode
if (tc) { next({ path: `/${tc}/${first}`, replace: true }); return }
const tc = tenantCodeFromUrl || userInfo.tenantCode;
if (tc) {
next({ path: `/${tc}/${first}`, replace: true });
return;
}
}
}
// 如果解析后的路由不是404说明路由存在重新导航
if (resolved.name !== "NotFound") {
next({ path: targetPath, replace: true })
next({ path: targetPath, replace: true });
} else {
// 如果路由不存在,尝试重定向到用户第一个菜单
if (authStore.menus && authStore.menus.length > 0) {
const findFirstMenuPath = (menus: any[]): string | null => {
const findFirstMenuPath = (
menus: {
path?: string;
component?: string;
children?: typeof menus;
}[],
): string | null => {
for (const menu of menus) {
if (menu.path && menu.component) {
// 移除开头的斜杠
return menu.path.startsWith("/")
? menu.path.slice(1)
: menu.path
: menu.path;
}
if (menu.children && menu.children.length > 0) {
const childPath = findFirstMenuPath(menu.children)
if (childPath) return childPath
const childPath = findFirstMenuPath(menu.children);
if (childPath) return childPath;
}
}
return null
}
return null;
};
const firstMenuPath = findFirstMenuPath(authStore.menus)
const firstMenuPath = findFirstMenuPath(authStore.menus);
if (firstMenuPath) {
const user = authStore.user as { tenantCode?: string } | null
const userTenantCode = user?.tenantCode
const userTenant = userInfo.tenantCode;
const tenantCode =
tenantCodeFromUrl ||
extractTenantCodeFromPath(to.path) ||
userTenantCode
userTenant;
if (tenantCode) {
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true })
return
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true });
return;
}
}
}
// 如果路由不存在但需要认证跳转到登录页而不是404
if (to.meta.requiresAuth === false) {
// 路由确实不存在允许继续会显示404页面
next()
next();
} else {
const tenantCode =
tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
tenantCodeFromUrl || extractTenantCodeFromPath(to.path);
if (tenantCode) {
next({
path: `/${tenantCode}/login`,
query: { redirect: to.fullPath },
})
});
} else {
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
next({ name: "LoginFallback", query: { redirect: to.fullPath } });
}
}
}
return
return;
} catch (error) {
// 获取失败,清除 token 并跳转到登录页
console.error("获取用户信息失败:", error)
authStore.logout()
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
console.error("获取用户信息失败:", error);
authStore.logout();
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path);
if (tenantCode) {
next({
path: `/${tenantCode}/login`,
query: { redirect: to.fullPath },
})
});
} else {
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
next({ name: "LoginFallback", query: { redirect: to.fullPath } });
}
return
return;
}
}
// 如果 token 不存在,但需要认证,跳转到登录页
if (!authStore.token && to.meta.requiresAuth !== false) {
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path);
if (tenantCode) {
next({
path: `/${tenantCode}/login`,
query: { redirect: to.fullPath },
})
});
} else {
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
next({ name: "LoginFallback", query: { redirect: to.fullPath } });
}
return
return;
}
// 如果已登录,检查租户编码一致性
if (authStore.isAuthenticated && authStore.user) {
const userTenantCode = authStore.user.tenantCode
const userTenantCode = authStore.user.tenantCode;
if (userTenantCode) {
// 如果URL中的租户编码与用户信息不一致更正URL
if (tenantCodeFromUrl && tenantCodeFromUrl !== userTenantCode) {
const correctedPath = buildPathWithTenantCode(
userTenantCode,
to.path.replace(`/${tenantCodeFromUrl}`, "")
)
next({ path: correctedPath, query: to.query, replace: true })
return
to.path.replace(`/${tenantCodeFromUrl}`, ""),
);
next({ path: correctedPath, query: to.query, replace: true });
return;
}
// 如果URL中没有租户编码添加租户编码排除不需要认证的特殊路由
const skipTenantCodePaths = ["/login", "/403"]
const shouldSkipTenantCode = skipTenantCodePaths.some(p => to.path.startsWith(p))
const skipTenantCodePaths = ["/login", "/403"];
const shouldSkipTenantCode = skipTenantCodePaths.some((p) =>
to.path.startsWith(p),
);
if (!tenantCodeFromUrl && !shouldSkipTenantCode) {
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
next({ path: correctedPath, query: to.query, replace: true })
return
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path);
next({ path: correctedPath, query: to.query, replace: true });
return;
}
}
}
// 如果已登录且有菜单数据,添加或更新动态路由
if (authStore.isAuthenticated && authStore.menus.length > 0) {
// 保存添加路由前的状态
const wasRoutesAdded = dynamicRoutesAdded
const wasRoutesAdded = dynamicRoutesAdded;
await addDynamicRoutes();
// 添加或更新动态路由
await addDynamicRoutes()
// 如果这是第一次添加路由,需要重新导航
if (!wasRoutesAdded && dynamicRoutesAdded) {
// 等待路由完全生效
await nextTick()
await nextTick()
await nextTick();
await nextTick();
// 保存原始目标路径
const targetPath = to.fullPath
// 路由已生效,重新解析目标路由
const resolved = router.resolve(targetPath)
const targetPath = to.fullPath;
const resolved = router.resolve(targetPath);
const isMainRoute = to.name === "Main";
// 如果访问的是主路由,重定向到第一个菜单
const isMainRoute = to.name === "Main"
console.log('Route guard debug:', {
targetPath,
resolvedName: resolved.name,
resolvedPath: resolved.path,
isMainRoute,
toName: to.name,
toPath: to.path,
})
// 如果解析后的路由不是404说明路由存在重新导航
if (resolved.name !== "NotFound" && !isMainRoute) {
next({ path: targetPath, replace: true })
return
next({ path: targetPath, replace: true });
return;
}
// 如果路由不存在或是主路由,尝试重定向到用户第一个菜单
if (authStore.menus && authStore.menus.length > 0) {
const findFirstMenuPath = (menus: any[]): string | null => {
const findFirstMenuPath = (
menus: {
path?: string;
component?: string;
children?: typeof menus;
}[],
): string | null => {
for (const menu of menus) {
if (menu.path && menu.component) {
// 移除开头的斜杠
return menu.path.startsWith("/") ? menu.path.slice(1) : menu.path
return menu.path.startsWith("/")
? menu.path.slice(1)
: menu.path;
}
if (menu.children && menu.children.length > 0) {
const childPath = findFirstMenuPath(menu.children)
if (childPath) return childPath
const childPath = findFirstMenuPath(menu.children);
if (childPath) return childPath;
}
}
return null
}
return null;
};
const firstMenuPath = findFirstMenuPath(authStore.menus)
const firstMenuPath = findFirstMenuPath(authStore.menus);
if (firstMenuPath) {
const userTenantCode = authStore.user
? (authStore.user.tenantCode as string | undefined)
: undefined
const userTenantCode = authStore.user?.tenantCode;
const tenantCode =
tenantCodeFromUrl ||
extractTenantCodeFromPath(to.path) ||
userTenantCode
userTenantCode;
if (tenantCode) {
// 再次等待,确保路由完全注册
await nextTick()
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true })
return
await nextTick();
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true });
return;
}
}
}
// 如果没有任何菜单跳转到404页面
const tenantCodeFor404 =
tenantCodeFromUrl ||
extractTenantCodeFromPath(to.path) ||
(authStore.user
? (authStore.user.tenantCode as string | undefined)
: undefined)
authStore.user?.tenantCode;
if (tenantCodeFor404) {
next({ path: `/${tenantCodeFor404}/404`, replace: true })
next({ path: `/${tenantCodeFor404}/404`, replace: true });
} else {
next({ name: "NotFound" })
next({ name: "NotFound" });
}
return
return;
}
}
// 如果已登录且有菜单,但路由已添加,检查当前路由是否存在
if (
authStore.isAuthenticated &&
authStore.menus.length > 0 &&
dynamicRoutesAdded
) {
const resolved = router.resolve(to.fullPath)
// 如果访问的是 Main 路由(无具体子路径)或路由不存在,重定向到用户第一个菜单
const isMainRouteWithoutChild = to.name === "Main" || to.matched.length === 1 && to.matched[0].name === "Main"
const resolved = router.resolve(to.fullPath);
const isMainRouteWithoutChild =
to.name === "Main" ||
(to.matched.length === 1 && to.matched[0].name === "Main");
if (
(resolved.name === "NotFound" || isMainRouteWithoutChild) &&
to.name !== "Login" &&
to.name !== "LoginFallback"
) {
const findFirstMenuPath = (menus: any[]): string | null => {
const findFirstMenuPath = (
menus: {
path?: string;
component?: string;
children?: typeof menus;
}[],
): string | null => {
for (const menu of menus) {
if (menu.path && menu.component) {
return menu.path.startsWith("/") ? menu.path.slice(1) : menu.path
return menu.path.startsWith("/") ? menu.path.slice(1) : menu.path;
}
if (menu.children && menu.children.length > 0) {
const childPath = findFirstMenuPath(menu.children)
if (childPath) return childPath
const childPath = findFirstMenuPath(menu.children);
if (childPath) return childPath;
}
}
return null
}
return null;
};
const firstMenuPath = findFirstMenuPath(authStore.menus)
const firstMenuPath = findFirstMenuPath(authStore.menus);
if (firstMenuPath) {
const userTenantCode = authStore.user
? (authStore.user.tenantCode as string | undefined)
: undefined
const userTenantCode = authStore.user?.tenantCode;
const tenantCode =
tenantCodeFromUrl ||
extractTenantCodeFromPath(to.path) ||
userTenantCode
userTenantCode;
if (tenantCode) {
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true })
return
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true });
return;
}
}
// 如果没有任何菜单跳转到404页面
const tenantCodeFor404 =
tenantCodeFromUrl ||
extractTenantCodeFromPath(to.path) ||
(authStore.user
? (authStore.user.tenantCode as string | undefined)
: undefined)
authStore.user?.tenantCode;
if (tenantCodeFor404) {
next({ path: `/${tenantCodeFor404}/404`, replace: true })
return
next({ path: `/${tenantCodeFor404}/404`, replace: true });
return;
}
}
}
// 检查是否需要认证
if (to.meta.requiresAuth !== false) {
// 如果没有 token跳转到登录页
if (!authStore.token) {
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path);
if (tenantCode) {
next({
path: `/${tenantCode}/login`,
query: { redirect: to.fullPath },
})
});
} else {
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
next({ name: "LoginFallback", query: { redirect: to.fullPath } });
}
return
return;
}
// 如果有 token 但没有用户信息,跳转到登录页
if (!authStore.user) {
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path);
if (tenantCode) {
next({
path: `/${tenantCode}/login`,
query: { redirect: to.fullPath },
})
});
} else {
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
next({ name: "LoginFallback", query: { redirect: to.fullPath } });
}
return
return;
}
}
// 如果已登录,访问登录页则重定向到首页
if (
(to.name === "Login" || to.name === "LoginFallback") &&
authStore.isAuthenticated
) {
// 确保动态路由已添加并等待生效
if (!dynamicRoutesAdded && authStore.menus.length > 0) {
await addDynamicRoutes()
await addDynamicRoutes();
}
// 重定向到带租户编码的根路径(路由守卫会处理跳转到第一个菜单)
const userTenantCode = authStore.user?.tenantCode || "default"
next({ path: `/${userTenantCode}` })
return
const userTenantCode = authStore.user?.tenantCode || "default";
next({ path: `/${userTenantCode}` });
return;
}
// 处理登录页面的租户编码
if (to.name === "LoginFallback" && !tenantCodeFromUrl) {
// 如果访问的是 /login但没有租户编码检查是否有用户信息中的租户编码
if (authStore.isAuthenticated && authStore.user?.tenantCode) {
const userTenantCode = authStore.user.tenantCode
next({ path: `/${userTenantCode}/login`, replace: true })
return
const userTenantCode = authStore.user.tenantCode;
next({ path: `/${userTenantCode}/login`, replace: true });
return;
}
// 如果没有租户编码,允许访问(会显示租户输入框)
next()
return
next();
return;
}
// 检查角色权限
const requiredRoles = to.meta.roles
const requiredRoles = to.meta.roles as string[] | undefined;
if (requiredRoles && requiredRoles.length > 0) {
if (!authStore.hasAnyRole(requiredRoles)) {
// 没有所需角色,跳转到 403 页面
next({ name: "Forbidden" })
return
next({ name: "Forbidden" });
return;
}
}
// 检查权限
const requiredPermissions = to.meta.permissions
const requiredPermissions = to.meta.permissions as string[] | undefined;
if (requiredPermissions && requiredPermissions.length > 0) {
if (!authStore.hasAnyPermission(requiredPermissions)) {
// 没有所需权限,跳转到 403 页面
next({ name: "Forbidden" })
return
next({ name: "Forbidden" });
return;
}
}
next()
})
next();
});
export default router
export default router;

View File

@ -1,29 +1,16 @@
import { h } from "vue"
import type { RouteRecordRaw } from "vue-router"
import type { MenuProps } from "ant-design-vue"
import type { Menu } from "@/api/menus"
import * as Icons from "@ant-design/icons-vue"
import { h } from "vue";
import type { RouteRecordRaw } from "vue-router";
import type { MenuProps } from "ant-design-vue";
import type { Menu } from "@/api/menus";
import * as Icons from "@ant-design/icons-vue";
/**
*
*
* Vite import()
* Vite import
*/
// 空布局组件,用于父级菜单
const EmptyLayout = () => import("@/layouts/EmptyLayout.vue")
const EmptyLayout = () => import("@/layouts/EmptyLayout.vue");
const componentMap: Record<string, () => Promise<any>> = {
// 工作台模块
const componentMap: Record<string, () => Promise<unknown>> = {
"workbench/Index": () => import("@/views/workbench/Index.vue"),
// 学校管理模块
"school/schools/Index": () => import("@/views/school/schools/Index.vue"),
"school/departments/Index": () =>
import("@/views/school/departments/Index.vue"),
"school/grades/Index": () => import("@/views/school/grades/Index.vue"),
"school/classes/Index": () => import("@/views/school/classes/Index.vue"),
"school/teachers/Index": () => import("@/views/school/teachers/Index.vue"),
"school/students/Index": () => import("@/views/school/students/Index.vue"),
// 活动管理模块
"contests/Index": () => import("@/views/contests/Index.vue"),
"contests/Activities": () => import("@/views/contests/Activities.vue"),
"contests/Create": () => import("@/views/contests/Create.vue"),
@ -42,28 +29,18 @@ const componentMap: Record<string, () => Promise<any>> = {
"contests/judges/Index": () => import("@/views/contests/judges/Index.vue"),
"contests/results/Index": () => import("@/views/contests/results/Index.vue"),
"contests/notices/Index": () => import("@/views/contests/notices/Index.vue"),
"contests/ReviewRules": () => import("@/views/contests/Index.vue"), // 评审规则临时使用活动列表
// 内容管理模块
"contests/ReviewRules": () => import("@/views/contests/Index.vue"),
"content/WorkReview": () => import("@/views/content/WorkReview.vue"),
"content/WorkManagement": () => import("@/views/content/WorkManagement.vue"),
"content/TagManagement": () => import("@/views/content/TagManagement.vue"),
// 数据统计模块
"analytics/Overview": () => import("@/views/analytics/Overview.vue"),
"analytics/Review": () => import("@/views/analytics/Review.vue"),
// 作业管理模块
"homework/Index": () => import("@/views/homework/Index.vue"),
"homework/Submissions": () => import("@/views/homework/Submissions.vue"),
"homework/ReviewRules": () => import("@/views/homework/ReviewRules.vue"),
// 学生端作业模块
"homework/StudentList": () => import("@/views/homework/StudentList.vue"),
"homework/StudentDetail": () => import("@/views/homework/StudentDetail.vue"),
// 我的评审模块(评委)
"activities/Guidance": () => import("@/views/activities/Guidance.vue"),
"activities/Review": () => import("@/views/activities/Review.vue"),
"activities/ReviewDetail": () => import("@/views/activities/ReviewDetail.vue"),
"activities/Comments": () => import("@/views/activities/Comments.vue"),
"activities/PresetComments": () => import("@/views/activities/PresetComments.vue"),
// 系统管理模块
"activities/PresetComments": () =>
import("@/views/activities/PresetComments.vue"),
"system/users/Index": () => import("@/views/system/users/Index.vue"),
"system/roles/Index": () => import("@/views/system/roles/Index.vue"),
"system/permissions/Index": () =>
@ -72,304 +49,244 @@ const componentMap: Record<string, () => Promise<any>> = {
"system/tenants/Index": () => import("@/views/system/tenants/Index.vue"),
"system/dict/Index": () => import("@/views/system/dict/Index.vue"),
"system/config/Index": () => import("@/views/system/config/Index.vue"),
"system/logs/Index": () => import("@/views/system/logs/Index.vue"),
"system/public-users/Index": () =>
import("@/views/system/public-users/Index.vue"),
}
};
// 用于兜底加载未在 componentMap 中声明的视图组件(避免 Vite 动态 import 分析警告)
const viewComponentModules = import.meta.glob(
"@/views/**/*.vue"
) as Record<string, () => Promise<any>>
"@/views/**/*.vue",
) as Record<string, () => Promise<unknown>>;
/**
*
*/
/** Ant Design 菜单图标 */
export function getIconComponent(iconName: string | null | undefined) {
if (!iconName) return null
const IconComponent = (Icons as any)[iconName]
if (!iconName) return null;
const IconComponent = (Icons as Record<string, unknown>)[iconName];
if (IconComponent) {
return () => h(IconComponent)
return () => h(IconComponent as object);
}
return null
return null;
}
/**
* convertMenusToRoutes
* 使ID来区分
* name BasicLayout handleMenuClick
*/
function getRouteNameFromPath(
path: string | null | undefined,
menuId: number,
isChild: boolean = false
): string {
if (path) {
const baseName = path
.split("/")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("")
// 统一添加 menuId避免顶级菜单同名例如多个“/contests”菜单导致路由被覆盖
return `${baseName}${menuId}`
.join("");
return `${baseName}${menuId}`;
}
return `Menu${menuId}`
return `Menu${menuId}`;
}
/**
* Ant Design Vue Menu items
* key 使
*
*/
export function convertMenusToMenuItems(
menus: Menu[],
isChild: boolean = false
): MenuProps["items"] {
const result: any[] = []
function removeParentPathFromRoutePath(
routePath: string,
parentPath: string,
): string {
if (!parentPath) {
return routePath;
}
const normalizedRoutePath = routePath.startsWith("/")
? routePath.slice(1)
: routePath;
const normalizedParentPath = parentPath.startsWith("/")
? parentPath.slice(1)
: parentPath;
if (normalizedRoutePath.startsWith(normalizedParentPath + "/")) {
return normalizedRoutePath.slice(normalizedParentPath.length + 1);
}
if (normalizedRoutePath === normalizedParentPath) {
return "";
}
return normalizedRoutePath;
}
/** 后端菜单树 → Ant Design Menu itemskey 为路由 name */
export function convertMenusToMenuItems(menus: Menu[]): MenuProps["items"] {
const result: NonNullable<MenuProps["items"]> = [];
menus.forEach((menu) => {
// 如果只有一个子菜单,直接提升子菜单到当前层级
if (menu.children && menu.children.length === 1) {
const onlyChild = menu.children[0]
const childRouteName = getRouteNameFromPath(onlyChild.path, onlyChild.id, true)
const onlyChild = menu.children[0];
const childRouteName = getRouteNameFromPath(onlyChild.path, onlyChild.id);
const item: any = {
const item: Record<string, unknown> = {
key: childRouteName,
label: onlyChild.name,
title: onlyChild.name,
}
};
// 优先使用父菜单的图标,如果没有则使用子菜单的图标
const iconName = menu.icon || onlyChild.icon
const iconName = menu.icon || onlyChild.icon;
if (iconName) {
const IconComponent = getIconComponent(iconName)
const IconComponent = getIconComponent(iconName);
if (IconComponent) {
item.icon = IconComponent
item.icon = IconComponent;
}
}
// 如果这个唯一的子菜单还有子菜单,继续递归处理
if (onlyChild.children && onlyChild.children.length > 0) {
item.children = convertMenusToMenuItems(onlyChild.children, true)
item.children = convertMenusToMenuItems(onlyChild.children);
}
result.push(item)
return
result.push(item as unknown as (typeof result)[number]);
return;
}
// 正常处理:使用路由名称作为 key
const routeName = getRouteNameFromPath(menu.path, menu.id, isChild)
const routeName = getRouteNameFromPath(menu.path, menu.id);
const item: any = {
const item: Record<string, unknown> = {
key: routeName,
label: menu.name,
title: menu.name,
}
};
// 添加图标
if (menu.icon) {
const IconComponent = getIconComponent(menu.icon)
const IconComponent = getIconComponent(menu.icon);
if (IconComponent) {
item.icon = IconComponent
item.icon = IconComponent;
}
}
// 如果有多个子菜单,递归处理
if (menu.children && menu.children.length > 1) {
item.children = convertMenusToMenuItems(menu.children, true)
item.children = convertMenusToMenuItems(menu.children);
}
result.push(item)
})
result.push(item as unknown as (typeof result)[number]);
});
return result
}
/**
* Vue Router
* /:tenantCode tenantCode
*/
/**
*
*/
function removeParentPathFromRoutePath(
routePath: string,
parentPath: string
): string {
if (!parentPath) {
return routePath
}
// 标准化路径:移除开头的斜杠
const normalizedRoutePath = routePath.startsWith("/")
? routePath.slice(1)
: routePath
const normalizedParentPath = parentPath.startsWith("/")
? parentPath.slice(1)
: parentPath
// 如果子路径以父路径开头,移除父路径部分
if (normalizedRoutePath.startsWith(normalizedParentPath + "/")) {
return normalizedRoutePath.slice(normalizedParentPath.length + 1)
} else if (normalizedRoutePath === normalizedParentPath) {
// 如果路径完全相同,返回空字符串(表示当前路由)
return ""
}
return normalizedRoutePath
return result;
}
/** 后端菜单树 → 挂到 `/:tenantCode`Main下的子路由 */
export function convertMenusToRoutes(
menus: Menu[],
parentPath: string = "",
isChild: boolean = false
): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
const routes: RouteRecordRaw[] = [];
menus.forEach((menu) => {
// 构建路由路径
// 注意:这些路由会被添加到 /:tenantCode 父路由下,所以路径应该是相对路径
let routePath = menu.path
? menu.path.startsWith("/")
? menu.path.slice(1) // 移除开头的斜杠,因为这是相对路径
? menu.path.slice(1)
: menu.path
: `menu-${menu.id}`
: `menu-${menu.id}`;
// 如果有父路径,移除与父路径重合的部分
if (parentPath) {
routePath = removeParentPathFromRoutePath(routePath, parentPath)
routePath = removeParentPathFromRoutePath(routePath, parentPath);
}
// 构建路由名称(与 convertMenusToMenuItems 中的逻辑一致)
const routeName = getRouteNameFromPath(menu.path, menu.id, isChild)
const routeName = getRouteNameFromPath(menu.path, menu.id);
// 确定组件加载器
let componentLoader: (() => Promise<any>) | undefined
let componentLoader: (() => Promise<unknown>) | undefined;
if (menu.component) {
// 从组件映射中获取导入函数
// 如果组件路径以 @/ 开头,说明是完整路径,需要去掉 @/views/ 前缀和 .vue 后缀来匹配
let componentKey = menu.component
let componentKey = menu.component;
if (componentKey.startsWith("@/views/")) {
componentKey = componentKey.replace("@/views/", "").replace(".vue", "")
componentKey = componentKey
.replace("@/views/", "")
.replace(".vue", "");
} else if (componentKey.endsWith(".vue")) {
componentKey = componentKey.replace(".vue", "")
componentKey = componentKey.replace(".vue", "");
}
// 从映射中获取组件导入函数
const mappedLoader = componentMap[componentKey]
const mappedLoader = componentMap[componentKey];
if (mappedLoader) {
componentLoader = mappedLoader
componentLoader = mappedLoader;
} else {
const componentPath = menu.component
console.warn(
`组件路径 "${componentPath}" (key: "${componentKey}") 未在 componentMap 中定义,请添加到 menu.ts 的 componentMap 中`
)
// 如果找不到映射,使用 import.meta.glob 的模块映射进行兜底加载(可被 Vite 分析)
let normalizedPath = componentPath.startsWith("@/")
? componentPath
: `@/views/${componentPath}`
let normalizedPath = menu.component.startsWith("@/")
? menu.component
: `@/views/${menu.component}`;
if (!normalizedPath.endsWith(".vue")) {
normalizedPath = `${normalizedPath}.vue`
normalizedPath = `${normalizedPath}.vue`;
}
const candidatePaths = [
normalizedPath,
// 兼容部分构建环境下 glob key 可能为 /src/xxx 形式
normalizedPath.startsWith("@/views/")
? normalizedPath.replace("@/views/", "/src/views/")
: normalizedPath.startsWith("@/")
? normalizedPath.replace("@/", "/src/")
: normalizedPath,
]
];
const fallbackLoader =
candidatePaths.map((p) => viewComponentModules[p]).find(Boolean) ||
undefined
const fallbackLoader = candidatePaths
.map((p) => viewComponentModules[p])
.find(Boolean);
if (fallbackLoader) {
componentLoader = fallbackLoader
componentLoader = fallbackLoader;
} else {
console.warn(
`组件路径 "${normalizedPath}" 未找到对应的视图文件(尝试:${candidatePaths.join(
", "
)}`
)
componentLoader = undefined
`组件未找到,已跳过菜单「${menu.name}」:尝试路径 ${candidatePaths.join(", ")}`,
);
componentLoader = undefined;
}
}
} else if (menu.children && menu.children.length > 0) {
// 如果没有 component 但有子菜单,使用空布局组件来渲染子路由
componentLoader = EmptyLayout
componentLoader = EmptyLayout;
}
// 如果有子菜单,先处理子菜单
let childrenRoutes: RouteRecordRaw[] | undefined
let redirectToDefaultChild: string | undefined
let childrenRoutes: RouteRecordRaw[] | undefined;
let redirectToDefaultChild: string | undefined;
if (menu.children && menu.children.length > 0) {
childrenRoutes = convertMenusToRoutes(
menu.children,
menu.path
? menu.path.startsWith("/")
? menu.path.slice(1)
: menu.path
: parentPath,
true // 标记为子菜单
)
const nextParent = menu.path
? menu.path.startsWith("/")
? menu.path.slice(1)
: menu.path
: parentPath;
childrenRoutes = convertMenusToRoutes(menu.children, nextParent);
// 如果父菜单没有 component但子菜单中有路径为空字符串的路由默认子路由
// 应该让父路由重定向到该子路由
if (!menu.component && childrenRoutes.length > 0) {
const defaultChild = childrenRoutes.find((child) => child.path === "")
const defaultChild = childrenRoutes.find((child) => child.path === "");
if (defaultChild && defaultChild.name) {
// 重定向到默认子路由(使用路由名称)
redirectToDefaultChild = defaultChild.name as string
redirectToDefaultChild = defaultChild.name as string;
}
}
}
// 如果既没有组件也没有子路由,跳过这个菜单(无法渲染)
if (!componentLoader && (!childrenRoutes || childrenRoutes.length === 0)) {
return
return;
}
const route: RouteRecordRaw = {
const route = {
path: routePath,
name: routeName,
meta: {
title: menu.name,
requiresAuth: true,
// 如果菜单有权限要求添加到路由meta中
...(menu.permission && { permissions: [menu.permission] }),
},
...(componentLoader && { component: componentLoader }),
// 如果有重定向,添加重定向
...(componentLoader && {
component: componentLoader as () => Promise<object>,
}),
...(redirectToDefaultChild && {
redirect: { name: redirectToDefaultChild },
}),
// 如果有子菜单,添加子路由
...(childrenRoutes &&
childrenRoutes.length > 0 && {
children: childrenRoutes,
}),
} as RouteRecordRaw
childrenRoutes.length > 0 && { children: childrenRoutes }),
} as RouteRecordRaw;
routes.push(route)
})
routes.push(route);
});
return routes
return routes;
}
/**
*
*/
export function flattenMenus(menus: Menu[]): Menu[] {
const result: Menu[] = []
const result: Menu[] = [];
menus.forEach((menu) => {
result.push(menu)
result.push(menu);
if (menu.children && menu.children.length > 0) {
result.push(...flattenMenus(menu.children))
result.push(...flattenMenus(menu.children));
}
})
return result
});
return result;
}

View File

@ -157,9 +157,9 @@ import {
registrationsApi,
type Contest,
type ContestRegistration,
type RegistrationCandidateUser,
} from "@/api/contests"
import { useListRequest } from "@/composables/useListRequest"
import { type Student } from "@/api/students"
import AddParticipantDrawer from "./components/AddParticipantDrawer.vue"
import AddTeacherDrawer from "./components/AddTeacherDrawer.vue"
@ -175,7 +175,7 @@ const currentRegistrationId = ref<number | null>(null)
const contest = ref<Contest | null>(null)
//
const selectedParticipants = ref<Student[]>([])
const selectedParticipants = ref<RegistrationCandidateUser[]>([])
// 使
const {
@ -245,7 +245,7 @@ const columns: TableColumnsType = [
]
//
const getOrganizationInfo = (record: ContestRegistration | Student): string => {
const getOrganizationInfo = (record: ContestRegistration | RegistrationCandidateUser): string => {
// user.student.class
if ("user" in record && record.user) {
const user = record.user as any
@ -255,9 +255,8 @@ const getOrganizationInfo = (record: ContestRegistration | Student): string => {
}`
}
}
// record.class
if ("class" in record && record.class) {
return `${record.class.grade?.name || ""} - ${record.class.name || ""}`
if ("roleNames" in record && record.roleNames?.length) {
return record.roleNames.join("、")
}
return "-"
}
@ -280,14 +279,18 @@ const selectedTeacherUserIds = computed(() => {
// +
const displayDataSource = computed(() => {
//
const pendingParticipants = selectedParticipants.value.map((student) => ({
id: student.id,
userId: student.userId,
user: student.user,
gender: student.gender,
phone: student.phone,
class: student.class,
registrationState: undefined, //
const pendingParticipants = selectedParticipants.value.map((u) => ({
id: u.id,
userId: u.id,
user: {
nickname: u.nickname,
username: u.username,
phone: u.phone,
gender: u.gender,
},
gender: u.gender === "male" ? 1 : u.gender === "female" ? 2 : undefined,
phone: u.phone,
registrationState: undefined,
}))
//
@ -326,11 +329,6 @@ const getStatusText = (status?: string) => {
}
}
//
const addParticipantRules = {
userId: [{ required: true, message: "请选择用户", trigger: "change" }],
}
//
const fetchContestDetail = async () => {
try {
@ -346,7 +344,7 @@ const handleAddParticipant = () => {
}
//
const handleAddParticipantConfirm = (students: Student[]) => {
const handleAddParticipantConfirm = (students: RegistrationCandidateUser[]) => {
//
const existingIds = new Set(selectedParticipants.value.map((p) => p.id))
const newStudents = students.filter((s) => !existingIds.has(s.id))
@ -387,7 +385,7 @@ const handleAddTeacherConfirm = async (teachers: any[]) => {
try {
//
const promises = teachers.map((teacher) =>
registrationsApi.addTeacher(currentRegistrationId.value!, teacher.userId)
registrationsApi.addTeacher(currentRegistrationId.value!, teacher.id)
)
await Promise.all(promises)
@ -462,7 +460,7 @@ const handleSubmit = async () => {
registrationsApi.create({
contestId,
registrationType: "individual",
userId: student.userId,
userId: student.id,
})
)

View File

@ -107,19 +107,19 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.user?.nickname || record.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
{{ record.user?.gender === 'male' ? '男' : record.user?.gender === 'female' ? '女' : '-' }}
{{ record.gender === 'male' ? '男' : record.gender === 'female' ? '女' : '-' }}
</template>
<template v-else-if="column.key === 'username'">
{{ record.user?.username || record.username || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.user?.phone || record.phone || "-" }}
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.user?.tenant?.name || "-" }}
{{ record.roleNames?.join("、") || "-" }}
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" @click="handleSelectLeader">
@ -146,19 +146,19 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.user?.nickname || record.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
{{ record.user?.gender === 'male' ? '男' : record.user?.gender === 'female' ? '女' : '-' }}
{{ record.gender === 'male' ? '男' : record.gender === 'female' ? '女' : '-' }}
</template>
<template v-else-if="column.key === 'username'">
{{ record.user?.username || record.username || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.user?.phone || record.phone || "-" }}
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.user?.tenant?.name || "-" }}
{{ record.roleNames?.join("、") || "-" }}
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" danger @click="handleRemoveMember(record)">
@ -185,23 +185,23 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.user?.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
{{ record.gender === 1 ? '男' : record.gender === 2 ? '女' : '-' }}
{{ record.gender === 'male' ? '男' : record.gender === 'female' ? '女' : '-' }}
</template>
<template v-else-if="column.key === 'username'">
{{ record.user?.username || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.department?.name || "-" }}
{{ record.roleNames?.join("、") || "-" }}
</template>
<template v-else-if="column.key === 'action'">
<a-button
v-if="!(currentTeacher && record.userId === currentTeacher.userId)"
v-if="!(currentTeacher && record.id === currentTeacher.id)"
type="link"
size="small"
danger
@ -271,19 +271,19 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.user?.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
{{ record.user?.gender === 'male' ? '男' : record.user?.gender === 'female' ? '女' : '-' }}
{{ record.gender === 'male' ? '男' : record.gender === 'female' ? '女' : '-' }}
</template>
<template v-else-if="column.key === 'username'">
{{ record.user?.username || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.user?.phone || "-" }}
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.user?.tenant?.name || "-" }}
{{ record.roleNames?.join("、") || "-" }}
</template>
</template>
</a-table>
@ -334,19 +334,19 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.user?.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
{{ record.gender === 1 ? '男' : record.gender === 2 ? '女' : '-' }}
{{ record.gender === 'male' ? '男' : record.gender === 'female' ? '女' : '-' }}
</template>
<template v-else-if="column.key === 'username'">
{{ record.user?.username || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.department?.name || "-" }}
{{ record.roleNames?.join("、") || "-" }}
</template>
</template>
</a-table>
@ -385,19 +385,22 @@ import { useRoute, useRouter } from "vue-router"
import { message } from "ant-design-vue"
import { PlusOutlined } from "@ant-design/icons-vue"
import type { FormInstance, TableProps } from "ant-design-vue"
import { teamsApi, type ContestTeam, type ContestTeamMember } from "@/api/contests"
import { studentsApi, type Student } from "@/api/students"
import { teachersApi, type Teacher } from "@/api/teachers"
import {
teamsApi,
registrationsApi,
type ContestTeam,
type ContestTeamMember,
type RegistrationCandidateUser,
} from "@/api/contests"
import { useAuthStore } from "@/stores/auth"
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const tenantCode = route.params.tenantCode as string
const contestId = Number(route.params.id)
//
const currentTeacher = ref<Teacher | null>(null)
// ID
const currentTeacher = ref<RegistrationCandidateUser | null>(null)
//
const loading = ref(false)
@ -423,15 +426,15 @@ const teamRules = {
}
//
const selectedLeader = ref<Student | null>(null)
const selectedMembers = ref<Student[]>([])
const selectedTeachers = ref<Teacher[]>([])
const selectedLeader = ref<RegistrationCandidateUser | null>(null)
const selectedMembers = ref<RegistrationCandidateUser[]>([])
const selectedTeachers = ref<RegistrationCandidateUser[]>([])
//
const selectStudentModalVisible = ref(false)
const selectStudentTitle = ref("选择学生")
const selectStudentType = ref<"leader" | "member" | "teacher">("member")
const studentList = ref<Student[]>([])
const studentList = ref<RegistrationCandidateUser[]>([])
const studentListLoading = ref(false)
const studentPagination = reactive({
current: 1,
@ -443,11 +446,11 @@ const studentSearchParams = reactive({
username: "",
})
const selectedStudentKeys = ref<number[]>([])
const selectedStudentRows = ref<Student[]>([])
const selectedStudentRows = ref<RegistrationCandidateUser[]>([])
//
const selectTeacherModalVisible = ref(false)
const teacherList = ref<Teacher[]>([])
const teacherList = ref<RegistrationCandidateUser[]>([])
const teacherListLoading = ref(false)
const teacherPagination = reactive({
current: 1,
@ -459,7 +462,7 @@ const teacherSearchParams = reactive({
username: "",
})
const selectedTeacherKeys = ref<number[]>([])
const selectedTeacherRows = ref<Teacher[]>([])
const selectedTeacherRows = ref<RegistrationCandidateUser[]>([])
//
const viewMembersModalVisible = ref(false)
@ -517,7 +520,7 @@ const viewMemberColumns = [
const studentRowSelection = computed<TableProps["rowSelection"]>(() => ({
type: selectStudentType.value === "leader" ? "radio" : "checkbox",
selectedRowKeys: selectedStudentKeys.value,
onChange: (keys: any, rows: Student[]) => {
onChange: (keys: any, rows: RegistrationCandidateUser[]) => {
selectedStudentKeys.value = keys
selectedStudentRows.value = rows
},
@ -536,13 +539,12 @@ const teacherSelectColumns = [
const teacherRowSelection = computed<TableProps["rowSelection"]>(() => ({
type: "checkbox",
selectedRowKeys: selectedTeacherKeys.value,
onChange: (keys: any, rows: Teacher[]) => {
onChange: (keys: any, rows: RegistrationCandidateUser[]) => {
selectedTeacherKeys.value = keys
selectedTeacherRows.value = rows
},
getCheckboxProps: (record: Teacher) => ({
//
disabled: currentTeacher.value && record.userId === currentTeacher.value.userId,
getCheckboxProps: (record: RegistrationCandidateUser) => ({
disabled: !!(currentTeacher.value && record.id === currentTeacher.value.id),
}),
}))
@ -582,13 +584,17 @@ const fetchTeamList = async () => {
const fetchStudentList = async () => {
studentListLoading.value = true
try {
const response = await studentsApi.getList({
const kw = [studentSearchParams.username, studentSearchParams.nickname]
.filter(Boolean)
.join(" ")
.trim()
const response = await registrationsApi.getCandidateUsers({
roleCode: "student",
keyword: kw || undefined,
page: studentPagination.current,
pageSize: studentPagination.pageSize,
nickname: studentSearchParams.nickname || undefined,
username: studentSearchParams.username || undefined,
})
studentList.value = response.list
studentList.value = response.records ?? response.list ?? []
studentPagination.total = response.total
} catch (error) {
message.error("获取学生列表失败")
@ -601,13 +607,17 @@ const fetchStudentList = async () => {
const fetchTeacherList = async () => {
teacherListLoading.value = true
try {
const response = await teachersApi.getList({
const kw = [teacherSearchParams.username, teacherSearchParams.nickname]
.filter(Boolean)
.join(" ")
.trim()
const response = await registrationsApi.getCandidateUsers({
roleCode: "teacher",
keyword: kw || undefined,
page: teacherPagination.current,
pageSize: teacherPagination.pageSize,
nickname: teacherSearchParams.nickname || undefined,
username: teacherSearchParams.username || undefined,
})
teacherList.value = response.list
teacherList.value = response.records ?? response.list ?? []
teacherPagination.total = response.total
} catch (error) {
message.error("获取老师列表失败")
@ -713,7 +723,7 @@ const handleSelectStudentConfirm = () => {
if (selectStudentType.value === "leader") {
if (selectedStudentRows.value.length > 0) {
selectedLeader.value = selectedStudentRows.value[0]
teamForm.leaderId = selectedLeader.value.userId
teamForm.leaderId = selectedLeader.value.id
}
} else if (selectStudentType.value === "member") {
selectedMembers.value = [...selectedStudentRows.value]
@ -731,7 +741,7 @@ const handleSelectTeacherConfirm = () => {
//
let teachers = [...selectedTeacherRows.value]
if (currentTeacher.value) {
const hasCurrentTeacher = teachers.some(t => t.userId === currentTeacher.value!.userId)
const hasCurrentTeacher = teachers.some((t) => t.id === currentTeacher.value!.id)
if (!hasCurrentTeacher) {
teachers = [currentTeacher.value, ...teachers]
}
@ -746,18 +756,17 @@ const handleSelectTeacherCancel = () => {
}
//
const handleRemoveMember = (record: Student) => {
selectedMembers.value = selectedMembers.value.filter(m => m.id !== record.id)
const handleRemoveMember = (record: RegistrationCandidateUser) => {
selectedMembers.value = selectedMembers.value.filter((m) => m.id !== record.id)
}
//
const handleRemoveTeacher = (record: Teacher) => {
//
if (currentTeacher.value && record.userId === currentTeacher.value.userId) {
const handleRemoveTeacher = (record: RegistrationCandidateUser) => {
if (currentTeacher.value && record.id === currentTeacher.value.id) {
message.warning("当前登录老师不能移除")
return
}
selectedTeachers.value = selectedTeachers.value.filter(t => t.id !== record.id)
selectedTeachers.value = selectedTeachers.value.filter((t) => t.id !== record.id)
}
//
@ -778,16 +787,16 @@ const handleTeamSubmit = async () => {
teamSubmitLoading.value = true
const memberIds = [
selectedLeader.value.userId,
...selectedMembers.value.map(m => m.userId),
selectedLeader.value.id,
...selectedMembers.value.map((m) => m.id),
]
const teacherIds = selectedTeachers.value.map(t => t.userId)
const teacherIds = selectedTeachers.value.map((t) => t.id)
await teamsApi.create({
contestId,
teamName: teamForm.teamName,
leaderId: selectedLeader.value.userId,
leaderId: selectedLeader.value.id,
memberIds,
teacherIds: teacherIds.length > 0 ? teacherIds : undefined,
})
@ -837,19 +846,16 @@ const handleBack = () => {
//
const fetchCurrentTeacher = async () => {
const userId = authStore.user?.id
if (!userId) return
const u = authStore.user
if (!u?.id) return
//
const roles = authStore.user?.roles || []
const roles = u.roles || []
if (!roles.includes("teacher")) return
try {
const teacher = await teachersApi.getByUserId(userId)
currentTeacher.value = teacher
} catch (error) {
//
console.log("当前用户不是老师")
currentTeacher.value = {
id: u.id,
username: u.username,
nickname: u.nickname || u.username,
}
}

View File

@ -13,7 +13,6 @@
</div>
</template>
<!-- 搜索/筛选区域 -->
<div class="search-section">
<div class="search-item">
<span class="search-label">账号</span>
@ -43,27 +42,8 @@
</template>
</a-input>
</div>
<div class="search-item">
<span class="search-label">机构信息</span>
<a-select
v-model:value="searchParams.classId"
placeholder="年份+班级"
allow-clear
style="width: 200px"
@change="handleSearch"
>
<a-select-option
v-for="cls in classOptions"
:key="cls.id"
:value="cls.id"
>
{{ cls.grade?.name }} - {{ cls.name }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
@ -81,21 +61,21 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.user?.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
<a-tag v-if="record.gender === 1" color="blue"></a-tag>
<a-tag v-else-if="record.gender === 2" color="pink"></a-tag>
<a-tag v-if="record.gender === 'male'" color="blue"></a-tag>
<a-tag v-else-if="record.gender === 'female'" color="pink"></a-tag>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'account'">
{{ record.user?.username || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'contact'">
{{ record.user?.phone || record.phone || "-" }}
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'organization'">
{{ record.class?.grade?.name }} - {{ record.class?.name }}
{{ record.roleNames?.join("、") || "-" }}
</template>
</template>
</a-table>
@ -104,7 +84,7 @@
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
<a-button type="primary" @click="handleSubmit">
确定
</a-button>
</a-space>
@ -113,40 +93,37 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from "vue"
import { ref, reactive, watch } from "vue"
import { message } from "ant-design-vue"
import { SearchOutlined } from "@ant-design/icons-vue"
import type { TableColumnsType } from "ant-design-vue"
import { studentsApi, type Student } from "@/api/students"
import { classesApi, type Class } from "@/api/classes"
import {
registrationsApi,
type RegistrationCandidateUser,
} from "@/api/contests"
import { useListRequest } from "@/composables/useListRequest"
interface Props {
open: boolean
registeredUserIds?: number[] // ID
registeredUserIds?: number[]
}
interface Emits {
(e: "update:open", value: boolean): void
(e: "confirm", students: Student[]): void
(e: "confirm", students: RegistrationCandidateUser[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = ref(false)
const submitLoading = ref(false)
const selectedKeys = ref<number[]>([])
const classOptions = ref<Class[]>([])
//
const searchParams = reactive<{
username?: string
nickname?: string
classId?: number
}>({})
//
const {
loading,
dataSource,
@ -154,116 +131,75 @@ const {
handleTableChange,
search,
fetchList,
} = useListRequest<Student, { username?: string; nickname?: string; classId?: number }>({
requestFn: studentsApi.getList,
} = useListRequest<RegistrationCandidateUser, { username?: string; nickname?: string }>({
requestFn: async (params) => {
const kw = [params.username, params.nickname].filter(Boolean).join(" ").trim()
return registrationsApi.getCandidateUsers({
roleCode: "student",
keyword: kw || undefined,
page: params.page,
pageSize: params.pageSize,
})
},
defaultSearchParams: {},
defaultPageSize: 10,
errorMessage: "获取学生列表失败",
immediate: false,
})
//
const columns: TableColumnsType = [
{
title: "姓名",
key: "name",
width: 120,
},
{
title: "性别",
key: "gender",
width: 80,
align: "center",
},
{
title: "账号",
key: "account",
width: 120,
},
{
title: "联系方式",
key: "contact",
width: 120,
},
{
title: "机构信息",
key: "organization",
width: 200,
},
{ title: "姓名", key: "name", width: 120 },
{ title: "性别", key: "gender", width: 80, align: "center" },
{ title: "账号", key: "account", width: 120 },
{ title: "联系方式", key: "contact", width: 120 },
{ title: "角色", key: "organization", width: 200 },
]
// open
watch(
() => props.open,
(newVal) => {
visible.value = newVal
if (newVal) {
//
selectedKeys.value = []
searchParams.username = ""
searchParams.nickname = ""
searchParams.classId = undefined
fetchClasses()
fetchList()
}
},
{ immediate: true }
)
// visible
watch(visible, (newVal) => {
emit("update:open", newVal)
})
//
const handleSearch = () => {
search({
username: searchParams.username || undefined,
nickname: searchParams.nickname || undefined,
classId: searchParams.classId || undefined,
})
}
//
const handleSelectionChange = (
selectedRowKeys: number[],
selectedRows: Student[]
_selectedRows: RegistrationCandidateUser[]
) => {
selectedKeys.value = selectedRowKeys
}
//
const getCheckboxProps = (record: Student) => {
const isRegistered = props.registeredUserIds?.includes(record.userId) ?? false
return {
disabled: isRegistered,
}
const getCheckboxProps = (record: RegistrationCandidateUser) => {
const isRegistered = props.registeredUserIds?.includes(record.id) ?? false
return { disabled: isRegistered }
}
//
const fetchClasses = async () => {
try {
const response = await classesApi.getList({
page: 1,
pageSize: 100,
type: 1,
})
classOptions.value = response.list
} catch (error) {
console.error("获取班级列表失败:", error)
}
}
//
const handleSubmit = () => {
if (selectedKeys.value.length === 0) {
message.warning("请至少选择一个参赛人")
return
}
//
const selectedStudents = dataSource.value.filter((student) =>
selectedKeys.value.includes(student.id)
const selectedStudents = dataSource.value.filter((row) =>
selectedKeys.value.includes(row.id)
)
emit("confirm", selectedStudents)
@ -271,18 +207,12 @@ const handleSubmit = () => {
selectedKeys.value = []
}
//
const handleCancel = () => {
visible.value = false
selectedKeys.value = []
searchParams.username = ""
searchParams.nickname = ""
searchParams.classId = undefined
}
onMounted(() => {
fetchClasses()
})
</script>
<style scoped lang="scss">
@ -338,4 +268,3 @@ onMounted(() => {
}
}
</style>

View File

@ -13,7 +13,6 @@
</div>
</template>
<!-- 搜索/筛选区域 -->
<div class="search-section">
<div class="search-item">
<span class="search-label">姓名</span>
@ -30,9 +29,9 @@
</a-input>
</div>
<div class="search-item">
<span class="search-label"></span>
<span class="search-label"></span>
<a-input
v-model:value="searchParams.employeeNo"
v-model:value="searchParams.username"
placeholder="请输入"
allow-clear
style="width: 200px"
@ -45,7 +44,6 @@
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
@ -63,22 +61,16 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.user?.nickname || "-" }}
{{ record.nickname || "-" }}
</template>
<template v-else-if="column.key === 'username'">
{{ record.user?.username || "-" }}
</template>
<template v-else-if="column.key === 'employeeNo'">
{{ record.employeeNo || "-" }}
</template>
<template v-else-if="column.key === 'department'">
{{ record.department?.name || "-" }}
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.phone || record.user?.phone || "-" }}
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'subject'">
{{ record.subject || "-" }}
<template v-else-if="column.key === 'roles'">
{{ record.roleNames?.join("、") || "-" }}
</template>
</template>
</a-table>
@ -87,7 +79,7 @@
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
<a-button type="primary" @click="handleSubmit">
确定
</a-button>
</a-space>
@ -96,37 +88,37 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from "vue"
import { ref, reactive, watch } from "vue"
import { message } from "ant-design-vue"
import { SearchOutlined } from "@ant-design/icons-vue"
import type { TableColumnsType } from "ant-design-vue"
import { teachersApi, type Teacher } from "@/api/teachers"
import {
registrationsApi,
type RegistrationCandidateUser,
} from "@/api/contests"
import { useListRequest } from "@/composables/useListRequest"
interface Props {
open: boolean
selectedTeacherUserIds?: number[] // ID
selectedTeacherUserIds?: number[]
}
interface Emits {
(e: "update:open", value: boolean): void
(e: "confirm", teachers: Teacher[]): void
(e: "confirm", teachers: RegistrationCandidateUser[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = ref(false)
const submitLoading = ref(false)
const selectedKeys = ref<number[]>([])
//
const searchParams = reactive<{
nickname?: string
employeeNo?: string
username?: string
}>({})
//
const {
loading,
dataSource,
@ -134,18 +126,15 @@ const {
handleTableChange,
search,
fetchList,
} = useListRequest<Teacher, { nickname?: string; employeeNo?: string }>({
} = useListRequest<RegistrationCandidateUser, { nickname?: string; username?: string }>({
requestFn: async (params) => {
// API
const apiParams: any = {
page: params.page || 1,
pageSize: params.pageSize || 10,
}
// nicknameusernickname
// APInickname
// 使
return teachersApi.getList(apiParams)
const kw = [params.nickname, params.username].filter(Boolean).join(" ").trim()
return registrationsApi.getCandidateUsers({
roleCode: "teacher",
keyword: kw || undefined,
page: params.page,
pageSize: params.pageSize,
})
},
defaultSearchParams: {},
defaultPageSize: 10,
@ -153,97 +142,58 @@ const {
immediate: false,
})
//
const columns: TableColumnsType = [
{
title: "姓名",
key: "name",
width: 120,
},
{
title: "账号",
key: "username",
width: 120,
},
{
title: "工号",
key: "employeeNo",
width: 120,
},
{
title: "部门",
key: "department",
width: 150,
},
{
title: "联系电话",
key: "phone",
width: 120,
},
{
title: "任教科目",
key: "subject",
width: 120,
},
{ title: "姓名", key: "name", width: 120 },
{ title: "账号", key: "username", width: 120 },
{ title: "联系电话", key: "phone", width: 120 },
{ title: "角色", key: "roles", width: 160 },
]
// open
watch(
() => props.open,
(newVal) => {
visible.value = newVal
if (newVal) {
//
selectedKeys.value = []
searchParams.nickname = ""
searchParams.employeeNo = ""
searchParams.username = ""
fetchList()
}
},
{ immediate: true }
)
// visible
watch(visible, (newVal) => {
emit("update:open", newVal)
})
//
const handleSearch = () => {
// APInicknameemployeeNo
//
search({
nickname: searchParams.nickname || undefined,
employeeNo: searchParams.employeeNo || undefined,
username: searchParams.username || undefined,
})
}
//
const handleSelectionChange = (
selectedRowKeys: number[],
selectedRows: Teacher[]
_selectedRows: RegistrationCandidateUser[]
) => {
selectedKeys.value = selectedRowKeys
}
//
const getCheckboxProps = (record: Teacher) => {
const isSelected = props.selectedTeacherUserIds?.includes(record.userId) ?? false
return {
disabled: isSelected,
}
const getCheckboxProps = (record: RegistrationCandidateUser) => {
const isSelected = props.selectedTeacherUserIds?.includes(record.id) ?? false
return { disabled: isSelected }
}
//
const handleSubmit = () => {
if (selectedKeys.value.length === 0) {
message.warning("请至少选择一个指导老师")
return
}
//
const selectedTeachers = dataSource.value.filter((teacher) =>
selectedKeys.value.includes(teacher.id)
const selectedTeachers = dataSource.value.filter((row) =>
selectedKeys.value.includes(row.id)
)
emit("confirm", selectedTeachers)
@ -251,12 +201,11 @@ const handleSubmit = () => {
selectedKeys.value = []
}
//
const handleCancel = () => {
visible.value = false
selectedKeys.value = []
searchParams.nickname = ""
searchParams.employeeNo = ""
searchParams.username = ""
}
</script>
@ -313,4 +262,3 @@ const handleCancel = () => {
}
}
</style>

View File

@ -1,601 +0,0 @@
<template>
<div class="homework-page">
<a-card class="mb-4">
<template #title>作业管理</template>
<template #extra>
<a-button
v-permission="'homework:create'"
type="primary"
@click="handleAdd"
>
<template #icon><PlusOutlined /></template>
创建作业
</a-button>
</template>
</a-card>
<!-- 搜索表单 -->
<a-form
:model="searchParams"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="作业名称">
<a-input
v-model:value="searchParams.name"
placeholder="请输入作业名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="作业状态">
<a-select
v-model:value="searchParams.status"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option value="published">已发布</a-select-option>
<a-select-option value="unpublished">未发布</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="提交时间">
<a-range-picker
v-model:value="submitTimeRange"
:placeholder="['开始时间', '结束时间']"
style="width: 280px"
@change="handleTimeRangeChange"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.name }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'published' ? 'success' : 'default'">
{{ record.status === "published" ? "已发布" : "未发布" }}
</a-tag>
</template>
<template v-else-if="column.key === 'publishScope'">
<span v-if="record.publishScopeNames && record.publishScopeNames.length > 0">
{{ record.publishScopeNames.join("、") }}
</span>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'submitTime'">
<div>
{{ formatDateTime(record.submitStartTime) }} ~
{{ formatDateTime(record.submitEndTime) }}
</div>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<!-- 发布/取消发布 -->
<a-button
v-permission="'homework:update'"
type="link"
size="small"
@click="handlePublish(record)"
>
{{ record.status === "published" ? "取消发布" : "发布" }}
</a-button>
<!-- 提交记录 -->
<a-button
v-permission="'homework:read'"
type="link"
size="small"
@click="handleViewSubmissions(record.id)"
>
提交记录
</a-button>
<!-- 编辑 -->
<a-button
v-permission="'homework:update'"
type="link"
size="small"
:disabled="record.status === 'published'"
@click="handleEdit(record)"
>
编辑
</a-button>
<!-- 删除 -->
<a-popconfirm
v-permission="'homework:delete'"
title="确定要删除这个作业吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 创建/编辑作业抽屉 -->
<a-drawer
v-model:open="drawerVisible"
:title="editingId ? '编辑作业' : '创建作业'"
placement="right"
width="600px"
:footer-style="{ textAlign: 'right' }"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form-item label="作业名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入作业名称" />
</a-form-item>
<a-form-item label="提交时间" name="submitTimeRange">
<a-range-picker
v-model:value="formData.submitTimeRange"
show-time
:placeholder="['开始时间', '结束时间']"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="评审规则" name="reviewRuleId">
<a-select
v-model:value="formData.reviewRuleId"
placeholder="请选择评审规则"
allow-clear
:options="reviewRuleOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</a-form-item>
<a-form-item label="作业内容" name="content">
<a-textarea
v-model:value="formData.content"
placeholder="请输入作业内容"
:rows="6"
/>
</a-form-item>
<a-form-item label="附件">
<a-upload
v-model:file-list="formData.fileList"
:before-upload="beforeUpload"
:custom-request="customUpload"
@remove="handleRemoveFile"
>
<a-button>
<template #icon><UploadOutlined /></template>
上传附件
</a-button>
</a-upload>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="drawerVisible = false">取消</a-button>
<a-button type="primary" :loading="submitting" @click="handleSave">
保存
</a-button>
<a-button
v-if="!editingId"
type="primary"
:loading="submitting"
@click="handleSaveAndPublish"
>
保存并发布
</a-button>
</a-space>
</template>
</a-drawer>
<!-- 发布时选择公开范围 -->
<a-modal
v-model:open="publishModalVisible"
title="选择公开范围"
@ok="confirmPublish"
@cancel="publishModalVisible = false"
>
<a-form layout="vertical">
<a-form-item label="公开范围" required>
<a-tree-select
v-model:value="publishScope"
:tree-data="classTreeData"
tree-checkable
:show-checked-strategy="TreeSelect.SHOW_CHILD"
placeholder="请选择班级"
style="width: 100%"
:field-names="{ label: 'name', value: 'id', children: 'children' }"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from "vue"
import { useRouter, useRoute } from "vue-router"
import { message, TreeSelect } from "ant-design-vue"
import type { FormInstance, UploadProps } from "ant-design-vue"
import {
PlusOutlined,
SearchOutlined,
ReloadOutlined,
UploadOutlined,
} from "@ant-design/icons-vue"
import { useListRequest } from "@/composables/useListRequest"
import {
homeworksApi,
reviewRulesApi,
submissionsApi,
type Homework,
type QueryHomeworkParams,
type HomeworkReviewRule,
type HomeworkAttachment,
type ClassTreeNode,
} from "@/api/homework"
import { uploadApi } from "@/api/upload"
import dayjs, { Dayjs } from "dayjs"
const router = useRouter()
const route = useRoute()
const tenantCode = route.params.tenantCode as string
// 使
const {
loading,
dataSource,
pagination,
searchParams,
fetchList,
resetSearch,
search,
handleTableChange,
} = useListRequest<Homework, QueryHomeworkParams>({
requestFn: homeworksApi.getList,
defaultSearchParams: {} as QueryHomeworkParams,
defaultPageSize: 10,
errorMessage: "获取作业列表失败",
})
//
const submitTimeRange = ref<[Dayjs, Dayjs] | null>(null)
//
const columns = [
{
title: "序号",
key: "index",
width: 60,
customRender: ({ index }: { index: number }) => index + 1,
},
{
title: "作业名称",
key: "name",
dataIndex: "name",
width: 200,
},
{
title: "作业状态",
key: "status",
dataIndex: "status",
width: 100,
},
{
title: "公开范围",
key: "publishScope",
width: 200,
},
{
title: "提交时间",
key: "submitTime",
width: 350,
},
{
title: "操作",
key: "action",
width: 280,
fixed: "right" as const,
},
]
//
const formatDateTime = (dateStr?: string) => {
if (!dateStr) return "-"
return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss")
}
//
const handleTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
if (dates) {
searchParams.submitStartTime = dates[0].format("YYYY-MM-DD")
searchParams.submitEndTime = dates[1].format("YYYY-MM-DD")
} else {
searchParams.submitStartTime = undefined
searchParams.submitEndTime = undefined
}
}
//
const handleSearch = () => {
search()
}
//
const handleReset = () => {
submitTimeRange.value = null
resetSearch()
}
//
const drawerVisible = ref(false)
const editingId = ref<number | null>(null)
const submitting = ref(false)
const formRef = ref<FormInstance>()
const reviewRuleOptions = ref<HomeworkReviewRule[]>([])
const formData = reactive<{
name: string
content: string
submitTimeRange: [Dayjs, Dayjs] | null
reviewRuleId: number | undefined
fileList: any[]
}>({
name: "",
content: "",
submitTimeRange: null,
reviewRuleId: undefined,
fileList: [],
})
const formRules = {
name: [{ required: true, message: "请输入作业名称" }],
submitTimeRange: [{ required: true, message: "请选择提交时间" }],
}
//
const loadReviewRules = async () => {
try {
const rules = await reviewRulesApi.getForSelect()
reviewRuleOptions.value = rules
} catch (error) {
console.error("加载评审规则失败", error)
}
}
//
const handleAdd = () => {
editingId.value = null
formData.name = ""
formData.content = ""
formData.submitTimeRange = null
formData.reviewRuleId = undefined
formData.fileList = []
drawerVisible.value = true
loadReviewRules()
}
//
const handleEdit = async (record: Homework) => {
editingId.value = record.id
formData.name = record.name
formData.content = record.content || ""
formData.submitTimeRange = [
dayjs(record.submitStartTime),
dayjs(record.submitEndTime),
]
formData.reviewRuleId = record.reviewRuleId
// attachments JSON
let attachments: HomeworkAttachment[] = []
if (record.attachments) {
if (typeof record.attachments === "string") {
try {
attachments = JSON.parse(record.attachments)
} catch (e) {
console.error("解析 attachments 失败", e)
attachments = []
}
} else if (Array.isArray(record.attachments)) {
attachments = record.attachments
}
}
formData.fileList = attachments.map((att, index) => ({
uid: `-${index}`,
name: att.fileName,
status: "done",
url: att.fileUrl,
}))
drawerVisible.value = true
loadReviewRules()
}
//
const beforeUpload: UploadProps["beforeUpload"] = () => {
return false
}
const customUpload = async ({ file, onSuccess, onError }: any) => {
try {
const formData = new FormData()
formData.append("file", file)
const result = await uploadApi.upload(formData)
file.url = result.url
onSuccess(result)
} catch (error) {
onError(error)
}
}
const handleRemoveFile = () => {
return true
}
//
const handleSave = async () => {
try {
await formRef.value?.validate()
submitting.value = true
const attachments = formData.fileList.map((file) => ({
fileName: file.name,
fileUrl: file.url || file.response?.url,
size: file.size?.toString(),
}))
const submitData = {
name: formData.name,
content: formData.content,
submitStartTime: formData.submitTimeRange?.[0]?.toISOString(),
submitEndTime: formData.submitTimeRange?.[1]?.toISOString(),
reviewRuleId: formData.reviewRuleId,
attachments,
}
if (editingId.value) {
await homeworksApi.update(editingId.value, submitData)
message.success("更新成功")
} else {
await homeworksApi.create(submitData as any)
message.success("创建成功")
}
drawerVisible.value = false
fetchList()
} catch (error: any) {
if (error?.errorFields) return
message.error(error?.response?.data?.message || "操作失败")
} finally {
submitting.value = false
}
}
//
const handleSaveAndPublish = async () => {
try {
await formRef.value?.validate()
submitting.value = true
const attachments = formData.fileList.map((file) => ({
fileName: file.name,
fileUrl: file.url || file.response?.url,
size: file.size?.toString(),
}))
const submitData = {
name: formData.name,
content: formData.content,
submitStartTime: formData.submitTimeRange?.[0]?.toISOString(),
submitEndTime: formData.submitTimeRange?.[1]?.toISOString(),
reviewRuleId: formData.reviewRuleId,
attachments,
}
const created = await homeworksApi.create(submitData as any)
currentPublishingId.value = created.id
drawerVisible.value = false
publishModalVisible.value = true
loadClassTree()
} catch (error: any) {
if (error?.errorFields) return
message.error(error?.response?.data?.message || "操作失败")
} finally {
submitting.value = false
}
}
//
const publishModalVisible = ref(false)
const publishScope = ref<number[]>([])
const currentPublishingId = ref<number | null>(null)
const classTreeData = ref<ClassTreeNode[]>([])
const loadClassTree = async () => {
try {
const tree = await submissionsApi.getClassTree()
classTreeData.value = tree
} catch (error) {
console.error("加载班级树失败", error)
}
}
const handlePublish = async (record: Homework) => {
if (record.status === "published") {
//
try {
await homeworksApi.unpublish(record.id)
message.success("取消发布成功")
fetchList()
} catch (error: any) {
message.error(error?.response?.data?.message || "操作失败")
}
} else {
//
currentPublishingId.value = record.id
publishScope.value = []
publishModalVisible.value = true
loadClassTree()
}
}
const confirmPublish = async () => {
if (!publishScope.value || publishScope.value.length === 0) {
message.warning("请选择公开范围")
return
}
try {
await homeworksApi.publish(currentPublishingId.value!, publishScope.value)
message.success("发布成功")
publishModalVisible.value = false
fetchList()
} catch (error: any) {
message.error(error?.response?.data?.message || "发布失败")
}
}
//
const handleViewSubmissions = (id: number) => {
router.push(`/${tenantCode}/homework/submissions?homeworkId=${id}`)
}
//
const handleDelete = async (id: number) => {
try {
await homeworksApi.delete(id)
message.success("删除成功")
fetchList()
} catch (error: any) {
message.error(error?.response?.data?.message || "删除失败")
}
}
onMounted(() => {
fetchList()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
</style>

View File

@ -1,399 +0,0 @@
<template>
<div class="review-rules-page">
<a-card class="mb-4">
<template #title>评审规则</template>
<template #extra>
<a-button
v-permission="'homework:update'"
type="primary"
@click="handleAdd"
>
<template #icon><PlusOutlined /></template>
新增规则
</a-button>
</template>
</a-card>
<!-- 搜索表单 -->
<a-form
:model="searchParams"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="规则名称">
<a-input
v-model:value="searchName"
placeholder="请输入规则名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="dataList"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.name }}
</template>
<template v-else-if="column.key === 'description'">
{{ record.description || "-" }}
</template>
<template v-else-if="column.key === 'homeworks'">
<template v-if="record.homeworks?.length">
<a-tag v-for="hw in record.homeworks" :key="hw.id">
{{ hw.name }}
</a-tag>
</template>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button
v-permission="'homework:update'"
type="link"
size="small"
@click="handleEdit(record)"
>
编辑
</a-button>
<a-popconfirm
v-permission="'homework:update'"
title="确定要删除这个规则吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 新增/编辑抽屉 -->
<a-drawer
v-model:open="drawerVisible"
:title="editingId ? '编辑规则' : '新增规则'"
placement="right"
width="600px"
>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="规则名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入规则名称" />
</a-form-item>
<a-form-item label="规则描述" name="description">
<a-textarea
v-model:value="formData.description"
placeholder="请输入规则描述"
:rows="3"
/>
</a-form-item>
<a-form-item label="评分标准" name="criteria">
<div class="criteria-list">
<div
v-for="(criterion, index) in formData.criteria"
:key="index"
class="criterion-item"
>
<a-input
v-model:value="criterion.name"
placeholder="维度名称"
style="width: 150px"
/>
<a-input-number
v-model:value="criterion.maxScore"
placeholder="满分"
:min="1"
style="width: 100px"
/>
<a-input
v-model:value="criterion.description"
placeholder="描述(可选)"
style="flex: 1"
/>
<a-button
type="text"
danger
@click="removeCriterion(index)"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</div>
<a-button type="dashed" block @click="addCriterion">
<template #icon><PlusOutlined /></template>
添加评分维度
</a-button>
</div>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="drawerVisible = false">取消</a-button>
<a-button type="primary" :loading="submitting" @click="handleSave">
保存
</a-button>
</a-space>
</template>
</a-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, reactive } from "vue";
import { message } from "ant-design-vue";
import type { FormInstance } from "ant-design-vue";
import {
PlusOutlined,
SearchOutlined,
DeleteOutlined,
} from "@ant-design/icons-vue";
import {
reviewRulesApi,
type HomeworkReviewRule,
type ReviewCriterion,
} from "@/api/homework";
//
const loading = ref(false);
const dataList = ref<HomeworkReviewRule[]>([]);
const total = ref(0);
const searchName = ref("");
const searchParams = reactive({
page: 1,
pageSize: 10,
});
//
const pagination = computed(() => ({
current: searchParams.page,
pageSize: searchParams.pageSize,
total: total.value,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t: number) => `${t} 条记录`,
}));
//
const selectedRowKeys = ref<number[]>([]);
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: number[]) => {
selectedRowKeys.value = keys;
},
}));
//
const columns = [
{
title: "序号",
key: "index",
width: 60,
customRender: ({ index }: { index: number }) => index + 1,
},
{
title: "规则名称",
key: "name",
dataIndex: "name",
width: 200,
},
{
title: "规则描述",
key: "description",
width: 300,
},
{
title: "关联作业",
key: "homeworks",
width: 250,
},
{
title: "操作",
key: "action",
width: 150,
fixed: "right" as const,
},
];
//
const fetchList = async () => {
loading.value = true;
try {
const res = await reviewRulesApi.getList({
name: searchName.value || undefined,
page: searchParams.page,
pageSize: searchParams.pageSize,
});
dataList.value = res.list;
total.value = res.total;
} catch (error: any) {
message.error(error?.response?.data?.message || "获取列表失败");
} finally {
loading.value = false;
}
};
//
const handleTableChange = (pag: any) => {
searchParams.page = pag.current;
searchParams.pageSize = pag.pageSize;
fetchList();
};
//
const handleSearch = () => {
searchParams.page = 1;
fetchList();
};
//
const handleReset = () => {
searchName.value = "";
searchParams.page = 1;
fetchList();
};
//
const drawerVisible = ref(false);
const editingId = ref<number | null>(null);
const submitting = ref(false);
const formRef = ref<FormInstance>();
const formData = reactive<{
name: string;
description: string;
criteria: ReviewCriterion[];
}>({
name: "",
description: "",
criteria: [],
});
const formRules = {
name: [{ required: true, message: "请输入规则名称" }],
criteria: [
{
validator: async () => {
if (formData.criteria.length === 0) {
return Promise.reject("请至少添加一个评分维度");
}
for (const c of formData.criteria) {
if (!c.name || !c.maxScore) {
return Promise.reject("请完善评分维度信息");
}
}
return Promise.resolve();
},
},
],
};
//
const handleAdd = () => {
editingId.value = null;
formData.name = "";
formData.description = "";
formData.criteria = [{ name: "", maxScore: 100, description: "" }];
drawerVisible.value = true;
};
//
const handleEdit = (record: HomeworkReviewRule) => {
editingId.value = record.id;
formData.name = record.name;
formData.description = record.description || "";
const criteria = record.criteria;
formData.criteria =
typeof criteria === "string" ? JSON.parse(criteria) : criteria || [];
drawerVisible.value = true;
};
//
const addCriterion = () => {
formData.criteria.push({ name: "", maxScore: 100, description: "" });
};
//
const removeCriterion = (index: number) => {
formData.criteria.splice(index, 1);
};
//
const handleSave = async () => {
try {
await formRef.value?.validate();
submitting.value = true;
const submitData = {
name: formData.name,
description: formData.description,
criteria: formData.criteria,
};
if (editingId.value) {
await reviewRulesApi.update(editingId.value, submitData);
message.success("更新成功");
} else {
await reviewRulesApi.create(submitData);
message.success("创建成功");
}
drawerVisible.value = false;
fetchList();
} catch (error: any) {
if (error?.errorFields) return;
message.error(error?.response?.data?.message || "操作失败");
} finally {
submitting.value = false;
}
};
//
const handleDelete = async (id: number) => {
try {
await reviewRulesApi.delete(id);
message.success("删除成功");
fetchList();
} catch (error: any) {
message.error(error?.response?.data?.message || "删除失败");
}
};
onMounted(() => {
fetchList();
});
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.criteria-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.criterion-item {
display: flex;
gap: 8px;
align-items: center;
}
</style>

Some files were not shown because too many files have changed in this diff Show More