# 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
  富友支付             ← 自研 SDK（pkg/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 → SubordinateShopIDs（Redis 缓存）
- 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 直接访问 DB（Medium）**
- 方案 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)）
└── 更新代理钱包余额
```

### 关键流 2：IoT 卡轮询 + 实名激活

```
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 RECURSIVE，Redis 缓存 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` 的条件 UPDATE，RowsAffected 判断
**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 {
    // 已被处理或并发冲突，根据当前状态决定返回
}
```

### 模式 3：Redis Sorted Set 驱动轮询调度

**What:** 以下次轮询时间为 Score，ICCID 为 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 注入 asynqClient（A-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 检测

### 反模式 5：Worker 内两套任务投递机制

**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 仍读环境变量（待修复） |
| 富友支付 | 自研 SDK（pkg/fuiou/） | SDK 完整，主调接口仍留桩（方案 J-1）|
| 联通云 OSS | S3 兼容 SDK | 预签名 URL 上传，异步导入任务 |

### 内部模块边界

| 边界 | 通信方式 | 注意事项 |
|------|---------|---------|
| API → Worker | Asynq 任务队列（Redis） | 任务投递幂等；失败重试需要业务幂等保证 |
| Order ↔ Commission | 异步（支付回调完成后投递任务） | 不能同步等待佣金计算完成 |
| Package ↔ IoT Card | 同步（Service 层直接调用） | 套餐激活时需持有卡信息，单事务 |
| Polling ↔ Gateway | 同步 HTTP（Worker 内） | 限速；超时；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/`
- 多层级佣金系统架构设计（TechNova，2026-02-28）：物化路径 vs 邻接表 vs 闭包表对比，CAP 权衡，批流一体
- Multi-Tenant SaaS Isolation（AverageDevs，2026-01-25）：pooled tables vs schema per tenant，Row Level Security，noisy neighbor 防护
- Postgres Multitenancy 2025（DebuggAI）：RLS vs Schema vs 独立 DB 性能对比
- Go Worker Pool + Asynq 并发控制（BackendBytes，OneUptime，2026）：任务队列分区，并发控制

---

*Architecture research for: IoT 卡管理平台（君鸿卡管系统）*
*Researched: 2026-03-27*
