Files
alert-contract-expiring-hr/scripts/db/connection.py
taojianmin 06c053d7d7 v1.1.0: 实现核心业务逻辑(扫描/任务书/派发/状态机)
新增(scripts/ 分层完整实现):
- scripts/util/{constants,date_util,runtime_paths,logging_setup}.py
- scripts/db/{connection,seed_data,repository}.py(SQLite + 5 张表 + 50 份样例合同)
- scripts/service/{scanner,task_generator,dispatcher,html_renderer}.py
- scripts/cli/app.py(argparse 子命令)
- scripts/templates/task.html.j2(Jinja2 渲染,无 jinja2 时降级到 str.replace)
- examples/{contracts_sample,performance_sample}.csv(本地样例数据)

修改:
- scripts/main.py:argparse + jiangchang_skill_core 降级
- SKILL.md:version 1.0.1 → 1.1.0
- CHANGELOG.md:加 v1.1.0 条目
- .env.example:加 DB_PATH / HTML_OUTPUT_DIR / JIANGCHANG_DATA_DIR

本地验证(9 个 CLI 命令全部 exit code = 0):
- health / version / config-path / init-db:基础 4 命令
- scan-expiring-contracts --thresholds 90,60,30:42 份可续聘 → 红 10 / 橙 10 / 黄 10,生成 30 任务
- render-html:写 output/<task_id>.html(4.9 KB)
- generate-renewal-task:单份合同任务书生成
- mark-contract-renewed:状态机更新 + 关闭 open alerts
- dispatch-task-notice:按单位/月份派发 payload(dry-run 模式)

业务规则(AI 续聘建议本地版,规则顺序判断):
1. 师德 < 60 → non_renew
2. 近 3 年考核有不合格(< 60)→ observe
3. 全部 >= 80 且逐年上升 → renew
4. 其他 → observe

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-14 14:17:43 +08:00

135 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""SQLite 连接 + DDL 定义 + init_db()。
5 张表:
- contracts: 合同台账
- performance_records: 近 3 年业绩记录
- alerts: 预警记录
- renewal_tasks: 续聘任务
- dispatch_log: 派发日志
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Iterable
from util.runtime_paths import ensure_dirs, get_db_path
# DDL:所有 IF NOT EXISTS,幂等可重入
DDL_STATEMENTS: tuple[str, ...] = (
"""
CREATE TABLE IF NOT EXISTS contracts (
contract_id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
teacher_name TEXT NOT NULL,
unit_id TEXT NOT NULL,
unit_name TEXT NOT NULL,
position TEXT NOT NULL,
contract_type TEXT NOT NULL,
contract_start TEXT NOT NULL,
contract_end TEXT NOT NULL,
trial_period_end TEXT NOT NULL,
status TEXT NOT NULL DEFAULT '可续聘',
last_material_id TEXT,
renewed_at TEXT
);
""",
"""
CREATE TABLE IF NOT EXISTS performance_records (
record_id TEXT PRIMARY KEY,
contract_id TEXT NOT NULL,
year INTEGER NOT NULL,
performance_score REAL NOT NULL,
moral_teaching_score REAL NOT NULL,
research_score REAL NOT NULL,
teaching_score REAL NOT NULL,
FOREIGN KEY (contract_id) REFERENCES contracts(contract_id)
);
""",
"CREATE INDEX IF NOT EXISTS idx_perf_contract ON performance_records(contract_id);",
"""
CREATE TABLE IF NOT EXISTS alerts (
alert_id TEXT PRIMARY KEY,
contract_id TEXT NOT NULL,
alert_level TEXT NOT NULL,
days_to_expire INTEGER NOT NULL,
scan_id TEXT NOT NULL,
generated_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
FOREIGN KEY (contract_id) REFERENCES contracts(contract_id)
);
""",
"CREATE INDEX IF NOT EXISTS idx_alerts_contract ON alerts(contract_id);",
"CREATE INDEX IF NOT EXISTS idx_alerts_status ON alerts(status);",
"""
CREATE TABLE IF NOT EXISTS renewal_tasks (
task_id TEXT PRIMARY KEY,
contract_id TEXT NOT NULL,
alert_id TEXT,
ai_suggestion TEXT NOT NULL,
suggestion_reason TEXT NOT NULL,
performance_summary TEXT NOT NULL,
materials_checklist TEXT NOT NULL,
generated_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
FOREIGN KEY (contract_id) REFERENCES contracts(contract_id),
FOREIGN KEY (alert_id) REFERENCES alerts(alert_id)
);
""",
"CREATE INDEX IF NOT EXISTS idx_tasks_contract ON renewal_tasks(contract_id);",
"""
CREATE TABLE IF NOT EXISTS dispatch_log (
log_id TEXT PRIMARY KEY,
unit_id TEXT NOT NULL,
month TEXT NOT NULL,
task_ids TEXT NOT NULL,
dispatched_at TEXT NOT NULL,
payload_path TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'dry_run'
);
""",
"CREATE INDEX IF NOT EXISTS idx_dispatch_unit_month ON dispatch_log(unit_id, month);",
)
def get_connection(db_path: Path | None = None) -> sqlite3.Connection:
"""获取 SQLite 连接。"""
ensure_dirs()
path = db_path or get_db_path()
conn = sqlite3.connect(str(path))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def init_db(db_path: Path | None = None) -> dict[str, int]:
"""建表 + 加载种子数据。返回表行数 dict。"""
ensure_dirs()
conn = get_connection(db_path)
try:
for ddl in DDL_STATEMENTS:
conn.execute(ddl)
conn.commit()
# 加载种子(如果 contracts 表为空)
from db.seed_data import load_seed_if_empty
load_seed_if_empty(conn)
# 统计
counts = {
"contracts": conn.execute("SELECT COUNT(*) AS n FROM contracts").fetchone()["n"],
"performance_records": conn.execute("SELECT COUNT(*) AS n FROM performance_records").fetchone()["n"],
"alerts": conn.execute("SELECT COUNT(*) AS n FROM alerts").fetchone()["n"],
"renewal_tasks": conn.execute("SELECT COUNT(*) AS n FROM renewal_tasks").fetchone()["n"],
"dispatch_log": conn.execute("SELECT COUNT(*) AS n FROM dispatch_log").fetchone()["n"],
}
return counts
finally:
conn.close()
def table_names() -> Iterable[str]:
return ("contracts", "performance_records", "alerts", "renewal_tasks", "dispatch_log")