Compare commits

..

No commits in common. 'main' and 'fix/exam-question-access-material-excel-delete-draft' have entirely different histories.

@ -1,69 +0,0 @@
package com.sztzjy.linkCommerce.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 启动时检查数据库连接和 AI 评估原始响应列的字符集。
* 字符集不是 utf8mb4 时,写入中文会报 "Incorrect string value";
* 这里只记录告警方便排查,不阻止应用启动。
*/
@Slf4j
@Component
public class DatabaseCharsetChecker {
private static final String SESSION_SQL =
"SELECT @@character_set_client AS client, @@character_set_connection AS conn";
private static final String COLUMN_SQL =
"SELECT CHARACTER_SET_NAME FROM information_schema.COLUMNS "
+ "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_training_evaluation' "
+ "AND COLUMN_NAME = 'assessment_raw_response'";
private final JdbcTemplate jdbcTemplate;
public DatabaseCharsetChecker(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostConstruct
public void check() {
try {
List<String> problems = findProblems();
if (problems.isEmpty()) {
log.info("Database charset check passed: connection and AI evaluation columns use utf8mb4");
return;
}
for (String problem : problems) {
log.warn("Database charset check: {}", problem);
}
} catch (RuntimeException e) {
log.warn("Database charset check skipped: {}", e.getMessage());
}
}
List<String> findProblems() {
List<String> problems = new ArrayList<String>();
Map<String, Object> session = jdbcTemplate.queryForMap(SESSION_SQL);
String client = String.valueOf(session.get("client"));
String connection = String.valueOf(session.get("conn"));
if (!"utf8mb4".equalsIgnoreCase(client)) {
problems.add("character_set_client=" + client + ", expected utf8mb4; Chinese text may fail to save");
}
if (!"utf8mb4".equalsIgnoreCase(connection)) {
problems.add("character_set_connection=" + connection + ", expected utf8mb4; Chinese text may fail to save");
}
List<String> columnCharsets = jdbcTemplate.queryForList(COLUMN_SQL, String.class);
for (String charset : columnCharsets) {
if (charset != null && !"utf8mb4".equalsIgnoreCase(charset)) {
problems.add("ai_training_evaluation.assessment_raw_response charset=" + charset
+ ", expected utf8mb4; AI raw responses may fail to save");
}
}
return problems;
}
}

@ -4,8 +4,6 @@ import com.sztzjy.linkCommerce.config.exception.UnAuthorizedException;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
import com.sztzjy.linkCommerce.util.ThrowableUtil; import com.sztzjy.linkCommerce.util.ThrowableUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
@ -15,8 +13,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
/** /**
* 全局异常处理 * 全局异常处理
*/ */
@ -24,8 +20,6 @@ import java.sql.SQLException;
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
private static final String DATABASE_ERROR_MESSAGE = "系统繁忙,请稍后重试";
@ExceptionHandler(IllegalArgumentException.class) @ExceptionHandler(IllegalArgumentException.class)
public ResultEntity<String> illegalArgumentException(IllegalArgumentException e) { public ResultEntity<String> illegalArgumentException(IllegalArgumentException e) {
log.error(ThrowableUtil.getStackTrace(e)); log.error(ThrowableUtil.getStackTrace(e));
@ -67,22 +61,9 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class) @ExceptionHandler(Throwable.class)
public ResultEntity<String> nullPointerException(Throwable e) { public ResultEntity<String> nullPointerException(Throwable e) {
log.error(ThrowableUtil.getStackTrace(e)); log.error(ThrowableUtil.getStackTrace(e));
if (isDatabaseException(e)) {
// 数据库异常原文包含完整 SQL 和表结构,只记日志,不返回给前端
return new ResultEntity<>(HttpStatus.BAD_REQUEST, DATABASE_ERROR_MESSAGE);
}
return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage()); return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage());
} }
private static boolean isDatabaseException(Throwable e) {
for (Throwable t = e; t != null; t = t.getCause() == t ? null : t.getCause()) {
if (t instanceof DataAccessException || t instanceof PersistenceException || t instanceof SQLException) {
return true;
}
}
return false;
}
/** /**
* BadCredentialsException * BadCredentialsException
*/ */

@ -466,6 +466,8 @@ public class SchoolAdminController {
String name = StringUtils.trimToEmpty(row.getName()); String name = StringUtils.trimToEmpty(row.getName());
String username = StringUtils.trimToEmpty(row.getUsername()); String username = StringUtils.trimToEmpty(row.getUsername());
String facultyName = StringUtils.trimToEmpty(row.getSchoolFacultyName());
String majorName = StringUtils.trimToEmpty(row.getSchoolMajorName());
String className = StringUtils.trimToEmpty(row.getSchoolClassName()); String className = StringUtils.trimToEmpty(row.getSchoolClassName());
String sex = normalizeImportSex(row.getSex()); String sex = normalizeImportSex(row.getSex());
@ -484,16 +486,32 @@ public class SchoolAdminController {
} else { } else {
errors.add("第 " + rowNumber + " 行:性别请填写男或女"); errors.add("第 " + rowNumber + " 行:性别请填写男或女");
} }
if (StringUtils.isBlank(facultyName)) {
errors.add("第 " + rowNumber + " 行:所属院系不能为空");
}
if (StringUtils.isBlank(majorName)) {
errors.add("第 " + rowNumber + " 行:所属专业不能为空");
}
if (StringUtils.isBlank(className)) { if (StringUtils.isBlank(className)) {
errors.add("第 " + rowNumber + " 行:所属班级不能为空"); errors.add("第 " + rowNumber + " 行:所属班级不能为空");
} }
//院系/专业列已取消,班级直接按本校行政班名称匹配 SchoolFaculty faculty = StringUtils.isBlank(facultyName) ? null : findFacultyByName(schoolId, facultyName);
SchoolMajor major = null;
SchoolClass schoolClass = null; SchoolClass schoolClass = null;
if (StringUtils.isNotBlank(className)) { if (StringUtils.isNotBlank(facultyName) && faculty == null) {
schoolClass = findAdminClassByName(schoolId, className); errors.add("第 " + rowNumber + " 行:未找到院系“" + facultyName + "”");
}
if (faculty != null && StringUtils.isNotBlank(majorName)) {
major = findMajorByName(faculty.getSchoolFacultyId(), majorName);
if (major == null) {
errors.add("第 " + rowNumber + " 行:院系“" + facultyName + "”下未找到专业“" + majorName + "”");
}
}
if (major != null && StringUtils.isNotBlank(className)) {
schoolClass = findClassByName(schoolId, major.getSchoolMajorId(), className);
if (schoolClass == null) { if (schoolClass == null) {
errors.add("第 " + rowNumber + " 行:未找到行政班:" + className); errors.add("第 " + rowNumber + " 行:专业“" + majorName + "”下未找到班级“" + className + "”");
} }
} }
@ -506,6 +524,8 @@ public class SchoolAdminController {
student.setSex(sex); student.setSex(sex);
student.setPhone(StringUtils.trimToEmpty(row.getPhone())); student.setPhone(StringUtils.trimToEmpty(row.getPhone()));
student.setEmail(StringUtils.trimToEmpty(row.getEmail())); student.setEmail(StringUtils.trimToEmpty(row.getEmail()));
student.setSchoolFacultyId(faculty.getSchoolFacultyId());
student.setSchoolMajorId(major.getSchoolMajorId());
student.setSchoolClassId(schoolClass.getSchoolClassId()); student.setSchoolClassId(schoolClass.getSchoolClassId());
student.setCreateTime(new Date()); student.setCreateTime(new Date());
student.setCodeFrom("Excel导入"); student.setCodeFrom("Excel导入");
@ -523,6 +543,8 @@ public class SchoolAdminController {
return StringUtils.isBlank(row.getName()) return StringUtils.isBlank(row.getName())
&& StringUtils.isBlank(row.getUsername()) && StringUtils.isBlank(row.getUsername())
&& StringUtils.isBlank(row.getSex()) && StringUtils.isBlank(row.getSex())
&& StringUtils.isBlank(row.getSchoolFacultyName())
&& StringUtils.isBlank(row.getSchoolMajorName())
&& StringUtils.isBlank(row.getSchoolClassName()) && StringUtils.isBlank(row.getSchoolClassName())
&& StringUtils.isBlank(row.getPhone()) && StringUtils.isBlank(row.getPhone())
&& StringUtils.isBlank(row.getEmail()); && StringUtils.isBlank(row.getEmail());
@ -552,13 +574,30 @@ public class SchoolAdminController {
return false; return false;
} }
//学生账号导入不再区分院系/专业,班级直接按本校行政班名称匹配 private SchoolFaculty findFacultyByName(String schoolId, String facultyName) {
private SchoolClass findAdminClassByName(String schoolId, String className) { SchoolFacultyExample example = new SchoolFacultyExample();
example.createCriteria()
.andSchoolIdEqualTo(schoolId)
.andSchoolFacultyNameEqualTo(facultyName);
List<SchoolFaculty> faculties = schoolFacultyMapper.selectByExample(example);
return faculties.isEmpty() ? null : faculties.get(0);
}
private SchoolMajor findMajorByName(String facultyId, String majorName) {
SchoolMajorExample example = new SchoolMajorExample();
example.createCriteria()
.andSchoolFacultyIdEqualTo(facultyId)
.andSchoolMajorNameEqualTo(majorName);
List<SchoolMajor> majors = schoolMajorMapper.selectByExample(example);
return majors.isEmpty() ? null : majors.get(0);
}
private SchoolClass findClassByName(String schoolId, String majorId, String className) {
SchoolClassExample example = new SchoolClassExample(); SchoolClassExample example = new SchoolClassExample();
example.createCriteria() example.createCriteria()
.andSchoolIdEqualTo(schoolId) .andSchoolIdEqualTo(schoolId)
.andClassNameEqualTo(className) .andSchoolMajorIdEqualTo(majorId)
.andClassTypeEqualTo("ADMIN"); .andClassNameEqualTo(className);
List<SchoolClass> classes = schoolClassMapper.selectByExample(example); List<SchoolClass> classes = schoolClassMapper.selectByExample(example);
return classes.isEmpty() ? null : classes.get(0); return classes.isEmpty() ? null : classes.get(0);
} }

@ -27,7 +27,6 @@ import com.sztzjy.linkCommerce.util.TzApi;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -95,9 +94,6 @@ public class LoginController {
} }
Userinfo user = userinfos.get(0); Userinfo user = userinfos.get(0);
return new ResultEntity(HttpStatus.OK, buildLoginResultMap(user, token)); return new ResultEntity(HttpStatus.OK, buildLoginResultMap(user, token));
}catch (BadCredentialsException e){
// 账号存在但密码不正确;账号不存在时 loadUserByUsername 会抛下标越界,走下方兜底
return new ResultEntity(HttpStatus.BAD_REQUEST, "密码错误");
}catch (Exception e){ }catch (Exception e){
return new ResultEntity(HttpStatus.BAD_REQUEST, "账号不存在"); return new ResultEntity(HttpStatus.BAD_REQUEST, "账号不存在");
} }

