471 lines
11 KiB
Markdown
471 lines
11 KiB
Markdown
# 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
|
||
|
||
这样可以保证每一步都是建立在更稳定的基础上,而不是继续在重复结构上迭代。 |