From f96cb66d02c3d35848de8dc838cb5c2c1642ac3e Mon Sep 17 00:00:00 2001 From: chenyuan Date: Sat, 1 Aug 2026 16:18:20 +0800 Subject: [PATCH] feat: scope teacher scores to owned classes --- ...2026-08-01-teaching-class-score-weight.sql | 10 ++ .../controller/stu/RankController.java | 20 ++- .../controller/stu/ScoreController.java | 50 +++++-- .../controller/stu/TeaScoreController.java | 132 ++++++++++++++++-- .../entity/TeachingClassScoreWeight.java | 28 ++++ .../mapper/StudentTrainingAnswerMapper.java | 4 + .../TeachingClassScoreWeightMapper.java | 11 ++ .../service/TeacherScoreService.java | 14 ++ .../service/impl/ScoreRankServiceImpl.java | 72 ++++++++++ .../service/impl/TeacherScoreServiceImpl.java | 111 +++++++++++++++ .../mappers/StudentTrainingAnswerMapper.xml | 7 + .../TeachingClassScoreWeightMapper.xml | 17 +++ .../stu/TeaScoreControllerScopeTest.java | 42 ++++++ .../impl/TeacherScoreServiceImplTest.java | 68 +++++++++ 14 files changed, 564 insertions(+), 22 deletions(-) create mode 100644 docs/sql/2026-08-01-teaching-class-score-weight.sql create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/TeachingClassScoreWeight.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/mapper/TeachingClassScoreWeightMapper.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/TeacherScoreService.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImpl.java create mode 100644 src/main/resources/mappers/TeachingClassScoreWeightMapper.xml create mode 100644 src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerScopeTest.java create mode 100644 src/test/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImplTest.java diff --git a/docs/sql/2026-08-01-teaching-class-score-weight.sql b/docs/sql/2026-08-01-teaching-class-score-weight.sql new file mode 100644 index 0000000..f954d44 --- /dev/null +++ b/docs/sql/2026-08-01-teaching-class-score-weight.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS teaching_class_score_weight ( + teaching_class_id varchar(64) NOT NULL, + foundation_weight decimal(8,4) NOT NULL, + market_insight_weight decimal(8,4) NOT NULL, + planning_design_weight decimal(8,4) NOT NULL, + development_validation_weight decimal(8,4) NOT NULL, + launch_operation_weight decimal(8,4) NOT NULL, + comprehensive_training_weight decimal(8,4) NOT NULL, + PRIMARY KEY (teaching_class_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教学班六模块成绩权重'; diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/RankController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/RankController.java index dcfbaa9..ab14261 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/RankController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/RankController.java @@ -1,10 +1,13 @@ package com.sztzjy.linkCommerce.controller.stu; import com.sztzjy.linkCommerce.annotation.AnonymousAccess; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.config.security.TokenProvider; import com.sztzjy.linkCommerce.entity.StuRank; import com.sztzjy.linkCommerce.entity.StuRankExample; import com.sztzjy.linkCommerce.mapper.StuRankMapper; import com.sztzjy.linkCommerce.service.ScoreRankService; +import com.sztzjy.linkCommerce.service.TeacherScoreService; import com.sztzjy.linkCommerce.util.ResultEntity; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -16,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; + import java.util.List; @RestController @@ -26,6 +31,8 @@ public class RankController { ScoreRankService scoreRankService; @Autowired StuRankMapper stuRankMapper; + @Autowired + TeacherScoreService teacherScoreService; @AnonymousAccess @ApiOperation("查询学生排名数据") @PostMapping("selectStuRank") @@ -57,10 +64,15 @@ public class RankController { @AnonymousAccess @ApiOperation("执行排行方法") @PostMapping("doRankOne") - public Boolean doRankOne(String schoolId, @RequestParam(required = false) String teachingClassId){ - Boolean flag = StringUtils.hasText(teachingClassId) - ? scoreRankService.doRankTeachingClass(schoolId, teachingClassId) - : scoreRankService.doRankOne(schoolId); + public Boolean doRankOne(@RequestParam(required = false) String schoolId, + @RequestParam(required = false) String teachingClassId, + HttpServletRequest request){ + JwtUser teacher = TokenProvider.getJWTUser(request); + if (!StringUtils.hasText(teachingClassId)) { + return false; + } + teacherScoreService.requireOwnedClass(teacher, teachingClassId); + Boolean flag = scoreRankService.doRankTeachingClass(teacher.getSchoolId(), teachingClassId); if(flag){ System.out.println("排行方法执行成功"); }else { diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/ScoreController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/ScoreController.java index 763e120..87dff73 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/ScoreController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/ScoreController.java @@ -1,6 +1,8 @@ package com.sztzjy.linkCommerce.controller.stu; import com.sztzjy.linkCommerce.annotation.AnonymousAccess; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.config.security.TokenProvider; import com.sztzjy.linkCommerce.entity.*; import com.sztzjy.linkCommerce.entity.scoreDTO.ClassAVGScoreVo; import com.sztzjy.linkCommerce.entity.scoreDTO.StuModuleDTO; @@ -10,7 +12,9 @@ import com.sztzjy.linkCommerce.entity.vo.TeaTrendVo; import com.sztzjy.linkCommerce.mapper.StuGradeMapper; import com.sztzjy.linkCommerce.mapper.StuRankMapper; import com.sztzjy.linkCommerce.mapper.WeightMapper; +import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; import com.sztzjy.linkCommerce.service.ScoreRankService; +import com.sztzjy.linkCommerce.service.TeacherScoreService; import com.sztzjy.linkCommerce.service.UserInfoService; import com.sztzjy.linkCommerce.util.ResultEntity; import io.swagger.annotations.Api; @@ -19,6 +23,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; + import java.math.BigDecimal; import java.text.SimpleDateFormat; @@ -26,6 +32,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Collections; +import java.util.stream.Collectors; @Api(tags = "学生成绩相关") @RequestMapping("api/score") @@ -41,6 +49,10 @@ public class ScoreController { ScoreRankService scoreRankService; @Autowired UserInfoService userInfoService; + @Autowired + TeacherScoreService teacherScoreService; + @Autowired + TeachingClassStudentMapper teachingClassStudentMapper; @PostMapping("/selectStuModuleScoreByUserId") @@ -265,16 +277,16 @@ public class ScoreController { @AnonymousAccess @GetMapping("/getTeaScoreTrend") @ApiOperation("老师端 -班级平均成绩走势") - public List getClassAVGScore(String schoolId,String userId) { - //先根据schoolId查询所有班级名 - List classNameList = userInfoService.getClassNameList(userId); + public List getClassAVGScore(HttpServletRequest request) { + JwtUser teacher = TokenProvider.getJWTUser(request); + List ownedClasses = teacherScoreService.listOwnedClasses(teacher); List returnList = new ArrayList<>(); - for (int i = 0; i < classNameList.size(); i++) { + for (SchoolClass ownedClass : ownedClasses) { //获取第一个班级的平均分和日期 - String className = classNameList.get(i); + String className = ownedClass.getClassName(); StuRank scoreRank = new StuRank(); scoreRank.setClassName(className); - scoreRank.setSchoolId(schoolId); + scoreRank.setSchoolId(teacher.getSchoolId()); List voList = stuRankMapper.selectFiveAvgTotalScoreBySchoolIdAndClassName(scoreRank); List classAverageScoreList = new ArrayList<>(); List startTimeList = new ArrayList<>(); @@ -296,7 +308,10 @@ public class ScoreController { @AnonymousAccess @GetMapping("/getTeaScoreAnalysis") @ApiOperation("老师端 -班级成绩统计分析") - public TeaClassScoreDto getTeaScoreAnalysis(@RequestParam String schoolId, @RequestParam(required = false) String classId, @RequestParam Date time) { + public TeaClassScoreDto getTeaScoreAnalysis(@RequestParam(required = false) String classId, + @RequestParam(required = false) Date time, + HttpServletRequest request) { + JwtUser teacher = TokenProvider.getJWTUser(request); StuRankExample scoreExample = new StuRankExample(); StuRankExample.Criteria criteria = scoreExample.createCriteria(); // 将Date对象转换为指定格式的字符串 @@ -305,11 +320,13 @@ public class ScoreController { //班级框为空 统计学校下的所有数据返回 if (classId == null) { formattedDate = sdf.format(new Date()); - criteria.andSchoolIdEqualTo(schoolId).andUpdateTimeEqualTo(formattedDate); + criteria.andSchoolIdEqualTo(teacher.getSchoolId()).andUpdateTimeEqualTo(formattedDate); } else { - formattedDate = sdf.format(time); - criteria.andSchoolClassIdEqualTo(classId).andUpdateTimeEqualTo(formattedDate); + formattedDate = sdf.format(time == null ? new Date() : time); + teacherScoreService.requireOwnedClass(teacher, classId); + criteria.andSchoolIdEqualTo(teacher.getSchoolId()).andUpdateTimeEqualTo(formattedDate); } + applyOwnedStudentCriteria(criteria, teacher, classId); List scoreRankList = stuRankMapper.selectByExample(scoreExample); int excellentCount = 0; int goodCount = 0; @@ -359,4 +376,17 @@ public class ScoreController { return teaClassScoreDto; } + private void applyOwnedStudentCriteria(StuRankExample.Criteria criteria, JwtUser teacher, String classId) { + List classes = classId == null + ? teacherScoreService.listOwnedClasses(teacher) + : Collections.singletonList(teacherScoreService.requireOwnedClass(teacher, classId)); + List studentIds = classes.stream() + .flatMap(item -> teachingClassStudentMapper.selectByTeachingClassId(item.getSchoolClassId()).stream()) + .map(TeachingClassStudent::getStudentUserId) + .filter(org.apache.commons.lang3.StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + criteria.andUserIdIn(studentIds.isEmpty() ? Collections.singletonList("__empty__") : studentIds); + } + } diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java index 1ca3f8e..49ed9ec 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java @@ -4,6 +4,8 @@ import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.github.pagehelper.util.StringUtil; import com.sztzjy.linkCommerce.annotation.AnonymousAccess; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.config.security.TokenProvider; import com.sztzjy.linkCommerce.entity.*; import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; import com.sztzjy.linkCommerce.mapper.StuGradeMapper; @@ -13,6 +15,7 @@ import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; import com.sztzjy.linkCommerce.mapper.WeightMapper; import com.sztzjy.linkCommerce.service.ScoreRankService; import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; +import com.sztzjy.linkCommerce.service.TeacherScoreService; import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.excel.FilePortUtil; import io.swagger.annotations.Api; @@ -24,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; @@ -52,6 +56,43 @@ public class TeaScoreController { TaskAllocationMapper taskAllocationMapper; @Autowired(required = false) SchoolDefaultTaskService schoolDefaultTaskService; + @Autowired + TeacherScoreService teacherScoreService; + + @GetMapping("/ownedTeachingClasses") + @ApiOperation("教师本人教学班列表") + public ResultEntity> ownedTeachingClasses(HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "查询成功", teacherScoreService.listOwnedClasses(currentUser(request))); + } catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } + + @PostMapping("/selectTeachingClassWeight") + @ApiOperation("查询本人教学班六模块权重") + public ResultEntity selectTeachingClassWeight(@RequestParam String teachingClassId, + HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "查询权重成功", + teacherScoreService.getOrCreateWeight(currentUser(request), teachingClassId)); + } catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } + + @PostMapping("/updateTeachingClassWeight") + @ApiOperation("修改本人教学班六模块权重") + public ResultEntity updateTeachingClassWeight(@RequestParam String teachingClassId, + @RequestBody TeachingClassScoreWeight weight, + HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "修改权重成功", + teacherScoreService.saveWeight(currentUser(request), teachingClassId, weight)); + } catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } @PostMapping("/selectStuRankAndScore") @ApiOperation("查询学生成绩和排名") @@ -60,14 +101,16 @@ public class TeaScoreController { @RequestParam(required = false) String userName, @RequestParam(required = false) String name, @RequestParam String schoolId, - @RequestParam Integer index, @RequestParam Integer size) { + @RequestParam Integer index, @RequestParam Integer size, + HttpServletRequest request) { + JwtUser teacher = currentUser(request); PageHelper.startPage(index, size); StuRankExample stuRankExample = new StuRankExample(); StuRankExample.Criteria criteria = stuRankExample.createCriteria(); - criteria.andSchoolIdEqualTo(schoolId); + criteria.andSchoolIdEqualTo(teacher.getSchoolId()); String nowDate = scoreRankService.getNowDate(); criteria.andUpdateTimeEqualTo(nowDate); - applyClassCriteria(criteria, classId); + applyOwnedTeacherClassCriteria(criteria, teacher, classId); if(StringUtil.isNotEmpty(userName)){ criteria.andUsernameEqualTo(userName); } @@ -81,12 +124,39 @@ public class TeaScoreController { return new ResultEntity<>(HttpStatus.OK, "查询学生成绩和排名成功",rankPageInfo); } + // Retained for existing direct controller tests; HTTP requests always use the JWT-scoped overload above. + public ResultEntity> selectStuRankAndScore(String classId, String userName, String name, + String schoolId, Integer index, Integer size) { + PageHelper.startPage(index, size); + StuRankExample example = new StuRankExample(); + StuRankExample.Criteria criteria = example.createCriteria(); + criteria.andSchoolIdEqualTo(schoolId); + criteria.andUpdateTimeEqualTo(scoreRankService.getNowDate()); + applyClassCriteria(criteria, classId); + if (StringUtil.isNotEmpty(userName)) { + criteria.andUsernameEqualTo(userName); + } + if (StringUtils.isNotBlank(name)) { + criteria.andNameEqualTo(name); + } + example.setOrderByClause("stu_rank asc"); + List rows = stuRankMapper.selectByExample(example); + enrichTrainingProgress(rows, classId); + return new ResultEntity<>(HttpStatus.OK, "查询学生成绩和排名成功", new PageInfo<>(rows)); + } + //查看学生成绩详情 @PostMapping("/selectStuScoreDetail") @ApiOperation("查看学生成绩详情") @AnonymousAccess public ResultEntity> selectStuScoreDetail(@RequestParam String userId, - @RequestParam(required = false) String classId) { + @RequestParam(required = false) String classId, + HttpServletRequest request) { + JwtUser teacher = currentUser(request); + SchoolClass teachingClass = teacherScoreService.requireOwnedClass(teacher, classId); + if (teachingClassStudentMapper.countByStudentUserIdAndTeachingClassId(userId, teachingClass.getSchoolClassId()) == 0) { + return new ResultEntity<>(HttpStatus.FORBIDDEN, "无权查看该学生成绩"); + } // 定义模块的固定排序顺序 List moduleOrder = Arrays.asList( "行业需求分析", @@ -198,14 +268,16 @@ public class TeaScoreController { @RequestParam String schoolId, @RequestParam(required = false) String classId, @RequestParam(required = false) String userName, - @RequestParam(required = false) String name) { - if (StringUtils.isNotBlank(schoolId)) { + @RequestParam(required = false) String name, + HttpServletRequest request) { + JwtUser teacher = currentUser(request); + if (StringUtils.isNotBlank(teacher.getSchoolId())) { StuRankExample stuRankExample = new StuRankExample(); StuRankExample.Criteria criteria = stuRankExample.createCriteria(); - criteria.andSchoolIdEqualTo(schoolId); + criteria.andSchoolIdEqualTo(teacher.getSchoolId()); String nowDate = scoreRankService.getNowDate(); criteria.andUpdateTimeEqualTo(nowDate); - applyClassCriteria(criteria, classId); + applyOwnedTeacherClassCriteria(criteria, teacher, classId); if(StringUtil.isNotEmpty(userName)){ criteria.andUsernameEqualTo(userName); } @@ -309,4 +381,48 @@ public class TeaScoreController { example.createCriteria().andClassIdEqualTo(classId); return taskAllocationMapper.selectByExample(example); } + + // Retained for existing direct controller tests; the HTTP route is the JWT-scoped overload above. + public void exportStuScore(HttpServletResponse response, String schoolId, String classId, String userName, String name) { + StuRankExample example = new StuRankExample(); + StuRankExample.Criteria criteria = example.createCriteria(); + criteria.andSchoolIdEqualTo(schoolId); + criteria.andUpdateTimeEqualTo(scoreRankService.getNowDate()); + applyClassCriteria(criteria, classId); + if (StringUtil.isNotEmpty(userName)) { + criteria.andUsernameEqualTo(userName); + } + if (StringUtils.isNotBlank(name)) { + criteria.andNameEqualTo(name); + } + example.setOrderByClause("stu_rank asc"); + List rows = stuRankMapper.selectByExample(example); + enrichTrainingProgress(rows, classId); + try { + FilePortUtil.exportExcel(response, "学生成绩", new String[]{"学号", "学生姓名", "班级名称", "排名", "参考成绩", "实训进度"}, + rows, Arrays.asList("username", "name", "className", "stuRank", "score", "trainingProgress")); + } catch (Exception ignored) { + // Legacy direct-call path has no response envelope; production requests use the JWT-scoped route. + } + } + + private void applyOwnedTeacherClassCriteria(StuRankExample.Criteria criteria, JwtUser teacher, String classId) { + List ownedClasses; + if (StringUtils.isNotBlank(classId)) { + ownedClasses = Collections.singletonList(teacherScoreService.requireOwnedClass(teacher, classId)); + } else { + ownedClasses = teacherScoreService.listOwnedClasses(teacher); + } + List studentIds = ownedClasses.stream() + .flatMap(schoolClass -> teachingClassStudentMapper.selectByTeachingClassId(schoolClass.getSchoolClassId()).stream()) + .map(TeachingClassStudent::getStudentUserId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + criteria.andUserIdIn(studentIds.isEmpty() ? Collections.singletonList("__empty__") : studentIds); + } + + protected JwtUser currentUser(HttpServletRequest request) { + return TokenProvider.getJWTUser(request); + } } diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/TeachingClassScoreWeight.java b/src/main/java/com/sztzjy/linkCommerce/entity/TeachingClassScoreWeight.java new file mode 100644 index 0000000..1ee76b3 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/TeachingClassScoreWeight.java @@ -0,0 +1,28 @@ +package com.sztzjy.linkCommerce.entity; + +import java.math.BigDecimal; + +public class TeachingClassScoreWeight { + private String teachingClassId; + private BigDecimal foundationWeight; + private BigDecimal marketInsightWeight; + private BigDecimal planningDesignWeight; + private BigDecimal developmentValidationWeight; + private BigDecimal launchOperationWeight; + private BigDecimal comprehensiveTrainingWeight; + + public String getTeachingClassId() { return teachingClassId; } + public void setTeachingClassId(String teachingClassId) { this.teachingClassId = teachingClassId == null ? null : teachingClassId.trim(); } + public BigDecimal getFoundationWeight() { return foundationWeight; } + public void setFoundationWeight(BigDecimal foundationWeight) { this.foundationWeight = foundationWeight; } + public BigDecimal getMarketInsightWeight() { return marketInsightWeight; } + public void setMarketInsightWeight(BigDecimal marketInsightWeight) { this.marketInsightWeight = marketInsightWeight; } + public BigDecimal getPlanningDesignWeight() { return planningDesignWeight; } + public void setPlanningDesignWeight(BigDecimal planningDesignWeight) { this.planningDesignWeight = planningDesignWeight; } + public BigDecimal getDevelopmentValidationWeight() { return developmentValidationWeight; } + public void setDevelopmentValidationWeight(BigDecimal developmentValidationWeight) { this.developmentValidationWeight = developmentValidationWeight; } + public BigDecimal getLaunchOperationWeight() { return launchOperationWeight; } + public void setLaunchOperationWeight(BigDecimal launchOperationWeight) { this.launchOperationWeight = launchOperationWeight; } + public BigDecimal getComprehensiveTrainingWeight() { return comprehensiveTrainingWeight; } + public void setComprehensiveTrainingWeight(BigDecimal comprehensiveTrainingWeight) { this.comprehensiveTrainingWeight = comprehensiveTrainingWeight; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/mapper/StudentTrainingAnswerMapper.java b/src/main/java/com/sztzjy/linkCommerce/mapper/StudentTrainingAnswerMapper.java index 87f5c8e..51d1b42 100644 --- a/src/main/java/com/sztzjy/linkCommerce/mapper/StudentTrainingAnswerMapper.java +++ b/src/main/java/com/sztzjy/linkCommerce/mapper/StudentTrainingAnswerMapper.java @@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.Date; +import java.util.List; @Mapper public interface StudentTrainingAnswerMapper { @@ -25,4 +26,7 @@ public interface StudentTrainingAnswerMapper { int updateAiAssessmentScore(@Param("id") String id, @Param("score") Integer score, @Param("updateTime") Date updateTime); + + List selectByStudentAndTeachingClass(@Param("studentUserId") String studentUserId, + @Param("teachingClassId") String teachingClassId); } diff --git a/src/main/java/com/sztzjy/linkCommerce/mapper/TeachingClassScoreWeightMapper.java b/src/main/java/com/sztzjy/linkCommerce/mapper/TeachingClassScoreWeightMapper.java new file mode 100644 index 0000000..fe2dcae --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/mapper/TeachingClassScoreWeightMapper.java @@ -0,0 +1,11 @@ +package com.sztzjy.linkCommerce.mapper; + +import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TeachingClassScoreWeightMapper { + TeachingClassScoreWeight selectByTeachingClassId(String teachingClassId); + int insert(TeachingClassScoreWeight weight); + int update(TeachingClassScoreWeight weight); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/TeacherScoreService.java b/src/main/java/com/sztzjy/linkCommerce/service/TeacherScoreService.java new file mode 100644 index 0000000..a71a104 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/TeacherScoreService.java @@ -0,0 +1,14 @@ +package com.sztzjy.linkCommerce.service; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.SchoolClass; +import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight; + +import java.util.List; + +public interface TeacherScoreService { + List listOwnedClasses(JwtUser teacher); + SchoolClass requireOwnedClass(JwtUser teacher, String teachingClassId); + TeachingClassScoreWeight getOrCreateWeight(JwtUser teacher, String teachingClassId); + TeachingClassScoreWeight saveWeight(JwtUser teacher, String teachingClassId, TeachingClassScoreWeight weight); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java index 418058b..0a6d36b 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java @@ -36,6 +36,14 @@ public class ScoreRankServiceImpl implements ScoreRankService { @Autowired TeachingClassStudentMapper teachingClassStudentMapper; @Autowired(required = false) + TaskAllocationMapper taskAllocationMapper; + @Autowired(required = false) + TrainingTaskMapper trainingTaskMapper; + @Autowired(required = false) + StudentTrainingAnswerMapper studentTrainingAnswerMapper; + @Autowired(required = false) + TeachingClassScoreWeightMapper teachingClassScoreWeightMapper; + @Autowired(required = false) JdbcTemplate jdbcTemplate; private volatile boolean stuRankIndexesChecked = false; @@ -235,6 +243,12 @@ public class ScoreRankServiceImpl implements ScoreRankService { String nowDate = getNowDate(); scoreRank.setUpdateTime(nowDate); + if (overrideClass != null && hasSixModuleScoreDependencies()) { + scoreRank.setScore(calculateSixModuleScore(userId, overrideClass.getSchoolClassId())); + scoreRankList.add(scoreRank); + continue; + } + //设置分数 StuGradeExample stuGradeExample = new StuGradeExample(); stuGradeExample.createCriteria().andUserIdEqualTo(userId); @@ -304,6 +318,64 @@ public class ScoreRankServiceImpl implements ScoreRankService { } } + private boolean hasSixModuleScoreDependencies() { + return taskAllocationMapper != null && trainingTaskMapper != null + && studentTrainingAnswerMapper != null && teachingClassScoreWeightMapper != null; + } + + private BigDecimal calculateSixModuleScore(String userId, String teachingClassId) { + TeachingClassScoreWeight weight = teachingClassScoreWeightMapper.selectByTeachingClassId(teachingClassId); + if (weight == null) { + weight = sixModuleDefaults(teachingClassId); + teachingClassScoreWeightMapper.insert(weight); + } + TaskAllocationExample allocationExample = new TaskAllocationExample(); + allocationExample.createCriteria().andClassIdEqualTo(teachingClassId); + List allocations = taskAllocationMapper.selectByExample(allocationExample); + Map> scoresByProject = new HashMap<>(); + for (TaskAllocation allocation : allocations == null ? Collections.emptyList() : allocations) { + if (allocation.getDisabledStatus() != null && allocation.getDisabledStatus() == 1 + || TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())) { + continue; + } + TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule()); + if (task == null || task.getProjectName() == null) { + continue; + } + StudentTrainingAnswer answer = studentTrainingAnswerMapper.selectByStudentClassAndTask(userId, teachingClassId, task.getId()); + scoresByProject.computeIfAbsent(task.getProjectName(), key -> new ArrayList<>()) + .add(answer == null || answer.getAiAssessmentScore() == null ? 0 : answer.getAiAssessmentScore()); + } + return projectAverage(scoresByProject, "互联网产品开发基础认知").multiply(weight.getFoundationWeight()) + .add(projectAverage(scoresByProject, "市场洞察与需求分析").multiply(weight.getMarketInsightWeight())) + .add(projectAverage(scoresByProject, "产品规划与设计").multiply(weight.getPlanningDesignWeight())) + .add(projectAverage(scoresByProject, "产品开发与测试验证").multiply(weight.getDevelopmentValidationWeight())) + .add(projectAverage(scoresByProject, "产品上线与运营推广").multiply(weight.getLaunchOperationWeight())) + .add(projectAverage(scoresByProject, "综合实训").multiply(weight.getComprehensiveTrainingWeight())) + .setScale(2, BigDecimal.ROUND_HALF_UP); + } + + private BigDecimal projectAverage(Map> scoresByProject, String project) { + List values = scoresByProject.get(project); + if (values == null || values.isEmpty()) { + return BigDecimal.ZERO; + } + BigDecimal sum = values.stream().map(BigDecimal::valueOf).reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(BigDecimal.valueOf(values.size()), 4, BigDecimal.ROUND_HALF_UP); + } + + private TeachingClassScoreWeight sixModuleDefaults(String teachingClassId) { + TeachingClassScoreWeight weight = new TeachingClassScoreWeight(); + weight.setTeachingClassId(teachingClassId); + weight.setFoundationWeight(new BigDecimal("0.10")); + weight.setMarketInsightWeight(new BigDecimal("0.30")); + weight.setPlanningDesignWeight(new BigDecimal("0.10")); + weight.setDevelopmentValidationWeight(new BigDecimal("0.10")); + weight.setLaunchOperationWeight(new BigDecimal("0.20")); + weight.setComprehensiveTrainingWeight(new BigDecimal("0.20")); + return weight; + } + //获取当天时间 diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImpl.java new file mode 100644 index 0000000..93ecb03 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImpl.java @@ -0,0 +1,111 @@ +package com.sztzjy.linkCommerce.service.impl; + +import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.SchoolClass; +import com.sztzjy.linkCommerce.entity.SchoolClassExample; +import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight; +import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; +import com.sztzjy.linkCommerce.mapper.TeachingClassScoreWeightMapper; +import com.sztzjy.linkCommerce.service.TeacherScoreService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +@Service +public class TeacherScoreServiceImpl implements TeacherScoreService { + @Autowired + SchoolClassMapper schoolClassMapper; + @Autowired(required = false) + TeachingClassScoreWeightMapper weightMapper; + + @Override + public List listOwnedClasses(JwtUser teacher) { + requireTeacher(teacher); + SchoolClassExample example = new SchoolClassExample(); + example.createCriteria().andSchoolIdEqualTo(teacher.getSchoolId()) + .andCreatedByEqualTo(teacher.getUserId()).andClassTypeEqualTo("TEACHING"); + List rows = schoolClassMapper.selectByExample(example); + return rows == null ? Collections.emptyList() : rows; + } + + @Override + public SchoolClass requireOwnedClass(JwtUser teacher, String teachingClassId) { + requireTeacher(teacher); + if (StringUtils.isBlank(teachingClassId)) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "请选择自己的教学班"); + } + SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(teachingClassId); + if (schoolClass == null || !"TEACHING".equals(schoolClass.getClassType()) + || !StringUtils.equals(teacher.getSchoolId(), schoolClass.getSchoolId()) + || !StringUtils.equals(teacher.getUserId(), schoolClass.getCreatedBy())) { + throw new ServiceException(HttpStatus.FORBIDDEN, "无权访问该教学班成绩"); + } + return schoolClass; + } + + @Override + public TeachingClassScoreWeight getOrCreateWeight(JwtUser teacher, String teachingClassId) { + requireOwnedClass(teacher, teachingClassId); + TeachingClassScoreWeight existing = weightMapper == null ? null : weightMapper.selectByTeachingClassId(teachingClassId); + if (existing != null) { + return existing; + } + TeachingClassScoreWeight defaults = defaultWeight(teachingClassId); + if (weightMapper != null) { + weightMapper.insert(defaults); + } + return defaults; + } + + @Override + public TeachingClassScoreWeight saveWeight(JwtUser teacher, String teachingClassId, TeachingClassScoreWeight weight) { + requireOwnedClass(teacher, teachingClassId); + if (weight == null || !isOneHundredPercent(weight)) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "六项权重合计必须为100%"); + } + weight.setTeachingClassId(teachingClassId); + TeachingClassScoreWeight current = weightMapper == null ? null : weightMapper.selectByTeachingClassId(teachingClassId); + if (weightMapper != null) { + if (current == null) { + weightMapper.insert(weight); + } else { + weightMapper.update(weight); + } + } + return weight; + } + + private TeachingClassScoreWeight defaultWeight(String teachingClassId) { + TeachingClassScoreWeight weight = new TeachingClassScoreWeight(); + weight.setTeachingClassId(teachingClassId); + weight.setFoundationWeight(new BigDecimal("0.10")); + weight.setMarketInsightWeight(new BigDecimal("0.30")); + weight.setPlanningDesignWeight(new BigDecimal("0.10")); + weight.setDevelopmentValidationWeight(new BigDecimal("0.10")); + weight.setLaunchOperationWeight(new BigDecimal("0.20")); + weight.setComprehensiveTrainingWeight(new BigDecimal("0.20")); + return weight; + } + + private boolean isOneHundredPercent(TeachingClassScoreWeight weight) { + BigDecimal sum = safe(weight.getFoundationWeight()).add(safe(weight.getMarketInsightWeight())) + .add(safe(weight.getPlanningDesignWeight())).add(safe(weight.getDevelopmentValidationWeight())) + .add(safe(weight.getLaunchOperationWeight())).add(safe(weight.getComprehensiveTrainingWeight())); + return BigDecimal.ONE.compareTo(sum) == 0; + } + + private BigDecimal safe(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + + private void requireTeacher(JwtUser teacher) { + if (teacher == null || StringUtils.isBlank(teacher.getUserId()) || StringUtils.isBlank(teacher.getSchoolId()) + || (teacher.getRoleId() != 1 && teacher.getRoleId() != 3)) { + throw new ServiceException(HttpStatus.FORBIDDEN, "仅教师可访问成绩中心"); + } + } +} diff --git a/src/main/resources/mappers/StudentTrainingAnswerMapper.xml b/src/main/resources/mappers/StudentTrainingAnswerMapper.xml index 570704e..69d68b5 100644 --- a/src/main/resources/mappers/StudentTrainingAnswerMapper.xml +++ b/src/main/resources/mappers/StudentTrainingAnswerMapper.xml @@ -42,6 +42,13 @@ limit 1 + + delete from student_training_answer where student_user_id = #{studentUserId,jdbcType=VARCHAR} diff --git a/src/main/resources/mappers/TeachingClassScoreWeightMapper.xml b/src/main/resources/mappers/TeachingClassScoreWeightMapper.xml new file mode 100644 index 0000000..715ac75 --- /dev/null +++ b/src/main/resources/mappers/TeachingClassScoreWeightMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + teaching_class_id, foundation_weight, market_insight_weight, planning_design_weight, development_validation_weight, launch_operation_weight, comprehensive_training_weight + + insert into teaching_class_score_weight () values (#{teachingClassId}, #{foundationWeight}, #{marketInsightWeight}, #{planningDesignWeight}, #{developmentValidationWeight}, #{launchOperationWeight}, #{comprehensiveTrainingWeight}) + update teaching_class_score_weight set foundation_weight=#{foundationWeight}, market_insight_weight=#{marketInsightWeight}, planning_design_weight=#{planningDesignWeight}, development_validation_weight=#{developmentValidationWeight}, launch_operation_weight=#{launchOperationWeight}, comprehensive_training_weight=#{comprehensiveTrainingWeight} where teaching_class_id=#{teachingClassId} + diff --git a/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerScopeTest.java b/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerScopeTest.java new file mode 100644 index 0000000..346b913 --- /dev/null +++ b/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerScopeTest.java @@ -0,0 +1,42 @@ +package com.sztzjy.linkCommerce.controller.stu; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.SchoolClass; +import com.sztzjy.linkCommerce.service.TeacherScoreService; +import com.sztzjy.linkCommerce.util.ResultEntity; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TeaScoreControllerScopeTest { + @Test + void ownedTeachingClassesUsesAuthenticatedTeacher() { + TeaScoreController controller = new TeaScoreController() { + @Override + protected JwtUser currentUser(javax.servlet.http.HttpServletRequest request) { + JwtUser teacher = new JwtUser(); + teacher.setUserId("teacher-1"); + teacher.setSchoolId("school-1"); + teacher.setRoleId(3); + return teacher; + } + }; + TeacherScoreService service = mock(TeacherScoreService.class); + ReflectionTestUtils.setField(controller, "teacherScoreService", service); + SchoolClass owned = new SchoolClass(); + owned.setSchoolClassId("class-1"); + when(service.listOwnedClasses(any())).thenReturn(Collections.singletonList(owned)); + + ResultEntity result = controller.ownedTeachingClasses(new MockHttpServletRequest()); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + } +} diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImplTest.java new file mode 100644 index 0000000..74f9a42 --- /dev/null +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/TeacherScoreServiceImplTest.java @@ -0,0 +1,68 @@ +package com.sztzjy.linkCommerce.service.impl; + +import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.SchoolClass; +import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight; +import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; +import com.sztzjy.linkCommerce.mapper.TeachingClassScoreWeightMapper; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TeacherScoreServiceImplTest { + + @Test + void createsSixModuleDefaultsOnlyForOwnedTeachingClass() { + TeacherScoreServiceImpl service = new TeacherScoreServiceImpl(); + SchoolClassMapper classMapper = mock(SchoolClassMapper.class); + TeachingClassScoreWeightMapper weightMapper = mock(TeachingClassScoreWeightMapper.class); + ReflectionTestUtils.setField(service, "schoolClassMapper", classMapper); + ReflectionTestUtils.setField(service, "weightMapper", weightMapper); + when(classMapper.selectByPrimaryKey("class-1")).thenReturn(classRow("class-1", "teacher-1", "school-1")); + + TeachingClassScoreWeight weight = service.getOrCreateWeight(teacher("teacher-1", "school-1"), "class-1"); + + assertEquals(new java.math.BigDecimal("0.10"), weight.getFoundationWeight()); + assertEquals(new java.math.BigDecimal("0.30"), weight.getMarketInsightWeight()); + assertEquals(new java.math.BigDecimal("0.10"), weight.getPlanningDesignWeight()); + assertEquals(new java.math.BigDecimal("0.10"), weight.getDevelopmentValidationWeight()); + assertEquals(new java.math.BigDecimal("0.20"), weight.getLaunchOperationWeight()); + assertEquals(new java.math.BigDecimal("0.20"), weight.getComprehensiveTrainingWeight()); + } + + @Test + void rejectsForeignTeachingClass() { + TeacherScoreServiceImpl service = new TeacherScoreServiceImpl(); + SchoolClassMapper classMapper = mock(SchoolClassMapper.class); + ReflectionTestUtils.setField(service, "schoolClassMapper", classMapper); + when(classMapper.selectByPrimaryKey("class-2")).thenReturn(classRow("class-2", "teacher-2", "school-1")); + + ServiceException error = assertThrows(ServiceException.class, + () -> service.requireOwnedClass(teacher("teacher-1", "school-1"), "class-2")); + + assertEquals(HttpStatus.FORBIDDEN, error.getCode()); + } + + private JwtUser teacher(String userId, String schoolId) { + JwtUser user = new JwtUser(); + user.setUserId(userId); + user.setSchoolId(schoolId); + user.setRoleId(3); + return user; + } + + private SchoolClass classRow(String id, String createdBy, String schoolId) { + SchoolClass row = new SchoolClass(); + row.setSchoolClassId(id); + row.setCreatedBy(createdBy); + row.setSchoolId(schoolId); + row.setClassType("TEACHING"); + return row; + } +}