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

124 lines
3.6 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.
"""派发器:按单位/月份汇总任务 → 生成派发 payload JSON → 写派发日志。
v1.1.0 dry-run 模式:payload 写 data/dispatch/{unit}_{month}.json,
但**不真正调用企业微信 API**。开发者后续接入:
- 真实 API:调用 WECOM_API_BASE_URL + WECOM_API_TOKEN
- 在 dispatch_log 写 status='sent' 而非 'dry_run'
"""
from __future__ import annotations
import json
import sqlite3
import uuid
from datetime import datetime
from pathlib import Path
from db import repository as repo
from util.date_util import parse_iso_date
from util.logging_setup import get_skill_logger
from util.runtime_paths import get_dispatch_dir
logger = get_skill_logger()
def build_payload(
conn: sqlite3.Connection,
unit_id: str,
month: str = "",
) -> dict:
"""构建派发 payload:含单位、月份、任务清单、收件人、消息正文。"""
rows = repo.list_open_alerts_by_unit_month(conn, unit_id, month)
# 单位名(取第一条)
unit_name = rows[0]["unit_name"] if rows else "(无)"
items = []
for r in rows:
items.append({
"contract_id": r["contract_id"],
"teacher_name": r["teacher_name"],
"alert_level": r["alert_level"],
"days_to_expire": r["days_to_expire"],
"contract_end": r["contract_end"],
})
if month:
title = f"{unit_name} {month} 合同到期人员清单及续聘建议"
else:
title = f"{unit_name} 待续聘合同清单"
body_lines = [
title,
"",
f"共 {len(items)} 份合同需在到期前办理续聘手续。",
"",
]
for it in items:
level_zh = {"yellow": "黄色", "orange": "橙色", "red": "红色"}.get(
it["alert_level"], it["alert_level"]
)
body_lines.append(
f"- {it['teacher_name']}(合同至 {it['contract_end']},{level_zh}预警,距到期 {it['days_to_expire']} 天)"
)
body_lines.append("")
body_lines.append("请人事秘书尽快通知相关教师备齐续聘材料。")
payload = {
"schema_version": 1,
"channel": "wecom",
"dry_run": True,
"to": {
"unit_id": unit_id,
"unit_name": unit_name,
"role": "人事秘书",
},
"title": title,
"body": "\n".join(body_lines),
"items": items,
"generated_at": datetime.now().isoformat(timespec="seconds"),
}
return payload
def dispatch(
conn: sqlite3.Connection,
unit_id: str,
month: str = "",
) -> dict:
"""派发 dry-run:写 payload 文件 + 写 dispatch_log。"""
payload = build_payload(conn, unit_id, month)
items = payload["items"]
task_ids = [f"T-{it['contract_id']}" for it in items]
dispatch_dir = get_dispatch_dir()
dispatch_dir.mkdir(parents=True, exist_ok=True)
month_part = month or "all"
payload_path = dispatch_dir / f"{unit_id}_{month_part}.json"
payload_path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
log_id = f"L-{uuid.uuid4().hex[:8]}"
repo.insert_dispatch_log(
conn, log_id, unit_id, month or "all", task_ids, str(payload_path),
status="dry_run",
)
conn.commit()
logger.info(
"dispatcher: unit=%s month=%s items=%d payload=%s log=%s",
unit_id, month or "(all)", len(items), payload_path, log_id,
)
return {
"log_id": log_id,
"unit_id": unit_id,
"unit_name": payload["to"]["unit_name"],
"month": month or "(all)",
"item_count": len(items),
"payload_path": str(payload_path),
"status": "dry_run",
}