11 KiB
PRD: App Factory + Config 统一重构(PR1)
背景
当前 ephron.ren 已经从单一 MVP 演进成多服务单仓结构,包含:
authblogcanvashomepromptshared
但服务初始化与配置层仍保留大量早期复制实现,主要集中在:
- 五个服务的
src/main.py - 五个服务的
src/config.py
这导致后续每次要统一安全头、health、docs、错误处理、配置约定时,都需要多处重复修改,并且很容易发生服务之间的行为漂移。
本 PRD 定义第一阶段重构:只收敛 app 初始化骨架与 config 读取骨架,不改业务逻辑。
问题定义
1. main.py 高度重复
五个服务都重复做了以下工作:
- 调用
validate_config() - 创建
FastAPI(...) - 安装
security headers - 创建并挂载
limiter - 挂载
static - 注册 routers
- 注册 404/500 handler
- 提供
/health
这种重复意味着:
- 加一个通用能力要改 5 份
- 某个服务漏改的概率很高
- 测试也更难形成统一契约
2. config.py 既重复又不一致
当前配置层存在两个问题:
重复
各服务都各自实现了:
.env加载- 必填/可选环境变量读取
- config summary 输出
- 环境模式判断
不一致
home使用ENVIRONMENT- 其他服务使用
ENV
这会提高部署、调试和文档维护成本。
3. 当前 shared 层只抽了一半
项目已经有这些共享能力:
shared/security_headers.pyshared/limiter.pyshared/health.pyshared/templating.pyshared/ports.py
说明项目已经具备共享层思路,但还没有把最核心的 app/config 装配流程统一起来。
本次目标
本 PR1 完成后,应该达到:
-
五个服务
main.py只保留:- service 专属 lifespan
- router 列表
- service 元信息
- 调用统一 app factory
-
五个服务
config.py改为复用共享 helper:.env加载- required/optional env 读取
- 环境模式判定
- summary 输出
-
ENV成为统一主环境变量,home暂时兼容ENVIRONMENT -
不改变外部行为:
- URL 不变
/health响应结构不变- 404/500 页面行为兼容
- dev 模式 docs_url 保持可用
非目标
这次不做:
- 不重做 service API
- 不收敛数据库 migration 归属
- 不彻底消灭所有
sys.path.insert - 不迁移目录结构
- 不改业务权限与业务逻辑
这些属于后续 PR。
方案设计
一、共享 app factory
新增:
shared/app_factory.py
职责:
- 创建标准化
FastAPIapp - 安装安全头
- 安装 limiter
- 挂载静态目录
- 注册 routers
- 注册默认错误处理
- 注册
/health
建议接口:
def create_service_app(
*,
service_name: str,
title: str,
description: str,
version: str,
is_development: bool,
templates_dir,
routers: list,
static_dir=None,
lifespan=None,
):
...
兼容要求
service_name用于 health 响应,例如blog.ephron.renstatic_dir=None时跳过 static 挂载canvas当前是“目录存在才挂载”,新实现必须兼容
二、共享错误处理安装器
新增:
shared/error_handlers.py
职责:
- 安装 404/500 handler
/api/...返回 JSON- 页面路径返回模板
- production 不暴露原始异常文本
- development 可注入错误信息方便调试
建议接口:
def install_default_error_handlers(app, *, templates_dir, is_development): ...
兼容要求
- API 404 继续返回
{"detail": "Not Found"} - 页面 404/500 继续使用各服务自己模板目录中的
404.html/500.html
三、共享 config helper
新增:
shared/config_base.py
职责:
- 加载服务目录下
.env - 提供 required/optional env 读取 helper
- 统一环境模式判定
- 统一 config summary 输出
建议接口:
from pathlib import Path
def load_service_env(service_root: Path) -> None: ...
def get_required_env(key: str) -> str: ...
def get_optional_env(key: str, default: str) -> str: ...
def resolve_environment(*, legacy_key: str | None = None) -> tuple[str, bool]: ...
def print_config_summary(service_name: str, items: dict[str, str]) -> None: ...
兼容要求
resolve_environment(legacy_key="ENVIRONMENT")在ENV缺失时回退旧字段- 若
ENV和ENVIRONMENT同时存在,以ENV为准
具体实施步骤
Step 1:先补 shared 层单元测试
新增:
tests/test_shared_config_base.pytests/test_error_handlers.pytests/test_app_factory.py
覆盖这些能力:
test_shared_config_base.py
- 能加载指定服务目录
.env - required env 缺失时报清晰错误
ENV优先于ENVIRONMENT- summary 输出格式稳定
test_error_handlers.py
/api/...404 返回 JSON- 页面 404 返回模板
- production 500 不暴露原始异常文本
- development 500 可显示
error_message
test_app_factory.py
- 自动安装安全头
- 自动安装 limiter
- 自动挂 routers
static_dir存在时挂载/staticstatic_dir=None时不挂载- 自动提供
/health - dev 模式 docs_url 存在,prod 模式关闭
Step 2:实现 shared/config_base.py
约束:
- 共享层只处理“如何读配置”,不处理每个服务自己的业务字段
- 不在这里写数据库或业务逻辑
Step 3:实现 shared/error_handlers.py
要求:
- 内部复用
shared.templating.create_templates - 用 path 前缀判断 API/页面行为
- production 不直接暴露
str(exc)
Step 4:实现 shared/app_factory.py
应复用:
shared.security_headers.install_security_headersshared.limiter.create_limitershared.error_handlers.install_default_error_handlersshared.health.build_health_response
不要做的事:
- 不在 factory 中写数据库初始化
- 不在 factory 中塞业务判断
factory 只负责通用 app 壳。
Step 5:改造五个服务 config.py
涉及:
auth/src/config.pyblog/src/config.pycanvas/src/config.pyhome/src/config.pyprompt/src/config.py
逐服务注意点
Auth
保留:
AUTH_SECRET_KEYDATABASE_PATHCOOKIE_DOMAINCOOKIE_NAMETOKEN_MAX_AGETEMPLATES_DIR
要求:
validate_config()不变- summary 输出语义基本不变
Blog
保留:
CONTENT_DIR校验CACHE_DIR.mkdir(...)SEARCH_INDEX_DIR.mkdir(...)
Canvas
保留:
CONTENT_DIR不存在时自动创建
不能把当前“自动建目录”改成“配置错误退出”。
Prompt
保留:
DATABASE_PATHTOKEN_MAX_AGE- 当前 summary 语义
Home
重点:
- 进入统一 config helper 体系
- 兼容
ENVIRONMENT AUTH_BASE_URL / AUTH_LOGIN_URL行为不变
必须验证:
- 仅设置
ENVIRONMENT=development时仍能进入开发模式 - 若同时设置
ENV=production和ENVIRONMENT=development,以ENV为准
Step 6:改造五个服务 main.py
涉及:
auth/src/main.pyblog/src/main.pycanvas/src/main.pyhome/src/main.pyprompt/src/main.py
目标:
- 删除重复 app 初始化逻辑
- 改为调用
shared.app_factory.create_service_app(...)
每个服务保留:
- service 专属 lifespan
- router 列表
- service 元信息
特别注意:Home 的 /health
home/src/routes/pages.py 当前也定义了 /health。
这次应统一只保留一处,建议:
- 删除
pages.py中的/health - 统一由 app factory 提供
否则容易出现重复注册或行为漂移。
Step 7:调整回归测试
建议检查并更新:
tests/test_security_hardening.pytests/test_frontend_backend_reuse_contract.py
新增建议断言:
- 所有服务继续使用 shared template factory
- 所有服务通过统一 app factory 提供 health
- 所有服务继续保留安全头
- home 环境兼容逻辑同时覆盖
ENV与ENVIRONMENT
Step 8:完整验证
建议执行:
python -m pytest tests -q
python -m pytest auth/tests -q
python -m pytest blog/tests -q
python -m pytest canvas/tests -q
python -m pytest home/tests -q
python -m pytest prompt/tests -q
如本地可启动,再手动验证:
python main.py --reload
检查:
- 五个服务均能启动
/health正常- API 不存在路径返回 JSON 404
- 页面不存在路径返回各自 404 模板
预计改动文件
新增
shared/config_base.pyshared/error_handlers.pyshared/app_factory.pytests/test_shared_config_base.pytests/test_error_handlers.pytests/test_app_factory.py
修改
auth/src/config.pyblog/src/config.pycanvas/src/config.pyhome/src/config.pyprompt/src/config.pyauth/src/main.pyblog/src/main.pycanvas/src/main.pyhome/src/main.pyprompt/src/main.pyhome/src/routes/pages.py(删除重复/health)tests/test_security_hardening.pytests/test_frontend_backend_reuse_contract.py
风险与注意事项
1. Home 环境变量兼容风险
历史部署如果只写了 ENVIRONMENT,不能因为统一而直接失效。
2. 404/500 行为变化风险
抽象错误处理时,很容易把 API 路径判断或模板上下文改坏,必须用单元测试兜住。
3. Canvas static 行为变化风险
当前是“存在才挂载”,不能在 app factory 中粗暴统一成“总是挂载”。
4. 测试依赖旧实现细节
如果某些测试直接断言 main.py 中出现某段文本,改造后会失效,需要调整为验证行为而不是验证源码写法。
建议提交拆分
建议拆成 5 个 commit,便于实现与 review:
test: add shared config and app factory coveragefeat: add shared config basefeat: add shared error handlers and app factoryrefactor: migrate service config modules to shared helpersrefactor: migrate service main modules to shared app factory
完成定义
本 PR1 完成后,必须满足:
- 五个服务
main.py不再手写重复 app 装配 - 五个服务
config.py使用共享 helper ENV成为统一主环境变量,home 兼容ENVIRONMENT/health外部行为不变- 404/500 行为兼容
- 全部现有测试通过,新增 shared 测试通过
- 没有新增
sys.path.insert使用点
后续建议
PR1 合并后,建议继续:
- PR2:抽
shared/service_api/*与tests/helpers/* - PR3:逐步收敛 import 结构,减少
src冲突与sys.path.insert - PR4:统一 DB schema / migration ownership
这样可以保证每一步都是建立在更稳定的基础上,而不是继续在重复结构上迭代。