package com.example.springboot.controller;

import com.example.springboot.dto.LongTermMemoryWithWord;
import com.example.springboot.entity.LongTermMemory;
import com.example.springboot.entity.Study;
import com.example.springboot.service.StudyService;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/study")
public class StudyController {

    @Resource
    private StudyService studyService;
    //  用户选择单词
    @PostMapping("/select-words")
    public ResponseEntity<?> userSelectWords(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        List<Integer> wordIds = (List<Integer>) payload.get("wordIds");

        if (userId == null || wordIds == null || wordIds.isEmpty()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("参数错误");
        }

        int insertedCount = studyService.insertUserSelectedWords(userId, wordIds);

        if (insertedCount == 0) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("所有单词已存在，无需再添加");
        }

        return ResponseEntity.ok("成功添加 " + insertedCount + " 个新单词");
    }

   //  获取已经选择的单词列表
    @GetMapping("/select-words")
    public ResponseEntity<?> getSelectedWords(@RequestParam(value = "userId", required = false) String userIdStr) {
        if (userIdStr == null || !userIdStr.matches("\\d+")) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("缺少或非法的 userId");
        }

        Integer userId = Integer.valueOf(userIdStr);
        return ResponseEntity.ok(studyService.getSelectedWordsByUserId(userId));
    }

    // 获取用户可选的未学习单词（用于前端弹窗选择）
    @GetMapping("/pending-words")
    public ResponseEntity<?> getPendingWords(
            @RequestParam("userId") Integer userId,
            @RequestParam(value = "limit", defaultValue = "100") Integer limit) {
        return ResponseEntity.ok(studyService.getPendingWords(userId, limit));
    }

    // 删除用户选择的单词
    @PostMapping("/remove-word")
    public ResponseEntity<?> removeWord(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        Integer wordId = (Integer) payload.get("wordId");
        if (userId == null || wordId == null) {
            return ResponseEntity.badRequest().body("参数错误");
        }
        int rows = studyService.deleteWord(userId, wordId);
        if (rows > 0) {
            return ResponseEntity.ok("删除成功");
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("未找到单词");
        }
    }

    //  批量删除用户选择的单词
    @PostMapping("/remove-words")
    public ResponseEntity<?> removeWords(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        List<Integer> wordIds = (List<Integer>) payload.get("wordIds");
        if (userId == null || wordIds == null || wordIds.isEmpty()) {
            return ResponseEntity.badRequest().body("参数错误");
        }
        int rows = studyService.deleteWords(userId, wordIds);
        if (rows > 0) {
            return ResponseEntity.ok("批量删除成功");
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("未找到单词");
        }
    }

    // 获取当前轮复习列表
    @GetMapping("/review-list")
    public ResponseEntity<?> getReviewList(@RequestParam Integer userId, @RequestParam Integer round) {
        List<Study> reviewList = studyService.getReviewList(userId, round);
        return ResponseEntity.ok(reviewList);
    }

    // 处理复习结果
    @PostMapping("/review-result")
    public ResponseEntity<?> postReviewResult(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        Integer wordId = (Integer) payload.get("wordId");
        Integer round = (Integer) payload.get("round");
        Boolean remembered = (Boolean) payload.get("remembered");

        if (userId == null || wordId == null || round == null || remembered == null) {
            return ResponseEntity.badRequest().body("参数错误");
        }

        studyService.processReviewResult(userId, wordId, round, remembered);
        return ResponseEntity.ok("复习结果处理成功");
    }

    // 获取用户待复习单词数量
    @GetMapping("/pending-count")
    public ResponseEntity<?> getPendingCount(@RequestParam Integer userId) {
        int count = studyService.getPendingCount(userId);
        return ResponseEntity.ok(count);
    }

    /**
     * 获取长期记忆阶段待复习单词列表（nextReviewDate <= 当前时间）
     */
//    @GetMapping("/long-term-review-list")
//    public ResponseEntity<?> getLongTermReviewList(
//            @RequestParam Integer userId,
//            @RequestParam(defaultValue = "0") int page,
//            @RequestParam(defaultValue = "3") int size,
//            @RequestParam(defaultValue = "0") int longmemorySeen
//    ) {
//        int offset = page * size;
//        List<LongTermMemoryWithWord> list = studyService.getLongTermReviewList(userId, longmemorySeen, offset, size);
//        return ResponseEntity.ok(list);
//    }
    @GetMapping("/long-term-review-list")
    public ResponseEntity<?> getLongTermReviewList(
            @RequestParam Integer userId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size
    ) {
        List<LongTermMemoryWithWord> list = studyService.getLongTermReviewList(userId, page, size);
        return ResponseEntity.ok(list);
    }
    /**
     * 处理长期记忆复习结果反馈
     * 请求体示例：
     * {
     *   "id": 123,
     *   "remembered": true
     * }
     */
    @PostMapping("/long-term-review-result")
    public ResponseEntity<?> postLongTermReviewResult(@RequestBody Map<String, Object> payload) {
        Integer id = (Integer) payload.get("id");
        Boolean remembered = (Boolean) payload.get("remembered");

        if (id == null || remembered == null) {
            return ResponseEntity.badRequest().body("参数错误");
        }

        LongTermMemory ltm = studyService.getLongTermMemoryById(id);
        if (ltm == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("未找到对应的长期记忆记录");
        }

        studyService.processLongTermReviewResult(ltm, remembered);
        return ResponseEntity.ok("长期记忆复习结果处理成功");
    }

}




