完善课程小结审核操作

This commit is contained in:
Codex
2026-06-16 20:03:20 +08:00
parent 5f6bf376b2
commit 25f32cc44b
5 changed files with 233 additions and 0 deletions
+76
View File
@@ -1585,6 +1585,8 @@ OPERATION_LABELS = {
"admin_course_summary_delete": "课程小结删除",
"admin_course_summary_duplicate_scan": "重复小结扫描",
"admin_course_summary_duplicate_delete": "重复小结删除",
"admin_course_summary_review_update": "课程小结审核修正",
"admin_course_summary_link_existing": "课程小结关联已有记录",
"register-class-records": "登记上课记录",
"register-payments": "登记缴费记录",
"admin-create-account": "新增课时账户",
@@ -2553,6 +2555,80 @@ def create_incomplete_course_summary_review_task(
return create_course_summary_review_task(tasks_path, summary, "", reasons)
def editable_course_summary_payload(summary: dict, updates: dict) -> dict:
merged = dict(summary)
for key in ("student", "date_iso", "time_range", "teacher", "subject"):
if key in updates:
merged[key] = str(updates.get(key) or "").strip()
normalized = normalize_course_summary(merged)
normalized["source_id"] = str(summary.get("source_id") or normalized.get("source_id") or "")
normalized["group"] = str(summary.get("group") or "")
normalized["sender"] = str(summary.get("sender") or "")
normalized["sender_id"] = str(summary.get("sender_id") or "")
normalized["message_time"] = str(summary.get("message_time") or "")
normalized["message_date"] = str(summary.get("message_date") or normalized.get("message_date") or "")
normalized["db"] = str(summary.get("db") or "")
normalized["local_id"] = str(summary.get("local_id") or "")
normalized["title"] = str(summary.get("title") or "")
normalized["body"] = str(summary.get("body") or normalized.get("body") or "")
normalized["recognition_source"] = str(summary.get("recognition_source") or "")
normalized["confidence"] = str(summary.get("confidence") or "")
normalized["teacher_trusted"] = True
normalized["remark"] = str(summary.get("remark") or "")
return normalized
def update_course_summary_review_task(
tasks_path: Path,
classnotes_path: Path,
accounts_path: Path,
task_id: int,
updates: dict,
) -> dict:
tasks = read_admin_tasks(tasks_path)
task = find_admin_task(tasks, task_id)
if task.get("type") != "course_summary_review":
raise ValueError("该任务不是课程小结审核")
if task.get("status") not in {"pending", "conflict"}:
raise ValueError("该任务已处理,不能修改")
summary = task.get("summary") or {}
updated_summary = editable_course_summary_payload(summary, updates)
reasons, proposed_line = auto_register_reasons(updated_summary, classnotes_path, accounts_path)
now = datetime.now().isoformat(timespec="seconds")
task["status"] = "pending"
task["updated_at"] = now
task.pop("message", None)
task["student"] = updated_summary["student"]
task["summary"] = updated_summary
task["proposed_line"] = proposed_line
task["reasons"] = reasons
write_admin_tasks(tasks_path, tasks)
return {"task": task_to_dict(task)}
def link_existing_course_summary_task(tasks_path: Path, classnotes_path: Path, task_id: int) -> dict:
tasks = read_admin_tasks(tasks_path)
task = find_admin_task(tasks, task_id)
if task.get("type") != "course_summary_review":
raise ValueError("该任务不是课程小结审核")
if task.get("status") not in {"pending", "conflict"}:
raise ValueError("该任务已处理,不能重复操作")
proposed_line = str(task.get("proposed_line") or "").strip()
if not proposed_line:
raise ValueError("课程小结缺少候选登记行,不能关联已有记录")
existing_lines = {raw.strip() for raw in classnotes_path.read_text(encoding="utf-8").splitlines()}
if proposed_line not in existing_lines:
raise ValueError("候选登记行在上课记录中不存在,不能关联已有记录")
now = datetime.now().isoformat(timespec="seconds")
task["status"] = "approved"
task["updated_at"] = now
task["reviewed_at"] = now
task["linked_record_line"] = proposed_line
task["message"] = "已关联已有上课记录,未重复扣课时"
write_admin_tasks(tasks_path, tasks)
return {"task": task_to_dict(task), "linked_record_line": proposed_line}
def approve_course_summary_task(
tasks_path: Path,
classnotes_path: Path,
+50
View File
@@ -18,6 +18,7 @@ from ..data import (
approve_admin_task,
create_course_summary_duplicate_review_tasks,
delete_course_summary,
link_existing_course_summary_task,
list_admin_tasks,
list_operation_logs,
migrate_operation_log_labels,
@@ -25,6 +26,7 @@ from ..data import (
reject_admin_task,
resolve_duplicate_course_summary_task,
rollback_operation_log,
update_course_summary_review_task,
update_course_summary_time,
)
@@ -168,6 +170,54 @@ def admin_resolve_duplicate_summary(task_id: int, payload: dict, _user: str = De
return {"ok": True, **result}
@router.post("/api/admin/tasks/{task_id}/course-summary-review")
def admin_update_course_summary_review(task_id: int, payload: dict, _user: str = Depends(verify_admin_auth)):
try:
with write_lock:
result = update_course_summary_review_task(
ADMIN_TASKS_PATH,
CLASSNOTES_PATH,
ACCOUNTS_PATH,
task_id,
payload,
)
task = result.get("task", {})
append_operation_log(
OPERATION_LOGS_PATH,
"课程小结审核修正",
"已更新",
task_id=task_id,
task_type=str(task.get("type") or ""),
student=str(task.get("student") or ""),
source_id=str(task.get("source_id") or ""),
proposed_line=str(task.get("proposed_line") or ""),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, **result}
@router.post("/api/admin/tasks/{task_id}/link-existing-course-summary")
def admin_link_existing_course_summary(task_id: int, _user: str = Depends(verify_admin_auth)):
try:
with write_lock:
result = link_existing_course_summary_task(ADMIN_TASKS_PATH, CLASSNOTES_PATH, task_id)
task = result.get("task", {})
append_operation_log(
OPERATION_LOGS_PATH,
"课程小结关联已有记录",
"已批准",
task_id=task_id,
task_type=str(task.get("type") or ""),
student=str(task.get("student") or ""),
source_id=str(task.get("source_id") or ""),
proposed_line=str(result.get("linked_record_line") or ""),
)
except ValueError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return {"ok": True, **result}
@router.post("/api/admin/tasks/{task_id}/reject")
def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
try:
+29
View File
@@ -267,6 +267,31 @@
<div class="drawer-label">课程信息</div>
<div id="summaryReviewDrawerCourse" class="drawer-text"></div>
</div>
<div id="summaryReviewEditSection" class="drawer-section" hidden>
<div class="drawer-label">修正信息</div>
<div class="form-grid compact-grid">
<label>
学生
<input id="summaryReviewEditStudent" autocomplete="off" />
</label>
<label>
日期
<input id="summaryReviewEditDate" type="date" />
</label>
<label>
时间
<input id="summaryReviewEditTime" autocomplete="off" placeholder="08:00-10:00" />
</label>
<label>
老师
<input id="summaryReviewEditTeacher" autocomplete="off" />
</label>
<label>
科目
<input id="summaryReviewEditSubject" autocomplete="off" />
</label>
</div>
</div>
<div class="drawer-section">
<div class="drawer-label">建议登记行</div>
<div id="summaryReviewDrawerLine"></div>
@@ -285,6 +310,8 @@
</div>
</div>
<div class="drawer-actions">
<button id="summaryReviewDrawerSave" class="secondary-button" type="button">保存修正</button>
<button id="summaryReviewDrawerLink" class="secondary-button" type="button">关联已有记录</button>
<button id="summaryReviewDrawerReject" class="secondary-button" type="button">驳回</button>
<button id="summaryReviewDrawerApprove" type="button">批准</button>
</div>
@@ -341,6 +368,8 @@
<option value="修改老师档案">修改老师档案</option>
<option value="课程小结接收">课程小结接收</option>
<option value="课程小结登记">课程小结登记</option>
<option value="课程小结审核修正">课程小结审核修正</option>
<option value="课程小结关联已有记录">课程小结关联已有记录</option>
<option value="重复小结扫描">重复小结扫描</option>
<option value="重复小结删除">重复小结删除</option>
<option value="审核批准">审核批准</option>
+74
View File
@@ -56,10 +56,18 @@ const summaryReviewDrawerTitle = document.querySelector("#summaryReviewDrawerTit
const summaryReviewDrawerMeta = document.querySelector("#summaryReviewDrawerMeta");
const summaryReviewDrawerStatus = document.querySelector("#summaryReviewDrawerStatus");
const summaryReviewDrawerCourse = document.querySelector("#summaryReviewDrawerCourse");
const summaryReviewEditSection = document.querySelector("#summaryReviewEditSection");
const summaryReviewEditStudent = document.querySelector("#summaryReviewEditStudent");
const summaryReviewEditDate = document.querySelector("#summaryReviewEditDate");
const summaryReviewEditTime = document.querySelector("#summaryReviewEditTime");
const summaryReviewEditTeacher = document.querySelector("#summaryReviewEditTeacher");
const summaryReviewEditSubject = document.querySelector("#summaryReviewEditSubject");
const summaryReviewDrawerLine = document.querySelector("#summaryReviewDrawerLine");
const summaryReviewDrawerReasons = document.querySelector("#summaryReviewDrawerReasons");
const summaryReviewDrawerBody = document.querySelector("#summaryReviewDrawerBody");
const summaryReviewDrawerSource = document.querySelector("#summaryReviewDrawerSource");
const summaryReviewDrawerSave = document.querySelector("#summaryReviewDrawerSave");
const summaryReviewDrawerLink = document.querySelector("#summaryReviewDrawerLink");
const summaryReviewDrawerApprove = document.querySelector("#summaryReviewDrawerApprove");
const summaryReviewDrawerReject = document.querySelector("#summaryReviewDrawerReject");
const summarySearchForm = document.querySelector("#summarySearchForm");
@@ -574,6 +582,11 @@ function canReviewSummary(item) {
return item && (item.status === "pending" || item.status === "conflict");
}
function canLinkExistingSummary(item) {
const reasons = Array.isArray(item && item.reasons) ? item.reasons : [];
return Boolean(item && item.proposed_line && reasons.includes("classnotes 已存在同一条上课记录"));
}
function courseInfoLines(summary) {
return [
summary.student ? `学生:${summary.student}` : "",
@@ -584,6 +597,24 @@ function courseInfoLines(summary) {
].filter(Boolean);
}
function fillSummaryReviewEditor(summary) {
summaryReviewEditStudent.value = summary.student || "";
summaryReviewEditDate.value = summary.date_iso || "";
summaryReviewEditTime.value = summary.time_range || "";
summaryReviewEditTeacher.value = summary.teacher || "";
summaryReviewEditSubject.value = summary.subject || "";
}
function summaryReviewEditorPayload() {
return {
student: summaryReviewEditStudent.value.trim(),
date_iso: summaryReviewEditDate.value,
time_range: summaryReviewEditTime.value.trim(),
teacher: summaryReviewEditTeacher.value.trim(),
subject: summaryReviewEditSubject.value.trim(),
};
}
function openSummaryReviewDrawer(taskId) {
const item = findSummaryReview(taskId);
if (!item) return;
@@ -601,6 +632,8 @@ function openSummaryReviewDrawer(taskId) {
].filter(Boolean).join(" ");
summaryReviewDrawerStatus.innerHTML = `<span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span>${item.message ? ` <small>${escapeHtml(item.message)}</small>` : ""}`;
summaryReviewDrawerCourse.innerHTML = courseInfoLines(summary).map(escapeHtml).join("<br>") || "暂无课程信息";
summaryReviewEditSection.hidden = duplicateReview;
if (!duplicateReview) fillSummaryReviewEditor(summary);
summaryReviewDrawerLine.innerHTML = duplicateReview
? "<span class=\"muted\">请选择下方重复小结删除</span>"
: item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>";
@@ -615,6 +648,10 @@ function openSummaryReviewDrawer(taskId) {
item.saved_path ? `保存文件:${escapeHtml(item.saved_path)}` : "",
item.batch_id ? `批次:${escapeHtml(item.batch_id)}` : "",
].filter(Boolean).join("<br>") || "暂无来源信息";
summaryReviewDrawerSave.hidden = duplicateReview;
summaryReviewDrawerSave.disabled = !canReview || duplicateReview;
summaryReviewDrawerLink.hidden = duplicateReview || !canLinkExistingSummary(item);
summaryReviewDrawerLink.disabled = !canReview || duplicateReview || !canLinkExistingSummary(item);
summaryReviewDrawerApprove.hidden = duplicateReview;
summaryReviewDrawerApprove.disabled = !canReview || duplicateReview;
summaryReviewDrawerReject.disabled = !canReview;
@@ -623,6 +660,9 @@ function openSummaryReviewDrawer(taskId) {
function closeSummaryReviewDrawer() {
activeSummaryReview = null;
summaryReviewEditSection.hidden = true;
summaryReviewDrawerSave.hidden = false;
summaryReviewDrawerLink.hidden = true;
summaryReviewDrawerApprove.hidden = false;
summaryReviewDrawerBackdrop.hidden = true;
}
@@ -754,6 +794,38 @@ async function deleteCourseSummary(summaryId) {
await loadSummarySearch();
}
async function saveSummaryReviewEdits() {
if (!activeSummaryReview) return;
try {
const result = await fetchJson(`/api/admin/tasks/${encodeURIComponent(activeSummaryReview.id)}/course-summary-review`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(summaryReviewEditorPayload()),
});
activeSummaryReview = result.task;
await loadSummaryReviews();
openSummaryReviewDrawer(activeSummaryReview.id);
await loadOperationLogs();
} catch (error) {
alert(error.message);
}
}
async function linkExistingSummaryReview() {
if (!activeSummaryReview) return;
if (!confirm("确认关联已有上课记录?系统不会重复扣课时。")) return;
try {
await fetchJson(`/api/admin/tasks/${encodeURIComponent(activeSummaryReview.id)}/link-existing-course-summary`, {
method: "POST",
});
closeSummaryReviewDrawer();
await loadSummaryReviews();
await loadOperationLogs();
} catch (error) {
alert(error.message);
}
}
async function resolveDuplicateSummary(taskId, summaryId) {
if (!confirm("确认删除这条重复课程小结?系统会先备份来源文件。")) return;
await fetchJson(`/api/admin/tasks/${encodeURIComponent(taskId)}/resolve-duplicate-summary`, {
@@ -1067,6 +1139,8 @@ summaryReviewDrawerClose.addEventListener("click", closeSummaryReviewDrawer);
summaryReviewDrawerBackdrop.addEventListener("click", (event) => {
if (event.target === summaryReviewDrawerBackdrop) closeSummaryReviewDrawer();
});
summaryReviewDrawerSave.addEventListener("click", saveSummaryReviewEdits);
summaryReviewDrawerLink.addEventListener("click", linkExistingSummaryReview);
summaryReviewDrawerApprove.addEventListener("click", () => {
if (activeSummaryReview) summaryReviewTask(activeSummaryReview.id, "approve");
});
+4
View File
@@ -369,6 +369,10 @@ select:focus {
font-weight: 700;
}
.compact-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.span-2 {
grid-column: span 2;
}