现在把长期记忆的逻辑改成：在短期记忆算法的基础上，加上一个时间间隔（1→2→4→7→15） 天，只有短期记忆通过，才能安排下一个阶段的时间。
比如一个单词从短期记忆区域，到长期记忆区域，首次出现在长期记忆区域实行短期记忆算法，实行完，才会安排1天后复习，一天后的复习也是要经过短期记忆算法，才能安排在两天后才能复习。


CREATE TABLE long_term_memory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    word_id INT NOT NULL,
    review_count INT DEFAULT 0,          -- 总复习次数
    interval_days INT DEFAULT 0,         -- 下次复习间隔（单位：天）
    next_review_date DATETIME NOT NULL,  -- 下一次复习目标日期（精确时间）
    last_review_date DATETIME NOT NULL,  -- 上一次复习日期
    longmemory_score INT DEFAULT NULL,  
		longmemory_seen INT DEFAULT -1,
    pass_count INT DEFAULT 0,             -- 新增字段，复习通过计数
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES user(id),
    FOREIGN KEY (word_id) REFERENCES words(id),
    UNIQUE KEY uniq_user_word (user_id, word_id)
);

package com.example.springboot.entity;

import java.sql.Date;
import java.time.LocalDateTime;

public class LongTermMemory {
    
}
<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;">
      <h2>长期记忆区域</h2>
      <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 currentLongTermWord = computed(() => longTermReviewList.value[longTermReviewIndex.value] || {})

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) } })
    longTermReviewList.value = res.data
    longTermReviewIndex.value = 0
    longTermIsFlipped.value = false

    if (longTermReviewList.value.length > 0) {
      speakWord(currentLongTermWord.value.word)
    }
  } catch (e) {
    ElMessage.error('加载长期记忆复习列表失败')
    console.error(e)
  }
}

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

async function submitLongTermReviewResult(remembered) {
  const userId = localStorage.getItem('userId')
  if (!userId) {
    ElMessage.warning('请先登录或设置 userId')
    return
  }
  const ltmId = currentLongTermWord.value.id
  try {
    await axios.post('/study/long-term-review-result', { userId: Number(userId), ltmId, remembered })
    longTermReviewIndex.value++
    longTermIsFlipped.value = false

    if (longTermReviewIndex.value >= longTermReviewList.value.length) {
      ElMessage.success('长期记忆阶段复习完成！')
      longTermReviewList.value = []
    } else {
      speakWord(currentLongTermWord.value.word)
    }
  } catch (e) {
    ElMessage.error('提交长期记忆复习结果失败')
    console.error(e)
  }
}

onMounted(async () => {
  speechSynthesis.speak(new SpeechSynthesisUtterance(' '))
  await fetchPendingCount()
  await fetchReviewList()
  await fetchLongTermReviewList()
})
</script>

<style scoped>
.left-panel {
  display: flex;
  flex-direction: column;
  border: 1px solid #eee;
  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-grow: 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>
package com.example.springboot.mapper;

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.Date;
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<LongTermMemory> selectDueReviews(@Param("userId") Integer userId,
                                          @Param("now") LocalDateTime now);

    int updateLongTermMemory(LongTermMemory ltm);

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

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);
    }




}
package com.example.springboot.service;

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.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.IntStream;

@Service
public class StudyService {

    @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;

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

    /**
     * 获取当前轮次可复习短期记忆单词列表
     */
    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) {
            // 达标迁移长期记忆阶段（首次长期复习未完成：intervalDays=0）
            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);      // 用户可立即复习首次长期任务
        ltm.setLastScore(score);

        longTermMemoryMapper.insertLongTermMemory(ltm);
    }

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

    /**
     * 根据Id获取长期记忆实体
     */
    public LongTermMemory getLongTermMemoryById(Integer id) {
        return longTermMemoryMapper.selectById(id);
    }

    /**
     * 处理长期复习结果，必须累计通过3次才进入下一阶段间隔
     */
    public void processLongTermReviewResult(LongTermMemory ltm, boolean remembered) {
        LocalDateTime now = LocalDateTime.now();
        Integer passCount = ltm.getPassCount() == null ? 0 : ltm.getPassCount();

        if (ltm.getIntervalDays() == 0) {
            // 首次长期复习
            if (remembered) {
                passCount++;
                ltm.setPassCount(passCount);
                if (passCount >= 3) {
                    ltm.setIntervalDays(INTERVALS[0]);
                    ltm.setNextReviewDate(now.plusDays(INTERVALS[0]));
                    ltm.setPassCount(0); // 重置通过计数
                } else {
                    ltm.setNextReviewDate(now.plusHours(1)); // 未达3次，间隔1小时后复习
                }
            } else {
                passCount = 0;
                ltm.setPassCount(passCount);
                ltm.setNextReviewDate(now.plusHours(1));
            }
            ltm.setLastReviewDate(now);
            ltm.setLastScore(remembered ? PASS_SCORE : 0);
            longTermMemoryMapper.updateLongTermMemory(ltm);
            return;
        }

        // 正常长期复习
        if (remembered) {
            passCount++;
        } else {
            passCount = 0;
            // 忘记则回退间隔
            int currentIndex = IntStream.range(0, INTERVALS.length)
                    .filter(i -> INTERVALS[i] == ltm.getIntervalDays())
                    .findFirst()
                    .orElse(0);
            ltm.setIntervalDays(INTERVALS[Math.max(currentIndex - 1, 0)]);
        }

        ltm.setPassCount(passCount);
        ltm.setReviewCount(ltm.getReviewCount() + 1);
        ltm.setLastReviewDate(now);
        ltm.setLastScore(remembered ? PASS_SCORE : 0);

        // 达到3次通过才升级间隔并设置下次复习时间
        if (passCount >= 3) {
            int currentIndex = IntStream.range(0, INTERVALS.length)
                    .filter(i -> INTERVALS[i] == ltm.getIntervalDays())
                    .findFirst()
                    .orElse(0);
            if (remembered) {
                ltm.setIntervalDays(INTERVALS[Math.min(currentIndex + 1, INTERVALS.length - 1)]);
            } else {
                ltm.setIntervalDays(INTERVALS[Math.max(currentIndex - 1, 0)]);
            }
            ltm.setNextReviewDate(now.plusDays(ltm.getIntervalDays()));
            ltm.setPassCount(0); // 重置通过计数
        } else {
            ltm.setNextReviewDate(now.plusHours(1)); // 未达3次，则1小时后重试
        }

        longTermMemoryMapper.updateLongTermMemory(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);
    }
}
<?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, last_score)
        VALUES(#{userId}, #{wordId}, #{reviewCount}, #{passCount}, #{intervalDays}, #{nextReviewDate}, #{lastReviewDate}, #{lastScore})
    </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} AND ltm.next_review_date &lt;= #{now}
        ORDER BY
            CASE WHEN ltm.pass_count &lt; 3 THEN 0 ELSE 1 END ASC,
            ltm.next_review_date ASC
    </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},
                                    last_score = #{lastScore}
        WHERE id = #{id}
    </update>

    <!-- 新增：通过id查询长期记忆 -->
    <select id="selectById" resultType="com.example.springboot.entity.LongTermMemory" parameterType="int">
        SELECT *
        FROM long_term_memory
        WHERE id = #{id}
    </select>

</mapper>