package com.example.springboot.dto;

import com.example.springboot.entity.LongTermMemory;

public class LongTermMemoryWithWord extends LongTermMemory {
    private String word;
    private String definition;
    // getter/setter


    public String getWord() {
        return word;
    }

    public void setWord(String word) {
        this.word = word;
    }

    public String getDefinition() {
        return definition;
    }

    public void setDefinition(String definition) {
        this.definition = definition;
    }
}
package com.example.springboot.entity;

import java.time.LocalDateTime;

public class LongTermMemory {

    private Integer id;
    private Integer userId;
    private Integer wordId;

    private Integer reviewCount = 0;       // 总复习次数，默认0
    private Integer intervalDays = 0;      // 下次复习间隔（单位：天），默认0

    private LocalDateTime nextReviewDate;  // 下一次复习目标日期
    private LocalDateTime lastReviewDate;  // 上一次复习日期

    private Integer longmemoryScore = 0;  // 默认0
    private Integer longmemorySeen = 0;   // 默认0
    private Integer passCount = 0;         // 复习通过计数，默认0

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    private Integer round;

    public LongTermMemory() {
        // 这里字段已赋默认值，无需额外赋值
    }


    public Integer getRound() {
        return round;
    }

    public void setRound(Integer round) {
        this.round = round;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public Integer getWordId() {
        return wordId;
    }

    public void setWordId(Integer wordId) {
        this.wordId = wordId;
    }

    public Integer getReviewCount() {
        return reviewCount;
    }

    public void setReviewCount(Integer reviewCount) {
        this.reviewCount = reviewCount;
    }

    public Integer getIntervalDays() {
        return intervalDays;
    }

    public void setIntervalDays(Integer intervalDays) {
        this.intervalDays = intervalDays;
    }

    public LocalDateTime getNextReviewDate() {
        return nextReviewDate;
    }

    public void setNextReviewDate(LocalDateTime nextReviewDate) {
        this.nextReviewDate = nextReviewDate;
    }

    public LocalDateTime getLastReviewDate() {
        return lastReviewDate;
    }

    public void setLastReviewDate(LocalDateTime lastReviewDate) {
        this.lastReviewDate = lastReviewDate;
    }

    public Integer getLongmemoryScore() {
        return longmemoryScore;
    }

    public void setLongmemoryScore(Integer longmemoryScore) {
        this.longmemoryScore = longmemoryScore;
    }

    public Integer getLongmemorySeen() {
        return longmemorySeen;
    }

    public void setLongmemorySeen(Integer longmemorySeen) {
        this.longmemorySeen = longmemorySeen;
    }

    public Integer getPassCount() {
        return passCount;
    }

    public void setPassCount(Integer passCount) {
        this.passCount = passCount;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
}
package com.example.springboot.entity;

public class Study {
    private Integer id;
    private Integer userId;
    private Integer wordId;
    private Integer score;
    private Integer lastSeen;
    private String word;        // 单词文本
    private String definition;  // 单词释义

    // Getters and setters
    public String getWord() { return word; }
    public void setWord(String word) { this.word = word; }

    public String getDefinition() { return definition; }
    public void setDefinition(String definition) { this.definition = definition; }



    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }

    public Integer getUserId() { return userId; }
    public void setUserId(Integer userId) { this.userId = userId; }

    public Integer getWordId() { return wordId; }
    public void setWordId(Integer wordId) { this.wordId = wordId; }

    public Integer getScore() { return score; }
    public void setScore(Integer score) { this.score = score; }

    public Integer getLastSeen() { return lastSeen; }
    public void setLastSeen(Integer lastSeen) { this.lastSeen = lastSeen; }
}
package com.example.springboot.mapper;

import com.example.springboot.dto.LongTermMemoryWithWord;
import com.example.springboot.entity.LongTermMemory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.time.LocalDateTime;
import java.util.List;

@Mapper
public interface LongTermMemoryMapper {
    int insertLongTermMemory(LongTermMemory ltm);

    List<Integer> selectExistingWordIds(@Param("userId") Integer userId, @Param("wordIds") List<Integer> wordIds);

    List<LongTermMemoryWithWord> selectDueReviews(@Param("userId") Integer userId,
                                                  @Param("now") LocalDateTime now,
                                                  @Param("offset") Integer offset,
                                                  @Param("size") Integer size,
                                                  @Param("longmemorySeenList") List<Integer> longmemorySeenList);

    int updateLongTermMemory(LongTermMemory ltm);

