返回作品列表

网友作品 #000228 · 提交于 2026/09/15

施工进度台账系统:手机在线填报与Excel导出

★★★★★零基础AI 编程工具国内其他模型python

作品作者

Mustang零基础
开发工具
AI 编程工具
AI 模型
国内其他模型
使用 Skill
python
提交你的作品

作品简介

这是一套开箱即用的施工进度台账在线填报系统,覆盖桥梁、隧道、路基三大工程专业:电脑上启动服务后,手机连同一 WiFi 扫码或输网址即可填报,无需微信或 App,数据集中存放在 SQLite 数据库中。

现场人员选择工程类别与分部分项,填写当日完成量、形象进度、质量安全情况后提交;管理端可按类别、分项、日期区间、关键字检索台账,随时修改、删除,并可通过数据看板掌握今日/本月/累计填报量与近 7 天趋势。

系统支持一键导出 Excel 台账(含汇总统计与三大专业明细表),可直接用于上报;还可设置访问口令,防止无关人员误操作。

核心亮点

  • 手机扫码填报:电脑起服务、手机同 WiFi 直接访问,底部「看板 / 填报 / 台账」三栏导航,现场即填
  • 三大专业预置字段:桥梁(桩基、墩台、梁体架设、桥面铺装等)、隧道(超前支护、洞身开挖、二衬、监控量测等)、路基(挖填方、软基处理、挡土墙等),各配墩台编号、围岩级别、压实度等专业字段
  • 累计工程量自动滚动:按「类别 + 分项 + 单位工程 + 部位」自动累加,不用手工计算
  • 数据看板:今日/本月/累计条数、近 7 天填报趋势、分部分项填报排行、质量安全概览
  • 检索与导出:多条件筛选分页查看,一键导出「汇总统计 + 三张明细表」Excel,可直接上报
  • 易定制易运维:增删分项或字段只改 catalog.py 一个文件,页面和 Excel 自动跟着变;换端口、改数据目录、设备份只需配置 .env;日志按天分文件保留 30 天

技术与工具

  • 技术栈:Python 3.12 + FastAPI + SQLModel(SQLite)+ Jinja2 模板 + openpyxl 导出,uv 启动,附 pytest 测试
  • 兼容性处理:受管控终端禁用 OpenSSL、uvicorn 不可用时,自动回退到内置标准库 HTTP 服务,功能一致
  • 开发方式:零基础把 AI 当程序员沟通,按自身需求逐步提出要求,借助 AI 编程类工具完成多人协作系统开发

适用场景

  • 公路、铁路等交通基建项目的施工进度台账集中填报与管理
  • 市政工程施工数据的多人协作采集、汇总统计与 Excel 上报
  • 需要现场手机填报 + 后台看板管理的中小型项目部
原始提交内容(点开查看,可复制全文去问 AI)

施工进度台账在线填报系统 · 源代码 本项目为桥梁 / 隧道 / 路基三大工程的施工进度台账在线填报系统。 本文件由 export_source_md.py 自动汇总生成,包含全部源码文件内容。

文件清单 pyproject.toml .env.example .gitignore .python-version README.md src/construction_ledger/init.py src/construction_ledger/main.py src/init.py src/construction_ledger/catalog.py src/construction_ledger/config.py src/construction_ledger/db.py src/construction_ledger/logging_setup.py src/construction_ledger/models.py src/construction_ledger/services/init.py src/construction_ledger/services/export_service.py src/construction_ledger/services/ledger_service.py src/construction_ledger/ssl_compat.py src/construction_ledger/web/init.py src/construction_ledger/web/app.py src/construction_ledger/web/server.py src/construction_ledger/web/static/app.css src/construction_ledger/web/templates/base.html src/construction_ledger/web/templates/detail.html src/construction_ledger/web/templates/fill.html src/construction_ledger/web/templates/index.html src/construction_ledger/web/templates/login.html src/construction_ledger/web/templates/records.html tests/conftest.py tests/test_config.py tests/test_ledger.py pyproject.toml [project] name = “construction_ledger” version = “0.1.0” description = “桥梁/隧道/路基工程施工进度台账在线填报系统” readme = “README.md” requires-python = “>=3.12” dependencies = [ “fastapi>=0.141.1”, “jinja2>=3.1.6”, “openpyxl>=3.1.5”, “python-dotenv>=1.0.1”, “python-multipart>=0.0.32”, “sqlmodel>=0.0.42”, “uvicorn[standard]>=0.52.4”, ]

[project.scripts] ledger = “construction_ledger.main:main”

[build-system] requires = [“hatchling”] build-backend = “hatchling.build”

[tool.ruff] line-length = 100 target-version = “py312”

[tool.ruff.lint] select = [“E”, “F”, “I”, “N”, “UP”]

[tool.pytest.ini_options] testpaths = [“tests”]

[tool.mypy] python_version = “3.12” ignore_missing_imports = true warn_return_any = true warn_unused_configs = true

[dependency-groups] dev = [ “pytest>=9.1.1”, ] .env.example APP_NAME=施工进度台账在线填报 HOST=0.0.0.0 PORT=8000 LOG_LEVEL=INFO PAGE_SIZE=20

数据存放目录(数据库与 Excel 导出都在这里)

DATA_DIR=data

访问口令:留空表示不设限制;设置后所有页面需先输入口令

ACCESS_CODE= .gitignore .venv/ pycache/ .pytest_cache/ .mypy_cache/ .ruff_cache/ logs/ data/ dist/ build/ *.spec .env .python-version 3.12 README.md

施工进度台账在线填报(桥梁 / 隧道 / 路基)

一个开箱即用的施工进度台账在线填报系统:电脑起服务,手机扫码/输网址即可填报, 数据集中存 SQLite,随时按条件检索、修改、导出 Excel 台账。

一、能做什么

功能 说明
手机端填报 选择工程类别 分部分项 填写当日完成量、形象进度、质量安全情况,提交即可
三大专业类别 桥梁、隧道、路基各自一套分部分项与专业字段(见下表)
台账检索 按类别、分部分项、日期区间、关键字(桩号/单位工程/填报人)筛选,分页查看
编辑 / 删除 列表页与详情页均可修改,误填可删除
累计工程量 系统按「类别 + 分项 + 单位工程 + 部位」自动滚动累加,不用手工算
数据看板 今日/本月/累计条数、近 7 天趋势、分项排行、质量安全概览
Excel 导出 一键导出,含「汇总统计」+ 桥梁/隧道/路基三张明细表,可直接上报
访问口令 可选,设置后需输口令才能查看和修改,防止无关人员误操作

预置的专业字段

类别 分部分项(节选) 专业字段
桥梁工程 桩基、承台/系梁、墩台身、盖梁、支座垫石、梁体预制、梁体架设、桥面铺装、防撞护栏、伸缩缝… 墩台编号、设计桩长、成孔深度、混凝土强度等级、浇筑方量、钢筋用量、张拉压浆、模板投入
隧道工程 洞口明洞、超前地质预报、超前支护、洞身开挖、初期支护、仰拱填充、二次衬砌、防排水、监控量测… 掌子面桩号、围岩级别、开挖方法、循环进尺、当日进尺、累计进尺、喷射混凝土方量、钢拱架榀数、二衬长度
路基工程 清表、挖方、填方、土工格栅、软基处理、台背回填、排水、防护、挡土墙、涵洞、边坡绿化… 起止桩号、填挖类型、层位、松铺/压实厚度、压实度、含水量、填料类型、填筑方量、处理面积