@ -441,7 +441,6 @@ public class TeaScoreController {
rank.setCompletedTaskCount(0); rank.setCompletedTaskCount(0);
rank.setTrainingProgress(null); rank.setTrainingProgress(null);
rank.setTeacherScore(null); rank.setTeacherScore(null);
rank.setAiAssessmentScore(null);
continue; continue;
} }
List<StudentTrainingAnswer> answers = studentTrainingAnswerMapper == null ? Collections.emptyList() List<StudentTrainingAnswer> answers = studentTrainingAnswerMapper == null ? Collections.emptyList()
@ -455,7 +454,6 @@ public class TeaScoreController {
.multiply(BigDecimal.valueOf(100)) .multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(totalTaskCount), 2, BigDecimal.ROUND_HALF_UP)); .divide(BigDecimal.valueOf(totalTaskCount), 2, BigDecimal.ROUND_HALF_UP));
rank.setTeacherScore(calculateTeacherScore(classId, allocations, answersByTaskKey)); rank.setTeacherScore(calculateTeacherScore(classId, allocations, answersByTaskKey));
rank.setAiAssessmentScore(calculateAiAssessmentScore(classId, allocations, answersByTaskKey));
} }
} }
@ -553,48 +551,6 @@ public class TeaScoreController {
if (!hasTeacherScore) { if (!hasTeacherScore) {
return null; return null;
} }
return weightedSixModuleScore(classId, scoresByProject, comprehensiveParticipating);
}
/**
* AI评测成绩:与教师评分同一套六项目加权口径,聚合各任务的 ai_assessment_score。
* 直接读 student_training_answer(实时),AI 评分完成即可见于教师端。
*/
private BigDecimal calculateAiAssessmentScore(String classId, List<TaskAllocation> allocations,
Map<String, StudentTrainingAnswer> answersByTaskKey) {
if (teacherScoreService == null || trainingTaskMapper == null) {
return null;
}
Map<String, List<BigDecimal>> scoresByProject = new HashMap<>();
boolean hasAiScore = false;
boolean comprehensiveParticipating = false;
for (TaskAllocation allocation : allocations) {
if (allocation == null || StringUtils.isBlank(allocation.getModule())) {
continue;
}
if ("comprehensive-case-training".equals(allocation.getModule())) {
comprehensiveParticipating = true;
}
TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule());
if (task == null || StringUtils.isBlank(task.getProjectName())) {
continue;
}
StudentTrainingAnswer answer = answersByTaskKey.get(allocation.getModule());
Integer taskScore = answer == null ? null : answer.getAiAssessmentScore();
if (taskScore != null) {
hasAiScore = true;
}
scoresByProject.computeIfAbsent(task.getProjectName(), key -> new ArrayList<>())
.add(taskScore == null ? BigDecimal.ZERO : BigDecimal.valueOf(taskScore));
}
if (!hasAiScore) {
return null;
}
return weightedSixModuleScore(classId, scoresByProject, comprehensiveParticipating);
}
private BigDecimal weightedSixModuleScore(String classId, Map<String, List<BigDecimal>> scoresByProject,
boolean comprehensiveParticipating) {
TeachingClassScoreWeight weight = teacherScoreService.getOrCreateWeightForCalculation(classId); TeachingClassScoreWeight weight = teacherScoreService.getOrCreateWeightForCalculation(classId);
if (weight == null) { if (weight == null) {
return null; return null;

@ -557,36 +557,32 @@ public class UserController {
@PostMapping("/addTeacherAccount") @PostMapping("/addTeacherAccount")
@ApiOperation("教师管理-添加教师账号") @ApiOperation("教师管理-添加教师账号")
@AnonymousAccess @AnonymousAccess
public ResultEntity addTeacherAccount(@ApiParam("学校ID") @RequestParam(required = false) String schoolId, public ResultEntity addTeacherAccount(@ApiParam("学校ID") @RequestParam String schoolId,
@ApiParam("学校名称") @RequestParam(required = false) String schoolName, @ApiParam("学校名称") @RequestParam String schoolName,
@ApiParam("院系ID") @RequestParam(required = false) String schoolFacultyId, @ApiParam("院系ID") @RequestParam String schoolFacultyId,
@ApiParam("专业ID") @RequestParam(required = false) String schoolMajorId, @ApiParam("专业ID") @RequestParam String schoolMajorId,
@ApiParam("专业名称") @RequestParam(required = false) String schoolMajorName, @ApiParam("专业名称") @RequestParam String schoolMajorName,
@ApiParam("教师名称") @RequestParam String name, @ApiParam("教师名称") @RequestParam String name,
@ApiParam("教师工号") @RequestParam String userName, @ApiParam("教师工号") @RequestParam String userName,
@ApiParam("电话号码") @RequestParam(required = false) String phone, @ApiParam("电话号码") @RequestParam(required = false) String phone,
@ApiParam("电子邮箱") @RequestParam(required = false) String email, @ApiParam("电子邮箱") @RequestParam(required = false) String email,
@RequestParam(required = false) String operatorId) { @RequestParam(required = false) String operatorId) {
//学校/院系/专业默认从当前操作用户继承;平台管理员可显式指定学校 //先根据schoolId进行查询 如果不存在则返回
Userinfo operator = StringUtils.isBlank(operatorId) ? null : userinfoMapper.selectByPrimaryKey(operatorId); School school = schoolMapper.selectByPrimaryKey(schoolId);
String effectiveSchoolId = isSchoolTeacher(operator) ? operator.getSchoolId() : StringUtils.trimToNull(schoolId);
if (StringUtils.isBlank(effectiveSchoolId)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "无法确定教师所属学校");
}
School school = schoolMapper.selectByPrimaryKey(effectiveSchoolId);
if (school == null) { if (school == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学校不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学校不存在");
} }
if (!canManageSchoolStructure(operatorId, effectiveSchoolId)) { if (!canManageSchoolStructure(operatorId, schoolId)) {
return noManagePermission(); return noManagePermission();
} }
String effectiveFacultyId = isSchoolTeacher(operator) ? StringUtils.trimToNull(operator.getSchoolFacultyId()) : StringUtils.trimToNull(schoolFacultyId); //再根据schoolFacultyId查询 如果不存在则返回
String effectiveMajorId = isSchoolTeacher(operator) ? StringUtils.trimToNull(operator.getSchoolMajorId()) : StringUtils.trimToNull(schoolMajorId); SchoolFaculty schoolFaculty = schoolFacultyMapper.selectByPrimaryKey(schoolFacultyId);
//院系/专业选填:有值才校验 if (schoolFaculty == null) {
if (StringUtils.isNotBlank(effectiveFacultyId) && schoolFacultyMapper.selectByPrimaryKey(effectiveFacultyId) == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在");
} }
if (StringUtils.isNotBlank(effectiveMajorId) && schoolMajorMapper.selectByPrimaryKey(effectiveMajorId) == null) { //再根据schoolMajorId查询 如果不存在则返回
SchoolMajor schoolMajor = schoolMajorMapper.selectByPrimaryKey(schoolMajorId);
if (schoolMajor == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该专业不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该专业不存在");
} }
//检查userName是否已经存在 //检查userName是否已经存在
@ -603,9 +599,9 @@ public class UserController {
userInfo.setEmail(email); userInfo.setEmail(email);
userInfo.setCreateTime(new Date()); userInfo.setCreateTime(new Date());
userInfo.setRole(3); userInfo.setRole(3);
userInfo.setSchoolId(effectiveSchoolId); userInfo.setSchoolId(schoolId);
userInfo.setSchoolFacultyId(effectiveFacultyId); userInfo.setSchoolFacultyId(schoolFacultyId);
userInfo.setSchoolMajorId(effectiveMajorId); userInfo.setSchoolMajorId(schoolMajorId);
userInfo.setSchoolClassId(null); userInfo.setSchoolClassId(null);
userInfo.setTeacherAdmin(false); userInfo.setTeacherAdmin(false);
userInfo.setCodeFrom("添加"); userInfo.setCodeFrom("添加");
@ -635,6 +631,7 @@ public class UserController {
} }
schoolId = operator.getSchoolId(); schoolId = operator.getSchoolId();
} }
PageHelper.startPage(index, size);
UserinfoExample userInfoExample = new UserinfoExample(); UserinfoExample userInfoExample = new UserinfoExample();
UserinfoExample.Criteria criteria = userInfoExample.createCriteria(); UserinfoExample.Criteria criteria = userInfoExample.createCriteria();
School school = new School(); School school = new School();
@ -649,8 +646,6 @@ public class UserController {
criteria.andUsernameEqualTo(userName); criteria.andUsernameEqualTo(userName);
} }
criteria.andRoleEqualTo(3); criteria.andRoleEqualTo(3);
// startPage 只对紧随其后的第一条查询生效,必须放在学校等辅助查询之后,否则教师主查询不会分页
PageHelper.startPage(index, size);
List<Userinfo> userInfoList = userinfoMapper.selectByExample(userInfoExample); List<Userinfo> userInfoList = userinfoMapper.selectByExample(userInfoExample);
for (int i = 0; i < userInfoList.size(); i++) { for (int i = 0; i < userInfoList.size(); i++) {
if (StringUtils.isNotBlank(schoolId)) { if (StringUtils.isNotBlank(schoolId)) {
@ -770,18 +765,15 @@ public class UserController {
if (!errorMessages.isEmpty()) { if (!errorMessages.isEmpty()) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "数据校验失败!", errorMessages); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "数据校验失败!", errorMessages);
} }
// 教师的学校/院系/专业直接取自当前操作用户,不再从Excel的院校名称列解析 for (TeacherImportDTO teacher : teacherList) {
Userinfo operator = userinfoMapper.selectByPrimaryKey(operatorId); String schoolId = schoolService.selectSchoolIdBySchoolName(teacher.getSchoolName());
String effectiveSchoolId = operator == null ? null : StringUtils.trimToEmpty(operator.getSchoolId()); if (!canManageSchoolStructure(operatorId, schoolId)) {
if (StringUtils.isBlank(effectiveSchoolId)) { return noManagePermission();
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "无法确定当前用户所属学校,导入失败"); }
}
if (!canManageSchoolStructure(operatorId, effectiveSchoolId)) {
return noManagePermission();
} }
// 异步处理导入***** // 异步处理导入*****
importService.processTeacherImport(teacherList, operator); importService.processTeacherImport(teacherList);
return new ResultEntity<>(HttpStatus.OK, "导入成功!"); return new ResultEntity<>(HttpStatus.OK, "导入成功!");
} catch (Exception e) { } catch (Exception e) {
@ -823,9 +815,9 @@ public class UserController {
@AnonymousAccess @AnonymousAccess
public ResultEntity addStudentAccount(@ApiParam("学校ID") @RequestParam String schoolId, public ResultEntity addStudentAccount(@ApiParam("学校ID") @RequestParam String schoolId,
@ApiParam("学校名称") @RequestParam String schoolName, @ApiParam("学校名称") @RequestParam String schoolName,
@ApiParam("院系ID") @RequestParam(required = false) String schoolFacultyId, @ApiParam("院系ID") @RequestParam String schoolFacultyId,
@ApiParam("专业ID") @RequestParam(required = false) String schoolMajorId, @ApiParam("专业ID") @RequestParam String schoolMajorId,
@ApiParam("专业名称") @RequestParam(required = false) String schoolMajorName, @ApiParam("专业名称") @RequestParam String schoolMajorName,
@ApiParam("班级名称") @RequestParam String schoolClassName, @ApiParam("班级名称") @RequestParam String schoolClassName,
@ApiParam("班级Id") @RequestParam String schoolClassId, @ApiParam("班级Id") @RequestParam String schoolClassId,
@ApiParam("学生姓名") @RequestParam String name, @ApiParam("学生姓名") @RequestParam String name,
@ -840,11 +832,14 @@ public class UserController {
if (school == null) { if (school == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学校不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学校不存在");
} }
//院系/专业改为选填:传了才校验,不传直接入库为空 //再根据schoolFacultyId查询 如果不存在则返回
if (StringUtils.isNotBlank(schoolFacultyId) && schoolFacultyMapper.selectByPrimaryKey(schoolFacultyId) == null) { SchoolFaculty schoolFaculty = schoolFacultyMapper.selectByPrimaryKey(schoolFacultyId);
if (schoolFaculty == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在");
} }
if (StringUtils.isNotBlank(schoolMajorId) && schoolMajorMapper.selectByPrimaryKey(schoolMajorId) == null) { //再根据schoolMajorId查询 如果不存在则返回
SchoolMajor schoolMajor = schoolMajorMapper.selectByPrimaryKey(schoolMajorId);
if (schoolMajor == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该专业不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该专业不存在");
} }
SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(schoolClassId); SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(schoolClassId);
@ -870,8 +865,8 @@ public class UserController {
userInfo.setRole(4); userInfo.setRole(4);
userInfo.setTeacherAdmin(false); userInfo.setTeacherAdmin(false);
userInfo.setSchoolId(effectiveSchoolId); userInfo.setSchoolId(effectiveSchoolId);
userInfo.setSchoolFacultyId(StringUtils.trimToNull(schoolFacultyId)); userInfo.setSchoolFacultyId(schoolFacultyId);
userInfo.setSchoolMajorId(StringUtils.trimToNull(schoolMajorId)); userInfo.setSchoolMajorId(schoolMajorId);
String adminClassId = "TEACHING".equals(schoolClass.getClassType()) String adminClassId = "TEACHING".equals(schoolClass.getClassType())
? resolveAdminClassIdForTeachingClass(schoolClass) ? resolveAdminClassIdForTeachingClass(schoolClass)
: schoolClassId; : schoolClassId;
@ -994,6 +989,7 @@ public class UserController {
if (teacherRosterScope) { if (teacherRosterScope) {
schoolId = operator.getSchoolId(); schoolId = operator.getSchoolId();
} }
PageHelper.startPage(index, size);
UserinfoExample userInfoExample = new UserinfoExample(); UserinfoExample userInfoExample = new UserinfoExample();
UserinfoExample.Criteria criteria = userInfoExample.createCriteria(); UserinfoExample.Criteria criteria = userInfoExample.createCriteria();
School school = new School(); School school = new School();
@ -1029,8 +1025,6 @@ public class UserController {
} }
} }
criteria.andRoleEqualTo(4); criteria.andRoleEqualTo(4);
// startPage 只对紧随其后的第一条查询生效,必须放在学校/教学班/花名册等辅助查询之后,否则学生主查询不会分页
PageHelper.startPage(index, size);
List<Userinfo> userInfoList = userinfoMapper.selectByExample(userInfoExample); List<Userinfo> userInfoList = userinfoMapper.selectByExample(userInfoExample);
for (int i = 0; i < userInfoList.size(); i++) { for (int i = 0; i < userInfoList.size(); i++) {
if (StringUtils.isNotBlank(schoolId)) { if (StringUtils.isNotBlank(schoolId)) {
@ -2135,6 +2129,8 @@ public class UserController {
String name = StringUtils.trimToEmpty(row.getName()); String name = StringUtils.trimToEmpty(row.getName());
String username = StringUtils.trimToEmpty(row.getUsername()); String username = StringUtils.trimToEmpty(row.getUsername());
String facultyName = StringUtils.trimToEmpty(row.getSchoolFacultyName());
String majorName = StringUtils.trimToEmpty(row.getSchoolMajorName());
String className = StringUtils.trimToEmpty(row.getSchoolClassName()); String className = StringUtils.trimToEmpty(row.getSchoolClassName());
String sex = normalizeTeacherImportSex(row.getSex()); String sex = normalizeTeacherImportSex(row.getSex());
@ -2151,16 +2147,32 @@ public class UserController {
if (StringUtils.isNotBlank(row.getSex()) && sex == null) { if (StringUtils.isNotBlank(row.getSex()) && sex == null) {
errors.add("第" + rowNumber + "行:性别请填写男或女"); errors.add("第" + rowNumber + "行:性别请填写男或女");
} }
if (StringUtils.isBlank(facultyName)) {
errors.add("第" + rowNumber + "行:所属院系不能为空");
}
if (StringUtils.isBlank(majorName)) {
errors.add("第" + rowNumber + "行:所属专业不能为空");
}
if (StringUtils.isBlank(className)) { if (StringUtils.isBlank(className)) {
errors.add("第" + rowNumber + "行:所属班级不能为空"); errors.add("第" + rowNumber + "行:所属班级不能为空");
} }
//院系/专业列已取消,班级直接按本校行政班名称匹配 SchoolFaculty faculty = StringUtils.isBlank(facultyName) ? null : findTeacherImportFaculty(operator.getSchoolId(), facultyName);
SchoolMajor major = null;
SchoolClass schoolClass = null; SchoolClass schoolClass = null;
if (StringUtils.isNotBlank(className)) { if (StringUtils.isNotBlank(facultyName) && faculty == null) {
schoolClass = findTeacherImportAdminClass(operator.getSchoolId(), className); errors.add("第" + rowNumber + "行:未找到院系“" + facultyName + "”");
}
if (faculty != null && StringUtils.isNotBlank(majorName)) {
major = findTeacherImportMajor(faculty.getSchoolFacultyId(), majorName);
if (major == null) {
errors.add("第" + rowNumber + "行:院系“" + facultyName + "”下未找到专业“" + majorName + "”");
}
}
if (major != null && StringUtils.isNotBlank(className)) {
schoolClass = findTeacherImportClass(operator.getSchoolId(), major.getSchoolMajorId(), className);
if (schoolClass == null) { if (schoolClass == null) {
errors.add("第" + rowNumber + "行:未找到行政班“" + className + "”"); errors.add("第" + rowNumber + "行:专业“" + majorName + "”下未找到班级“" + className + "”");
} else if (!isAdminClass(schoolClass)) { } else if (!isAdminClass(schoolClass)) {
errors.add("第" + rowNumber + "行:学生账号只能导入行政班"); errors.add("第" + rowNumber + "行:学生账号只能导入行政班");
} else if (!canManageAdminClass(operator, schoolClass)) { } else if (!canManageAdminClass(operator, schoolClass)) {
@ -2180,6 +2192,8 @@ public class UserController {
student.setRole(4); student.setRole(4);
student.setTeacherAdmin(false); student.setTeacherAdmin(false);
student.setSchoolId(operator.getSchoolId()); student.setSchoolId(operator.getSchoolId());
student.setSchoolFacultyId(faculty.getSchoolFacultyId());
student.setSchoolMajorId(major.getSchoolMajorId());
student.setSchoolClassId(schoolClass.getSchoolClassId()); student.setSchoolClassId(schoolClass.getSchoolClassId());
student.setCreateTime(new Date()); student.setCreateTime(new Date());
student.setCodeFrom("Excel导入"); student.setCodeFrom("Excel导入");
@ -2196,6 +2210,8 @@ public class UserController {
return StringUtils.isBlank(row.getName()) return StringUtils.isBlank(row.getName())
&& StringUtils.isBlank(row.getUsername()) && StringUtils.isBlank(row.getUsername())
&& StringUtils.isBlank(row.getSex()) && StringUtils.isBlank(row.getSex())
&& StringUtils.isBlank(row.getSchoolFacultyName())
&& StringUtils.isBlank(row.getSchoolMajorName())
&& StringUtils.isBlank(row.getSchoolClassName()) && StringUtils.isBlank(row.getSchoolClassName())
&& StringUtils.isBlank(row.getPhone()) && StringUtils.isBlank(row.getPhone())
&& StringUtils.isBlank(row.getEmail()); && StringUtils.isBlank(row.getEmail());
@ -2225,23 +2241,30 @@ public class UserController {
return false; return false;
} }
private SchoolClass findTeacherImportClass(String schoolId, String majorId, String className) { private SchoolFaculty findTeacherImportFaculty(String schoolId, String facultyName) {
SchoolClassExample example = new SchoolClassExample(); SchoolFacultyExample example = new SchoolFacultyExample();
example.createCriteria() example.createCriteria()
.andSchoolIdEqualTo(schoolId) .andSchoolIdEqualTo(schoolId)
.andSchoolMajorIdEqualTo(majorId) .andSchoolFacultyNameEqualTo(facultyName);
.andClassNameEqualTo(className); List<SchoolFaculty> faculties = schoolFacultyMapper.selectByExample(example);
List<SchoolClass> classes = schoolClassMapper.selectByExample(example); return faculties.isEmpty() ? null : faculties.get(0);
return classes.isEmpty() ? null : classes.get(0); }
private SchoolMajor findTeacherImportMajor(String facultyId, String majorName) {
SchoolMajorExample example = new SchoolMajorExample();
example.createCriteria()
.andSchoolFacultyIdEqualTo(facultyId)
.andSchoolMajorNameEqualTo(majorName);
List<SchoolMajor> majors = schoolMajorMapper.selectByExample(example);
return majors.isEmpty() ? null : majors.get(0);
} }
//学生账号导入不再区分院系/专业,班级直接按本校行政班名称匹配 private SchoolClass findTeacherImportClass(String schoolId, String majorId, String className) {
private SchoolClass findTeacherImportAdminClass(String schoolId, String className) {
SchoolClassExample example = new SchoolClassExample(); SchoolClassExample example = new SchoolClassExample();
example.createCriteria() example.createCriteria()
.andSchoolIdEqualTo(schoolId) .andSchoolIdEqualTo(schoolId)
.andClassNameEqualTo(className) .andSchoolMajorIdEqualTo(majorId)
.andClassTypeEqualTo("ADMIN"); .andClassNameEqualTo(className);
List<SchoolClass> classes = schoolClassMapper.selectByExample(example); List<SchoolClass> classes = schoolClassMapper.selectByExample(example);
return classes.isEmpty() ? null : classes.get(0); return classes.isEmpty() ? null : classes.get(0);
} }

@ -52,9 +52,6 @@ public class StuRank {
@ApiModelProperty(notes = "老师人工评分(查询时从教学班评分表读取)") @ApiModelProperty(notes = "老师人工评分(查询时从教学班评分表读取)")
private BigDecimal teacherScore; private BigDecimal teacherScore;
@ApiModelProperty(notes = "AI评测成绩(查询时按教学班任务聚合各任务ai_assessment_score,不落库)")
private BigDecimal aiAssessmentScore;
@ApiModelProperty(notes = "市场需求挖掘分数") @ApiModelProperty(notes = "市场需求挖掘分数")
private BigDecimal scqxwjScore; private BigDecimal scqxwjScore;
@ -203,14 +200,6 @@ public class StuRank {
this.teacherScore = teacherScore; this.teacherScore = teacherScore;
} }
public BigDecimal getAiAssessmentScore() {
return aiAssessmentScore;
}
public void setAiAssessmentScore(BigDecimal aiAssessmentScore) {
this.aiAssessmentScore = aiAssessmentScore;
}
public BigDecimal getScqxwjScore() { public BigDecimal getScqxwjScore() {
return scqxwjScore; return scqxwjScore;
} }

@ -18,6 +18,14 @@ public class SchoolAdminStudentImportDTO {
@ApiModelProperty("性别") @ApiModelProperty("性别")
private String sex; private String sex;
@ExcelProperty("所属院系")
@ApiModelProperty("所属院系")
private String schoolFacultyName;
@ExcelProperty("所属专业")
@ApiModelProperty("所属专业")
private String schoolMajorName;
@ExcelProperty("所属班级") @ExcelProperty("所属班级")
@ApiModelProperty("所属班级") @ApiModelProperty("所属班级")
private String schoolClassName; private String schoolClassName;

@ -1,24 +1,28 @@
package com.sztzjy.linkCommerce.entity.importDto; package com.sztzjy.linkCommerce.entity.importDto;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
@Data @Data
public class TeacherImportDTO { public class TeacherImportDTO {
@ExcelProperty("教师姓名") @ApiModelProperty("院校名称")
private String schoolName;
@ApiModelProperty("院系名称")
private String schoolFacultyName;
@ApiModelProperty("专业名称")
private String schoolMajorName;
@ApiModelProperty("教师姓名") @ApiModelProperty("教师姓名")
private String teacherName; private String teacherName;
@ExcelProperty("工号")
@ApiModelProperty("工号") @ApiModelProperty("工号")
private String userName; private String userName;
@ExcelProperty("电话")
@ApiModelProperty("电话") @ApiModelProperty("电话")
private String phone; private String phone;
@ExcelProperty("邮箱")
@ApiModelProperty("邮箱") @ApiModelProperty("邮箱")
private String email; private String email;
} }

@ -1,13 +1,12 @@
package com.sztzjy.linkCommerce.service; package com.sztzjy.linkCommerce.service;
import com.sztzjy.linkCommerce.entity.Userinfo;
import com.sztzjy.linkCommerce.entity.importDto.StudentImportDTO; import com.sztzjy.linkCommerce.entity.importDto.StudentImportDTO;
import com.sztzjy.linkCommerce.entity.importDto.TeacherImportDTO; import com.sztzjy.linkCommerce.entity.importDto.TeacherImportDTO;
import java.util.List; import java.util.List;
public interface ImportService { public interface ImportService {
void processTeacherImport(List<TeacherImportDTO> teacherList, Userinfo operator); void processTeacherImport(List<TeacherImportDTO> teacherList);
void processStudentImport(List<StudentImportDTO> studentList); void processStudentImport(List<StudentImportDTO> studentList);
} }

@ -3,7 +3,6 @@ package com.sztzjy.linkCommerce.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sztzjy.linkCommerce.ai.QwenChatClient; import com.sztzjy.linkCommerce.ai.QwenChatClient;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
@ -20,10 +19,8 @@ import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper;
import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper;
import com.sztzjy.linkCommerce.mapper.TrainingTaskClassConfigMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskClassConfigMapper;
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService; import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
import com.sztzjy.linkCommerce.service.ScoreRankService;
import com.sztzjy.linkCommerce.service.TrainingKnowledgeService; import com.sztzjy.linkCommerce.service.TrainingKnowledgeService;
import com.sztzjy.linkCommerce.training.knowledge.EffectiveTrainingTaskContent; import com.sztzjy.linkCommerce.training.knowledge.EffectiveTrainingTaskContent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -36,24 +33,16 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service @Service
public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationService { public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationService {
private static final String NOT_STARTED = "NOT_STARTED"; private static final String NOT_STARTED = "NOT_STARTED";
private static final String PROCESSING = "PROCESSING"; private static final String PROCESSING = "PROCESSING";
private static final String SUCCEEDED = "SUCCEEDED"; private static final String SUCCEEDED = "SUCCEEDED";
private static final String FAILURE_MESSAGE = "AI服务暂时不可用,请稍后重试";
// 与 AiTrainingEvaluationMapper.xml 中 claimHelp/claimAssessment 的 INTERVAL 10 MINUTE 保持一致
private static final long PROCESSING_TIMEOUT_MILLIS = 10 * 60 * 1000L;
private static final int LOGGED_RAW_RESPONSE_LENGTH = 500;
private static final int MAX_ERROR_LENGTH = 1000; private static final int MAX_ERROR_LENGTH = 1000;
private static final int MAX_HELP_QUESTION_LENGTH = 500; private static final int MAX_HELP_QUESTION_LENGTH = 500;
private static final int MAX_STEP_CONTEXT_LENGTH = 2000; private static final int MAX_STEP_CONTEXT_LENGTH = 2000;
private static final int MAX_ASSESSMENT_RATIONALE_CHARACTERS = 200; private static final int MAX_ASSESSMENT_RATIONALE_CHARACTERS = 200;
private static final int MAX_CHOICE_TITLE_LENGTH = 120;
private static final int MAX_CHOICE_OPTION_LENGTH = 80;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@ -65,8 +54,6 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
public StudentTrainingAnswerMapper studentTrainingAnswerMapper; public StudentTrainingAnswerMapper studentTrainingAnswerMapper;
@Autowired @Autowired
public TeachingClassStudentMapper teachingClassStudentMapper; public TeachingClassStudentMapper teachingClassStudentMapper;
@Autowired
public ScoreRankService scoreRankService;
@Autowired(required = false) @Autowired(required = false)
public TrainingTaskClassConfigMapper trainingTaskClassConfigMapper; public TrainingTaskClassConfigMapper trainingTaskClassConfigMapper;
@Autowired(required = false) @Autowired(required = false)
@ -95,7 +82,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
String normalizedStepContext = normalizeStepContext(stepContext); String normalizedStepContext = normalizeStepContext(stepContext);
EvaluationContext context = resolveContext(taskKey, user, true); EvaluationContext context = resolveContext(taskKey, user, true);
AiTrainingEvaluation evaluation = getOrCreate(context, user); AiTrainingEvaluation evaluation = getOrCreate(context, user);
if (PROCESSING.equals(evaluation.getHelpStatus()) && !isProcessingTimedOut(evaluation)) { if (PROCESSING.equals(evaluation.getHelpStatus())) {
return toView(evaluation); return toView(evaluation);
} }
if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) { if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) {
@ -123,7 +110,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
completed.setAssessmentStatus(evaluation.getAssessmentStatus()); completed.setAssessmentStatus(evaluation.getAssessmentStatus());
return toView(completed); return toView(completed);
} catch (RuntimeException e) { } catch (RuntimeException e) {
recordHelpFailure(evaluation.getId(), e); aiTrainingEvaluationMapper.failHelp(evaluation.getId(), cappedMessage(e), new Date());
throw asServiceException(e); throw asServiceException(e);
} }
} }
@ -132,16 +119,12 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
public AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user) { public AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user) {
EvaluationContext context = resolveContext(taskKey, user, true); EvaluationContext context = resolveContext(taskKey, user, true);
AiTrainingEvaluation evaluation = getOrCreate(context, user); AiTrainingEvaluation evaluation = getOrCreate(context, user);
if (PROCESSING.equals(evaluation.getAssessmentStatus()) && !isProcessingTimedOut(evaluation)) { if (PROCESSING.equals(evaluation.getAssessmentStatus())) {
log.info("AI assessment still processing, evaluationId={}", evaluation.getId());
return toView(evaluation); return toView(evaluation);
} }
String answerSnapshot = answerSnapshot(context.answer); String answerSnapshot = answerSnapshot(context.answer);
boolean reassessment = SUCCEEDED.equals(evaluation.getAssessmentStatus()); boolean reassessment = SUCCEEDED.equals(evaluation.getAssessmentStatus());
if (reassessment && answerSnapshot.equals(evaluation.getAssessmentAnswerSnapshot())) { if (reassessment && answerSnapshot.equals(evaluation.getAssessmentAnswerSnapshot())) {
log.info("AI assessment cached hit, evaluationId={}, answerId={}, snapshotLength={}",
evaluation.getId(), context.answer == null ? null : context.answer.getId(),
answerSnapshot.length());
return toView(evaluation); return toView(evaluation);
} }
String taskSnapshot = taskSnapshot(context.effectiveContent); String taskSnapshot = taskSnapshot(context.effectiveContent);
@ -150,19 +133,13 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
? aiTrainingEvaluationMapper.claimReassessment(evaluation.getId(), evaluation.getAssessmentAnswerSnapshot(), new Date()) ? aiTrainingEvaluationMapper.claimReassessment(evaluation.getId(), evaluation.getAssessmentAnswerSnapshot(), new Date())
: aiTrainingEvaluationMapper.claimAssessment(evaluation.getId(), new Date()); : aiTrainingEvaluationMapper.claimAssessment(evaluation.getId(), new Date());
if (claimed != 1) { if (claimed != 1) {
log.info("AI assessment claim skipped, evaluationId={}, reassessment={}, claimed={}",
evaluation.getId(), reassessment, claimed);
return currentView(context, user); return currentView(context, user);
} }
String rawResponse = null; String rawResponse = null;
try { try {
String knowledgeContext = knowledgeContext(context, user, ""); String knowledgeContext = knowledgeContext(context, user, "");
String choiceQuestionContext = choiceQuestionContext(context.effectiveContent, context.answer); rawResponse = qwenChatClient.completeJson(assessmentSystemPrompt(criterionPlan), assessmentUserPrompt(taskSnapshot, answerSnapshot, knowledgeContext));
log.info("AI assessment choice question context prepared, evaluationId={}, entries={}",
evaluation.getId(), choiceQuestionEntryCount(choiceQuestionContext));
rawResponse = qwenChatClient.completeJson(assessmentSystemPrompt(criterionPlan),
assessmentUserPrompt(taskSnapshot, answerSnapshot, knowledgeContext, choiceQuestionContext));
JsonNode assessmentReport = object(rawResponse); JsonNode assessmentReport = object(rawResponse);
int score = validateAssessment(assessmentReport, criterionPlan); int score = validateAssessment(assessmentReport, criterionPlan);
compactAssessmentRationales(assessmentReport); compactAssessmentRationales(assessmentReport);
@ -178,14 +155,11 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
completed.setAssessmentCompletedAt(completedAt); completed.setAssessmentCompletedAt(completedAt);
completed.setUpdateTime(completedAt); completed.setUpdateTime(completedAt);
completeAssessmentAndWriteScore(completed, context.answer.getId(), score, completedAt); completeAssessmentAndWriteScore(completed, context.answer.getId(), score, completedAt);
// 教师端成绩列表读的是 stu_rank 每日快照(凌晨1点定时重建),
// 不在这里立即刷新本班快照,新 AI 分数要等次日才会出现在教师端。
refreshScoreRankAfterAssessment(user, context.teachingClassId);
completed.setHelpStatus(evaluation.getHelpStatus()); completed.setHelpStatus(evaluation.getHelpStatus());
completed.setAssessmentStatus(SUCCEEDED); completed.setAssessmentStatus(SUCCEEDED);
return toView(completed); return toView(completed);
} catch (RuntimeException e) { } catch (RuntimeException e) {
recordAssessmentFailure(evaluation.getId(), e, rawResponse); aiTrainingEvaluationMapper.failAssessment(evaluation.getId(), cappedMessage(e), rawResponse, new Date());
throw asServiceException(e); throw asServiceException(e);
} }
} }
@ -225,26 +199,6 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
}); });
} }
/**
* AI 分数落库后立即重建本教学班的 stu_rank 当日快照(与每日凌晨定时重建同一套逻辑),
* 教师端成绩列表马上能看到新分数。异步执行且失败只记日志:排名刷新失败不能影响评分结果。
*/
private void refreshScoreRankAfterAssessment(JwtUser user, String teachingClassId) {
String schoolId = user == null ? null : user.getSchoolId();
if (scoreRankService == null || StringUtils.isAnyBlank(schoolId, teachingClassId)) {
return;
}
CompletableFuture.runAsync(() -> {
try {
if (!scoreRankService.doRankTeachingClass(schoolId, teachingClassId)) {
scoreRankService.doRankOne(schoolId);
}
} catch (RuntimeException e) {
log.warn("AI assessment score rank refresh failed, teachingClassId={}", teachingClassId, e);
}
});
}
private AiTrainingEvaluation getOrCreate(EvaluationContext context, JwtUser user) { private AiTrainingEvaluation getOrCreate(EvaluationContext context, JwtUser user) {
AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask( AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask(
user.getUserId(), context.teachingClassId, context.task.getId()); user.getUserId(), context.teachingClassId, context.task.getId());
@ -356,59 +310,6 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return json(node); return json(node);
} }
/**
* 第一步选择题的评分上下文:题干、选项文字、标准答案与学生作答逐题对照。
* 只用于 AI 助评——助学的提示词面向学生,带标准答案会泄题。
*/
private String choiceQuestionContext(EffectiveTrainingTaskContent content, StudentTrainingAnswer answer) {
ArrayNode result = objectMapper.createArrayNode();
JsonNode questions = parseJsonArray(content.getStepOneQuestions());
JsonNode choiceAnswers = parseChoiceAnswers(answer == null ? null : answer.getStep1Answer());
for (JsonNode question : questions) {
ObjectNode entry = result.addObject();
entry.put("title", StringUtils.abbreviate(question.path("title").asText(), MAX_CHOICE_TITLE_LENGTH));
ArrayNode options = entry.putArray("options");
for (JsonNode option : question.path("options")) {
ObjectNode item = options.addObject();
item.put("key", option.path("key").asText());
item.put("content", StringUtils.abbreviate(option.path("content").asText(), MAX_CHOICE_OPTION_LENGTH));
}
entry.put("correctAnswer", question.path("answer").asText());
entry.put("studentAnswer", choiceAnswers.path(question.path("id").asText()).asText(""));
}
return json(result);
}
private int choiceQuestionEntryCount(String choiceQuestionContext) {
try {
JsonNode node = objectMapper.readTree(StringUtils.trimToEmpty(choiceQuestionContext));
return node.isArray() ? node.size() : 0;
} catch (JsonProcessingException | RuntimeException e) {
return 0;
}
}
private JsonNode parseJsonArray(String value) {
try {
String normalized = StringUtils.trimToEmpty(value);
if (!normalized.startsWith("[")) return objectMapper.createArrayNode();
JsonNode node = objectMapper.readTree(normalized);
return node != null && node.isArray() ? node : objectMapper.createArrayNode();
} catch (JsonProcessingException | RuntimeException e) {
return objectMapper.createArrayNode();
}
}
private JsonNode parseChoiceAnswers(String step1Answer) {
try {
if (StringUtils.isBlank(step1Answer)) return objectMapper.createObjectNode();
JsonNode choiceAnswers = objectMapper.readTree(step1Answer).path("choiceAnswers");
return choiceAnswers.isObject() ? choiceAnswers : objectMapper.createObjectNode();
} catch (JsonProcessingException | RuntimeException e) {
return objectMapper.createObjectNode();
}
}
private String helpAnswerSnapshot(String question, String stepContext, StudentTrainingAnswer answer) { private String helpAnswerSnapshot(String question, String stepContext, StudentTrainingAnswer answer) {
ObjectNode node = objectMapper.createObjectNode(); ObjectNode node = objectMapper.createObjectNode();
node.put("studentQuestion", question); node.put("studentQuestion", question);
@ -452,22 +353,19 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
} }
requireText(criterion, "name"); requireText(criterion, "name");
requireText(criterion, "rationale"); requireText(criterion, "rationale");
Integer max = wholeNumber(criterion.get("maxScore"));
Integer earned = wholeNumber(criterion.get("score")); Integer earned = wholeNumber(criterion.get("score"));
// maxScore 以评分计划为准:模型偶发把 maxScore 抄错导致合计≠100, if (!expected.name.equals(StringUtils.trimToEmpty(criterion.path("name").asText()))
// 不再校验模型返回的 maxScore,只用计划值判定得分范围。 || max == null || earned == null || max != expected.maxScore || earned < 0 || earned > max) {
if (!normalizeCriterionName(expected.name).equals(normalizeCriterionName(criterion.path("name").asText()))
|| earned == null || earned < 0 || earned > expected.maxScore) {
throw invalidReport(); throw invalidReport();
} }
maxTotal += expected.maxScore; maxTotal += max;
earnedTotal += earned; earnedTotal += earned;
} }
// 模型偶发把总分写错(与逐项得分之和不一致):以逐项得分为准并回写报告, if (maxTotal != 100 || earnedTotal != score) {
// 避免仅因算术错误整份作废。 throw invalidReport();
if (earnedTotal != score) {
((ObjectNode) root).put("score", earnedTotal);
} }
return earnedTotal; return score;
} }
private List<AssessmentCriterionPlan> assessmentCriterionPlan(String steps) { private List<AssessmentCriterionPlan> assessmentCriterionPlan(String steps) {
@ -588,16 +486,6 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return new ServiceException(HttpStatus.BAD_GATEWAY, "AI returned an invalid evaluation report"); return new ServiceException(HttpStatus.BAD_GATEWAY, "AI returned an invalid evaluation report");
} }
/**
* 评项名比对前的归一化:去掉模型从提示词的编号列表里照搬回来的枚举前缀
* (如 "1."、"2、"、"(3)"、"Step 1"、"第2步")和所有空白。
* 没有编号+分隔符前缀的纯名字(如 "26电商2")不会被误删。
*/
private String normalizeCriterionName(String value) {
String name = StringUtils.trimToEmpty(value).replaceAll("\\s+", "");
return name.replaceFirst("^(?:(?i:step)\\d{0,3}[.::、))-]*|[((]?第\\d{1,3}步?[)).、::-]*|[((]?\\d{1,3}[)).、::-]+)", "");
}
private String helpSystemPrompt() { private String helpSystemPrompt() {
return "You are a training learning assistant. Return only JSON with overallDiagnosis, strengths, improvementAreas, and recommendedActions. Do not include a score."; return "You are a training learning assistant. Return only JSON with overallDiagnosis, strengths, improvementAreas, and recommendedActions. Do not include a score.";
} }
@ -620,11 +508,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
+ "Determine each criterion score from its evidence first, then sum the scores. Do not choose a target total, default score or invent differences between students. " + "Determine each criterion score from its evidence first, then sum the scores. Do not choose a target total, default score or invent differences between students. "
+ "Each rationale must identify the actual evidence or omission and explain the awarded points or deduction. " + "Each rationale must identify the actual evidence or omission and explain the awarded points or deduction. "
+ "There are exactly " + criterionPlan.size() + " training steps and their maximum scores sum to 100. " + "There are exactly " + criterionPlan.size() + " training steps and their maximum scores sum to 100. "
+ "Return exactly " + criterionPlan.size() + " criteria in this exact order. " + "Return exactly " + criterionPlan.size() + " criteria in this exact order. Each name and maxScore must exactly match this required step plan: "
+ "Each criterion's name must be copied verbatim from the quoted name in this required step plan, without adding any numbering or other characters; maxScore must exactly match: "
+ criterionPlanPrompt(criterionPlan) + ". " + criterionPlanPrompt(criterionPlan) + ". "
+ "The user prompt may include a choiceQuestions array: grade each pick strictly against its correctAnswer; "
+ "every wrong or unanswered pick must lose points within the first criterion's score, and that criterion's rationale must name the wrong question numbers. "
+ "score, maxScore, and each criterion score must be JSON integer numbers (not strings). " + "score, maxScore, and each criterion score must be JSON integer numbers (not strings). "
+ "Write one complete concise Chinese sentence for each rationale and end it with Chinese punctuation. " + "Write one complete concise Chinese sentence for each rationale and end it with Chinese punctuation. "
+ "The combined text length of all criteria.rationale values must not exceed 200 Chinese characters; target no more than " + "The combined text length of all criteria.rationale values must not exceed 200 Chinese characters; target no more than "
@ -637,17 +522,14 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
for (int index = 0; index < criterionPlan.size(); index++) { for (int index = 0; index < criterionPlan.size(); index++) {
AssessmentCriterionPlan item = criterionPlan.get(index); AssessmentCriterionPlan item = criterionPlan.get(index);
if (index > 0) result.append(", "); if (index > 0) result.append(", ");
result.append(index + 1).append(". \"").append(item.name).append("\" (max ").append(item.maxScore).append(")"); result.append(index + 1).append(". ").append(item.name).append(" (max ").append(item.maxScore).append(")");
} }
return result.append("]").toString(); return result.append("]").toString();
} }
private String assessmentUserPrompt(String taskSnapshot, String answerSnapshot, String knowledgeContext, private String assessmentUserPrompt(String taskSnapshot, String answerSnapshot, String knowledgeContext) {
String choiceQuestionContext) {
return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot
+ "\nResolved case material and this student's uploaded-file excerpts: " + knowledgeContext + "\nResolved case material and this student's uploaded-file excerpts: " + knowledgeContext
+ "\nchoiceQuestions (step-one single-choice questions): " + choiceQuestionContext
+ " Each entry pairs a question's title and options with its correctAnswer and this student's studentAnswer; an empty studentAnswer means the question was not answered."
+ "\nIf the answer snapshot contains a server-generated calculationCheck object, treat it as verified calculation evidence. " + "\nIf the answer snapshot contains a server-generated calculationCheck object, treat it as verified calculation evidence. "
+ "When calculationCorrect is false, deduct points for the reported incorrectItemCount rather than treating the calculation as correct."; + "When calculationCorrect is false, deduct points for the reported incorrectItemCount rather than treating the calculation as correct.";
} }
@ -666,57 +548,16 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return view; return view;
} }
/** private String cappedMessage(RuntimeException exception) {
* 落库的失败原因:只保存我们自己抛出的 ServiceException 文案, String message = StringUtils.defaultIfBlank(exception.getMessage(), "AI evaluation failed");
* 其余异常(数据库、网络等)的原文只写日志,避免把 SQL 等长文本存进库或带给前端。 return StringUtils.left(message, MAX_ERROR_LENGTH);
*/
private String storedFailureMessage(RuntimeException exception) {
if (exception instanceof ServiceException && StringUtils.isNotBlank(exception.getMessage())) {
return StringUtils.left(exception.getMessage(), MAX_ERROR_LENGTH);
}
return FAILURE_MESSAGE;
}
/**
* 记录失败状态本身出错时不能再往外抛:否则会盖掉真正的失败原因,
* 并让记录一直停在 PROCESSING,学生无法重试。
*/
private void recordHelpFailure(String evaluationId, RuntimeException cause) {
log.warn("AI help failed, evaluationId={}", evaluationId, cause);
try {
aiTrainingEvaluationMapper.failHelp(evaluationId, storedFailureMessage(cause), new Date());
} catch (RuntimeException recordError) {
log.error("Could not record AI help failure, evaluationId={}", evaluationId, recordError);
}
}
private void recordAssessmentFailure(String evaluationId, RuntimeException cause, String rawResponse) {
log.warn("AI assessment failed, evaluationId={}", evaluationId, cause);
String message = storedFailureMessage(cause);
try {
aiTrainingEvaluationMapper.failAssessment(evaluationId, message, rawResponse, new Date());
return;
} catch (RuntimeException recordError) {
log.warn("Could not record AI assessment failure with the raw response, retrying without it, evaluationId={}, rawResponse={}",
evaluationId, StringUtils.left(rawResponse, LOGGED_RAW_RESPONSE_LENGTH), recordError);
}
try {
aiTrainingEvaluationMapper.failAssessment(evaluationId, message, null, new Date());
} catch (RuntimeException recordError) {
log.error("Could not record AI assessment failure, evaluationId={}", evaluationId, recordError);
}
}
private boolean isProcessingTimedOut(AiTrainingEvaluation evaluation) {
Date updatedAt = evaluation.getUpdateTime();
return updatedAt != null && System.currentTimeMillis() - updatedAt.getTime() > PROCESSING_TIMEOUT_MILLIS;
} }
private ServiceException asServiceException(RuntimeException exception) { private ServiceException asServiceException(RuntimeException exception) {
HttpStatus status = exception instanceof ServiceException && ((ServiceException) exception).getCode() != null if (exception instanceof ServiceException) {
? ((ServiceException) exception).getCode() return (ServiceException) exception;
: HttpStatus.BAD_GATEWAY; }
return new ServiceException(status, FAILURE_MESSAGE); return new ServiceException(HttpStatus.BAD_GATEWAY, "AI evaluation failed");
} }
private static class AssessmentCriterionPlan { private static class AssessmentCriterionPlan {

@ -21,8 +21,7 @@ public class ImportServiceImpl implements ImportService {
@Autowired @Autowired
UserinfoMapper userInfoMapper; UserinfoMapper userInfoMapper;
@Override @Override
public void processTeacherImport(List<TeacherImportDTO> teacherList, Userinfo operator) { public void processTeacherImport(List<TeacherImportDTO> teacherList) {
// 教师的学校/院系/专业直接继承当前操作用户
for (int i = 0; i < teacherList.size(); i++) { for (int i = 0; i < teacherList.size(); i++) {
TeacherImportDTO teacherImportDTO = teacherList.get(i); TeacherImportDTO teacherImportDTO = teacherList.get(i);
Userinfo userInfo = new Userinfo(); Userinfo userInfo = new Userinfo();
@ -35,9 +34,9 @@ public class ImportServiceImpl implements ImportService {
userInfo.setCreateTime(new Date()); userInfo.setCreateTime(new Date());
userInfo.setRole(3); userInfo.setRole(3);
userInfo.setTeacherAdmin(false); userInfo.setTeacherAdmin(false);
userInfo.setSchoolId(operator == null ? null : operator.getSchoolId()); userInfo.setSchoolId(schoolService.selectSchoolIdBySchoolName(teacherImportDTO.getSchoolName()));
userInfo.setSchoolFacultyId(operator == null ? null : operator.getSchoolFacultyId()); userInfo.setSchoolFacultyId(schoolService.selectFacultyIdBySchoolIdAndFacultyName(userInfo.getSchoolId(),teacherImportDTO.getSchoolFacultyName()));
userInfo.setSchoolMajorId(operator == null ? null : operator.getSchoolMajorId()); userInfo.setSchoolMajorId(schoolService.selectMajorIdByFacultyIdAndMajorName(userInfo.getSchoolFacultyId(),teacherImportDTO.getSchoolMajorName()));
userInfo.setSchoolClassId(null); userInfo.setSchoolClassId(null);
userInfo.setCodeFrom("导入"); userInfo.setCodeFrom("导入");
userInfoMapper.insert(userInfo); userInfoMapper.insert(userInfo);

@ -74,10 +74,6 @@ public class SchoolServiceImpl implements SchoolService {
} }
List<SchoolFaculty> schoolFaculties = schoolFacultyMapper.selectByExample(schoolFacultyExample); List<SchoolFaculty> schoolFaculties = schoolFacultyMapper.selectByExample(schoolFacultyExample);
// 基于分页查询结果读取真实总数/页数,映射出的 DTO 列表最后再替换进去;
// 直接包装 DTO 列表会把 total 错算成当页条数
PageInfo<SchoolFaculty> sourcePage = new PageInfo<>(schoolFaculties);
// 将院系信息转换为 DTO // 将院系信息转换为 DTO
List<SchoolFacultyDto> dtoList = schoolFaculties.stream() List<SchoolFacultyDto> dtoList = schoolFaculties.stream()
.map(schoolFaculty -> { .map(schoolFaculty -> {
@ -90,13 +86,8 @@ public class SchoolServiceImpl implements SchoolService {
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
PageInfo<SchoolFacultyDto> pageInfo = new PageInfo<>(); // 创建 PageInfo 对象,将 DTO 列表封装成分页结果
pageInfo.setTotal(sourcePage.getTotal()); return new PageInfo<>(dtoList);
pageInfo.setPageNum(sourcePage.getPageNum());
pageInfo.setPageSize(sourcePage.getPageSize());
pageInfo.setPages(sourcePage.getPages());
pageInfo.setList(dtoList);
return pageInfo;
} }
@Override @Override
@ -140,10 +131,6 @@ public class SchoolServiceImpl implements SchoolService {
} }
List<SchoolClass> schoolClasses = schoolClassMapper.selectByExample(schoolClassExample); List<SchoolClass> schoolClasses = schoolClassMapper.selectByExample(schoolClassExample);
// 基于分页查询结果读取真实总数/页数,映射出的 DTO 列表最后再替换进去;
// 直接包装 DTO 列表会把 total 错算成当页条数
PageInfo<SchoolClass> sourcePage = new PageInfo<>(schoolClasses);
// 将院系信息转换为 DTO // 将院系信息转换为 DTO
String schoolName = schoolMapper.selectSchoolNameById(schoolId); String schoolName = schoolMapper.selectSchoolNameById(schoolId);
List<String> schoolMajorIds = schoolClasses.stream() List<String> schoolMajorIds = schoolClasses.stream()
@ -214,13 +201,8 @@ public class SchoolServiceImpl implements SchoolService {
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
PageInfo<SchoolClassDto> pageInfo = new PageInfo<>(); // 创建 PageInfo 对象,将 DTO 列表封装成分页结果
pageInfo.setTotal(sourcePage.getTotal()); return new PageInfo<>(dtoList);
pageInfo.setPageNum(sourcePage.getPageNum());
pageInfo.setPageSize(sourcePage.getPageSize());
pageInfo.setPages(sourcePage.getPages());
pageInfo.setList(dtoList);
return pageInfo;
} }
@Override @Override

@ -18,25 +18,11 @@ import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
@Service @Service
public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabaseService { public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabaseService {
/** BI 数据源管理中的平台内置表,顺序与用户提供的文件夹顺序一致。 */
private static final List<String> BUILT_IN_TABLE_NAMES = Collections.unmodifiableList(Arrays.asList(
"三大品类选品数据",
"三大品类用户评价数据",
"用户RFM数据",
"销量预测数据",
"帕累托ABC分析",
"智能眼镜渠道运营数据",
"智能眼镜用户评论数据",
"智能健身镜产品数据",
"智能美妆镜产品数据",
"智能卫浴镜产品数据",
"用户评论数据"
));
@Autowired @Autowired
private StuSpendingLevelMapper stuSpendingLevelMapper; private StuSpendingLevelMapper stuSpendingLevelMapper;
@ -62,7 +48,25 @@ public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabase
@Override @Override
public ResultEntity<StuUserProfileDto> getBaseInfo(String userId) { public ResultEntity<StuUserProfileDto> getBaseInfo(String userId) {
List<String> list = new ArrayList<>(BUILT_IN_TABLE_NAMES); List<String> list = new ArrayList<>();
list.add("客户细分数据表");
list.add("销售预测数据表");
list.add("购物车数据表");
list.add("商品评论数据");
list.add("RFM明细数据");
list.add("AARRR传播数据");
list.add("超市销售数据");
list.add("集团商品销售总表");
list.add("用户登录表");
list.add("用户留存分析");
list.add("用户粘性分析");
list.add("AARRR示例数据");
list.add("帕累托ABC分析");
list.add("智能健身镜产品数据");
list.add("智能卫浴镜产品数据");
list.add("智能美妆镜产品数据");
//用户属性表 //用户属性表
StuSpendingLevelExample userAttributeExample = new StuSpendingLevelExample(); StuSpendingLevelExample userAttributeExample = new StuSpendingLevelExample();
@ -172,7 +176,7 @@ public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabase
//用户行为表 //用户行为表
StuUserCommentExample userAttributeBehaviorExample = new StuUserCommentExample(); StuUserCommentExample userAttributeBehaviorExample = new StuUserCommentExample();
userAttributeBehaviorExample.setOrderByClause("id asc"); userAttributeBehaviorExample.setOrderByClause("id asc");
List<StuUserComment> userAttributeBehaviorList = stuUserCommentMapper.selectByExampleWithBLOBs(userAttributeBehaviorExample); List<StuUserComment> userAttributeBehaviorList = stuUserCommentMapper.selectByExample(userAttributeBehaviorExample);
if (userAttributeBehaviorList.isEmpty()) { if (userAttributeBehaviorList.isEmpty()) {
throw new ServiceException(HttpStatus.OK, "未查询到用户数据!"); throw new ServiceException(HttpStatus.OK, "未查询到用户数据!");
} }
@ -182,7 +186,10 @@ public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabase
} }
if (BUILT_IN_TABLE_NAMES.contains(tableName)) String[] list = {"RFM明细数据","AARRR传播数据","超市销售数据","集团商品销售总表","用户登录表","用户留存分析","用户粘性分析","AARRR示例数据",
"帕累托ABC分析","智能健身镜产品数据","智能卫浴镜产品数据","智能美妆镜产品数据"};
List<String> collect = Arrays.stream(list).collect(Collectors.toList());
if (collect.contains(tableName))
{ {
//开启分页 //开启分页
PageHelper.startPage(index, size); PageHelper.startPage(index, size);

@ -38,7 +38,25 @@ public class ValidateServiceImpl implements ValidateService {
TeacherImportDTO teacher = teacherList.get(i); TeacherImportDTO teacher = teacherList.get(i);
int rowNum = i + 2; // Excel行号(标题行+1) int rowNum = i + 2; // Excel行号(标题行+1)
// 1. 校验教师姓名(教师归属的学校/院系/专业直接取自当前操作用户,不再从Excel读取) // 1. 校验院校名称
if (StringUtils.isBlank(teacher.getSchoolName())) {
errors.add("第" + rowNum + "行: 院校名称不能为空");
}
// 2. 校验院系名称
if (StringUtils.isBlank(teacher.getSchoolFacultyName())) {
errors.add("第" + rowNum + "行: 院系名称不能为空");
}
// 3. 校验专业名称
if (StringUtils.isBlank(teacher.getSchoolMajorName())) {
errors.add("第" + rowNum + "行: 专业名称不能为空");
} else if (!schoolService.existsBySchoolMajorName(teacher.getSchoolName(),teacher.getSchoolMajorName(), teacher.getSchoolFacultyName())) {
errors.add("第" + rowNum + "行: 专业'" + teacher.getSchoolMajorName() +
"'在院系'" + teacher.getSchoolFacultyName() + "'下不存在");
}
// 4. 校验教师姓名
if (StringUtils.isBlank(teacher.getTeacherName())) { if (StringUtils.isBlank(teacher.getTeacherName())) {
errors.add("第" + rowNum + "行: 教师姓名不能为空"); errors.add("第" + rowNum + "行: 教师姓名不能为空");
} }

@ -15,11 +15,10 @@ public class EffectiveTrainingTaskContent {
private final String steps; private final String steps;
private final String materialName; private final String materialName;
private final String materialUrl; private final String materialUrl;
private final String stepOneQuestions;
private EffectiveTrainingTaskContent(String scope, String effectiveConfigId, String knowledge, String background, private EffectiveTrainingTaskContent(String scope, String effectiveConfigId, String knowledge, String background,
String objectives, String requirements, String steps, String materialName, String objectives, String requirements, String steps, String materialName,
String materialUrl, String stepOneQuestions) { String materialUrl) {
this.scope = scope; this.scope = scope;
this.effectiveConfigId = effectiveConfigId; this.effectiveConfigId = effectiveConfigId;
this.knowledge = knowledge; this.knowledge = knowledge;
@ -29,7 +28,6 @@ public class EffectiveTrainingTaskContent {
this.steps = steps; this.steps = steps;
this.materialName = materialName; this.materialName = materialName;
this.materialUrl = materialUrl; this.materialUrl = materialUrl;
this.stepOneQuestions = stepOneQuestions;
} }
public static EffectiveTrainingTaskContent resolve(TrainingTask task, TrainingTaskClassConfig classConfig) { public static EffectiveTrainingTaskContent resolve(TrainingTask task, TrainingTaskClassConfig classConfig) {
@ -44,8 +42,7 @@ public class EffectiveTrainingTaskContent {
value(useTeacher ? classConfig.getRequirements() : null, task.getRequirements()), value(useTeacher ? classConfig.getRequirements() : null, task.getRequirements()),
value(useTeacher ? classConfig.getSteps() : null, task.getSteps()), value(useTeacher ? classConfig.getSteps() : null, task.getSteps()),
value(useTeacher ? classConfig.getMaterialName() : null, task.getMaterialName()), value(useTeacher ? classConfig.getMaterialName() : null, task.getMaterialName()),
value(useTeacher ? classConfig.getMaterialUrl() : null, task.getMaterialUrl()), value(useTeacher ? classConfig.getMaterialUrl() : null, task.getMaterialUrl()));
value(useTeacher ? classConfig.getStepOneQuestions() : null, task.getStepOneQuestions()));
} }
private static String value(String preferred, String fallback) { private static String value(String preferred, String fallback) {
@ -61,5 +58,4 @@ public class EffectiveTrainingTaskContent {
public String getSteps() { return steps; } public String getSteps() { return steps; }
public String getMaterialName() { return materialName; } public String getMaterialName() { return materialName; }
public String getMaterialUrl() { return materialUrl; } public String getMaterialUrl() { return materialUrl; }
public String getStepOneQuestions() { return stepOneQuestions; }
} }

@ -63,9 +63,6 @@ spring:
validation-query: SELECT 1 validation-query: SELECT 1
validation-query-timeout: 3 validation-query-timeout: 3
keep-alive: true keep-alive: true
# 固定驱动端编码:这里的参数优先于连接串,避免生产环境私有配置里的连接串
# 缺少或写错 characterEncoding 时,驱动按平台默认编码(GBK)发送中文
connection-properties: characterEncoding=utf8;connectionCollation=utf8mb4_unicode_ci
connection-init-sqls: connection-init-sqls:
- SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci - SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci
# 配置监控统计 # 配置监控统计

@ -61,16 +61,14 @@
UPDATE ai_training_evaluation UPDATE ai_training_evaluation
SET help_status = 'PROCESSING', help_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP} SET help_status = 'PROCESSING', help_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP}
WHERE id = #{id,jdbcType=VARCHAR} WHERE id = #{id,jdbcType=VARCHAR}
AND (help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED') AND help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED')
OR (help_status = 'PROCESSING' AND update_time &lt; DATE_SUB(#{updateTime,jdbcType=TIMESTAMP}, INTERVAL 10 MINUTE)))
</update> </update>
<update id="claimAssessment"> <update id="claimAssessment">
UPDATE ai_training_evaluation UPDATE ai_training_evaluation
SET assessment_status = 'PROCESSING', assessment_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP} SET assessment_status = 'PROCESSING', assessment_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP}
WHERE id = #{id,jdbcType=VARCHAR} WHERE id = #{id,jdbcType=VARCHAR}
AND (assessment_status IN ('NOT_STARTED', 'FAILED') AND assessment_status IN ('NOT_STARTED', 'FAILED')
OR (assessment_status = 'PROCESSING' AND update_time &lt; DATE_SUB(#{updateTime,jdbcType=TIMESTAMP}, INTERVAL 10 MINUTE)))
</update> </update>
<update id="claimReassessment"> <update id="claimReassessment">
@ -78,6 +76,7 @@
SET assessment_status = 'PROCESSING', assessment_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP} SET assessment_status = 'PROCESSING', assessment_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP}
WHERE id = #{id,jdbcType=VARCHAR} WHERE id = #{id,jdbcType=VARCHAR}
AND assessment_status = 'SUCCEEDED' AND assessment_status = 'SUCCEEDED'
AND (BINARY assessment_answer_snapshot &lt;=&gt; BINARY #{expectedAnswerSnapshot,jdbcType=LONGVARCHAR})
</update> </update>
<update id="completeHelp" parameterType="com.sztzjy.linkCommerce.entity.AiTrainingEvaluation"> <update id="completeHelp" parameterType="com.sztzjy.linkCommerce.entity.AiTrainingEvaluation">

@ -1,75 +0,0 @@
package com.sztzjy.linkCommerce.config;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class DatabaseCharsetCheckerTest {
@Test
void reportsNothingWhenConnectionAndColumnAreUtf8mb4() {
DatabaseCharsetChecker checker = checker("utf8mb4", "utf8mb4", Collections.singletonList("utf8mb4"));
assertTrue(checker.findProblems().isEmpty());
}
@Test
void reportsConnectionCharsetThatIsNotUtf8mb4() {
DatabaseCharsetChecker checker = checker("gbk", "gbk", Collections.singletonList("utf8mb4"));
List<String> problems = checker.findProblems();
assertEquals(2, problems.size());
assertTrue(problems.get(0).contains("character_set_client=gbk"));
assertTrue(problems.get(1).contains("character_set_connection=gbk"));
}
@Test
void reportsRawResponseColumnThatIsNotUtf8mb4() {
DatabaseCharsetChecker checker = checker("utf8mb4", "utf8mb4", Collections.singletonList("latin1"));
List<String> problems = checker.findProblems();
assertEquals(1, problems.size());
assertTrue(problems.get(0).contains("assessment_raw_response"));
assertTrue(problems.get(0).contains("latin1"));
}
@Test
void skipsColumnCheckWhenTableDoesNotExistYet() {
DatabaseCharsetChecker checker = checker("utf8mb4", "utf8mb4", Collections.<String>emptyList());
assertTrue(checker.findProblems().isEmpty());
}
@Test
void neverBlocksStartupWhenDatabaseIsUnavailable() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.queryForMap(anyString())).thenThrow(new DataAccessResourceFailureException("db down"));
assertDoesNotThrow(() -> new DatabaseCharsetChecker(jdbcTemplate).check());
}
private static DatabaseCharsetChecker checker(String client, String connection, List<String> columnCharsets) {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
Map<String, Object> session = new HashMap<String, Object>();
session.put("client", client);
session.put("conn", connection);
when(jdbcTemplate.queryForMap(anyString())).thenReturn(session);
when(jdbcTemplate.queryForList(anyString(), org.mockito.ArgumentMatchers.<Class<String>>any()))
.thenReturn(columnCharsets);
return new DatabaseCharsetChecker(jdbcTemplate);
}
}

@ -1,59 +0,0 @@
package com.sztzjy.linkCommerce.config.exception.handler;
import com.sztzjy.linkCommerce.util.ResultEntity;
import org.apache.ibatis.exceptions.PersistenceException;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.BadSqlGrammarException;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class GlobalExceptionHandlerTest {
private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
@Test
void databaseExceptionsReturnShortMessageWithoutSql() {
PersistenceException e = new PersistenceException(
"\n### Error updating database. Cause: java.sql.SQLException: Incorrect string value"
+ "\n### SQL: UPDATE ai_training_evaluation SET assessment_raw_response = ?");
ResultEntity<String> result = handler.nullPointerException(e);
assertShortAndClean(result.getBody().getMsg());
}
@Test
void springJdbcExceptionsReturnShortMessageWithoutSql() {
BadSqlGrammarException e = new BadSqlGrammarException("PreparedStatementCallback",
"select p.question from platform_question p", new SQLException("syntax error near 'x'"));
ResultEntity<String> result = handler.nullPointerException(e);
assertShortAndClean(result.getBody().getMsg());
}
@Test
void wrappedSqlExceptionIsAlsoShortened() {
RuntimeException e = new RuntimeException("### Could not execute: select * from ai_training_evaluation",
new SQLException("Incorrect string value: '\\xC9'"));
ResultEntity<String> result = handler.nullPointerException(e);
assertShortAndClean(result.getBody().getMsg());
}
@Test
void ordinaryBusinessMessagesAreUnchanged() {
ResultEntity<String> result = handler.nullPointerException(new RuntimeException("考试不存在或无权访问"));
assertEquals("考试不存在或无权访问", result.getBody().getMsg());
}
private static void assertShortAndClean(String message) {
assertTrue(message != null && message.length() <= 30, "message: " + message);
assertTrue(!message.contains("###") && !message.contains("SQL") && !message.contains("select "),
"internal detail leaked: " + message);
}
}

@ -1304,12 +1304,14 @@ class UserControllerTeacherAdminTest {
adminClass.setClassType("ADMIN"); adminClass.setClassType("ADMIN");
adminClass.setCreatedBy("teacher-1"); adminClass.setCreatedBy("teacher-1");
when(controller.userinfoMapper.selectByPrimaryKey("teacher-1")).thenReturn(teacher("teacher-1", true)); when(controller.userinfoMapper.selectByPrimaryKey("teacher-1")).thenReturn(teacher("teacher-1", true));
when(controller.schoolFacultyMapper.selectByExample(any())).thenReturn(Collections.singletonList(faculty("faculty-1", "院系")));
when(controller.schoolMajorMapper.selectByExample(any())).thenReturn(Collections.singletonList(major("major-1", "faculty-1", "专业")));
when(controller.schoolClassMapper.selectByExample(any())).thenReturn(Collections.singletonList(adminClass)); when(controller.schoolClassMapper.selectByExample(any())).thenReturn(Collections.singletonList(adminClass));
when(controller.userInfoService.existsByUserName("20260001")).thenReturn(false); when(controller.userInfoService.existsByUserName("20260001")).thenReturn(false);
when(controller.userinfoMapper.insertSelective(any(Userinfo.class))).thenReturn(1); when(controller.userinfoMapper.insertSelective(any(Userinfo.class))).thenReturn(1);
ResultEntity result = controller.importTeacherStudentRows( ResultEntity result = controller.importTeacherStudentRows(
Collections.singletonList(studentAccountRow("张三", "20260001", "男", "行政一班")), Collections.singletonList(studentAccountRow("张三", "20260001", "男", "院系", "专业", "行政一班")),
"teacher-1"); "teacher-1");
ArgumentCaptor<Userinfo> captor = ArgumentCaptor.forClass(Userinfo.class); ArgumentCaptor<Userinfo> captor = ArgumentCaptor.forClass(Userinfo.class);
@ -1333,12 +1335,14 @@ class UserControllerTeacherAdminTest {
adminClass.setClassType("ADMIN"); adminClass.setClassType("ADMIN");
adminClass.setCreatedBy("teacher-2"); adminClass.setCreatedBy("teacher-2");
when(controller.userinfoMapper.selectByPrimaryKey("teacher-1")).thenReturn(teacher("teacher-1", true)); when(controller.userinfoMapper.selectByPrimaryKey("teacher-1")).thenReturn(teacher("teacher-1", true));
when(controller.schoolFacultyMapper.selectByExample(any())).thenReturn(Collections.singletonList(faculty("faculty-1", "院系")));
when(controller.schoolMajorMapper.selectByExample(any())).thenReturn(Collections.singletonList(major("major-1", "faculty-1", "专业")));
when(controller.schoolClassMapper.selectByExample(any())).thenReturn(Collections.singletonList(adminClass)); when(controller.schoolClassMapper.selectByExample(any())).thenReturn(Collections.singletonList(adminClass));
when(controller.userInfoService.existsByUserName("20260001")).thenReturn(false); when(controller.userInfoService.existsByUserName("20260001")).thenReturn(false);
when(controller.userinfoMapper.insertSelective(any(Userinfo.class))).thenReturn(1); when(controller.userinfoMapper.insertSelective(any(Userinfo.class))).thenReturn(1);
ResultEntity result = controller.importTeacherStudentRows( ResultEntity result = controller.importTeacherStudentRows(
Collections.singletonList(studentAccountRow("张三", "20260001", "男", "行政一班")), Collections.singletonList(studentAccountRow("张三", "20260001", "男", "院系", "专业", "行政一班")),
"teacher-1"); "teacher-1");
assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(HttpStatus.OK, result.getStatusCode());
@ -1460,11 +1464,15 @@ class UserControllerTeacherAdminTest {
private SchoolAdminStudentImportDTO studentAccountRow(String name, private SchoolAdminStudentImportDTO studentAccountRow(String name,
String username, String username,
String sex, String sex,
String facultyName,
String majorName,
String className) { String className) {
SchoolAdminStudentImportDTO row = new SchoolAdminStudentImportDTO(); SchoolAdminStudentImportDTO row = new SchoolAdminStudentImportDTO();
row.setName(name); row.setName(name);
row.setUsername(username); row.setUsername(username);
row.setSex(sex); row.setSex(sex);
row.setSchoolFacultyName(facultyName);
row.setSchoolMajorName(majorName);
row.setSchoolClassName(className); row.setSchoolClassName(className);
row.setPhone("13800000000"); row.setPhone("13800000000");
row.setEmail("student@example.com"); row.setEmail("student@example.com");

@ -501,124 +501,6 @@ class AiTrainingEvaluationServiceImplTest {
verify(service.qwenChatClient, never()).completeJson(any(), any()); verify(service.qwenChatClient, never()).completeJson(any(), any());
} }
@Test
void recentProcessingAssessmentIsNotReclaimed() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "PROCESSING");
evaluation.setUpdateTime(minutesAgo(1));
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation);
AiTrainingEvaluationView view = service.generateAssessment("new-product-survey", student());
assertEquals("PROCESSING", view.getAssessmentStatus());
verify(service.aiTrainingEvaluationMapper, never()).claimAssessment(any(), any(Date.class));
verify(service.qwenChatClient, never()).completeJson(any(), any());
}
@Test
void staleProcessingAssessmentIsReclaimedSoStudentIsNotStuckForever() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "PROCESSING");
evaluation.setUpdateTime(minutesAgo(11));
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation);
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(assessmentJson());
when(service.aiTrainingEvaluationMapper.completeAssessment(any(AiTrainingEvaluation.class))).thenReturn(1);
when(service.studentTrainingAnswerMapper.updateAiAssessmentScore(eq("answer-1"), eq(86), any(Date.class))).thenReturn(1);
AiTrainingEvaluationView view = service.generateAssessment("new-product-survey", student());
assertEquals(Integer.valueOf(86), view.getAssessmentScore());
verify(service.aiTrainingEvaluationMapper).claimAssessment(eq("evaluation-1"), any(Date.class));
}
@Test
void failureRecordingErrorFallsBackToShortRecordAndKeepsStudentRetryable() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
String invalidResponse = assessmentJson().replace("\"maxScore\":25", "\"maxScore\":24");
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
.thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(invalidResponse);
when(service.aiTrainingEvaluationMapper.failAssessment(eq("evaluation-1"), any(), eq(invalidResponse), any(Date.class)))
.thenThrow(new org.apache.ibatis.exceptions.PersistenceException(longDatabaseMessage()));
when(service.aiTrainingEvaluationMapper.failAssessment(eq("evaluation-1"), any(), isNull(), any(Date.class))).thenReturn(1);
ServiceException thrown = assertThrows(ServiceException.class,
() -> service.generateAssessment("new-product-survey", student()));
assertShortFriendlyMessage(thrown.getMessage());
verify(service.aiTrainingEvaluationMapper).failAssessment(eq("evaluation-1"), any(), isNull(), any(Date.class));
}
@Test
void failureRecordingBothAttemptsFailingStillReturnsShortError() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
.thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenThrow(new IllegalStateException("upstream"));
when(service.aiTrainingEvaluationMapper.failAssessment(any(), any(), any(), any(Date.class)))
.thenThrow(new org.apache.ibatis.exceptions.PersistenceException(longDatabaseMessage()));
ServiceException thrown = assertThrows(ServiceException.class,
() -> service.generateAssessment("new-product-survey", student()));
assertShortFriendlyMessage(thrown.getMessage());
}
@Test
void helpFailureRecordingErrorStillReturnsShortError() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
.thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenThrow(new IllegalStateException("upstream"));
when(service.aiTrainingEvaluationMapper.failHelp(any(), any(), any(Date.class)))
.thenThrow(new org.apache.ibatis.exceptions.PersistenceException(longDatabaseMessage()));
ServiceException thrown = assertThrows(ServiceException.class,
() -> service.generateHelp("new-product-survey", student(), "如何完善调研证据?"));
assertShortFriendlyMessage(thrown.getMessage());
}
@Test
void storedFailureMessageNeverContainsInternalExceptionText() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
.thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenThrow(new IllegalStateException(longDatabaseMessage()));
when(service.aiTrainingEvaluationMapper.failAssessment(any(), any(), any(), any(Date.class))).thenReturn(1);
assertThrows(ServiceException.class, () -> service.generateAssessment("new-product-survey", student()));
ArgumentCaptor<String> stored = ArgumentCaptor.forClass(String.class);
verify(service.aiTrainingEvaluationMapper).failAssessment(eq("evaluation-1"), stored.capture(), any(), any(Date.class));
assertShortFriendlyMessage(stored.getValue());
}
private static String longDatabaseMessage() {
return "\n### Error updating database. Cause: java.sql.SQLException: Incorrect string value"
+ "\n### SQL: UPDATE ai_training_evaluation SET assessment_raw_response = ? WHERE id = ?";
}
private static void assertShortFriendlyMessage(String message) {
assertNotNull(message);
assertTrue(message.length() <= 60, "message too long: " + message);
assertTrue(!message.contains("###") && !message.contains("SQL") && !message.contains("Exception"),
"internal detail leaked: " + message);
}
private static Date minutesAgo(int minutes) {
return new Date(System.currentTimeMillis() - minutes * 60_000L);
}
@Test @Test
void rejectsNonStudentJwtUser() { void rejectsNonStudentJwtUser() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));

@ -3,65 +3,37 @@ package com.sztzjy.linkCommerce.service.impl;
import com.sztzjy.linkCommerce.entity.Userinfo; import com.sztzjy.linkCommerce.entity.Userinfo;
import com.sztzjy.linkCommerce.entity.importDto.TeacherImportDTO; import com.sztzjy.linkCommerce.entity.importDto.TeacherImportDTO;
import com.sztzjy.linkCommerce.mapper.UserinfoMapper; import com.sztzjy.linkCommerce.mapper.UserinfoMapper;
import com.sztzjy.linkCommerce.service.SchoolService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import java.util.Collections; import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class ImportServiceImplTest { class ImportServiceImplTest {
@Test @Test
void teacherImportCreatesOrdinaryTeacher() { void teacherImportCreatesOrdinaryTeacher() {
ImportServiceImpl service = new ImportServiceImpl(); ImportServiceImpl service = new ImportServiceImpl();
service.schoolService = mock(SchoolService.class);
service.userInfoMapper = mock(UserinfoMapper.class); service.userInfoMapper = mock(UserinfoMapper.class);
when(service.schoolService.selectSchoolIdBySchoolName(any())).thenReturn("school-1");
// 操作用户的学校/院系/专业应直接继承给导入的教师 when(service.schoolService.selectFacultyIdBySchoolIdAndFacultyName(any(), any())).thenReturn("faculty-1");
Userinfo operator = new Userinfo(); when(service.schoolService.selectMajorIdByFacultyIdAndMajorName(any(), any())).thenReturn("major-1");
operator.setUserId("operator-1");
operator.setSchoolId("school-1");
operator.setSchoolFacultyId("faculty-1");
operator.setSchoolMajorId("major-1");
TeacherImportDTO row = new TeacherImportDTO(); TeacherImportDTO row = new TeacherImportDTO();
row.setTeacherName("张老师"); row.setTeacherName("张老师");
row.setUserName("t001"); row.setUserName("t001");
service.processTeacherImport(Collections.singletonList(row), operator); service.processTeacherImport(Collections.singletonList(row));
ArgumentCaptor<Userinfo> captor = ArgumentCaptor.forClass(Userinfo.class); ArgumentCaptor<Userinfo> captor = ArgumentCaptor.forClass(Userinfo.class);
verify(service.userInfoMapper).insert(captor.capture()); verify(service.userInfoMapper).insert(captor.capture());
assertFalse(captor.getValue().getTeacherAdmin()); assertFalse(captor.getValue().getTeacherAdmin());
assertEquals("school-1", captor.getValue().getSchoolId());
assertEquals("faculty-1", captor.getValue().getSchoolFacultyId());
assertEquals("major-1", captor.getValue().getSchoolMajorId());
}
@Test
void teacherImportWithoutOperatorOrganizationLeavesFieldsEmpty() {
ImportServiceImpl service = new ImportServiceImpl();
service.userInfoMapper = mock(UserinfoMapper.class);
Userinfo operator = new Userinfo();
operator.setUserId("operator-2");
operator.setSchoolId("school-1");
TeacherImportDTO row = new TeacherImportDTO();
row.setTeacherName("李老师");
row.setUserName("t002");
service.processTeacherImport(Collections.singletonList(row), operator);
ArgumentCaptor<Userinfo> captor = ArgumentCaptor.forClass(Userinfo.class);
verify(service.userInfoMapper).insert(captor.capture());
assertEquals("school-1", captor.getValue().getSchoolId());
assertNull(captor.getValue().getSchoolFacultyId());
assertNull(captor.getValue().getSchoolMajorId());
} }
} }

Loading…
Cancel
Save