    LongTermMemory selectById(@Param("id") Integer id);
}
package com.example.springboot.mapper;

import com.example.springboot.entity.Study;
import com.example.springboot.entity.Word;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface StudyMapper {

    // 从user_word_plan抽取指定数量且user_word_memory中不存在的单词
    List<Study> selectPendingWordsFromPlan(@Param("userId") Integer userId, @Param("limit") int limit);

    // 查询 user_word_memory 中对应记录
    Study selectStudyByUserIdAndWordId(@Param("userId") Integer userId, @Param("wordId") Integer wordId);

    // 插入 user_word_memory
    int insertStudy(Study study);

    // 更新 user_word_memory
    int updateStudy(Study study);

    // 查询单词详情
    Word selectWordById(@Param("id") Integer id);

    // 查询某个用户的所有已选单词详细信息
    List<Word> selectSelectedWordsByUserId(@Param("userId") Integer userId);

    //  单个删除
    int deleteStudyByUserIdAndWordId(@Param("userId") Integer userId, @Param("wordId") Integer wordId);

    //  批量删除
    int deleteStudiesByUserIdAndWordIds(@Param("userId") Integer userId, @Param("wordIds") List<Integer> wordIds);


    // 根据冷却轮数筛选出符合条件的单词，排序返回前N个
    List<Study> selectReviewCandidates(@Param("userId") Integer userId,
                                       @Param("round") int round,
                                       @Param("cooldown") int cooldown,
                                       @Param("limit") int limit);


    // 删除user_word_plan中对应记录
    int deletePlanWord(@Param("userId") Integer userId, @Param("wordId") Integer wordId);

    int countPendingWords(@Param("userId") Integer userId);
}
package com.example.springboot.service;

import com.example.springboot.dto.LongTermMemoryWithWord;
import com.example.springboot.entity.LongTermMemory;
import com.example.springboot.entity.Study;
import com.example.springboot.entity.Word;
import com.example.springboot.mapper.LongTermMemoryMapper;
import com.example.springboot.mapper.StudyMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@Service
public class StudyService {

    // --- Mapper 注入 ---
    @Resource
    private StudyMapper studyMapper;

    @Resource
    private LongTermMemoryMapper longTermMemoryMapper;

    // ================= 短期记忆相关 =================

    private static final int COOLDOWN = 2;
    private static final int PASS_SCORE = 3;
    private static final int N = 3;

    /**
     * 获取当前轮次的短期记忆复习候选单词
     */
    public List<Study> getReviewList(Integer userId, int round) {
        return studyMapper.selectReviewCandidates(userId, round, COOLDOWN, N);
    }

    /**
     * 处理短期记忆复习结果：
     * 通过累加分数判断是否通过阶段，
     * 通过后迁移到长期记忆表，否则更新分数和复习轮次
     */
    public void processReviewResult(Integer userId, Integer wordId, int currentRound, boolean remembered) {
        Study study = studyMapper.selectStudyByUserIdAndWordId(userId, wordId);
        if (study == null) return;

        int newScore = study.getScore();
        if (remembered) {
            newScore += 1;
        } else {
            newScore = Math.max(newScore - 1, 0);
        }

        if (newScore >= PASS_SCORE) {
            // 通过短期考核，迁移到长期记忆，删除短期记录
            moveToLongTermMemory(study, currentRound, newScore);
            studyMapper.deleteStudyByUserIdAndWordId(userId, wordId);
        } else {
            // 未通过，更新短期记忆分数和最近复习的轮次
            study.setScore(newScore);
            study.setLastSeen(currentRound);
            studyMapper.updateStudy(study);
        }
    }

    /**
     * 短期记忆→长期记忆迁移
     * intervalDays=0 表示首次长期复习未完成，
     * nextReviewDate设置为当前时间以供首次长期复习
     */
    private void moveToLongTermMemory(Study study, int currentRound, int score) {
        LongTermMemory ltm = new LongTermMemory();

        ltm.setUserId(study.getUserId());
        ltm.setWordId(study.getWordId());
        ltm.setReviewCount(0);       // 首次长期复习前 复习计数0
        ltm.setIntervalDays(0);      // 0表示首次长期复习阶段
        LocalDateTime now = LocalDateTime.now();
        ltm.setLastReviewDate(now);
        ltm.setNextReviewDate(now);  // 立即可复习首次长期任务
        // longmemoryScore, longmemorySeen等字段保持默认即可

        longTermMemoryMapper.insertLongTermMemory(ltm);
    }

    /**
     * 用户手动添加单词到短期记忆
     */
    public int insertUserSelectedWords(Integer userId, List<Integer> wordIds) {
        int count = 0;
        for (Integer wordId : wordIds) {
            Study exist = studyMapper.selectStudyByUserIdAndWordId(userId, wordId);
            if (exist == null) {
                Study study = new Study();
                study.setUserId(userId);
                study.setWordId(wordId);
                study.setScore(0);
                study.setLastSeen(-1);
                studyMapper.insertStudy(study);
                count++;
                studyMapper.deletePlanWord(userId, wordId);
            }
        }
        return count;
    }

    /**
     * 获取用户已选短期记忆单词列表
     */
    public List<Word> getSelectedWordsByUserId(Integer userId) {
        return studyMapper.selectSelectedWordsByUserId(userId);
    }

    /**
     * 获取用户待学习的短期记忆单词（待加入短期记忆）
     */
    public List<Study> getPendingWords(Integer userId, int limit) {
        return studyMapper.selectPendingWordsFromPlan(userId, limit);
    }

