改造为 SQLite 原生读写
This commit is contained in:
@@ -11,17 +11,13 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app import repository # noqa: E402
|
||||
from app.data import ( # noqa: E402
|
||||
append_operation_log,
|
||||
course_summary_to_class_record_line,
|
||||
course_summary_semantic_key,
|
||||
create_course_summary_review_task,
|
||||
normalize_course_summary,
|
||||
read_admin_tasks,
|
||||
read_course_summary_state,
|
||||
safe_filename_part,
|
||||
sha1_text,
|
||||
write_course_summary_state,
|
||||
)
|
||||
|
||||
|
||||
@@ -41,12 +37,13 @@ EXCLUDE_PREFIXES = (
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="导入历史课程小结 Markdown 到 VPS 数据目录")
|
||||
parser = argparse.ArgumentParser(description="导入历史课程小结 Markdown 到 SQLite")
|
||||
parser.add_argument("--source", required=True, type=Path, help="本机历史课程小结采集目录")
|
||||
parser.add_argument("--target", default=Path("/data/course_summaries"), type=Path, help="VPS 课程小结正式目录")
|
||||
parser.add_argument("--state", default=Path("/data/course_summary_state.json"), type=Path, help="课程小结状态文件")
|
||||
parser.add_argument("--tasks", default=Path("/data/admin_tasks.json"), type=Path, help="管理任务文件")
|
||||
parser.add_argument("--operation-logs", default=Path("/data/operation_logs.jsonl"), type=Path, help="操作日志文件")
|
||||
parser.add_argument("--db-path", default=Path("/data/xsk_education.db"), type=Path, help="SQLite 数据库路径")
|
||||
parser.add_argument("--target", type=Path, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--state", type=Path, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--tasks", type=Path, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--operation-logs", type=Path, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--missing-table", type=Path, help="历史 classnotes缺失.txt;不传则尝试 source/classnotes缺失.txt")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只统计,不写入")
|
||||
return parser.parse_args()
|
||||
@@ -292,30 +289,123 @@ def import_missing_tasks(source: Path, missing_table: Path, tasks_path: Path, dr
|
||||
return created
|
||||
|
||||
|
||||
def collect_history_entries(source: Path) -> tuple[int, list[dict], int]:
|
||||
scanned = 0
|
||||
skipped = 0
|
||||
entries: list[dict] = []
|
||||
for path in sorted(source.rglob("*.md")):
|
||||
if not should_import_markdown(path, source):
|
||||
continue
|
||||
scanned += 1
|
||||
for entry in iter_markdown_entries(path):
|
||||
try:
|
||||
entries.append(normalize_course_summary(entry))
|
||||
except ValueError:
|
||||
skipped += 1
|
||||
return scanned, entries, skipped
|
||||
|
||||
|
||||
def collect_missing_review_tasks(source: Path, missing_table: Path) -> list[tuple[dict, str]]:
|
||||
rows = parse_missing_table(missing_table)
|
||||
tasks: list[tuple[dict, str]] = []
|
||||
for row in rows:
|
||||
source_seed = "|".join(str(row.get(key, "")) for key in ("日期", "学生", "老师", "科目", "来源文件", "备注"))
|
||||
source_id = f"history-missing:{sha1_text(source_seed, 20)}"
|
||||
summary = {
|
||||
"source_id": source_id,
|
||||
"student": row.get("学生", ""),
|
||||
"date_iso": str(row.get("日期", "")).replace(".", "-"),
|
||||
"time_range": row.get("时间段", ""),
|
||||
"duration": row.get("时长", ""),
|
||||
"teacher": row.get("老师", ""),
|
||||
"subject": row.get("科目", ""),
|
||||
"group": row.get("群聊", ""),
|
||||
"title": "历史 classnotes 缺失",
|
||||
"body": f"历史 classnotes 缺失项:{source_seed}",
|
||||
"recognition_source": "history_missing_table",
|
||||
"confidence": "review",
|
||||
"teacher_trusted": False,
|
||||
"remark": row.get("备注", ""),
|
||||
}
|
||||
summary.update({key: value for key, value in source_summary_for_missing_row(source, row).items() if value not in ("", None)})
|
||||
proposed_line = ""
|
||||
try:
|
||||
proposed_line = course_summary_to_class_record_line(summary)
|
||||
except ValueError:
|
||||
pass
|
||||
tasks.append((summary, proposed_line))
|
||||
return tasks
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
source = args.source.resolve()
|
||||
target = args.target.resolve()
|
||||
repository.SQLITE_DB_PATH = args.db_path.resolve()
|
||||
missing_table = args.missing_table or (source / "classnotes缺失.txt")
|
||||
scanned, copied = copy_markdown_files(source, target, args.dry_run)
|
||||
imported, skipped = rebuild_state_from_markdown(target if target.exists() else source, args.state, args.dry_run)
|
||||
review_tasks = import_missing_tasks(source, missing_table, args.tasks, args.dry_run)
|
||||
scanned, entries, skipped = collect_history_entries(source)
|
||||
missing_tasks = collect_missing_review_tasks(source, missing_table)
|
||||
batch_id = f"history-import-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
if not args.dry_run:
|
||||
append_operation_log(
|
||||
args.operation_logs,
|
||||
"history_course_summary_import",
|
||||
"completed",
|
||||
source=str(source),
|
||||
target=str(target),
|
||||
scanned_files=scanned,
|
||||
copied_files=copied,
|
||||
indexed_summaries=imported,
|
||||
skipped_summaries=skipped,
|
||||
review_tasks=review_tasks,
|
||||
)
|
||||
def work(conn, backup_id):
|
||||
imported = 0
|
||||
duplicates = 0
|
||||
for summary in entries:
|
||||
saved = repository._save_course_summary(conn, summary)
|
||||
repository._add_summary_seen(conn, summary["source_id"], course_summary_semantic_key(summary))
|
||||
imported += 1 if saved.get("added") else 0
|
||||
duplicates += 0 if saved.get("added") else 1
|
||||
created_tasks = 0
|
||||
for summary, proposed_line in missing_tasks:
|
||||
repository._create_course_summary_review_task(
|
||||
conn,
|
||||
summary,
|
||||
proposed_line,
|
||||
["历史 classnotes缺失导入,默认只进入审核,不自动扣课时"],
|
||||
saved_path=str(summary.get("remark") or ""),
|
||||
)
|
||||
created_tasks += 1
|
||||
repository._add_ingest_batch(
|
||||
conn,
|
||||
{
|
||||
"batch_id": batch_id,
|
||||
"received_at": datetime.now().isoformat(timespec="seconds"),
|
||||
"window": {"mode": "历史导入", "source": str(source)},
|
||||
"students": [],
|
||||
"result": {
|
||||
"received": len(entries),
|
||||
"saved": imported,
|
||||
"auto_registered": 0,
|
||||
"review_pending": created_tasks,
|
||||
"duplicates": duplicates,
|
||||
"rejected": skipped,
|
||||
},
|
||||
},
|
||||
)
|
||||
repository._append_operation_log(
|
||||
conn,
|
||||
"历史课程小结导入",
|
||||
"完成",
|
||||
source=str(source),
|
||||
scanned_files=scanned,
|
||||
indexed_summaries=imported,
|
||||
skipped_summaries=skipped,
|
||||
review_tasks=created_tasks,
|
||||
backup_id=backup_id,
|
||||
)
|
||||
return {"imported": imported, "duplicates": duplicates, "review_tasks": created_tasks}
|
||||
|
||||
result = repository._write_transaction("history-course-summary-import", [str(source), batch_id], work)
|
||||
imported = int(result.get("imported") or 0)
|
||||
duplicates = int(result.get("duplicates") or 0)
|
||||
review_tasks = int(result.get("review_tasks") or 0)
|
||||
else:
|
||||
imported = len(entries)
|
||||
duplicates = 0
|
||||
review_tasks = len(missing_tasks)
|
||||
print(
|
||||
f"历史小结导入完成:扫描 Markdown {scanned} 个,复制 {copied} 个,"
|
||||
f"索引小结 {imported} 条,跳过 {skipped} 条,生成审核任务 {review_tasks} 条。"
|
||||
f"历史小结导入完成:扫描 Markdown {scanned} 个,"
|
||||
f"写入/待写入小结 {imported} 条,重复 {duplicates} 条,"
|
||||
f"跳过 {skipped} 条,生成审核任务 {review_tasks} 条。"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user