- 迁移链追加 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>
134 lines
4.3 KiB
Python
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
|
|
# ----------------------------------------------------------------------
|