    /**
     * 删除单个短期记忆单词记录
     */
    public int deleteWord(Integer userId, Integer wordId) {
        return studyMapper.deleteStudyByUserIdAndWordId(userId, wordId);
    }

    /**
     * 批量删除短期记忆单词记录
     */
    public int deleteWords(Integer userId, List<Integer> wordIds) {
        return studyMapper.deleteStudiesByUserIdAndWordIds(userId, wordIds);
    }

    /**
     * 查询用户短期记忆待复习单词总数（用于展示）
     */
    public int getPendingCount(Integer userId) {
        return studyMapper.countPendingWords(userId);
    }


    // ================= 长期记忆相关 =================

    // 长期记忆复习阶段间隔（单位：天）
    private static final int[] INTERVALS = {1, 2, 4, 7, 15};

    /**
     * 获取长期记忆阶段待复习单词列表（nextReviewDate <= 当前时间）
     */
//    public List<LongTermMemoryWithWord> getLongTermReviewList(Integer userId, int longMemorySeen, int offset, int size) {
//        LocalDateTime now = LocalDateTime.now();
//        return longTermMemoryMapper.selectDueReviews(userId, now, offset, size, longMemorySeen);
//    }

    public List<LongTermMemoryWithWord> getLongTermReviewList(Integer userId, int page, int size) {
        int offset = page * size;
        LocalDateTime now = LocalDateTime.now();

        List<Integer> stages = Arrays.asList(0,1,2,3,4,5); // 支持多阶段轮换查询

        return longTermMemoryMapper.selectDueReviews(userId, now, offset, size, stages);
    }


    /**
     * 根据长期记忆ID查询对应实体
     */
    public LongTermMemory getLongTermMemoryById(Integer id) {
        return longTermMemoryMapper.selectById(id);
    }

    /**
     * 处理长期记忆复习结果：
     * 需累计通过3次才进入下一阶段间隔；
     * 未通过扣减分数，但不降低阶段；
     * 通过后更新时间间隔及复习时间。
     */
    public void processLongTermReviewResult(LongTermMemory ltm, boolean remembered) {
        if (ltm == null) return;

        int reviewCount = (ltm.getReviewCount() == null ? 0 : ltm.getReviewCount()) + 1;
        int score = ltm.getLongmemoryScore() == null ? 0 : ltm.getLongmemoryScore();
        int passCount = ltm.getPassCount() == null ? 0 : ltm.getPassCount();
        int round = ltm.getLongmemorySeen() == null ? 0 : ltm.getLongmemorySeen();

        // 阶段间隔小时数，依次递增
        int[] intervalsHours = {24, 48, 96, 168, 360};

        if (remembered) {
            score++;
            passCount++;

            if (score >= 3) {
                int currentInterval = intervalsHours[round];

                ltm.setNextReviewDate(LocalDateTime.now().plusHours(currentInterval));
                ltm.setLastReviewDate(LocalDateTime.now());
                ltm.setIntervalDays(currentInterval / 24);

                score = 0;
                passCount = 0;

                // 轮次后移，将在下一轮复习时使用该轮次
                round = Math.min(round + 1, intervalsHours.length - 1);
                ltm.setLongmemorySeen(round);
            }
        } else {
            score = Math.max(score - 1, 0);
            // 不更新复习时间和阶段
        }

        ltm.setReviewCount(reviewCount);
        ltm.setLongmemoryScore(score);
        ltm.setPassCount(passCount);

        longTermMemoryMapper.updateLongTermMemory(ltm);
    }

}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springboot.mapper.LongTermMemoryMapper">

    <insert id="insertLongTermMemory" parameterType="com.example.springboot.entity.LongTermMemory" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO long_term_memory(
            user_id, word_id, review_count, pass_count, interval_days,
            next_review_date, last_review_date,
            longmemory_score, longmemory_seen, created_at, updated_at
        )
        VALUES(
                  #{userId}, #{wordId}, #{reviewCount}, #{passCount}, #{intervalDays},
                  #{nextReviewDate}, #{lastReviewDate},
                  #{longmemoryScore}, #{longmemorySeen}, NOW(), NOW()
              )
    </insert>

    <select id="selectExistingWordIds" resultType="int" parameterType="map">
        SELECT word_id
        FROM long_term_memory
        WHERE user_id = #{userId}
        AND word_id IN
        <foreach collection="wordIds" item="wordId" open="(" separator="," close=")">
            #{wordId}
        </foreach>
    </select>

    <select id="selectDueReviews" resultType="com.example.springboot.dto.LongTermMemoryWithWord" parameterType="map">
        SELECT ltm.*, w.word, w.definition
        FROM long_term_memory ltm
        LEFT JOIN words w ON ltm.word_id = w.id
        WHERE ltm.user_id = #{userId}
        <if test="longmemorySeenList != null and longmemorySeenList.size() > 0">
            AND ltm.longmemory_seen IN
            <foreach collection="longmemorySeenList" item="item" open="(" separator="," close=")">
                #{item}
            </foreach>
        </if>
        AND ltm.next_review_date &lt;= #{now}
        ORDER BY
        CASE
        WHEN ltm.longmemory_seen = 0 THEN 1
        WHEN ltm.longmemory_seen = 1 THEN 2
        WHEN ltm.longmemory_seen = 2 THEN 3
        WHEN ltm.longmemory_seen = 3 THEN 4
        WHEN ltm.longmemory_seen = 4 THEN 5
        WHEN ltm.longmemory_seen = 5 THEN 6
        ELSE 7
        END ASC,
        ltm.next_review_date ASC,
        ltm.last_review_date ASC
        LIMIT #{offset}, #{size}
    </select>

