为什么:原 controller handler 在 HTTP 层同时承担了 4 项 usecase 职责 (多源拼装、错误降级策略、limit 硬编码、响应结构组装),违反了 "controller 只做参数解析 + 调用 + 序列化" 的边界。等后续给衍生品加 缓存 / taker volume / 数据质量字段时,HTTP 层会持续变厚。 改动: - 新增 internal/usecase/market_query.go: MarketQueryUsecase.GetDerivatives() 并发拉 current funding/OI + 串行查历史 + 错误聚合为 warnings 返回 - /derivatives handler 退化到 4 行;MarketDeps / restapi.Deps 同步收窄 - /klines 保持直查 KlineRepo(thin query,G11 例外) - 新增 G11 守卫:grep "FundingRepo|OIRepo|LSRepo|TakerRepo" internal/controller 破坏性 API 变更(/derivatives 响应): - 删除 funding.error / openInterest.error - 新增顶层 warnings: []string 顶层 symbol / funding / openInterest / longShortRatio / takerBuySellVolume 保持。 harness 同步: - AGENTS.md §6 加 G11 grep - ai/project-map.md 加 market_query.go + /derivatives 数据流分支 - ai/task-templates.md T6 加"破坏性变体"分支(本次重构暴露了原假设过严) - ai/harness-health.md W1 记一次轻量验证 + 守卫数 10→11 验收: go build / go vet / G6 grep / G11 grep 全绿。
10 KiB
Project Map — cryptoHermes
代码 agent 工作的地形图。每个目录写明:职责、可以依赖谁、不可以依赖谁、扩展点在哪。
完整设计见 docs/dev.md。
顶层布局
cryptoHermes/
├── cmd/ 二进制入口
├── config/ cleanenv 加载
├── internal/ 业务代码(不可被外部 import)
├── migrations/ SQL(含 hypertable)
├── pkg/ 项目内可复用基础设施
├── docs/ 设计文档(dev.md 是源头)
├── ai/ harness 工程文档(本目录)
├── docker-compose.yml Timescale + 服务
├── Dockerfile multi-stage golang:1.23-alpine → alpine:3.20
├── Makefile 所有验证命令
└── go.mod Go 1.23
cmd/ — 二进制入口
| 路径 | 职责 |
|---|---|
cmd/app/main.go |
主服务。读 config → 调 internal/app.Run(cfg) |
cmd/backfill/main.go |
历史 K 线回填 CLI。直接装配 binance client + kline repo,不走 fiber |
约束:cmd/ 里只允许装配 + 调用,不允许写业务逻辑。
config/ — 配置
config.go:cleanenv 加载,yaml + env双源。环境变量优先(见env:tag)。config.yml:默认配置。不要在这里放敏感信息(没有敏感信息可放,因为本服务无私钥)。
扩展点:新增配置项 → config.go 加字段(同时加 yaml: 和 env: tag)→ config.yml 加默认值 → 在 README "配置" 段补一行。
internal/ — 业务核心
Clean Architecture 四层。依赖方向单向:controller → usecase → entity ← repo。
┌──────────────┐
│ controller │ HTTP 层:参数校验、调 usecase、序列化
└──────┬───────┘
│ depends on
▼
┌──────────────┐
│ usecase │ 业务编排。只依赖 ports.go 中的接口
└──────┬───────┘
│ depends on (interface only)
▼
┌──────────────┐ ┌──────────────┐
│ entity │ ◄────── │ repo │ 外部依赖实现
└──────────────┘ └──────────────┘
(binance / postgres)
internal/entity/ — 纯业务实体
kline.go,ticker.go,funding.go,open_interest.go,long_short_ratio.go,taker_volume.go,market_context.go- 价格/成交量字段全部
string,避免 float64 精度问题。落库时 PG 转NUMERIC(36,18)。 Kline有IsClosed bool字段——未收线 K 线 (close_time > now()) 由 mapper 自动判定。LongShortRatio有常量RatioTypeGlobalAccount/RatioTypeTopTraderPosition/RatioTypeTopTraderAccount。
不依赖:fiber、pgx、binance、任何外部库。可以从这里 import 到任何其他层。
internal/usecase/ — 业务接口与编排
ports.go:全部对外接口在这里定义。这是 Clean Arch 的边界文件。MarketDataProvider(K线、ticker24h)DerivativesProvider(funding/OI/多空比/taker volume 共 7 个方法)KlineRepository/FundingRepository/OpenInterestRepository/LongShortRatioRepository/TakerVolumeRepository
market_context.go:聚合/v1/market/context。errgroup 并发拉 snapshot/funding/OI,DB K 线不足 200 根回源 Binance 补,异步写回。market_query.go:/v1/market/derivatives后端。GetDerivatives(symbol, period)并发拉 current funding/OI + 串行查 funding/OI/多空比历史,错误降级为 warnings 返回。
约束(grep 可验证):
grep -r "repo/webapi/binance\|repo/persistent" internal/usecase # 必须无输出
usecase 不可直接 import 任何具体实现。新增业务流程 → 在 ports.go 加接口 → 在 usecase 编排 → 由 internal/app/app.go 注入实现。
internal/controller/ — HTTP 层
restapi/router.go+middleware.go:fiber app、recover、logger、request-idrestapi/v1/health_routes.go:/v1/healthrestapi/v1/market_routes.go:/v1/market/{context,klines,snapshot,derivatives}
约束:
grep -r "repo/webapi/binance\|repo/persistent" internal/controller # 必须无输出
grep -rn "FundingRepo\|OIRepo\|LSRepo\|TakerRepo" internal/controller # 必须无输出(G11)
controller 只能依赖 usecase 和 entity,不能直接调 binance client 或 postgres repo。衍生品多源查询必须经 usecase(详见 ai/risk-guardrails.md G11)。KlineRepo 暂时允许直查 /klines,因为它现在是 thin query。
扩展点:新增路由 → 加 handler → 通过 MarketDeps 注入依赖(不要直接 import repo 包)。
internal/repo/ — 外部依赖实现
| 子目录 | 实现哪些接口 |
|---|---|
webapi/binance/ |
MarketDataProvider、DerivativesProvider |
persistent/postgres/ |
5 个 *Repository |
Binance client(webapi/binance/):
client.go:包pkg/httpclient,统一 base URLhttps://fapi.binance.com,错误用ExternalAPIError{Source, Path, StatusCode, Message}包装。dto.go:贴近 Binance 原始响应(K 线是数组)。mapper.go:DTO → entity。K 线按下标解析(row[0]=openTime ... row[10]=takerBuyQuoteVolume),自动设置IsClosed = close_time < now()。market.go:GetKlines/GetKlinesRange/GetTicker24hderivatives.go:funding × 2 + OI × 2 + 多空比 × 2 + taker volume
Postgres repos(persistent/postgres/):
- 5 个 repo,每个有
UpsertMany(ON CONFLICT DO UPDATE)和FindRecent(按时间倒序拉,返回前反转成正序)。 kline_repo.go的UpsertMany会过滤掉IsClosed=false的 K 线。- 使用 pgxpool + transaction。
扩展点:
- 新增数据源(CoinGlass / ETF)→ 新建
webapi/coinglass/,实现新的 Provider 接口(先在ports.go定义)。 - 新增表 → 新 migration + 新 repo + 在
ports.go加新接口。
internal/worker/ — Collector
collector.go:被 cron 调度的采集逻辑。每个Collect*方法遍历symbols × intervals/periods,拉数据 → upsert。- 不直接被 controller 调用,只通过
internal/app/scheduler.go触发。
internal/app/ — 装配与生命周期
app.go:DI 装配的唯一位置。- 顺序:pool → binanceClient → repos → contextUC → collector → scheduler → fiber app
- graceful shutdown:监听 SIGINT/SIGTERM,10s 超时,先停 cron、再停 fiber、最后关 pool。
scheduler.go:cron spec 在这里。
这里是 Clean Arch 的"组合根"——所有具体类型在这里被实例化并注入抽象接口。
pkg/ — 项目内可复用基础设施
| 包 | 职责 |
|---|---|
pkg/httpclient/ |
通用 HTTP client + 重试(429/5xx 最多 2 次)+ x/time/rate 限流 |
pkg/logger/ |
slog 包装(按 env 选 JSON / Text) |
pkg/postgres/ |
pgxpool 初始化 + Ping |
约束:pkg/ 不依赖 internal/,反向依赖。可以被外部项目 import(虽然目前没有)。
migrations/ — SQL
10 个文件(5 张表 × up/down)。
每个 .up.sql 末尾必须有:
SELECT create_hypertable('<table>', '<time_col>',
chunk_time_interval => <ms>,
if_not_exists => TRUE,
migrate_data => TRUE);
chunk 间隔:
- K 线 / OI / LS / taker → 1 周 = 604800000 ms
- funding → 30 天 = 2592000000 ms
改 migration 前必读 ai/risk-guardrails.md 中"DB migration"段。
数据流(控制流)
写路径(采集)
cron tick
└→ internal/app/scheduler.go
└→ internal/worker/collector.go (Collect*)
├→ binance.GetXxx (HTTP → DTO → entity)
└→ repo.UpsertMany (entity → SQL upsert)
读路径(API)
HTTP request
└→ internal/controller/restapi/v1/market_routes.go
├→ /context → usecase.MarketContext.Build
│ ├→ marketData.GetTicker24h ┐
│ ├→ derivatives.GetCurrent… ├ errgroup 并发
│ ├→ derivatives.GetCurrent… ┘
│ ├→ klineRepo.FindRecent × 5 周期
│ │ └ DB 不足 → 回源 binance.GetKlines(异步写回)
│ └→ fundingRepo / oiRepo / lsRepo.FindRecent
├→ /derivatives → usecase.MarketQuery.GetDerivatives
│ ├→ derivatives.GetCurrentFunding ┐ errgroup
│ ├→ derivatives.GetCurrentOI ┘
│ └→ fundingRepo / oiRepo / lsRepo.FindRecent
├→ /klines → klineRepo.FindRecent(thin query,允许直查)
└→ /snapshot → marketData.GetTicker24h
不可逾越的边界(自动验证见 AGENTS.md §6)
- controller ↛ binance/postgres:必须经 usecase。
- usecase ↛ binance/postgres 包:只依赖
ports.go接口。 - entity ↛ 任何外部库:可被所有层 import。
- pkg ↛ internal:基础设施不知道业务。
- 任何代码 ↛ 私钥/签名:grep
apikey|hmac|x-mbx-apikey|secret_key|api_secret必须无 Go 代码命中。
关键扩展点速查
| 场景 | 改哪 |
|---|---|
| 新增 REST 路由 | internal/controller/restapi/v1/,依赖通过 MarketDeps 注入 |
| 新增业务编排 | internal/usecase/ports.go 加接口 → 新 usecase 文件 → internal/app/app.go 装配 |
| 新增数据表 | 新 migration → 新 entity(可选)→ 新 repo → ports.go 接口 → 装配 |
| 新增数据源 | ai/specs/<source>.md → 新 internal/repo/webapi/<source>/ → 新 Provider 接口 |
| 新增定时任务 | internal/worker/collector.go 加方法 → internal/app/scheduler.go 注册 cron |
| 改配置项 | config/config.go 加字段 → config/config.yml 加默认 → README 记录 |