|
@@ -0,0 +1,225 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.etotem.cfc.entity.MathChildLevel;
|
|
|
|
|
+import com.etotem.cfc.entity.MathQuizLog;
|
|
|
|
|
+import com.etotem.cfc.mapper.MathChildLevelMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.MathQuizLogMapper;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+public class WisdomMathService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(WisdomMathService.class);
|
|
|
|
|
+
|
|
|
|
|
+ private static final String[] LEVEL_NAMES = {
|
|
|
|
|
+ "一位数加减", "两位数加减", "一位数四则运算", "两位数四则运算含括号"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static final int[][] LEVEL_AGE_MAP = {
|
|
|
|
|
+ {3, 6}, // level 1: 3-6岁
|
|
|
|
|
+ {6, 9}, // level 2: 6-9岁
|
|
|
|
|
+ {9, 12}, // level 3: 9-12岁
|
|
|
|
|
+ {12, 99} // level 4: 12岁+
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${math.verify-mode:dify}")
|
|
|
|
|
+ private String verifyMode;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${math.workflow-id:}")
|
|
|
|
|
+ private String workflowId;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MathChildLevelMapper childLevelMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MathQuizLogMapper quizLogMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AIService aiService;
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> startSession(Long childId, Integer age) {
|
|
|
|
|
+ MathChildLevel level = childLevelMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<MathChildLevel>().eq(MathChildLevel::getChildId, childId));
|
|
|
|
|
+ int currentLevel = 1;
|
|
|
|
|
+ if (level != null) {
|
|
|
|
|
+ currentLevel = level.getCurrentLevel();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentLevel = levelByAge(age);
|
|
|
|
|
+ level = new MathChildLevel();
|
|
|
|
|
+ level.setChildId(childId);
|
|
|
|
|
+ level.setCurrentLevel(currentLevel);
|
|
|
|
|
+ level.setTotalPassed(0);
|
|
|
|
|
+ childLevelMapper.insert(level);
|
|
|
|
|
+ }
|
|
|
|
|
+ int suggestedTarget = suggestedTargetN(currentLevel, age);
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("level", currentLevel);
|
|
|
|
|
+ result.put("levelName", LEVEL_NAMES[currentLevel - 1]);
|
|
|
|
|
+ result.put("age", age);
|
|
|
|
|
+ result.put("suggestedTargetN", suggestedTarget);
|
|
|
|
|
+ result.put("totalPassed", level != null ? level.getTotalPassed() : 0);
|
|
|
|
|
+ result.put("bestTimeMs", level != null ? level.getBestTimeMs() : null);
|
|
|
|
|
+ result.put("bestAccuracy", level != null ? level.getBestAccuracy() : null);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> verifyAnswer(Long childId, String expression, String userAnswer) {
|
|
|
|
|
+ if ("dify".equals(verifyMode) && workflowId != null && !workflowId.isEmpty()) {
|
|
|
|
|
+ return verifyViaDify(expression, userAnswer, String.valueOf(childId));
|
|
|
|
|
+ }
|
|
|
|
|
+ return verifyViaEval(expression, userAnswer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> verifyViaDify(String expression, String userAnswer, String userId) {
|
|
|
|
|
+ Map<String, Object> inputs = new LinkedHashMap<>();
|
|
|
|
|
+ inputs.put("expression", expression);
|
|
|
|
|
+ inputs.put("userAnswer", userAnswer);
|
|
|
|
|
+ Map<String, Object> outputs = aiService.runWorkflow(workflowId, inputs, userId);
|
|
|
|
|
+ if (outputs.containsKey("error")) {
|
|
|
|
|
+ log.warn("Dify workflow failed, falling back to eval mode");
|
|
|
|
|
+ return verifyViaEval(expression, userAnswer);
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("correct", outputs.getOrDefault("correct", false));
|
|
|
|
|
+ result.put("correctAnswer", outputs.getOrDefault("correctAnswer", ""));
|
|
|
|
|
+ result.put("feedback", outputs.getOrDefault("feedback", ""));
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> verifyViaEval(String expression, String userAnswer) {
|
|
|
|
|
+ boolean correct = false;
|
|
|
|
|
+ String correctAnswer = "";
|
|
|
|
|
+ String feedback = "";
|
|
|
|
|
+ try {
|
|
|
|
|
+ String expr = expression.replaceAll("×", "*").replaceAll("÷", "/")
|
|
|
|
|
+ .replaceAll("x", "*").replaceAll("X", "*")
|
|
|
|
|
+ .replaceAll("=", "=").replaceAll("=", "");
|
|
|
|
|
+ javax.script.ScriptEngineManager mgr = new javax.script.ScriptEngineManager();
|
|
|
|
|
+ javax.script.ScriptEngine engine = mgr.getEngineByName("nashorn");
|
|
|
|
|
+ if (engine != null) {
|
|
|
|
|
+ Object evalResult = engine.eval(expr);
|
|
|
|
|
+ correctAnswer = String.valueOf(evalResult);
|
|
|
|
|
+ correct = correctAnswer.equals(userAnswer.trim());
|
|
|
|
|
+ feedback = correct ? "正确!" : "再想想哦";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ feedback = "算式无法解析,请用标准格式(如 12+38)";
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("math eval failed for: {}", expression, e);
|
|
|
|
|
+ feedback = "算式格式有误,请检查";
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("correct", correct);
|
|
|
|
|
+ result.put("correctAnswer", correctAnswer);
|
|
|
|
|
+ result.put("feedback", feedback);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> completeSession(Long childId, int level, int targetN,
|
|
|
|
|
+ int correctCount, int totalAttempts,
|
|
|
|
|
+ long totalTimeMs, List<String> expressions) {
|
|
|
|
|
+ int accuracy = totalAttempts > 0 ? (correctCount * 100 / totalAttempts) : 0;
|
|
|
|
|
+ boolean passed = correctCount >= targetN;
|
|
|
|
|
+
|
|
|
|
|
+ MathQuizLog logEntry = new MathQuizLog();
|
|
|
|
|
+ logEntry.setChildId(childId);
|
|
|
|
|
+ logEntry.setLevel(level);
|
|
|
|
|
+ logEntry.setTargetN(targetN);
|
|
|
|
|
+ logEntry.setCorrectCount(correctCount);
|
|
|
|
|
+ logEntry.setTotalAttempts(totalAttempts);
|
|
|
|
|
+ logEntry.setTotalTimeMs(totalTimeMs);
|
|
|
|
|
+ logEntry.setAccuracy(accuracy);
|
|
|
|
|
+ logEntry.setPassed(passed ? 1 : 0);
|
|
|
|
|
+ logEntry.setExprs(expressions != null ? String.join(",", expressions) : "");
|
|
|
|
|
+ quizLogMapper.insert(logEntry);
|
|
|
|
|
+
|
|
|
|
|
+ boolean levelUp = false;
|
|
|
|
|
+ boolean levelDown = false;
|
|
|
|
|
+ int newLevel = level;
|
|
|
|
|
+ String reason = "继续加油";
|
|
|
|
|
+
|
|
|
|
|
+ if (passed) {
|
|
|
|
|
+ MathChildLevel levelRecord = childLevelMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<MathChildLevel>().eq(MathChildLevel::getChildId, childId));
|
|
|
|
|
+ if (levelRecord == null) {
|
|
|
|
|
+ levelRecord = new MathChildLevel();
|
|
|
|
|
+ levelRecord.setChildId(childId);
|
|
|
|
|
+ levelRecord.setCurrentLevel(level);
|
|
|
|
|
+ levelRecord.setTotalPassed(1);
|
|
|
|
|
+ childLevelMapper.insert(levelRecord);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ levelRecord.setTotalPassed(levelRecord.getTotalPassed() != null
|
|
|
|
|
+ ? levelRecord.getTotalPassed() + 1 : 1);
|
|
|
|
|
+ if (levelRecord.getBestTimeMs() == null || totalTimeMs < levelRecord.getBestTimeMs()) {
|
|
|
|
|
+ levelRecord.setBestTimeMs(totalTimeMs);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (levelRecord.getBestAccuracy() == null || accuracy > levelRecord.getBestAccuracy()) {
|
|
|
|
|
+ levelRecord.setBestAccuracy(accuracy);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ double avgTimePerQuestion = totalAttempts > 0 ? (double) totalTimeMs / totalAttempts : 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (accuracy >= 90 && avgTimePerQuestion <= 30000 && level < 4) {
|
|
|
|
|
+ levelUp = true;
|
|
|
|
|
+ newLevel = level + 1;
|
|
|
|
|
+ reason = "太棒了!正确率" + accuracy + "%,平均每题" + (avgTimePerQuestion / 1000) + "秒,升级到" + LEVEL_NAMES[newLevel - 1] + "!";
|
|
|
|
|
+ } else if (accuracy >= 80) {
|
|
|
|
|
+ reason = "表现不错!正确率" + accuracy + "%,继续练习可以挑战更高级别。";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reason = "过关了!正确率" + accuracy + "%,多练习提高准确率。";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (levelRecord.getCurrentLevel() != newLevel) {
|
|
|
|
|
+ levelRecord.setCurrentLevel(newLevel);
|
|
|
|
|
+ }
|
|
|
|
|
+ childLevelMapper.updateById(levelRecord);
|
|
|
|
|
+
|
|
|
|
|
+ if (accuracy < 50 && avgTimePerQuestion > 60000 && level > 1) {
|
|
|
|
|
+ levelDown = true;
|
|
|
|
|
+ newLevel = level - 1;
|
|
|
|
|
+ reason = "正确率仅" + accuracy + "%,建议降级到" + LEVEL_NAMES[newLevel - 1] + "巩固基础。";
|
|
|
|
|
+ levelRecord.setCurrentLevel(newLevel);
|
|
|
|
|
+ childLevelMapper.updateById(levelRecord);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reason = "未达到目标,再接再厉!";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("passed", passed);
|
|
|
|
|
+ result.put("levelUp", levelUp);
|
|
|
|
|
+ result.put("levelDown", levelDown);
|
|
|
|
|
+ result.put("newLevel", newLevel);
|
|
|
|
|
+ result.put("reason", reason);
|
|
|
|
|
+ result.put("accuracy", accuracy);
|
|
|
|
|
+ result.put("totalAttempts", totalAttempts);
|
|
|
|
|
+ result.put("totalTimeMs", totalTimeMs);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int levelByAge(Integer age) {
|
|
|
|
|
+ if (age == null) return 1;
|
|
|
|
|
+ for (int i = 0; i < LEVEL_AGE_MAP.length; i++) {
|
|
|
|
|
+ if (age >= LEVEL_AGE_MAP[i][0] && age <= LEVEL_AGE_MAP[i][1]) {
|
|
|
|
|
+ return i + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int suggestedTargetN(int level, Integer age) {
|
|
|
|
|
+ if (age == null) age = 6;
|
|
|
|
|
+ if (age <= 6) return 3;
|
|
|
|
|
+ if (age <= 9) return 5;
|
|
|
|
|
+ if (age <= 12) return 8;
|
|
|
|
|
+ return 10;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|