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