diff --git a/app/app/data.py b/app/app/data.py index 593f92b..63f7a6f 100644 --- a/app/app/data.py +++ b/app/app/data.py @@ -524,7 +524,7 @@ def create_teacher(path: Path, teacher: Teacher) -> dict: prune_data_backups(backup_dir.parent) except OSError: pass - return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name} + return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name, "operation": "新增老师档案"} def update_teacher(path: Path, old_teacher_id: str, teacher: Teacher) -> dict: @@ -542,7 +542,7 @@ def update_teacher(path: Path, old_teacher_id: str, teacher: Teacher) -> dict: prune_data_backups(backup_dir.parent) except OSError: pass - return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name} + return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name, "operation": "修改老师档案"} def teacher_to_dict(teacher: Teacher) -> dict: @@ -590,7 +590,7 @@ def create_account(path: Path, account: Account) -> dict: prune_data_backups(backup_dir.parent) except OSError: pass - return {"account": account_to_dict(account), "backup_id": backup_dir.name} + return {"account": account_to_dict(account), "backup_id": backup_dir.name, "operation": "新增课时账户"} def update_account(path: Path, old_student_id: str, account: Account) -> dict: @@ -608,7 +608,7 @@ def update_account(path: Path, old_student_id: str, account: Account) -> dict: prune_data_backups(backup_dir.parent) except OSError: pass - return {"account": account_to_dict(account), "backup_id": backup_dir.name} + return {"account": account_to_dict(account), "backup_id": backup_dir.name, "operation": "修改课时账户"} def render_accounts_text(path: Path, accounts: list[Account]) -> str: @@ -691,7 +691,7 @@ def register_class_record_lines( prune_data_backups(backup_dir.parent) except OSError: pass - return {"registered": len(record_lines), "lines": record_lines, "backup_id": backup_dir.name} + return {"registered": len(record_lines), "lines": record_lines, "backup_id": backup_dir.name, "operation": "登记上课记录"} def register_payment_lines( @@ -726,7 +726,7 @@ def register_payment_lines( prune_data_backups(backup_dir.parent) except OSError: pass - return {"registered": len(registered), "lines": registered, "backup_id": backup_dir.name} + return {"registered": len(registered), "lines": registered, "backup_id": backup_dir.name, "operation": "登记缴费记录"} def default_admin_tasks() -> dict: @@ -1358,18 +1358,96 @@ def append_operation_log(path: Path, operation: str, status: str, **fields: obje path.parent.mkdir(parents=True, exist_ok=True) now = datetime.now().isoformat(timespec="seconds") log_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{sha1_text(json.dumps(fields, ensure_ascii=False, sort_keys=True), 8)}" - row = { + row = localize_operation_log_item({ "id": log_id, "created_at": now, "operation": operation, "status": status, **fields, - } + }) with path.open("a", encoding="utf-8") as handle: handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") return log_id +OPERATION_LABELS = { + "history_course_summary_import": "历史课程小结导入", + "course_summary_manual_register": "课程小结登记", + "course_summary_ingest": "课程小结接收", + "admin_task_approve": "审核批准", + "admin_task_reject": "审核驳回", + "admin_course_summary_update_time": "课程小结补齐时间", + "admin_course_summary_delete": "课程小结删除", + "register-class-records": "登记上课记录", + "register-payments": "登记缴费记录", + "admin-create-account": "新增课时账户", + "admin-update-account": "修改课时账户", + "admin-create-teacher": "新增老师档案", + "admin-update-teacher": "修改老师档案", + "admin-approve-correction": "审核批准上课记录纠错", + "admin-approve-deletion": "审核批准上课记录删除", + "admin-update-course-summary-time": "课程小结补齐时间", + "admin-delete-course-summary": "课程小结删除", +} + +STATUS_LABELS = { + "completed": "完成", + "approved": "已批准", + "rejected": "已驳回", + "updated": "已更新", + "deleted": "已删除", + "duplicate": "重复", + "auto_registered": "自动入账", + "review": "待审核", + "conflict": "冲突", + "pending": "待处理", +} + +TYPE_LABELS = { + "class_record_correction": "上课记录纠错", + "class_record_deletion": "上课记录删除", + "course_summary_review": "课程小结审核", +} + + +def localize_operation_log_item(item: dict) -> dict: + row = dict(item) + operation = str(row.get("operation") or "") + status = str(row.get("status") or "") + task_type = str(row.get("task_type") or "") + row["operation"] = OPERATION_LABELS.get(operation, operation) + row["status"] = STATUS_LABELS.get(status, status) + if task_type: + row["task_type"] = TYPE_LABELS.get(task_type, task_type) + if row.get("message"): + row["message"] = str(row["message"]) + return row + + +def migrate_operation_log_labels(path: Path) -> dict: + if not path.exists(): + return {"updated": 0, "path": str(path)} + lines = path.read_text(encoding="utf-8").splitlines() + updated_lines: list[str] = [] + changed = 0 + for raw_line in lines: + if not raw_line.strip(): + updated_lines.append(raw_line) + continue + try: + item = json.loads(raw_line) + except json.JSONDecodeError: + updated_lines.append(raw_line) + continue + localized = localize_operation_log_item(item) + if localized != item: + changed += 1 + updated_lines.append(json.dumps(localized, ensure_ascii=False, sort_keys=True)) + if changed: + atomic_write_text(path, "\n".join(updated_lines) + "\n") + return {"updated": changed, "path": str(path)} + + def list_operation_logs(path: Path, limit: int = 100, operation: str = "", status_filter: str = "", student: str = "") -> dict: rows: list[dict] = [] if path.exists(): @@ -1380,6 +1458,7 @@ def list_operation_logs(path: Path, limit: int = 100, operation: str = "", statu item = json.loads(raw_line) except json.JSONDecodeError: continue + item = localize_operation_log_item(item) if operation and item.get("operation") != operation: continue if status_filter and item.get("status") != status_filter: @@ -1948,8 +2027,8 @@ def register_course_summary_texts( result["duplicates"] += 1 log_id = append_operation_log( operation_logs_path, - "course_summary_manual_register", - "duplicate", + "课程小结登记", + "重复", source_id=source_id, student=normalized["student"], ) @@ -1987,8 +2066,8 @@ def register_course_summary_texts( write_course_summary_state(state_path, state) log_id = append_operation_log( operation_logs_path, - "course_summary_manual_register", - "auto_registered", + "课程小结登记", + "自动入账", source_id=source_id, student=normalized["student"], teacher=normalized.get("teacher", ""), @@ -2012,8 +2091,8 @@ def register_course_summary_texts( student = str((normalized or raw or {}).get("student") or "") log_id = append_operation_log( operation_logs_path, - "course_summary_manual_register", - "rejected", + "课程小结登记", + "已驳回", source_id=source_id, student=student, error=str(exc), @@ -2067,8 +2146,8 @@ def ingest_course_summaries( result["duplicates"] += 1 log_id = append_operation_log( operation_logs_path, - "course_summary_ingest", - "duplicate", + "课程小结接收", + "重复", batch_id=batch_id, source_id=source_id, student=normalized["student"], @@ -2090,12 +2169,12 @@ def ingest_course_summaries( saved_path=str(saved.get("path") or ""), ) result["review_pending"] += 1 - status_value = "review" + status_value = "待审核" task_id = task.get("id") else: register_result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line) result["auto_registered"] += 1 - status_value = "auto_registered" + status_value = "自动入账" task_id = None backup_id = str(register_result.get("backup_id") or "") @@ -2103,7 +2182,7 @@ def ingest_course_summaries( seen_semantic_keys.add(semantic_key) log_id = append_operation_log( operation_logs_path, - "course_summary_ingest", + "课程小结接收", status_value, batch_id=batch_id, source_id=source_id, @@ -2131,8 +2210,8 @@ def ingest_course_summaries( source_id = str((normalized or raw).get("source_id") or "") log_id = append_operation_log( operation_logs_path, - "course_summary_ingest", - "rejected", + "课程小结接收", + "已驳回", batch_id=batch_id, source_id=source_id, student=str((normalized or raw).get("student") or ""), diff --git a/app/app/routers/accounts.py b/app/app/routers/accounts.py index 4116e0f..8bc7ec3 100644 --- a/app/app/routers/accounts.py +++ b/app/app/routers/accounts.py @@ -20,9 +20,11 @@ from ..data import ( DuplicateRecordError, account_summary, account_to_dict, + append_operation_log, create_account, create_teacher, filter_accounts, + parse_class_record_line, register_class_record_lines, register_course_summary_texts, register_payment_lines, @@ -47,6 +49,16 @@ async def register_class_records(request: Request, _user: str = Depends(verify_a lines=payload.lines, line=payload.line, ) + for line in result.get("lines", []): + record = parse_class_record_line(str(line)) + append_operation_log( + OPERATION_LOGS_PATH, + str(result.get("operation") or "登记上课记录"), + "完成", + student=record.student, + proposed_line=str(line), + backup_id=str(result.get("backup_id") or ""), + ) except DuplicateRecordError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc except ValueError as exc: @@ -60,6 +72,16 @@ async def register_payments(request: Request, _user: str = Depends(verify_admin_ payload = await read_register_payload(request) with write_lock: result = register_payment_lines(ACCOUNTS_PATH, lines=payload.lines, line=payload.line) + for line in result.get("lines", []): + student = str(line).split("-", 1)[0] + append_operation_log( + OPERATION_LOGS_PATH, + str(result.get("operation") or "登记缴费记录"), + "完成", + student=student, + proposed_line=str(line), + backup_id=str(result.get("backup_id") or ""), + ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return {"ok": True, **result} @@ -144,6 +166,15 @@ def admin_create_teacher(payload: TeacherPayload, _user: str = Depends(verify_ad try: with write_lock: result = create_teacher(TEACHERS_PATH, payload_to_teacher(payload)) + log_operation = str(result.get("operation") or "新增老师档案") + append_operation_log( + OPERATION_LOGS_PATH, + log_operation, + "完成", + teacher_id=str(result.get("teacher", {}).get("teacher_id") or ""), + teacher=str(result.get("teacher", {}).get("name") or ""), + backup_id=str(result.get("backup_id") or ""), + ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return {"ok": True, **result} @@ -158,6 +189,15 @@ def admin_update_teacher(teacher_id: str, payload: TeacherPayload, _user: str = teacher_id, payload_to_teacher(payload, teacher_id=payload.teacher_id.strip() or teacher_id), ) + log_operation = str(result.get("operation") or "修改老师档案") + append_operation_log( + OPERATION_LOGS_PATH, + log_operation, + "完成", + teacher_id=str(result.get("teacher", {}).get("teacher_id") or ""), + teacher=str(result.get("teacher", {}).get("name") or ""), + backup_id=str(result.get("backup_id") or ""), + ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return {"ok": True, **result} @@ -168,6 +208,15 @@ def admin_create_account(payload: AccountPayload, _user: str = Depends(verify_ad try: with write_lock: result = create_account(ACCOUNTS_PATH, payload_to_account(payload)) + log_operation = str(result.get("operation") or "新增课时账户") + append_operation_log( + OPERATION_LOGS_PATH, + log_operation, + "完成", + student_id=str(result.get("account", {}).get("student_id") or ""), + student=str(result.get("account", {}).get("student") or ""), + backup_id=str(result.get("backup_id") or ""), + ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return {"ok": True, **result} @@ -178,6 +227,15 @@ def admin_update_account(student_id: str, payload: AccountPayload, _user: str = try: with write_lock: result = update_account(ACCOUNTS_PATH, student_id, payload_to_account(payload)) + log_operation = str(result.get("operation") or "修改课时账户") + append_operation_log( + OPERATION_LOGS_PATH, + log_operation, + "完成", + student_id=str(result.get("account", {}).get("student_id") or ""), + student=str(result.get("account", {}).get("student") or ""), + backup_id=str(result.get("backup_id") or ""), + ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return {"ok": True, **result} diff --git a/app/app/routers/admin.py b/app/app/routers/admin.py index d372407..5bcf0c0 100644 --- a/app/app/routers/admin.py +++ b/app/app/routers/admin.py @@ -17,6 +17,7 @@ from ..data import ( delete_course_summary, list_admin_tasks, list_operation_logs, + migrate_operation_log_labels, query_course_summaries, reject_admin_task, update_course_summary_time, @@ -46,6 +47,7 @@ def admin_operation_logs( student: str = Query(""), _user: str = Depends(verify_admin_auth), ): + migrate_operation_log_labels(OPERATION_LOGS_PATH) return list_operation_logs( OPERATION_LOGS_PATH, limit=limit, @@ -91,8 +93,8 @@ def admin_approve_task(task_id: int, _user: str = Depends(verify_admin_auth)): task = result.get("task", {}) append_operation_log( OPERATION_LOGS_PATH, - "admin_task_approve", - "approved", + "审核批准", + "已批准", task_id=task_id, task_type=str(task.get("type") or ""), student=str(task.get("student") or task.get("corrected", {}).get("student") or ""), @@ -111,8 +113,8 @@ def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)): task = reject_admin_task(ADMIN_TASKS_PATH, task_id) append_operation_log( OPERATION_LOGS_PATH, - "admin_task_reject", - "rejected", + "审核驳回", + "已驳回", task_id=task_id, task_type=str(task.get("type") or ""), student=str(task.get("student") or task.get("corrected", {}).get("student") or ""), @@ -130,8 +132,8 @@ def admin_update_course_summary_time(summary_id: str, payload: dict, _user: str result = update_course_summary_time(COURSE_SUMMARIES_ROOT, summary_id, str(payload.get("time_range") or "")) append_operation_log( OPERATION_LOGS_PATH, - "admin_course_summary_update_time", - "updated", + "课程小结补齐时间", + "已更新", summary_id=summary_id, time_range=str(payload.get("time_range") or ""), backup_id=str(result.get("backup_id") or ""), @@ -148,8 +150,8 @@ def admin_delete_course_summary(summary_id: str, _user: str = Depends(verify_adm result = delete_course_summary(COURSE_SUMMARIES_ROOT, summary_id) append_operation_log( OPERATION_LOGS_PATH, - "admin_course_summary_delete", - "deleted", + "课程小结删除", + "已删除", summary_id=summary_id, backup_id=str(result.get("backup_id") or ""), ) diff --git a/app/app/static/admin.html b/app/app/static/admin.html index 087fb28..b983325 100644 --- a/app/app/static/admin.html +++ b/app/app/static/admin.html @@ -330,17 +330,29 @@