修复登记预览与审批逻辑
This commit is contained in:
@@ -123,10 +123,16 @@ def normalize_register_type(value: str) -> str:
|
||||
return register_type
|
||||
|
||||
|
||||
def collect_input_lines(text: str | None = None, lines: list[str] | None = None) -> list[str]:
|
||||
def collect_input_lines(
|
||||
text: str | None = None,
|
||||
lines: list[str] | None = None,
|
||||
register_type: str = "",
|
||||
) -> list[str]:
|
||||
if lines is not None:
|
||||
return normalize_lines(lines=lines)
|
||||
raw = str(text or "")
|
||||
if register_type in {"class_record", "payment"}:
|
||||
return normalize_lines(lines=raw.splitlines())
|
||||
if "\n\n" in raw:
|
||||
chunks = [item.strip() for item in re.split(r"\n\s*\n", raw) if item.strip()]
|
||||
if chunks:
|
||||
@@ -547,7 +553,7 @@ def preview_register(
|
||||
conversation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_type = normalize_register_type(register_type)
|
||||
source_lines = collect_input_lines(text=text, lines=lines)
|
||||
source_lines = collect_input_lines(text=text, lines=lines, register_type=normalized_type)
|
||||
joined_text = "\n\n".join(source_lines)
|
||||
conversation_id, session = _session_for(conversation_id, normalized_type, joined_text)
|
||||
merged_answers = {**dict(session.get("answers") or {}), **(answers or {})}
|
||||
|
||||
+93
-18
@@ -1149,7 +1149,7 @@ def reject_admin_task(tasks_path: Path, task_id: int) -> dict:
|
||||
return mark_admin_task(tasks_path, task_id, "rejected")
|
||||
|
||||
|
||||
def approve_correction_task(tasks_path: Path, classnotes_path: Path, task_id: int) -> dict:
|
||||
def approve_correction_task(tasks_path: Path, classnotes_path: Path, accounts_path: Path, task_id: int) -> dict:
|
||||
tasks = read_admin_tasks(tasks_path)
|
||||
task = find_admin_task(tasks, task_id)
|
||||
if task.get("type") != "class_record_correction":
|
||||
@@ -1159,13 +1159,25 @@ def approve_correction_task(tasks_path: Path, classnotes_path: Path, task_id: in
|
||||
|
||||
original_line = str(task.get("original_line", "")).strip()
|
||||
corrected_line = str(task.get("corrected_line", "")).strip()
|
||||
parse_class_record_line(original_line)
|
||||
original = parse_class_record_line(original_line)
|
||||
corrected = parse_class_record_line(corrected_line)
|
||||
corrected_line = class_record_to_line(corrected)
|
||||
|
||||
original_classnotes = classnotes_path.read_text(encoding="utf-8")
|
||||
try:
|
||||
new_classnotes = replace_class_record_line(original_classnotes, original_line, corrected_line)
|
||||
accounts = read_accounts(accounts_path)
|
||||
updated_accounts = list(accounts)
|
||||
original_account_index = find_account_index(updated_accounts, original.student)
|
||||
updated_accounts[original_account_index] = update_account_remaining(
|
||||
updated_accounts[original_account_index],
|
||||
original.duration_hours,
|
||||
)
|
||||
corrected_account_index = find_account_index(updated_accounts, corrected.student)
|
||||
updated_accounts[corrected_account_index] = update_account_remaining(
|
||||
updated_accounts[corrected_account_index],
|
||||
-corrected.duration_hours,
|
||||
)
|
||||
except ValueError as exc:
|
||||
task["status"] = "conflict"
|
||||
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||
@@ -1173,27 +1185,42 @@ def approve_correction_task(tasks_path: Path, classnotes_path: Path, task_id: in
|
||||
write_admin_tasks(tasks_path, tasks)
|
||||
raise
|
||||
|
||||
original_accounts = accounts_path.read_text(encoding="utf-8")
|
||||
original_tasks = json.dumps(tasks, ensure_ascii=False, indent=2) + "\n"
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
task["status"] = "approved"
|
||||
task["updated_at"] = now
|
||||
task["reviewed_at"] = now
|
||||
task["original"] = record_to_dict(original)
|
||||
task["corrected_line"] = corrected_line
|
||||
task["corrected"] = record_to_dict(corrected)
|
||||
changed_account_ids = {
|
||||
updated_accounts[original_account_index].student_id,
|
||||
updated_accounts[corrected_account_index].student_id,
|
||||
}
|
||||
updated_accounts_by_id = {
|
||||
account.student_id: account
|
||||
for account in updated_accounts
|
||||
if account.student_id in changed_account_ids
|
||||
}
|
||||
new_accounts = replace_account_lines(original_accounts, updated_accounts_by_id)
|
||||
new_tasks = json.dumps(tasks, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
backup_dir = create_data_backup(
|
||||
"admin-approve-correction",
|
||||
{
|
||||
accounts_path: original_accounts,
|
||||
classnotes_path: original_classnotes,
|
||||
tasks_path: original_tasks,
|
||||
},
|
||||
[original_line, corrected_line],
|
||||
)
|
||||
try:
|
||||
atomic_write_text(accounts_path, new_accounts)
|
||||
atomic_write_text(classnotes_path, new_classnotes)
|
||||
atomic_write_text(tasks_path, new_tasks)
|
||||
except Exception:
|
||||
atomic_write_text(accounts_path, original_accounts)
|
||||
atomic_write_text(classnotes_path, original_classnotes)
|
||||
atomic_write_text(tasks_path, original_tasks)
|
||||
raise
|
||||
@@ -2483,15 +2510,23 @@ def course_summary_heading(summary: dict, existing_headings: set[str]) -> str:
|
||||
def save_course_summary_markdown(root: Path, summary: dict) -> dict:
|
||||
path = course_summary_path(root, summary)
|
||||
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
existing_headings = set(re.findall(r"^###\s+(.+)$", existing, flags=re.M))
|
||||
existing_compact = re.sub(r"\s+", "", existing)
|
||||
matches = list(COURSE_SUMMARY_HEADING_RE.finditer(existing))
|
||||
existing_headings = {match.group("title").strip() for match in matches}
|
||||
body = str(summary.get("body") or "").rstrip()
|
||||
body_compact = re.sub(r"\s+", "", body)
|
||||
if body_compact and body_compact in existing_compact:
|
||||
return {"path": str(path), "added": False}
|
||||
|
||||
group_name = str(summary.get("group") or summary["student"])
|
||||
heading = course_summary_heading(summary, existing_headings)
|
||||
source_id = str(summary.get("source_id") or "").strip()
|
||||
if source_id and f"来源ID:`{source_id}`" in existing:
|
||||
return {"path": str(path), "added": False}
|
||||
existing_blocks: set[tuple[str, str]] = set()
|
||||
for index, match in enumerate(matches):
|
||||
start = match.end()
|
||||
end = matches[index + 1].start() if index + 1 < len(matches) else len(existing)
|
||||
block_body = existing[start:end].strip()
|
||||
block_body = re.sub(r"^(?:>\s+.*\n)+\s*", "", block_body).strip()
|
||||
existing_blocks.add((match.group("title").strip(), re.sub(r"\s+", "", block_body)))
|
||||
if (heading, re.sub(r"\s+", "", body)) in existing_blocks:
|
||||
return {"path": str(path), "added": False}
|
||||
lines: list[str] = []
|
||||
if not existing.strip():
|
||||
lines.extend([f"# {path.stem}", "", f"## {group_name}", ""])
|
||||
@@ -2733,7 +2768,50 @@ def approve_course_summary_task(
|
||||
raise ValueError("课程小结信息未补齐,不能直接批准入账")
|
||||
|
||||
try:
|
||||
result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line)
|
||||
original_classnotes = classnotes_path.read_text(encoding="utf-8")
|
||||
original_accounts = accounts_path.read_text(encoding="utf-8")
|
||||
original_tasks = json.dumps(tasks, ensure_ascii=False, indent=2) + "\n"
|
||||
record = parse_class_record_line(proposed_line)
|
||||
record_line = class_record_to_line(record)
|
||||
existing_lines = {raw.strip() for raw in original_classnotes.splitlines()}
|
||||
if record_line in existing_lines:
|
||||
raise ValueError(f"上课记录已存在: {record_line}")
|
||||
accounts = read_accounts(accounts_path)
|
||||
updated_accounts = list(accounts)
|
||||
account_index = find_account_index(updated_accounts, record.student)
|
||||
updated_accounts[account_index] = update_account_remaining(
|
||||
updated_accounts[account_index],
|
||||
-record.duration_hours,
|
||||
)
|
||||
separator = "" if not original_classnotes or original_classnotes.endswith("\n") else "\n"
|
||||
new_classnotes = f"{original_classnotes}{separator}{record_line}\n"
|
||||
updated_accounts_by_id = {updated_accounts[account_index].student_id: updated_accounts[account_index]}
|
||||
new_accounts = replace_account_lines(original_accounts, updated_accounts_by_id)
|
||||
task["status"] = "approved"
|
||||
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||
task["reviewed_at"] = task["updated_at"]
|
||||
task["proposed_line"] = record_line
|
||||
task["registered_line"] = record_line
|
||||
backup_dir = create_data_backup(
|
||||
"admin-approve-course-summary",
|
||||
{
|
||||
accounts_path: original_accounts,
|
||||
classnotes_path: original_classnotes,
|
||||
tasks_path: original_tasks,
|
||||
},
|
||||
[record_line],
|
||||
)
|
||||
task["backup_id"] = backup_dir.name
|
||||
new_tasks = json.dumps(tasks, ensure_ascii=False, indent=2) + "\n"
|
||||
try:
|
||||
atomic_write_text(accounts_path, new_accounts)
|
||||
atomic_write_text(classnotes_path, new_classnotes)
|
||||
atomic_write_text(tasks_path, new_tasks)
|
||||
except Exception:
|
||||
atomic_write_text(accounts_path, original_accounts)
|
||||
atomic_write_text(classnotes_path, original_classnotes)
|
||||
atomic_write_text(tasks_path, original_tasks)
|
||||
raise
|
||||
except ValueError as exc:
|
||||
task["status"] = "conflict"
|
||||
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||
@@ -2741,14 +2819,11 @@ def approve_course_summary_task(
|
||||
write_admin_tasks(tasks_path, tasks)
|
||||
raise
|
||||
|
||||
task["status"] = "approved"
|
||||
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||
task["reviewed_at"] = task["updated_at"]
|
||||
task["proposed_line"] = proposed_line
|
||||
task["registered_line"] = proposed_line
|
||||
task["backup_id"] = result.get("backup_id", "")
|
||||
write_admin_tasks(tasks_path, tasks)
|
||||
return {"task": task_to_dict(task), "backup_id": result.get("backup_id", "")}
|
||||
try:
|
||||
prune_data_backups(backup_dir.parent)
|
||||
except OSError:
|
||||
pass
|
||||
return {"task": task_to_dict(task), "backup_id": backup_dir.name}
|
||||
|
||||
|
||||
def approve_admin_task(
|
||||
@@ -2759,7 +2834,7 @@ def approve_admin_task(
|
||||
) -> dict:
|
||||
task = find_admin_task(read_admin_tasks(tasks_path), task_id)
|
||||
if task.get("type") == "class_record_correction":
|
||||
return approve_correction_task(tasks_path, classnotes_path, task_id)
|
||||
return approve_correction_task(tasks_path, classnotes_path, accounts_path, task_id)
|
||||
if task.get("type") == "class_record_deletion":
|
||||
return approve_deletion_task(tasks_path, classnotes_path, accounts_path, task_id)
|
||||
if task.get("type") == "course_summary_review":
|
||||
|
||||
+17
-3
@@ -971,6 +971,20 @@ function clearRegisterPreview(type, resetState = true) {
|
||||
if (confirmButton) confirmButton.hidden = true;
|
||||
}
|
||||
|
||||
function renderStandardLinesPreview(type, lines) {
|
||||
const standardLines = Array.isArray(lines) ? lines : [];
|
||||
if (type === "class_record") {
|
||||
const rows = standardLines
|
||||
.map(
|
||||
(line, index) =>
|
||||
`<li><span>${index + 1}</span><code>${escapeHtml(line)}</code></li>`
|
||||
)
|
||||
.join("");
|
||||
return `<div class="register-preview-head"><strong>标准登记内容</strong><span>共 ${standardLines.length} 条</span></div><ol class="register-line-list">${rows}</ol>`;
|
||||
}
|
||||
return `<strong>标准登记内容</strong><pre>${escapeHtml(standardLines.join("\n\n"))}</pre>`;
|
||||
}
|
||||
|
||||
function renderRegisterPreview(type, data, statusNode) {
|
||||
const previewNode = {
|
||||
class_record: classRegisterPreview,
|
||||
@@ -985,11 +999,12 @@ function renderRegisterPreview(type, data, statusNode) {
|
||||
previewNode.hidden = false;
|
||||
if (data.status === "ready") {
|
||||
const warning = data.warning ? `<p>${escapeHtml(data.warning)}</p>` : "";
|
||||
previewNode.innerHTML = `${warning}<strong>标准登记内容</strong><pre>${escapeHtml((data.standard_lines || []).join("\n\n"))}</pre>`;
|
||||
const standardLines = data.standard_lines || [];
|
||||
previewNode.innerHTML = `${warning}${renderStandardLinesPreview(type, standardLines)}`;
|
||||
confirmButton.hidden = false;
|
||||
registerPreviewState[type] = {
|
||||
conversationId: data.conversation_id,
|
||||
standardLines: data.standard_lines || [],
|
||||
standardLines,
|
||||
};
|
||||
statusNode.textContent = "已通过脚本生成预览,请确认后写入";
|
||||
return;
|
||||
@@ -1289,5 +1304,4 @@ refreshBtn.addEventListener("click", () => {
|
||||
});
|
||||
|
||||
loadAdminHealth();
|
||||
addSummaryRegisterItem();
|
||||
loadAccounts();
|
||||
|
||||
@@ -427,6 +427,62 @@ textarea:focus {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.register-preview-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.register-preview-head span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.register-line-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-height: 220px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.register-line-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.register-line-list span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
border-radius: 999px;
|
||||
background: #e9f5f3;
|
||||
color: var(--accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.register-line-list code {
|
||||
color: #344054;
|
||||
font-family: Arial, "Songti SC", SimSun, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.register-preview textarea {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user