Compare commits

..

20 Commits

Author SHA1 Message Date
chenyuan 7f1a3a48f8 fix: 参考答案转换PDF设置文档标题为reference-answer
- Blob URL 预览时浏览器会读取 PDF 文档标题,
  清除 Word 原始二进制标题,避免显示成 yAAAA... 之类的乱码
3 hours ago
chenyuan 9e4decf8cc fix: word转PDF内置宋体资源并显式配置aspose字体源
- 内置 simsun.ttf 资源,首次转换解压到临时字体目录,
  与系统字体目录一起交给 aspose,服务器无中文字体也能正常转换
- 字体配置线程安全(双重检查),按文档设置字体设置
- 学生/教师参考答案预览链路适配
4 hours ago
chenyuan 2ca158d330 fix: word转PDF字体目录补充Windows系统字体目录
- setFontsFolders 会替换 aspose 的默认字体源,原配置只有 Linux 路径
  (/usr/share/fonts),Windows 服务器上 aspose 找不到任何系统字体,
  全部替换为内置 Fanwood(无中文字形),转换出的 PDF 中文全部为方框
- 字体目录加入 C:\Windows\Fonts(保留 Linux 路径兼容),修复后
  PDF 正确嵌入宋体子集,中文正常显示
4 hours ago
chenyuan e2f550de99 feat: 学校默认实训任务初始化默认只开放第一个项目前三项
- initializeSchoolDefault 复制平台基线时按默认开放清单置停用状态,
  其余任务保留完整目录但停用,学校管理员可在默认实训任务配置页放开
- 同步修复脚本说明与测试
5 hours ago
chenyuan d6ebf06afa feat: 参考答案推送与学生实验报告成绩展示
- 任务配置开放参考答案开关(setClassTaskReferenceAnswerEnabled),
  学生任务接口仅在推送后返回参考答案字段
- 学生实验报告任务明细携带AI评分/老师评分与评语
- 新增学生端参考答案预览接口(docx转PDF,推送门控)
- 同步测试
7 hours ago
chenyuan 78dbd4d01a fix: spring-boot 打包含 system 依赖(aspose-words/cells/pdf)
- 这些 jar 走 systemPath 依赖,默认不进 fat jar,只作为资源落在
  BOOT-INF/classes/lib 下,运行时类加载器读不到,
  参考答案转 PDF 预览首次在线上执行即 NoClassDefFoundError: com/aspose/cells/SaveOptions
- includeSystemScope=true 使其正确进入 BOOT-INF/lib
8 hours ago
chenyuan 245dc497ad fix: AI评语改为30-50字(过短自动重写一次),AI评分增加知识检索/模型调用分段计时日志
- 验收:评语按AI评分数据精简到30-50字默认填入评语框,教师可改
- 排查AI评分过慢:实测知识检索32ms、千问模型调用约12s为主体,接口已异步化
8 hours ago
chenyuan 0e9ddeda21 feat: 任务评阅教师触发AI评分与AI评语生成
- 教师可对单个/批量学生异步发起AI评分(复用学生端助评链路),评阅面板轮询展示AI分
- 新增 POST /api/weight/taskReview/ai-comment:基于该生AI评分报告+AI助学报告
  生成10-50字中文教师评语(超长按句子边界截断),供评阅弹窗自动填入
- student_training_answer 新增 teacher_comment 列(启动自动补列),保存评分支持携带评语
- pom 固定 lombok 1.18.32 修复 JDK21 构建兼容
9 hours ago
chenyuan 99c7167a0a fix(sql): 平台/学校默认任务分配 module 存中文任务名导致评阅与学生端解析不到任务的修复脚本
- task_allocation 里 999999999 与 SCHOOL_DEFAULT:* 的 module 存的是中文展示名,
  消费方都用它匹配 training_task.task_key,导致无班级级分配的教学班评阅下拉全空、
  学生列表为 0
- 附判定查询与按学校修复脚本;验收库已手工修复并备份 task_allocation_bak_20261016
9 hours ago
chenyuan 516aef2156 feat: 实训任务配置新增参考答案字段(reference_answer_name/url)
- TrainingTask/TrainingTaskClassConfig 实体与两个 mapper 透传新字段
- TrainingTaskServiceImpl:平台保存未传字段保留原值、默认任务修改同步班级、
  建表语句与 ensureReferenceAnswerColumns 存量自动补列
- 附手工迁移脚本 docs/sql/2026-10-15-training-task-reference-answer.sql
11 hours ago
chenyuan e2ee0b1d42 feat: 教师端成绩列表AI评测成绩列返回真实数据——按教学班任务聚合各任务ai_assessment_score(六项目加权,同教师评分口径,实时读作答表) 2 days ago
chenyuan e5a2bd740f fix: AI评分后教师端成绩列表延迟~24小时才更新——重评claim的快照BINARY长文本比对失效导致重评被静默跳过,移除该条件(快照变化已在Java侧判断);AI评分落库后异步重建本教学班stu_rank当日快照 2 days ago
chenyuan de02726c97 feat: AI助评纳入第一步选择题——评分上下文带题干/选项/标准答案/学生作答,错题在第一评项扣分;校验容错:评项名归一化、忽略模型maxScore按计划值、总分与逐项不一致时以逐项为准 2 days ago
chenyuan 8fd2aabbba fix: 学生/教师列表分页错位与班级院系total丢失;登录密码错误与账号不存在分开提示;AI助评评项名归一化修复误报502 3 days ago
chenyuan b3a9a50fc8 feat: 学生Excel导入取消院系/专业列、班级直接按本校行政班匹配;添加教师账号默认继承操作者学校/院系/专业 4 days ago
chenyuan 179d18e754 feat: 启动时检查数据库连接与AI评估表字符集,Druid固定characterEncoding防按平台默认GBK写库 4 days ago
chenyuan 247669f648 fix: AI助评失败记录不再抛错卡死,PROCESSING超时可重试,报错信息精简 5 days ago
chenyuan 15f1a399df refactor: BI数据源内置表名仅保留新增数据源 6 days ago
chenyuan 0bd56cdeea fix: 用户行为表查询改用selectByExampleWithBLOBs以包含大字段 7 days ago
chenyuan e1effbf35c feat: BI数据源内置表名抽取为常量并新增数据源表 7 days ago

@ -0,0 +1,10 @@
-- 实训任务新增“参考答案”资料位(教师端/平台管理端实训任务管理上传)。
-- 后端 TrainingTaskServiceImpl 启动时会自动补列(ensureReferenceAnswerColumns),
-- 本脚本供手工执行或核对用,重复执行前请先确认列不存在。
ALTER TABLE training_task
ADD COLUMN reference_answer_name varchar(255) NULL AFTER ai_manual_url,
ADD COLUMN reference_answer_url varchar(500) NULL AFTER reference_answer_name;
ALTER TABLE training_task_class_config
ADD COLUMN reference_answer_name varchar(255) NULL AFTER ai_manual_url,
ADD COLUMN reference_answer_url varchar(500) NULL AFTER reference_answer_name;

@ -0,0 +1,53 @@
-- 默认任务开放范围调整:平台默认/学校默认只默认开放第一个项目的前 3 个任务。
--
-- 背景:此前平台默认(class_id='999999999')与学校默认(class_id LIKE 'SCHOOL_DEFAULT:%')
-- 的任务目录默认全部开放,与产品约定不一致——任务分配的默认口径是
-- "教学班未保存发布配置时,仅默认开放第一个项目的前三项,不是全部任务"。
--
-- 影响范围:只调整 disabled_status,不增删任务、不改 module/sort;
-- 第一个项目的前 3 项置为开放(0),其余任务(含综合实训)置为停用(1)。
-- 学校管理员仍可在"默认实训任务配置"页按需放开。
--
-- 前置条件:task_allocation.module 已是英文 task_key,即已执行
-- 2026-10-16-repair-task-allocation-module-keys.sql
-- 若仍有中文任务名残留,JOIN 不上的行保持原值,请先执行修复脚本重建。
--
-- 幂等:可重复执行。执行前建议备份:
-- CREATE TABLE task_allocation_bak_20261016_first3 AS
-- SELECT * FROM task_allocation
-- WHERE class_id='999999999' OR class_id LIKE 'SCHOOL_DEFAULT:%';
-- 1) 第一项目前 3 项开放,其余启用目录内的任务置为停用
UPDATE task_allocation a
JOIN training_task t ON t.task_key = a.module
JOIN (
SELECT task_key,
ROW_NUMBER() OVER (ORDER BY sort ASC, id ASC) AS rank_in_project,
COALESCE(project_name, '其他') AS project_name
FROM training_task
WHERE enabled = 1
AND task_key <> 'comprehensive-case-training'
) ranked ON ranked.task_key = a.module
CROSS JOIN (
SELECT COALESCE(project_name, '其他') AS project_name
FROM training_task
WHERE enabled = 1 AND task_key <> 'comprehensive-case-training'
ORDER BY sort ASC, id ASC
LIMIT 1
) fp
SET a.disabled_status = CASE WHEN ranked.project_name = fp.project_name
AND ranked.rank_in_project <= 3
THEN 0 ELSE 1 END
WHERE a.class_id = '999999999' OR a.class_id LIKE 'SCHOOL_DEFAULT:%';
-- 2) 综合实训始终默认停用(各班发布后才参与)
UPDATE task_allocation
SET disabled_status = 1
WHERE (class_id = '999999999' OR class_id LIKE 'SCHOOL_DEFAULT:%')
AND module = 'comprehensive-case-training';
-- 3) 校验:每个 class_id 下应只有 3 条开放(即第一个项目的前 3 项)
SELECT a.class_id, SUM(a.disabled_status = 0) AS open_count, COUNT(*) AS total
FROM task_allocation a
WHERE a.class_id = '999999999' OR a.class_id LIKE 'SCHOOL_DEFAULT:%'
GROUP BY a.class_id;

@ -0,0 +1,76 @@
-- 修复平台默认/学校默认任务分配的 module 存了中文任务名的问题。
--
-- 现象:task_allocation 里 class_id='999999999'(平台默认)与 class_id LIKE 'SCHOOL_DEFAULT:%'
-- (学校默认,由 initializeSchoolDefault 从平台默认复制而来)的 module 存的是中文任务展示名
-- (如"行业需求分析-蛋糕指数"),而所有消费方(任务评阅 taskReview、学生端实训任务解析等)
-- 都用 module 匹配 training_task.task_key(英文标识),一条都匹配不上。
-- 表现为:没有班级级任务分配的教学班,任务评阅页实训项目/实训任务下拉为空、学生列表为 0,
-- 学生端实训列表为空;有班级级分配的教学班(班级管理/实训任务管理保存过)不受影响。
--
-- 2026-10-16 补充:重建时默认只开放第一个项目的前 3 个任务(disabled_status=0),
-- 其余任务保留完整目录但置为停用(disabled_status=1),与产品约定一致——
-- 教学班未保存发布配置时,学生端默认只开放第一个项目的前三项,不是全部任务。
-- 综合实训(comprehensive-case-training)仍默认停用,由各班发布后参与。
--
-- 执行前先备份:
-- CREATE TABLE task_allocation_bak_20261016 AS
-- SELECT * FROM task_allocation
-- WHERE class_id='999999999' OR class_id LIKE 'SCHOOL_DEFAULT:%';
--
-- 判定是否需要执行:
-- SELECT a.module FROM task_allocation a
-- LEFT JOIN training_task t ON t.task_key = a.module
-- WHERE (a.class_id='999999999' OR a.class_id LIKE 'SCHOOL_DEFAULT:%') AND t.id IS NULL;
-- 查出中文任务名即需要修复。
-- 1) 平台默认:以 training_task 为基线重建(启用任务全量入目录;默认仅第一项目前 3 项开放)
DELETE FROM task_allocation WHERE class_id = '999999999';
INSERT INTO task_allocation (id, class_id, school_id, module, disabled_status, sort)
SELECT UUID(), '999999999', '999999999', tt.task_key,
CASE WHEN tt.project_name = fp.project_name
AND tt.rank_in_project <= 3
AND tt.task_key <> 'comprehensive-case-training'
THEN 0 ELSE 1 END,
tt.sort
FROM (
SELECT t.task_key, t.sort,
COALESCE(t.project_name, '其他') AS project_name,
ROW_NUMBER() OVER (PARTITION BY COALESCE(t.project_name, '其他')
ORDER BY t.sort ASC, t.id ASC) AS rank_in_project
FROM training_task t
WHERE t.enabled = 1
) tt
CROSS JOIN (
SELECT COALESCE(project_name, '其他') AS project_name
FROM training_task
WHERE enabled = 1 AND task_key <> 'comprehensive-case-training'
ORDER BY sort ASC, id ASC
LIMIT 1
) fp;
-- 2) 学校默认:逐个学校执行(把 <SCHOOL_ID> 替换为实际学校ID后运行;
-- 已确认无问题的学校跳过。学校清单:
-- SELECT DISTINCT school_id FROM school_class WHERE class_type='TEACHING';)
-- DELETE FROM task_allocation WHERE class_id = CONCAT('SCHOOL_DEFAULT:', '<SCHOOL_ID>');
-- INSERT INTO task_allocation (id, class_id, school_id, module, disabled_status, sort)
-- SELECT UUID(), CONCAT('SCHOOL_DEFAULT:', '<SCHOOL_ID>'), '<SCHOOL_ID>', tt.task_key,
-- CASE WHEN tt.project_name = fp.project_name
-- AND tt.rank_in_project <= 3
-- AND tt.task_key <> 'comprehensive-case-training'
-- THEN 0 ELSE 1 END,
-- tt.sort
-- FROM (
-- SELECT t.task_key, t.sort,
-- COALESCE(t.project_name, '其他') AS project_name,
-- ROW_NUMBER() OVER (PARTITION BY COALESCE(t.project_name, '其他')
-- ORDER BY t.sort ASC, t.id ASC) AS rank_in_project
-- FROM training_task t
-- WHERE t.enabled = 1
-- ) tt
-- CROSS JOIN (
-- SELECT COALESCE(project_name, '其他') AS project_name
-- FROM training_task
-- WHERE enabled = 1 AND task_key <> 'comprehensive-case-training'
-- ORDER BY sort ASC, id ASC
-- LIMIT 1
-- ) fp;

@ -0,0 +1,3 @@
-- 教师任务评阅评语字段;应用启动时也会由 schema initializer 自动补列。
ALTER TABLE student_training_answer
ADD COLUMN teacher_comment varchar(1000) NULL AFTER teacher_score;

@ -140,6 +140,9 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<!-- Spring Boot 2.7.12 manages an older Lombok release that
is incompatible with JDK 21 (JCImport.qualid was removed). -->
<version>1.18.32</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.hibernate</groupId> <groupId>org.hibernate</groupId>
@ -336,6 +339,11 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- aspose-words/cells/pdf 走 systemPath 依赖,默认不会打进 fat jar,
运行时 NoClassDefFoundError(参考答案转 PDF 预览即此问题),必须包含 -->
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

@ -0,0 +1,69 @@
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;
}
}

@ -18,6 +18,7 @@ public class StudentTrainingAnswerTeacherScoreSchemaInitializer {
addColumnIfMissing("teacher_score", "decimal(5,2) NULL"); addColumnIfMissing("teacher_score", "decimal(5,2) NULL");
addColumnIfMissing("teacher_scored_by", "varchar(64) NULL"); addColumnIfMissing("teacher_scored_by", "varchar(64) NULL");
addColumnIfMissing("teacher_scored_at", "datetime NULL"); addColumnIfMissing("teacher_scored_at", "datetime NULL");
addColumnIfMissing("teacher_comment", "varchar(1000) NULL");
} }
private void addColumnIfMissing(String columnName, String definition) { private void addColumnIfMissing(String columnName, String definition) {

@ -4,6 +4,8 @@ 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;
@ -13,6 +15,8 @@ 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;
/** /**
* 全局异常处理 * 全局异常处理
*/ */
@ -20,6 +24,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
@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));
@ -61,9 +67,22 @@ 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,8 +466,6 @@ 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());
@ -486,32 +484,16 @@ 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(facultyName) && faculty == null) { if (StringUtils.isNotBlank(className)) {
errors.add("第 " + rowNumber + " 行:未找到院系“" + facultyName + "”"); schoolClass = findAdminClassByName(schoolId, className);
}
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 + " 行:专业“" + majorName + "”下未找到班级“" + className + "”"); errors.add("第 " + rowNumber + " 行:未找到行政班:" + className);
} }
} }
@ -524,8 +506,6 @@ 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导入");
@ -543,8 +523,6 @@ 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());
@ -574,30 +552,13 @@ public class SchoolAdminController {
return false; return false;
} }
private SchoolFaculty findFacultyByName(String schoolId, String facultyName) { //学生账号导入不再区分院系/专业,班级直接按本校行政班名称匹配
SchoolFacultyExample example = new SchoolFacultyExample(); private SchoolClass findAdminClassByName(String schoolId, String className) {
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)
.andSchoolMajorIdEqualTo(majorId) .andClassNameEqualTo(className)
.andClassNameEqualTo(className); .andClassTypeEqualTo("ADMIN");
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);
} }

@ -56,8 +56,10 @@ public class AiTrainingEvaluationController {
@ApiOperation("Generate AI assessment") @ApiOperation("Generate AI assessment")
public ResultEntity<AiTrainingEvaluationView> assessment(@PathVariable String taskKey, HttpServletRequest request) { public ResultEntity<AiTrainingEvaluationView> assessment(@PathVariable String taskKey, HttpServletRequest request) {
try { try {
return new ResultEntity<>(HttpStatus.OK, "AI assessment generated", AiTrainingEvaluationView view = aiTrainingEvaluationService.startAssessment(taskKey, currentUser(request));
aiTrainingEvaluationService.generateAssessment(taskKey, currentUser(request))); // 兼容旧版服务实现/测试替身;正式实现始终立即返回 PROCESSING 状态。
if (view == null) view = aiTrainingEvaluationService.generateAssessment(taskKey, currentUser(request));
return new ResultEntity<>(HttpStatus.OK, "AI assessment submitted", view);
} catch (ServiceException e) { } catch (ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage()); return new ResultEntity<>(e.getCode(), e.getMessage());
} }

@ -27,6 +27,7 @@ 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;
@ -94,6 +95,9 @@ 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, "账号不存在");
} }

