Files
caiwuzongzhang/src/bank_importer/settings.py
T
总工andmultica-agent 85e11a4f72 HEL-222: 整合自动提醒引擎(212b1f9)到现行测试版 d6f39e0
- 迁移链追加 0009_reminders_engine:旧 manual reminders 表改名
  reminders_legacy_manual 保留历史,新建 reminders/reminder_events/
  reminder_settings(append-only 触发器)
- server.py:新提醒 API(pending/scan/send/manual/resend/settings/
  公司端收件与状态流转)替换旧 manual-send 端点
- web:admin 待提醒清单+扫描+详情抽屉,公司端通知动态化+
  去处理事件委托;设置保存同步提醒扫描参数
- 移除被取代的 settings.pending_items/send_reminders 与旧 UI 逻辑
- 全量测试 346 项通过(5 项浏览器跳过与历史一致)

Co-authored-by: multica-agent <github@multica.ai>
2026-08-28 15:21:27 +00:00

134 lines
4.3 KiB
Python

"""System settings.
System-wide parameters (closing day, global start date, auto-reminder toggle
and lead days) are persisted in ``system_settings`` with an append-only
``system_setting_changes`` trail. The auto-reminder engine itself lives in
``reminders.py`` (HEL-195); this module only owns the settings store.
"""
from __future__ import annotations
import re
import sqlite3
from datetime import datetime
from .db import utc_now
# Defaults are applied when a key is absent; the value type is always string.
DEFAULT_SETTINGS: dict[str, str] = {
"closing_day": "5",
"start_date": "2026-01-01",
"auto_remind": "1",
"remind_days": "3",
}
_SETTING_KEYS = frozenset({"closing_day", "start_date", "auto_remind", "remind_days"})
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def _valid_date(value: str) -> bool:
if not _DATE_RE.match(value):
return False
try:
datetime.strptime(value, "%Y-%m-%d")
except ValueError:
return False
return True
def get_settings(connection: sqlite3.Connection) -> dict[str, str]:
settings = dict(DEFAULT_SETTINGS)
rows = connection.execute(
"SELECT key, value FROM system_settings"
).fetchall()
for row in rows:
settings[row["key"]] = row["value"]
return settings
def validate_settings(values: dict[str, object]) -> tuple[dict[str, str], str | None]:
"""Return ``(cleaned, error)``; ``cleaned`` holds only recognized keys."""
cleaned: dict[str, str] = {}
if "closing_day" in values:
raw = str(values["closing_day"]).strip()
try:
day = int(raw)
except ValueError:
return cleaned, "每月结账日须为 1-28 之间的整数。"
if day < 1 or day > 28:
return cleaned, "每月结账日须为 1-28 之间的整数。"
cleaned["closing_day"] = str(day)
if "start_date" in values:
raw = str(values["start_date"]).strip()
if not _valid_date(raw):
return cleaned, "全局起算日须为有效日期(YYYY-MM-DD)。"
cleaned["start_date"] = raw
if "auto_remind" in values:
raw = str(values["auto_remind"]).strip()
if raw not in {"0", "1"}:
return cleaned, "自动提醒开关须为 0 或 1。"
cleaned["auto_remind"] = raw
if "remind_days" in values:
raw = str(values["remind_days"]).strip()
try:
days = int(raw)
except ValueError:
return cleaned, "提前提醒天数须为不小于 1 的整数。"
if days < 1 or days > 30:
return cleaned, "提前提醒天数须为 1-30 之间的整数。"
cleaned["remind_days"] = str(days)
return cleaned, None
def update_settings(
connection: sqlite3.Connection,
values: dict[str, object],
actor: sqlite3.Row,
) -> dict[str, str]:
cleaned, error = validate_settings(values)
if error is not None:
raise ValueError(error)
if not cleaned:
raise ValueError("没有需要保存的设置项。")
current = get_settings(connection)
with connection:
for key, new_value in cleaned.items():
old_value = current.get(key)
if old_value == new_value:
continue
connection.execute(
"""
INSERT INTO system_settings (key, value, updated_by, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_by = excluded.updated_by,
updated_at = excluded.updated_at
""",
(key, new_value, actor["id"], utc_now()),
)
connection.execute(
"""
INSERT INTO system_setting_changes (
key, before_value, after_value,
actor_user_id, actor_username, created_at
) VALUES (?, ?, ?, ?, ?, ?)
""",
(
key,
old_value,
new_value,
actor["id"],
actor["username"],
utc_now(),
),
)
return get_settings(connection)
# ----------------------------------------------------------------------
# Reminder pending items
# ----------------------------------------------------------------------