Files
zhixing-system/.trellis/tasks/08-07-home-market-overview/design.md
T

99 lines
5.7 KiB
Markdown
Raw Normal View History

2026-08-07 13:29:31 +08:00
# 技术设计:Home 市场数据概览
## 1. 边界与模块归属
市场数据概览属于现有 `modules/market_data` bounded context:
```text
zhixing-server/src/zhixing_server/modules/market_data/
├── domain/overview.py # 无框架的读取模型和值
├── application/overview.py # 读取用例/端口编排
├── infrastructure/postgres.py # PostgreSQL 聚合读取适配器
└── presentation/home.py # Pydantic 模型与 /home/overview 路由
```
顶层 `interfaces/http/router.py` 只挂载 bounded context 的 presentation router。FastAPI 依赖和 Pydantic 只出现在 presentation;domain/application 不导入 FastAPI 或 Psycopg。
前端增加 `features/home/` 垂直切片;`shared/ui` 仅承载无业务状态的 shadcn primitives。Home shell 和 overview card 的业务组合保留在 feature 内。
## 2. HTTP 契约
`GET /api/v1/home/overview` 返回:
```json
{
"status": "success",
"stock_pool_count": 2,
"latest_update": {
"trade_date": "2026-08-06",
"completed_at": "2026-08-07T16:30:00+08:00",
"status": "success",
"target_count": 2,
"successful_count": 2,
"failed_count": 0,
"valid_count": 2,
"coverage_rate": 1.0,
"failures": [],
"batch_errors": []
}
}
```
约定:
- 顶层 `status` 表示 Home 当前状态。没有任何批次时为 `no_data`,`latest_update` 为 `null`。
- `latest_update.status` 只使用 `updating`、`success`、`partial_success`、`failed`;无批次不创建虚假的 update 对象。
- `completed_at` 对 running 批次为 `null`。所有非空时间在 presentation 边界转换到 `Asia/Shanghai`。
- `failure` 包含 `ts_code`、`name`、`failure_type`、`reason`、`trade_date`。
- `batch_error` 包含 `item_key`、`error_type`、`reason`;只聚合 `market_sync_item.item_kind = 'batch'`,不把日期级 `daily_basic` 错误伪造成股票级错误。
- 覆盖率以 `Decimal` 在读取层计算,Pydantic 在 JSON 边界输出数值;分母为 0 时返回 0。
## 3. 批次选择与事实计算
读取适配器在一个 PostgreSQL 连接/读取事务中完成聚合:
1. 统计 `market_stock.is_active = true` 的当前目标股票池。
2. 找出最新 `target_trade_date`;该日期上的最新 running 批次优先,否则选该日期 `status != 'running'` 的最新已完成批次,排序使用 `finished_at DESC, created_at DESC`。这保证同日 retry 只展示最后完成结果,且不会让旧日期 running 批次遮蔽更新日期。
3. 若没有任何批次,返回 `no_data`。
4. 对选中批次的目标交易日,使用 active 股票与 `market_daily_bar`、`market_daily_basic` 的 left join;两张事实表均存在的股票构成有效/成功集合。
5. `failed_count` 是 active 目标股票池减去有效集合;失败详情优先使用同批次 `market_sync_item` 中该代码的 `bar` 失败记录,否则按缺失的日线/估值事实生成可读原因。若两类事实都缺失,优先呈现已记录的股票级错误,再使用稳定的缺失类型。
6. 读取 `item_kind = 'batch' AND status = 'failed'` 作为独立 batch errors。
7. `target_count` 保留选中批次记录的目标数;`stock_pool_count` 是当前 active 数;正常同步期间两者相同。`successful_count` / `valid_count` 来自当前事实完整性计算,`coverage_rate` 为成功数除以目标数,分母为 0 时为 0。
状态映射:
| 事实 | 状态 |
| --- | --- |
| 无同步批次 | `no_data` |
| 最新交易日仍有 running 批次 | `updating` |
| 已完成批次无失败且所有目标股票完整 | `success` |
| 已完成批次存在失败但至少一只股票完整 | `partial_success` |
| 已完成批次失败或没有任何完整股票 | `failed` |
这条读取路径不触发同步、不修改数据、不读取 CSV。数据库错误继续抛出 `MarketDataRepositoryError`,由现有 FastAPI 默认 500 边界暴露,避免把基础设施错误伪装成 `no_data`。
## 4. 依赖与测试 seam
- 增加 `MarketDataOverviewReader` protocol,HTTP 依赖工厂返回具体 `PostgresMarketDataRepository`;测试通过 `app.dependency_overrides` 注入内存 reader,HTTP 测试仍调用 `TestClient(create_app())`。
- 读取模型使用 frozen dataclass,Pydantic response model 负责日期/时间和字段名转换。
- 后端契约测试使用多个 active/inactive 股票、完整/缺失事实、running/completed/retry/batch error 记录构造 fake reader,验证用户可见 JSON,不断言 SQL。
- 前端 API 通过 `requestJson` 保留 `AbortSignal`;query 使用 feature 内 `as const` key。
## 5. shadcn/ui 组合策略
现有 `Card`、`Badge`、`Button` 作为基础,按当前 `components.json` 的 `base-nova` 约定补齐并复用:
- shell:`Avatar`、`Input`、`Separator`、`Button`;
- loading:`Skeleton`;
- status/data:`Card`、`Badge`、`Progress`;
- failure details:`Dialog` + `ScrollArea`。
优先用 shadcn CLI 生成标准 primitive;如果生成器与锁定依赖版本不兼容,则采用 CLI 对应的当前源代码形状,在 `shared/ui` 保持同样的可访问属性、原生 props、`cn` 合并和组件组合接口。Home feature 不直接依赖第三方 modal/scroll implementation。
## 6. 兼容性、迁移与回滚
- 不新增数据库迁移;只读模型适配现有五张表。
- API 新增,不修改 `/api/v1/system/status`;根路由从 smoke 页面切换为 Home,但既有 system feature 测试文件保留。
- 若读取查询需要兼容已有迁移字段,使用当前 schema 中的字段和状态值,不变更同步写入语义。
- 回滚点是移除 Home route、overview presentation/reader 及新增前端 feature/shared primitives;数据库无需回滚。