docs: add PRD for app factory and config unification
This commit is contained in:
471
prd-app-factory-config-unification.md
Normal file
471
prd-app-factory-config-unification.md
Normal file
@@ -0,0 +1,471 @@
|
||||
# PRD: App Factory + Config 统一重构(PR1)
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `ephron.ren` 已经从单一 MVP 演进成多服务单仓结构,包含:
|
||||
|
||||
- `auth`
|
||||
- `blog`
|
||||
- `canvas`
|
||||
- `home`
|
||||
- `prompt`
|
||||
- `shared`
|
||||
|
||||
但服务初始化与配置层仍保留大量早期复制实现,主要集中在:
|
||||
|
||||
- 五个服务的 `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.py`
|
||||
- `shared/limiter.py`
|
||||
- `shared/health.py`
|
||||
- `shared/templating.py`
|
||||
- `shared/ports.py`
|
||||
|
||||
说明项目已经具备共享层思路,但还没有把最核心的 app/config 装配流程统一起来。
|
||||
|
||||
---
|
||||
|
||||
## 本次目标
|
||||
|
||||
本 PR1 完成后,应该达到:
|
||||
|
||||
1. 五个服务 `main.py` 只保留:
|
||||
- service 专属 lifespan
|
||||
- router 列表
|
||||
- service 元信息
|
||||
- 调用统一 app factory
|
||||
|
||||
2. 五个服务 `config.py` 改为复用共享 helper:
|
||||
- `.env` 加载
|
||||
- required/optional env 读取
|
||||
- 环境模式判定
|
||||
- summary 输出
|
||||
|
||||
3. `ENV` 成为统一主环境变量,`home` 暂时兼容 `ENVIRONMENT`
|
||||
|
||||
4. 不改变外部行为:
|
||||
- URL 不变
|
||||
- `/health` 响应结构不变
|
||||
- 404/500 页面行为兼容
|
||||
- dev 模式 docs_url 保持可用
|
||||
|
||||
---
|
||||
|
||||
## 非目标
|
||||
|
||||
这次**不做**:
|
||||
|
||||
- 不重做 service API
|
||||
- 不收敛数据库 migration 归属
|
||||
- 不彻底消灭所有 `sys.path.insert`
|
||||
- 不迁移目录结构
|
||||
- 不改业务权限与业务逻辑
|
||||
|
||||
这些属于后续 PR。
|
||||
|
||||
---
|
||||
|
||||
## 方案设计
|
||||
|
||||
## 一、共享 app factory
|
||||
|
||||
新增:
|
||||
|
||||
- `shared/app_factory.py`
|
||||
|
||||
职责:
|
||||
- 创建标准化 `FastAPI` app
|
||||
- 安装安全头
|
||||
- 安装 limiter
|
||||
- 挂载静态目录
|
||||
- 注册 routers
|
||||
- 注册默认错误处理
|
||||
- 注册 `/health`
|
||||
|
||||
建议接口:
|
||||
|
||||
```python
|
||||
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.ren`
|
||||
- `static_dir=None` 时跳过 static 挂载
|
||||
- `canvas` 当前是“目录存在才挂载”,新实现必须兼容
|
||||
|
||||
---
|
||||
|
||||
## 二、共享错误处理安装器
|
||||
|
||||
新增:
|
||||
|
||||
- `shared/error_handlers.py`
|
||||
|
||||
职责:
|
||||
- 安装 404/500 handler
|
||||
- `/api/...` 返回 JSON
|
||||
- 页面路径返回模板
|
||||
- production 不暴露原始异常文本
|
||||
- development 可注入错误信息方便调试
|
||||
|
||||
建议接口:
|
||||
|
||||
```python
|
||||
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 输出
|
||||
|
||||
建议接口:
|
||||
|
||||
```python
|
||||
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.py`
|
||||
- `tests/test_error_handlers.py`
|
||||
- `tests/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` 存在时挂载 `/static`
|
||||
- `static_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_headers`
|
||||
- `shared.limiter.create_limiter`
|
||||
- `shared.error_handlers.install_default_error_handlers`
|
||||
- `shared.health.build_health_response`
|
||||
|
||||
不要做的事:
|
||||
- 不在 factory 中写数据库初始化
|
||||
- 不在 factory 中塞业务判断
|
||||
|
||||
factory 只负责通用 app 壳。
|
||||
|
||||
---
|
||||
|
||||
## Step 5:改造五个服务 `config.py`
|
||||
涉及:
|
||||
- `auth/src/config.py`
|
||||
- `blog/src/config.py`
|
||||
- `canvas/src/config.py`
|
||||
- `home/src/config.py`
|
||||
- `prompt/src/config.py`
|
||||
|
||||
### 逐服务注意点
|
||||
|
||||
#### Auth
|
||||
保留:
|
||||
- `AUTH_SECRET_KEY`
|
||||
- `DATABASE_PATH`
|
||||
- `COOKIE_DOMAIN`
|
||||
- `COOKIE_NAME`
|
||||
- `TOKEN_MAX_AGE`
|
||||
- `TEMPLATES_DIR`
|
||||
|
||||
要求:
|
||||
- `validate_config()` 不变
|
||||
- summary 输出语义基本不变
|
||||
|
||||
#### Blog
|
||||
保留:
|
||||
- `CONTENT_DIR` 校验
|
||||
- `CACHE_DIR.mkdir(...)`
|
||||
- `SEARCH_INDEX_DIR.mkdir(...)`
|
||||
|
||||
#### Canvas
|
||||
保留:
|
||||
- `CONTENT_DIR` 不存在时自动创建
|
||||
|
||||
不能把当前“自动建目录”改成“配置错误退出”。
|
||||
|
||||
#### Prompt
|
||||
保留:
|
||||
- `DATABASE_PATH`
|
||||
- `TOKEN_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.py`
|
||||
- `blog/src/main.py`
|
||||
- `canvas/src/main.py`
|
||||
- `home/src/main.py`
|
||||
- `prompt/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.py`
|
||||
- `tests/test_frontend_backend_reuse_contract.py`
|
||||
|
||||
新增建议断言:
|
||||
- 所有服务继续使用 shared template factory
|
||||
- 所有服务通过统一 app factory 提供 health
|
||||
- 所有服务继续保留安全头
|
||||
- home 环境兼容逻辑同时覆盖 `ENV` 与 `ENVIRONMENT`
|
||||
|
||||
---
|
||||
|
||||
## Step 8:完整验证
|
||||
建议执行:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
如本地可启动,再手动验证:
|
||||
|
||||
```bash
|
||||
python main.py --reload
|
||||
```
|
||||
|
||||
检查:
|
||||
- 五个服务均能启动
|
||||
- `/health` 正常
|
||||
- API 不存在路径返回 JSON 404
|
||||
- 页面不存在路径返回各自 404 模板
|
||||
|
||||
---
|
||||
|
||||
## 预计改动文件
|
||||
|
||||
### 新增
|
||||
- `shared/config_base.py`
|
||||
- `shared/error_handlers.py`
|
||||
- `shared/app_factory.py`
|
||||
- `tests/test_shared_config_base.py`
|
||||
- `tests/test_error_handlers.py`
|
||||
- `tests/test_app_factory.py`
|
||||
|
||||
### 修改
|
||||
- `auth/src/config.py`
|
||||
- `blog/src/config.py`
|
||||
- `canvas/src/config.py`
|
||||
- `home/src/config.py`
|
||||
- `prompt/src/config.py`
|
||||
- `auth/src/main.py`
|
||||
- `blog/src/main.py`
|
||||
- `canvas/src/main.py`
|
||||
- `home/src/main.py`
|
||||
- `prompt/src/main.py`
|
||||
- `home/src/routes/pages.py`(删除重复 `/health`)
|
||||
- `tests/test_security_hardening.py`
|
||||
- `tests/test_frontend_backend_reuse_contract.py`
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
### 1. Home 环境变量兼容风险
|
||||
历史部署如果只写了 `ENVIRONMENT`,不能因为统一而直接失效。
|
||||
|
||||
### 2. 404/500 行为变化风险
|
||||
抽象错误处理时,很容易把 API 路径判断或模板上下文改坏,必须用单元测试兜住。
|
||||
|
||||
### 3. Canvas static 行为变化风险
|
||||
当前是“存在才挂载”,不能在 app factory 中粗暴统一成“总是挂载”。
|
||||
|
||||
### 4. 测试依赖旧实现细节
|
||||
如果某些测试直接断言 `main.py` 中出现某段文本,改造后会失效,需要调整为验证行为而不是验证源码写法。
|
||||
|
||||
---
|
||||
|
||||
## 建议提交拆分
|
||||
|
||||
建议拆成 5 个 commit,便于实现与 review:
|
||||
|
||||
1. `test: add shared config and app factory coverage`
|
||||
2. `feat: add shared config base`
|
||||
3. `feat: add shared error handlers and app factory`
|
||||
4. `refactor: migrate service config modules to shared helpers`
|
||||
5. `refactor: 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 合并后,建议继续:
|
||||
|
||||
1. PR2:抽 `shared/service_api/*` 与 `tests/helpers/*`
|
||||
2. PR3:逐步收敛 import 结构,减少 `src` 冲突与 `sys.path.insert`
|
||||
3. PR4:统一 DB schema / migration ownership
|
||||
|
||||
这样可以保证每一步都是建立在更稳定的基础上,而不是继续在重复结构上迭代。
|
||||
Reference in New Issue
Block a user