From 91b6802e76773abbba1c41476a3482c4c4f549fe Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期一, 12 十月 2026 16:20:30 +0800
Subject: [PATCH] fix(ruoyi-study): 修复学习进度推进异常及前端展示错误

---
 ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/controller/TStudyController.java        |   39 +++----
 ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/service/impl/TUserStudyServiceImpl.java |   66 ++++--------
 doc/design/学习_学习进度day推进修复_20261012.md                                                           |  140 ++++++++++++++++++++++++++++
 3 files changed, 181 insertions(+), 64 deletions(-)

diff --git "a/doc/design/\345\255\246\344\271\240_\345\255\246\344\271\240\350\277\233\345\272\246day\346\216\250\350\277\233\344\277\256\345\244\215_20261012.md" "b/doc/design/\345\255\246\344\271\240_\345\255\246\344\271\240\350\277\233\345\272\246day\346\216\250\350\277\233\344\277\256\345\244\215_20261012.md"
new file mode 100644
index 0000000..96d8a6a
--- /dev/null
+++ "b/doc/design/\345\255\246\344\271\240_\345\255\246\344\271\240\350\277\233\345\272\246day\346\216\250\350\277\233\344\277\256\345\244\215_20261012.md"
@@ -0,0 +1,140 @@
+# 学习端学习进度 day 推进异常修复
+
+- 模块:ruoyi-study(学习端)
+- 功能:自主学习进度记录 / 学习 day 推进 / 进度展示
+- 日期:2026-10-12
+- 类型:线上缺陷修复(fix)
+
+## 一、问题现象
+
+用户(手机号 17766757175)反馈:
+
+1. 第 1~5 天内容全部已完成,界面仍显示「未完成」。
+2. 第 2 天反复重刷多遍、正确率 100%,仍显示「未完成」。
+3. 无法进入第 6 天(自主游戏)课程。
+
+## 二、学习进度模型(约定)
+
+`t_user_study` 按「周-天」组织,每个学习日包含 5 种自主学习活动:
+
+| type | 活动 | 落库字段 |
+| --- | --- | --- |
+| 1 | 听音选图 | listen |
+| 2 | 看图选音 | look |
+| 3 | 归纳排除 | induction |
+| 4 | 有问有答 | answer |
+| 5 | 音图相配 | pair |
+
+字段语义:
+
+- `listen/look/induction/answer/pair`:`100` = 该项已完成,`-1` = 未开始(`Constants.BURDEN_ONE`)。
+- `day`:`1~5` = 自主学习日,`6` = 自主游戏日,`7` = 听故事。
+
+关键规则:**同一天的 5 个活动共用同一个 `day`;5 项全部完成后才把 `day` 推进到下一天,并把 5 项重置为 -1。**
+
+## 三、根因分析
+
+### 3.1 主因(进度写入)
+
+位置:[TUserStudyServiceImpl.java](file:///d:/work/DolphinEnglish/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/service/impl/TUserStudyServiceImpl.java#L107-L151) 方法 `exchangeStudyRecord`。
+
+提交 `401510a`(2025-07-08)把「5 项全部完成后推进 day」的正确逻辑注释掉,改成「每完成一项就把 `day` 写成 2/3/4/5/6」,把「活动序号」当成了「学习日」:
+
+```java
+if (userStudyRecord.getListen().equals(Constants.ONE_HUNDRED)) { userStudyRecord.setDay(2); }
+if (userStudyRecord.getLook().equals(Constants.ONE_HUNDRED)) { userStudyRecord.setDay(3); }
+// ... 依次到 setDay(6)
+```
+
+该改动叠加方法起始的 `week/day` 守卫后形成死锁:
+
+1. 第 1 个活动(type=1)传入 `day=1`,与库中 `day=1` 相等 → 通过守卫 → `listen=100`,随即被 `setDay(2)` 把库中 `day` 改成 2。
+2. 同一天后续 4 个活动仍传入 `day=1`,与库中 `day=2` 不等 → 守卫不成立 → **整段逻辑静默跳过**。
+3. 每天重演,导致只有第一个活动能落库,`day` 卡在 2,第 5 天永远无法完成,`day` 永远到不了 6。
+
+与库中实际数据完全吻合:`week=1, day=2, listen=100, look/induction/answer/pair=-1`。
+
+### 3.2 次因(进度展示)
+
+位置:[TStudyController.java](file:///d:/work/DolphinEnglish/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/controller/TStudyController.java#L1120-L1140) 方法 `studySchedule`。
+
+数据库存的是「已完成进度」,返回前端需转换为「剩余进度」。但转换守卫为 `if (x != 100 && x != -1)`,把 `100`(已完成)排除在转换外:已完成项返回 100,前端按「剩余 100%」渲染,仍显示「未完成」。
+
+### 3.3 佐证
+
+- 正确实现见提交 `a3d6cf8`(2024-07-19):5 项全 100 → `DAY_MAP` 推进 day,进入游戏日初始化难度,否则重置 5 项为 -1。
+- `week/day` 守卫由 `aa34f5b`(2024-06-19)引入;`100 - x` 转换由 `0287a32`(2024-07-08)引入。
+- 缺陷自 2025-07 起存在,**所有按「一天 5 活动」学习的用户均可能受影响**。
+
+## 四、修复方案
+
+1. `exchangeStudyRecord`:删除「每项 setDay」级联,恢复「5 项全部完成后按 `DAY_MAP` 推进 day」;进入游戏日(day=6)初始化游戏难度,否则重置 5 项为 -1。保留 `week/day` 守卫;仅在学习日(day 1~5)允许推进,避免 day=6 被 `DAY_MAP` 回绕成 1。
+2. `studySchedule`:进度展示转换改为仅跳过 -1(未开始),已完成 100 → 0(剩余 0%),并抽离公共方法 `convertToSurplus`。
+3. 执行数据修复 SQL,恢复受影响用户被写坏的历史数据。
+
+不改动 `t_user_study` 表结构,无 DDL 变更。
+
+## 五、数据修复 SQL
+
+**前提结论(day 语义已核实)**:`day` = 学习日(`Day xx`,一周内的第几天),每个 `day` 包含全部 5 个活动。依据:内容表 `t_study_listen` / `t_study_look` / `t_study_induction` / `t_study_answer` 的 `day` 字段注释均为 `Day xx`,且同一 `week+day` 下 5 张内容表成组存在。因此本条记录应回退到 day=1。
+
+第 1 步:备份命中记录(务必先执行,供回滚)。
+
+```sql
+CREATE TABLE t_user_study_bak_20261012 AS
+SELECT * FROM t_user_study
+WHERE day = 2 AND listen = 100
+  AND look = -1 AND induction = -1 AND answer = -1 AND pair = -1;
+```
+
+第 2 步:核对影响范围(预期 8 条)。
+
+```sql
+SELECT id, userId, week, day, listen, look, induction, answer, pair
+FROM t_user_study
+WHERE day = 2 AND listen = 100
+  AND look = -1 AND induction = -1 AND answer = -1 AND pair = -1;
+```
+
+第 3 步:执行修复。该记录实际只完成了第 1 学习日的「听音选图」,DB 被错误推进到 day=2;回退 `day=1` 并保留 `listen=100`(该完成度归属第 1 天),用户再完成其余 4 项即自动推进。
+
+```sql
+UPDATE t_user_study
+SET day = 1
+WHERE day = 2 AND listen = 100
+  AND look = -1 AND induction = -1 AND answer = -1 AND pair = -1;
+```
+
+回滚(如需还原,从备份表按主键恢复):
+
+```sql
+UPDATE t_user_study t
+JOIN t_user_study_bak_20261012 b ON b.id = t.id
+SET t.week = b.week, t.day = b.day, t.listen = b.listen, t.look = b.look,
+    t.induction = b.induction, t.answer = b.answer, t.pair = b.pair;
+```
+
+> 说明:`t_user_study` 的账号列为 `userId`(驼峰,非 `user_id`)。修复代码上线后,用户重新进入即可正常完成后续活动。若需精准只处理反馈用户,可在 WHERE 中追加其 `userId` 或经 `t_user` 按手机号关联(该手机号存在 id=94[已删除]、id=170[正常] 两个账号,执行前需确认生效账号)。
+
+## 六、影响面与回归点
+
+影响面:
+
+- 学习端「完成学习」写进度(`/completeLearning` → `exchangeStudyRecord`)。
+- 学习端首页进度展示(`/studySchedule`)。
+- 涉及所有按「一天 5 活动」推进的线上用户的历史数据。
+
+回归点:
+
+1. 第 1 学习日依次完成 5 个活动:过程中 `day` 保持不变,5 项依次置 100;第 5 项完成后 `day=2` 且 5 项重置为 -1。
+2. 逐日推进至第 5 学习日完成:`day=6` 且 `gameDifficulty=0`,可进入第 6 天自主游戏。
+3. 首页进度:未开始(-1)显示「未完成」,已完成(100)显示「已完成」,部分完成显示对应「剩余%」。
+4. 中途退出(`/exitLearning`)写入的部分进度不受影响。
+5. 季度切换时内存伪造进度的展示(`studySchedule` 服务层)仍正确。
+
+## 七、上线步骤
+
+1. 代码改动:`TUserStudyServiceImpl`、`TStudyController`。
+2. 打包发布 `ruoyi-study` 模块。
+3. 执行第五节的核对 SQL 与修复 SQL(建议先备份 `t_user_study`)。
+4. 用反馈用户账号回归第六节 1~3 点。
diff --git a/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/controller/TStudyController.java b/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/controller/TStudyController.java
index e3c7e21..9bd5017 100644
--- a/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/controller/TStudyController.java
+++ b/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/controller/TStudyController.java
@@ -1118,31 +1118,30 @@
             studyRecord.setTotalStudy(Math.round((float) (totalStudy + sum) / 3600));
             studyRecord = studyService.studySchedule(studyRecord, week);
             // 数据库保存的是已完成的学习进度,返回前端时需要转换为剩余学习进度
-            Integer look = studyRecord.getLook();
-            if (look != 100 && look != -1) {
-                studyRecord.setLook(100 - look);
-            }
-            Integer pair = studyRecord.getPair();
-            if (pair != 100 && pair != -1) {
-                studyRecord.setPair(100 - pair);
-            }
-            Integer answer = studyRecord.getAnswer();
-            if (answer != 100 && answer != -1) {
-                studyRecord.setAnswer(100 - answer);
-            }
-            Integer induction = studyRecord.getInduction();
-            if (induction != 100 && induction != -1) {
-                studyRecord.setInduction(100 - induction);
-            }
-            Integer listen = studyRecord.getListen();
-            if (listen != 100 && listen != -1) {
-                studyRecord.setListen(100 - listen);
-            }
+            // -1 表示未开始(中性值)原样返回;100 表示已完成,转换为 0(剩余0%)
+            studyRecord.setLook(convertToSurplus(studyRecord.getLook()));
+            studyRecord.setPair(convertToSurplus(studyRecord.getPair()));
+            studyRecord.setAnswer(convertToSurplus(studyRecord.getAnswer()));
+            studyRecord.setInduction(convertToSurplus(studyRecord.getInduction()));
+            studyRecord.setListen(convertToSurplus(studyRecord.getListen()));
         }
         return R.ok(studyRecord);
     }
 
     /**
+     * 将数据库中的「已完成学习进度」转换为前端展示的「剩余学习进度」
+     *
+     * @param completedProgress 已完成进度;-1 表示未开始,100 表示已完成
+     * @return 剩余进度;未开始(-1)原样返回
+     */
+    private Integer convertToSurplus(Integer completedProgress) {
+        if (null == completedProgress || Constants.BURDEN_ONE.equals(completedProgress)) {
+            return completedProgress;
+        }
+        return Constants.ONE_HUNDRED - completedProgress;
+    }
+
+    /**
      * 查询周目列表
      *
      * @param type    所属类型
diff --git a/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/service/impl/TUserStudyServiceImpl.java b/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/service/impl/TUserStudyServiceImpl.java
index de6e73a..b2fc8d4 100644
--- a/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/service/impl/TUserStudyServiceImpl.java
+++ b/ruoyi-service/ruoyi-study/src/main/java/com/ruoyi/study/service/impl/TUserStudyServiceImpl.java
@@ -104,51 +104,29 @@
             } else if (Constants.FIVE.equals(type)) {
                 userStudyRecord.setPair(Constants.ONE_HUNDRED);
             }
-            if (userStudyRecord.getListen().equals(Constants.ONE_HUNDRED)){
-                userStudyRecord.setDay(2);
-
+            // STEP 1: 当天五项自主学习全部完成(均为100)后,才推进到下一个学习day
+            boolean allLearningFinished = Constants.ONE_HUNDRED.equals(userStudyRecord.getListen())
+                    && Constants.ONE_HUNDRED.equals(userStudyRecord.getLook())
+                    && Constants.ONE_HUNDRED.equals(userStudyRecord.getInduction())
+                    && Constants.ONE_HUNDRED.equals(userStudyRecord.getAnswer())
+                    && Constants.ONE_HUNDRED.equals(userStudyRecord.getPair());
+            // 仅自主学习日(day 1~5)允许推进,避免 day=6(游戏日)被 DAY_MAP 回绕成 1
+            if (allLearningFinished && userStudyRecord.getDay() < Constants.SIX) {
+                // 按 DAY_MAP 推进:1→2→3→4→5→6(自主游戏日)
+                Integer nextDay = DAY_MAP.get(String.valueOf(userStudyRecord.getDay()));
+                userStudyRecord.setDay(nextDay);
+                if (Constants.SIX.equals(nextDay)) {
+                    // 进入自主游戏日,游戏难度初始化 0(入门难度)
+                    userStudyRecord.setGameDifficulty(Constants.ZERO);
+                } else {
+                    // 进入新学习日,重置五项自主学习进度为未开始(-1)
+                    userStudyRecord.setListen(Constants.BURDEN_ONE);
+                    userStudyRecord.setLook(Constants.BURDEN_ONE);
+                    userStudyRecord.setInduction(Constants.BURDEN_ONE);
+                    userStudyRecord.setAnswer(Constants.BURDEN_ONE);
+                    userStudyRecord.setPair(Constants.BURDEN_ONE);
+                }
             }
-            if (userStudyRecord.getLook().equals(Constants.ONE_HUNDRED)){
-                userStudyRecord.setDay(3);
-
-            }
-            if (userStudyRecord.getInduction().equals(Constants.ONE_HUNDRED)){
-                userStudyRecord.setDay(4);
-
-            }
-            if (userStudyRecord.getAnswer().equals(Constants.ONE_HUNDRED)){
-                userStudyRecord.setDay(5);
-
-            }
-            if (userStudyRecord.getPair().equals(Constants.ONE_HUNDRED)){
-                userStudyRecord.setDay(6);
-
-            }
-            if (userStudyRecord.getDay().equals(Constants.SIX)) {
-                // 游戏难度初始化 0(入门难度)
-                userStudyRecord.setGameDifficulty(Constants.ZERO);
-            }
-//            if (userStudyRecord.getLook().equals(Constants.ONE_HUNDRED) &&
-//                    userStudyRecord.getAnswer().equals(Constants.ONE_HUNDRED) &&
-//                    userStudyRecord.getPair().equals(Constants.ONE_HUNDRED) &&
-//                    userStudyRecord.getInduction().equals(Constants.ONE_HUNDRED) &&
-//                    userStudyRecord.getListen().equals(Constants.ONE_HUNDRED)) {
-//                // 学习完成,进入下一学习day
-//                Integer nextDay = DAY_MAP.get(String.valueOf(userStudyRecord.getDay()));
-//                userStudyRecord.setDay(nextDay);
-//                // 下一day为 6,进入游戏日
-//                if (nextDay.equals(Constants.SIX)) {
-//                    // 游戏难度初始化 0(入门难度)
-//                    userStudyRecord.setGameDifficulty(Constants.ZERO);
-//                } else {
-//                    // 重置学习进度
-//                    userStudyRecord.setListen(Constants.BURDEN_ONE);
-//                    userStudyRecord.setLook(Constants.BURDEN_ONE);
-//                    userStudyRecord.setInduction(Constants.BURDEN_ONE);
-//                    userStudyRecord.setAnswer(Constants.BURDEN_ONE);
-//                    userStudyRecord.setPair(Constants.BURDEN_ONE);
-//                }
-//            }
         }
         // 更新学习时长
         userStudyRecord.setTotalStudy(userStudyRecord.getTotalStudy() + studyTime);

--
Gitblit v1.7.1