    <update id="updateLongTermMemory" parameterType="com.example.springboot.entity.LongTermMemory">
        UPDATE long_term_memory
        SET review_count = #{reviewCount},
            pass_count = #{passCount},
            interval_days = #{intervalDays},
            next_review_date = #{nextReviewDate},
            last_review_date = #{lastReviewDate},
            longmemory_score = #{longmemoryScore},
            longmemory_seen = #{longmemorySeen},
            updated_at = NOW()
        WHERE id = #{id}
    </update>

    <select id="selectById" resultType="com.example.springboot.entity.LongTermMemory" parameterType="int">
        SELECT *
        FROM long_term_memory
        WHERE id = #{id}
    </select>

</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springboot.mapper.StudyMapper">

    <!-- 从 user_word_plan 中抽取待学习且 user_word_memory 中不存在的单词 -->
    <select id="selectPendingWordsFromPlan" parameterType="map" resultType="com.example.springboot.entity.Study">
        SELECT
            NULL AS id,
            up.user_id AS userId,
            up.word_id AS wordId,
            w.word AS word,
            w.definition AS definition,
            0 AS score,
            -1 AS lastSeen
        FROM user_word_plan up
                 JOIN words w ON up.word_id = w.id
                 LEFT JOIN user_word_memory um ON up.user_id = um.user_id AND up.word_id = um.word_id
        WHERE up.user_id = #{userId} AND up.status = 'pending' AND um.id IS NULL
        ORDER BY up.added_at ASC
        LIMIT #{limit}
    </select>


    <!-- 查询 user_word_memory -->
    <select id="selectStudyByUserIdAndWordId" parameterType="map" resultType="com.example.springboot.entity.Study">
        SELECT id, user_id AS userId, word_id AS wordId, score, last_seen AS lastSeen
        FROM user_word_memory
        WHERE user_id = #{userId} AND word_id = #{wordId}
    </select>

