Files
alert-contract-expiring-hr/scripts/db/repository.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

196 lines
5.4 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.
"""CRUD 仓储:contracts / performance_records / alerts / renewal_tasks / dispatch_log。
所有方法接收一个外部传入的 sqlite3.Connection,调用方负责事务边界。
"""
from __future__ import annotations
import json
import sqlite3
from datetime import date, datetime
from typing import Any, Iterable
# ---------- contracts ----------
def list_active_contracts(conn: sqlite3.Connection) -> list[sqlite3.Row]:
"""列出 status='可续聘' 的合同。"""
return conn.execute(
"SELECT * FROM contracts WHERE status = '可续聘' ORDER BY contract_end ASC"
).fetchall()
def get_contract(conn: sqlite3.Connection, contract_id: str) -> sqlite3.Row | None:
return conn.execute(
"SELECT * FROM contracts WHERE contract_id = ?", (contract_id,)
).fetchone()
def update_contract_status(
conn: sqlite3.Connection,
contract_id: str,
status: str,
material_id: str | None = None,
) -> int:
"""更新合同状态。material_id 仅在 status='已续签' 时写入。"""
renewed_at = datetime.now().isoformat(timespec="seconds") if status == "已续签" else None
cur = conn.execute(
"""
UPDATE contracts
SET status = ?,
last_material_id = COALESCE(?, last_material_id),
renewed_at = COALESCE(?, renewed_at)
WHERE contract_id = ?
""",
(status, material_id, renewed_at, contract_id),
)
return cur.rowcount
# ---------- performance ----------
def list_performance_by_contract(
conn: sqlite3.Connection, contract_id: str
) -> list[sqlite3.Row]:
"""某合同的业绩记录,按年份升序。"""
return conn.execute(
"SELECT * FROM performance_records WHERE contract_id = ? ORDER BY year ASC",
(contract_id,),
).fetchall()
# ---------- alerts ----------
def insert_alert(
conn: sqlite3.Connection,
alert_id: str,
contract_id: str,
level: str,
days_to_expire: int,
scan_id: str,
) -> None:
conn.execute(
"""
INSERT OR REPLACE INTO alerts
(alert_id, contract_id, alert_level, days_to_expire, scan_id, generated_at, status)
VALUES (?, ?, ?, ?, ?, ?, 'open')
""",
(
alert_id,
contract_id,
level,
days_to_expire,
scan_id,
datetime.now().isoformat(timespec="seconds"),
),
)
def close_open_alerts_for_contract(conn: sqlite3.Connection, contract_id: str) -> int:
"""将某合同所有 open 状态的 alerts 关闭(关联 '已续签' 等终态)。"""
cur = conn.execute(
"UPDATE alerts SET status = 'closed' WHERE contract_id = ? AND status = 'open'",
(contract_id,),
)
return cur.rowcount
def list_open_alerts_by_unit_month(
conn: sqlite3.Connection, unit_id: str, month: str
) -> list[sqlite3.Row]:
"""按单位/月份列出关联合同的 open alerts(用于派发汇总)。"""
sql = """
SELECT a.alert_id, a.contract_id, a.alert_level, a.days_to_expire,
c.contract_end, c.teacher_name, c.unit_name
FROM alerts a
JOIN contracts c ON c.contract_id = a.contract_id
WHERE a.status = 'open'
AND c.unit_id = ?
"""
params: list[Any] = [unit_id]
if month:
sql += " AND substr(c.contract_end, 1, 7) = ?"
params.append(month)
sql += " ORDER BY c.contract_end ASC"
return conn.execute(sql, params).fetchall()
# ---------- renewal_tasks ----------
def insert_task(
conn: sqlite3.Connection,
task_id: str,
contract_id: str,
alert_id: str,
suggestion: str,
reason: str,
summary: str,
checklist: list[str],
) -> None:
conn.execute(
"""
INSERT OR REPLACE INTO renewal_tasks
(task_id, contract_id, alert_id, ai_suggestion, suggestion_reason,
performance_summary, materials_checklist, generated_at, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending')
""",
(
task_id,
contract_id,
alert_id,
suggestion,
reason,
summary,
json.dumps(checklist, ensure_ascii=False),
datetime.now().isoformat(timespec="seconds"),
),
)
def get_task(conn: sqlite3.Connection, task_id: str) -> sqlite3.Row | None:
return conn.execute(
"""
SELECT t.*, c.teacher_name, c.unit_name, c.position, c.contract_type,
c.contract_start, c.contract_end, c.employee_id
FROM renewal_tasks t
JOIN contracts c ON c.contract_id = t.contract_id
WHERE t.task_id = ?
""",
(task_id,),
).fetchone()
# ---------- dispatch_log ----------
def insert_dispatch_log(
conn: sqlite3.Connection,
log_id: str,
unit_id: str,
month: str,
task_ids: list[str],
payload_path: str,
status: str = "dry_run",
) -> None:
conn.execute(
"""
INSERT INTO dispatch_log
(log_id, unit_id, month, task_ids, dispatched_at, payload_path, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
log_id,
unit_id,
month,
json.dumps(task_ids, ensure_ascii=False),
datetime.now().isoformat(timespec="seconds"),
payload_path,
status,
),
)
def list_dispatch_logs(conn: sqlite3.Connection) -> list[sqlite3.Row]:
return conn.execute(
"SELECT * FROM dispatch_log ORDER BY dispatched_at DESC"
).fetchall()