12 KiB
系统架构地图
系统职责与非职责
本系统保存物联网卡、设备、套餐、订单、钱包、分佣、审批、通知和审计的本地业务事实,并协调支付、运营商、Gateway、企业微信、短信和对象存储。第三方内部状态、审批流配置、支付清算和运营商网络行为不是本系统事实;外部契约见 docs/integrations/。
Composition Roots 与运行单元
| 单元 | 入口 | 启动链与外部依赖 | 验证 |
|---|---|---|---|
| HTTP API | cmd/api/main.go |
Viper 配置 → 日志目录 → PostgreSQL → Redis/Asynq Client → JWT/验证码 → Storage/Gateway → internal/bootstrap.Bootstrap → Fiber 中间件与路由;启动时写 logs/openapi.yaml |
go build ./cmd/api;启动后请求 /health、/ready |
| Worker | cmd/worker/main.go |
PostgreSQL、Redis、Storage、Gateway → internal/bootstrap.BootstrapWorker → Task Handler、Outbox Relay、任务救援;角色决定单例调度模块 |
go build ./cmd/worker;从应用日志核对角色、模块和任务注册 |
| OpenAPI 生成 | cmd/gendocs/main.go |
使用占位 Handler 复用 internal/routes.RegisterRoutesWithDoc,生成接口文档 |
go run cmd/gendocs/main.go;它证明路由元数据,不单独证明真实 Bootstrap 可达性 |
| 治理 CLI | cmd/foundation-check、cmd/audit-coverage、cmd/migration-finalize、cmd/audit-retention-simulate |
读取源码、迁移或数据库状态并输出门禁/模拟结果 | 逐命令检查输入;audit-coverage 当前仍依赖已删除路径,见工程约束 BLOCKED 项 |
启动、隔离数据库、smoke 与日志命令见 README.md。生产为独立的 systemd 手工二进制发布拓扑,不以 Docker Compose 或 CI 工作流为准;运行事实与人工发布边界见 docs/deployment/production-runbook.md。
Worker 角色
pkg/config/config.go 接受 all、leader、consumer:
- 三种角色都启动 Asynq 消费服务、Outbox Relay 和未完成导入任务救援。
leader与all额外启动轮询初始化器、轮询调度器和 Asynq Scheduler。consumer不运行单例调度模块,适合横向扩展消费能力。- 实际启停判断位于
cmd/worker/main.go的buildWorkerModuleStatus与runsSingletonModules。
HTTP 路由与信任边界
总入口是 internal/routes/routes.go:
| 路由域 | 认证/信任边界 | 主要入口 | 当前 Spec 索引 |
|---|---|---|---|
/health、/ready |
公开,只返回进程健康/就绪状态 | internal/routes/health.go |
openspec/specs/operations-audit/spec.md |
/api/auth |
后台账号登录、刷新、登出与当前身份;认证中间件在路由组装配 | internal/routes/auth.go |
openspec/specs/identity-access/spec.md |
/api/admin |
后台认证、角色权限、店铺/企业数据范围;业务层仍需资源级校验 | internal/routes/admin.go |
identity-access、asset-device、package-lifecycle、order-payment-wallet、operations-audit |
/api/c/v1 |
个人客户 Token 与资产归属边界 | internal/routes/personal.go |
openspec/specs/personal-customer/spec.md |
/api/open/v1 |
代理 Open API 独立认证/签名,不复用后台账号权限 | internal/routes/open.go |
openspec/specs/agent-open-api/spec.md |
/api/callback |
无登录认证;每类渠道必须在 Handler/Adapter 内验签、解密、校验金额或事件身份 | internal/routes/order.go、wecom_callback.go 及运营商回调注册 |
openspec/specs/external-integration/spec.md、order-payment-wallet |
HTTP 入参、回调 Body/Header、上传文件和代理签名材料均是不可信输入。Handler 只做绑定、边界校验与响应;状态和金额不变量必须由业务用例重新判断。
分层与实际共存结构
internal/routes:可达 HTTP 入口与 RouteSpec。internal/handler:Fiber 信任边界、参数绑定和统一响应。internal/application:新用例编排、事务脚本、Port 和可靠副作用边界。internal/domain:状态、金额与并发不变量;不依赖 Fiber、GORM、Redis、Asynq 或外部 SDK。internal/query:只读权限、联表、分页和 DTO 投影。internal/service:旧用例;允许直接使用 Store/GORM,按完整用例渐进迁移。internal/infrastructure:Port Adapter、GORM Repository、Outbox、审计与外部交互。internal/store/postgres、internal/model:旧持久化模型与显式查询。internal/bootstrap:API/Worker 依赖装配,不承载业务规则。pkg:跨模块基础能力和第三方客户端。migrations:Schema 演进事实。
允许方向:Handler → Application/Query/旧 Service;Application → Domain + Port;Infrastructure → Application/Domain Port;旧 Service → Store/GORM。禁止 Domain 依赖框架或基础设施、Query 修改状态、Handler 拼装领域规则、Infrastructure 定义业务状态机。
! grep -RIlE 'gofiber|gorm.io|go-redis|hibiken/asynq' internal/domain --include='*.go'
关键业务模块导航
以下表是入口地图,不替代业务 Specs;事务与副作用必须按具体调用链证明。
| 业务能力 | HTTP/异步入口 | 用例与领域 | 持久化/外部边界 | Spec |
|---|---|---|---|---|
| 后台身份、账号、角色、权限、店铺 | routes/auth.go、account.go、role.go、permission.go、shop.go |
`handler/auth | admin;旧 service/auth |
account |
| 卡、设备、资产与企业授权 | iot_card.go、device.go、asset*.go、enterprise_card.go、enterprise_device.go;导入/轮询任务 |
旧 `service/iot_card | device | asset |
| 套餐、系列、授权、订购与到期 | package*.go、批量分配/调价/订购路由;激活、到期和失效任务 |
旧 `service/package | package_series | shop_*;domain/package;application/packageexpiry` |
| 订单、支付、充值、钱包、退款、分佣与换货 | order.go、agent_recharge.go、refund.go、commission.go、exchange.go、支付回调 |
旧 `service/order | recharge | refund |
| 个人客户 | routes/personal.go 及 C 端子路由 |
handler/app;`service/client_* |
personal_customer | customer_binding`;通知 Application/Query |
| 代理 Open API | routes/open.go |
handler/openapi → service/agent_open_api |
独立认证、店铺数据范围、卡/套餐/钱包 Store | agent-open-api |
| 轮询、通知、导出、配置与审计调查 | polling_*.go、notification.go、export_task.go、system_config.go、audit.go;Scheduler/Task/Outbox |
`application/notification | systemconfig | auditarchive |
| 外部集成接点 | 支付/运营商/企微回调,Gateway 调用,对象存储、短信 | internal/gateway、`internal/infrastructure/wecom |
payment | carriercallback |
任务 7 会把当前过大的 Spec 索引拆成完整业务能力;拆分后必须同步本表链接。
同步数据流
HTTP Route
→ 认证/数据范围中间件
→ Handler(绑定与信任边界)
→ Application / Query / 旧 Service
→ Domain(仅适用于已迁移复杂写)
→ Repository / Store / GORM
→ pkg/response 或全局 ErrorHandler
并非所有写操作都在统一事务中:旧 Service 可能直接写 Store,新 Application 可能使用事务脚本,复杂写可能通过 Domain + Port。审计、状态条件和事务范围必须从具体用例证明,不能由这张地图推断。
异步与可靠事件
Asynq Task
Worker 当前处理的任务族包括:卡/设备导入、导出分片与收尾、资产批量购包、订单套餐失效、自动充值后购包、佣金、订单过期、五类轮询、告警、数据/通知清理、套餐到期提醒、每日流量落盘、企微审批同步/恢复、代理充值恢复、审计/集成归档和 Outbox 投递。
发现真实注册与生产位置:
rg -o 'constants\.TaskType[A-Za-z0-9_]+' cmd/worker/main.go internal/task internal/bootstrap/worker.go internal/infrastructure | sort -u
直接 EnqueueTask 只证明已提交,不等于业务终态成功;最终状态由 Task Handler 的条件更新、记录或补偿决定。
Outbox
当前事件族覆盖审批提交/终态、卡实名/流量/网络/系列、代理充值支付确认、代理主钱包扣款/预留/入账/退款,以及后台/个人通知。
rg -o 'constants\.OutboxEventType[A-Za-z0-9_]+' cmd/worker/main.go internal | sort -u
需要业务事实与可靠异步副作用原子提交的用例应同事务写 Outbox;是否已经做到必须逐调用链核对。Relay 和消费者按至少一次投递设计,消费者必须容忍重复。
外部调用与回调
- 支付:创建本地支付事实后调用渠道;回调需要验签、金额/业务号/状态校验,重复回调不得重复入账。
- 企业微信:本地审批实例 → Outbox 提交 → 渠道审批;加密回调、轮询和人工同步进入统一状态同步语义。
- Gateway/运营商:查询与控制结果来自外部;超时或未知结果不得理想化为成功。
- 对象存储:上传/预签名 URL 是外部副作用,数据库只保存对象引用。
- SMS:发送结果是外部通知结果,不等同业务事务成功。
外部网络调用不应持有长数据库事务;确需提交后可靠执行时使用 Outbox/Task。实际例外按 As-Is Spec 记录。
事务、审计与日志边界
- Access Log:HTTP 调试事实。
- Audit Event:操作者、资源、动作、前后值和结果。
- Domain Ledger/钱包交易:金额与状态领域事实。
- Integration Log:外部调用尝试和脱敏结果。
- Outbox:提交后可靠副作用,不替代前三者。
成功高风险操作是否与审计同事务必须逐用例核对。业务已回滚后的 failed/denied 审计使用独立短事务。不得用 Access Log 证明资金或状态事实。
结构与生成验证
go build ./cmd/api ./cmd/worker
go run cmd/gendocs/main.go
openspec doctor --json
openspec validate --all
./scripts/context-health.sh
! grep -RIlE 'gofiber|gorm.io|go-redis|hibiken/asynq' internal/domain --include='*.go'
rg 'Register[A-Za-z]*Routes|register[A-Za-z]*Routes' internal/routes
OpenAPI 生成器使用占位 Handler,因此还要对照 internal/bootstrap 与真实路由装配。Schema 事实以 migrations/ 和隔离数据库为准,禁止使用 Makefile 中的历史连接信息。