    <!-- 插入 user_word_memory -->
    <insert id="insertStudy" parameterType="com.example.springboot.entity.Study" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user_word_memory(user_id, word_id, score, last_seen)
        VALUES(#{userId}, #{wordId}, #{score}, #{lastSeen})
    </insert>

    <!-- 更新 user_word_memory -->
    <update id="updateStudy" parameterType="com.example.springboot.entity.Study">
        UPDATE user_word_memory
        SET score = #{score},
            last_seen = #{lastSeen}
        WHERE id = #{id}
    </update>

    <!-- 查询单词详情 -->
    <select id="selectWordById" parameterType="int" resultType="com.example.springboot.entity.Word">
        SELECT id, word, definition, pronunciation, custom_content AS customContent, created_at AS createdAt
        FROM words
        WHERE id = #{id}
    </select>

    <!-- 查询用户已选择的单词 -->
    <select id="selectSelectedWordsByUserId" parameterType="int" resultType="com.example.springboot.entity.Word">
        SELECT
            w.id,
            w.word,
            w.definition,
            w.pronunciation,
            w.custom_content AS customContent,
            w.created_at AS createdAt
        FROM user_word_memory uwm
                 JOIN words w ON uwm.word_id = w.id
        WHERE uwm.user_id = #{userId}
    </select>

    <!--  删除单个单词 -->
    <delete id="deleteStudyByUserIdAndWordId" parameterType="map">
        DELETE FROM user_word_memory WHERE user_id = #{userId} AND word_id = #{wordId}
    </delete>

    <!--  批量删除单词 -->
    <delete id="deleteStudiesByUserIdAndWordIds" parameterType="map">
        DELETE FROM user_word_memory WHERE user_id = #{userId} AND word_id IN
        <foreach item="wordId" collection="wordIds" open="(" separator="," close=")">
            #{wordId}
        </foreach>
    </delete>

    <select id="selectReviewCandidates" resultType="com.example.springboot.entity.Study" parameterType="map">
        SELECT
            um.id,
            um.user_id AS userId,
            um.word_id AS wordId,
            um.score,
            um.last_seen AS lastSeen,
            w.word,
            w.definition,
            w.pronunciation
        FROM user_word_memory um
                 JOIN words w ON um.word_id = w.id
        WHERE um.user_id = #{userId} AND (#{round} - um.last_seen) >= #{cooldown}
        ORDER BY um.score ASC, um.last_seen ASC
        LIMIT #{limit}
    </select>

    <!-- 删除user_word_plan中对应记录 -->
    <delete id="deletePlanWord" parameterType="map">
        DELETE FROM user_word_plan WHERE user_id = #{userId} AND word_id = #{wordId}
    </delete>

    <select id="countPendingWords" resultType="int" parameterType="int">
        SELECT COUNT(*) FROM user_word_memory WHERE user_id = #{userId}
    </select>




</mapper>
<template>
  <el-row style="height: 100vh; padding: 20px;" :gutter="20">

    <!-- 左侧：学习单词区域 -->
    <el-col :span="12" class="left-panel" style="height: 100%;">

      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
        <h2>短期记忆区域</h2>
        <div>
          <el-button type="primary" @click="openSelectedDialog">查看已选单词</el-button>
          <el-button type="success" @click="openDialog" style="margin-left: 10px;">选择单词</el-button>
        </div>
      </div>

      <!-- 待复习单词数量 -->
      <div class="pending-count" v-if="reviewList.length">
        待复习单词数量: {{ pendingCount }}
      </div>

      <!-- 单词卡片 -->
      <div class="card-container" v-if="reviewList.length">

        <div class="card-wrapper">
          <div class="card" :class="{ flipped: isFlipped }">

            <div class="card-face card-front">
              <!-- 正面不显示通过次数，如果需要，类似写这里 -->
              <h1>{{ currentWord.word }}</h1>
              <el-button @click.stop="speakWord(currentWord.word)">🔊 发音</el-button>
            </div>

            <div class="card-face card-back" style="position: relative;">
              <!-- 通过次数左上角绝对定位 -->
              <div class="score-badge">
                通过次数: {{ currentWord.score ?? 0 }}
              </div>
              <h1 style="margin-top: 40px;">{{ currentWord.word }}</h1>
              <p><strong>释义：</strong>{{ currentWord.definition }}</p>
              <el-button @click.stop="speakWord(currentWord.word)">🔊 发音</el-button>
            </div>

          </div>

          <div class="btn-group">
            <el-button type="danger" v-if="!isFlipped" @click="flipCard">忘记</el-button>
            <el-button type="success" v-if="!isFlipped" @click="flipCard">认识</el-button>
            <el-button type="danger" v-if="isFlipped" @click="submitReviewResult(false)">忘记</el-button>
            <el-button type="success" v-if="isFlipped" @click="submitReviewResult(true)">认识</el-button>
          </div>
        </div>
      </div>

      <!-- 无可复习单词提示 -->
      <div v-else style="text-align:center; margin-top: 50px; font-size: 20px; color: #999;">
        无可复习单词，复习完成！
      </div>

      <!-- 已选单词弹窗 -->
      <el-dialog v-model="selectedDialogVisible" title="已选单词列表" width="800px">
        <el-table
            :data="selectedWords"
            style="width: 100%;"
            border
            @selection-change="handleSelectedChange"
        >
          <el-table-column type="selection" width="55" />
          <el-table-column prop="id" label="ID" width="80" />
          <el-table-column prop="word" label="单词" />
          <el-table-column prop="definition" label="释义" />
          <el-table-column label="操作" width="100">
            <template #default="scope">
              <el-button type="danger" size="small" @click="removeWord(scope.row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="selectedDialogVisible = false">关闭</el-button>
            <el-button type="danger" :disabled="!selectedToDelete.length" @click="batchRemoveWords">
              批量删除（{{ selectedToDelete.length }}）
            </el-button>
          </span>
        </template>
      </el-dialog>

    </el-col>

    <!-- 右侧区域：长期记忆区域 -->
    <el-col :span="12" class="right-panel" style="border: 1px solid #eee; padding: 20px;">
      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
        <h2>长期记忆区域</h2>
        <div>
          <el-button size="big" type="primary" @click="refreshLongTermReview" style="margin-left: 10px;">
            刷新
          </el-button>
        </div>
      </div>


      <div v-if="longTermReviewList.length">

        <div class="card-container">

          <div class="card-wrapper">
            <div class="card" :class="{ flipped: longTermIsFlipped }">

              <div class="card-face card-front">
                <h1>{{ currentLongTermWord.word }}</h1>
                <el-button @click.stop="speakWord(currentLongTermWord.word)">🔊 发音</el-button>
              </div>

              <div class="card-face card-back" style="position: relative;">
                <div class="score-badge">
                  复习次数: {{ currentLongTermWord.reviewCount ?? 0 }} <br/>
                  间隔天数: {{ currentLongTermWord.intervalDays ?? 0 }}
                </div>
                <h1 style="margin-top: 40px;">{{ currentLongTermWord.word }}</h1>
                <p><strong>释义：</strong>{{ currentLongTermWord.definition }}</p>
                <el-button @click.stop="speakWord(currentLongTermWord.word)">🔊 发音</el-button>
              </div>

            </div>

            <div class="btn-group">
              <el-button type="danger" v-if="!longTermIsFlipped" @click="flipLongTermCard">忘记</el-button>
              <el-button type="success" v-if="!longTermIsFlipped" @click="flipLongTermCard">认识</el-button>
              <el-button type="danger" v-if="longTermIsFlipped" @click="submitLongTermReviewResult(false)">忘记</el-button>
              <el-button type="success" v-if="longTermIsFlipped" @click="submitLongTermReviewResult(true)">认识</el-button>
            </div>
          </div>
        </div>

      </div>

      <div v-else style="text-align:center; margin-top: 50px; font-size: 20px; color: #999;">
        无可复习的长期记忆单词，复习完成！
      </div>
    </el-col>

    <!-- 选择待学习单词弹窗 -->
    <el-dialog v-model="dialogVisible" title="选择待学习单词" width="600px">
      <el-table :data="pendingWords" style="width: 100%;" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" />
        <el-table-column prop="word" label="单词" />
        <el-table-column prop="definition" label="释义" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitSelectedWords">确认选择（{{ selectedToAdd.length }}）</el-button>
        </span>
      </template>
    </el-dialog>

  </el-row>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'

axios.defaults.baseURL = 'http://localhost:8080'

const selectedWords = ref([])
const pendingWords = ref([])
const selectedToAdd = ref([])
const selectedToDelete = ref([])
const dialogVisible = ref(false)
const selectedDialogVisible = ref(false)

const round = ref(1)
const reviewList = ref([])
const reviewIndex = ref(0)
const pendingCount = ref(0)

const isFlipped = ref(false)

const currentWord = computed(() => reviewList.value[reviewIndex.value] || {})

const utterance = new SpeechSynthesisUtterance()
utterance.lang = 'en-US'

// 长期记忆相关状态
const longTermReviewList = ref([])
const longTermReviewIndex = ref(0)
const longTermIsFlipped = ref(false)
const currentPage = ref(0)

const currentLongTermWord = computed(() => longTermReviewList.value[longTermReviewIndex.value] || {})

async function refreshLongTermReview() {
  currentPage.value = 0           // 刷新时重置页码
  longTermReviewIndex.value = 0   // 复位当前卡片索引
  await fetchLongTermReviewList() // 重新获取数据
}

async function fetchPendingCount() {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning('请先登录或设置 userId')
  try {
    const res = await axios.get('/study/pending-count', { params: { userId: Number(userId) } })
    pendingCount.value = res.data
  } catch {
    ElMessage.error('获取待复习单词数量失败')
  }
}

async function fetchSelectedWords() {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning('请先登录或设置 userId')
  try {
    const res = await axios.get('/study/select-words', { params: { userId } })
    selectedWords.value = res.data
  } catch {
    ElMessage.error('获取已选单词失败')
  }
}

async function fetchReviewList() {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning('请先登录或设置 userId')
  try {
    const res = await axios.get('/study/review-list', {
      params: { userId: Number(userId), round: round.value }
    })
    reviewList.value = res.data
    reviewIndex.value = 0
    isFlipped.value = false

    if (reviewList.value.length > 0) {
      speakWord(currentWord.value.word)
    }
  } catch (e) {
    ElMessage.error('加载复习单词失败')
    console.error(e)
  }
}

function flipCard() {
  if (!currentWord.value.word) return
  speakWord(currentWord.value.word)
  isFlipped.value = true
}

async function submitReviewResult(remembered) {
  const userId = localStorage.getItem('userId')
  if (!userId) {
    ElMessage.warning('请先登录或设置 userId')
    return
  }

  try {
    await axios.post('/study/review-result', {
      userId: Number(userId),
      wordId: currentWord.value.wordId || currentWord.value.id,
      round: round.value,
      remembered: remembered
    })

    reviewIndex.value++
    isFlipped.value = false

    await fetchPendingCount() // 刷新待复习数量

    if (reviewIndex.value >= reviewList.value.length) {
      round.value++
      await fetchReviewList()
    } else {
      speakWord(currentWord.value.word)
    }
  } catch (e) {
    ElMessage.error('提交复习结果失败')
    console.error(e)
  }
}

function speakWord(text) {
  if (!text) return
  if (speechSynthesis.speaking) speechSynthesis.cancel()
  utterance.text = text
  speechSynthesis.speak(utterance)
}

async function openSelectedDialog() {
  await fetchSelectedWords()
  selectedToDelete.value = []
  selectedDialogVisible.value = true
}

async function openDialog() {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning('请先登录或设置 userId')
  try {
    const res = await axios.get('/study/pending-words', {
      params: { userId, limit: 100 }
    })
    pendingWords.value = res.data
    dialogVisible.value = true
  } catch {
    ElMessage.error('获取待学习单词失败')
  }
}

function handleSelectionChange(val) {
  selectedToAdd.value = val
}

function handleSelectedChange(val) {
  selectedToDelete.value = val
}

async function submitSelectedWords() {
  const userId = localStorage.getItem('userId')
  const wordIds = selectedToAdd.value.map(item => item.wordId || item.id)
  if (wordIds.length < 10) {
    ElMessage.warning('请至少选择10个单词')
    return
  }
  try {
    await axios.post('/study/select-words', { userId: Number(userId), wordIds })
    dialogVisible.value = false
    await fetchSelectedWords()
    await fetchPendingCount() // 选择后刷新数量
    await fetchReviewList()
    ElMessage.success('添加成功')
  } catch {
    ElMessage.error('添加单词失败')
  }
}

async function removeWord(row) {
  const userId = localStorage.getItem('userId')
  try {
    await axios.post('/study/remove-word', {
      userId: Number(userId),
      wordId: row.wordId || row.id
    })
    await fetchSelectedWords()
    await fetchPendingCount() // 删除后刷新数量
    await fetchReviewList()
    ElMessage.success('删除成功')
  } catch {
    ElMessage.error('删除失败')
  }
}

async function batchRemoveWords() {
  const userId = localStorage.getItem('userId')
  const wordIds = selectedToDelete.value.map(item => item.wordId || item.id)
  if (!wordIds.length) {
    ElMessage.warning('请选择要删除的单词')
    return
  }
  try {
    await axios.post('/study/remove-words', { userId: Number(userId), wordIds })
    await fetchSelectedWords()
    selectedToDelete.value = []
    await fetchPendingCount() // 批量删除后刷新数量
    await fetchReviewList()
    ElMessage.success('批量删除成功')
  } catch {
    ElMessage.error('批量删除失败')
  }
}

// 长期记忆相关方法

async function fetchLongTermReviewList() {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning('请先登录或设置 userId')
  try {
    const res = await axios.get('/study/long-term-review-list', {
      params: {
        userId: Number(userId),
        page: currentPage.value,
        size: 3,
        longmemorySeen: 0
      }
    })
    if (res.data.length === 0 && currentPage.value > 0) {
      currentPage.value = 0
      return await fetchLongTermReviewList()
    }

    longTermReviewList.value = res.data
    longTermReviewIndex.value = 0
    longTermIsFlipped.value = false

    if (longTermReviewList.value.length > 0) {
      speakWord(currentLongTermWord.value.word)
    } else {
      // ElMessage.success('长期记忆阶段复习完成！')
    }
  } catch (error) {
    // ElMessage.error('加载长期记忆复习列表失败')
    console.error(error)
  }
}

function flipLongTermCard() {
  if (!currentLongTermWord.value.word) return
  speakWord(currentLongTermWord.value.word)
  longTermIsFlipped.value = true
}

async function submitLongTermReviewResult(remembered) {
  if (!currentLongTermWord.value.id) return
  const userId = localStorage.getItem('userId')
  if (!userId) {
    ElMessage.warning('请先登录或设置 userId')
    return
  }

  try {
    await axios.post('/study/long-term-review-result', {
      userId: Number(userId),
      id: currentLongTermWord.value.id,
      remembered
    })

    longTermReviewIndex.value++
    longTermIsFlipped.value = false

    if (longTermReviewIndex.value >= longTermReviewList.value.length) {
      currentPage.value++
      await fetchLongTermReviewList()
    } else {
      speakWord(currentLongTermWord.value.word)
    }
  } catch (error) {
    ElMessage.error('提交长期记忆复习结果失败')
    console.error(error)
  }
}
onMounted(async () => {
  speechSynthesis.speak(new SpeechSynthesisUtterance(' '))
  await fetchPendingCount()
  await fetchReviewList()
  await fetchLongTermReviewList()
})
</script>

<style scoped>
.left-panel,
.right-panel {
  display: flex;
  flex-direction: column;
  border: 1px solid #eee;
  padding: 20px;
  height: 100%;
}
.pending-count {
  font-size: 18px;
  font-weight: bold;
  color: #409EFF;
  padding: 10px 0 0 0;
  margin-bottom: 10px;
  user-select: none;
}

.card-container {
  flex: 1;
  margin-top: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  perspective: 1000px;
  width: 100%;
}

.card {
  width: 100%;
  max-width: 550px;
  height: 500px;
  position: relative;
  transition: transform 0.6s;
  transform-style: preserve-3d;
  border-radius: 16px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
  background-color: #fff;
}

.score-badge {
  position: absolute;
  top: 15px;
  left: 15px;
  background-color: #409EFF;
  color: white;
  font-weight: bold;
  padding: 6px 12px;
  border-radius: 12px;
  font-size: 14px;
  pointer-events: none;
  user-select: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 10;
}

.card.flipped {
  transform: rotateY(180deg);
}

.card-face {
  position: absolute;
  width: 100%;
  height: 100%;
  padding: 20px;
  box-sizing: border-box;
  backface-visibility: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.card-front {
  font-size: 1em;
  font-weight: bold;
}

.card-back {
  transform: rotateY(180deg);
  font-size: 1.2em;
  text-align: center;
  line-height: 1.6;
}

.card-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.btn-group {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 30px;
  margin-top: 30px;
}

.btn-group .el-button {
  padding: 18px 40px;
  font-size: 18px;
  border-radius: 10px;
  font-weight: bold;
  min-width: 140px;
  transition: all 0.2s ease-in-out;
}

.btn-group .el-button:hover {
  transform: scale(1.05);
}
</style>

帮忙用plantuml画系统层次架构图，本系统采用前后端分离架构，基于 Spring Boot3 与 Vue3等主流技术栈开发，其中后端开发严格遵循 MVC 设计模式。前端界面依托 Element-UI plus组件库实现高效的 UI 开发；后端则细分为控制层、服务层与持久层，与前端视图层共同构成系统整体架构。各层级间通过标准化接口进行解耦交互，充分满足高内聚、低耦合的工程化设计原则。