feat: 初始化高校人事请假考勤技能 (process-leave-attendance-default)
All checks were successful
技能自动化发布 / release (push) Successful in 15s

基于 skill-template 脚手架开发,包含:
- 5 张业务表(教职工、请假政策、考勤日历、请假记录、假期余额)
- 请假核算引擎(扣薪计算、审批层级判定、年假抵扣)
- CLI 入口(import-employees / calculate-leave / render-html)
- HTML 审批与扣薪报告生成
- 示例数据与完整测试套件(295 tests passed)
This commit is contained in:
chenfuqiang
2026-09-07 16:59:24 +08:00
commit ee2a4d9e22
206 changed files with 29510 additions and 0 deletions

View File

@@ -0,0 +1,285 @@
"""任务编排:鉴权、进度 emit、请假考勤领域调度。"""
from __future__ import annotations
import json
import os
from typing import Any, Optional
from jiangchang_skill_core import collect_runtime_diagnostics, config, format_runtime_health_lines
from jiangchang_skill_core.activity import JobStopped, emit, finish, job_context
from db.connection import init_db
from service.employee_import_service import IMPORT_SUBDIR, ensure_builtin_samples
from service.entitlement_service import check_entitlement
from service.leave_service import calculate_leave, render_leave_report
from util.constants import LOG_LOGGER_NAME, PLATFORM_KIT_MIN_VERSION, SKILL_SLUG, SKILL_VERSION
from util.logging_config import get_skill_logger, setup_skill_logging
from util.runtime_paths import get_db_path, get_skill_data_dir, get_skill_root
def _get_task_logger():
try:
return get_skill_logger()
except RuntimeError:
setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)
return get_skill_logger()
def _print_json(payload: dict[str, Any]) -> None:
print(json.dumps(payload, ensure_ascii=False, indent=2))
_FINISH_RESERVED = frozenset(
{"status", "message", "skill", "error_code", "stage", "ok", "type", "schema_version"}
)
def _finish_success(message: str, payload: Optional[dict[str, Any]] = None, **extra: Any) -> None:
body: dict[str, Any] = {}
if payload:
body.update({k: v for k, v in payload.items() if k not in _FINISH_RESERVED})
body.update({k: v for k, v in extra.items() if k not in _FINISH_RESERVED})
finish(status="success", message=message, skill=SKILL_SLUG, **body)
def _parse_int(raw: Optional[str], name: str) -> int:
if raw is None or not str(raw).strip():
raise ValueError(f"缺少参数: {name}")
text = str(raw).strip()
if "{{" in text or "}}" in text:
raise ValueError(f"参数 {name} 未正确代入(收到 {text!r})。请选中行后再操作。")
try:
return int(text)
except ValueError as exc:
raise ValueError(f"参数 {name} 必须是整数,当前为 {text!r}") from exc
def _parse_optional_int(raw: Optional[str], name: str) -> Optional[int]:
if raw is None or not str(raw).strip():
return None
text = str(raw).strip()
if "{{" in text or "}}" in text:
return None
return _parse_int(raw, name)
def _check_entitlement_or_fail(log, task_type: str) -> bool:
ok, reason = check_entitlement(SKILL_SLUG)
if ok:
return True
log.warning(
"entitlement_failed task_type=%s stage=auth error_code=ENTITLEMENT_DENIED",
task_type,
)
emit("鉴权失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="auth")
finish(
status="failed",
message=reason,
skill=SKILL_SLUG,
error_code="ENTITLEMENT_DENIED",
stage="auth",
)
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return False
def _parse_bool(raw: Optional[str]) -> bool:
if raw is None:
return False
text = str(raw).strip().lower()
if "{{" in text or "}}" in text:
return False
return text in ("1", "true", "yes", "是", "y")
def cmd_calculate_leave(
employee_id: Optional[str] = None,
leave_type: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
has_certificate: Optional[str] = None,
certificate_type: Optional[str] = None,
) -> int:
log = _get_task_logger()
task_type = "calculate-leave"
with job_context(skill=SKILL_SLUG):
log.info(
"task_start task_type=%s employee_id=%s leave_type=%s",
task_type,
employee_id,
leave_type,
)
emit("开始核算请假", skill=SKILL_SLUG, stage="calculate")
try:
if not _check_entitlement_or_fail(log, task_type):
return 1
eid = _parse_int(employee_id, "employee_id")
lt = (leave_type or "").strip()
if not lt or "{{" in lt:
raise ValueError("缺少参数: leave_type")
sd = (start_date or "").strip()
if not sd or "{{" in sd:
raise ValueError("缺少参数: start_date")
ed = (end_date or "").strip()
if not ed or "{{" in ed:
raise ValueError("缺少参数: end_date")
ct = (certificate_type or "").strip()
if "{{" in ct or "}}" in ct:
ct = None
payload = calculate_leave(
eid,
leave_type=lt,
start_date=sd,
end_date=ed,
has_certificate=_parse_bool(has_certificate),
certificate_type=ct or None,
)
_finish_success(str(payload.get("message") or "核算完成"), payload)
print("OK")
_print_json(payload)
return 0
except ValueError as exc:
log.warning("task_failed task_type=%s msg=%s", task_type, exc)
print(f"ERROR:{exc}")
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="VALIDATION")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=%s", task_type)
finish(status="failed", message="请假核算失败", skill=SKILL_SLUG, stage="calculate")
return 1
def cmd_import_employees(file_path: Optional[str] = None) -> int:
log = _get_task_logger()
task_type = "import-employees"
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=%s file=%s", task_type, file_path)
emit("导入教职工", skill=SKILL_SLUG, stage="import")
try:
if not _check_entitlement_or_fail(log, task_type):
return 1
from service.employee_import_service import import_employees_json
path = (file_path or "").strip() or "示例教职工.json"
payload = import_employees_json(path)
_finish_success(str(payload.get("message") or "教职工已导入"), payload)
print("OK")
_print_json(payload)
return 0
except ValueError as exc:
log.warning("task_failed task_type=%s msg=%s", task_type, exc)
print(f"ERROR:{exc}")
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="VALIDATION")
return 1
except Exception:
log.exception("task_failed task_type=%s", task_type)
finish(status="failed", message="导入教职工失败", skill=SKILL_SLUG, stage="import")
return 1
def cmd_render_html(request_id: Optional[str] = None) -> int:
log = _get_task_logger()
emit("生成请假报告", skill=SKILL_SLUG, stage="report")
try:
rid = _parse_optional_int(request_id, "request_id")
payload = render_leave_report(rid)
log.info("render_html_ok path=%s", payload.get("html_path"))
print("OK")
_print_json(payload)
return 0
except ValueError as exc:
log.warning("render_html_failed msg=%s", exc)
print(f"ERROR:{exc}")
return 1
def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int:
"""兼容入口:需 employee_id(来自 target 或 input_id)。"""
log = _get_task_logger()
employee_raw = target or input_id
if not employee_raw or not str(employee_raw).strip():
msg = "缺少参数: employee_id(run 需指定教职工编号,或使用 calculate-leave)"
log.warning("task_failed task_type=run msg=%s", msg)
print(f"ERROR:{msg}")
return 1
print("ERROR:run 入口仅兼容旧调用,请使用 calculate-leave 子命令并提供完整参数。")
return 1
def cmd_health() -> int:
log = _get_task_logger()
try:
init_db()
ensure_builtin_samples(overwrite=False)
except Exception as exc: # noqa: BLE001
log.warning("health_init_db_failed msg=%s", exc)
print(f"ERROR:init_db {exc}")
return 1
runtime = collect_runtime_diagnostics(
skill_slug=SKILL_SLUG,
platform_kit_min_version=PLATFORM_KIT_MIN_VERSION,
skill_root=get_skill_root(),
)
example_path = os.path.join(get_skill_root(), ".env.example")
env_path = config.get_env_file_path() or ""
env_exists = bool(env_path and os.path.isfile(env_path))
health_status = "failed" if runtime.has_fatal_issues else "ok"
lines = [
f"{SKILL_SLUG} health: {health_status}",
*format_runtime_health_lines(runtime),
f"env_path: {env_path}",
f"env_exists: {env_exists}",
f"example_path: {os.path.abspath(example_path)}",
]
for line in lines:
print(line)
log.info("health_ok status=%s", health_status)
return 1 if runtime.has_fatal_issues else 0
def cmd_version() -> int:
print(json.dumps({"version": SKILL_VERSION, "skill": SKILL_SLUG}, ensure_ascii=False))
return 0
def cmd_config_path() -> int:
example_path = os.path.join(get_skill_root(), ".env.example")
env_path = config.get_env_file_path() or ""
data = get_skill_data_dir()
print(
json.dumps(
{
"skill": SKILL_SLUG,
"env_path": env_path,
"example_path": os.path.abspath(example_path),
"data_dir": data,
"employees_dir": f"input/{IMPORT_SUBDIR}",
},
ensure_ascii=False,
)
)
return 0
def cmd_init_db() -> int:
try:
init_db()
ensure_builtin_samples(overwrite=False)
except Exception as exc: # noqa: BLE001
print(json.dumps({"ok": False, "skill": SKILL_SLUG, "error": str(exc)}, ensure_ascii=False))
return 1
print(
json.dumps(
{
"ok": True,
"skill": SKILL_SLUG,
"db_path": get_db_path(),
"employees_dir": f"input/{IMPORT_SUBDIR}",
},
ensure_ascii=False,
)
)
return 0