feat: 项目经费进度播报助手首版 v1.0.1(track-fund-progress-default)
All checks were successful
技能自动化发布 / release (push) Successful in 7s

This commit is contained in:
chenfuqiang
2026-09-13 14:25:23 +08:00
commit a91ead3862
207 changed files with 30383 additions and 0 deletions

252
scripts/db/connection.py Normal file
View File

@@ -0,0 +1,252 @@
"""SQLite 连接与幂等初始化(项目经费进度播报助手)。
业务表 3 张:
- fund_projects(项目台账,金额/日期/状态字段可编辑)
- progress_reports(播报记录,只读,播报自动写入)
- alert_rules(预警规则,阈值可编辑)
表间禁止 SQLite 外键,仅存业务 id(见 references/SCHEMA.md)。
"""
from __future__ import annotations
import sqlite3
from db.display_metadata import (
init_display_metadata,
seed_column_metadata_batch,
upsert_table_metadata,
)
from db.seed_data import seed_all
from db.timestamp_columns import init_updated_at_trigger
from util.runtime_paths import get_db_path
def get_conn() -> sqlite3.Connection:
return sqlite3.connect(get_db_path())
_FUND_PROJECTS_DDL = """
CREATE TABLE IF NOT EXISTS fund_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_no TEXT NOT NULL UNIQUE,
project_name TEXT,
project_type TEXT,
pi_name TEXT,
department TEXT,
total_approved REAL,
total_received REAL,
total_spent REAL,
frozen_amount REAL,
frozen_since TEXT,
start_date TEXT,
end_date TEXT,
status TEXT NOT NULL DEFAULT '在研',
remark TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_PROGRESS_REPORTS_DDL = """
CREATE TABLE IF NOT EXISTS progress_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL,
project_id INTEGER,
project_no TEXT,
project_name TEXT,
pi_name TEXT,
as_of TEXT,
spend_rate REAL,
time_progress REAL,
deviation REAL,
status_tag TEXT NOT NULL,
summary TEXT,
suggestions_json TEXT,
report_file TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_PROGRESS_REPORTS_INDEX = """
CREATE INDEX IF NOT EXISTS idx_progress_reports_batch
ON progress_reports(batch_no)
"""
_ALERT_RULES_DDL = """
CREATE TABLE IF NOT EXISTS alert_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_key TEXT NOT NULL UNIQUE,
display_name TEXT,
threshold_value REAL NOT NULL,
description TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_TS_COLUMN_NAMES = {"created_at", "updated_at"}
def _col(column_name: str, display_name: str, description: str, **extra: object) -> dict[str, object]:
base: dict[str, object] = {
"column_name": column_name,
"display_name": display_name,
"description": description,
"visible": 1,
"searchable": 0,
"editable": 0,
}
base.update(extra)
if column_name in _TS_COLUMN_NAMES:
base["display_type"] = "datetime_unix_seconds"
return base
_FUND_PROJECTS_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "编号", "主键", searchable=1),
_col("project_no", "项目编号", "项目唯一编号(财务/科研系统口径)。", searchable=1),
_col("project_name", "项目名称", "项目全称。", searchable=1, editable=1),
_col(
"project_type",
"项目类别",
"纵向 / 横向 / 其他。",
searchable=1,
editable=1,
options_json='[{"value":"纵向","label":"纵向"},{"value":"横向","label":"横向"},{"value":"其他","label":"其他"}]',
),
_col("pi_name", "负责人", "项目负责人(PI)姓名。", searchable=1, editable=1),
_col("department", "所属学院", "二级学院/单位。", searchable=1, editable=1),
_col("total_approved", "总立项经费", "立项批复总经费(元)。", searchable=1, editable=1, display_type="number"),
_col("total_received", "已到账金额", "已到账经费(元),执行率分母。", searchable=1, editable=1, display_type="number"),
_col("total_spent", "已报销金额", "已报销支出(元),执行率分子。", searchable=1, editable=1, display_type="number"),
_col("frozen_amount", "冻结/暂付款", "冻结或暂付款金额(元)。", searchable=1, editable=1, display_type="number"),
_col("frozen_since", "暂付款最早发生日", "最早一笔暂付款发生日期(YYYY-MM-DD),用于账龄提醒。", editable=1),
_col("start_date", "执行期开始", "项目执行期开始日期(YYYY-MM-DD)。", searchable=1, editable=1),
_col("end_date", "执行期结束", "项目执行期结束日期(YYYY-MM-DD)。", searchable=1, editable=1),
_col(
"status",
"项目状态",
"在研 / 已结题 / 暂缓;播报默认只处理“在研”。",
searchable=1,
editable=1,
options_json='[{"value":"在研","label":"在研"},{"value":"已结题","label":"已结题"},{"value":"暂缓","label":"暂缓"}]',
),
_col("remark", "备注", "维护说明。", editable=1),
_col("created_at", "创建时间", "记录创建时间(Unix 秒级时间戳,自动生成)。"),
_col("updated_at", "更新时间", "记录最后更新时间(Unix 秒级时间戳,自动维护)。"),
)
_PROGRESS_REPORTS_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "报告编号", "主键,播报记录编号。", searchable=1),
_col("batch_no", "播报批次号", "同一次播报请求的批次号。", searchable=1),
_col("project_id", "项目编号(id)", "关联项目台账主键。", searchable=1),
_col("project_no", "项目编号", "项目唯一编号。", searchable=1),
_col("project_name", "项目名称", "项目全称。", searchable=1),
_col("pi_name", "负责人", "项目负责人姓名。", searchable=1),
_col("as_of", "评估基准日", "本次播报的数据截止日期(YYYY-MM-DD)。", searchable=1),
_col("spend_rate", "实际支出率", "已报销/已到账(小数)。", searchable=1, display_type="number"),
_col("time_progress", "序时进度", "已过天数/总执行天数(小数)。", searchable=1, display_type="number"),
_col("deviation", "偏差", "实际支出率 − 序时进度(小数,正=偏快,负=偏慢)。", searchable=1, display_type="number"),
_col(
"status_tag",
"执行状态",
"严重滞后 / 支出异常偏快 / 正常 / 待补录 / 未到账。",
searchable=1,
options_json='[{"value":"严重滞后","label":"🔴 严重滞后"},{"value":"支出异常偏快","label":"🟠 支出异常偏快"},{"value":"正常","label":"🟢 正常"},{"value":"待补录","label":"⚪ 待补录"},{"value":"未到账","label":"⚪ 未到账"}]',
),
_col("summary", "播报摘要", "三段式播报文本(现状/诊断/建议)。", visible=0, display_type="textarea"),
_col("suggestions_json", "建议清单", "建议条目(JSON 数组,默认隐藏)。", visible=0, display_type="textarea"),
_col("report_file", "简报文件", "《经费执行简报》HTML 文件路径。", visible=0),
_col("created_at", "创建时间", "报告创建时间(Unix 秒级时间戳,自动生成)。"),
)
_ALERT_RULES_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "编号", "主键", searchable=1),
_col("rule_key", "规则键", "阈值键名(lag_threshold / overshoot_threshold / pending_aging_days / closing_warn_days)。", searchable=1),
_col("display_name", "名称", "阈值中文名。", searchable=1),
_col("threshold_value", "阈值", "阈值数值(比例为小数,天数为正整数)。", searchable=1, editable=1, display_type="number"),
_col("description", "说明", "阈值口径说明。", searchable=1, editable=1),
_col("created_at", "创建时间", "记录创建时间(Unix 秒级,自动生成)。"),
_col("updated_at", "更新时间", "记录最后更新时间(Unix 秒级,自动维护)。"),
)
def _init_fund_projects_metadata(cursor) -> None:
cursor.execute(_FUND_PROJECTS_DDL)
init_updated_at_trigger(cursor, "fund_projects")
upsert_table_metadata(
cursor,
table_name="fund_projects",
display_name="项目台账",
description="PI 名下项目的基础台账:立项/到账/报销/暂付款与执行周期;播报的数据源。",
grid_help="金额单位为元;顶栏按钮可录入项目,行内可直接编辑金额与日期;播报默认只处理“在研”项目。",
visible=1,
readonly=0,
default_sort_json='[{"column":"id","direction":"desc"}]',
default_visible_columns_json='["project_no","project_name","pi_name","department","total_approved","total_received","total_spent","status"]',
)
seed_column_metadata_batch(
cursor,
table_name="fund_projects",
columns=_FUND_PROJECTS_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _FUND_PROJECTS_COLUMNS],
)
def _init_progress_reports_metadata(cursor) -> None:
cursor.execute(_PROGRESS_REPORTS_DDL)
cursor.execute(_PROGRESS_REPORTS_INDEX)
upsert_table_metadata(
cursor,
table_name="progress_reports",
display_name="播报记录",
description="每次播报的项目执行状态、偏差与三段式摘要;可重建 HTML 简报。",
grid_help="“严重滞后/支出异常偏快”的行建议由科研秘书或财务处跟进;简报可用顶栏按钮重建。",
visible=1,
readonly=1,
default_sort_json='[{"column":"id","direction":"desc"}]',
default_visible_columns_json='["id","as_of","project_no","project_name","pi_name","spend_rate","time_progress","deviation","status_tag"]',
)
seed_column_metadata_batch(
cursor,
table_name="progress_reports",
columns=_PROGRESS_REPORTS_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _PROGRESS_REPORTS_COLUMNS],
)
def _init_alert_rules_metadata(cursor) -> None:
cursor.execute(_ALERT_RULES_DDL)
init_updated_at_trigger(cursor, "alert_rules")
upsert_table_metadata(
cursor,
table_name="alert_rules",
display_name="预警规则",
description="偏差诊断与提醒阈值(严重滞后/异常偏快/暂付款账龄/结题预警)。",
grid_help="比例为小数(0.10=10个百分点),天数为正整数;修改后下一次播报即生效。",
visible=1,
readonly=0,
)
seed_column_metadata_batch(
cursor,
table_name="alert_rules",
columns=_ALERT_RULES_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _ALERT_RULES_COLUMNS],
)
def init_db() -> dict[str, int]:
"""幂等创建/迁移本地 SQLite、数据管理元数据与业务种子,返回种子统计。"""
conn = get_conn()
try:
cur = conn.cursor()
init_display_metadata(cur)
_init_fund_projects_metadata(cur)
_init_progress_reports_metadata(cur)
_init_alert_rules_metadata(cur)
seeded = seed_all(cur)
conn.commit()
return seeded
finally:
conn.close()