Files
junhong_cmp_fiber/.planning/research/ARCHITECTURE.md
2026-03-27 19:04:17 +08:00

475 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architecture Research
**Domain:** IoT 卡管理 + 多级分销 + 分佣结算平台Brownfield 单体)
**Researched:** 2026-03-27
**Confidence:** HIGH基于当前代码库深度分析 + 该领域已知模式验证)
---
## 当前架构概述
### 系统总览
```
┌──────────────────────────────────────────────────────────────────────┐
│ API 进程cmd/api
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ /auth │ │ /admin │ │ /c/v1 │ │ /callback│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──── Handler 层 ────────────┘ │
│ │ │
│ Service 层(~55 个服务包) │
│ │ │
│ Store 层54 个 Store 文件) │
│ │ │
│ PostgreSQL + Redis │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Worker 进程cmd/worker
│ ┌────────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │
│ │ Asynq Worker │ │ 轮询调度器 │ │ Asynq Scheduler │ │
│ │ (任务消费) │ │ (Redis SortedSet)│ │ (定时任务) │ │
│ └────────┬───────┘ └────────┬─────────┘ └──────────┬──────────┘ │
│ └──────────────────┴────────────────────────┘ │
│ │ │
│ Task Handler 层16 个任务) │
│ │ │
│ Service / Store 层(复用 API 侧) │
└──────────────────────────────────────────────────────────────────────┘
外部集成:
Gateway运营商接口 ← HTTP Client → IoT 卡/设备状态同步
微信支付JSAPI/H5 ← PowerWeChat SDK
富友支付 ← 自研 SDKpkg/fuiou
联通云 OSS ← S3 兼容(批量导入/导出)
```
### 组件职责
| 组件 | 职责 | 关键文件 |
|------|------|---------|
| Bootstrap | 依赖装配,进程启动唯一入口 | `internal/bootstrap/*.go` |
| RouteSpec + Register() | 路由即文档,单次声明驱动 Fiber + OpenAPI | `internal/routes/registry.go` |
| Auth Middleware | JWT/Redis Token 解析,写入 UserContextInfo | `pkg/middleware/auth.go` |
| DataScope Filter | Store 层显式过滤,多租户数据隔离 | `pkg/middleware/data_scope.go` |
| Service业务核心| 幂等、事务编排、业务规则、任务提交 | `internal/service/**` |
| Store数据访问| SQL、分页、显式权限过滤 | `internal/store/postgres/*.go` |
| Task Handler | Asynq 任务处理(轮询/佣金/导入/过期) | `internal/task/*.go` |
| Polling Scheduler | Redis SortedSet 驱动卡片轮询队列 | `internal/polling/scheduler.go` |
| Commission Calculation | 多级分佣链计算(差价佣金 + 一次性佣金) | `internal/service/commission_calculation/` |
---
## 组件边界分析
### 当前已正确划分的边界
**① 双进程隔离API vs Worker**
- API 进程HTTP 快速响应,无阻塞 I/O
- Worker 进程:轮询、批量导入、佣金计算、定时任务
- 边界清晰,独立部署、独立扩容 ✅
**② 数据权限隔离Shop 层级过滤)**
- 用户身份 → UserContextInfo → SubordinateShopIDsRedis 缓存)
- Store 层显式调用 ApplyShopFilter / ApplyEnterpriseFilter
- 不依赖 GORM Callback权限过滤显式可见 ✅
**③ 任务队列解耦Asynq**
- 同步调用路径Handler → Service → 提交 Asynq 任务
- 异步执行路径Worker → Task Handler → Service/Store
- 支付回调触发的佣金计算走异步路径,不阻塞回调响应 ✅
**④ 外部服务封装Gateway / 微信 / 富友)**
- Gateway HTTP Client 封装在 `internal/gateway/`
- 微信 SDK 封装在 `pkg/wechat/`
- 富友 SDK 封装在 `pkg/fuiou/`
- 外部依赖注入,可以独立替换 ✅
### 当前边界薄弱点(需要关注)
**⑤ Order Service 超大文件Critical**
- `internal/service/order/service.go` = **2365 行**
- 支付发起、支付回调、订单状态流转、幂等锁、钱包扣款全部耦合
- 任何一处改动都有全链路回归风险
- **建议:** 按职责拆分为子文件:`payment_service.go``callback_service.go``order_state.go``wallet_deduction.go`
**⑥ 轮询系统 asynqClient 依赖缺失High**
- `internal/task/polling_handler.go` 中实名激活通过 `Redis RPush` 降级实现,而非 Asynq 任务
- 架构偏差:同一 Worker 进程内用两套机制投递任务Asynq + RPush
- **建议:** 在 Worker Bootstrap 中注入 asynqClient统一走 Asynq 路径
**⑦ C 端 Handler 直接访问 DBMedium**
- 方案 F-4部分 C 端 Handler 绕过 Service 层直接访问 DB
- 破坏了分层边界,导致业务逻辑散落在 Handler 层
- **建议:** 迁回 Service 层Handler 只做 HTTP 入参解析和响应封装
---
## 数据流方向
### 关键流 1订单创建 + 支付 + 佣金链
```
用户发起购买
Handler → 解析 DTO → 调用 OrderService.Create()
OrderService.Create()
├── Redis 幂等键检查(业务键防重)
├── 分布式锁RedisOrderCreateLockKey
├── 二次检查(加锁后再查 Redis
├── DB 事务:创建订单 + 冻结钱包余额
└── 提交 Asynq 支付超时任务30min
支付回调(微信/富友)
CallbackHandler → PaymentService.HandleCallback()
├── WHERE status=pending 条件更新(防重)
├── 更新订单状态为已支付
├── 解冻钱包余额 / 扣款
└── 提交 CommissionCalculation Asynq 任务
Worker: CommissionCalculationTask
├── 查询代理层级路径WITH RECURSIVE / parent_id 链)
├── 计算差价佣金链(每层代理差价 × 数量)
├── 计算一次性佣金(绑定规则)
├── DB 事务:写入 commission_record幂等unique(order_id, agent_id)
└── 更新代理钱包余额
```
### 关键流 2IoT 卡轮询 + 实名激活
```
Polling Scheduler独立 Goroutine
├── 从 Redis Sorted Set 取到期卡片
│ key: polling:realname:queue
投递 Asynq 任务 → Worker: PollingHandler
├── 调用 Gateway HTTP 接口拉取实名状态
├── 更新 iot_card.realname_status
实名状态变为已认证 → 触发套餐激活
当前路径RPush待修复为 Asynq
激活任务 → PackageService.ActivateByRealname()
├── 查找 status=3待实名激活套餐
├── 第一个主套餐 → status=1激活
├── 其余主套餐 → status=4排队
└── 加油包检查并激活
```
### 关键流 3流量计费当前方案 vs 改革方案)
```
当前(方案 E 改革前):
Gateway 上报流量 → 直接覆盖写 current_month_usage_mb
问题:自然月由上游归零,历史数据被破坏
改革后(方案 E
Gateway 上报流量增量
Redis 增量累加(今日流量缓冲)
每日凌晨批任务 → 写入 tb_card_daily_usage日粒度记录
查询层Redis 今日数据 + DB 历史数据 合并返回
优点:历史数据不可变,查询范围可控
```
### 关键流 4多租户数据权限
```
HTTP 请求 → Auth Middleware
├── 解析 JWT/Redis Token
├── 查询用户 user_type, shop_id, enterprise_id
├── GetSubordinateShopIDs()WITH RECURSIVERedis 缓存 30min
└── 写入 UserContextInfo 到 context.Context
Store 层查询
├── Agent 用户 → ApplyShopFilter(shopIDs)
│ WHERE shop_id IN (当前+下级)
├── Enterprise 用户 → ApplyEnterpriseFilter(enterpriseID)
│ WHERE enterprise_id = X
└── SuperAdmin/Platform → 不过滤
```
---
## 架构模式
### 模式 1两阶段幂等防重订单创建
**What:** Redis 业务键 + 分布式锁双重防护,防止并发创建重复订单
**When:** 任何创建类写操作(订单、提现、激活任务)
**Trade-offs:** 增加 2~3 次 Redis 往返,换取金融级安全
```go
// Step 1: Redis GET 快速检测
val, err := redis.Get(ctx, idempotencyKey).Result()
if err == nil && val != "" { return existingResult }
// Step 2: SetNX 分布式锁
locked, _ := redis.SetNX(ctx, lockKey, now, lockTTL).Result()
if !locked { return ErrTooManyRequests }
defer redis.Del(ctx, lockKey)
// Step 3: 加锁后二次检测(防止锁等待期间重复创建)
val, err = redis.Get(ctx, idempotencyKey).Result()
if err == nil && val != "" { return existingResult }
// Step 4: 执行业务逻辑...
// Step 5: 标记成功
redis.Set(ctx, idempotencyKey, resultID, ttl)
```
### 模式 2条件更新防并发状态流转
**What:** `WHERE status = expected_status` 的条件 UPDATERowsAffected 判断
**When:** 支付回调、套餐激活、订单状态变更等有明确状态机的场景
**Trade-offs:** 需要数据库层有索引支持,效率高但依赖状态设计的正确性
```go
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", orderID, PaymentStatusPending).
Updates(map[string]any{"payment_status": PaymentStatusPaid})
if result.RowsAffected == 0 {
// 已被处理或并发冲突,根据当前状态决定返回
}
```
### 模式 3Redis Sorted Set 驱动轮询调度
**What:** 以下次轮询时间为 ScoreICCID 为 Member定时扫描到期项投递 Asynq 任务
**When:** 需要"不同间隔、海量卡片"的轮询调度场景
**Trade-offs:** 比 cron 更灵活内存占用与卡片数量正相关Redis 重启需重建队列
```go
// 投入轮询队列Score = 下次轮询 Unix 时间戳)
redis.ZAdd(ctx, "polling:realname:queue", redis.Z{
Score: float64(nextPollAt.Unix()),
Member: iccid,
})
// Scheduler 每秒扫描
members := redis.ZRangeByScore(ctx, key, &redis.ZRangeBy{
Min: "-inf",
Max: strconv.FormatInt(time.Now().Unix(), 10),
Limit: &redis.Limit{Count: batchSize},
})
// 投递 Asynq 任务...
```
### 模式 4多级佣金链计算向上遍历
**What:** 从发生订单的代理出发,沿 parent_id 向上遍历,逐级计算差价或固定佣金
**When:** 每笔订单支付成功后触发(异步任务)
**Trade-offs:** 当前 parent_id 邻接表实现,层级 7 层时 WITH RECURSIVE 性能尚可;超 10 层或高并发时需物化路径优化
```go
// 当前实现DB 递归查询代理链WITH RECURSIVE 或逐级查询)
// 建议:超 1000 TPS 时改为物化路径 + Redis 缓存代理链
func (s *Service) CalculateCommissionChain(ctx context.Context, orderID uint) error {
order := fetchOrder(ctx, orderID)
agentChain := fetchAncestorChain(ctx, order.SellerShopID) // 向上遍历
for i, agent := range agentChain {
diff := agent.SellPrice - agent.CostPrice
writeCommissionRecord(ctx, tx, order.ID, agent.ShopID, diff)
}
}
```
---
## 构建顺序建议
**原则:** 优先修复已存在但断裂的主链路,再扩展新功能,最后优化架构
```
阶段 0 — 主链路修复P0当前 MVP 阶段的首要目标)
├── 1. 实名常量统一A-1→ 解锁整个实名链路
├── 2. C 端实名校验A-2依赖 A-1
├── 3. 轮询 Handler 注入 asynqClientA-3→ 修复架构偏差
├── 4. 充值回调购包触发 + 佣金补全A-4→ 核心收入链路
├── 5. 代购订单金额修正A-5→ 佣金归零问题
└── 6. 并发校验修复A-7, C-3→ 提现/激活配置并发
阶段 1 — 功能补全(参考方案 B/C/D/H/J
├── 企业端权限完善B-1, B-2
├── 提现/审批流程完善C-1, C-2
├── 设备体系完善D-0 ~ D-3
└── 轮询系统小修H-1 ~ H-4
阶段 2 — 流量计费改革(方案 E独立窗口
├── 新建 tb_card_daily_usage迁移
├── Redis 增量缓存层
└── 查询层适配(双段数据合并)
阶段 3 — 退款体系(方案 I
├── 退款模型 + 7 个接口
└── 佣金钱包反向扣减
阶段 4 — Order Service 拆分(架构改善)
├── payment_service.go支付发起
├── callback_service.go回调处理
├── order_state.go状态流转
└── wallet_deduction.go钱包操作
```
**组件依赖关系:**
```
IoT卡状态 → 实名认证 → 套餐激活 → 流量计费 → 订单计算
│ │ │
▼ ▼ ▼
Gateway 数据轮询 Commission
集成层 调度层 计算层
```
---
## 扩展挑战与 SaaS 化瓶颈
### SaaS 化时会成为瓶颈的当前决策
| 当前决策 | SaaS 化面临的问题 | 迁移路径 |
|---------|-----------------|---------|
| 单一 PostgreSQL 数据库 | 所有"租户"shop共享同一 DB | 添加 `tenant_id` 列 + RLS或 Schema 隔离 |
| GORM 数据过滤基于 shop_id | 过滤逻辑散布于 54 个 Store 文件 | 统一 Tenant Context 注入层,确保覆盖率 |
| Config 嵌入二进制 | 多租户需动态配置微信AppID/支付商户号) | 已有 `tb_wechat_config`,需完全消除环境变量硬依赖 |
| 单进程 Worker | 一个 Worker 处理所有租户的轮询/佣金 | 按租户分队列;大租户独立 Worker |
| parent_id 邻接表代理层级 | 跨租户代理关系查询膨胀 | 加 `tenant_id` 隔离;超 5 万代理考虑物化路径 |
| commission_calculation 单服务 | 高 TPS 下单文件 653 行线性计算 | 异步 + 批量写入;超 1000 TPS 考虑 Flink |
### 各规模架构调整
| 规模 | 架构方案 |
|------|---------|
| 当前(< 100 代理,< 1000 卡)| 当前单体完全够用,重点是修复断链 |
| 成长期1000+ 代理10万+ 卡)| Worker 分队列 + 优化轮询 SortedSet 分片 |
| SaaS 初期(多客户,共享平台)| 添加 tenant_id 隔离层;动态支付配置;统一 OpenAPI 网关 |
| SaaS 规模(每客户数万卡)| Schema 隔离 or DB 隔离;独立 Worker 进程 per 大客户 |
| 高并发计费(> 5000 TPS| 引入消息队列Kafka缓冲计费事件离线批处理结算 |
### 第一个会触发的瓶颈:轮询调度
- **现状:** Redis Sorted Set + Asynq 混合投递Worker 单机并发
- **触发条件:** 超过 5 万张活跃卡,每卡 5 分钟轮询一次 = ~167 TPS 持续 Gateway 请求
- **修复方向:** Gateway 并发限速配置 + 批量 API 合并 + Sorted Set 分片(多个 queue key
### 第二个会触发的瓶颈:佣金计算的 DB 递归查询
- **现状:** `WITH RECURSIVE` 或逐级 parent_id 查询,最深 7 层
- **触发条件:** 单日 > 1 万笔订单,并发处理时 DB CPU 飙升
- **修复方向:** 代理链 Redis 缓存shop_id → ancestor_chain写入时物化路径 `path` 字段
### 第三个会触发的瓶颈Order Service 耦合带来的发布风险
- **现状:** 2365 行,支付/回调/钱包全耦合
- **触发条件:** 添加新支付方式(富友小程序)或退款逻辑时,改动影响范围不可控
- **修复方向:** 拆分子文件(见构建顺序阶段 4不需要跨进程拆分
---
## 反模式警告
### 反模式 1在支付回调中同步计算佣金
**What:** 支付回调 Handler 直接在回调请求的生命周期内计算完整的多级佣金链
**Why bad:** 回调超时 → 第三方重推 → 佣金重复计算;而且 Gateway 通常要求快速响应3s 内)
**Do this instead:** 回调只更新订单状态,通过 Asynq 任务异步计算佣金(当前架构已正确实现)
### 反模式 2代理层级递归查询不加缓存
**What:** 每次数据权限过滤都调用 `WITH RECURSIVE` 查下级 shop_id
**Why bad:** 高并发下递归查询是 DB 杀手7 层 × 每层 N 个子节点 = 指数级扫描
**Do this instead:** `GetSubordinateShopIDs` 结果 Redis 缓存 30 分钟(当前已实现),注意缓存失效策略
### 反模式 3流量数据直接被上游覆盖写
**What:** Gateway 上报当月累计流量 → 直接 UPDATE current_month_usage_mb
**Why bad:** 上游自然月归零时,历史用量记录丢失;无法回查历史;无法审计计费
**Do this instead:** 日粒度缓冲(方案 E增量写入 `tb_card_daily_usage`,查询层合并 Redis + DB
### 反模式 4把租户隔离硬编码在每个 Store 文件
**What:** 54 个 Store 文件各自调用 ApplyShopFilter过滤逻辑分散
**Why bad:** 新增 Store 文件时容易遗漏SaaS 化时难以统一改造
**Do this instead:** MVP 阶段保持现状(显式>隐式SaaS 化时考虑统一 TenantContext 注入层 + 强制 lint 检测
### 反模式 5Worker 内两套任务投递机制
**What:** 轮询实名激活通过 `Redis RPush` 投递,其他任务走 Asynq
**Why bad:** 两套消费机制Asynq 的可观测性/重试/超时管理对 RPush 任务无效
**Do this instead:** Worker Bootstrap 注入 `asynq.Client`,统一走 Asynq方案 A-3
---
## 集成点分析
### 外部服务
| 服务 | 集成模式 | 关键注意点 |
|------|---------|-----------|
| 运营商 Gateway | 同步 HTTP 调用(`internal/gateway/` | Gateway 宕机时需降级;速率限制;超时设置 |
| 微信支付JSAPI/H5| PowerWeChat SDK | 配置已迁移至 `tb_wechat_config`,但 OAuth 仍读环境变量(待修复) |
| 富友支付 | 自研 SDKpkg/fuiou/ | SDK 完整,主调接口仍留桩(方案 J-1|
| 联通云 OSS | S3 兼容 SDK | 预签名 URL 上传,异步导入任务 |
### 内部模块边界
| 边界 | 通信方式 | 注意事项 |
|------|---------|---------|
| API → Worker | Asynq 任务队列Redis | 任务投递幂等;失败重试需要业务幂等保证 |
| Order ↔ Commission | 异步(支付回调完成后投递任务) | 不能同步等待佣金计算完成 |
| Package ↔ IoT Card | 同步Service 层直接调用) | 套餐激活时需持有卡信息,单事务 |
| Polling ↔ Gateway | 同步 HTTPWorker 内) | 限速超时Gateway 不可用时不应导致整个轮询系统崩溃 |
| Auth ↔ RBAC | 同步 Redis + DB中间件层 | SubordinateShopIDs 缓存是性能关键 |
---
## 架构变更的高风险区
基于当前代码分析,以下区域变更风险最高,需要在 roadmap 中单独规划:
| 区域 | 风险原因 | 建议处理方式 |
|------|---------|------------|
| `internal/service/order/service.go` | 2365 行,全链路耦合 | 先修复 Bug再在低风险时拆分 |
| 流量计费体系(方案 E | 涉及 DB 迁移 + Redis 架构 + 查询兼容 | 单独发布窗口,充分测试迁移脚本 |
| 支付配置(微信/富友切换)| 半迁移状态(环境变量 vs DB 配置同时存在)| 明确单一配置源后再添加新支付方式 |
| 轮询系统Polling Scheduler| RPush 降级路径 + asynqClient 缺失 | A-3 修复后,逐步迁移到统一 Asynq 路径 |
| 提现并发逻辑A-7, C-3| 并发锁逻辑不完整,线上可能已有脏数据 | 修复时需同步修复历史数据 |
---
## Sources
- 当前代码库深度分析2026-03-27`internal/service/`, `internal/store/postgres/`, `internal/task/`, `internal/routing/`
- 多层级佣金系统架构设计TechNova2026-02-28物化路径 vs 邻接表 vs 闭包表对比CAP 权衡,批流一体
- Multi-Tenant SaaS IsolationAverageDevs2026-01-25pooled tables vs schema per tenantRow Level Securitynoisy neighbor 防护
- Postgres Multitenancy 2025DebuggAIRLS vs Schema vs 独立 DB 性能对比
- Go Worker Pool + Asynq 并发控制BackendBytesOneUptime2026任务队列分区并发控制
---
*Architecture research for: IoT 卡管理平台(君鸿卡管系统)*
*Researched: 2026-03-27*