diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md new file mode 100644 index 0000000..ec1a9c8 --- /dev/null +++ b/.planning/research/ARCHITECTURE.md @@ -0,0 +1,474 @@ +# 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* diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md new file mode 100644 index 0000000..c42ea0e --- /dev/null +++ b/.planning/research/FEATURES.md @@ -0,0 +1,263 @@ +# Feature Research + +**Domain:** 物联网卡/号卡/设备全生命周期管理平台(面向代理商 B 端 + C 端个人用户) +**Researched:** 2026-03-27 +**Confidence:** HIGH(主要来源:广梦云/SIMBOSS/Simbest/iotcard 实际竞品研究 + 项目现有功能盘点) + +--- + +## 背景:已实现功能(Brownfield 基线) + +以下功能已在代码库中落地,本文档**不重复列出**,仅聚焦**缺口分析**: + +| 已实现模块 | 关键能力 | +|-----------|---------| +| IoT 卡生命周期 | 开卡/激活/停复机/销户 | +| 套餐系统 | 主套餐/加油包/排队/囤货待实名激活/流量扣减 | +| 订单系统 | 5 种购买场景,微信/富友/钱包支付 | +| 代理商体系 | 7 级层级,差价佣金 + 一次性佣金,提现申请流程 | +| RBAC 权限 | 角色/权限/菜单,基于店铺层级数据过滤 | +| C 端认证 | 微信 OAuth + 手机号绑定 | +| 设备管理 | 绑卡/解绑/批量 Excel 导入 | +| 流量轮询 | 实名状态/流量/套餐余额,Asynq + Redis Sorted Set | +| 退款流程 | tb_refund_request 模型,7 个退款接口(方案 I,已规划) | + +--- + +## Feature Landscape + +### Table Stakes(用户期望有,缺了就感觉产品不完整) + +#### 卡/资产管理 + +| Feature | Why Expected | Complexity | 当前状态 | Notes | +|---------|--------------|------------|---------|-------| +| 卡批量操作(停/复/注销) | 运营场景必须,一次管几百张卡不能单张操作 | MEDIUM | ❌ 缺失 | 已有单卡接口,缺批量接口;广梦云/SIMBOSS 均有 | +| 卡信息批量导出(Excel/CSV) | 资产台账、对账、交接必须 | LOW | ❌ 缺失 | 已有导入;竞品均有导出;格式含 ICCID/IMEI/状态/套餐/流量 | +| 实名状态手动刷新(单卡/批量) | 运营商实名状态有延迟,客服需要手动触发同步 | LOW | ⚠️ 部分 | 有轮询系统但无手动触发单卡实名刷新接口 | +| 卡状态变更历史记录 | 出现纠纷时需要溯源"什么时候谁停了这张卡" | MEDIUM | ❌ 缺失 | 审计日志只覆盖账号操作,不覆盖卡操作 | +| 流量使用日粒度详单 | C 端用户最常问"今天用了多少流量" | MEDIUM | ⚠️ 规划中 | 方案 E(流量体系改革)已规划 tb_card_daily_usage | + +#### 套餐/订单 + +| Feature | Why Expected | Complexity | 当前状态 | Notes | +|---------|--------------|------------|---------|-------| +| 订单搜索与筛选(多维度) | 运营/客服必须能按 ICCID/手机号/订单号/状态/时间段查单 | LOW | ⚠️ 部分 | 基础筛选存在,高级组合筛选可能不完整 | +| 订单批量导出 | 财务对账、月度报告 | LOW | ❌ 缺失 | 竞品标配;广梦云最近大幅提升导出速度 | +| 退款完整流程(申请/审批/退回) | 用户错购、套餐未生效退款场景必须有 | HIGH | ⚠️ 规划中 | 方案 I 已规划 7 个退款接口,尚未实现 | +| 订单超时提醒/催付 | 代理采购大额预存时需要提醒 | LOW | ❌ 缺失 | 已有 30 分钟自动取消,无催付通知 | + +#### 代理/分销 + +| Feature | Why Expected | Complexity | 当前状态 | Notes | +|---------|--------------|------------|---------|-------| +| 代理商资金流水(充值/消费/佣金/提现完整账单) | 代理商需要对自己的每一笔资金进来去出有清晰记录 | MEDIUM | ⚠️ 部分 | 有钱包余额,但完整流水账单(按时间倒序,标注来源类型)不确定是否完整 | +| 套餐分配记录(谁分配了给谁,什么时候) | 分配纠纷溯源必须 | LOW | ⚠️ 部分 | asset_allocation_record 已实现,但可能不全面 | +| 下级代理业绩报表 | 代理商管理下级时必须知道业绩 | MEDIUM | ❌ 缺失 | 有佣金统计 commission_stats,但完整业绩报表(订单数/金额/活跃卡数)缺失 | +| 代理商余额充值(线上转账充值) | 代理预存款是核心资金流入,不能只靠线下 | MEDIUM | ⚠️ 部分 | agent_recharge 存在,但富友支付 JSAPI 接口为留桩(方案 J-1)| + +#### C 端体验 + +| Feature | Why Expected | Complexity | 当前状态 | Notes | +|---------|--------------|------------|---------|-------| +| C 端查询套餐列表与详情 | 用户购套餐前必须看清楚买的是什么 | LOW | ⚠️ 部分 | customer_view_service 存在,但套餐展示完整性待确认 | +| 套餐到期提醒(微信通知/短信) | 用户断网会直接差评,预告到期是行业标配 | MEDIUM | ❌ 缺失 | 无主动推送机制;竞品用微信模板消息/公众号推送 | +| 充值记录/消费历史 | 用户自助查账必须 | LOW | ⚠️ 部分 | 有钱包,但消费历史接口(按时间倒序列表)待确认 | +| 卡/设备转手后钱包自然转移展示 | 钱包绑卡而非绑人的设计已实现,需 C 端有清晰提示 | LOW | ✅ 基本实现 | 设计已对 C 端透出 | + +#### 实名认证(中国市场法规强制) + +| Feature | Why Expected | Complexity | 当前状态 | Notes | +|---------|--------------|------------|---------|-------| +| 二要素身份验证(姓名+身份证) | 工信部实名制要求,物联网卡/号卡必须实名 | MEDIUM | ⚠️ 部分 | 有实名状态轮询,但主动二要素接口验证待确认 | +| 三要素身份验证(姓名+身份证+手机号) | 号卡开户严格场景 | MEDIUM | ❌ 可能缺失 | 广梦云 v2.14.8 专门完善了三要素验证 | +| 实名认证状态全链路可见(平台→代理→C端) | 合规审计必须 | LOW | ⚠️ 部分 | 实名状态字段存在,但链路完整性待确认 | + +--- + +### Differentiators(竞争差异化功能) + +| Feature | Value Proposition | Complexity | 优先级 | Notes | +|---------|-------------------|------------|-------|-------| +| 流量池(多卡共享流量) | 降低单卡超额风险,SIMBOSS/广梦云核心卖点,运营商原生支持 | HIGH | P2 | 当前每卡独立流量;实现需运营商 API 支持 | +| 数据大屏/经营报表(首页工作台) | 让代理一眼看清业绩,销售额/活跃卡数/今日新增 | MEDIUM | P2 | 广梦云 v2.7.13 增加工作台;核心商业决策支持 | +| 开放 API 平台(供企业客户对接) | 企业客户有自有系统,需要 API 接入卡信息/套餐/流量 | HIGH | P2 | Simbest/SIMBOSS 均有 SDK;广梦云有开放平台 | +| 短信通知(套餐到期/流量告警/提现审批) | 微信通知依赖公众号关注,短信是兜底保障 | MEDIUM | P2 | 广梦云 v0.1.6 就支持;触达率关键 | +| 微信公众号模板消息推送 | 在微信生态内操作,公众号消息触达率最高 | MEDIUM | P2 | 广梦云/SIMBOSS 标配;需绑定公众号 APPID | +| 流量告警(达量预警/超量告警) | 避免客户意外断网,自动预警降低客服工单 | MEDIUM | P2 | 轮询系统框架存在 polling_alert;但告警通知渠道未接通 | +| AI 推广文案生成 | 代理商自动生成商品推广素材,降低运营门槛 | MEDIUM | P3 | 广梦云 v2.11 增加 AI 推广文案;ToB 差异化 | +| 电子签约合同(代理商签约) | 合规合同管理,法律保障 | HIGH | P3 | 广梦云 v0.12 增加;降低纠纷风险 | +| LBS 基站定位(卡位置查询) | IoT 场景设备追踪 | HIGH | P3 | iotcard/SIMBOSS 支持;需运营商 API | +| 卡在线诊断(信号/状态/异常) | 客服自助排查工单,减少人工 | HIGH | P3 | iotcard 有"在线诊断"功能 | +| 多支付方式(支付宝 + 微信) | 部分用户无微信支付,支付宝是标配 | MEDIUM | P2 | 当前仅微信 + 富友 + 钱包;缺支付宝 | +| 代理商独立品牌域名/店铺定制 | 白标分销,代理商可用自己品牌 | HIGH | P3 | SIMBOSS/广梦云核心卖点;v2 方向 | + +--- + +### Anti-Features(看起来好,实际容易踩坑的功能) + +| Feature | Why Requested | Why Problematic | Alternative | +|---------|---------------|-----------------|-------------| +| 多租户 SaaS 架构(现阶段) | 想一套系统卖给多个卡商 | 当前 MVP 阶段做 SaaS 重构会推迟上线 6-12 个月;数据隔离模型完全不同 | 单租户先上线验证,v2 规划 SaaS;已在 Out of Scope | +| 设备定期后台轮询 | 想实时掌握设备在线状态 | 高频轮询对运营商 API 有调用限额,极易触发封禁;成本高 | 按需实时拉取(sync-info),已决策 | +| 自动化测试覆盖 | 提升代码质量信心 | 项目明确不做,IoT 卡状态依赖运营商真实回调,Mock 成本极高 | 人工验收 + 生产日志监控;已在项目约束中 | +| 实时所有卡流量推送 | 想让用户实时看到流量 | WebSocket 长连接并发维护成本极高;百万卡量级时不可行 | 轮询拉取 + 流量超阈值主动告警即可 | +| 全球/国际物联网卡管理 | 扩展市场 | 需要对接 Jasper/SIM Alliance 等国际 CMP,运营商 API 完全不同,开发量是现有的 2-3 倍 | 先做国内三大运营商 + 广电,国际方向单独立项 | +| 卡号靓号系统(号卡场景) | 号卡分销平台都有 | 物联网卡无靓号概念;号卡业务涉及选号/归属地/运营商配号,与物联网卡逻辑完全不同 | 物联网卡和号卡维护分离的套餐/渠道体系 | + +--- + +## Feature Dependencies + +``` +退款完整流程(方案I) + └──requires──> 订单系统(已完成) + └──requires──> 代理佣金计算(已完成) + +批量操作(停/复/导出) + └──requires──> 单卡操作接口(已完成) + +套餐到期提醒(微信/短信) + └──requires──> 微信公众号配置(wechat_config 已实现) + └──requires──> 短信服务商接入(❌ 未接入) + +流量告警通知 + └──requires──> 轮询系统 alert_service(已实现,告警规则存在) + └──requires──> 通知渠道(短信/微信,❌ 未打通) + +数据报表/工作台 + └──enhances──> 代理佣金统计(commission_stats 已实现) + └──enhances──> 订单系统(已完成) + +流量池(共享池) + └──requires──> 运营商 API 支持流量池管理(独立对接) + └──conflicts──> 当前每卡独立流量计费模型 + +开放 API 平台 + └──requires──> 认证体系(已完成) + └──enhances──> 企业客户管理(已完成) + +二/三要素实名验证 + └──enhances──> 实名认证轮询(已完成) + └──requires──> 第三方实名验证服务商接入(国政通/公安部接口) +``` + +### Dependency Notes + +- **退款 requires 佣金计算**:退款审批通过后需按比例扣减代理佣金钱包,佣金必须已正确落账 +- **告警 requires 通知渠道**:alert_service 框架已有,但告警结果只写日志,未接短信/微信推送 +- **流量池 conflicts 当前计费模型**:流量池是设备级/卡群组共享,与当前每套餐独立计费逻辑有冲突,需专项设计 +- **二要素验证 requires 第三方接口**:国政通/华道数据/阿里实人认证等,需采购接入 + +--- + +## MVP Definition + +### 当前阶段:修复上线(v1 Production Ready) + +必须完成(对应 PROJECT.md Active 方案 A-J): + +- [x] 实名常量修复(A-1 ~ A-3)— 主链路断裂,P0 +- [x] 充值回调 + 佣金补全(A-4 ~ A-5)— 资金链路,P0 +- [x] 退款完整流程(方案 I)— 用户期望有,缺了影响信任 +- [x] 订单批量导出 — 财务对账必须 +- [x] 卡批量操作(停/复) — 运营必须 +- [x] 富友支付 JSAPI(J-1)— 支付链路完整性 + +### Add After Validation(v1.x,上线后 1-2 月内) + +- [ ] 套餐到期提醒(微信模板消息) — 核心留存功能,上线第一周就会收到投诉 +- [ ] 流量告警通知渠道打通(短信/微信) — polling_alert 框架已有,补上通知 +- [ ] 代理商业绩报表(工作台首页) — 增加代理黏性 +- [ ] 卡状态变更历史 — 减少客服纠纷 + +### Future Consideration(v2+) + +- [ ] 流量池(共享池)— 需运营商 API 合作,独立立项 +- [ ] 开放 API 平台 — 企业客户集成需求 +- [ ] 多租户 SaaS — 卖给其他卡商 +- [ ] LBS 基站定位 — IoT 定位场景 +- [ ] 支付宝支付 — 扩大支付覆盖,目前微信+富友已覆盖主流 + +--- + +## Feature Prioritization Matrix + +### 缺口功能优先级 + +| Feature | User Value | Implementation Cost | Priority | 对应方案 | +|---------|------------|---------------------|----------|---------| +| 退款流程(申请/审批/退回) | HIGH | HIGH | P1 | 方案 I | +| 富友支付 JSAPI 实现 | HIGH | MEDIUM | P1 | 方案 J-1 | +| 卡批量操作(停/复/导出) | HIGH | LOW | P1 | 新增 | +| 订单批量导出 | HIGH | LOW | P1 | 新增 | +| 套餐到期微信推送 | HIGH | MEDIUM | P1 | 新增 | +| 流量告警通知渠道 | HIGH | MEDIUM | P1 | 补全 alert_service | +| 代理商资金流水完整账单 | MEDIUM | LOW | P2 | 补全 | +| 代理商业绩报表(工作台) | MEDIUM | MEDIUM | P2 | 新增 | +| 卡状态变更历史 | MEDIUM | LOW | P2 | 新增 | +| 二要素实名验证接口 | MEDIUM | MEDIUM | P2 | 新增 | +| 流量池(共享池) | HIGH | HIGH | P3 | v2 | +| 开放 API 平台 | MEDIUM | HIGH | P3 | v2 | +| LBS 定位 | LOW | HIGH | P3 | v2 | + +**Priority key:** +- P1: 必须有,影响上线质量 +- P2: 上线后优先补充,影响留存 +- P3: 竞争差异化,长期方向 + +--- + +## Competitor Feature Analysis + +| Feature | 广梦云(竞品) | SIMBOSS(竞品) | Simbest(竞品) | 君鸿当前状态 | +|---------|--------------|---------------|---------------|------------| +| 批量停/复/注销 | ✅ 有 | ✅ 有 | ✅ 有 | ❌ 缺 | +| 批量数据导出 | ✅ 有(高速) | ✅ 有 | ✅ 有 | ❌ 缺 | +| 流量告警 + 通知 | ✅ 微信公众号告警 | ✅ 达量告警 | ✅ 有 | ⚠️ 框架有,通知未打通 | +| 套餐到期通知 | ✅ 微信模板消息 | ✅ 公众号推送 | ✅ 有 | ❌ 缺 | +| 退款流程 | ✅ 一键退款(卡板/套餐) | ✅ 有 | ✅ 有 | ⚠️ 规划中(方案 I) | +| 代理资金流水 | ✅ 完整账单 | ✅ 有 | ✅ 有 | ⚠️ 基础有,完整性待确认 | +| 多支付宝支付 | ✅ 支付宝/微信/当面付 | ✅ 有 | ✅ 有 | ⚠️ 微信+富友,缺支付宝 | +| 流量池共享 | ✅ 共享池 + 月账单 | ✅ 流量共享 | ✅ 有 | ❌ 缺(v2) | +| 开放 API | ✅ 开放平台 + SDK | ✅ 多语言 SDK | ✅ 有 | ❌ 缺(v2) | +| 业绩报表/工作台 | ✅ v2.7 增加工作台 | ✅ 数据助力运营 | ✅ 营收成本核算 | ❌ 缺 | +| 代理商独立域名 | ✅ 品牌定制 | ✅ 个性化域名 | ✅ 有 | ❌ 缺(v2/SaaS) | +| 设备 WiFi 远程控制 | ✅ v2.13 增加 | — | — | ⚠️ 设备 sync-info 规划中 | +| 实名二/三要素验证 | ✅ 国政通对接 | ✅ 有 | ✅ 有 | ⚠️ 部分 | + +--- + +## 中国市场特殊要求 + +### 监管合规 +- **物联网实名制**(工信部要求):所有物联网卡必须实名,平台需支持实名状态追踪和验证 +- **号卡实名**(2021 年起更严):号卡开户必须姓名+身份证+手机号三要素,平台需有三证上传 +- **广电运营商支持**:2022 年起广电(第四运营商)加入物联网卡市场,竞品已快速对接 +- **运营商 API 依赖**: + - 移动:CT-Boss、PBoss + - 电信:CMP、DCP + - 联通:Jasper、CMP + - 广电:广电直连 API + +### 支付合规 +- 提现需支持**灵活用工平台**(代理商收入合规开票) +- 大额打款需支持**对公转账**或**企业支付宝批量打款** + +--- + +## Sources + +- **广梦云更新日志**(https://docs.facms.cn)— 2019-2026 完整版本历史,揭示了市场真实功能演进路径 +- **SIMBOSS 产品页**(https://www.simboss.com)— 核心竞品,国内头部物联网卡平台 +- **Simbest 平台**(https://www.simbest.cn)— 专注三网对接的分销管理平台 +- **iotcard IOT CardBOSS**(https://www.iotcard.com/platform)— 运营商系统深度融合的企业级方案 +- **项目 PROJECT.md + ARCHITECTURE.md** — 已实现功能基线,方案 A-J 规划 +- **置信度评估**: + - Table Stakes 功能:HIGH(多个竞品交叉验证) + - Differentiators:MEDIUM(竞品分析 + 市场观察,非用户调研) + - 合规要求:HIGH(工信部政策 + 竞品普遍实现) + +--- +*Feature research for: 物联网卡/号卡/设备全生命周期管理平台(中国市场)* +*Researched: 2026-03-27* diff --git a/.planning/research/PITFALLS.md b/.planning/research/PITFALLS.md new file mode 100644 index 0000000..c359192 --- /dev/null +++ b/.planning/research/PITFALLS.md @@ -0,0 +1,245 @@ +# Pitfalls Research + +**Domain:** IoT 卡管理平台 — SIM 卡分销 + 多级代理佣金 + 异步套餐激活 +**Researched:** 2026-03-27 +**Confidence:** HIGH(基于真实代码库分析 + 完整规格书审查) + +> **已知问题不重复**:实名状态常量不一致、Asynq 任务依赖注入不完整、并发乐观锁/悲观锁漏用、流量数据被上游重置覆盖——这四类已在修正方案中覆盖,不再列入本文档。 + +--- + +## Critical Pitfalls + +### Pitfall 1: 佣金差额基准字段(sellerCostPrice)多处同类 Bug + +**What goes wrong:** +`sellerCostPrice = buyerCostPrice` 在 `internal/service/order/service.go` 中出现 **6 次**(第 187、262、472、548、734、809 行),只有场景 5(代理代购下级)被规格书点名,其余 5 处是否也存在同样语义错误尚未被审查。每一处错误都会导致整个上级佣金链计算为 0。 + +**Why it happens:** +场景 5 是"代理帮下级代理购包",此时 `sellerCostPrice` 表示"卖出方自己的成本价"(用于和再上级计算差额),但开发者误用了"买方(下级)的成本价"。五种购买场景共用的模板代码一起复制了这个错误。 + +**How to avoid:** +1. 修复 A-5 时,用 `grep` 全量检索 `sellerCostPrice = buyerCostPrice`,对每一行逐场景验证语义 +2. 添加注释:`sellerCostPrice 含义 = 本订单"卖家"向其上级结算时使用的成本基准(不是买家看到的价格)` +3. 为每种购买场景(5 种)单独写一个私有方法,避免在同一个大函数里 if-else 六分支 + +**Warning signs:** +- 某代理参与了套餐分配(有授权),但佣金记录里上级代理的 `amount` 持续为 0 +- DBHub 查询 `tb_commission_record WHERE amount = 0 AND commission_source = 'cost_diff'` 有大量记录 + +**Phase to address:** 方案 A(A-5)实施时,同步修复全部 6 处,不只修 1 处 + +--- + +### Pitfall 2: 退款佣金回扣的浮点精度丢失 + +**What goes wrong:** +方案 I-3 的退款比例计算: +```go +ratio := float64(refund.ApprovedRefundAmount) / float64(refund.ActualReceivedAmount) +deductAmount := int64(float64(commission.Amount) * ratio) +``` +`float64` 乘除后截断到 `int64` 会丢失 1 分精度。多级佣金链叠加后,累计误差可能造成"批量退款后所有上级代理多扣或少扣总计数十元"。 + +**Why it happens:** +Go 没有内置 Decimal 类型,金融计算的精度问题容易被忽视。项目其他地方(如提现手续费)用的是整数基点计算(`int64`),但退款比例必须用小数,开发者自然选择了 `float64`。 + +**How to avoid:** +- 退款比例乘法使用整数算术代替:`deductAmount = commission.Amount * refund.ApprovedRefundAmount / refund.ActualReceivedAmount`(全用 `int64`,先乘后除,结果向下取整) +- 或引入 `math/big.Rat` 进行精确比例计算 +- 绝对不要在金额计算链路中出现 `float64`(余额、成本价、佣金均以分为单位存储,应坚持整数运算) + +**Warning signs:** +- 退款后批量查各级代理钱包,总扣减金额与退款金额比例不相符 +- 极端案例:`ApprovedRefundAmount=1, ActualReceivedAmount=3` → `ratio=0.3333...` → `commission*0.3333` 截断 + +**Phase to address:** 方案 I(退款功能)实现时在 I-3 处预防 + +--- + +### Pitfall 3: 多级分佣树形遍历中的循环引用与孤岛 + +**What goes wrong:** +`CalculateCostDiffCommission` 通过 `shop.ParentID` 向上遍历代理链,直到 `ParentID == nil`。如果数据库中代理层级数据被人工修改,出现: +- **循环引用**:A 的父是 B,B 的父是 A → 无限循环直到超时 +- **孤岛**:某代理的 `parent_id` 指向一个已删除的店铺(软删除残留)→ `GetByID` 返回 Not Found,`break` 掉链路,上游代理丢失佣金 + +**Why it happens:** +项目禁止外键约束(见 AGENTS.md),代理层级完全依赖代码维护。店铺软删除时没有检查是否有子代理仍在引用它。递归遍历没有深度上限(最多 7 级,但没有强制)。 + +**How to avoid:** +1. 在遍历循环加深度计数器:`for depth := 0; depth < 8 && currentShopID != nil; depth++` +2. 用 `visited map[uint]bool` 检测循环引用,发现后立即 `break` 并打告警日志 +3. 店铺删除接口增加"有子代理时拒绝删除"的前置检查(参考 H-4 Series 的做法) +4. 每日定时任务扫描层级数据一致性(检查 `parent_id` 指向已删除记录) + +**Warning signs:** +- 某个订单的佣金任务卡在 Worker 里超时重试 +- `tb_shop` 中存在 `parent_id` 指向 `deleted_at IS NOT NULL` 的记录 + +**Phase to address:** 方案 A(A-5 佣金修复)+ 方案 H 小修阶段 + +--- + +### Pitfall 4: 异步任务幂等键缺失导致佣金重复计算 + +**What goes wrong:** +`enqueueCommissionCalculation` 在 service 层多处调用(第 303、618、864、1121、1565、1617 行),但没有业务层面的幂等防护。Asynq 默认重试策略 + 多个触发点并发时,同一个 `order_id` 的佣金计算任务会被入队多次。`CalculateCommission` 开头虽然检查了 `CommissionStatusCalculated`,但在高并发下两个 Worker 可能同时读到 `status != calculated`,然后同时写入两份佣金记录。 + +**Why it happens:** +Order Service 的支付回调(微信/富友)与自动购包任务(A-4 新增)在成功时都会调用 `enqueueCommissionCalculation`,单个订单存在多条触发路径。`CalculateCommission` 的幂等检查不是原子操作(读 status + 写 records 之间有窗口)。 + +**How to avoid:** +1. 给 `TaskTypeCommission` 加 Asynq 唯一任务选项:`asynq.TaskID(fmt.Sprintf("commission:%d", orderID))` → Asynq 保证相同 TaskID 只入队一次 +2. 在 `CalculateCommission` 内部,将"读取 status + 写入 records + 更新 status"放在同一个事务内,利用 `UPDATE tb_order SET commission_status=? WHERE id=? AND commission_status != 'calculated'` 的 RowsAffected 做原子幂等 + +**Warning signs:** +- `tb_commission_record` 中同一个 `order_id` 出现两条或多条记录 +- 代理钱包余额远高于预期 + +**Phase to address:** 方案 A(A-4 自动购包)实施时同步处理 + +--- + +### Pitfall 5: 运营商 API 回调重放与支付状态对齐 + +**What goes wrong:** +微信支付回调(`HandlePaymentCallback`)使用条件更新做幂等:`WHERE payment_status = 'pending'`,这是正确的。但富友支付回调(方案 J-1,当前是留桩)一旦实现,如果直接复制微信回调逻辑而忘记验签,攻击者可以伪造回调通知触发充值到账。另一个场景:运营商 Gateway 的停复机回调(`StopResumeCallback / ResumeCallback`,方案 F-6)目前 bootstrap 中未注入,一旦注入后,回调的幂等保护是否健全尚未验证。 + +**Why it happens:** +第三方 API 回调通常有以下特点:(1) 会重复发送直到收到成功响应,(2) 需要验证签名,(3) 业务状态变更必须是幂等的。开发者经常在实现新回调时遗漏验签步骤,尤其是"复制微信回调改改"时。 + +**How to avoid:** +1. 富友支付回调(J-1)实现时:先验签(`pkg/fuiou/` 已有签名工具),再做业务处理 +2. 所有回调 Handler 必须在最开头验签,验签失败直接返回 `400`,不进入业务逻辑 +3. 停复机回调(F-6 注入后):检查 `WHERE status = 'stopping'` 条件更新,RowsAffected 为 0 则幂等跳过 + +**Warning signs:** +- 回调接口没有签名验证中间件或函数调用 +- 日志中同一笔充值/停复机出现两次"处理成功" + +**Phase to address:** 方案 F(F-6 注入)、方案 J(J-1 富友支付实现) + +--- + +## Technical Debt Patterns + +| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable | +|----------|-------------------|----------------|-----------------| +| Handler 层直接访问 DB(见 F-4) | 少写 Service 方法,快速出货 | 绕过数据权限过滤层,越权漏洞温床;Service 层单测无法覆盖 | Never(项目明确禁止) | +| `cards, _ :=` 忽略 error(见 F-3) | 代码简洁 | 上游查询失败时静默返回空数据,下游表现为"数据丢失"而非报错,极难定位 | Never | +| 任务断链用 `RPush` 降级(见 A-3 临时方案) | 临时绕过依赖注入问题 | 形成两套并行队列机制,消费者只消费 Asynq,Redis List 堆积无人处理 | Never(应立即修复) | +| `sellerCostPrice = buyerCostPrice` 5 处场景(见 Pitfall 1) | 逻辑统一,代码复制快 | 多个购买场景佣金链全部归零,代理分佣核算错误 | Never | +| 留桩函数(FuiouPayJSAPI 等)返回业务错误 | 功能模块先占位 | 在线上环境直接暴露"功能未实现"给用户,影响信任度 | MVP 阶段可以,但必须有 TODO 标记和发版前检查清单 | +| 激活配置两步 UPDATE 无锁(见 C-3) | 代码简单 | 并发切换配置时可出现两条 `active=true`,支付配置/提现配置混乱 | Never | +| `gatewayClient=nil` 时返回成功(见 H-2) | 开发环境无 Gateway 时流程走通 | 本地 DB 标记为停机成功,但运营商侧未操作;数据一致性破坏 | Never(测试环境应 mock,不应用 nil 返回成功) | + +--- + +## Integration Gotchas + +| Integration | Common Mistake | Correct Approach | +|-------------|----------------|------------------| +| 运营商 Gateway(sync-info) | 按 IMEI 调用时长度判断逻辑(>15 位用 ICCID,=15 位用 IMEI,=11 位用 SN)写反或遗漏,导致用错标识调用 API | 严格按文档:`len > 15 → ICCID`,`len == 15 → IMEI`,`len == 11 → SN`;写单独函数 `resolveCardNo(device)` 封装判断,避免散落多处 | +| 运营商 Gateway(停复机) | `gatewayClient == nil` 时假装成功(见 H-2),或 Gateway 超时时直接 `return err` 而不记录"本地已标记停机但运营商未执行"的中间态 | 停复机必须是原子的:要么 Gateway + DB 一起成功,要么一起失败;Gateway 失败时 DB 不更新,并告警 | +| 微信支付回调 | 回调成功响应后再做业务处理 → 微信认为"回调已处理",但业务处理失败时没有重试机会 | 先做幂等业务处理,成功后再返回 `{"code": "SUCCESS"}`;失败时返回非 SUCCESS,触发微信重发 | +| 富友支付(J-1 待实现) | 直接复制微信支付回调,遗漏富友私钥验签步骤 | 富友使用 RSA 私钥签名,`pkg/fuiou/` 已有 `VerifySign()` 方法,回调首行必须调用 | +| 运营商流量数据(联通 27 号重置) | 用当前值直接覆盖 `current_month_usage_mb`(E-2 问题),重置日后数据归零 | 改为增量累加;重置检测窗口:`当天 ∈ [reset_day-1, reset_day+1]` 才认为是正常重置,否则告警 | +| Asynq 任务注册 | 任务处理器注册(`mux.HandleFunc`)和 Scheduler 调度是两回事,注册后不一定会被调度(见 H-1 polling:protect) | 新增任务类型时检查清单:① 定义常量 → ② 实现 Handler → ③ 在 `RegisterHandlers()` 里注册 → ④ 在 Scheduler 里调度 | + +--- + +## Performance Traps + +| Trap | Symptoms | Prevention | When It Breaks | +|------|----------|------------|----------------| +| 递归查询下级店铺(`GetSubordinateShopIDs`)没有缓存 | 每次 HTTP 请求触发 PostgreSQL WITH RECURSIVE 查询,7 层 × N 个代理时极慢 | 已有 Redis 缓存 30 分钟机制,确保所有数据权限过滤路径都走缓存 | 代理商数量超过 100 时 P99 开始劣化 | +| 轮询任务批量处理时 N+1 查询 | Worker CPU 高但 DB 慢,`polling_handler.go` 里对每张卡单独查套餐 | 轮询任务改为批量拉取卡的套餐信息,一次 `WHERE iot_card_id IN (?)` | 轮询卡数量超过 1000 时 | +| `tb_card_daily_usage` 缺少分区 | 流量历史查询(跨月范围)全表扫描 | 建立 `idx_card_daily_date` 索引(E-1 已规划);如果日后数据量大,按月分区 | 单卡每日一条,100 万卡 × 365 天 = 3.65 亿行 | +| 流量 Redis key 不设 TTL | `traffic:daily:{card_id}:{date}` 无 TTL 永久占用内存 | 设置 48h TTL(E-1 已规划 `Expire(ctx, key, 48*time.Hour)`);落盘任务消费后 `DEL` key | Redis 内存超限时所有业务 key 被 evict | +| 套餐列表接口无分页(见 H-3) | C 端用户请求套餐列表时一次返回全量,单卡绑 100+ 套餐时响应体超大 | 强制分页,`pageSize` 默认 50 最大 100 | 套餐历史积累超过 100 条时 | +| 1 个设备绑 4 张卡,佣金计算 4 次 | 设备订单的一次性佣金被触发 4 次(每张绑定卡各触发一次) | 设备级一次性佣金必须以设备为单位去重(README 已说明"代理分佣只计算一次");检查 `triggerOneTimeCommissionForDeviceInTx` 是否有设备级幂等 | 销售设备套餐时 | + +--- + +## Security Mistakes + +| Mistake | Risk | Prevention | +|---------|------|------------| +| 企业账号 `Resolve` 接口被错误拦截(见 B-1)→ 修复时移除拦截,但未验证越权范围 | 企业账号能查询到不属于自己的卡(只要知道 ICCID) | `Resolve` 恢复后,必须在 Store 层加企业数据范围过滤:`WHERE enterprise_id = ? OR shop_id IN (?)` | +| C 端 Handler 直接 `h.db.SkipPermissionCtx` 访问 DB(见 F-4) | 个人客户能构造请求查看他人订单 | Handler 绑上游数据权限强制走 Service 层;Service 层验证 `buyer_id = 当前用户绑定资产 ID` | +| 金额字段以"分"为单位存储,但前端传来 float(如 "0.01" 元)未乘 100 | 金额精度丢失,1 分变 0 分 | 所有金额 API 明确文档"单位:分,整数";DTO 验证标签加 `min=1`;前端统一在传参前乘 100 | +| 停复机操作无 RBAC 权限检查,任何登录的代理账号都可操作 | 代理 A 可以通过 ICCID 停掉代理 B 的卡 | 停复机前检查:`卡的 shop_id ∈ 当前代理的下级店铺范围`;企业账号只能操作被分配给自己企业的卡 | +| 提现申请中拒绝 remark 前端可传空(见 C-2) | 审批人拒绝时没有填写拒绝原因,代理投诉无凭据 | `Reject` Handler 必须调用 `validator.Validate(&req)` 且 DTO 字段 `validate:"required"` | + +--- + +## UX Pitfalls + +| Pitfall | User Impact | Better Approach | +|---------|-------------|-----------------| +| 套餐激活失败静默(`break` 不创建记录,见 F-1)| 代理看到"下单成功"但实际套餐没有激活,多次联系客服才发现 | 改为创建 `status=99` 待审记录并通知平台,代理端展示"套餐待激活,平台处理中" | +| 自动购包任务失败后 `auto_purchase_status` 停留在 `pending` | 客户充值后一直看到"余额已到账,套餐未开通",不知是否需要手动购买 | Worker 失败超出最大重试次数后,将 `auto_purchase_status` 更新为 `failed`,前端展示"自动购包失败,请手动购买"并推送通知 | +| 退款状态"已退回(可重提)"语义不清 | 用户不知道"退回"意味着"申请被退回请你修改重提"还是"钱退回给你了" | 状态名改为"已驳回(可重提)"或"需修改重提",避免与"金钱退回"混淆 | +| 停复机 Gateway 未配置时返回错误(H-2 修复后)| 已有卡的用户突然发现"停机功能不可用"且错误信息是技术语言 | 错误消息对外返回"停机功能暂时不可用,请联系客服",内部日志详细记录"Gateway 未配置" | +| 提现手续费在 `FeeRate` 字段注释中用"万分比"但代码里有地方用"基点(100=1%)"(两种不同描述)| 代理计算实际到账金额与系统计算不符,投诉频发 | 统一文档:全部使用"基点(100=1%)";前端展示时换算为百分比:"手续费率:1%(100基点)" | + +--- + +## "Looks Done But Isn't" Checklist + +- [ ] **佣金计算完整性**:任务注册(`mux.HandleFunc`)+ Scheduler 调度 + 入队调用 + 幂等检查——四项缺一不可,逐项确认 +- [ ] **Asynq 任务处理器注入**:每个新增的 Task Handler struct,其构造函数的所有参数是否都在 `pkg/queue/handler.go` 的对应 `registerXxx()` 里传入(A-3、A-4 教训) +- [ ] **富友支付回调**:实现后验签是否在第一行,是否有幂等检查,是否有超时重试机制 +- [ ] **设备批量导入**:Excel 解析 → DeviceRow → model.Device 的每个字段贯通(IMEI 教训,见 A-6);未来新增 Device 字段时同步更新 Excel 解析规则 +- [ ] **回调注入到 bootstrap**:新增的回调方法(StopResumeCallback、ResumeCallback)是否在 `internal/bootstrap/services.go` 中真正调用了(F-6 教训) +- [ ] **停复机 Gateway nil 检查**:所有调用 `gatewayClient` 方法的地方,`nil` 情况是否明确拒绝而非静默成功(H-2 教训) +- [ ] **金额精度**:所有含 `float64` 转 `int64` 的金额运算,是否改为纯整数运算(退款比例 Pitfall 2 教训) +- [ ] **佣金链遍历深度上限**:`CalculateCostDiffCommission` 遍历循环是否有深度检测和循环引用检测(Pitfall 3 教训) + +--- + +## Recovery Strategies + +| Pitfall | Recovery Cost | Recovery Steps | +|---------|---------------|----------------| +| 佣金差额多处 `sellerCostPrice` 错误 | HIGH | 逐笔订单重新计算差价佣金;DBHub 查出所有 `amount=0` 的 cost_diff 记录;人工复算并补入差额;通知代理补发佣金 | +| 浮点金额精度错误(退款回扣)| MEDIUM | 查出所有已执行的退款佣金扣减记录;计算正确值与实际扣减值之差;通过管理端手动调整代理佣金钱包余额 | +| 佣金任务重复入队导致重复发放 | HIGH | 按 `order_id` 分组查 `tb_commission_record`,找出 count > 正常值的订单;手动删除多余记录并扣减对应代理钱包;加 `asynq.TaskID` 幂等后部署 | +| 流量数据被运营商重置归零(已发生) | MEDIUM | 从 Gateway 历史接口补拉历史流量数据(如有);未来改为增量累加后运营商重置不再影响(方案 E) | +| 激活配置出现两条 `is_active=true`(并发问题) | LOW | DBHub 找出多余记录,手动 UPDATE 置 `is_active=false`;加锁后重部署 | +| 停复机 Gateway nil 假装成功(DB 与运营商不一致)| HIGH | DBHub 查出 `status='stopped'` 但 Gateway 实际在运行的卡;逐卡对比并重发停机指令;F-6 注入后历史不一致需人工修正 | + +--- + +## Pitfall-to-Phase Mapping + +| Pitfall | Prevention Phase | Verification | +|---------|------------------|--------------| +| `sellerCostPrice` 多处同类 Bug | 方案 A(A-5 实施)| `grep -n "sellerCostPrice = buyerCostPrice"` 结果为 0 | +| 退款浮点精度丢失 | 方案 I(I-3 实现)| Code Review 检查不含 `float64` 金额运算 | +| 佣金树形遍历循环引用/孤岛 | 方案 A + 方案 F | 深度计数器 + visited map 存在于 `CalculateCostDiffCommission`;店铺删除有前置检查 | +| 佣金任务重复入队 | 方案 A(A-4 实施)| `asynq.TaskID` 选项用于 commission 任务;`task_id` 可在 Asynq 监控台验证唯一性 | +| 运营商回调未验签 | 方案 J(J-1 富友支付)、方案 F(F-6)| Code Review 确认每个回调 Handler 首行有签名验证 | +| Handler 直接访问 DB | 方案 F(F-4 迁移)| `grep -rn "h\.db\." internal/handler` 结果为 0 | +| 任务"注册未调度"模式 | 每次新增 Asynq 任务 | 新增任务时 Code Review 检查清单:4 项全勾选 | +| bootstrap 回调未注入 | 方案 F(F-6)| `grep -rn "SetStopResumeCallback\|SetResumeCallback" internal/bootstrap` 有调用 | +| Gateway nil 假装成功 | 方案 H(H-2)| `grep -n "return nil" internal/service/iot_card/stop_resume_service.go` 不出现在 `gatewayClient == nil` 分支 | +| 金额 `float64` 误用 | 全程 Code Review | `grep -rn "float64.*[Aa]mount\|[Aa]mount.*float64" internal/service` 结果为 0 | + +--- + +## Sources + +- `internal/service/commission_calculation/service.go`:佣金树形遍历实现(直接代码审查) +- `internal/service/order/service.go`:5 种购买场景 + sellerCostPrice 赋值(grep 分析,第 187、262、472、548、734、809 行) +- `internal/task/polling_handler.go`:流量计算逻辑(直接代码审查,第 388-437 行) +- `internal/service/iot_card/stop_resume_service.go`:停复机 nil 问题(直接代码审查) +- `.sisyphus/plans/修正业务-完整方案.md`:方案 A-J 完整规格书(主要信息来源) +- `pkg/queue/handler.go`:Asynq 任务注册 vs 调度模式(直接代码审查) +- `internal/model/financial.go`:金额字段均以 `int64` 分为单位(设计确认) +- Go 官方 `float64` 精度限制文档(内置知识,HIGH 置信度) + +--- +*Pitfalls research for: IoT 卡管理平台(SIM 卡分销 + 多级代理佣金 + 异步套餐激活)* +*Researched: 2026-03-27* diff --git a/.planning/research/STACK.md b/.planning/research/STACK.md new file mode 100644 index 0000000..46a419d --- /dev/null +++ b/.planning/research/STACK.md @@ -0,0 +1,353 @@ +# Stack Research + +**Domain:** IoT 卡管理平台 — SIM 卡全生命周期 + 代理分销 + 分佣结算 +**Researched:** 2026-03-27 +**Confidence:** MEDIUM-HIGH(核心技术栈 HIGH,外围选项 MEDIUM) + +--- + +## 结论先行:当前技术栈评估 + +**整体结论:当前技术选型对于该领域是合理的,无需大规模替换。** +存在 3 个明确的技术债和 1 个值得关注的升级时机。 + +| 组件 | 当前版本 | 最新版本 | 状态 | 建议 | +|------|---------|---------|------|------| +| Fiber | v2.52.9 | **v3.1.0** | ⚠️ 大版本落后 | 保持 v2,不升级(见分析) | +| GORM | v1.31.1 | v1.31.x | ✅ 最新 | 继续使用 | +| Asynq | v0.25.1 | **v0.26.0** | ⚠️ 落后一个版本 | 可选升级(安全依赖升级) | +| go-redis | v9.7.0 | v9.18.0 | ⚠️ 落后较多 | 建议升级(无 API 破坏性变更) | +| sonic | v1.14.2 | v1.14.x | ✅ 近期版本 | 继续使用 | +| PowerWeChat | v3.4.38 | v3.4.38 | ✅ 最新 | 继续使用 | +| Go runtime | 1.25.x | 1.25.x | ✅ 最新 | 继续使用 | + +--- + +## 核心技术栈 + +### 一、HTTP 框架 — Fiber v2(**继续坚守**) + +| 项目 | 内容 | +|------|------| +| 当前版本 | v2.52.9(已是 v2.x 最新) | +| 最新大版本 | v3.1.0(2026-02-02 正式发布) | +| **不升级的原因** | v3 有重大破坏性变更,迁移成本高,且本项目有大量存量代码 | + +**v3 核心破坏性变更(对本项目影响分析):** + +1. **`c.UserContext()` 被移除** → `Ctx` 本身现在实现 `context.Context`,直接传 `c` 即可。 + - **影响:**本项目全代码库大量使用 `c.UserContext()` 传递用户 Context 到 Service/Store 层,这是最大的迁移成本。 + - **估算:**全局搜索 `UserContext()` 调用点,预估 100+ 处改动。 + +2. **`c.BodyParser()` 移除** → 改用 `c.Bind().Body()` + - **影响:**所有 Handler 层的请求解析代码需要改写。 + +3. **路由注册 `Add` 方法签名变更** → 影响 `internal/routes/registry.go` 的统一注册逻辑。 + +4. **`app.Mount()` 移除** → 使用 `app.Use()`。 + +**置信度:HIGH**(官方文档 https://docs.gofiber.io/whats_new/ 已确认) + +**建议:** 保持 v2.52.x,待 v2 EOL 公告或下一个重大重构机会再考虑升级。v2 安全维护至少到 2026 年底,风险可控。 + +--- + +### 二、ORM — GORM v2(**继续使用,无更好替代方案**) + +**2026 年 Go ORM 格局:** + +| ORM | 定位 | 适合本项目 | +|-----|------|-----------| +| **GORM** | 全功能 ORM,code-first,ActiveRecord 风格 | ✅ **最适合** | +| sqlc | 编译时类型安全,从 SQL 生成 Go 代码 | ❌ 需要从头重写所有查询 | +| sqlx | SQL 轻量封装,接近原生 database/sql | ❌ 需要全部改写,收益有限 | +| ent | 图模型 ORM,compile-time type safety | ❌ 学习成本高,编译慢,不适合现有代码库 | + +**为什么 GORM 对本项目是正确选择:** +- CRUD 密集型应用(卡管理、套餐管理、订单、佣金),GORM 的约定优于配置大幅减少样板代码 +- 软删除、钩子(Hooks)、事务等特性本项目大量依赖 +- 当前 v1.31.x 已是最新,无需升级 + +**GORM 的已知性能问题(针对本项目的建议):** +- 多级代理佣金树查询(最深 7 层)不应使用 GORM `Preload`,而应使用原生 `WITH RECURSIVE` CTE +- 批量导入(ICCID Excel)应使用 `db.CreateInBatches()` 而非逐行 `Create()` + +**置信度:HIGH**(多个 2026 年对比文章,主流观点一致) + +--- + +### 三、任务队列 — Asynq(**继续使用,考虑升级到 v0.26.0**) + +**最新版本:** v0.26.0(2026-02-03 发布) + +| 版本 | 关键变更 | +|------|---------| +| v0.25.1(当前) | 安全依赖升级 + 增加 `RedisUniversalClient` 支持 | +| **v0.26.0** | 增加 Task Headers 支持(PR #1070)+ TLS 选项 + 最低 Go 版本升至 1.24 | + +**为什么不替换 Asynq:** +在 IoT 卡管理场景中,Asynq 的优势: +- 基于 Redis(与现有基础设施共享,无额外运维成本) +- 内置定时任务调度(Scheduler)适合轮询任务场景 +- 任务可视化(Asynqmon Web UI)在生产调试中有价值 +- 竞品(Machinery、Watermill)依赖更重,对单节点场景没有额外收益 + +**建议:** 升级到 v0.26.0(`go get github.com/hibiken/asynq@v0.26.0`),主要收益是上游安全依赖修复,API 完全兼容。 + +**置信度:HIGH**(GitHub Releases 直接验证) + +--- + +### 四、Redis 客户端 — go-redis v9(**建议升级**) + +**当前版本:** v9.7.0 +**最新版本:** v9.18.0(2026-02-16) + +v9.7.0 → v9.18.0 跨越 11 个 patch 版本,均为向后兼容。建议升级原因: +- 包含若干 bug 修复和性能改进 +- 升级命令:`go get github.com/redis/go-redis/v9@v9.18.0` + +**置信度:MEDIUM**(pkg.go.dev 直接确认版本) + +--- + +### 五、JSON 序列化 — sonic(**继续使用**) + +**sonic(bytedance/sonic)** 在 2025 年基准测试中仍是 Go 生态最快的 JSON 库: + +| 库 | 相对速度 | 适用场景 | +|----|---------|---------| +| **sonic** | ~3-4x 快于 std | 高并发 API,大 payload(如 Excel 导入响应) | +| encoding/json v2(Go 1.25 内置) | ~1.8x 快于 std | 标准库,无需引入依赖 | +| encoding/json(std) | 1x 基线 | 无额外依赖场景 | + +**注意:** Go 1.25 引入了 `encoding/json/v2`,性能约为 sonic 的 50%,但消除了历史行为不一致问题。对于本项目,sonic 仍是正确选择:本项目已集成且使用 Fiber 的 sonic 替换,迁移收益不大。 + +**置信度:HIGH**(2025-09 Reddit benchmark + Trendyol 生产迁移案例) + +--- + +## 领域特有技术栈缺口分析 + +### 缺口 1:金融精度问题 ⚠️(**重大技术债**) + +**问题描述:** +IoT 卡管理平台涉及两类精度敏感计算: +1. **货币金额**:钱包余额、佣金金额(差价佣金、一次性佣金)、套餐价格 +2. **流量计费**:MB/GB 流量用量计算、按比例退款 + +**当前实现风险:** +- 余额字段使用 `int64`(存储"分")—— **钱包余额做法正确** +- 但佣金链计算(差价 = 上层价格 - 下层成本)中,如果涉及百分比分佣(如"提成 15%"),用 `float64` 计算后再转 `int64` 会有精度损失 + +**标准做法(2025 年行业共识):** + +``` +选项 A:纯整数(分/厘)存储,避免所有浮点运算 + - 适合固定金额计算 + - 本项目钱包余额已是此做法 ✅ + +选项 B:shopspring/decimal(用于百分比/比例计算) + - 适合佣金比例计算、退款按比例扣减 + - 7000+ GitHub stars,已是 Go 社区财务计算标准库 + +选项 C:go-money(Rhymond/go-money) + - 适合多币种场景,本项目不需要 +``` + +**建议:** +引入 `github.com/shopspring/decimal` v1.4.0,**仅用于**: +- 佣金比例计算(如方案 I-3:退款按比例扣减代理佣金) +- 流量按比例计费(方案 E 的流量体系改革) + +不需要全局替换 int64 余额字段,仅在需要乘除法的计算环节使用 decimal,最终结果转 int64 存储。 + +**置信度:HIGH**(Modern Treasury 官方博客 2025-08 + shopspring/decimal 7286 stars) + +--- + +### 缺口 2:多级代理查询性能 ⚠️(**架构风险**) + +**问题描述:** +本项目代理层级最多 7 层,`GetSubordinateShopIDs()` 使用 PostgreSQL `WITH RECURSIVE` CTE 查询,结果缓存 30 分钟。这个架构是正确的。 + +**潜在风险:** +- 佣金链计算时,N 层递归分佣可能退化为 N 次单独 DB 查询(N+1 问题) +- 7 层 × 每层查询 = 最坏 7 次 DB 往返,而 DB 查询要求 < 50ms + +**2025 年标准方案:** +对于多级分佣树计算,行业标准是一次性将整条链路数据加载到内存,而非逐层查询: + +```sql +-- 一次性获取完整佣金链(从叶子节点到根节点) +WITH RECURSIVE commission_chain AS ( + SELECT shop_id, parent_id, level, 0 AS depth + FROM tb_shop WHERE id = $1 + UNION ALL + SELECT s.shop_id, s.parent_id, s.level, cc.depth + 1 + FROM tb_shop s + INNER JOIN commission_chain cc ON s.id = cc.parent_id +) +SELECT * FROM commission_chain ORDER BY depth; +``` + +当前代码已经有 `WITH RECURSIVE` 基础,但需要确认佣金计算是否利用了这个能力。 + +**置信度:MEDIUM**(基于代码库 PROJECT.md 分析 + PostgreSQL 官方文档) + +--- + +### 缺口 3:可观测性 ⚠️(**生产上线前需解决**) + +**问题描述:** +当前技术栈的可观测性仅有: +- 应用日志(Zap) +- 访问日志 +- Asynq 任务状态(via Asynqmon 可选) +- 健康检查 `GET /health` + +**缺失:** +- 没有指标(Metrics)收集 +- 没有链路追踪(Tracing) +- 没有告警(Alerting) + +**对于当前阶段(MVP 修复上线):** 可以暂时接受,PROJECT.md 的 Out of Scope 已明确 "P2-6 告警通知渠道 — 无关紧要,暂不做"。 + +**但如果平台上线后扩展:** +- 轻量级方案:Prometheus + Grafana(已是 Go 社区标准) +- 零侵入方案:OpenTelemetry Go SDK(标准化,支持后续接入任意后端) + +**置信度:MEDIUM**(基于 Go 社区 2025 年可观测性实践) + +--- + +### 缺口 4(IoT 领域特有):运营商网关客户端 — 无通用标准库 + +**问题描述:** +本项目有 `internal/gateway/client.go`(外部 Gateway 服务 HTTP 客户端)。IoT SIM 卡管理平台通常需要对接: +- GSMA M2M 标准(SM-DP+/SM-DS for eSIM) +- 运营商私有 API(中国联通、移动、电信) + +**2025 年实际情况:** +国内运营商 IoT 平台(移动 OneNET、电信 CTWing、联通 Open)均为私有 REST/SOAP API,**没有标准化的 Go SDK**。 + +当前项目自研 `internal/gateway/client.go` 是唯一可行路径,符合行业惯例。 + +**置信度:HIGH**(行业调研确认,无通用 Go SDK 存在) + +--- + +## 当前栈中已弃用/有问题的依赖 + +| 依赖 | 问题 | 建议 | +|------|------|------| +| `aws/aws-sdk-go` (v1) | AWS SDK Go v1 已于 2025-07-31 进入 Maintenance Mode,不再接收功能更新 | 低优先级,当前对接联通云 OSS,可继续使用;如有大范围重构可迁移到 `aws/aws-sdk-go-v2` | +| `excelize/v2` | 无严重问题,版本跟进即可 | — | + +**AWS SDK v1 注意:** 仅 Maintenance Mode,不是 End of Life,安全补丁仍会发布。迁移到 v2 需要较大代码改动,不建议在当前 MVP 阶段操作。 + +**置信度:MEDIUM**(AWS 官方公告,2025-07-31) + +--- + +## 可选补充库 + +以下库不在当前技术栈中,但对特定需求有价值: + +| 库 | 版本 | 用途 | 引入时机 | +|----|------|------|---------| +| `shopspring/decimal` | v1.4.0 | 佣金比例计算、退款按比例计算 | **方案 I(退款)实现时** | +| `golang-migrate/migrate` | v4.18.x | 数据库迁移(已在项目使用) | — 已用 | +| `hibiken/asynqmon` | latest | Asynq 任务可视化 Web UI | 生产环境调试时 | + +--- + +## 替代方案对比 + +| 类别 | 当前选择 | 备选 | 不替换的理由 | +|------|---------|------|------------| +| HTTP 框架 | Fiber v2 | Gin, Echo, Chi, Fiber v3 | 大量存量代码;v3 破坏性变更成本高 | +| ORM | GORM | sqlc, sqlx, ent | 项目已深度集成,CRUD 场景 GORM 是最优解 | +| 任务队列 | Asynq | Machinery, Watermill, Temporal | Redis 复用优势;Temporal 对单节点过重 | +| 消息队列 | 无(Asynq 兼任) | NATS, Kafka | 当前规模不需要独立 MQ | +| 缓存 | Redis | Memcached | Redis 功能更丰富,已是基础设施 | +| 数据库 | PostgreSQL | MySQL, CockroachDB | PostgreSQL 的递归 CTE 对层级数据有优势 | +| JSON | sonic | encoding/json, json/v2 | sonic 性能优势在高并发场景显著 | + +--- + +## 版本兼容性注意事项 + +| 依赖组合 | 状态 | 注意 | +|---------|------|------| +| Fiber v2 + Go 1.25 | ✅ 兼容 | Fiber v2.52.9 已适配 Go 1.25 | +| Fiber v3 + Go 1.25 | ⚠️ 要求 Go 1.25+ | v3 以 Go 1.25 为最低版本 | +| Asynq v0.26.0 + Go 1.25 | ✅ 兼容 | v0.26.0 要求 Go 1.24+,1.25 满足 | +| GORM v1.31.1 + PostgreSQL 14+ | ✅ 兼容 | — | +| PowerWeChat v3.4.38 + Go 1.25 | ✅ 兼容 | 官方声明 Go 1.23+ | + +--- + +## 关键技术债总结(优先级排序) + +### P0:必须在上线前解决 + +**金融精度风险**(佣金计算) +- 检查所有涉及百分比/比例的佣金计算代码,确认没有 `float64` 中间步骤 +- 方案 I(退款按比例扣减佣金)实现时必须引入 `shopspring/decimal` + +### P1:上线后尽快处理 + +**go-redis 版本落后** +- 从 v9.7.0 升级到 v9.18.0,无 API 破坏性变更,安全隐患修复 + +**Asynq 版本落后** +- 从 v0.25.1 升级到 v0.26.0,获得安全修复 + +### P2:中期规划 + +**AWS SDK v1 → v2 迁移**(对象存储) +- 不紧急,v1 仍有安全支持,规划下一个重大重构时处理 + +**Fiber v2 → v3 迁移评估** +- v2 仍在维护,不需要立即升级 +- 建议在明年底(2027)重新评估是否升级 + +--- + +## 安装/升级命令参考 + +```bash +# 升级 Asynq(推荐) +go get github.com/hibiken/asynq@v0.26.0 + +# 升级 go-redis(推荐) +go get github.com/redis/go-redis/v9@v9.18.0 + +# 引入 decimal(P0 佣金精度修复时) +go get github.com/shopspring/decimal@v1.4.0 + +# 升级所有兼容依赖(谨慎,逐一验证) +go get -u ./... +``` + +--- + +## Sources + +| 来源 | 内容 | 置信度 | +|------|------|--------| +| https://docs.gofiber.io/whats_new/ | Fiber v3 破坏性变更完整列表 | HIGH | +| https://newreleases.io/project/github/hibiken/asynq/release/v0.26.0 | Asynq v0.26.0 发布说明 | HIGH | +| https://pkg.go.dev/github.com/redis/go-redis/v9 | go-redis 最新版本 v9.18.0 | HIGH | +| https://reintech.io/blog/sqlc-vs-gorm-vs-sqlx-go-database-libraries-compared-2026 | GORM vs sqlc vs sqlx 2026 深度对比 | MEDIUM | +| https://encore.cloud/resources/go-orms | Go ORM 2026 对比(GORM/sqlc/ent)| MEDIUM | +| https://pkg.go.dev/github.com/shopspring/decimal@v1.4.0 | shopspring/decimal v1.4.0 | HIGH | +| https://www.moderntreasury.com/journal/floats-dont-work-for-storing-cents | 金融精度使用整数的行业实践 | HIGH | +| Reddit r/golang 2025-09 JSON benchmark | sonic vs std vs json/v2 基准 | MEDIUM | +| https://pkg.go.dev/github.com/ArtisanCloud/PowerWeChat/v3@v3.4.38 | PowerWeChat 最新版本确认 | HIGH | + +--- + +*Stack research for: IoT SIM 卡管理平台(Go + Fiber + GORM + Asynq + PostgreSQL + Redis)* +*Researched: 2026-03-27* diff --git a/.planning/research/SUMMARY.md b/.planning/research/SUMMARY.md new file mode 100644 index 0000000..f668c46 --- /dev/null +++ b/.planning/research/SUMMARY.md @@ -0,0 +1,121 @@ +# Research Summary + +**Domain:** 君鸿卡管系统 — IoT 卡/号卡/设备全生命周期管理平台 +**Synthesized:** 2026-03-27 +**Sources:** STACK.md · FEATURES.md · ARCHITECTURE.md · PITFALLS.md + +--- + +## 技术栈评估 + +**结论:当前技术选型合理,无需大规模替换。** + +| 组件 | 状态 | 行动建议 | +|------|------|---------| +| Fiber v2.52.9 | ✅ 继续使用 | 不升级 v3(破坏性变更 100+ 处,迁移成本过高) | +| GORM v1.31.x | ✅ 最新 | 继续使用;多级递归查询用原生 `WITH RECURSIVE` | +| Asynq v0.24.x | ⚠️ 落后 | 可选升级到 v0.26.0(无破坏性变更) | +| go-redis v9.7.0 | ⚠️ 落后 | 建议升级到 v9.18.x(无破坏性变更) | +| sonic / PowerWeChat | ✅ 正常 | 继续使用 | + +**技术债**:佣金计算全程使用 `int64` 整数运算是正确的;退款比例计算(方案 I-3)禁止用 `float64`,应使用整数算术 `a * b / c`。 + +--- + +## 功能完整性分析 + +### 已覆盖的 Table Stakes ✅ +- IoT 卡全生命周期(开卡/激活/停复机/销户) +- 套餐系统(主套餐/加油包/排队/囤货待实名/流量扣减) +- 五种订单购买场景 + 三种支付方式 +- 代理商多级分销(7 级层级,差价佣金+一次性佣金) +- RBAC 权限系统 + 多租户数据隔离 +- C 端微信认证 + 钱包充值购包 + +### 缺失的 Table Stakes ⚠️(修复阶段应关注) + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 卡/资产批量操作(停/复/注销) | HIGH | 运营场景必须,单卡操作无法应对批量场景 | +| 卡/订单批量导出(Excel) | HIGH | 财务对账必须,已有导入但缺导出 | +| 退款完整流程 | HIGH | 方案 I 已规划,必须实现 | +| 卡状态变更历史记录 | MEDIUM | 出现纠纷时需要溯源 | +| 套餐到期提醒(微信模板消息) | MEDIUM | 用户断网会差评,竞品标配 | +| 代理商完整资金流水账单 | MEDIUM | 代理需要对账 | +| 富友支付 JSAPI/小程序接口补全 | HIGH | 方案 J-1,SDK 已实现但接口是留桩 | + +### 竞品差异化(v2+ 方向) +- 智能套餐推荐(基于历史用量) +- 告警推送(流量耗尽前预警) +- 数据大屏(代理商业绩可视化) + +--- + +## 架构风险 + +### 当前正确的设计决策 ✅ +- 双进程隔离(API vs Worker)— 独立扩容 +- 数据权限显式过滤(非 GORM Callback)— 可见性强 +- Asynq 任务队列解耦同步/异步链路 +- Bootstrap 集中依赖装配 + +### 需要立即处理的架构问题 🔴 + +**1. PollingHandler 缺少 asynqClient(方案 A-3)** +- 实名激活任务通过 RPush Redis List 降级,绕过 Asynq +- 修复后才能保证异步任务链路可靠性 + +**2. 多个 Service 实例依赖注入残缺(方案 F-6)** +- StopResumeCallback / ResumeCallback 未在 bootstrap 注入 +- 停复机后套餐联动回调不触发 + +**3. Worker 注册遗漏(方案 A-4)** +- AutoPurchaseHandler 未在 handler.go 注册 +- 充值回调后自动购包任务入队但永远不消费 + +### SaaS 化时的潜在瓶颈(供参考,v2+ 考量) +- 代理层级 7 级限制需松绑为配置化 +- tb_shop/tb_enterprise 的 owner_shop_id 设计需调整为 tenant_id +- 套餐成本价体系(每个代理一套)将成为最大多租户难点 + +--- + +## 关键陷阱(超出修正方案范围) + +### 🔴 高危 + +**1. sellerCostPrice 赋值错误可能不止 1 处(A-5 相关)** +- 修正方案只点名场景 5,但 order/service.go 中有 6 处 `sellerCostPrice = buyerCostPrice` +- 修复时必须逐一验证每个场景的语义正确性 + +**2. 退款佣金回扣禁用 float64(方案 I-3)** +- `ratio := float64(x) / float64(y)` 会引入精度误差 +- 应使用:`deductAmount = commission.Amount * approvedAmount / actualAmount`(纯整数) + +**3. 分佣树形遍历可能循环引用** +- `shop.ParentID` 向上遍历没有深度上限和循环检测 +- 人工修改数据库可能导致 Worker 死循环 + +### 🟡 中危 + +**4. 店铺软删除没有级联检查** +- 删除有子代理的店铺后,子代理 parent_id 指向软删除记录 +- 佣金计算时找不到父链接,上级代理静默丢失佣金 + +**5. 实名状态轮询无限重试设计** +- 卡在"未实名"状态的卡每次轮询都入队,没有最大重试次数限制 +- 大量长期未实名的卡会持续占用轮询资源 + +--- + +## 构建优先级建议 + +``` +立即(1-2 天):方案 A(7 个 P0 Bug) +近期(3-5 天):方案 B/C/G/H + J-4(企业权限/提现/实名重构/轮询小修) +中期(1-2 周):方案 D/I/J-1/J-2/J-3/F(设备/退款/富友支付/代码清理) +长期(低峰期):方案 E(流量体系改革,破坏性 DB 迁移) +``` + +--- +*Research synthesized: 2026-03-27*