HEL-144 返工A:四件事(改名/登录端标识/结账设置真保存/提醒回改)+ 迁移升为 7 + 移除废弃 B-44 前端样式测试

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
leefer
2026-08-25 18:41:01 +08:00
co-authored by multica-agent
parent 5816e8aa71
commit ece3e53472
17 changed files with 1142 additions and 1050 deletions
+216 -3
View File
@@ -12,7 +12,7 @@ from urllib.parse import parse_qs, urlparse
from bank_importer import (
auth, importing, ledger_events, manual_records, master_data, matching,
multipart, personal_transit, positions, subjects,
multipart, personal_transit, positions, settings, subjects,
)
from bank_importer.db import connect, migrate, utc_now
@@ -80,6 +80,15 @@ class AppHandler(SimpleHTTPRequestHandler):
if path == "/api/admin/audit-log":
self._handle_admin_audit_log(query)
return
if path == "/api/admin/settings":
self._handle_admin_settings()
return
if path == "/api/admin/reminders":
self._handle_admin_reminders(query)
return
if path == "/api/admin/reminders/pending":
self._handle_admin_reminder_pending(query)
return
if path == "/api/admin/transfer-events":
self._handle_admin_transfer_events(query)
return
@@ -232,6 +241,12 @@ class AppHandler(SimpleHTTPRequestHandler):
if mapping_review:
self._handle_admin_review_personal_mapping(int(mapping_review.group(1)))
return
if path == "/api/admin/settings":
self._handle_admin_update_settings()
return
if path == "/api/admin/reminders/send":
self._handle_admin_send_reminders()
return
# B-44 intercompany positions (admin writes)
subject_decision = re.fullmatch(
@@ -297,7 +312,7 @@ class AppHandler(SimpleHTTPRequestHandler):
if user is None:
return None
if user["role"] != "admin":
self._send_json(403, {"status": "error", "message": "该操作仅限总账管理员。"})
self._send_json(403, {"status": "error", "message": "该操作仅限管理员。"})
return None
return user
@@ -354,7 +369,7 @@ class AppHandler(SimpleHTTPRequestHandler):
return
if reason == "disabled":
self._send_json(
403, {"status": "error", "message": "账号已停用,请联系总账管理员。"}
403, {"status": "error", "message": "账号已停用,请联系管理员。"}
)
return
if user is None:
@@ -1404,6 +1419,204 @@ class AppHandler(SimpleHTTPRequestHandler):
finally:
connection.close()
# ------------------------------------------------------------------
# System settings (admin read/write, persisted + audited)
# ------------------------------------------------------------------
def _handle_admin_settings(self) -> None:
connection = connect(DB_PATH)
try:
user = self._require_admin(connection)
if user is None:
return
values = settings.get_settings(connection)
self._send_json(200, {"status": "ok", "settings": values})
finally:
connection.close()
def _handle_admin_update_settings(self) -> None:
connection = connect(DB_PATH)
try:
user = self._require_admin(connection)
if user is None:
return
data = self._read_json_body()
if data is None:
return
before = settings.get_settings(connection)
try:
updated = settings.update_settings(connection, data, user)
except ValueError as exc:
self._send_json(400, {"status": "error", "message": str(exc)})
return
changed = {
key: updated[key]
for key in updated
if before.get(key) != updated[key]
}
auth.audit(
connection,
"settings_update",
actor=user,
target="system_settings",
detail=";".join(
f"{key}:{before.get(key)}->{updated[key]}" for key in changed
),
ip=self._client_ip,
)
self._send_json(200, {"status": "ok", "settings": updated})
finally:
connection.close()
# ------------------------------------------------------------------
# Reminder management (admin)
# ------------------------------------------------------------------
@staticmethod
def _reminder_payload(row) -> dict[str, object]:
payload = {
"id": row["id"],
"company_id": row["company_id"],
"company_name": row["company_name"],
"kind": row["kind"],
"content": row["content"],
"deadline": row["deadline"],
"source": row["source"],
"status": row["status"],
"actor_username": row["actor_username"],
"created_at": row["created_at"],
}
return payload
def _handle_admin_reminders(self, query: dict[str, list[str]]) -> None:
connection = connect(DB_PATH)
try:
user = self._require_admin(connection)
if user is None:
return
conditions: list[str] = []
params: list[object] = []
raw_company = (query.get("company_id") or [None])[0]
if raw_company:
try:
params.append(int(raw_company))
except ValueError:
self._send_json(400, {"status": "error", "message": "company_id 参数无效。"})
return
conditions.append("r.company_id = ?")
raw_limit = (query.get("limit") or ["200"])[0]
try:
limit = max(1, min(int(raw_limit), 500))
except ValueError:
limit = 200
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
rows = connection.execute(
f"""
SELECT r.id, r.company_id, c.name AS company_name, r.kind,
r.content, r.deadline, r.source, r.status,
r.actor_username, r.created_at
FROM reminders r
JOIN companies c ON c.id = r.company_id
{where}
ORDER BY r.id DESC
LIMIT ?
""",
(*params, limit),
).fetchall()
self._send_json(
200,
{"status": "ok",
"reminders": [self._reminder_payload(row) for row in rows]},
)
finally:
connection.close()
def _handle_admin_reminder_pending(self, query: dict[str, list[str]]) -> None:
connection = connect(DB_PATH)
try:
user = self._require_admin(connection)
if user is None:
return
raw_company = (query.get("company_id") or [None])[0]
try:
company_id = int(raw_company)
except (TypeError, ValueError):
self._send_json(400, {"status": "error", "message": "company_id 参数无效。"})
return
company = connection.execute(
"SELECT id, name FROM companies WHERE id = ?", (company_id,)
).fetchone()
if company is None:
self._send_json(404, {"status": "error", "message": "公司不存在。"})
return
items = settings.pending_items(connection, company_id)
self._send_json(
200,
{
"status": "ok",
"company_id": company_id,
"company_name": company["name"],
"items": items,
},
)
finally:
connection.close()
def _handle_admin_send_reminders(self) -> None:
connection = connect(DB_PATH)
try:
user = self._require_admin(connection)
if user is None:
return
data = self._read_json_body()
if data is None:
return
try:
company_id = int(str(data.get("company_id")))
except (TypeError, ValueError):
self._send_json(400, {"status": "error", "message": "company_id 参数无效。"})
return
company = connection.execute(
"SELECT id, name FROM companies WHERE id = ?", (company_id,)
).fetchone()
if company is None:
self._send_json(404, {"status": "error", "message": "公司不存在。"})
return
try:
created, deadline = settings.send_reminders(
connection, company_id, user
)
except ValueError as exc:
self._send_json(400, {"status": "error", "message": str(exc)})
return
if not created:
self._send_json(
400,
{"status": "error",
"message": "该公司当前没有待提醒事项,无需发送。"},
)
return
auth.audit(
connection,
"reminder_send",
actor=user,
target=f"company:{company_id}",
detail=f"items:{len(created)}",
ip=self._client_ip,
)
self._send_json(
200,
{
"status": "ok",
"company_id": company_id,
"company_name": company["name"],
"deadline": deadline,
"reminders": created,
},
)
finally:
connection.close()
# ------------------------------------------------------------------
# Canonical transfer events (admin)
# ------------------------------------------------------------------