想增删分项或字段,改 src/construction_ledger/catalog.py 一个文件即可,页面和 Excel 会自动跟着变。

二、怎么启动

环境要求:Python 3.12(当前项目已用官方发行的 Python 3.12.10 建好虚拟环境)。

在 construction-ledger 目录下执行:

uv run python -m construction_ledger.main
启动后会打印:

本机访问:http://127.0.0.1:8000(会自动打开浏览器)
手机访问:http://192.168.x.x:8000(手机与电脑连同一个 WiFi 即可,无需微信/App)
数据文件、导出目录、日志文件的位置
停止服务:在窗口按 Ctrl + C。

换端口:把 .env.example 复制成 .env,改 PORT=9000 即可;想加访问口令就填 ACCESS_CODE=你的口令。

三、目录结构
src/construction_ledger/
 main.py              启动入口
 config.py            配置读取(.env)
 db.py                数据库连接与建表
 models.py            台账数据模型
 catalog.py           三大类别的分部分项与字段定义(要改字段改这里)
 ssl_compat.py        OpenSSL 被禁用时的兼容处理
 services/
    ledger_service.py  台账增删改查、累计量、看板统计
    export_service.py  Excel 导出
 web/
     app.py           页面与 JSON 接口
     server.py        内置轻量 HTTP 服务(uvicorn 不可用时的兜底)
     templates/       手机端页面
     static/          样式
数据默认放在 data/ledger.db,导出文件放在 data/exports/,日志放在 logs/(按天分文件,保留 30 天)。

四、常见问题
1. 手机打不开? 确认手机和电脑在同一个 WiFi;Windows 防火墙首次会弹窗,点「允许访问」。

2. 想换端口 / 数据要放别的盘? 复制 .env.example 为 .env,改 PORT、DATA_DIR。

3. 想备份台账? 直接复制 data/ledger.db 即可;也可以定期点「导出」存一份 Excel。

4. 提示 uvicorn 不可用、改用内置 HTTP 服务? 说明这台电脑的终端管控策略禁用了 OpenSSL 等模块,程序会自动切换到内置标准库 HTTP 服务, 功能完全一致,只是不支持 HTTPS(内网填报本来也用 HTTP)。

已实测:本机的 Smart App Control 会拦截无签名的 Python 扩展 DLL,请务必使用 python.org 的官方 Python, 不要使用未经签名的绿色版 Python。

五、自测
uv run python -m pytest -q

---

## `src/construction_ledger/__init__.py`

