--- /dev/null
+-- ==========================================
+-- 1. 创建学习进度与统计日志表 (study_logs)
+-- ==========================================
+CREATE TABLE IF NOT EXISTS study_logs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ group_id INTEGER NOT NULL, -- 组号
+ op_type TEXT NOT NULL, -- 操作类型: 'study', 'test', 'review1', 'review2', 'review3'
+ status TEXT NOT NULL DEFAULT 'completed', -- 状态: 'completed', 'started'
+ duration_sec INTEGER DEFAULT 0, -- 执行耗时 (秒)
+ word_count INTEGER DEFAULT 0, -- 本组单词总数
+ first_correct INTEGER DEFAULT 0, -- 第一次尝试正确个数
+ first_wrong INTEGER DEFAULT 0, -- 第一次尝试错误个数
+ total_rounds INTEGER DEFAULT 1, -- 通过本组所用的执行轮数
+ log_date DATE, -- 业务完成日期 (YYYY-MM-DD)
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 记录入库时间
+ remark TEXT, -- 备注
+ UNIQUE(group_id, op_type, log_date) -- 唯一性约束
+);
+
+CREATE INDEX IF NOT EXISTS idx_progress_lookup ON study_logs (op_type, group_id);
+
+-- ==========================================
+-- 2. 将结构信息补充到 schema_comments
+-- ==========================================
+
+-- 插入表描述
+INSERT OR REPLACE INTO schema_comments (object_type, table_name, column_name, description)
+VALUES ('table', 'study_logs', NULL, '存储单词学习、测试和复习的进度及效率统计日志');
+
+-- 插入列描述
+INSERT OR REPLACE INTO schema_comments (object_type, table_name, column_name, description) VALUES
+('column', 'study_logs', 'id', '自增主键'),
+('column', 'study_logs', 'group_id', '单词分组编号'),
+('column', 'study_logs', 'op_type', '操作模式类型: study(学习), test(测试), review1/2/3(复习轮次)'),
+('column', 'study_logs', 'status', '任务执行状态: started(开始), completed(已完成)'),
+('column', 'study_logs', 'duration_sec', '完成该任务所花费的总时长(秒)'),
+('column', 'study_logs', 'word_count', '该分组包含的单词总数'),
+('column', 'study_logs', 'first_correct', '在第一轮测试/复习中直接答对的单词数量'),
+('column', 'study_logs', 'first_wrong', '在第一轮测试/复习中答错的单词数量'),
+('column', 'study_logs', 'total_rounds', '为了达到全对通过,该组测试重复执行的总轮数'),
+('column', 'study_logs', 'log_date', '学习任务归属的业务日期'),
+('column', 'study_logs', 'created_at', '数据库记录创建的系统时间'),
+('column', 'study_logs', 'remark', '额外备注信息或原始日志文本');
from fastapi.middleware.cors import CORSMiddleware
import sqlite3
import os
-import re
import datetime
from pydantic import BaseModel
from typing import List, Optional
allow_headers=["*"],
)
-# --- 文件路径配置 ---
-# 确保路径与你的服务器目录结构一致
+# --- 路径自动修复逻辑 ---
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-DATABASE_PATH = os.path.join(BASE_DIR, "db", "bcd.db")
-LOG_FILE_PATH = os.path.join(BASE_DIR, "a", "log.txt")
+# 优先检查 /home/words/backend/a/bcd.db
+DATABASE_PATH = os.path.join(BASE_DIR, "a", "bcd.db")
+if not os.path.exists(DATABASE_PATH):
+ # 备选检查 /home/words/backend/db/bcd.db
+ DATABASE_PATH = os.path.join(BASE_DIR, "db", "bcd.db")
# --- 数据模型 ---
class HomeData(BaseModel):
review2: str
review3: str
-# --- 辅助逻辑函数(移植自原 MainApp) ---
+# --- 辅助逻辑函数 ---
def calculate_hours_since(date_str: str) -> Optional[float]:
if not date_str:
return None
try:
- # 兼容原格式 "2023-05-24 10:00:00"
- last = datetime.datetime.strptime(date_str[:19], "%Y-%m-%d %H:%M:%S")
+ # 兼容数据库中的 YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD
+ date_part = date_str[:19]
+ fmt = "%Y-%m-%d %H:%M:%S" if " " in date_part else "%Y-%m-%d"
+ last = datetime.datetime.strptime(date_part, fmt)
return (datetime.datetime.now() - last).total_seconds() / 3600
except:
return None
-def get_progress_from_log():
- """解析 log.txt 获取每个组的完成时间"""
+def get_progress_from_db():
+ """解析数据库 study_logs 表获取每个组的完成时间"""
progress = {}
- if os.path.exists(LOG_FILE_PATH):
- with open(LOG_FILE_PATH, 'r', encoding='utf-8') as f:
- for line in f:
- # 匹配原有的日志格式
- match = re.search(r'PROGRESS_UPDATE: Group:(.+?), Type:(.+?), Status:completed, Date:(.+)', line)
- if match:
- g_n, a_t, c_d = match.groups()
- g_n = g_n.strip()
- if g_n not in progress:
- progress[g_n] = {'study': None, 'test': None, 'review1': None, 'review2': None, 'review3': None}
- progress[g_n][a_t.strip()] = c_d.strip()
+ try:
+ if not os.path.exists(DATABASE_PATH):
+ return progress
+
+ conn = sqlite3.connect(DATABASE_PATH)
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+
+ # 查询每个组每种类型的最后完成时间
+ query = """
+ SELECT group_id, op_type, MAX(created_at) as last_date
+ FROM study_logs
+ WHERE status = 'completed'
+ GROUP BY group_id, op_type
+ """
+ cursor.execute(query)
+ rows = cursor.fetchall()
+
+ for row in rows:
+ g_n = str(row['group_id'])
+ a_t = row['op_type']
+ c_d = row['last_date']
+
+ if g_n not in progress:
+ progress[g_n] = {'study': None, 'test': None, 'review1': None, 'review2': None, 'review3': None}
+
+ if a_t in progress[g_n]:
+ progress[g_n][a_t] = c_d
+
+ conn.close()
+ except Exception as e:
+ print(f"Database error in get_progress_from_db: {e}")
return progress
# --- 路由实现 ---
@app.get("/api/home/process", response_model=List[ProgressData])
async def get_home_process():
try:
- # 1. 从数据库获取所有分组
+ # 1. 获取所有分组
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT groupid FROM words ORDER BY CAST(groupid AS INTEGER) ASC")
all_groups = [str(row[0]) for row in cursor.fetchall()]
conn.close()
- # 2. ä»\8eæ\97¥å¿\97è§£æ\9e\90进度
- log_progress = get_progress_from_log()
+ # 2. ä»\8eæ\95°æ\8d®åº\93读å\8f\96进度
+ log_progress = get_progress_from_db()
- # 3. 模拟原有的 UI 显示逻辑
+ # 3. 业务逻辑处理 (完全保留你要求的 UI 逻辑)
results = []
first_unstudied = None
for g in all_groups:
for g_name in all_groups:
p = log_progress.get(g_name, {'study': None, 'test': None, 'review1': None, 'review2': None, 'review3': None})
-
- # 构建每一行数据
row = {"group": f"Group {g_name}"}
# 基础学习列
row[key] = f"✔️ {p[key][:10]}"
elif p[prev_key]:
hours = calculate_hours_since(p[prev_key])
- # 原逻辑:超过4小时显示“尽快复习”,否则显示“开始复习”
if hours and hours > 4:
row[key] = "⚠️请尽快复习"
else:
text-align: center;
}
- /* 四列等宽均分 */
- th:nth-child(1), td:nth-child(1) { width: 25%; font-weight: 600; color: #1d4ed8; }
- th:nth-child(2), td:nth-child(2) { width: 25%; }
- th:nth-child(3), td:nth-child(3) { width: 25%; }
- th:nth-child(4), td:nth-child(4) { width: 25%; color: #64748b; font-size: 13px; }
+ /* 修改为六列均分(100 / 6 ≈ 16.6%) */
+ th:nth-child(1), td:nth-child(1) { width: 16.6%; font-weight: 600; color: #1d4ed8; }
+ th:nth-child(2), td:nth-child(2) { width: 16.6%; }
+ th:nth-child(3), td:nth-child(3) { width: 16.6%; }
+ th:nth-child(4), td:nth-child(4) { width: 16.6%; }
+ th:nth-child(5), td:nth-child(5) { width: 16.6%; }
+ th:nth-child(6), td:nth-child(6) { width: 16.6%; color: #64748b; font-size: 13px; }
/* 最后一行无边框 */
tr:last-child td {