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