Initial commit

This commit is contained in:
Misaka_Company
2026-07-30 14:17:18 +08:00
commit 4dc563cdd9
16 changed files with 1144 additions and 0 deletions

1
core/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""core 包:配置、数据库访问、字体。"""

65
core/db.py Normal file
View File

@@ -0,0 +1,65 @@
"""数据库访问层。
提供 run_query读取 .sql 文件、剥离注释、参数化绑定执行、返回 list[dict]。
所有报表查询必须走参数绑定,禁止字符串拼接 SQL防注入
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from core.settings import settings
_engine: Engine | None = None
def get_engine() -> Engine:
"""惰性创建并缓存 SQLAlchemy engine。"""
global _engine
if _engine is None:
_engine = create_engine(
settings.database_url,
pool_pre_ping=True,
pool_recycle=1800,
)
return _engine
def load_sql(sql_path: str | Path) -> str:
"""读取 .sql 文件并剥离行级 `--` 注释,返回纯 SQL 文本。
支持 SQL 文件里自由书写中文注释(便于阅读与 AI 理解)。
仅剥离以 `--` 开头的整行注释(行首可有空白),不处理行内注释,
避免误删含 `--` 的字符串字面量。
"""
path = Path(sql_path)
if not path.is_absolute():
from core.settings import PROJECT_ROOT
path = PROJECT_ROOT / path
raw = path.read_text(encoding="utf-8")
lines = []
for line in raw.splitlines():
# 去掉行首空白后判断是否为注释行
if re.match(r"\s*--", line):
continue
lines.append(line)
return "\n".join(lines).strip()
def run_query(sql_path: str | Path, params: dict[str, Any]) -> list[dict[str, Any]]:
"""执行参数化查询,返回 list[dict](每行一个 dict键为列别名
:param sql_path: .sql 文件路径(相对项目根或绝对路径)。
:param params: 绑定参数,键名对应 SQL 中的 :name 占位符。
"""
sql_text = load_sql(sql_path)
engine = get_engine()
with engine.connect() as conn:
result = conn.execute(text(sql_text), params)
cols = list(result.keys())
return [dict(zip(cols, row)) for row in result.fetchall()]

46
core/fonts.py Normal file
View File

@@ -0,0 +1,46 @@
"""中文字体配置。
reportbro-lib 的核心字体helvetica 等)无法编码 CJK需通过 additional_fonts
注册支持中文的 TrueType 字体。本机使用 Windows 自带的 simhei.ttf黑体
模板里所有样式的 font 字段必须设为这里的 value"simhei")。
若将来跨机器部署,将字体文件放入 assets/ 并改为相对路径。
"""
import os
# 模板中引用的字体名小写reportbro 内部按小写存储)
CJK_FONT_NAME = "simhei"
# 默认字体文件路径Windows 系统字体目录)
_DEFAULT_FONT_PATH = r"C:/Windows/Fonts/simhei.ttf"
_BAHNSCHRIFT_PATH = r"C:/Windows/Fonts/bahnschrift.ttf"
def additional_fonts() -> list[dict]:
"""返回 reportbro Report(additional_fonts=...) 所需的字体清单。
注册两类字体:
- simhei中文CJK主字体
- bahnschrift西文等宽风格字体用于装箱单信息条排产号/订单号等编号)
若环境变量 REPORT_CJK_FONT 指定了备用 ttf 路径,则改用该路径。
simhei 无独立 bold/italic 文件,四种字形全部映射到同一个 ttf。
"""
font_path = os.environ.get("REPORT_CJK_FONT", _DEFAULT_FONT_PATH)
if not os.path.exists(font_path):
raise FileNotFoundError(
f"中文字体文件不存在: {font_path}\n"
f"请安装 simhei.ttf或通过环境变量 REPORT_CJK_FONT 指定其它支持中文的 ttf 路径。"
)
fonts = [
{
"value": CJK_FONT_NAME,
"filename": font_path,
# bold_filename / italic_filename / bold_italic_filename 省略,
# reportbro 会自动把所有字形映射到 filename见 FPDFRB 初始化)。
}
]
# Bahnschrift西文编号字体可选缺失不致命
bahn_path = os.environ.get("REPORT_LATIN_FONT", _BAHNSCHRIFT_PATH)
if os.path.exists(bahn_path):
fonts.append({"value": "bahnschrift", "filename": bahn_path})
return fonts

111
core/settings.py Normal file
View File

@@ -0,0 +1,111 @@
"""数据库配置加载。
与 services/fastapiCargoTrace 后端)的 config/settings.py 保持同构,
复用同一份 config/settings.yaml 凭据。
"""
from pathlib import Path
from typing import Literal
from pydantic import BaseModel
from pydantic_yaml import parse_yaml_raw_as
from sqlalchemy.engine import URL
class SqlServerConfig(BaseModel):
"""SQL Server 连接配置"""
host: str
port: int = 1433
database: str
username: str
password: str
driver: str = "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: str = "yes"
class PostgreSqlConfig(BaseModel):
"""PostgreSQL 连接配置"""
host: str
port: int = 5432
database: str
username: str
password: str
class DatabaseConfig(BaseModel):
"""数据库配置"""
active: Literal["sql_server", "postgresql"] = "sql_server"
sql_server: SqlServerConfig
postgresql: PostgreSqlConfig | None = None
class ReportConfig(BaseModel):
"""报表配置"""
# 调试开关True 时给所有文本元素加边框,便于核对每个元素的实际占位。
debug_border: bool = False
class Settings(BaseModel):
"""应用配置"""
database: DatabaseConfig
report: ReportConfig = ReportConfig()
@property
def database_url(self) -> URL:
"""构建数据库连接 URL"""
if self.database.active == "postgresql":
return self._postgresql_url()
return self._sql_server_url()
def _sql_server_url(self) -> URL:
conf = self.database.sql_server
return URL.create(
"mssql+pyodbc",
username=conf.username,
password=conf.password,
host=conf.host,
port=conf.port,
database=conf.database,
query={
"driver": conf.driver.strip("{}"),
"TrustServerCertificate": conf.trust_server_certificate,
},
)
def _postgresql_url(self) -> URL:
conf = self.database.postgresql
if conf is None:
raise ValueError("已选择 postgresql但未配置 database.postgresql")
return URL.create(
"postgresql+psycopg",
username=conf.username,
password=conf.password,
host=conf.host,
port=conf.port,
database=conf.database,
)
# 项目根目录core/settings.py 的 parent.parent
PROJECT_ROOT = Path(__file__).resolve().parent.parent
def load_settings(config_path: str = "config/settings.yaml") -> Settings:
"""加载 YAML 配置文件(相对路径基于项目根目录解析)。"""
path = Path(config_path)
if not path.is_absolute():
path = PROJECT_ROOT / config_path
if not path.exists():
raise FileNotFoundError(
f"配置文件不存在: {config_path}\n"
f"请复制 config/settings.example.yaml 为 config/settings.yaml 并填入凭据。"
)
with open(path, "r", encoding="utf-8") as f:
return parse_yaml_raw_as(Settings, f)
settings = load_settings()