)
""")
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ today_prefix = now[:10]
+ # 去重: 当天已有同 (group_id, op_type) 的完成记录则跳过,防止前端重复提交产生脏数据
+ cursor.execute(
+ "SELECT id FROM study_logs WHERE group_id=? AND op_type=? AND status='completed' AND substr(created_at,1,10)=?",
+ (data.group_id, data.op_type, today_prefix)
+ )
+ if cursor.fetchone():
+ conn.close()
+ return {"status": "skipped", "date": now, "reason": "duplicate same-day record"}
cursor.execute(
"INSERT INTO study_logs (group_id, op_type, status, created_at) VALUES (?, ?, ?, ?)",
(data.group_id, data.op_type, 'completed', now)
const hasSpokenRef = useRef(null);
const transitioningRef = useRef(false);
+ const submittedRef = useRef(false); // 防止完成时重复提交进度
const currentWord = words[currentIndex];
// 朗读逻辑:通过后端 TTS 代理(有道美式发音)播放两遍,间隔 500ms
setCorrectionInput("");
setTimeout(() => { transitioningRef.current = false; }, 300);
} else {
+ if (submittedRef.current) return; // 防重入(按住Enter等场景)
+ submittedRef.current = true;
await axios.post(`${API_BASE}/api/progress/update`, {
group_id: groupId,
op_type: 'study'
const inputRef = useRef(null);
const copyRef = useRef(null);
+ const submittedRef = useRef(false); // 防止完成时重复提交进度
const currentWord = queue[currentIndex];
// 获取单词数据
// 完成测试,记录进度(op_type 依据活动类型:test / review1 / review2 / review3)
const finishTest = useCallback(async () => {
+ if (submittedRef.current) return; // 防重入(按住Enter等场景)
+ submittedRef.current = true;
await axios.post(`${API_BASE}/api/progress/update`, {
group_id: groupId,
op_type: activityType
<div className="test-card test-complete-card">
<h1>🎉 测试完成!</h1>
<p>Group {groupId} 全部通过!</p>
- <button className="action-btn" onClick={finishTest}>完成</button>
+ <button className="action-btn" onClick={finishTest} disabled={submittedRef.current}>完成</button>
</div>
</div>
);