```python
"""项目包入口。"""
src/construction_ledger/main.py
"""施工进度台账在线填报系统启动入口。"""

from __future__ import annotations

import logging
import socket
import webbrowser

from construction_ledger.config import CONFIG
from construction_ledger.db import init_db
from construction_ledger.logging_setup import configure_logging
from construction_ledger.ssl_compat import ensure_ssl_module

LOGGER = logging.getLogger(__name__)


def get_lan_ip() -> str:
    """获取本机局域网 IP,方便手机同网访问。"""
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
            sock.connect(("8.8.8.8", 80))
            return sock.getsockname()[0]
    except OSError:
        return "127.0.0.1"


def pick_server() -> str:
    """优先使用 uvicorn;受管控终端上回退到标准库服务器。"""
    try:
        import uvicorn  # noqa: F401

        return "uvicorn"
    except Exception as exc:  # noqa: BLE001
        LOGGER.warning("uvicorn 不可用(%s),改用内置标准库 HTTP 服务", exc)
        return "stdlib"


def main() -> None:
    log_file = configure_logging()
    ensure_ssl_module()
    init_db()

    lan_ip = get_lan_ip()
    print("=" * 58)
    print(f"  {CONFIG.app_name}")
    print("=" * 58)
    print(f"  本机访问:  http://127.0.0.1:{CONFIG.port}")
    print(f"  手机访问:  http://{lan_ip}:{CONFIG.port}  (需与电脑同一 WiFi)")
    print(f"  数据文件:  {CONFIG.database_path}")
    print(f"  导出目录:  {CONFIG.export_dir}")
    print(f"  日志文件:  {log_file}")
    print("  停止服务:  在此窗口按 Ctrl + C")
    print("=" * 58)

    from construction_ledger.web.app import app

    try:
        webbrowser.open(f"http://127.0.0.1:{CONFIG.port}")
    except Exception:  # noqa: BLE001 打开浏览器失败不影响服务
        LOGGER.debug("自动打开浏览器失败,可手动访问上述地址")

    if pick_server() == "uvicorn":
        import uvicorn

        uvicorn.run(
            app,
            host=CONFIG.host,
            port=CONFIG.port,
            log_level=CONFIG.log_level.lower(),
            access_log=False,
        )
    else:
        from construction_ledger.web.server import run_stdlib_server

        run_stdlib_server(app, CONFIG.host, CONFIG.port)


if __name__ == "__main__":
    main()
src/__init__.py
"""src 包入口。"""
src/construction_ledger/catalog.py
"""工程类别、分部分项与台账字段定义。

三大类别:桥梁工程、隧道工程、路基工程。
每个类别预置常用分部分项和专业化扩展字段,后续只需改本文件即可调整。
"""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class FieldSpec:
    """单个填报字段的描述。"""

    key: str
    label: str
    ftype: str = "text"  # text / number / textarea / select
    unit: str = ""
    options: tuple[str, ...] = ()
    required: bool = False
    placeholder: str = ""


@dataclass(frozen=True, slots=True)
class CategorySpec:
    """一个工程类别的完整定义。"""

    key: str
    name: str
    short: str
    sub_items: tuple[str, ...]
    fields: tuple[FieldSpec, ...]
    default_unit: str = ""


COMMON_FIELDS: tuple[FieldSpec, ...] = (
    FieldSpec("unit_project", "单位/分部工程名称", required=True, placeholder="如:XX 大桥下部结构"),
    FieldSpec("location", "桩号 / 部位", required=True, placeholder="如:K12+300 左幅 3# 墩"),
    FieldSpec("quantity", "当日完成工程量", ftype="number", required=True),
    FieldSpec("unit", "计量单位", required=True, placeholder="如:m / m3 / t / 根 / 榀"),
    FieldSpec(
        "progress_status",
        "形象进度",
        ftype="select",
        required=True,
        options=("未开始", "进行中", "已完成", "暂停施工"),
    ),
    FieldSpec("progress_desc", "形象进度描述", ftype="textarea", placeholder="简述当日完成的形象部位"),
    FieldSpec("workers", "投入人员", ftype="number", unit="人"),
    FieldSpec("equipment", "主要机械设备", placeholder="如:旋挖钻 1 台、吊车 2 台"),
    FieldSpec(
        "weather",
        "天气情况",
        ftype="select",
        options=("晴", "阴", "小雨", "中雨", "大雨", "雪", "大风", "高温", "其他"),
    ),
    FieldSpec(
        "quality_status",
        "质量情况",
        ftype="select",
        options=("合格", "一般", "整改中", "不合格", "停工待检"),
    ),
    FieldSpec("quality_note", "质量说明", ftype="textarea"),
    FieldSpec(
        "safety_status",
        "安全情况",
        ftype="select",
        options=("正常", "一般隐患", "隐患整改中", "停工整改", "发生事故"),
    ),
    FieldSpec("safety_note", "安全说明", ftype="textarea"),
    FieldSpec("problem", "存在问题及处理措施", ftype="textarea"),
    FieldSpec("team", "施工班组"),
    FieldSpec("reporter", "填报人", required=True),
)

BRIDGE = CategorySpec(
    key="bridge",
    name="桥梁工程",
    short="桥梁",
    default_unit="m3",
    sub_items=(
        "桩基(钻孔灌注桩)",
        "承台 / 系梁",
        "墩台身",
        "盖梁 / 台帽",
        "支座垫石及支座安装",
        "梁体预制",
        "梁体架设",
        "湿接缝 / 体系转换",
        "桥面铺装",
        "防撞护栏",
        "伸缩缝",
        "桥头搭板 / 锥坡",
        "其他",
    ),
    fields=(
        FieldSpec("pier_no", "墩台编号 / 构件编号", placeholder="如:3# 墩、左幅 2-1# 梁"),
        FieldSpec("pile_length", "设计桩长", ftype="number", unit="m"),
        FieldSpec("hole_depth", "本次成孔 / 施工深度", ftype="number", unit="m"),
        FieldSpec(
            "concrete_grade",
            "混凝土强度等级",
            ftype="select",
            options=("C25", "C30", "C35", "C40", "C45", "C50", "C55", "C60"),
        ),
        FieldSpec("concrete_volume", "混凝土浇筑方量", ftype="number", unit="m3"),
        FieldSpec("rebar_weight", "钢筋用量", ftype="number", unit="t"),
        FieldSpec("prestress", "张拉 / 压浆情况"),
        FieldSpec("formwork", "模板投入", ftype="number", unit="套"),
    ),
)

TUNNEL = CategorySpec(
    key="tunnel",
    name="隧道工程",
    short="隧道",
    default_unit="m",
    sub_items=(
        "洞口及明洞工程",
        "超前地质预报",
        "超前支护(管棚 / 小导管)",
        "洞身开挖",
        "初期支护",
        "仰拱及填充",
        "二次衬砌",
        "防排水工程",
        "隧道路面 / 基层",
        "机电预埋及附属",
        "监控量测",
        "其他",
    ),
    fields=(
        FieldSpec("face_chainage", "掌子面 / 施工桩号", placeholder="如:YK12+345"),
        FieldSpec(
            "rock_grade",
            "围岩级别",
            ftype="select",
            options=("I", "II", "III", "IV", "V", "VI"),
        ),
        FieldSpec(
            "excavate_method",
            "开挖方法",
            ftype="select",
            options=(
                "全断面法",
                "台阶法",
                "环形开挖预留核心土法",
                "中隔壁法(CD)",
                "交叉中隔壁法(CRD)",
                "双侧壁导坑法",
            ),
        ),
        FieldSpec("cycle_advance", "循环进尺", ftype="number", unit="m"),
        FieldSpec("day_advance", "当日进尺", ftype="number", unit="m"),
        FieldSpec("total_advance", "累计进尺", ftype="number", unit="m"),
        FieldSpec("shotcrete_volume", "喷射混凝土方量", ftype="number", unit="m3"),
        FieldSpec("steel_arch", "钢拱架 / 格栅", ftype="number", unit="榀"),
        FieldSpec("lining_length", "二衬浇筑长度", ftype="number", unit="m"),
    ),
)

SUBGRADE = CategorySpec(
    key="subgrade",
    name="路基工程",
    short="路基",
    default_unit="m3",
    sub_items=(
        "清表与场地清理",
        "挖方(路堑开挖)",
        "填方(路堤填筑)",
        "土工格栅 / 加筋材料",
        "软基处理",
        "台背 / 涵背回填",
        "排水工程(边沟、截水沟)",
        "防护工程(骨架、喷锚)",
        "挡土墙",
        "涵洞 / 通道",
        "边坡绿化",
        "其他",
    ),
    fields=(
        FieldSpec("start_chainage", "起始桩号", placeholder="如:K12+300"),
        FieldSpec("end_chainage", "终止桩号", placeholder="如:K12+500"),
        FieldSpec(
            "fill_type",
            "填挖类型",
            ftype="select",
            options=("填方", "挖方", "半填半挖"),
        ),
        FieldSpec("layer_no", "层位 / 层数"),
        FieldSpec("loose_thickness", "松铺厚度", ftype="number", unit="cm"),
        FieldSpec("compact_thickness", "压实厚度", ftype="number", unit="cm"),
        FieldSpec("compaction", "压实度", ftype="number", unit="%"),
        FieldSpec("moisture", "含水量", ftype="number", unit="%"),
        FieldSpec("material", "填料类型", placeholder="如:碎石土、砂砾、石灰土"),
        FieldSpec("fill_volume", "填筑 / 开挖方量", ftype="number", unit="m3"),
        FieldSpec("treat_area", "处理面积", ftype="number", unit="m2"),
    ),
)

CATEGORIES: dict[str, CategorySpec] = {
    BRIDGE.key: BRIDGE,
    TUNNEL.key: TUNNEL,
    SUBGRADE.key: SUBGRADE,
}

CATEGORY_ORDER: tuple[str, ...] = (BRIDGE.key, TUNNEL.key, SUBGRADE.key)


def get_category(key: str) -> CategorySpec:
    """按 key 取类别定义,未知 key 回落为路基工程。"""
    return CATEGORIES.get(key, SUBGRADE)


def category_name(key: str) -> str:
    return get_category(key).name


def catalog_payload() -> dict[str, object]:
    """给前端动态渲染表单用的目录数据。"""

    def dump(field: FieldSpec) -> dict[str, object]:
        return {
            "key": field.key,
            "label": field.label,
            "type": field.ftype,
            "unit": field.unit,
            "options": list(field.options),
            "required": field.required,
            "placeholder": field.placeholder,
        }

    return {
        "order": list(CATEGORY_ORDER),
        "commonFields": [dump(f) for f in COMMON_FIELDS],
        "categories": {
            key: {
                "key": spec.key,
                "name": spec.name,
                "short": spec.short,
                "defaultUnit": spec.default_unit,
                "subItems": list(spec.sub_items),
                "fields": [dump(f) for f in spec.fields],
            }
            for key, spec in CATEGORIES.items()
        },
    }
src/construction_ledger/config.py
"""应用配置读取。

真实配置放 `.env`,仓库只保留 `.env.example`。
"""

from __future__ import annotations

import os
import sys
from dataclasses import dataclass, field
from pathlib import Path

from dotenv import load_dotenv


def _get_runtime_root() -> Path:
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parents[2]


def _load_runtime_env() -> None:
    runtime_root = _get_runtime_root()
    for candidate in (runtime_root / ".env", runtime_root / ".env.example"):
        if candidate.exists():
            load_dotenv(candidate, override=False)
            break


_load_runtime_env()


def _default_data_dir() -> Path:
    return _get_runtime_root() / "data"


@dataclass(slots=True)
class AppConfig:
    app_name: str = os.getenv("APP_NAME", "施工进度台账在线填报")
    host: str = os.getenv("HOST", "0.0.0.0")
    port: int = int(os.getenv("PORT", "8000"))
    log_level: str = os.getenv("LOG_LEVEL", "INFO")
    page_size: int = int(os.getenv("PAGE_SIZE", "20"))
    data_dir: Path = field(default_factory=_default_data_dir)
    access_code: str = os.getenv("ACCESS_CODE", "")

    @property
    def database_path(self) -> Path:
        self.data_dir.mkdir(parents=True, exist_ok=True)
        return self.data_dir / "ledger.db"

    @property
    def export_dir(self) -> Path:
        target = self.data_dir / "exports"
        target.mkdir(parents=True, exist_ok=True)
        return target


CONFIG = AppConfig()
src/construction_ledger/db.py
"""数据库连接与初始化。"""

from __future__ import annotations

import logging
from collections.abc import Iterator
from pathlib import Path

from sqlalchemy import event
from sqlmodel import Session, SQLModel, create_engine

from construction_ledger.config import CONFIG

LOGGER = logging.getLogger(__name__)

_engine = None


def get_database_url(db_path: Path | None = None) -> str:
    path = db_path or CONFIG.database_path
    return f"sqlite:///{path.as_posix()}"


def get_engine(db_path: Path | None = None):
    """获取全局数据库引擎(单例)。"""
    global _engine
    if db_path is not None:
        return create_engine(get_database_url(db_path), echo=False, connect_args={"check_same_thread": False})
    if _engine is None:
        url = get_database_url()
        _engine = create_engine(url, echo=False, connect_args={"check_same_thread": False})

        @event.listens_for(_engine, "connect")
        def _set_sqlite_pragma(dbapi_connection, _connection_record) -> None:  # type: ignore[no-untyped-def]
            cursor = dbapi_connection.cursor()
            cursor.execute("PRAGMA foreign_keys=ON")
            cursor.close()

        LOGGER.info("数据库引擎初始化完成:%s", url)
    return _engine


def init_db(db_path: Path | None = None) -> None:
    """建表(幂等)。"""
    import construction_ledger.models as models  # noqa: F401  确保表已注册

    engine = get_engine(db_path)
    SQLModel.metadata.create_all(engine)
    LOGGER.info("数据库表结构已就绪")


def get_session() -> Iterator[Session]:
    """FastAPI 依赖用的会话生成器。"""
    with Session(get_engine()) as session:
        yield session
src/construction_ledger/logging_setup.py
from __future__ import annotations

from datetime import datetime, timedelta
import logging
from pathlib import Path
import os
import subprocess
import sys

from dotenv import load_dotenv


_LOG_FILE_PREFIX = "app"
_MAX_LOG_BYTES = 5 * 1024 * 1024
_RETENTION_DAYS = 30


class DateSizedFileHandler(logging.Handler):
    def __init__(
        self,
        log_dir: Path,
        *,
        prefix: str = _LOG_FILE_PREFIX,
        max_bytes: int = _MAX_LOG_BYTES,
        retention_days: int = _RETENTION_DAYS,
        encoding: str = "utf-8",
    ) -> None:
        super().__init__()
        self.log_dir = log_dir
        self.prefix = prefix
        self.max_bytes = max_bytes
        self.retention_days = retention_days
        self.encoding = encoding
        self.log_dir.mkdir(parents=True, exist_ok=True)

    def get_current_log_file(self, message: str = "") -> Path:
        date_part = datetime.now().strftime("%Y-%m-%d")
        message_size = len((message + "\n").encode(self.encoding)) if message else 0
        index = 0

        while True:
            suffix = "" if index == 0 else f"-{index}"
            log_file = self.log_dir / f"{self.prefix}-{date_part}{suffix}.log"
            if not log_file.exists():
                return log_file
            if log_file.stat().st_size + message_size <= self.max_bytes:
                return log_file
            index += 1

    def cleanup_old_logs(self) -> None:
        cutoff = datetime.now() - timedelta(days=self.retention_days)
        for log_file in self.log_dir.glob(f"{self.prefix}-*.log"):
            try:
                modified = datetime.fromtimestamp(log_file.stat().st_mtime)
            except OSError:
                continue
            if modified < cutoff:
                try:
                    log_file.unlink()
                except OSError:
                    continue

    def emit(self, record: logging.LogRecord) -> None:
        try:
            message = self.format(record)
            log_file = self.get_current_log_file(message)
            with log_file.open("a", encoding=self.encoding) as handle:
                handle.write(message + "\n")
        except Exception:
            self.handleError(record)


def get_runtime_root() -> Path:
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parents[2]


def load_runtime_env() -> Path | None:
    runtime_root = get_runtime_root()
    for candidate in (runtime_root / ".env", runtime_root / ".env.example"):
        if candidate.exists():
            load_dotenv(candidate, override=False)
            return candidate
    return None


def get_log_dir() -> Path:
    log_dir = get_runtime_root() / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    return log_dir


def get_log_file() -> Path:
    handler = DateSizedFileHandler(get_log_dir())
    return handler.get_current_log_file()


def configure_logging() -> Path:
    env_file = load_runtime_env()
    logger = logging.getLogger()
    if logger.handlers:
        return get_log_file()

    log_level_name = os.getenv("LOG_LEVEL", "INFO").upper()
    log_level = getattr(logging, log_l

</details>

<script type="application/json" data-raw-detail>"施工进度台账在线填报系统 · 源代码\n本项目为桥梁 / 隧道 / 路基三大工程的施工进度台账在线填报系统。 本文件由 export_source_md.py 自动汇总生成,包含全部源码文件内容。\n\n文件清单\npyproject.toml\n.env.example\n.gitignore\n.python-version\nREADME.md\nsrc/construction_ledger/__init__.py\nsrc/construction_ledger/main.py\nsrc/__init__.py\nsrc/construction_ledger/catalog.py\nsrc/construction_ledger/config.py\nsrc/construction_ledger/db.py\nsrc/construction_ledger/logging_setup.py\nsrc/construction_ledger/models.py\nsrc/construction_ledger/services/__init__.py\nsrc/construction_ledger/services/export_service.py\nsrc/construction_ledger/services/ledger_service.py\nsrc/construction_ledger/ssl_compat.py\nsrc/construction_ledger/web/__init__.py\nsrc/construction_ledger/web/app.py\nsrc/construction_ledger/web/server.py\nsrc/construction_ledger/web/static/app.css\nsrc/construction_ledger/web/templates/base.html\nsrc/construction_ledger/web/templates/detail.html\nsrc/construction_ledger/web/templates/fill.html\nsrc/construction_ledger/web/templates/index.html\nsrc/construction_ledger/web/templates/login.html\nsrc/construction_ledger/web/templates/records.html\ntests/conftest.py\ntests/test_config.py\ntests/test_ledger.py\npyproject.toml\n[project]\nname = \"construction_ledger\"\nversion = \"0.1.0\"\ndescription = \"桥梁/隧道/路基工程施工进度台账在线填报系统\"\nreadme = \"README.md\"\nrequires-python = \">=3.12\"\ndependencies = [\n    \"fastapi>=0.141.1\",\n    \"jinja2>=3.1.6\",\n    \"openpyxl>=3.1.5\",\n    \"python-dotenv>=1.0.1\",\n    \"python-multipart>=0.0.32\",\n    \"sqlmodel>=0.0.42\",\n    \"uvicorn[standard]>=0.52.4\",\n]\n\n[project.scripts]\nledger = \"construction_ledger.main:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py312\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\", \"N\", \"UP\"]\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\n\n[tool.mypy]\npython_version = \"3.12\"\nignore_missing_imports = true\nwarn_return_any = true\nwarn_unused_configs = true\n\n[dependency-groups]\ndev = [\n    \"pytest>=9.1.1\",\n]\n.env.example\nAPP_NAME=施工进度台账在线填报\nHOST=0.0.0.0\nPORT=8000\nLOG_LEVEL=INFO\nPAGE_SIZE=20\n# 数据存放目录(数据库与 Excel 导出都在这里)\nDATA_DIR=data\n# 访问口令:留空表示不设限制;设置后所有页面需先输入口令\nACCESS_CODE=\n.gitignore\n.venv/\n__pycache__/\n.pytest_cache/\n.mypy_cache/\n.ruff_cache/\nlogs/\ndata/\ndist/\nbuild/\n*.spec\n.env\n.python-version\n3.12\nREADME.md\n# 施工进度台账在线填报(桥梁 / 隧道 / 路基)\n\n一个开箱即用的**施工进度台账在线填报系统**:电脑起服务,手机扫码/输网址即可填报,\n数据集中存 SQLite,随时按条件检索、修改、导出 Excel 台账。\n\n## 一、能做什么\n\n| 功能 | 说明 |\n| --- | --- |\n| 手机端填报 | 选择工程类别  分部分项  填写当日完成量、形象进度、质量安全情况,提交即可 |\n| 三大专业类别 | 桥梁、隧道、路基各自一套分部分项与专业字段(见下表) |\n| 台账检索 | 按类别、分部分项、日期区间、关键字(桩号/单位工程/填报人)筛选,分页查看 |\n| 编辑 / 删除 | 列表页与详情页均可修改,误填可删除 |\n| 累计工程量 | 系统按「类别 + 分项 + 单位工程 + 部位」自动滚动累加,不用手工算 |\n| 数据看板 | 今日/本月/累计条数、近 7 天趋势、分项排行、质量安全概览 |\n| Excel 导出 | 一键导出,含「汇总统计」+ 桥梁/隧道/路基三张明细表,可直接上报 |\n| 访问口令 | 可选,设置后需输口令才能查看和修改,防止无关人员误操作 |\n\n### 预置的专业字段\n\n| 类别 | 分部分项(节选) | 专业字段 |\n| --- | --- | --- |\n| 桥梁工程 | 桩基、承台/系梁、墩台身、盖梁、支座垫石、梁体预制、梁体架设、桥面铺装、防撞护栏、伸缩缝… | 墩台编号、设计桩长、成孔深度、混凝土强度等级、浇筑方量、钢筋用量、张拉压浆、模板投入 |\n| 隧道工程 | 洞口明洞、超前地质预报、超前支护、洞身开挖、初期支护、仰拱填充、二次衬砌、防排水、监控量测… | 掌子面桩号、围岩级别、开挖方法、循环进尺、当日进尺、累计进尺、喷射混凝土方量、钢拱架榀数、二衬长度 |\n| 路基工程 | 清表、挖方、填方、土工格栅、软基处理、台背回填、排水、防护、挡土墙、涵洞、边坡绿化… | 起止桩号、填挖类型、层位、松铺/压实厚度、压实度、含水量、填料类型、填筑方量、处理面积 |\n\n> 想增删分项或字段,改 `src/construction_ledger/catalog.py` 一个文件即可,页面和 Excel 会自动跟着变。\n\n## 二、怎么启动\n\n**环境要求**:Python 3.12(当前项目已用官方发行的 Python 3.12.10 建好虚拟环境)。\n\n在 `construction-ledger` 目录下执行:\n\n```powershell\nuv run python -m construction_ledger.main\n启动后会打印:\n\n本机访问:http://127.0.0.1:8000(会自动打开浏览器)\n手机访问:http://192.168.x.x:8000(手机与电脑连同一个 WiFi 即可,无需微信/App)\n数据文件、导出目录、日志文件的位置\n停止服务:在窗口按 Ctrl + C。\n\n换端口:把 .env.example 复制成 .env,改 PORT=9000 即可;想加访问口令就填 ACCESS_CODE=你的口令。\n\n三、目录结构\nsrc/construction_ledger/\n main.py              启动入口\n config.py            配置读取(.env)\n db.py                数据库连接与建表\n models.py            台账数据模型\n catalog.py           三大类别的分部分项与字段定义(要改字段改这里)\n ssl_compat.py        OpenSSL 被禁用时的兼容处理\n services/\n    ledger_service.py  台账增删改查、累计量、看板统计\n    export_service.py  Excel 导出\n web/\n     app.py           页面与 JSON 接口\n     server.py        内置轻量 HTTP 服务(uvicorn 不可用时的兜底)\n     templates/       手机端页面\n     static/          样式\n数据默认放在 data/ledger.db,导出文件放在 data/exports/,日志放在 logs/(按天分文件,保留 30 天)。\n\n四、常见问题\n1. 手机打不开? 确认手机和电脑在同一个 WiFi;Windows 防火墙首次会弹窗,点「允许访问」。\n\n2. 想换端口 / 数据要放别的盘? 复制 .env.example 为 .env,改 PORT、DATA_DIR。\n\n3. 想备份台账? 直接复制 data/ledger.db 即可;也可以定期点「导出」存一份 Excel。\n\n4. 提示 uvicorn 不可用、改用内置 HTTP 服务? 说明这台电脑的终端管控策略禁用了 OpenSSL 等模块,程序会自动切换到内置标准库 HTTP 服务, 功能完全一致,只是不支持 HTTPS(内网填报本来也用 HTTP)。\n\n已实测:本机的 Smart App Control 会拦截无签名的 Python 扩展 DLL,请务必使用 python.org 的官方 Python, 不要使用未经签名的绿色版 Python。\n\n五、自测\nuv run python -m pytest -q\n\n---\n\n## `src/construction_ledger/__init__.py`\n\n```python\n\"\"\"项目包入口。\"\"\"\nsrc/construction_ledger/main.py\n\"\"\"施工进度台账在线填报系统启动入口。\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport socket\nimport webbrowser\n\nfrom construction_ledger.config import CONFIG\nfrom construction_ledger.db import init_db\nfrom construction_ledger.logging_setup import configure_logging\nfrom construction_ledger.ssl_compat import ensure_ssl_module\n\nLOGGER = logging.getLogger(__name__)\n\n\ndef get_lan_ip() -> str:\n    \"\"\"获取本机局域网 IP,方便手机同网访问。\"\"\"\n    try:\n        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:\n            sock.connect((\"8.8.8.8\", 80))\n            return sock.getsockname()[0]\n    except OSError:\n        return \"127.0.0.1\"\n\n\ndef pick_server() -> str:\n    \"\"\"优先使用 uvicorn;受管控终端上回退到标准库服务器。\"\"\"\n    try:\n        import uvicorn  # noqa: F401\n\n        return \"uvicorn\"\n    except Exception as exc:  # noqa: BLE001\n        LOGGER.warning(\"uvicorn 不可用(%s),改用内置标准库 HTTP 服务\", exc)\n        return \"stdlib\"\n\n\ndef main() -> None:\n    log_file = configure_logging()\n    ensure_ssl_module()\n    init_db()\n\n    lan_ip = get_lan_ip()\n    print(\"=\" * 58)\n    print(f\"  {CONFIG.app_name}\")\n    print(\"=\" * 58)\n    print(f\"  本机访问:  http://127.0.0.1:{CONFIG.port}\")\n    print(f\"  手机访问:  http://{lan_ip}:{CONFIG.port}  (需与电脑同一 WiFi)\")\n    print(f\"  数据文件:  {CONFIG.database_path}\")\n    print(f\"  导出目录:  {CONFIG.export_dir}\")\n    print(f\"  日志文件:  {log_file}\")\n    print(\"  停止服务:  在此窗口按 Ctrl + C\")\n    print(\"=\" * 58)\n\n    from construction_ledger.web.app import app\n\n    try:\n        webbrowser.open(f\"http://127.0.0.1:{CONFIG.port}\")\n    except Exception:  # noqa: BLE001 打开浏览器失败不影响服务\n        LOGGER.debug(\"自动打开浏览器失败,可手动访问上述地址\")\n\n    if pick_server() == \"uvicorn\":\n        import uvicorn\n\n        uvicorn.run(\n            app,\n            host=CONFIG.host,\n            port=CONFIG.port,\n            log_level=CONFIG.log_level.lower(),\n            access_log=False,\n        )\n    else:\n        from construction_ledger.web.server import run_stdlib_server\n\n        run_stdlib_server(app, CONFIG.host, CONFIG.port)\n\n\nif __name__ == \"__main__\":\n    main()\nsrc/__init__.py\n\"\"\"src 包入口。\"\"\"\nsrc/construction_ledger/catalog.py\n\"\"\"工程类别、分部分项与台账字段定义。\n\n三大类别:桥梁工程、隧道工程、路基工程。\n每个类别预置常用分部分项和专业化扩展字段,后续只需改本文件即可调整。\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True, slots=True)\nclass FieldSpec:\n    \"\"\"单个填报字段的描述。\"\"\"\n\n    key: str\n    label: str\n    ftype: str = \"text\"  # text / number / textarea / select\n    unit: str = \"\"\n    options: tuple[str, ...] = ()\n    required: bool = False\n    placeholder: str = \"\"\n\n\n@dataclass(frozen=True, slots=True)\nclass CategorySpec:\n    \"\"\"一个工程类别的完整定义。\"\"\"\n\n    key: str\n    name: str\n    short: str\n    sub_items: tuple[str, ...]\n    fields: tuple[FieldSpec, ...]\n    default_unit: str = \"\"\n\n\nCOMMON_FIELDS: tuple[FieldSpec, ...] = (\n    FieldSpec(\"unit_project\", \"单位/分部工程名称\", required=True, placeholder=\"如:XX 大桥下部结构\"),\n    FieldSpec(\"location\", \"桩号 / 部位\", required=True, placeholder=\"如:K12+300 左幅 3# 墩\"),\n    FieldSpec(\"quantity\", \"当日完成工程量\", ftype=\"number\", required=True),\n    FieldSpec(\"unit\", \"计量单位\", required=True, placeholder=\"如:m / m3 / t / 根 / 榀\"),\n    FieldSpec(\n        \"progress_status\",\n        \"形象进度\",\n        ftype=\"select\",\n        required=True,\n        options=(\"未开始\", \"进行中\", \"已完成\", \"暂停施工\"),\n    ),\n    FieldSpec(\"progress_desc\", \"形象进度描述\", ftype=\"textarea\", placeholder=\"简述当日完成的形象部位\"),\n    FieldSpec(\"workers\", \"投入人员\", ftype=\"number\", unit=\"人\"),\n    FieldSpec(\"equipment\", \"主要机械设备\", placeholder=\"如:旋挖钻 1 台、吊车 2 台\"),\n    FieldSpec(\n        \"weather\",\n        \"天气情况\",\n        ftype=\"select\",\n        options=(\"晴\", \"阴\", \"小雨\", \"中雨\", \"大雨\", \"雪\", \"大风\", \"高温\", \"其他\"),\n    ),\n    FieldSpec(\n        \"quality_status\",\n        \"质量情况\",\n        ftype=\"select\",\n        options=(\"合格\", \"一般\", \"整改中\", \"不合格\", \"停工待检\"),\n    ),\n    FieldSpec(\"quality_note\", \"质量说明\", ftype=\"textarea\"),\n    FieldSpec(\n        \"safety_status\",\n        \"安全情况\",\n        ftype=\"select\",\n        options=(\"正常\", \"一般隐患\", \"隐患整改中\", \"停工整改\", \"发生事故\"),\n    ),\n    FieldSpec(\"safety_note\", \"安全说明\", ftype=\"textarea\"),\n    FieldSpec(\"problem\", \"存在问题及处理措施\", ftype=\"textarea\"),\n    FieldSpec(\"team\", \"施工班组\"),\n    FieldSpec(\"reporter\", \"填报人\", required=True),\n)\n\nBRIDGE = CategorySpec(\n    key=\"bridge\",\n    name=\"桥梁工程\",\n    short=\"桥梁\",\n    default_unit=\"m3\",\n    sub_items=(\n        \"桩基(钻孔灌注桩)\",\n        \"承台 / 系梁\",\n        \"墩台身\",\n        \"盖梁 / 台帽\",\n        \"支座垫石及支座安装\",\n        \"梁体预制\",\n        \"梁体架设\",\n        \"湿接缝 / 体系转换\",\n        \"桥面铺装\",\n        \"防撞护栏\",\n        \"伸缩缝\",\n        \"桥头搭板 / 锥坡\",\n        \"其他\",\n    ),\n    fields=(\n        FieldSpec(\"pier_no\", \"墩台编号 / 构件编号\", placeholder=\"如:3# 墩、左幅 2-1# 梁\"),\n        FieldSpec(\"pile_length\", \"设计桩长\", ftype=\"number\", unit=\"m\"),\n        FieldSpec(\"hole_depth\", \"本次成孔 / 施工深度\", ftype=\"number\", unit=\"m\"),\n        FieldSpec(\n            \"concrete_grade\",\n            \"混凝土强度等级\",\n            ftype=\"select\",\n            options=(\"C25\", \"C30\", \"C35\", \"C40\", \"C45\", \"C50\", \"C55\", \"C60\"),\n        ),\n        FieldSpec(\"concrete_volume\", \"混凝土浇筑方量\", ftype=\"number\", unit=\"m3\"),\n        FieldSpec(\"rebar_weight\", \"钢筋用量\", ftype=\"number\", unit=\"t\"),\n        FieldSpec(\"prestress\", \"张拉 / 压浆情况\"),\n        FieldSpec(\"formwork\", \"模板投入\", ftype=\"number\", unit=\"套\"),\n    ),\n)\n\nTUNNEL = CategorySpec(\n    key=\"tunnel\",\n    name=\"隧道工程\",\n    short=\"隧道\",\n    default_unit=\"m\",\n    sub_items=(\n        \"洞口及明洞工程\",\n        \"超前地质预报\",\n        \"超前支护(管棚 / 小导管)\",\n        \"洞身开挖\",\n        \"初期支护\",\n        \"仰拱及填充\",\n        \"二次衬砌\",\n        \"防排水工程\",\n        \"隧道路面 / 基层\",\n        \"机电预埋及附属\",\n        \"监控量测\",\n        \"其他\",\n    ),\n    fields=(\n        FieldSpec(\"face_chainage\", \"掌子面 / 施工桩号\", placeholder=\"如:YK12+345\"),\n        FieldSpec(\n            \"rock_grade\",\n            \"围岩级别\",\n            ftype=\"select\",\n            options=(\"I\", \"II\", \"III\", \"IV\", \"V\", \"VI\"),\n        ),\n        FieldSpec(\n            \"excavate_method\",\n            \"开挖方法\",\n            ftype=\"select\",\n            options=(\n                \"全断面法\",\n                \"台阶法\",\n                \"环形开挖预留核心土法\",\n                \"中隔壁法(CD)\",\n                \"交叉中隔壁法(CRD)\",\n                \"双侧壁导坑法\",\n            ),\n        ),\n        FieldSpec(\"cycle_advance\", \"循环进尺\", ftype=\"number\", unit=\"m\"),\n        FieldSpec(\"day_advance\", \"当日进尺\", ftype=\"number\", unit=\"m\"),\n        FieldSpec(\"total_advance\", \"累计进尺\", ftype=\"number\", unit=\"m\"),\n        FieldSpec(\"shotcrete_volume\", \"喷射混凝土方量\", ftype=\"number\", unit=\"m3\"),\n        FieldSpec(\"steel_arch\", \"钢拱架 / 格栅\", ftype=\"number\", unit=\"榀\"),\n        FieldSpec(\"lining_length\", \"二衬浇筑长度\", ftype=\"number\", unit=\"m\"),\n    ),\n)\n\nSUBGRADE = CategorySpec(\n    key=\"subgrade\",\n    name=\"路基工程\",\n    short=\"路基\",\n    default_unit=\"m3\",\n    sub_items=(\n        \"清表与场地清理\",\n        \"挖方(路堑开挖)\",\n        \"填方(路堤填筑)\",\n        \"土工格栅 / 加筋材料\",\n        \"软基处理\",\n        \"台背 / 涵背回填\",\n        \"排水工程(边沟、截水沟)\",\n        \"防护工程(骨架、喷锚)\",\n        \"挡土墙\",\n        \"涵洞 / 通道\",\n        \"边坡绿化\",\n        \"其他\",\n    ),\n    fields=(\n        FieldSpec(\"start_chainage\", \"起始桩号\", placeholder=\"如:K12+300\"),\n        FieldSpec(\"end_chainage\", \"终止桩号\", placeholder=\"如:K12+500\"),\n        FieldSpec(\n            \"fill_type\",\n            \"填挖类型\",\n            ftype=\"select\",\n            options=(\"填方\", \"挖方\", \"半填半挖\"),\n        ),\n        FieldSpec(\"layer_no\", \"层位 / 层数\"),\n        FieldSpec(\"loose_thickness\", \"松铺厚度\", ftype=\"number\", unit=\"cm\"),\n        FieldSpec(\"compact_thickness\", \"压实厚度\", ftype=\"number\", unit=\"cm\"),\n        FieldSpec(\"compaction\", \"压实度\", ftype=\"number\", unit=\"%\"),\n        FieldSpec(\"moisture\", \"含水量\", ftype=\"number\", unit=\"%\"),\n        FieldSpec(\"material\", \"填料类型\", placeholder=\"如:碎石土、砂砾、石灰土\"),\n        FieldSpec(\"fill_volume\", \"填筑 / 开挖方量\", ftype=\"number\", unit=\"m3\"),\n        FieldSpec(\"treat_area\", \"处理面积\", ftype=\"number\", unit=\"m2\"),\n    ),\n)\n\nCATEGORIES: dict[str, CategorySpec] = {\n    BRIDGE.key: BRIDGE,\n    TUNNEL.key: TUNNEL,\n    SUBGRADE.key: SUBGRADE,\n}\n\nCATEGORY_ORDER: tuple[str, ...] = (BRIDGE.key, TUNNEL.key, SUBGRADE.key)\n\n\ndef get_category(key: str) -> CategorySpec:\n    \"\"\"按 key 取类别定义,未知 key 回落为路基工程。\"\"\"\n    return CATEGORIES.get(key, SUBGRADE)\n\n\ndef category_name(key: str) -> str:\n    return get_category(key).name\n\n\ndef catalog_payload() -> dict[str, object]:\n    \"\"\"给前端动态渲染表单用的目录数据。\"\"\"\n\n    def dump(field: FieldSpec) -> dict[str, object]:\n        return {\n            \"key\": field.key,\n            \"label\": field.label,\n            \"type\": field.ftype,\n            \"unit\": field.unit,\n            \"options\": list(field.options),\n            \"required\": field.required,\n            \"placeholder\": field.placeholder,\n        }\n\n    return {\n        \"order\": list(CATEGORY_ORDER),\n        \"commonFields\": [dump(f) for f in COMMON_FIELDS],\n        \"categories\": {\n            key: {\n                \"key\": spec.key,\n                \"name\": spec.name,\n                \"short\": spec.short,\n                \"defaultUnit\": spec.default_unit,\n                \"subItems\": list(spec.sub_items),\n                \"fields\": [dump(f) for f in spec.fields],\n            }\n            for key, spec in CATEGORIES.items()\n        },\n    }\nsrc/construction_ledger/config.py\n\"\"\"应用配置读取。\n\n真实配置放 `.env`,仓库只保留 `.env.example`。\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\nfrom dotenv import load_dotenv\n\n\ndef _get_runtime_root() -> Path:\n    if getattr(sys, \"frozen\", False):\n        return Path(sys.executable).resolve().parent\n    return Path(__file__).resolve().parents[2]\n\n\ndef _load_runtime_env() -> None:\n    runtime_root = _get_runtime_root()\n    for candidate in (runtime_root / \".env\", runtime_root / \".env.example\"):\n        if candidate.exists():\n            load_dotenv(candidate, override=False)\n            break\n\n\n_load_runtime_env()\n\n\ndef _default_data_dir() -> Path:\n    return _get_runtime_root() / \"data\"\n\n\n@dataclass(slots=True)\nclass AppConfig:\n    app_name: str = os.getenv(\"APP_NAME\", \"施工进度台账在线填报\")\n    host: str = os.getenv(\"HOST\", \"0.0.0.0\")\n    port: int = int(os.getenv(\"PORT\", \"8000\"))\n    log_level: str = os.getenv(\"LOG_LEVEL\", \"INFO\")\n    page_size: int = int(os.getenv(\"PAGE_SIZE\", \"20\"))\n    data_dir: Path = field(default_factory=_default_data_dir)\n    access_code: str = os.getenv(\"ACCESS_CODE\", \"\")\n\n    @property\n    def database_path(self) -> Path:\n        self.data_dir.mkdir(parents=True, exist_ok=True)\n        return self.data_dir / \"ledger.db\"\n\n    @property\n    def export_dir(self) -> Path:\n        target = self.data_dir / \"exports\"\n        target.mkdir(parents=True, exist_ok=True)\n        return target\n\n\nCONFIG = AppConfig()\nsrc/construction_ledger/db.py\n\"\"\"数据库连接与初始化。\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Iterator\nfrom pathlib import Path\n\nfrom sqlalchemy import event\nfrom sqlmodel import Session, SQLModel, create_engine\n\nfrom construction_ledger.config import CONFIG\n\nLOGGER = logging.getLogger(__name__)\n\n_engine = None\n\n\ndef get_database_url(db_path: Path | None = None) -> str:\n    path = db_path or CONFIG.database_path\n    return f\"sqlite:///{path.as_posix()}\"\n\n\ndef get_engine(db_path: Path | None = None):\n    \"\"\"获取全局数据库引擎(单例)。\"\"\"\n    global _engine\n    if db_path is not None:\n        return create_engine(get_database_url(db_path), echo=False, connect_args={\"check_same_thread\": False})\n    if _engine is None:\n        url = get_database_url()\n        _engine = create_engine(url, echo=False, connect_args={\"check_same_thread\": False})\n\n        @event.listens_for(_engine, \"connect\")\n        def _set_sqlite_pragma(dbapi_connection, _connection_record) -> None:  # type: ignore[no-untyped-def]\n            cursor = dbapi_connection.cursor()\n            cursor.execute(\"PRAGMA foreign_keys=ON\")\n            cursor.close()\n\n        LOGGER.info(\"数据库引擎初始化完成:%s\", url)\n    return _engine\n\n\ndef init_db(db_path: Path | None = None) -> None:\n    \"\"\"建表(幂等)。\"\"\"\n    import construction_ledger.models as models  # noqa: F401  确保表已注册\n\n    engine = get_engine(db_path)\n    SQLModel.metadata.create_all(engine)\n    LOGGER.info(\"数据库表结构已就绪\")\n\n\ndef get_session() -> Iterator[Session]:\n    \"\"\"FastAPI 依赖用的会话生成器。\"\"\"\n    with Session(get_engine()) as session:\n        yield session\nsrc/construction_ledger/logging_setup.py\nfrom __future__ import annotations\n\nfrom datetime import datetime, timedelta\nimport logging\nfrom pathlib import Path\nimport os\nimport subprocess\nimport sys\n\nfrom dotenv import load_dotenv\n\n\n_LOG_FILE_PREFIX = \"app\"\n_MAX_LOG_BYTES = 5 * 1024 * 1024\n_RETENTION_DAYS = 30\n\n\nclass DateSizedFileHandler(logging.Handler):\n    def __init__(\n        self,\n        log_dir: Path,\n        *,\n        prefix: str = _LOG_FILE_PREFIX,\n        max_bytes: int = _MAX_LOG_BYTES,\n        retention_days: int = _RETENTION_DAYS,\n        encoding: str = \"utf-8\",\n    ) -> None:\n        super().__init__()\n        self.log_dir = log_dir\n        self.prefix = prefix\n        self.max_bytes = max_bytes\n        self.retention_days = retention_days\n        self.encoding = encoding\n        self.log_dir.mkdir(parents=True, exist_ok=True)\n\n    def get_current_log_file(self, message: str = \"\") -> Path:\n        date_part = datetime.now().strftime(\"%Y-%m-%d\")\n        message_size = len((message + \"\\n\").encode(self.encoding)) if message else 0\n        index = 0\n\n        while True:\n            suffix = \"\" if index == 0 else f\"-{index}\"\n            log_file = self.log_dir / f\"{self.prefix}-{date_part}{suffix}.log\"\n            if not log_file.exists():\n                return log_file\n            if log_file.stat().st_size + message_size \u003c= self.max_bytes:\n                return log_file\n            index += 1\n\n    def cleanup_old_logs(self) -> None:\n        cutoff = datetime.now() - timedelta(days=self.retention_days)\n        for log_file in self.log_dir.glob(f\"{self.prefix}-*.log\"):\n            try:\n                modified = datetime.fromtimestamp(log_file.stat().st_mtime)\n            except OSError:\n                continue\n            if modified \u003c cutoff:\n                try:\n                    log_file.unlink()\n                except OSError:\n                    continue\n\n    def emit(self, record: logging.LogRecord) -> None:\n        try:\n            message = self.format(record)\n            log_file = self.get_current_log_file(message)\n            with log_file.open(\"a\", encoding=self.encoding) as handle:\n                handle.write(message + \"\\n\")\n        except Exception:\n            self.handleError(record)\n\n\ndef get_runtime_root() -> Path:\n    if getattr(sys, \"frozen\", False):\n        return Path(sys.executable).resolve().parent\n    return Path(__file__).resolve().parents[2]\n\n\ndef load_runtime_env() -> Path | None:\n    runtime_root = get_runtime_root()\n    for candidate in (runtime_root / \".env\", runtime_root / \".env.example\"):\n        if candidate.exists():\n            load_dotenv(candidate, override=False)\n            return candidate\n    return None\n\n\ndef get_log_dir() -> Path:\n    log_dir = get_runtime_root() / \"logs\"\n    log_dir.mkdir(parents=True, exist_ok=True)\n    return log_dir\n\n\ndef get_log_file() -> Path:\n    handler = DateSizedFileHandler(get_log_dir())\n    return handler.get_current_log_file()\n\n\ndef configure_logging() -> Path:\n    env_file = load_runtime_env()\n    logger = logging.getLogger()\n    if logger.handlers:\n        return get_log_file()\n\n    log_level_name = os.getenv(\"LOG_LEVEL\", \"INFO\").upper()\n    log_level = getattr(logging, log_l"</script>