@ -14,25 +14,37 @@ import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; 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.UserinfoMapper; import com.sztzjy.linkCommerce.mapper.UserinfoMapper;
import com.sztzjy.linkCommerce.mapper.WeightMapper; import com.sztzjy.linkCommerce.mapper.WeightMapper;
import com.sztzjy.linkCommerce.entity.dto.*; import com.sztzjy.linkCommerce.entity.dto.*;
import com.sztzjy.linkCommerce.service.ScoreRankService; import com.sztzjy.linkCommerce.service.ScoreRankService;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.TeacherScoreService; import com.sztzjy.linkCommerce.service.TeacherScoreService;
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
import com.sztzjy.linkCommerce.util.excel.FilePortUtil; import com.sztzjy.linkCommerce.util.excel.FilePortUtil;
import com.sztzjy.linkCommerce.util.file.IFileUtil;
import com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.File;
import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -41,6 +53,8 @@ import java.util.stream.Collectors;
@RequestMapping("api/weight") @RequestMapping("api/weight")
@RestController @RestController
public class TeaScoreController { public class TeaScoreController {
private static final Logger log = LoggerFactory.getLogger(TeaScoreController.class);
@Autowired @Autowired
WeightMapper weightMapper; WeightMapper weightMapper;
@Autowired @Autowired
@ -58,6 +72,8 @@ public class TeaScoreController {
@Autowired(required = false) @Autowired(required = false)
TrainingTaskMapper trainingTaskMapper; TrainingTaskMapper trainingTaskMapper;
@Autowired(required = false) @Autowired(required = false)
TrainingTaskClassConfigMapper trainingTaskClassConfigMapper;
@Autowired(required = false)
StudentTrainingAnswerMapper studentTrainingAnswerMapper; StudentTrainingAnswerMapper studentTrainingAnswerMapper;
@Autowired(required = false) @Autowired(required = false)
SchoolDefaultTaskService schoolDefaultTaskService; SchoolDefaultTaskService schoolDefaultTaskService;
@ -65,6 +81,78 @@ public class TeaScoreController {
UserinfoMapper userinfoMapper; UserinfoMapper userinfoMapper;
@Autowired @Autowired
TeacherScoreService teacherScoreService; TeacherScoreService teacherScoreService;
@Autowired
IFileUtil fileUtil;
@Autowired
AiTrainingEvaluationService aiTrainingEvaluationService;
@PostMapping("/taskReview/ai-assessment")
@ApiOperation("异步发起单个任务AI评分")
public ResultEntity<AiTrainingEvaluationView> startTaskAiAssessment(@RequestParam String classId,
@RequestParam String taskId,
@RequestParam String studentUserId,
HttpServletRequest request) {
try {
JwtUser teacher = currentUser(request);
teacherScoreService.requireOwnedClass(teacher, classId);
return new ResultEntity<>(HttpStatus.OK, "AI评分任务已提交",
aiTrainingEvaluationService.startAssessmentForTeacher(classId, taskId, studentUserId, teacher));
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@GetMapping("/taskReview/ai-assessment")
@ApiOperation("查询任务AI评分状态")
public ResultEntity<AiTrainingEvaluationView> getTaskAiAssessment(@RequestParam String classId,
@RequestParam String taskId,
@RequestParam String studentUserId,
HttpServletRequest request) {
try {
JwtUser teacher = currentUser(request);
teacherScoreService.requireAccessibleClass(teacher, classId);
return new ResultEntity<>(HttpStatus.OK, "查询成功",
aiTrainingEvaluationService.getForTeacher(classId, taskId, studentUserId, teacher));
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@PostMapping("/taskReview/ai-comment")
@ApiOperation("基于AI评分与助学报告生成10-50字教师评语")
public ResultEntity<Map<String, String>> generateTaskAiComment(@RequestParam String classId,
@RequestParam String taskId,
@RequestParam String studentUserId,
HttpServletRequest request) {
try {
JwtUser teacher = currentUser(request);
teacherScoreService.requireAccessibleClass(teacher, classId);
return new ResultEntity<>(HttpStatus.OK, "评语生成成功",
aiTrainingEvaluationService.generateCommentForTeacher(classId, taskId, studentUserId, teacher));
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@PostMapping("/taskReview/ai-assessment/batch")
@ApiOperation("异步发起批量任务AI评分")
public ResultEntity<List<AiTrainingEvaluationView>> startBatchTaskAiAssessment(@RequestBody BatchTaskAiAssessmentRequest body,
HttpServletRequest request) {
try {
JwtUser teacher = currentUser(request);
teacherScoreService.requireOwnedClass(teacher, body == null ? null : body.getClassId());
if (body == null || body.getStudentUserIds() == null || body.getStudentUserIds().isEmpty()) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "请选择学生");
}
List<AiTrainingEvaluationView> result = new ArrayList<>();
for (String studentUserId : body.getStudentUserIds()) {
result.add(aiTrainingEvaluationService.startAssessmentForTeacher(body.getClassId(), body.getTaskId(), studentUserId, teacher));
}
return new ResultEntity<>(HttpStatus.OK, "批量AI评分任务已提交", result);
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@GetMapping("/ownedTeachingClasses") @GetMapping("/ownedTeachingClasses")
@ApiOperation("教师本人教学班列表") @ApiOperation("教师本人教学班列表")
@ -130,7 +218,8 @@ public class TeaScoreController {
public ResultEntity<?> saveTaskTeacherScore(@RequestParam String classId, public ResultEntity<?> saveTaskTeacherScore(@RequestParam String classId,
@RequestParam String studentUserId, @RequestParam String studentUserId,
@RequestParam String taskId, @RequestParam String taskId,
@RequestParam BigDecimal teacherScore, @RequestParam(required = false) BigDecimal teacherScore,
@RequestParam(required = false) String teacherComment,
HttpServletRequest request) { HttpServletRequest request) {
try { try {
JwtUser teacher = currentUser(request); JwtUser teacher = currentUser(request);
@ -148,9 +237,10 @@ public class TeaScoreController {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "教师评分必须为0至100之间的数值"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "教师评分必须为0至100之间的数值");
} }
Date scoredAt = new Date(); Date scoredAt = new Date();
studentTrainingAnswerMapper.updateTeacherScore(answer.getId(), teacherScore.setScale(2, BigDecimal.ROUND_HALF_UP), studentTrainingAnswerMapper.updateTeacherScoreWithComment(answer.getId(), teacherScore.setScale(2, BigDecimal.ROUND_HALF_UP),
teacher.getUserId(), scoredAt); teacher.getUserId(), scoredAt, StringUtils.left(StringUtils.trimToEmpty(teacherComment), 1000));
answer.setTeacherScore(teacherScore.setScale(2, BigDecimal.ROUND_HALF_UP)); answer.setTeacherScore(teacherScore.setScale(2, BigDecimal.ROUND_HALF_UP));
answer.setTeacherComment(StringUtils.left(StringUtils.trimToEmpty(teacherComment), 1000));
answer.setTeacherScoredBy(teacher.getUserId()); answer.setTeacherScoredBy(teacher.getUserId());
answer.setTeacherScoredAt(scoredAt); answer.setTeacherScoredAt(scoredAt);
return new ResultEntity<>(HttpStatus.OK, "任务评分保存成功", answer); return new ResultEntity<>(HttpStatus.OK, "任务评分保存成功", answer);
@ -174,6 +264,72 @@ public class TeaScoreController {
} }
} }
@GetMapping("/taskReview/reference-answer/preview")
@ApiOperation("在线预览任务参考答案")
public ResponseEntity<byte[]> previewTaskReferenceAnswer(@RequestParam String classId,
@RequestParam String taskId,
HttpServletRequest request) {
long startAt = System.currentTimeMillis();
JwtUser operator = currentUser(request);
try {
teacherScoreService.requireAccessibleClass(operator, classId);
TrainingTask task = trainingTaskMapper.selectByPrimaryKey(taskId);
if (task == null) {
log.warn("参考答案预览失败:任务不存在 classId={} taskId={} userId={}", classId, taskId,
operator == null ? null : operator.getUserId());
return ResponseEntity.notFound().build();
}
TrainingTaskClassConfig classConfig = trainingTaskClassConfigMapper == null ? null
: trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(classId, task.getTaskKey());
String referenceUrl = classConfig != null && StringUtils.isNotBlank(classConfig.getReferenceAnswerUrl())
? classConfig.getReferenceAnswerUrl() : task == null ? null : task.getReferenceAnswerUrl();
String referenceName = classConfig != null && StringUtils.isNotBlank(classConfig.getReferenceAnswerName())
? classConfig.getReferenceAnswerName() : task == null ? null : task.getReferenceAnswerName();
if (StringUtils.isBlank(referenceUrl)) {
log.warn("参考答案预览失败:未配置参考答案 classId={} taskId={} taskKey={} userId={}",
classId, taskId, task.getTaskKey(), operator == null ? null : operator.getUserId());
return ResponseEntity.notFound().build();
}
String name = StringUtils.defaultIfBlank(referenceName, referenceUrl);
String path = referenceUrl;
int slash = path.indexOf("/file/");
if (slash >= 0) path = path.substring(slash + 1);
File source = new File(fileUtil.getFullPath(path));
if (!source.exists() || !PDFConvertUtil.isCreatePdf(name)) {
log.warn("参考答案预览失败:文件不存在或类型不支持转换 classId={} taskKey={} userId={} name={} url={} path={} exists={}",
classId, task.getTaskKey(), operator == null ? null : operator.getUserId(),
name, referenceUrl, source.getAbsolutePath(), source.exists());
return ResponseEntity.notFound().build();
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (InputStream input = Files.newInputStream(source.toPath())) {
if (!PDFConvertUtil.convertToPDFByFileNameSuffix(name, input, output)) {
log.warn("参考答案预览失败:文档转换未成功 classId={} taskKey={} userId={} name={} path={}",
classId, task.getTaskKey(), operator == null ? null : operator.getUserId(), name, source.getAbsolutePath());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
log.info("参考答案预览成功 classId={} taskKey={} userId={} name={} pdfBytes={} elapsed={}ms",
classId, task.getTaskKey(), operator == null ? null : operator.getUserId(),
name, output.size(), System.currentTimeMillis() - startAt);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).header("Content-Disposition", "inline; filename=reference-answer.pdf").body(output.toByteArray());
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
log.warn("参考答案预览失败:权限或参数校验未通过 classId={} taskId={} userId={} status={} reason={}",
classId, taskId, operator == null ? null : operator.getUserId(), e.getCode(), e.getMessage());
return ResponseEntity.status(e.getCode()).build();
} catch (RuntimeException | java.io.IOException e) {
log.warn("参考答案预览失败:转换异常 classId={} taskId={} userId={} name={}",
classId, taskId, operator == null ? null : operator.getUserId(),
StringUtils.defaultIfBlank(currentTaskKey(taskId), "-"), e);
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
private String currentTaskKey(String taskId) {
TrainingTask task = trainingTaskMapper == null ? null : trainingTaskMapper.selectByPrimaryKey(taskId);
return task == null ? null : task.getTaskKey();
}
@PostMapping("/batchSaveTaskTeacherScore") @PostMapping("/batchSaveTaskTeacherScore")
@ApiOperation("批量保存教师任务评分") @ApiOperation("批量保存教师任务评分")
@Transactional @Transactional
@ -189,7 +345,8 @@ public class TeaScoreController {
BigDecimal score = body.getTeacherScore().setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal score = body.getTeacherScore().setScale(2, BigDecimal.ROUND_HALF_UP);
Date scoredAt = new Date(); Date scoredAt = new Date();
for (StudentTrainingAnswer answer : answers) { for (StudentTrainingAnswer answer : answers) {
studentTrainingAnswerMapper.updateTeacherScore(answer.getId(), score, teacher.getUserId(), scoredAt); studentTrainingAnswerMapper.updateTeacherScoreWithComment(answer.getId(), score, teacher.getUserId(), scoredAt,
StringUtils.left(StringUtils.trimToEmpty(body.getTeacherComment()), 1000));
} }
return new ResultEntity<>(HttpStatus.OK, "批量任务评分保存成功", body.getStudentUserIds()); return new ResultEntity<>(HttpStatus.OK, "批量任务评分保存成功", body.getStudentUserIds());
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) { } catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
@ -441,6 +598,7 @@ 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()
@ -454,6 +612,7 @@ 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));
} }
} }
@ -551,6 +710,48 @@ 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;
@ -625,7 +826,7 @@ public class TeaScoreController {
if (task == null || StringUtils.isBlank(task.getId()) || !seenTaskIds.add(task.getId())) { if (task == null || StringUtils.isBlank(task.getId()) || !seenTaskIds.add(task.getId())) {
continue; continue;
} }
TeacherTaskReviewOptionDTO option = reviewOption(task); TeacherTaskReviewOptionDTO option = reviewOption(task, classId);
allTasks.add(option); allTasks.add(option);
if (StringUtils.isNotBlank(option.getProjectName())) { if (StringUtils.isNotBlank(option.getProjectName())) {
projectNames.add(option.getProjectName()); projectNames.add(option.getProjectName());
@ -659,13 +860,19 @@ public class TeaScoreController {
return page; return page;
} }
private TeacherTaskReviewOptionDTO reviewOption(TrainingTask task) { private TeacherTaskReviewOptionDTO reviewOption(TrainingTask task, String classId) {
TeacherTaskReviewOptionDTO option = new TeacherTaskReviewOptionDTO(); TeacherTaskReviewOptionDTO option = new TeacherTaskReviewOptionDTO();
option.setTaskId(task.getId()); option.setTaskId(task.getId());
option.setTaskKey(task.getTaskKey()); option.setTaskKey(task.getTaskKey());
option.setProjectName(task.getProjectName()); option.setProjectName(task.getProjectName());
option.setTaskName(task.getTaskName()); option.setTaskName(task.getTaskName());
option.setSteps(task.getSteps()); option.setSteps(task.getSteps());
TrainingTaskClassConfig classConfig = trainingTaskClassConfigMapper == null ? null
: trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(classId, task.getTaskKey());
option.setReferenceAnswerName(classConfig != null && StringUtils.isNotBlank(classConfig.getReferenceAnswerName())
? classConfig.getReferenceAnswerName() : task.getReferenceAnswerName());
option.setReferenceAnswerUrl(classConfig != null && StringUtils.isNotBlank(classConfig.getReferenceAnswerUrl())
? classConfig.getReferenceAnswerUrl() : task.getReferenceAnswerUrl());
return option; return option;
} }
@ -681,6 +888,7 @@ public class TeaScoreController {
if (answer != null) { if (answer != null) {
row.setAiAssessmentScore(answer.getAiAssessmentScore()); row.setAiAssessmentScore(answer.getAiAssessmentScore());
row.setTeacherScore(answer.getTeacherScore()); row.setTeacherScore(answer.getTeacherScore());
row.setTeacherComment(answer.getTeacherComment());
row.setStep1Answer(answer.getStep1Answer()); row.setStep1Answer(answer.getStep1Answer());
row.setStep2Answer(answer.getStep2Answer()); row.setStep2Answer(answer.getStep2Answer());
row.setStep3Answer(answer.getStep3Answer()); row.setStep3Answer(answer.getStep3Answer());

@ -7,12 +7,16 @@ import com.sztzjy.linkCommerce.config.security.TokenProvider;
import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.TrainingTask;
import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO; import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO;
import com.sztzjy.linkCommerce.service.TrainingTaskService; import com.sztzjy.linkCommerce.service.TrainingTaskService;
import com.sztzjy.linkCommerce.util.file.IFileUtil;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
import com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil;
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.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -20,9 +24,12 @@ import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -33,6 +40,9 @@ public class TrainingTaskController {
@Autowired @Autowired
private TrainingTaskService trainingTaskService; private TrainingTaskService trainingTaskService;
@Autowired
private IFileUtil fileUtil;
@GetMapping @GetMapping
@ApiOperation("Training task list") @ApiOperation("Training task list")
public ResultEntity<List<TrainingTask>> list(@RequestParam(required = false) String projectName, public ResultEntity<List<TrainingTask>> list(@RequestParam(required = false) String projectName,
@ -41,8 +51,10 @@ public class TrainingTaskController {
HttpServletRequest request) { HttpServletRequest request) {
JwtUser user = TokenProvider.getJWTUser(request); JwtUser user = TokenProvider.getJWTUser(request);
if (isStudent(user)) { if (isStudent(user)) {
List<TrainingTask> studentTasks = trainingTaskService.listForStudent(user, enabledOnly);
studentTasks.forEach(this::removeReferenceAnswerForStudent);
return new ResultEntity<>(HttpStatus.OK, "Query success", return new ResultEntity<>(HttpStatus.OK, "Query success",
trainingTaskService.listForStudent(user, enabledOnly)); studentTasks);
} }
return new ResultEntity<>(HttpStatus.OK, "Query success", return new ResultEntity<>(HttpStatus.OK, "Query success",
trainingTaskService.listDefaults(projectName, taskName, enabledOnly)); trainingTaskService.listDefaults(projectName, taskName, enabledOnly));
@ -69,9 +81,45 @@ public class TrainingTaskController {
if (task == null) { if (task == null) {
return new ResultEntity<>(HttpStatus.OK, "Training task not configured", null); return new ResultEntity<>(HttpStatus.OK, "Training task not configured", null);
} }
if (isStudent(user)) {
removeReferenceAnswerForStudent(task);
}
return new ResultEntity<>(HttpStatus.OK, "Query success", task); return new ResultEntity<>(HttpStatus.OK, "Query success", task);
} }
@GetMapping("/key/{taskKey}/reference-answer/preview")
@ApiOperation("学生在线查看已推送的参考答案(docx 转 PDF)")
public ResponseEntity<byte[]> previewReferenceAnswerForStudent(@PathVariable String taskKey,
HttpServletRequest request) {
try {
JwtUser user = TokenProvider.getJWTUser(request);
if (!isStudent(user)) return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
// getStudentTaskByTaskKey 走班级配置解析;未推送(未开放)时参考答案字段对学生为空
TrainingTask task = trainingTaskService.getStudentTaskByTaskKey(taskKey, user);
if (task == null || StringUtils.isBlank(task.getReferenceAnswerUrl())) {
return ResponseEntity.notFound().build();
}
String name = StringUtils.defaultIfBlank(task.getReferenceAnswerName(), task.getReferenceAnswerUrl());
String path = task.getReferenceAnswerUrl();
int slash = path.indexOf("/file/");
if (slash >= 0) path = path.substring(slash + 1);
File source = new File(fileUtil.getFullPath(path));
if (!source.exists() || !PDFConvertUtil.isCreatePdf(name)) return ResponseEntity.notFound().build();
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (InputStream input = Files.newInputStream(source.toPath())) {
if (!PDFConvertUtil.convertToPDFByFileNameSuffix(name, input, output)) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header("Content-Disposition", "inline; filename=reference-answer.pdf")
.body(output.toByteArray());
} catch (RuntimeException | java.io.IOException e) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
@GetMapping("/classes/{teachingClassId}") @GetMapping("/classes/{teachingClassId}")
@ApiOperation("Training task list for teaching class") @ApiOperation("Training task list for teaching class")
public ResultEntity<List<TrainingTask>> listForTeachingClass(@PathVariable String teachingClassId, public ResultEntity<List<TrainingTask>> listForTeachingClass(@PathVariable String teachingClassId,
@ -118,6 +166,22 @@ public class TrainingTaskController {
} }
} }
@PutMapping("/classes/{teachingClassId}/{taskKey}/reference-answer")
@ApiOperation("Toggle reference answer visibility for a teaching-class task")
public ResultEntity<TrainingTask> setReferenceAnswerVisibility(@PathVariable String teachingClassId,
@PathVariable String taskKey,
@RequestParam Boolean enabled,
HttpServletRequest request) {
JwtUser user = requireTeacher(request);
try {
return new ResultEntity<>(HttpStatus.OK, "Reference answer visibility updated",
trainingTaskService.setClassTaskReferenceAnswerEnabled(
teachingClassId, taskKey, enabled, user.getUserId()));
} catch (IllegalArgumentException e) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
@DeleteMapping("/classes/{teachingClassId}/{taskKey}/override") @DeleteMapping("/classes/{teachingClassId}/{taskKey}/override")
@ApiOperation("Restore training task default for teaching class") @ApiOperation("Restore training task default for teaching class")
public ResultEntity restoreClassTaskDefault(@PathVariable String teachingClassId, public ResultEntity restoreClassTaskDefault(@PathVariable String teachingClassId,
@ -240,6 +304,14 @@ public class TrainingTaskController {
return user != null && user.getRoleId() == 4; return user != null && user.getRoleId() == 4;
} }
/** 参考答案默认对学生隐藏,只有教学班任务配置明确开放时才返回。 */
private void removeReferenceAnswerForStudent(TrainingTask task) {
if (task == null) return;
if (Boolean.TRUE.equals(task.getReferenceAnswerEnabled())) return;
task.setReferenceAnswerName(null);
task.setReferenceAnswerUrl(null);
}
private JwtUser requireAdmin(HttpServletRequest request) { private JwtUser requireAdmin(HttpServletRequest request) {
JwtUser user = TokenProvider.getJWTUser(request); JwtUser user = TokenProvider.getJWTUser(request);
if (user == null || user.getRoleId() != 1) { if (user == null || user.getRoleId() != 1) {

@ -557,32 +557,36 @@ public class UserController {
@PostMapping("/addTeacherAccount") @PostMapping("/addTeacherAccount")
@ApiOperation("教师管理-添加教师账号") @ApiOperation("教师管理-添加教师账号")
@AnonymousAccess @AnonymousAccess
public ResultEntity addTeacherAccount(@ApiParam("学校ID") @RequestParam String schoolId, public ResultEntity addTeacherAccount(@ApiParam("学校ID") @RequestParam(required = false) String schoolId,
@ApiParam("学校名称") @RequestParam String schoolName, @ApiParam("学校名称") @RequestParam(required = false) String schoolName,
@ApiParam("院系ID") @RequestParam String schoolFacultyId, @ApiParam("院系ID") @RequestParam(required = false) String schoolFacultyId,
@ApiParam("专业ID") @RequestParam String schoolMajorId, @ApiParam("专业ID") @RequestParam(required = false) String schoolMajorId,
@ApiParam("专业名称") @RequestParam String schoolMajorName, @ApiParam("专业名称") @RequestParam(required = false) 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进行查询 如果不存在则返回 //学校/院系/专业默认从当前操作用户继承;平台管理员可显式指定学校
School school = schoolMapper.selectByPrimaryKey(schoolId); Userinfo operator = StringUtils.isBlank(operatorId) ? null : userinfoMapper.selectByPrimaryKey(operatorId);
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, schoolId)) { if (!canManageSchoolStructure(operatorId, effectiveSchoolId)) {
return noManagePermission(); return noManagePermission();
} }
//再根据schoolFacultyId查询 如果不存在则返回 String effectiveFacultyId = isSchoolTeacher(operator) ? StringUtils.trimToNull(operator.getSchoolFacultyId()) : StringUtils.trimToNull(schoolFacultyId);
SchoolFaculty schoolFaculty = schoolFacultyMapper.selectByPrimaryKey(schoolFacultyId); String effectiveMajorId = isSchoolTeacher(operator) ? StringUtils.trimToNull(operator.getSchoolMajorId()) : StringUtils.trimToNull(schoolMajorId);
if (schoolFaculty == null) { //院系/专业选填:有值才校验
if (StringUtils.isNotBlank(effectiveFacultyId) && schoolFacultyMapper.selectByPrimaryKey(effectiveFacultyId) == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在");
} }
//再根据schoolMajorId查询 如果不存在则返回 if (StringUtils.isNotBlank(effectiveMajorId) && schoolMajorMapper.selectByPrimaryKey(effectiveMajorId) == null) {
SchoolMajor schoolMajor = schoolMajorMapper.selectByPrimaryKey(schoolMajorId);
if (schoolMajor == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该专业不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该专业不存在");
} }
//检查userName是否已经存在 //检查userName是否已经存在
@ -599,9 +603,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(schoolId); userInfo.setSchoolId(effectiveSchoolId);
userInfo.setSchoolFacultyId(schoolFacultyId); userInfo.setSchoolFacultyId(effectiveFacultyId);
userInfo.setSchoolMajorId(schoolMajorId); userInfo.setSchoolMajorId(effectiveMajorId);
userInfo.setSchoolClassId(null); userInfo.setSchoolClassId(null);
userInfo.setTeacherAdmin(false); userInfo.setTeacherAdmin(false);
userInfo.setCodeFrom("添加"); userInfo.setCodeFrom("添加");
@ -631,7 +635,6 @@ 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();
@ -646,6 +649,8 @@ 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)) {
@ -765,15 +770,18 @@ public class UserController {
if (!errorMessages.isEmpty()) { if (!errorMessages.isEmpty()) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "数据校验失败!", errorMessages); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "数据校验失败!", errorMessages);
} }
for (TeacherImportDTO teacher : teacherList) { // 教师的学校/院系/专业直接取自当前操作用户,不再从Excel的院校名称列解析
String schoolId = schoolService.selectSchoolIdBySchoolName(teacher.getSchoolName()); Userinfo operator = userinfoMapper.selectByPrimaryKey(operatorId);
if (!canManageSchoolStructure(operatorId, schoolId)) { String effectiveSchoolId = operator == null ? null : StringUtils.trimToEmpty(operator.getSchoolId());
return noManagePermission(); if (StringUtils.isBlank(effectiveSchoolId)) {
} return new ResultEntity<>(HttpStatus.BAD_REQUEST, "无法确定当前用户所属学校,导入失败");
}
if (!canManageSchoolStructure(operatorId, effectiveSchoolId)) {
return noManagePermission();
} }
// 异步处理导入***** // 异步处理导入*****
importService.processTeacherImport(teacherList); importService.processTeacherImport(teacherList, operator);
return new ResultEntity<>(HttpStatus.OK, "导入成功!"); return new ResultEntity<>(HttpStatus.OK, "导入成功!");
} catch (Exception e) { } catch (Exception e) {
@ -815,9 +823,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 String schoolFacultyId, @ApiParam("院系ID") @RequestParam(required = false) String schoolFacultyId,
@ApiParam("专业ID") @RequestParam String schoolMajorId, @ApiParam("专业ID") @RequestParam(required = false) String schoolMajorId,
@ApiParam("专业名称") @RequestParam String schoolMajorName, @ApiParam("专业名称") @RequestParam(required = false) 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,
@ -832,14 +840,11 @@ public class UserController {
if (school == null) { if (school == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学校不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学校不存在");
} }
//再根据schoolFacultyId查询 如果不存在则返回 //院系/专业改为选填:传了才校验,不传直接入库为空
SchoolFaculty schoolFaculty = schoolFacultyMapper.selectByPrimaryKey(schoolFacultyId); if (StringUtils.isNotBlank(schoolFacultyId) && schoolFacultyMapper.selectByPrimaryKey(schoolFacultyId) == null) {
if (schoolFaculty == null) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在"); return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该院系不存在");
} }
//再根据schoolMajorId查询 如果不存在则返回 if (StringUtils.isNotBlank(schoolMajorId) && schoolMajorMapper.selectByPrimaryKey(schoolMajorId) == null) {
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);
@ -865,8 +870,8 @@ public class UserController {
userInfo.setRole(4); userInfo.setRole(4);
userInfo.setTeacherAdmin(false); userInfo.setTeacherAdmin(false);
userInfo.setSchoolId(effectiveSchoolId); userInfo.setSchoolId(effectiveSchoolId);
userInfo.setSchoolFacultyId(schoolFacultyId); userInfo.setSchoolFacultyId(StringUtils.trimToNull(schoolFacultyId));
userInfo.setSchoolMajorId(schoolMajorId); userInfo.setSchoolMajorId(StringUtils.trimToNull(schoolMajorId));
String adminClassId = "TEACHING".equals(schoolClass.getClassType()) String adminClassId = "TEACHING".equals(schoolClass.getClassType())
? resolveAdminClassIdForTeachingClass(schoolClass) ? resolveAdminClassIdForTeachingClass(schoolClass)
: schoolClassId; : schoolClassId;
@ -989,7 +994,6 @@ 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();
@ -1025,6 +1029,8 @@ 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)) {
@ -2129,8 +2135,6 @@ 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());
@ -2147,32 +2151,16 @@ 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(facultyName) && faculty == null) { if (StringUtils.isNotBlank(className)) {
errors.add("第" + rowNumber + "行:未找到院系“" + facultyName + "”"); schoolClass = findTeacherImportAdminClass(operator.getSchoolId(), className);
}
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 + "行:专业“" + majorName + "”下未找到班级“" + className + "”"); errors.add("第" + rowNumber + "行:未找到行政班“" + 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)) {
@ -2192,8 +2180,6 @@ 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导入");
@ -2210,8 +2196,6 @@ 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());
@ -2241,30 +2225,23 @@ public class UserController {
return false; return false;
} }
private SchoolFaculty findTeacherImportFaculty(String schoolId, String facultyName) { private SchoolClass findTeacherImportClass(String schoolId, String majorId, String className) {
SchoolFacultyExample example = new SchoolFacultyExample(); SchoolClassExample example = new SchoolClassExample();
example.createCriteria() example.createCriteria()
.andSchoolIdEqualTo(schoolId) .andSchoolIdEqualTo(schoolId)
.andSchoolFacultyNameEqualTo(facultyName); .andSchoolMajorIdEqualTo(majorId)
List<SchoolFaculty> faculties = schoolFacultyMapper.selectByExample(example); .andClassNameEqualTo(className);
return faculties.isEmpty() ? null : faculties.get(0); List<SchoolClass> classes = schoolClassMapper.selectByExample(example);
} 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)
.andSchoolMajorIdEqualTo(majorId) .andClassNameEqualTo(className)
.andClassNameEqualTo(className); .andClassTypeEqualTo("ADMIN");
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,6 +52,9 @@ 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;
@ -200,6 +203,14 @@ 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;
} }

@ -53,6 +53,9 @@ public class StudentTrainingAnswer {
@ApiModelProperty("教师任务评分") @ApiModelProperty("教师任务评分")
private BigDecimal teacherScore; private BigDecimal teacherScore;
@ApiModelProperty("教师评语")
private String teacherComment;
@ApiModelProperty("教师评分人") @ApiModelProperty("教师评分人")
private String teacherScoredBy; private String teacherScoredBy;
@ -197,6 +200,8 @@ public class StudentTrainingAnswer {
public void setTeacherScoredBy(String teacherScoredBy) { this.teacherScoredBy = teacherScoredBy == null ? null : teacherScoredBy.trim(); } public void setTeacherScoredBy(String teacherScoredBy) { this.teacherScoredBy = teacherScoredBy == null ? null : teacherScoredBy.trim(); }
public Date getTeacherScoredAt() { return teacherScoredAt; } public Date getTeacherScoredAt() { return teacherScoredAt; }
public void setTeacherScoredAt(Date teacherScoredAt) { this.teacherScoredAt = teacherScoredAt; } public void setTeacherScoredAt(Date teacherScoredAt) { this.teacherScoredAt = teacherScoredAt; }
public String getTeacherComment() { return teacherComment; }
public void setTeacherComment(String teacherComment) { this.teacherComment = teacherComment == null ? null : teacherComment.trim(); }
public String getSaveAction() { public String getSaveAction() {
return saveAction; return saveAction;

@ -59,6 +59,15 @@ public class TrainingTask {
@ApiModelProperty("AI排错指导手册地址") @ApiModelProperty("AI排错指导手册地址")
private String aiManualUrl; private String aiManualUrl;
@ApiModelProperty("参考答案名称")
private String referenceAnswerName;
@ApiModelProperty("参考答案地址")
private String referenceAnswerUrl;
@ApiModelProperty("是否向学生开放参考答案")
private Boolean referenceAnswerEnabled;
@ApiModelProperty("排序") @ApiModelProperty("排序")
private Integer sort; private Integer sort;
@ -218,6 +227,30 @@ public class TrainingTask {
this.aiManualUrl = aiManualUrl == null ? null : aiManualUrl.trim(); this.aiManualUrl = aiManualUrl == null ? null : aiManualUrl.trim();
} }
public String getReferenceAnswerName() {
return referenceAnswerName;
}
public void setReferenceAnswerName(String referenceAnswerName) {
this.referenceAnswerName = referenceAnswerName == null ? null : referenceAnswerName.trim();
}
public String getReferenceAnswerUrl() {
return referenceAnswerUrl;
}
public void setReferenceAnswerUrl(String referenceAnswerUrl) {
this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim();
}
public Boolean getReferenceAnswerEnabled() {
return referenceAnswerEnabled;
}
public void setReferenceAnswerEnabled(Boolean referenceAnswerEnabled) {
this.referenceAnswerEnabled = referenceAnswerEnabled;
}
public Integer getSort() { public Integer getSort() {
return sort; return sort;
} }

@ -62,6 +62,15 @@ public class TrainingTaskClassConfig {
@ApiModelProperty("AI troubleshooting manual URL") @ApiModelProperty("AI troubleshooting manual URL")
private String aiManualUrl; private String aiManualUrl;
@ApiModelProperty("Reference answer name")
private String referenceAnswerName;
@ApiModelProperty("Reference answer URL")
private String referenceAnswerUrl;
@ApiModelProperty("Whether the reference answer is visible to students")
private Boolean referenceAnswerEnabled;
@ApiModelProperty("Sort order") @ApiModelProperty("Sort order")
private Integer sort; private Integer sort;
@ -229,6 +238,30 @@ public class TrainingTaskClassConfig {
this.aiManualUrl = aiManualUrl == null ? null : aiManualUrl.trim(); this.aiManualUrl = aiManualUrl == null ? null : aiManualUrl.trim();
} }
public String getReferenceAnswerName() {
return referenceAnswerName;
}
public void setReferenceAnswerName(String referenceAnswerName) {
this.referenceAnswerName = referenceAnswerName == null ? null : referenceAnswerName.trim();
}
public String getReferenceAnswerUrl() {
return referenceAnswerUrl;
}
public void setReferenceAnswerUrl(String referenceAnswerUrl) {
this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim();
}
public Boolean getReferenceAnswerEnabled() {
return referenceAnswerEnabled;
}
public void setReferenceAnswerEnabled(Boolean referenceAnswerEnabled) {
this.referenceAnswerEnabled = referenceAnswerEnabled;
}
public Integer getSort() { public Integer getSort() {
return sort; return sort;
} }

@ -9,6 +9,7 @@ public class AiTrainingEvaluationView {
private Date helpCompletedAt; private Date helpCompletedAt;
private String assessmentStatus; private String assessmentStatus;
private Integer assessmentScore; private Integer assessmentScore;
private String assessmentComment;
private String assessmentReportJson; private String assessmentReportJson;
private Date assessmentCompletedAt; private Date assessmentCompletedAt;
@ -22,6 +23,8 @@ public class AiTrainingEvaluationView {
public void setAssessmentStatus(String assessmentStatus) { this.assessmentStatus = assessmentStatus; } public void setAssessmentStatus(String assessmentStatus) { this.assessmentStatus = assessmentStatus; }
public Integer getAssessmentScore() { return assessmentScore; } public Integer getAssessmentScore() { return assessmentScore; }
public void setAssessmentScore(Integer assessmentScore) { this.assessmentScore = assessmentScore; } public void setAssessmentScore(Integer assessmentScore) { this.assessmentScore = assessmentScore; }
public String getAssessmentComment() { return assessmentComment; }
public void setAssessmentComment(String assessmentComment) { this.assessmentComment = assessmentComment; }
public String getAssessmentReportJson() { return assessmentReportJson; } public String getAssessmentReportJson() { return assessmentReportJson; }
public void setAssessmentReportJson(String assessmentReportJson) { this.assessmentReportJson = assessmentReportJson; } public void setAssessmentReportJson(String assessmentReportJson) { this.assessmentReportJson = assessmentReportJson; }
public Date getAssessmentCompletedAt() { return assessmentCompletedAt; } public Date getAssessmentCompletedAt() { return assessmentCompletedAt; }

@ -0,0 +1,15 @@
package com.sztzjy.linkCommerce.entity.dto;
import java.util.List;
public class BatchTaskAiAssessmentRequest {
private String classId;
private String taskId;
private List<String> studentUserIds;
public String getClassId() { return classId; }
public void setClassId(String classId) { this.classId = classId; }
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public List<String> getStudentUserIds() { return studentUserIds; }
public void setStudentUserIds(List<String> studentUserIds) { this.studentUserIds = studentUserIds; }
}

@ -8,6 +8,7 @@ public class BatchTaskTeacherScoreRequest {
private String taskId; private String taskId;
private List<String> studentUserIds; private List<String> studentUserIds;
private BigDecimal teacherScore; private BigDecimal teacherScore;
private String teacherComment;
public String getClassId() { return classId; } public String getClassId() { return classId; }
public void setClassId(String classId) { this.classId = classId; } public void setClassId(String classId) { this.classId = classId; }
@ -17,4 +18,6 @@ public class BatchTaskTeacherScoreRequest {
public void setStudentUserIds(List<String> studentUserIds) { this.studentUserIds = studentUserIds; } public void setStudentUserIds(List<String> studentUserIds) { this.studentUserIds = studentUserIds; }
public BigDecimal getTeacherScore() { return teacherScore; } public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; } public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; }
public String getTeacherComment() { return teacherComment; }
public void setTeacherComment(String teacherComment) { this.teacherComment = teacherComment; }
} }

@ -1,4 +1,26 @@
package com.sztzjy.linkCommerce.entity.dto; package com.sztzjy.linkCommerce.entity.dto;
import java.math.BigDecimal; import java.util.List; import java.math.BigDecimal; import java.util.List;
public class StudentExperimentReportDTO { private String teachingClassId; private Integer rank; private BigDecimal totalScore; private List<StudentExperimentReportModuleDTO> modules; private List<StudentExperimentReportTaskDTO> tasks; public class StudentExperimentReportDTO {
public String getTeachingClassId(){return teachingClassId;} public void setTeachingClassId(String v){teachingClassId=v;} public Integer getRank(){return rank;} public void setRank(Integer v){rank=v;} public BigDecimal getTotalScore(){return totalScore;} public void setTotalScore(BigDecimal v){totalScore=v;} public List<StudentExperimentReportModuleDTO> getModules(){return modules;} public void setModules(List<StudentExperimentReportModuleDTO> v){modules=v;} public List<StudentExperimentReportTaskDTO> getTasks(){return tasks;} public void setTasks(List<StudentExperimentReportTaskDTO> v){tasks=v;} } private String teachingClassId;
private Integer rank;
private BigDecimal totalScore;
private BigDecimal aiAssessmentScore;
private BigDecimal teacherScore;
private List<StudentExperimentReportModuleDTO> modules;
private List<StudentExperimentReportTaskDTO> tasks;
public String getTeachingClassId() { return teachingClassId; }
public void setTeachingClassId(String v) { teachingClassId = v; }
public Integer getRank() { return rank; }
public void setRank(Integer v) { rank = v; }
public BigDecimal getTotalScore() { return totalScore; }
public void setTotalScore(BigDecimal v) { totalScore = v; }
public BigDecimal getAiAssessmentScore() { return aiAssessmentScore; }
public void setAiAssessmentScore(BigDecimal v) { aiAssessmentScore = v; }
public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal v) { teacherScore = v; }
public List<StudentExperimentReportModuleDTO> getModules() { return modules; }
public void setModules(List<StudentExperimentReportModuleDTO> v) { modules = v; }
public List<StudentExperimentReportTaskDTO> getTasks() { return tasks; }
public void setTasks(List<StudentExperimentReportTaskDTO> v) { tasks = v; }
}

@ -1,3 +1,22 @@
package com.sztzjy.linkCommerce.entity.dto; package com.sztzjy.linkCommerce.entity.dto;
public class StudentExperimentReportTaskDTO { private String taskName; private String moduleName; private String progressStatus; private Boolean submitted; private Integer aiAssessmentScore; public class StudentExperimentReportTaskDTO {
public String getTaskName(){return taskName;} public void setTaskName(String v){taskName=v;} public String getModuleName(){return moduleName;} public void setModuleName(String v){moduleName=v;} public String getProgressStatus(){return progressStatus;} public void setProgressStatus(String v){progressStatus=v;} public Boolean getSubmitted(){return submitted;} public void setSubmitted(Boolean v){submitted=v;} public Integer getAiAssessmentScore(){return aiAssessmentScore;} public void setAiAssessmentScore(Integer v){aiAssessmentScore=v;} } private String taskName;
private String moduleName;
private String progressStatus;
private Boolean submitted;
private Integer aiAssessmentScore;
private java.math.BigDecimal teacherScore;
public String getTaskName() { return taskName; }
public void setTaskName(String v) { taskName = v; }
public String getModuleName() { return moduleName; }
public void setModuleName(String v) { moduleName = v; }
public String getProgressStatus() { return progressStatus; }
public void setProgressStatus(String v) { progressStatus = v; }
public Boolean getSubmitted() { return submitted; }
public void setSubmitted(Boolean v) { submitted = v; }
public Integer getAiAssessmentScore() { return aiAssessmentScore; }
public void setAiAssessmentScore(Integer v) { aiAssessmentScore = v; }
public java.math.BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(java.math.BigDecimal v) { teacherScore = v; }
}

@ -6,6 +6,8 @@ public class TeacherTaskReviewOptionDTO {
private String taskName; private String taskName;
private String taskKey; private String taskKey;
private String steps; private String steps;
private String referenceAnswerName;
private String referenceAnswerUrl;
public String getTaskId() { return taskId; } public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; } public void setTaskId(String taskId) { this.taskId = taskId; }
@ -17,4 +19,8 @@ public class TeacherTaskReviewOptionDTO {
public void setTaskKey(String taskKey) { this.taskKey = taskKey; } public void setTaskKey(String taskKey) { this.taskKey = taskKey; }
public String getSteps() { return steps; } public String getSteps() { return steps; }
public void setSteps(String steps) { this.steps = steps; } public void setSteps(String steps) { this.steps = steps; }
public String getReferenceAnswerName() { return referenceAnswerName; }
public void setReferenceAnswerName(String referenceAnswerName) { this.referenceAnswerName = referenceAnswerName; }
public String getReferenceAnswerUrl() { return referenceAnswerUrl; }
public void setReferenceAnswerUrl(String referenceAnswerUrl) { this.referenceAnswerUrl = referenceAnswerUrl; }
} }

@ -11,6 +11,7 @@ public class TeacherTaskReviewStudentDTO {
private Boolean scorable; private Boolean scorable;
private Integer aiAssessmentScore; private Integer aiAssessmentScore;
private BigDecimal teacherScore; private BigDecimal teacherScore;
private String teacherComment;
private String step1Answer; private String step1Answer;
private String step2Answer; private String step2Answer;
private String step3Answer; private String step3Answer;
@ -33,6 +34,8 @@ public class TeacherTaskReviewStudentDTO {
public void setAiAssessmentScore(Integer aiAssessmentScore) { this.aiAssessmentScore = aiAssessmentScore; } public void setAiAssessmentScore(Integer aiAssessmentScore) { this.aiAssessmentScore = aiAssessmentScore; }
public BigDecimal getTeacherScore() { return teacherScore; } public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; } public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; }
public String getTeacherComment() { return teacherComment; }
public void setTeacherComment(String teacherComment) { this.teacherComment = teacherComment; }
public String getStep1Answer() { return step1Answer; } public String getStep1Answer() { return step1Answer; }
public void setStep1Answer(String step1Answer) { this.step1Answer = step1Answer; } public void setStep1Answer(String step1Answer) { this.step1Answer = step1Answer; }
public String getStep2Answer() { return step2Answer; } public String getStep2Answer() { return step2Answer; }

@ -18,14 +18,6 @@ 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,28 +1,24 @@
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 {
@ApiModelProperty("院校名称") @ExcelProperty("教师姓名")
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;
} }

@ -32,6 +32,12 @@ public interface StudentTrainingAnswerMapper {
@Param("teacherUserId") String teacherUserId, @Param("teacherUserId") String teacherUserId,
@Param("scoredAt") Date scoredAt); @Param("scoredAt") Date scoredAt);
int updateTeacherScoreWithComment(@Param("id") String id,
@Param("score") java.math.BigDecimal score,
@Param("teacherUserId") String teacherUserId,
@Param("scoredAt") Date scoredAt,
@Param("teacherComment") String teacherComment);
List<StudentTrainingAnswer> selectByStudentAndTeachingClass(@Param("studentUserId") String studentUserId, List<StudentTrainingAnswer> selectByStudentAndTeachingClass(@Param("studentUserId") String studentUserId,
@Param("teachingClassId") String teachingClassId); @Param("teachingClassId") String teachingClassId);
} }

@ -3,6 +3,8 @@ package com.sztzjy.linkCommerce.service;
import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.config.security.JwtUser;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView; import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView;
import java.util.Map;
public interface AiTrainingEvaluationService { public interface AiTrainingEvaluationService {
AiTrainingEvaluationView get(String taskKey, JwtUser user); AiTrainingEvaluationView get(String taskKey, JwtUser user);
@ -13,4 +15,15 @@ public interface AiTrainingEvaluationService {
} }
AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user); AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user);
AiTrainingEvaluationView startAssessment(String taskKey, JwtUser user);
AiTrainingEvaluationView getForTeacher(String classId, String taskId, String studentUserId, JwtUser teacher);
AiTrainingEvaluationView startAssessmentForTeacher(String classId, String taskId, String studentUserId, JwtUser teacher);
/**
* 教师评阅用:基于该生 AI 评分报告与 AI 助学报告生成 10-50 字中文评语。
* 返回 {comment: 评语};该生尚无 AI 评分数据时抛出 ServiceException。
*/
Map<String, String> generateCommentForTeacher(String classId, String taskId, String studentUserId, JwtUser teacher);
} }

@ -1,12 +1,13 @@
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); void processTeacherImport(List<TeacherImportDTO> teacherList, Userinfo operator);
void processStudentImport(List<StudentImportDTO> studentList); void processStudentImport(List<StudentImportDTO> studentList);
} }

@ -5,6 +5,7 @@ import com.sztzjy.linkCommerce.entity.TrainingTask;
import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO; import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO;
import java.util.List; import java.util.List;
import java.util.Set;
public interface TrainingTaskService { public interface TrainingTaskService {
List<TrainingTask> list(String projectName, String taskName, Boolean enabledOnly); List<TrainingTask> list(String projectName, String taskName, Boolean enabledOnly);
@ -25,6 +26,11 @@ public interface TrainingTaskService {
void ensureDefaultTasks(String operatorId); void ensureDefaultTasks(String operatorId);
/**
* 教学班从未保存过发布配置时的默认开放任务:第一个项目的前三个任务。
*/
Set<String> getDefaultPublishedTaskKeys();
List<TrainingTask> listDefaults(String projectName, String taskName, Boolean enabledOnly); List<TrainingTask> listDefaults(String projectName, String taskName, Boolean enabledOnly);
List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator); List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator);
@ -33,6 +39,9 @@ public interface TrainingTaskService {
TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId); TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId);
TrainingTask setClassTaskReferenceAnswerEnabled(String teachingClassId, String taskKey,
Boolean enabled, String operatorId);
void restoreClassTaskDefault(String teachingClassId, String taskKey, String operatorId); void restoreClassTaskDefault(String teachingClassId, String taskKey, String operatorId);
void restoreClassTasksDefaults(String teachingClassId, String operatorId); void restoreClassTasksDefaults(String teachingClassId, String operatorId);

@ -3,6 +3,7 @@ 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;
@ -19,8 +20,10 @@ 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;
@ -30,19 +33,29 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
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();
@ -54,6 +67,8 @@ 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)
@ -82,7 +97,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())) { if (PROCESSING.equals(evaluation.getHelpStatus()) && !isProcessingTimedOut(evaluation)) {
return toView(evaluation); return toView(evaluation);
} }
if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) { if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) {
@ -110,7 +125,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) {
aiTrainingEvaluationMapper.failHelp(evaluation.getId(), cappedMessage(e), new Date()); recordHelpFailure(evaluation.getId(), e);
throw asServiceException(e); throw asServiceException(e);
} }
} }
@ -119,27 +134,109 @@ 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())) { if (PROCESSING.equals(evaluation.getAssessmentStatus()) && !isProcessingTimedOut(evaluation)) {
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);
List<AssessmentCriterionPlan> criterionPlan = assessmentCriterionPlan(context.effectiveContent.getSteps());
int claimed = reassessment int claimed = reassessment
? 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);
} }
return runAssessment(context, user, evaluation);
}
@Override
public AiTrainingEvaluationView startAssessment(String taskKey, JwtUser user) {
EvaluationContext context = resolveContext(taskKey, user, true);
AiTrainingEvaluation evaluation = getOrCreate(context, user);
if (PROCESSING.equals(evaluation.getAssessmentStatus()) && !isProcessingTimedOut(evaluation)) return toView(evaluation);
String snapshot = answerSnapshot(context.answer);
boolean reassessment = SUCCEEDED.equals(evaluation.getAssessmentStatus());
if (reassessment && snapshot.equals(evaluation.getAssessmentAnswerSnapshot())) return toView(evaluation);
int claimed = reassessment
? aiTrainingEvaluationMapper.claimReassessment(evaluation.getId(), evaluation.getAssessmentAnswerSnapshot(), new Date())
: aiTrainingEvaluationMapper.claimAssessment(evaluation.getId(), new Date());
if (claimed != 1) return currentView(context, user);
CompletableFuture.runAsync(() -> {
try { runAssessment(context, user, evaluation); }
catch (RuntimeException e) { log.warn("Student-triggered AI assessment failed, evaluationId={}", evaluation.getId(), e); }
});
AiTrainingEvaluationView view = toView(evaluation);
view.setAssessmentStatus(PROCESSING);
return view;
}
@Override
public AiTrainingEvaluationView getForTeacher(String classId, String taskId, String studentUserId, JwtUser teacher) {
EvaluationContext context = resolveTeacherContext(classId, taskId, studentUserId, teacher, false);
return toView(getOrCreate(context, studentUserId));
}
@Override
public AiTrainingEvaluationView startAssessmentForTeacher(String classId, String taskId, String studentUserId, JwtUser teacher) {
EvaluationContext context = resolveTeacherContext(classId, taskId, studentUserId, teacher, true);
AiTrainingEvaluation evaluation = getOrCreate(context, studentUserId);
if (PROCESSING.equals(evaluation.getAssessmentStatus()) && !isProcessingTimedOut(evaluation)) {
return toView(evaluation);
}
String answerSnapshot = answerSnapshot(context.answer);
boolean reassessment = SUCCEEDED.equals(evaluation.getAssessmentStatus());
if (reassessment && answerSnapshot.equals(evaluation.getAssessmentAnswerSnapshot())) {
return toView(evaluation);
}
int claimed = reassessment
? aiTrainingEvaluationMapper.claimReassessment(evaluation.getId(), evaluation.getAssessmentAnswerSnapshot(), new Date())
: aiTrainingEvaluationMapper.claimAssessment(evaluation.getId(), new Date());
if (claimed != 1) return currentView(context, studentUserId);
AiTrainingEvaluation processing = new AiTrainingEvaluation();
processing.setId(evaluation.getId());
processing.setHelpStatus(evaluation.getHelpStatus());
processing.setAssessmentStatus(PROCESSING);
CompletableFuture.runAsync(() -> {
try {
runAssessment(context, teacher, evaluation);
} catch (RuntimeException e) {
log.warn("Teacher-triggered AI assessment failed, evaluationId={}", evaluation.getId(), e);
}
});
return toView(processing);
}
private AiTrainingEvaluationView runAssessment(EvaluationContext context, JwtUser actor, AiTrainingEvaluation evaluation) {
long startedAt = System.currentTimeMillis();
String answerSnapshot = answerSnapshot(context.answer);
String taskSnapshot = taskSnapshot(context.effectiveContent);
List<AssessmentCriterionPlan> criterionPlan = assessmentCriterionPlan(context.effectiveContent.getSteps());
String rawResponse = null; String rawResponse = null;
try { try {
String knowledgeContext = knowledgeContext(context, user, ""); long knowledgeStartedAt = System.currentTimeMillis();
rawResponse = qwenChatClient.completeJson(assessmentSystemPrompt(criterionPlan), assessmentUserPrompt(taskSnapshot, answerSnapshot, knowledgeContext)); String knowledgeContext = knowledgeContext(context, actor, "");
long knowledgeCost = System.currentTimeMillis() - knowledgeStartedAt;
String choiceQuestionContext = choiceQuestionContext(context.effectiveContent, context.answer);
long modelStartedAt = System.currentTimeMillis();
rawResponse = qwenChatClient.completeJson(assessmentSystemPrompt(criterionPlan),
assessmentUserPrompt(taskSnapshot, answerSnapshot, knowledgeContext, choiceQuestionContext));
long modelCost = System.currentTimeMillis() - modelStartedAt;
// AI 评分过慢排查:分段记录知识检索与模型调用耗时(prompt 长度一并输出)
log.info("AI assessment timing, evaluationId={}, knowledgeContext={} ms, modelCall={} ms, total={} ms, "
+ "knowledgeContextChars={}, answerSnapshotChars={}, promptChars={}",
evaluation.getId(), knowledgeCost, modelCost, System.currentTimeMillis() - startedAt,
knowledgeContext.length(), answerSnapshot.length(),
assessmentSystemPrompt(criterionPlan).length()
+ assessmentUserPrompt(taskSnapshot, answerSnapshot, knowledgeContext, choiceQuestionContext).length());
JsonNode assessmentReport = object(rawResponse); JsonNode assessmentReport = object(rawResponse);
int score = validateAssessment(assessmentReport, criterionPlan); int score = validateAssessment(assessmentReport, criterionPlan);
compactAssessmentRationales(assessmentReport); compactAssessmentRationales(assessmentReport);
@ -155,11 +252,12 @@ 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);
refreshScoreRankAfterAssessment(actor, 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) {
aiTrainingEvaluationMapper.failAssessment(evaluation.getId(), cappedMessage(e), rawResponse, new Date()); recordAssessmentFailure(evaluation.getId(), e, rawResponse);
throw asServiceException(e); throw asServiceException(e);
} }
} }
@ -183,6 +281,88 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
EffectiveTrainingTaskContent.resolve(task, classConfig)); EffectiveTrainingTaskContent.resolve(task, classConfig));
} }
private EvaluationContext resolveTeacherContext(String classId, String taskId, String studentUserId,
JwtUser teacher, boolean requireAnswer) {
if (teacher == null || StringUtils.isBlank(teacher.getUserId())) {
throw new ServiceException(HttpStatus.UNAUTHORIZED, "Please login first");
}
if (StringUtils.isAnyBlank(classId, taskId, studentUserId)) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "班级、任务和学生不能为空");
}
TrainingTask task = trainingTaskMapper.selectByPrimaryKey(taskId);
if (task == null) throw new ServiceException(HttpStatus.BAD_REQUEST, "Training task does not exist");
if (teachingClassStudentMapper.countByStudentUserIdAndTeachingClassId(studentUserId, classId) == 0) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "该学生不在当前教学班");
}
StudentTrainingAnswer answer = studentTrainingAnswerMapper.selectByStudentClassAndTask(studentUserId, classId, taskId);
if (requireAnswer && (answer == null || !hasAnyAnswer(answer))) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "该学生尚未完成可评分内容");
}
TrainingTaskClassConfig classConfig = trainingTaskClassConfigMapper == null ? null
: trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(classId, task.getTaskKey());
return new EvaluationContext(task, classId, answer, EffectiveTrainingTaskContent.resolve(task, classConfig));
}
@Override
public Map<String, String> generateCommentForTeacher(String classId, String taskId, String studentUserId, JwtUser teacher) {
EvaluationContext context = resolveTeacherContext(classId, taskId, studentUserId, teacher, false);
AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask(
studentUserId, context.teachingClassId, context.task.getId());
String assessmentReport = evaluation == null ? "" : StringUtils.trimToEmpty(evaluation.getAssessmentReportJson());
String helpReport = evaluation == null ? "" : StringUtils.trimToEmpty(evaluation.getHelpReportJson());
if (StringUtils.isBlank(assessmentReport)) {
// AI 评分报告是评语的主要依据;助学报告只做补充,没有也能生成
throw new ServiceException(HttpStatus.BAD_REQUEST, "该学生还没有AI评分数据,请先执行AI评分");
}
String comment = normalizeTeacherComment(generateCommentOnce(assessmentReport, helpReport, ""));
if (comment.codePointCount(0, comment.length()) < 30) {
// 验收要求 30-50 字;过短时带纠正指令重写一次,取更长的那条
String instruction = " 上一次生成的评语只有" + comment.codePointCount(0, comment.length())
+ "个字,太短;重新生成的评语必须不少于30个字、不超过50个字。";
String retried = normalizeTeacherComment(generateCommentOnce(assessmentReport, helpReport, instruction));
if (retried.codePointCount(0, retried.length()) > comment.codePointCount(0, comment.length())) {
comment = retried;
}
}
return Collections.singletonMap("comment", comment);
}
private String generateCommentOnce(String assessmentReport, String helpReport, String extraInstruction) {
String rawResponse = qwenChatClient.completeJson(commentSystemPrompt(),
commentUserPrompt(assessmentReport, helpReport) + StringUtils.trimToEmpty(extraInstruction));
JsonNode report = object(rawResponse);
JsonNode commentNode = report.get("comment");
if (commentNode == null || !commentNode.isTextual()) {
throw invalidReport();
}
return commentNode.textValue();
}
private String commentSystemPrompt() {
return "You are a vocational school teacher writing feedback for one student's training task. "
+ "Based on the given AI scoring report (and AI tutoring report if present), write ONE Chinese teacher comment. "
+ "Requirements: 30 to 50 Chinese characters; a single short paragraph; objective, specific and encouraging; "
+ "comment on the performance and give one improvement direction; "
+ "no bullet points, no quotes, no salutation, no prefix or suffix such as 评语. "
+ "Return only JSON: {\"comment\":\"...\"}";
}
private String commentUserPrompt(String assessmentReport, String helpReport) {
return "AI评分报告: " + assessmentReport + "\nAI助学报告: " + (StringUtils.isBlank(helpReport) ? "(无)" : helpReport);
}
/** 评语超 50 字时按句子边界截断;首尾引号一并去掉。 */
private String normalizeTeacherComment(String comment) {
String value = StringUtils.trimToEmpty(comment)
.replaceAll("^[\"'“”‘’]+", "")
.replaceAll("[\"'“”‘’]+$", "")
.trim();
if (value.codePointCount(0, value.length()) > 50) {
value = truncateRationaleAtSentenceBoundary(value, 50);
}
return value;
}
private void completeAssessmentAndWriteScore(AiTrainingEvaluation completed, private void completeAssessmentAndWriteScore(AiTrainingEvaluation completed,
String answerId, String answerId,
int score, int score,
@ -199,16 +379,40 @@ 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) {
return getOrCreate(context, user.getUserId());
}
private AiTrainingEvaluation getOrCreate(EvaluationContext context, String studentUserId) {
AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask( AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask(
user.getUserId(), context.teachingClassId, context.task.getId()); studentUserId, context.teachingClassId, context.task.getId());
if (evaluation != null) { if (evaluation != null) {
return evaluation; return evaluation;
} }
Date now = new Date(); Date now = new Date();
AiTrainingEvaluation created = new AiTrainingEvaluation(); AiTrainingEvaluation created = new AiTrainingEvaluation();
created.setId(UUID.randomUUID().toString()); created.setId(UUID.randomUUID().toString());
created.setStudentUserId(user.getUserId()); created.setStudentUserId(studentUserId);
created.setTeachingClassId(context.teachingClassId); created.setTeachingClassId(context.teachingClassId);
created.setTaskId(context.task.getId()); created.setTaskId(context.task.getId());
created.setTaskKey(context.task.getTaskKey()); created.setTaskKey(context.task.getTaskKey());
@ -218,7 +422,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
created.setUpdateTime(now); created.setUpdateTime(now);
aiTrainingEvaluationMapper.insertIgnore(created); aiTrainingEvaluationMapper.insertIgnore(created);
evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask( evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask(
user.getUserId(), context.teachingClassId, context.task.getId()); studentUserId, context.teachingClassId, context.task.getId());
if (evaluation == null) { if (evaluation == null) {
throw new ServiceException(HttpStatus.SERVICE_UNAVAILABLE, "AI evaluation is temporarily unavailable"); throw new ServiceException(HttpStatus.SERVICE_UNAVAILABLE, "AI evaluation is temporarily unavailable");
} }
@ -230,6 +434,11 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
user.getUserId(), context.teachingClassId, context.task.getId())); user.getUserId(), context.teachingClassId, context.task.getId()));
} }
private AiTrainingEvaluationView currentView(EvaluationContext context, String studentUserId) {
return toView(aiTrainingEvaluationMapper.findByStudentClassAndTask(
studentUserId, context.teachingClassId, context.task.getId()));
}
private void requireStudent(JwtUser user) { private void requireStudent(JwtUser user) {
if (user == null || StringUtils.isBlank(user.getUserId())) { if (user == null || StringUtils.isBlank(user.getUserId())) {
throw new ServiceException(HttpStatus.UNAUTHORIZED, "Please login first"); throw new ServiceException(HttpStatus.UNAUTHORIZED, "Please login first");
@ -310,6 +519,59 @@ 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);
@ -353,19 +615,22 @@ 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"));
if (!expected.name.equals(StringUtils.trimToEmpty(criterion.path("name").asText())) // maxScore 以评分计划为准:模型偶发把 maxScore 抄错导致合计≠100,
|| max == null || earned == null || max != expected.maxScore || earned < 0 || earned > max) { // 不再校验模型返回的 maxScore,只用计划值判定得分范围。
if (!normalizeCriterionName(expected.name).equals(normalizeCriterionName(criterion.path("name").asText()))
|| earned == null || earned < 0 || earned > expected.maxScore) {
throw invalidReport(); throw invalidReport();
} }
maxTotal += max; maxTotal += expected.maxScore;
earnedTotal += earned; earnedTotal += earned;
} }
if (maxTotal != 100 || earnedTotal != score) { // 模型偶发把总分写错(与逐项得分之和不一致):以逐项得分为准并回写报告,
throw invalidReport(); // 避免仅因算术错误整份作废。
if (earnedTotal != score) {
((ObjectNode) root).put("score", earnedTotal);
} }
return score; return earnedTotal;
} }
private List<AssessmentCriterionPlan> assessmentCriterionPlan(String steps) { private List<AssessmentCriterionPlan> assessmentCriterionPlan(String steps) {
@ -486,6 +751,16 @@ 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.";
} }
@ -497,7 +772,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
private String assessmentSystemPrompt(List<AssessmentCriterionPlan> criterionPlan) { private String assessmentSystemPrompt(List<AssessmentCriterionPlan> criterionPlan) {
return "You are a training assessor. Return exactly one JSON object, with no Markdown fence or other text. " return "You are a training assessor. Return exactly one JSON object, with no Markdown fence or other text. "
+ "Required fields: score (integer total earned from criteria), overallComment (Chinese summary), " + "Required fields: score (integer total earned from criteria), overallComment (Chinese summary of 30 to 50 Chinese characters), "
+ "criteria (array of objects with name, maxScore, score, rationale), strengths (array of Chinese strings), " + "criteria (array of objects with name, maxScore, score, rationale), strengths (array of Chinese strings), "
+ "and improvements (array of Chinese strings). " + "and improvements (array of Chinese strings). "
+ "Assess each step independently using the task requirements, this student's saved answers and available case/file evidence. " + "Assess each step independently using the task requirements, this student's saved answers and available case/file evidence. "
@ -508,8 +783,11 @@ 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. Each name and maxScore must exactly match this required step plan: " + "Return exactly " + criterionPlan.size() + " criteria in this exact order. "
+ "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 "
@ -522,14 +800,17 @@ 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.";
} }
@ -542,22 +823,87 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
view.setHelpReportJson(evaluation.getHelpReportJson()); view.setHelpReportJson(evaluation.getHelpReportJson());
view.setHelpCompletedAt(evaluation.getHelpCompletedAt()); view.setHelpCompletedAt(evaluation.getHelpCompletedAt());
view.setAssessmentScore(evaluation.getAssessmentScore()); view.setAssessmentScore(evaluation.getAssessmentScore());
view.setAssessmentComment(assessmentComment(evaluation.getAssessmentReportJson()));
view.setAssessmentReportJson(evaluation.getAssessmentReportJson()); view.setAssessmentReportJson(evaluation.getAssessmentReportJson());
view.setAssessmentCompletedAt(evaluation.getAssessmentCompletedAt()); view.setAssessmentCompletedAt(evaluation.getAssessmentCompletedAt());
} }
return view; return view;
} }
private String cappedMessage(RuntimeException exception) { private String assessmentComment(String reportJson) {
String message = StringUtils.defaultIfBlank(exception.getMessage(), "AI evaluation failed"); if (StringUtils.isBlank(reportJson)) return "";
return StringUtils.left(message, MAX_ERROR_LENGTH); try {
JsonNode report = objectMapper.readTree(reportJson);
String comment = StringUtils.trimToEmpty(report.path("overallComment").asText());
if (StringUtils.isBlank(comment)) comment = StringUtils.trimToEmpty(report.path("overallDiagnosis").asText());
if (StringUtils.isBlank(comment)) comment = StringUtils.trimToEmpty(report.path("strengths").path(0).asText());
if (StringUtils.isBlank(comment)) return "";
if (comment.codePointCount(0, comment.length()) < 30 && report.path("criteria").isArray()) {
for (JsonNode criterion : report.path("criteria")) {
String rationale = StringUtils.trimToEmpty(criterion.path("rationale").asText());
if (StringUtils.isNotBlank(rationale)) comment = comment + rationale;
if (comment.codePointCount(0, comment.length()) >= 30) break;
}
}
int count = comment.codePointCount(0, comment.length());
if (count > 50) comment = comment.substring(0, comment.offsetByCodePoints(0, 50));
return comment;
} catch (Exception ignored) {
return "";
}
} }
private ServiceException asServiceException(RuntimeException exception) { /**
if (exception instanceof ServiceException) { * 落库的失败原因:只保存我们自己抛出的 ServiceException 文案,
return (ServiceException) exception; * 其余异常(数据库、网络等)的原文只写日志,避免把 SQL 等长文本存进库或带给前端。
*/
private String storedFailureMessage(RuntimeException exception) {
if (exception instanceof ServiceException && StringUtils.isNotBlank(exception.getMessage())) {
return StringUtils.left(exception.getMessage(), MAX_ERROR_LENGTH);
} }
return new ServiceException(HttpStatus.BAD_GATEWAY, "AI evaluation failed"); 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) {
HttpStatus status = exception instanceof ServiceException && ((ServiceException) exception).getCode() != null
? ((ServiceException) exception).getCode()
: HttpStatus.BAD_GATEWAY;
return new ServiceException(status, FAILURE_MESSAGE);
} }
private static class AssessmentCriterionPlan { private static class AssessmentCriterionPlan {

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

@ -8,6 +8,7 @@ import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.SchoolDefaultTaskInitializationMapper; import com.sztzjy.linkCommerce.mapper.SchoolDefaultTaskInitializationMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.TrainingTaskService;
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.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Service @Service
@ -30,6 +32,8 @@ public class SchoolDefaultTaskServiceImpl implements SchoolDefaultTaskService {
@Autowired(required = false) @Autowired(required = false)
SchoolDefaultTaskInitializationMapper initializationMapper; SchoolDefaultTaskInitializationMapper initializationMapper;
@Autowired(required = false) @Autowired(required = false)
TrainingTaskService trainingTaskService;
@Autowired(required = false)
@Qualifier("asyncTaskExecutor") @Qualifier("asyncTaskExecutor")
AsyncTaskExecutor taskExecutor; AsyncTaskExecutor taskExecutor;
@ -117,19 +121,37 @@ public class SchoolDefaultTaskServiceImpl implements SchoolDefaultTaskService {
if (platformTasks.isEmpty()) { if (platformTasks.isEmpty()) {
throw new IllegalStateException("平台基线实训任务为空"); throw new IllegalStateException("平台基线实训任务为空");
} }
Set<String> defaultOpenTaskKeys = defaultOpenTaskKeys();
for (TaskAllocation source : platformTasks) { for (TaskAllocation source : platformTasks) {
TaskAllocation copy = new TaskAllocation(); TaskAllocation copy = new TaskAllocation();
copy.setId(UUID.randomUUID().toString()); copy.setId(UUID.randomUUID().toString());
copy.setClassId(schoolDefaultClassId(schoolId)); copy.setClassId(schoolDefaultClassId(schoolId));
copy.setSchoolId(schoolId); copy.setSchoolId(schoolId);
copy.setModule(source.getModule()); copy.setModule(source.getModule());
copy.setDisabledStatus(source.getDisabledStatus()); copy.setDisabledStatus(resolveInitialDisabledStatus(source, defaultOpenTaskKeys));
copy.setSort(source.getSort()); copy.setSort(source.getSort());
taskAllocationMapper.insert(copy); taskAllocationMapper.insert(copy);
} }
markReady(schoolId); markReady(schoolId);
} }
/**
* 学校默认任务只默认开放第一个项目的前三个任务(与教学班未配置时的学生端一致),
* 其余任务保留完整目录但置为停用,学校管理员可在默认实训任务配置页手动放开。
*/
private byte resolveInitialDisabledStatus(TaskAllocation source, Set<String> defaultOpenTaskKeys) {
if (defaultOpenTaskKeys.isEmpty() || StringUtils.isBlank(source.getModule())) {
return source.getDisabledStatus() == null ? (byte) 1 : source.getDisabledStatus();
}
return defaultOpenTaskKeys.contains(source.getModule().trim()) ? (byte) 0 : (byte) 1;
}
private Set<String> defaultOpenTaskKeys() {
return trainingTaskService == null
? Collections.emptySet()
: trainingTaskService.getDefaultPublishedTaskKeys();
}
private void initializeInBackground(String schoolId) { private void initializeInBackground(String schoolId) {
markRunning(schoolId); markRunning(schoolId);
try { try {

@ -74,6 +74,10 @@ 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 -> {
@ -86,8 +90,13 @@ public class SchoolServiceImpl implements SchoolService {
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
// 创建 PageInfo 对象,将 DTO 列表封装成分页结果 PageInfo<SchoolFacultyDto> pageInfo = new PageInfo<>();
return new PageInfo<>(dtoList); pageInfo.setTotal(sourcePage.getTotal());
pageInfo.setPageNum(sourcePage.getPageNum());
pageInfo.setPageSize(sourcePage.getPageSize());
pageInfo.setPages(sourcePage.getPages());
pageInfo.setList(dtoList);
return pageInfo;
} }
@Override @Override
@ -131,6 +140,10 @@ 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()
@ -201,8 +214,13 @@ public class SchoolServiceImpl implements SchoolService {
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
// 创建 PageInfo 对象,将 DTO 列表封装成分页结果 PageInfo<SchoolClassDto> pageInfo = new PageInfo<>();
return new PageInfo<>(dtoList); pageInfo.setTotal(sourcePage.getTotal());
pageInfo.setPageNum(sourcePage.getPageNum());
pageInfo.setPageSize(sourcePage.getPageSize());
pageInfo.setPages(sourcePage.getPages());
pageInfo.setList(dtoList);
return pageInfo;
} }
@Override @Override

@ -18,11 +18,25 @@ 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;
@ -48,25 +62,7 @@ public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabase
@Override @Override
public ResultEntity<StuUserProfileDto> getBaseInfo(String userId) { public ResultEntity<StuUserProfileDto> getBaseInfo(String userId) {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>(BUILT_IN_TABLE_NAMES);
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();
@ -176,7 +172,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.selectByExample(userAttributeBehaviorExample); List<StuUserComment> userAttributeBehaviorList = stuUserCommentMapper.selectByExampleWithBLOBs(userAttributeBehaviorExample);
if (userAttributeBehaviorList.isEmpty()) { if (userAttributeBehaviorList.isEmpty()) {
throw new ServiceException(HttpStatus.OK, "未查询到用户数据!"); throw new ServiceException(HttpStatus.OK, "未查询到用户数据!");
} }
@ -186,10 +182,7 @@ public class StuUserProfileDatabaseServiceImpl implements StuUserProfileDatabase
} }
String[] list = {"RFM明细数据","AARRR传播数据","超市销售数据","集团商品销售总表","用户登录表","用户留存分析","用户粘性分析","AARRR示例数据", if (BUILT_IN_TABLE_NAMES.contains(tableName))
"帕累托ABC分析","智能健身镜产品数据","智能卫浴镜产品数据","智能美妆镜产品数据"};
List<String> collect = Arrays.stream(list).collect(Collectors.toList());
if (collect.contains(tableName))
{ {
//开启分页 //开启分页
PageHelper.startPage(index, size); PageHelper.startPage(index, size);

@ -1,11 +1,217 @@
package com.sztzjy.linkCommerce.service.impl; package com.sztzjy.linkCommerce.service.impl;
import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.*; import com.sztzjy.linkCommerce.entity.dto.*; import com.sztzjy.linkCommerce.mapper.*; import com.sztzjy.linkCommerce.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
import java.math.*; import java.util.*; import com.sztzjy.linkCommerce.config.security.JwtUser;
@Service public class StudentExperimentReportServiceImpl implements StudentExperimentReportService { import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer;
@Autowired public StudentTeachingClassResolver studentTeachingClassResolver; @Autowired public SchoolDefaultTaskService schoolDefaultTaskService; @Autowired public TrainingTaskMapper trainingTaskMapper; @Autowired public StudentTrainingAnswerMapper studentTrainingAnswerMapper; @Autowired(required=false) public TeachingClassScoreWeightMapper teachingClassScoreWeightMapper; @Autowired(required=false) public TaskAllocationMapper taskAllocationMapper; import com.sztzjy.linkCommerce.entity.TaskAllocation;
private static final String[] NAMES={"互联网产品开发基础认知","市场洞察与需求分析","产品规划与设计","产品开发与测试验证","产品上线与运营推广","综合实训"}; import com.sztzjy.linkCommerce.entity.TaskAllocationExample;
private static final BigDecimal[] DEFAULTS={new BigDecimal("0.10"),new BigDecimal("0.30"),new BigDecimal("0.10"),new BigDecimal("0.10"),new BigDecimal("0.20"),new BigDecimal("0.20")}; import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight;
public StudentExperimentReportDTO getCurrentReport(JwtUser user){ String classId=studentTeachingClassResolver.resolveRequired(user); StudentExperimentReportDTO report=new StudentExperimentReportDTO(); report.setTeachingClassId(classId); List<StudentExperimentReportModuleDTO> modules=new ArrayList<>(); Map<String,StudentExperimentReportModuleDTO> index=new HashMap<>(); BigDecimal[] weights=weights(classId); boolean comprehensiveParticipating=hasPublishedComprehensiveTask(classId); for(int i=0;i<NAMES.length;i++){StudentExperimentReportModuleDTO m=new StudentExperimentReportModuleDTO();m.setModuleName(NAMES[i]);m.setWeight(weights[i]);m.setParticipating(i<NAMES.length-1||comprehensiveParticipating);modules.add(m);index.put(NAMES[i],m);} List<StudentExperimentReportTaskDTO> tasks=new ArrayList<>(); for(TaskAllocation a: schoolDefaultTaskService.resolveForTeachingClass(classId)){if(a==null||a.getDisabledStatus()!=null&&a.getDisabledStatus()==1||TaskAllocation.PUBLICATION_MARKER.equals(a.getModule())||("comprehensive-case-training".equals(a.getModule())&&!comprehensiveParticipating))continue; TrainingTask t=trainingTaskMapper.selectByTaskKey(a.getModule()); if(t==null||!index.containsKey(t.getProjectName()))continue; StudentTrainingAnswer ans=studentTrainingAnswerMapper.selectByStudentClassAndTask(user.getUserId(),classId,t.getId()); StudentExperimentReportModuleDTO m=index.get(t.getProjectName());m.setTotalTaskCount(m.getTotalTaskCount()+1);int score=ans==null||ans.getAiAssessmentScore()==null?0:ans.getAiAssessmentScore();m.setScore(m.getScore().add(BigDecimal.valueOf(score)));if(ans!=null&&(Boolean.TRUE.equals(ans.getSubmitted())||"COMPLETED".equals(ans.getProgressStatus())))m.setCompletedTaskCount(m.getCompletedTaskCount()+1);StudentExperimentReportTaskDTO row=new StudentExperimentReportTaskDTO();row.setTaskName(t.getTaskName());row.setModuleName(t.getProjectName());if(ans!=null){row.setProgressStatus(ans.getProgressStatus());row.setSubmitted(ans.getSubmitted());row.setAiAssessmentScore(ans.getAiAssessmentScore());}tasks.add(row);} BigDecimal total=BigDecimal.ZERO;BigDecimal participatingWeight=BigDecimal.ZERO;for(StudentExperimentReportModuleDTO m:modules){BigDecimal score=m.getTotalTaskCount()==0?BigDecimal.ZERO:m.getScore().divide(BigDecimal.valueOf(m.getTotalTaskCount()),2,BigDecimal.ROUND_HALF_UP);m.setScore(score);if(Boolean.TRUE.equals(m.getParticipating())){BigDecimal weight=m.getWeight()==null?BigDecimal.ZERO:m.getWeight();total=total.add(score.multiply(weight));participatingWeight=participatingWeight.add(weight);}}report.setTotalScore(participatingWeight.signum()==0?BigDecimal.ZERO:total.divide(participatingWeight,2,BigDecimal.ROUND_HALF_UP));report.setModules(modules);report.setTasks(tasks);return report; } import com.sztzjy.linkCommerce.entity.TrainingTask;
private boolean hasPublishedComprehensiveTask(String classId){if(taskAllocationMapper==null)return false;TaskAllocationExample example=new TaskAllocationExample();example.createCriteria().andClassIdEqualTo(classId);List<TaskAllocation> allocations=taskAllocationMapper.selectByExample(example);boolean published=false;boolean enabled=false;for(TaskAllocation allocation:allocations==null?Collections.<TaskAllocation>emptyList():allocations){if(allocation==null)continue;if(TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule()))published=true;else if("comprehensive-case-training".equals(allocation.getModule())&&(allocation.getDisabledStatus()==null||allocation.getDisabledStatus()==0))enabled=true;}return published&&enabled;} import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportDTO;
private BigDecimal[] weights(String id){TeachingClassScoreWeight w=teachingClassScoreWeightMapper==null?null:teachingClassScoreWeightMapper.selectByTeachingClassId(id);if(w==null)return DEFAULTS;return new BigDecimal[]{w.getFoundationWeight(),w.getMarketInsightWeight(),w.getPlanningDesignWeight(),w.getDevelopmentValidationWeight(),w.getLaunchOperationWeight(),w.getComprehensiveTrainingWeight()};} import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportModuleDTO;
import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportTaskDTO;
import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.mapper.TeachingClassScoreWeightMapper;
import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.StudentExperimentReportService;
import com.sztzjy.linkCommerce.service.StudentTeachingClassResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class StudentExperimentReportServiceImpl implements StudentExperimentReportService {
@Autowired
public StudentTeachingClassResolver studentTeachingClassResolver;
@Autowired
public SchoolDefaultTaskService schoolDefaultTaskService;
@Autowired
public TrainingTaskMapper trainingTaskMapper;
@Autowired
public StudentTrainingAnswerMapper studentTrainingAnswerMapper;
@Autowired(required = false)
public TeachingClassScoreWeightMapper teachingClassScoreWeightMapper;
@Autowired(required = false)
public TaskAllocationMapper taskAllocationMapper;
private static final String[] NAMES = {
"互联网产品开发基础认知", "市场洞察与需求分析", "产品规划与设计",
"产品开发与测试验证", "产品上线与运营推广", "综合实训"
};
private static final BigDecimal[] DEFAULTS = {
new BigDecimal("0.10"), new BigDecimal("0.30"), new BigDecimal("0.10"),
new BigDecimal("0.10"), new BigDecimal("0.20"), new BigDecimal("0.20")
};
@Override
public StudentExperimentReportDTO getCurrentReport(JwtUser user) {
String classId = studentTeachingClassResolver.resolveRequired(user);
StudentExperimentReportDTO report = new StudentExperimentReportDTO();
report.setTeachingClassId(classId);
List<StudentExperimentReportModuleDTO> modules = new ArrayList<>();
Map<String, StudentExperimentReportModuleDTO> moduleByName = new HashMap<>();
Map<String, Integer> moduleIndex = new HashMap<>();
BigDecimal[] weights = weights(classId);
boolean comprehensiveParticipating = hasPublishedComprehensiveTask(classId);
for (int i = 0; i < NAMES.length; i++) {
StudentExperimentReportModuleDTO module = new StudentExperimentReportModuleDTO();
module.setModuleName(NAMES[i]);
module.setWeight(weights[i]);
module.setParticipating(i < NAMES.length - 1 || comprehensiveParticipating);
modules.add(module);
moduleByName.put(NAMES[i], module);
moduleIndex.put(NAMES[i], i);
}
BigDecimal[] aiSums = zeroScores();
BigDecimal[] teacherSums = zeroScores();
int[] taskCounts = new int[NAMES.length];
boolean[] hasAiScore = new boolean[NAMES.length];
boolean[] hasTeacherScore = new boolean[NAMES.length];
List<StudentExperimentReportTaskDTO> tasks = new ArrayList<>();
List<TaskAllocation> allocations = schoolDefaultTaskService.resolveForTeachingClass(classId);
for (TaskAllocation allocation : allocations == null
? Collections.<TaskAllocation>emptyList() : allocations) {
if (allocation == null || (allocation.getDisabledStatus() != null && allocation.getDisabledStatus() == 1)
|| TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())
|| ("comprehensive-case-training".equals(allocation.getModule()) && !comprehensiveParticipating)) {
continue;
}
TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule());
Integer index = task == null ? null : moduleIndex.get(task.getProjectName());
if (index == null) {
continue;
}
StudentTrainingAnswer answer = studentTrainingAnswerMapper
.selectByStudentClassAndTask(user.getUserId(), classId, task.getId());
StudentExperimentReportModuleDTO module = moduleByName.get(task.getProjectName());
module.setTotalTaskCount(module.getTotalTaskCount() + 1);
taskCounts[index]++;
StudentExperimentReportTaskDTO row = new StudentExperimentReportTaskDTO();
row.setTaskName(task.getTaskName());
row.setModuleName(task.getProjectName());
if (answer != null) {
row.setProgressStatus(answer.getProgressStatus());
row.setSubmitted(answer.getSubmitted());
row.setAiAssessmentScore(answer.getAiAssessmentScore());
row.setTeacherScore(answer.getTeacherScore());
if (answer.getAiAssessmentScore() != null) {
aiSums[index] = aiSums[index].add(BigDecimal.valueOf(answer.getAiAssessmentScore()));
hasAiScore[index] = true;
}
if (answer.getTeacherScore() != null) {
teacherSums[index] = teacherSums[index].add(answer.getTeacherScore());
hasTeacherScore[index] = true;
}
if (Boolean.TRUE.equals(answer.getSubmitted())
|| "COMPLETED".equals(answer.getProgressStatus())) {
module.setCompletedTaskCount(module.getCompletedTaskCount() + 1);
}
}
tasks.add(row);
}
boolean anyAiScore = false;
boolean anyTeacherScore = false;
for (int i = 0; i < NAMES.length; i++) {
BigDecimal aiAverage = average(aiSums[i], taskCounts[i], hasAiScore[i]);
BigDecimal teacherAverage = average(teacherSums[i], taskCounts[i], hasTeacherScore[i]);
anyAiScore = anyAiScore || aiAverage != null;
anyTeacherScore = anyTeacherScore || teacherAverage != null;
// 教师评分优先;本模块没有教师评分时才使用 AI 评分。
modules.get(i).setScore(teacherAverage != null ? teacherAverage : aiAverage);
}
BigDecimal aiTotal = aggregate(modules, aiSums, taskCounts, hasAiScore);
BigDecimal teacherTotal = aggregate(modules, teacherSums, taskCounts, hasTeacherScore);
report.setAiAssessmentScore(anyAiScore ? aiTotal : null);
report.setTeacherScore(anyTeacherScore ? teacherTotal : null);
// 总成绩同样遵循教师评分优先,且没有任何成绩时返回 null。
report.setTotalScore(anyTeacherScore ? teacherTotal : (anyAiScore ? aiTotal : null));
report.setModules(modules);
report.setTasks(tasks);
return report;
}
private BigDecimal[] zeroScores() {
BigDecimal[] scores = new BigDecimal[NAMES.length];
for (int i = 0; i < scores.length; i++) {
scores[i] = BigDecimal.ZERO;
}
return scores;
}
private BigDecimal average(BigDecimal sum, int count, boolean present) {
if (!present || count <= 0) {
return null;
}
return sum.divide(BigDecimal.valueOf(count), 2, BigDecimal.ROUND_HALF_UP);
}
private BigDecimal aggregate(List<StudentExperimentReportModuleDTO> modules,
BigDecimal[] sums, int[] taskCounts, boolean[] present) {
BigDecimal total = BigDecimal.ZERO;
BigDecimal participatingWeight = BigDecimal.ZERO;
for (int i = 0; i < modules.size(); i++) {
BigDecimal average = average(sums[i], taskCounts[i], present[i]);
if (average == null || !Boolean.TRUE.equals(modules.get(i).getParticipating())) {
continue;
}
BigDecimal weight = modules.get(i).getWeight() == null
? BigDecimal.ZERO : modules.get(i).getWeight();
total = total.add(average.multiply(weight));
participatingWeight = participatingWeight.add(weight);
}
return participatingWeight.signum() == 0
? BigDecimal.ZERO
: total.divide(participatingWeight, 2, BigDecimal.ROUND_HALF_UP);
}
private boolean hasPublishedComprehensiveTask(String classId) {
if (taskAllocationMapper == null) {
return false;
}
TaskAllocationExample example = new TaskAllocationExample();
example.createCriteria().andClassIdEqualTo(classId);
List<TaskAllocation> allocations = taskAllocationMapper.selectByExample(example);
boolean published = false;
boolean enabled = false;
for (TaskAllocation allocation : allocations == null
? Collections.<TaskAllocation>emptyList() : allocations) {
if (allocation == null) {
continue;
}
if (TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())) {
published = true;
} else if ("comprehensive-case-training".equals(allocation.getModule())
&& (allocation.getDisabledStatus() == null || allocation.getDisabledStatus() == 0)) {
enabled = true;
}
}
return published && enabled;
}
private BigDecimal[] weights(String id) {
TeachingClassScoreWeight weight = teachingClassScoreWeightMapper == null
? null : teachingClassScoreWeightMapper.selectByTeachingClassId(id);
if (weight == null) {
return DEFAULTS;
}
return new BigDecimal[]{
weight.getFoundationWeight(), weight.getMarketInsightWeight(),
weight.getPlanningDesignWeight(), weight.getDevelopmentValidationWeight(),
weight.getLaunchOperationWeight(), weight.getComprehensiveTrainingWeight()
};
}
} }

@ -36,6 +36,7 @@ import java.util.Date;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -142,6 +143,8 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
String incomingGuideManualUrl = task.getGuideManualUrl(); String incomingGuideManualUrl = task.getGuideManualUrl();
String incomingAiManualName = task.getAiManualName(); String incomingAiManualName = task.getAiManualName();
String incomingAiManualUrl = task.getAiManualUrl(); String incomingAiManualUrl = task.getAiManualUrl();
String incomingReferenceAnswerName = task.getReferenceAnswerName();
String incomingReferenceAnswerUrl = task.getReferenceAnswerUrl();
normalizeTask(task); normalizeTask(task);
if (incomingTrainingDataName == null) task.setTrainingDataName(existing.getTrainingDataName()); if (incomingTrainingDataName == null) task.setTrainingDataName(existing.getTrainingDataName());
if (incomingTrainingDataUrl == null) task.setTrainingDataUrl(existing.getTrainingDataUrl()); if (incomingTrainingDataUrl == null) task.setTrainingDataUrl(existing.getTrainingDataUrl());
@ -149,6 +152,8 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
if (incomingGuideManualUrl == null) task.setGuideManualUrl(existing.getGuideManualUrl()); if (incomingGuideManualUrl == null) task.setGuideManualUrl(existing.getGuideManualUrl());
if (incomingAiManualName == null) task.setAiManualName(existing.getAiManualName()); if (incomingAiManualName == null) task.setAiManualName(existing.getAiManualName());
if (incomingAiManualUrl == null) task.setAiManualUrl(existing.getAiManualUrl()); if (incomingAiManualUrl == null) task.setAiManualUrl(existing.getAiManualUrl());
if (incomingReferenceAnswerName == null) task.setReferenceAnswerName(existing.getReferenceAnswerName());
if (incomingReferenceAnswerUrl == null) task.setReferenceAnswerUrl(existing.getReferenceAnswerUrl());
TrainingTask builtInTask = requireBuiltInTask(existing.getTaskKey()); TrainingTask builtInTask = requireBuiltInTask(existing.getTaskKey());
task.setId(id); task.setId(id);
task.setTaskKey(existing.getTaskKey()); task.setTaskKey(existing.getTaskKey());
@ -247,6 +252,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
@Override @Override
public List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) { public List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) {
requireReadableTeachingClass(teachingClassId, operator); requireReadableTeachingClass(teachingClassId, operator);
ensureDefaultTasks(null);
List<TrainingTask> defaults = trainingTaskMapper.selectList(null, null, null); List<TrainingTask> defaults = trainingTaskMapper.selectList(null, null, null);
return filterEnabled(readClassTasks(teachingClassId, defaults), enabledOnly); return filterEnabled(readClassTasks(teachingClassId, defaults), enabledOnly);
} }
@ -313,6 +319,26 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
return toTask(effective); return toTask(effective);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public TrainingTask setClassTaskReferenceAnswerEnabled(String teachingClassId, String taskKey,
Boolean enabled, String operatorId) {
requireOwnedTeachingClass(teachingClassId, operatorId);
ensureDefaultTasks(null);
String normalizedTaskKey = StringUtils.trimToEmpty(taskKey);
ensureClassTasksInitialized(teachingClassId, trainingTaskMapper.selectList(null, null, null), operatorId);
TrainingTaskClassConfig config = trainingTaskClassConfigMapper
.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey);
if (config == null) {
throw new IllegalArgumentException("Training task not found");
}
config.setReferenceAnswerEnabled(Boolean.TRUE.equals(enabled));
config.setUpdateTime(new Date());
trainingTaskClassConfigMapper.updateByPrimaryKeySelective(config);
TrainingTaskClassConfig saved = trainingTaskClassConfigMapper.selectByPrimaryKey(config.getId());
return toTask(saved == null ? config : saved);
}
private void indexCaseMaterial(TrainingTask task, EffectiveTrainingTaskContent content, String teachingClassId) { private void indexCaseMaterial(TrainingTask task, EffectiveTrainingTaskContent content, String teachingClassId) {
if (trainingKnowledgeService == null || task == null) return; if (trainingKnowledgeService == null || task == null) return;
try { try {
@ -441,12 +467,12 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
} }
/** /**
* @return null when this teaching class has never been configured (default: all tasks), * @return the explicitly published task keys, or the first three tasks in the first
* otherwise the explicitly published task keys, which may be empty. * project when this teaching class has never been configured.
*/ */
private Set<String> getPublishedTaskKeys(String teachingClassId) { private Set<String> getPublishedTaskKeys(String teachingClassId) {
if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) { if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) {
return null; return getDefaultPublishedTaskKeys();
} }
TaskAllocationExample example = new TaskAllocationExample(); TaskAllocationExample example = new TaskAllocationExample();
example.createCriteria().andClassIdEqualTo(teachingClassId); example.createCriteria().andClassIdEqualTo(teachingClassId);
@ -462,7 +488,35 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
} }
} }
} }
return configured ? taskKeys : null; return configured ? taskKeys : getDefaultPublishedTaskKeys();
}
@Override
public Set<String> getDefaultPublishedTaskKeys() {
List<TrainingTask> defaults = sortTasks(trainingTaskMapper.selectList(null, null, null));
Set<String> taskKeys = new LinkedHashSet<>();
if (defaults == null) {
return taskKeys;
}
String firstProjectName = null;
for (TrainingTask task : defaults) {
if (task == null || StringUtils.isBlank(task.getTaskKey())
|| requiresExplicitPublication(task.getTaskKey())) {
continue;
}
String projectName = StringUtils.defaultString(task.getProjectName(), "其他");
if (firstProjectName == null) {
firstProjectName = projectName;
}
if (!StringUtils.equals(firstProjectName, projectName)) {
continue;
}
taskKeys.add(task.getTaskKey());
if (taskKeys.size() >= 3) {
break;
}
}
return taskKeys;
} }
private String requireStudentTeachingClass(JwtUser student) { private String requireStudentTeachingClass(JwtUser student) {
@ -647,6 +701,14 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
update.setAiManualUrl(updatedTask.getAiManualUrl()); update.setAiManualUrl(updatedTask.getAiManualUrl());
changed = true; changed = true;
} }
if (shouldSynchronize(classTask.getReferenceAnswerName(), previousTask.getReferenceAnswerName(), updatedTask.getReferenceAnswerName())) {
update.setReferenceAnswerName(updatedTask.getReferenceAnswerName());
changed = true;
}
if (shouldSynchronize(classTask.getReferenceAnswerUrl(), previousTask.getReferenceAnswerUrl(), updatedTask.getReferenceAnswerUrl())) {
update.setReferenceAnswerUrl(updatedTask.getReferenceAnswerUrl());
changed = true;
}
if (changed) { if (changed) {
update.setUpdateTime(new Date()); update.setUpdateTime(new Date());
trainingTaskClassConfigMapper.updateByPrimaryKeySelective(update); trainingTaskClassConfigMapper.updateByPrimaryKeySelective(update);
@ -723,6 +785,9 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
task.setGuideManualUrl(config.getGuideManualUrl()); task.setGuideManualUrl(config.getGuideManualUrl());
task.setAiManualName(config.getAiManualName()); task.setAiManualName(config.getAiManualName());
task.setAiManualUrl(config.getAiManualUrl()); task.setAiManualUrl(config.getAiManualUrl());
task.setReferenceAnswerName(config.getReferenceAnswerName());
task.setReferenceAnswerUrl(config.getReferenceAnswerUrl());
task.setReferenceAnswerEnabled(config.getReferenceAnswerEnabled());
task.setSort(config.getSort()); task.setSort(config.getSort());
task.setEnabled(config.getEnabled()); task.setEnabled(config.getEnabled());
task.setCreatedBy(config.getCreatedBy()); task.setCreatedBy(config.getCreatedBy());
@ -751,6 +816,8 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
config.setGuideManualUrl(task.getGuideManualUrl()); config.setGuideManualUrl(task.getGuideManualUrl());
config.setAiManualName(task.getAiManualName()); config.setAiManualName(task.getAiManualName());
config.setAiManualUrl(task.getAiManualUrl()); config.setAiManualUrl(task.getAiManualUrl());
config.setReferenceAnswerName(task.getReferenceAnswerName());
config.setReferenceAnswerUrl(task.getReferenceAnswerUrl());
config.setSort(task.getSort()); config.setSort(task.getSort());
config.setEnabled(task.getEnabled()); config.setEnabled(task.getEnabled());
config.setCreatedBy(operatorId); config.setCreatedBy(operatorId);
@ -780,6 +847,8 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
"guide_manual_url varchar(500) NULL COMMENT '实训指导手册地址'," + "guide_manual_url varchar(500) NULL COMMENT '实训指导手册地址'," +
"ai_manual_name varchar(255) NULL COMMENT 'AI排错指导手册名称'," + "ai_manual_name varchar(255) NULL COMMENT 'AI排错指导手册名称'," +
"ai_manual_url varchar(500) NULL COMMENT 'AI排错指导手册地址'," + "ai_manual_url varchar(500) NULL COMMENT 'AI排错指导手册地址'," +
"reference_answer_name varchar(255) NULL COMMENT '参考答案名称'," +
"reference_answer_url varchar(500) NULL COMMENT '参考答案地址'," +
"sort int DEFAULT 0 COMMENT '排序'," + "sort int DEFAULT 0 COMMENT '排序'," +
"enabled bit(1) DEFAULT b'1' COMMENT '是否启用'," + "enabled bit(1) DEFAULT b'1' COMMENT '是否启用'," +
"created_by varchar(64) NULL COMMENT '创建人'," + "created_by varchar(64) NULL COMMENT '创建人'," +
@ -793,6 +862,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
jdbcTemplate.execute("ALTER TABLE training_task CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); jdbcTemplate.execute("ALTER TABLE training_task CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
ensureTextColumn("training_task", "step_one_questions", "第一步选择题JSON数组"); ensureTextColumn("training_task", "step_one_questions", "第一步选择题JSON数组");
ensureMaterialColumns("training_task"); ensureMaterialColumns("training_task");
ensureReferenceAnswerColumns("training_task");
trainingTaskTableChecked = true; trainingTaskTableChecked = true;
} }
@ -820,6 +890,9 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
"guide_manual_url varchar(500) NULL," + "guide_manual_url varchar(500) NULL," +
"ai_manual_name varchar(255) NULL," + "ai_manual_name varchar(255) NULL," +
"ai_manual_url varchar(500) NULL," + "ai_manual_url varchar(500) NULL," +
"reference_answer_name varchar(255) NULL," +
"reference_answer_url varchar(500) NULL," +
"reference_answer_enabled bit(1) NOT NULL DEFAULT b'0' COMMENT '是否向学生开放参考答案'," +
"sort int DEFAULT 0," + "sort int DEFAULT 0," +
"enabled bit(1) DEFAULT b'1'," + "enabled bit(1) DEFAULT b'1'," +
"created_by varchar(64) NULL," + "created_by varchar(64) NULL," +
@ -832,9 +905,27 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Class scoped training task config'"); ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Class scoped training task config'");
ensureTextColumn("training_task_class_config", "step_one_questions", "第一步选择题JSON数组"); ensureTextColumn("training_task_class_config", "step_one_questions", "第一步选择题JSON数组");
ensureMaterialColumns("training_task_class_config"); ensureMaterialColumns("training_task_class_config");
ensureReferenceAnswerColumns("training_task_class_config");
ensureReferenceAnswerVisibilityColumn("training_task_class_config");
trainingTaskClassConfigTableChecked = true; trainingTaskClassConfigTableChecked = true;
} }
private void ensureReferenceAnswerVisibilityColumn(String tableName) {
if (jdbcTemplate == null) {
return;
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM information_schema.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'reference_answer_enabled'",
Integer.class, tableName);
if (count != null && count > 0) {
return;
}
jdbcTemplate.execute("ALTER TABLE " + tableName
+ " ADD COLUMN reference_answer_enabled bit(1) NOT NULL DEFAULT b'0' "
+ "COMMENT '是否向学生开放参考答案' AFTER reference_answer_url");
}
/** Add the field for deployments that already created the task tables before this feature existed. */ /** Add the field for deployments that already created the task tables before this feature existed. */
private void ensureTextColumn(String tableName, String columnName, String comment) { private void ensureTextColumn(String tableName, String columnName, String comment) {
if (jdbcTemplate == null) { if (jdbcTemplate == null) {
@ -872,6 +963,23 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
+ " ADD COLUMN ai_manual_url varchar(500) NULL AFTER ai_manual_name"); + " ADD COLUMN ai_manual_url varchar(500) NULL AFTER ai_manual_name");
} }
/** 补齐参考答案列:已有四类资料列的部署需要追加参考答案两列。 */
private void ensureReferenceAnswerColumns(String tableName) {
if (jdbcTemplate == null) {
return;
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM information_schema.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'reference_answer_url'",
Integer.class, tableName);
if (count != null && count > 0) {
return;
}
jdbcTemplate.execute("ALTER TABLE " + tableName
+ " ADD COLUMN reference_answer_name varchar(255) NULL AFTER ai_manual_url,"
+ " ADD COLUMN reference_answer_url varchar(500) NULL AFTER reference_answer_name");
}
TrainingTask buildTask(TrainingTaskImportDTO row, int rowNumber, List<String> errors) { TrainingTask buildTask(TrainingTaskImportDTO row, int rowNumber, List<String> errors) {
String projectName = StringUtils.trimToEmpty(row.getProjectName()); String projectName = StringUtils.trimToEmpty(row.getProjectName());
String taskKey = StringUtils.trimToEmpty(row.getTaskKey()); String taskKey = StringUtils.trimToEmpty(row.getTaskKey());
@ -952,11 +1060,24 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
task.setGuideManualUrl(StringUtils.trimToEmpty(task.getGuideManualUrl())); task.setGuideManualUrl(StringUtils.trimToEmpty(task.getGuideManualUrl()));
task.setAiManualName(StringUtils.trimToEmpty(task.getAiManualName())); task.setAiManualName(StringUtils.trimToEmpty(task.getAiManualName()));
task.setAiManualUrl(StringUtils.trimToEmpty(task.getAiManualUrl())); task.setAiManualUrl(StringUtils.trimToEmpty(task.getAiManualUrl()));
task.setReferenceAnswerName(StringUtils.trimToEmpty(task.getReferenceAnswerName()));
task.setReferenceAnswerUrl(StringUtils.trimToEmpty(task.getReferenceAnswerUrl()));
validateReferenceAnswerFile(task.getReferenceAnswerName());
if (task.getEnabled() == null) { if (task.getEnabled() == null) {
task.setEnabled(Boolean.TRUE); task.setEnabled(Boolean.TRUE);
} }
} }
private void validateReferenceAnswerFile(String fileName) {
if (StringUtils.isBlank(fileName)) {
return;
}
String normalized = fileName.trim().toLowerCase(Locale.ROOT);
if (!normalized.endsWith(".doc") && !normalized.endsWith(".docx")) {
throw new IllegalArgumentException("参考答案仅支持Word文件(.doc或.docx)");
}
}
private TrainingTask requireBuiltInTask(String taskKey) { private TrainingTask requireBuiltInTask(String taskKey) {
TrainingTask builtInTask = getBuiltInTask(taskKey); TrainingTask builtInTask = getBuiltInTask(taskKey);
if (builtInTask == null) { if (builtInTask == null) {

@ -38,25 +38,7 @@ 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. 校验院校名称 // 1. 校验教师姓名(教师归属的学校/院系/专业直接取自当前操作用户,不再从Excel读取)
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,10 +15,11 @@ 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 materialUrl, String stepOneQuestions) {
this.scope = scope; this.scope = scope;
this.effectiveConfigId = effectiveConfigId; this.effectiveConfigId = effectiveConfigId;
this.knowledge = knowledge; this.knowledge = knowledge;
@ -28,6 +29,7 @@ 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) {
@ -42,7 +44,8 @@ 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) {
@ -58,4 +61,5 @@ 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; }
} }

@ -11,11 +11,17 @@ import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter; import com.itextpdf.text.pdf.PdfWriter;
import org.apache.poi.hslf.usermodel.*; import org.apache.poi.hslf.usermodel.*;
import org.apache.poi.xslf.usermodel.*; import org.apache.poi.xslf.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -32,6 +38,10 @@ import static com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil.TypeEnum.*;
*/ */
public final class PDFConvertUtil { public final class PDFConvertUtil {
private static final Logger log = LoggerFactory.getLogger(PDFConvertUtil.class);
private static final String BUNDLED_FONT_RESOURCE = "/simsun.ttf";
private static volatile boolean chineseFontsConfigured;
final private static Map<TypeEnum, List<String>> types = new HashMap<>(); final private static Map<TypeEnum, List<String>> types = new HashMap<>();
public static enum TypeEnum { public static enum TypeEnum {
@ -119,27 +129,31 @@ public final class PDFConvertUtil {
* @param outputStream pdf文件输出流 * @param outputStream pdf文件输出流
**/ **/
public static boolean wordToPdfByAspose(InputStream inputStream, OutputStream outputStream) { public static boolean wordToPdfByAspose(InputStream inputStream, OutputStream outputStream) {
return convertWordToPdfByAspose(inputStream, outputStream);
}
private static boolean convertWordToPdfByAspose(InputStream inputStream, OutputStream outputStream) {
// 验证License 若不验证则转化出的pdf文档会有水印产生 // 验证License 若不验证则转化出的pdf文档会有水印产生
if (!getLicense()) { if (!getLicense()) {
log.warn("word转PDF失败:Aspose License 校验未通过");
return false; return false;
} }
try { try {
//此处处理乱码和小方块 FontSettings fontSettings = configureChineseFonts();
//如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题
//指定文件库内容路径
FontSettings.getDefaultInstance().setFontsFolders(
new String[] {"/usr/share/fonts", "/usr/share/fonts/win"}
, true);
// 将源文件保存在com.aspose.words.Document中,具体的转换格式依靠里面的save方法 // 将源文件保存在com.aspose.words.Document中,具体的转换格式依靠里面的save方法
com.aspose.words.Document doc = new com.aspose.words.Document(inputStream); com.aspose.words.Document doc = new com.aspose.words.Document(inputStream);
doc.setFontSettings(fontSettings);
// Blob URL 预览时浏览器会读取 PDF 的文档标题。清除 Word 原始标题,
// 避免某些文档的二进制标题被显示成 yAAAA... 之类的乱码。
doc.getBuiltInDocumentProperties().setTitle("reference-answer");
// 全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF,EPUB, XPS, SWF 相互转换 // 全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF,EPUB, XPS, SWF 相互转换
doc.save(outputStream, SaveFormat.PDF); doc.save(outputStream, SaveFormat.PDF);
System.out.println("word转换完毕"); System.out.println("word转换完毕");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.warn("word转PDF异常", e);
return false; return false;
}finally { }finally {
closeOutput(outputStream); closeOutput(outputStream);
@ -148,6 +162,67 @@ public final class PDFConvertUtil {
return true; return true;
} }
/**
* 配置 Word 转 PDF 使用的中文字体。
*
* 服务器通常没有 Windows 的宋体,单纯扫描系统字体目录会导致 DOC/DOCX
* 中的中文被替换成方框或乱码。项目内置 simsun.ttf,首次转换时解压到
* 临时字体目录,并和常见系统字体目录一起交给 Aspose。
*/
private static FontSettings configureChineseFonts() {
FontSettings settings = FontSettings.getDefaultInstance();
if (chineseFontsConfigured) {
return settings;
}
synchronized (PDFConvertUtil.class) {
if (chineseFontsConfigured) {
return settings;
}
List<String> fontFolders = new ArrayList<>();
addExistingFontFolder(fontFolders, "C:\\Windows\\Fonts");
addExistingFontFolder(fontFolders, "/usr/share/fonts");
addExistingFontFolder(fontFolders, "/usr/local/share/fonts");
addExistingFontFolder(fontFolders, "/usr/share/fonts/chinese");
addExistingFontFolder(fontFolders, "/usr/share/fonts/win");
try {
Path bundledFontFolder = Paths.get(System.getProperty("java.io.tmpdir"), "link-commerce-fonts");
Files.createDirectories(bundledFontFolder);
Path bundledFont = bundledFontFolder.resolve("simsun.ttf");
if (!Files.exists(bundledFont) || Files.size(bundledFont) == 0) {
try (InputStream input = PDFConvertUtil.class.getResourceAsStream(BUNDLED_FONT_RESOURCE)) {
if (input == null) {
throw new FileNotFoundException("Missing bundled font " + BUNDLED_FONT_RESOURCE);
}
Files.copy(input, bundledFont, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
}
addExistingFontFolder(fontFolders, bundledFontFolder.toString());
} catch (IOException | SecurityException e) {
log.warn("内置中文字体加载失败,将继续使用系统字体目录", e);
}
if (!fontFolders.isEmpty()) {
settings.setFontsFolders(fontFolders.toArray(new String[0]), true);
}
settings.setDefaultFontName("SimSun");
settings.setFontSubstitutes("宋体", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei");
settings.setFontSubstitutes("等线", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei");
settings.setFontSubstitutes("微软雅黑", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei");
settings.setFontSubstitutes("黑体", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei");
settings.setFontSubstitutes("仿宋", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei");
chineseFontsConfigured = true;
return settings;
}
}
private static void addExistingFontFolder(List<String> folders, String folder) {
if (folder != null && Files.isDirectory(Paths.get(folder))) {
folders.add(folder);
}
}
/** /**
* 关闭输出流 * 关闭输出流
* @param outputStream * @param outputStream
@ -208,7 +283,7 @@ public final class PDFConvertUtil {
aposeLic.setLicense(is); aposeLic.setLicense(is);
result = true; result = true;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.warn("Aspose Words License 加载异常", e);
} }
return result; return result;
} }
@ -225,8 +300,11 @@ public final class PDFConvertUtil {
//此处处理乱码和小方块 //此处处理乱码和小方块
//如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题 //如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题
//指定文件库内容路径 //指定文件库内容路径
// 字体目录同时包含 Windows 系统字体与 Linux 常见目录:
// setFontsFolders 会替换 aspose 的默认字体源,漏掉 Windows 字体目录时
// 宋体等中文字体不可见,转换出的 PDF 中文全部变成方框
FontSettings.getDefaultInstance().setFontsFolders( FontSettings.getDefaultInstance().setFontsFolders(
new String[] {"/usr/share/fonts", "/usr/share/fonts/win"} new String[] {"C:\\Windows\\Fonts", "/usr/share/fonts", "/usr/share/fonts/win"}
, true); , true);
// 原始excel路径 // 原始excel路径
com.aspose.cells.Workbook wb = new com.aspose.cells.Workbook(inputStream); com.aspose.cells.Workbook wb = new com.aspose.cells.Workbook(inputStream);
@ -329,8 +407,11 @@ public final class PDFConvertUtil {
//此处处理乱码和小方块 //此处处理乱码和小方块
//如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题 //如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题
//指定文件库内容路径 //指定文件库内容路径
// 字体目录同时包含 Windows 系统字体与 Linux 常见目录:
// setFontsFolders 会替换 aspose 的默认字体源,漏掉 Windows 字体目录时
// 宋体等中文字体不可见,转换出的 PDF 中文全部变成方框
FontSettings.getDefaultInstance().setFontsFolders( FontSettings.getDefaultInstance().setFontsFolders(
new String[] {"/usr/share/fonts", "/usr/share/fonts/win"} new String[] {"C:\\Windows\\Fonts", "/usr/share/fonts", "/usr/share/fonts/win"}
, true); , true);
hslfSlideShow = new HSLFSlideShow(inputStream); hslfSlideShow = new HSLFSlideShow(inputStream);
@ -411,8 +492,11 @@ public final class PDFConvertUtil {
//此处处理乱码和小方块 //此处处理乱码和小方块
//如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题 //如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题
//指定文件库内容路径 //指定文件库内容路径
// 字体目录同时包含 Windows 系统字体与 Linux 常见目录:
// setFontsFolders 会替换 aspose 的默认字体源,漏掉 Windows 字体目录时
// 宋体等中文字体不可见,转换出的 PDF 中文全部变成方框
FontSettings.getDefaultInstance().setFontsFolders( FontSettings.getDefaultInstance().setFontsFolders(
new String[] {"/usr/share/fonts", "/usr/share/fonts/win"} new String[] {"C:\\Windows\\Fonts", "/usr/share/fonts", "/usr/share/fonts/win"}
, true); , true);
slideShow = new XMLSlideShow(inputStream); slideShow = new XMLSlideShow(inputStream);
@ -535,4 +619,4 @@ public final class PDFConvertUtil {
String[] split = fileName.split("\\."); String[] split = fileName.split("\\.");
return split[split.length - 1]; return split[split.length - 1];
} }
} }

@ -63,6 +63,9 @@ 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,14 +61,16 @@
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">
@ -76,7 +78,6 @@
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">

@ -18,6 +18,7 @@
<result column="submitted" jdbcType="BIT" property="submitted" /> <result column="submitted" jdbcType="BIT" property="submitted" />
<result column="ai_assessment_score" jdbcType="INTEGER" property="aiAssessmentScore" /> <result column="ai_assessment_score" jdbcType="INTEGER" property="aiAssessmentScore" />
<result column="teacher_score" jdbcType="DECIMAL" property="teacherScore" /> <result column="teacher_score" jdbcType="DECIMAL" property="teacherScore" />
<result column="teacher_comment" jdbcType="VARCHAR" property="teacherComment" />
<result column="teacher_scored_by" jdbcType="VARCHAR" property="teacherScoredBy" /> <result column="teacher_scored_by" jdbcType="VARCHAR" property="teacherScoredBy" />
<result column="teacher_scored_at" jdbcType="TIMESTAMP" property="teacherScoredAt" /> <result column="teacher_scored_at" jdbcType="TIMESTAMP" property="teacherScoredAt" />
<result column="submit_time" jdbcType="TIMESTAMP" property="submitTime" /> <result column="submit_time" jdbcType="TIMESTAMP" property="submitTime" />
@ -27,7 +28,7 @@
<sql id="Base_Column_List"> <sql id="Base_Column_List">
id, student_user_id, teaching_class_id, task_id, task_key, task_name, step1_answer, step2_answer, id, student_user_id, teaching_class_id, task_id, task_key, task_name, step1_answer, step2_answer,
step3_answer, step4_answer, dynamic_step_answers, current_step, progress_status, submitted, ai_assessment_score, teacher_score, teacher_scored_by, teacher_scored_at, submit_time, create_time, update_time step3_answer, step4_answer, dynamic_step_answers, current_step, progress_status, submitted, ai_assessment_score, teacher_score, teacher_comment, teacher_scored_by, teacher_scored_at, submit_time, create_time, update_time
</sql> </sql>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap"> <select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
@ -79,6 +80,7 @@
<if test="submitted != null">submitted,</if> <if test="submitted != null">submitted,</if>
<if test="aiAssessmentScore != null">ai_assessment_score,</if> <if test="aiAssessmentScore != null">ai_assessment_score,</if>
<if test="teacherScore != null">teacher_score,</if> <if test="teacherScore != null">teacher_score,</if>
<if test="teacherComment != null">teacher_comment,</if>
<if test="teacherScoredBy != null">teacher_scored_by,</if> <if test="teacherScoredBy != null">teacher_scored_by,</if>
<if test="teacherScoredAt != null">teacher_scored_at,</if> <if test="teacherScoredAt != null">teacher_scored_at,</if>
<if test="submitTime != null">submit_time,</if> <if test="submitTime != null">submit_time,</if>
@ -102,6 +104,7 @@
<if test="submitted != null">#{submitted,jdbcType=BIT},</if> <if test="submitted != null">#{submitted,jdbcType=BIT},</if>
<if test="aiAssessmentScore != null">#{aiAssessmentScore,jdbcType=INTEGER},</if> <if test="aiAssessmentScore != null">#{aiAssessmentScore,jdbcType=INTEGER},</if>
<if test="teacherScore != null">#{teacherScore,jdbcType=DECIMAL},</if> <if test="teacherScore != null">#{teacherScore,jdbcType=DECIMAL},</if>
<if test="teacherComment != null">#{teacherComment,jdbcType=VARCHAR},</if>
<if test="teacherScoredBy != null">#{teacherScoredBy,jdbcType=VARCHAR},</if> <if test="teacherScoredBy != null">#{teacherScoredBy,jdbcType=VARCHAR},</if>
<if test="teacherScoredAt != null">#{teacherScoredAt,jdbcType=TIMESTAMP},</if> <if test="teacherScoredAt != null">#{teacherScoredAt,jdbcType=TIMESTAMP},</if>
<if test="submitTime != null">#{submitTime,jdbcType=TIMESTAMP},</if> <if test="submitTime != null">#{submitTime,jdbcType=TIMESTAMP},</if>
@ -128,6 +131,7 @@
<if test="submitted != null">submitted = #{submitted,jdbcType=BIT},</if> <if test="submitted != null">submitted = #{submitted,jdbcType=BIT},</if>
<if test="aiAssessmentScore != null">ai_assessment_score = #{aiAssessmentScore,jdbcType=INTEGER},</if> <if test="aiAssessmentScore != null">ai_assessment_score = #{aiAssessmentScore,jdbcType=INTEGER},</if>
<if test="teacherScore != null">teacher_score = #{teacherScore,jdbcType=DECIMAL},</if> <if test="teacherScore != null">teacher_score = #{teacherScore,jdbcType=DECIMAL},</if>
<if test="teacherComment != null">teacher_comment = #{teacherComment,jdbcType=VARCHAR},</if>
<if test="teacherScoredBy != null">teacher_scored_by = #{teacherScoredBy,jdbcType=VARCHAR},</if> <if test="teacherScoredBy != null">teacher_scored_by = #{teacherScoredBy,jdbcType=VARCHAR},</if>
<if test="teacherScoredAt != null">teacher_scored_at = #{teacherScoredAt,jdbcType=TIMESTAMP},</if> <if test="teacherScoredAt != null">teacher_scored_at = #{teacherScoredAt,jdbcType=TIMESTAMP},</if>
<if test="submitTime != null">submit_time = #{submitTime,jdbcType=TIMESTAMP},</if> <if test="submitTime != null">submit_time = #{submitTime,jdbcType=TIMESTAMP},</if>
@ -152,4 +156,13 @@
update_time = #{scoredAt,jdbcType=TIMESTAMP} update_time = #{scoredAt,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=VARCHAR} where id = #{id,jdbcType=VARCHAR}
</update> </update>
<update id="updateTeacherScoreWithComment">
update student_training_answer
set teacher_score = #{score,jdbcType=DECIMAL},
teacher_comment = #{teacherComment,jdbcType=VARCHAR},
teacher_scored_by = #{teacherUserId,jdbcType=VARCHAR},
teacher_scored_at = #{scoredAt,jdbcType=TIMESTAMP},
update_time = #{scoredAt,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper> </mapper>

@ -21,6 +21,9 @@
<result column="guide_manual_url" jdbcType="VARCHAR" property="guideManualUrl" /> <result column="guide_manual_url" jdbcType="VARCHAR" property="guideManualUrl" />
<result column="ai_manual_name" jdbcType="VARCHAR" property="aiManualName" /> <result column="ai_manual_name" jdbcType="VARCHAR" property="aiManualName" />
<result column="ai_manual_url" jdbcType="VARCHAR" property="aiManualUrl" /> <result column="ai_manual_url" jdbcType="VARCHAR" property="aiManualUrl" />
<result column="reference_answer_name" jdbcType="VARCHAR" property="referenceAnswerName" />
<result column="reference_answer_url" jdbcType="VARCHAR" property="referenceAnswerUrl" />
<result column="reference_answer_enabled" jdbcType="BIT" property="referenceAnswerEnabled" />
<result column="sort" jdbcType="INTEGER" property="sort" /> <result column="sort" jdbcType="INTEGER" property="sort" />
<result column="enabled" jdbcType="BIT" property="enabled" /> <result column="enabled" jdbcType="BIT" property="enabled" />
<result column="created_by" jdbcType="VARCHAR" property="createdBy" /> <result column="created_by" jdbcType="VARCHAR" property="createdBy" />
@ -32,6 +35,7 @@
id, teaching_class_id, task_key, project_name, task_name, knowledge, background, objectives, requirements, id, teaching_class_id, task_key, project_name, task_name, knowledge, background, objectives, requirements,
steps, step_one_questions, material_name, material_url, steps, step_one_questions, material_name, material_url,
training_data_name, training_data_url, guide_manual_name, guide_manual_url, ai_manual_name, ai_manual_url, training_data_name, training_data_url, guide_manual_name, guide_manual_url, ai_manual_name, ai_manual_url,
reference_answer_name, reference_answer_url, reference_answer_enabled,
sort, enabled, created_by, create_time, update_time sort, enabled, created_by, create_time, update_time
</sql> </sql>
@ -98,6 +102,9 @@
<if test="guideManualUrl != null">guide_manual_url,</if> <if test="guideManualUrl != null">guide_manual_url,</if>
<if test="aiManualName != null">ai_manual_name,</if> <if test="aiManualName != null">ai_manual_name,</if>
<if test="aiManualUrl != null">ai_manual_url,</if> <if test="aiManualUrl != null">ai_manual_url,</if>
<if test="referenceAnswerName != null">reference_answer_name,</if>
<if test="referenceAnswerUrl != null">reference_answer_url,</if>
<if test="referenceAnswerEnabled != null">reference_answer_enabled,</if>
<if test="sort != null">sort,</if> <if test="sort != null">sort,</if>
<if test="enabled != null">enabled,</if> <if test="enabled != null">enabled,</if>
<if test="createdBy != null">created_by,</if> <if test="createdBy != null">created_by,</if>
@ -124,6 +131,9 @@
<if test="guideManualUrl != null">#{guideManualUrl,jdbcType=VARCHAR},</if> <if test="guideManualUrl != null">#{guideManualUrl,jdbcType=VARCHAR},</if>
<if test="aiManualName != null">#{aiManualName,jdbcType=VARCHAR},</if> <if test="aiManualName != null">#{aiManualName,jdbcType=VARCHAR},</if>
<if test="aiManualUrl != null">#{aiManualUrl,jdbcType=VARCHAR},</if> <if test="aiManualUrl != null">#{aiManualUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerName != null">#{referenceAnswerName,jdbcType=VARCHAR},</if>
<if test="referenceAnswerUrl != null">#{referenceAnswerUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerEnabled != null">#{referenceAnswerEnabled,jdbcType=BIT},</if>
<if test="sort != null">#{sort,jdbcType=INTEGER},</if> <if test="sort != null">#{sort,jdbcType=INTEGER},</if>
<if test="enabled != null">#{enabled,jdbcType=BIT},</if> <if test="enabled != null">#{enabled,jdbcType=BIT},</if>
<if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if> <if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if>
@ -151,6 +161,9 @@
<if test="guideManualUrl != null">guide_manual_url = #{guideManualUrl,jdbcType=VARCHAR},</if> <if test="guideManualUrl != null">guide_manual_url = #{guideManualUrl,jdbcType=VARCHAR},</if>
<if test="aiManualName != null">ai_manual_name = #{aiManualName,jdbcType=VARCHAR},</if> <if test="aiManualName != null">ai_manual_name = #{aiManualName,jdbcType=VARCHAR},</if>
<if test="aiManualUrl != null">ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR},</if> <if test="aiManualUrl != null">ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerName != null">reference_answer_name = #{referenceAnswerName,jdbcType=VARCHAR},</if>
<if test="referenceAnswerUrl != null">reference_answer_url = #{referenceAnswerUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerEnabled != null">reference_answer_enabled = #{referenceAnswerEnabled,jdbcType=BIT},</if>
<if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if> <if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if>
<if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if> <if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if>
<if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if> <if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if>

@ -20,6 +20,8 @@
<result column="guide_manual_url" jdbcType="VARCHAR" property="guideManualUrl" /> <result column="guide_manual_url" jdbcType="VARCHAR" property="guideManualUrl" />
<result column="ai_manual_name" jdbcType="VARCHAR" property="aiManualName" /> <result column="ai_manual_name" jdbcType="VARCHAR" property="aiManualName" />
<result column="ai_manual_url" jdbcType="VARCHAR" property="aiManualUrl" /> <result column="ai_manual_url" jdbcType="VARCHAR" property="aiManualUrl" />
<result column="reference_answer_name" jdbcType="VARCHAR" property="referenceAnswerName" />
<result column="reference_answer_url" jdbcType="VARCHAR" property="referenceAnswerUrl" />
<result column="sort" jdbcType="INTEGER" property="sort" /> <result column="sort" jdbcType="INTEGER" property="sort" />
<result column="enabled" jdbcType="BIT" property="enabled" /> <result column="enabled" jdbcType="BIT" property="enabled" />
<result column="created_by" jdbcType="VARCHAR" property="createdBy" /> <result column="created_by" jdbcType="VARCHAR" property="createdBy" />
@ -31,6 +33,7 @@
id, task_key, project_name, task_name, knowledge, background, objectives, requirements, id, task_key, project_name, task_name, knowledge, background, objectives, requirements,
steps, step_one_questions, material_name, material_url, steps, step_one_questions, material_name, material_url,
training_data_name, training_data_url, guide_manual_name, guide_manual_url, ai_manual_name, ai_manual_url, training_data_name, training_data_url, guide_manual_name, guide_manual_url, ai_manual_name, ai_manual_url,
reference_answer_name, reference_answer_url,
sort, enabled, created_by, create_time, update_time sort, enabled, created_by, create_time, update_time
</sql> </sql>
@ -93,6 +96,8 @@
<if test="guideManualUrl != null">guide_manual_url,</if> <if test="guideManualUrl != null">guide_manual_url,</if>
<if test="aiManualName != null">ai_manual_name,</if> <if test="aiManualName != null">ai_manual_name,</if>
<if test="aiManualUrl != null">ai_manual_url,</if> <if test="aiManualUrl != null">ai_manual_url,</if>
<if test="referenceAnswerName != null">reference_answer_name,</if>
<if test="referenceAnswerUrl != null">reference_answer_url,</if>
<if test="sort != null">sort,</if> <if test="sort != null">sort,</if>
<if test="enabled != null">enabled,</if> <if test="enabled != null">enabled,</if>
<if test="createdBy != null">created_by,</if> <if test="createdBy != null">created_by,</if>
@ -118,6 +123,8 @@
<if test="guideManualUrl != null">#{guideManualUrl,jdbcType=VARCHAR},</if> <if test="guideManualUrl != null">#{guideManualUrl,jdbcType=VARCHAR},</if>
<if test="aiManualName != null">#{aiManualName,jdbcType=VARCHAR},</if> <if test="aiManualName != null">#{aiManualName,jdbcType=VARCHAR},</if>
<if test="aiManualUrl != null">#{aiManualUrl,jdbcType=VARCHAR},</if> <if test="aiManualUrl != null">#{aiManualUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerName != null">#{referenceAnswerName,jdbcType=VARCHAR},</if>
<if test="referenceAnswerUrl != null">#{referenceAnswerUrl,jdbcType=VARCHAR},</if>
<if test="sort != null">#{sort,jdbcType=INTEGER},</if> <if test="sort != null">#{sort,jdbcType=INTEGER},</if>
<if test="enabled != null">#{enabled,jdbcType=BIT},</if> <if test="enabled != null">#{enabled,jdbcType=BIT},</if>
<if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if> <if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if>
@ -146,6 +153,8 @@
<if test="guideManualUrl != null">guide_manual_url = #{guideManualUrl,jdbcType=VARCHAR},</if> <if test="guideManualUrl != null">guide_manual_url = #{guideManualUrl,jdbcType=VARCHAR},</if>
<if test="aiManualName != null">ai_manual_name = #{aiManualName,jdbcType=VARCHAR},</if> <if test="aiManualName != null">ai_manual_name = #{aiManualName,jdbcType=VARCHAR},</if>
<if test="aiManualUrl != null">ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR},</if> <if test="aiManualUrl != null">ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerName != null">reference_answer_name = #{referenceAnswerName,jdbcType=VARCHAR},</if>
<if test="referenceAnswerUrl != null">reference_answer_url = #{referenceAnswerUrl,jdbcType=VARCHAR},</if>
<if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if> <if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if>
<if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if> <if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if>
<if test="createdBy != null">created_by = #{createdBy,jdbcType=VARCHAR},</if> <if test="createdBy != null">created_by = #{createdBy,jdbcType=VARCHAR},</if>

@ -0,0 +1,75 @@
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);
}
}

@ -0,0 +1,59 @@
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);
}
}

@ -46,6 +46,17 @@ class TeaScoreControllerTaskReviewTest {
"taskReview", String.class, String.class, String.class, HttpServletRequest.class)); "taskReview", String.class, String.class, String.class, HttpServletRequest.class));
} }
@Test
void taskScoreRejectsMissingTeacherScoreWithBusinessMessage() {
TeaScoreController controller = controllerWithCompletedAnswer();
ResultEntity<?> result = controller.saveTaskTeacherScore(
"class-1", "student-1", "task-1", null, "评语", new MockHttpServletRequest());
assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode());
assertEquals("教师评分必须为0至100之间的数值", result.getBody().getMsg());
}
@Test @Test
void batchScoreRejectsTaskThatIsNotAllocatedToTheClassBeforeWritingAnyScore() { void batchScoreRejectsTaskThatIsNotAllocatedToTheClassBeforeWritingAnyScore() {
TeaScoreController controller = controllerWithCompletedAnswer(); TeaScoreController controller = controllerWithCompletedAnswer();

@ -1304,14 +1304,12 @@ 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);
@ -1335,14 +1333,12 @@ 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());
@ -1464,15 +1460,11 @@ 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,6 +501,124 @@ 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,37 +3,65 @@ 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"); // 操作用户的学校/院系/专业应直接继承给导入的教师
when(service.schoolService.selectMajorIdByFacultyIdAndMajorName(any(), any())).thenReturn("major-1"); Userinfo operator = new Userinfo();
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)); service.processTeacherImport(Collections.singletonList(row), operator);
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());
} }
} }

@ -5,12 +5,17 @@ import com.sztzjy.linkCommerce.entity.TaskAllocation;
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.SchoolDefaultTaskInitializationMapper; import com.sztzjy.linkCommerce.mapper.SchoolDefaultTaskInitializationMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.service.TrainingTaskService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -68,6 +73,63 @@ class SchoolDefaultTaskServiceImplTest {
verify(executor).execute(any(Runnable.class)); verify(executor).execute(any(Runnable.class));
} }
@Test
void initializationOpensOnlyFirstProjectFirstThreeTasksAndDisablesRest() {
TaskAllocationMapper taskMapper = mock(TaskAllocationMapper.class);
TrainingTaskService trainingTaskService = mock(TrainingTaskService.class);
SchoolDefaultTaskServiceImpl service = new SchoolDefaultTaskServiceImpl();
ReflectionTestUtils.setField(service, "taskAllocationMapper", taskMapper);
ReflectionTestUtils.setField(service, "trainingTaskService", trainingTaskService);
when(taskMapper.countByExample(any())).thenReturn(0L);
when(taskMapper.selectByExample(any())).thenReturn(Arrays.asList(
baselineTask("task-1", (byte) 0),
baselineTask("task-2", (byte) 0),
baselineTask("task-3", (byte) 0),
baselineTask("task-4", (byte) 0),
baselineTask("task-5", (byte) 0),
baselineTask("comprehensive-case-training", (byte) 1)));
Set<String> defaultOpen = new LinkedHashSet<>(Arrays.asList("task-1", "task-2", "task-3"));
when(trainingTaskService.getDefaultPublishedTaskKeys()).thenReturn(defaultOpen);
service.initializeSchoolDefault("school-1");
ArgumentCaptor<TaskAllocation> captor = ArgumentCaptor.forClass(TaskAllocation.class);
verify(taskMapper, times(6)).insert(captor.capture());
for (TaskAllocation inserted : captor.getAllValues()) {
assertEquals("SCHOOL_DEFAULT:school-1", inserted.getClassId());
if (defaultOpen.contains(inserted.getModule())) {
assertEquals((byte) 0, inserted.getDisabledStatus(), inserted.getModule() + " 应默认开放");
} else {
assertEquals((byte) 1, inserted.getDisabledStatus(), inserted.getModule() + " 应默认停用");
}
}
}
@Test
void initializationFallsBackToBaselineStatusWhenDefaultOpenKeysUnavailable() {
TaskAllocationMapper taskMapper = mock(TaskAllocationMapper.class);
SchoolDefaultTaskServiceImpl service = new SchoolDefaultTaskServiceImpl();
ReflectionTestUtils.setField(service, "taskAllocationMapper", taskMapper);
when(taskMapper.countByExample(any())).thenReturn(0L);
when(taskMapper.selectByExample(any())).thenReturn(Collections.singletonList(
baselineTask("task-1", (byte) 0)));
service.initializeSchoolDefault("school-1");
ArgumentCaptor<TaskAllocation> captor = ArgumentCaptor.forClass(TaskAllocation.class);
verify(taskMapper).insert(captor.capture());
assertEquals((byte) 0, captor.getValue().getDisabledStatus());
}
private TaskAllocation baselineTask(String module, byte disabledStatus) {
TaskAllocation task = new TaskAllocation();
task.setModule(module);
task.setDisabledStatus(disabledStatus);
return task;
}
private TaskAllocation task(String module) { private TaskAllocation task(String module) {
TaskAllocation task = new TaskAllocation(); TaskAllocation task = new TaskAllocation();
task.setModule(module); task.setModule(module);

@ -128,6 +128,20 @@ class TrainingTaskServiceImplTest {
assertFalse(task.getSteps().contains("Step 3")); assertFalse(task.getSteps().contains("Step 3"));
} }
@Test
void referenceAnswerRejectsNonWordFiles() {
TrainingTaskServiceImpl service = new TrainingTaskServiceImpl();
service.trainingTaskMapper = mock(TrainingTaskMapper.class);
TrainingTask task = storedTask(null, "new-product-survey");
task.setReferenceAnswerName("reference-answer.pdf");
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
() -> service.create(task, "teacher-1"));
assertEquals("参考答案仅支持Word文件(.doc或.docx)", error.getMessage());
verify(service.trainingTaskMapper, never()).insertSelective(any(TrainingTask.class));
}
@Test @Test
void importTaskRejectsUnknownTaskKeyBecauseTaskCountIsFixed() { void importTaskRejectsUnknownTaskKeyBecauseTaskCountIsFixed() {
TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); TrainingTaskServiceImpl service = new TrainingTaskServiceImpl();
@ -649,6 +663,8 @@ class TrainingTaskServiceImplTest {
payload.setTaskName("Class Name"); payload.setTaskName("Class Name");
payload.setKnowledge("Class Knowledge"); payload.setKnowledge("Class Knowledge");
payload.setMaterialName("Class Material"); payload.setMaterialName("Class Material");
payload.setReferenceAnswerName("参考答案.docx");
payload.setReferenceAnswerUrl("/file/reference-answer.docx");
payload.setEnabled(Boolean.TRUE); payload.setEnabled(Boolean.TRUE);
service.saveClassTask("class-1", "new-product-survey", payload, "teacher-1"); service.saveClassTask("class-1", "new-product-survey", payload, "teacher-1");
@ -665,6 +681,8 @@ class TrainingTaskServiceImplTest {
assertEquals(Integer.valueOf(3), savedOverride.getSort()); assertEquals(Integer.valueOf(3), savedOverride.getSort());
assertEquals("Class Knowledge", savedOverride.getKnowledge()); assertEquals("Class Knowledge", savedOverride.getKnowledge());
assertEquals("Class Material", savedOverride.getMaterialName()); assertEquals("Class Material", savedOverride.getMaterialName());
assertEquals("参考答案.docx", savedOverride.getReferenceAnswerName());
assertEquals("/file/reference-answer.docx", savedOverride.getReferenceAnswerUrl());
} }
@Test @Test

Loading…
Cancel
Save