Files
junhong_cmp_fiber/docs/polling-system/manual-trigger-analysis.md
huang 7308afe801
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
归档
2026-04-13 15:03:02 +08:00

19 KiB
Raw Permalink Blame History

轮询系统手动触发功能深入分析报告

执行摘要

轮询系统的手动触发功能包含 884 行代码Service 469行 + Handler 307行 + Store 108行实现了 6 个 API 接口,支持单卡、批量、条件筛选三种触发方式。

核心发现

  • 功能完整性高:支持多种触发方式、权限控制、进度追踪、任务取消
  • ⚠️ 使用价值存疑缺乏实际业务需求支撑频次限制每日100次、1000张卡设置不合理
  • 过度设计风险:复杂度与实际使用场景不匹配,维护成本高

1. 功能接口完整分析

1.1 API 接口清单

接口 方法 功能 代码行数
/polling-manual-trigger/single POST 单卡手动触发 18
/polling-manual-trigger/batch POST 批量手动触发最多1000张 19
/polling-manual-trigger/by-condition POST 条件筛选触发 29
/polling-manual-trigger/status GET 获取触发状态 34
/polling-manual-trigger/history GET 获取触发历史 31
/polling-manual-trigger/cancel POST 取消触发任务 18

1.2 触发方式详解

方式1单卡触发TriggerSingle

// 限制每日100次
// 流程:
// 1. 验证任务类型realname/carddata/package
// 2. 权限检查(企业账号禁止,代理只能管理自己的卡)
// 3. 检查每日触发次数≥100则拒绝
// 4. Redis Set 去重1小时过期
// 5. 加入手动触发队列List
// 6. 更新日志状态为 completed

问题

  • 单卡触发后立即标记为 completed但实际处理是异步的
  • 去重 key 1小时过期但日限制是按天计算时间不对齐

方式2批量触发TriggerBatch

// 限制单次最多1000张卡每日100次
// 流程:
// 1. 验证卡数量(>1000则拒绝
// 2. 权限检查(批量检查所有卡)
// 3. 异步处理:
//    - 逐卡检查去重
//    - 加入队列
//    - 每100卡更新一次进度
// 4. 最终更新状态为 completed

问题

  • 异步处理中的去重失败被计为 failedCount但实际上是重复触发的防护
  • 没有考虑批量操作的原子性

方式3条件筛选触发TriggerByCondition

// 限制筛选结果最多1000张卡每日100次
// 支持筛选条件:
// - card_status: 卡状态
// - carrier_code: 运营商
// - card_type: 卡类型
// - shop_id: 店铺ID
// - package_ids: 套餐ID列表
// - enable_polling: 是否启用轮询
// 流程:
// 1. 权限过滤(代理只能筛选自己店铺的卡)
// 2. 数据库查询符合条件的卡
// 3. 异步批量处理同方式2

问题

  • 权限过滤逻辑:代理未指定 ShopID 时,自动限制为当前店铺(而非所有下级店铺)
  • 这与代理的权限模型不一致(代理应该能管理下级店铺的卡)

2. 业务需求支撑分析

2.1 需求来源

通过代码搜索,找到的需求记录:

./openspec/changes/archive/2026-02-10-polling-system-implementation/tasks.md
- [x] 12.12 实现手动触发限流单次限制1000张、每日限制100次

问题

  • 没有找到需求文档proposal.md 或 spec.md
  • 没有业务场景说明
  • 没有使用频率预测

2.2 实际使用场景推测

根据代码注释和文档,推测的使用场景:

场景 频率 合理性 说明
故障恢复 低(<1次/天) 轮询失败时手动重试
数据修复 低(<1次/周) 修复错误数据后重新检查
性能测试 低(<1次/月) 测试轮询系统性能
日常运维 1-10次/天) ⚠️ 运维人员定期检查
业务需求 高(>100次/天) 业务系统频繁触发

结论

  • 如果是故障恢复/数据修复场景每日100次限制过高实际需求<10次
  • 如果是业务系统频繁触发每日100次限制过低实际需求>1000次
  • 频次限制设置不合理,说明需求定义不清

2.3 与自动轮询的关系

自动轮询流程:
┌─────────────────────────────────────────┐
│ Scheduler定时调度                    │
│ - 每秒从分片队列出队卡                   │
│ - 按配置间隔重新入队                     │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│ 手动触发队列(优先级高)                 │
│ - List 结构FIFO                      │
│ - 调度器优先消费                         │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│ Asynq 任务队列                           │
│ - 实际执行轮询任务                       │
└─────────────────────────────────────────┘

关键发现

  • 手动触发队列是 ListFIFO自动轮询队列是 Sorted Set(按时间戳排序)
  • 调度器优先消费手动触发队列(processManualQueueprocessScheduledQueue 之前)
  • 手动触发卡会立即进入 Asynq 队列,不受轮询间隔限制

必要性评估

  • 必要:如果需要快速重试失败的卡
  • 不必要:如果自动轮询已经足够频繁
  • ⚠️ 可替代:可以通过调整轮询配置的间隔来实现相同效果

3. 频次限制合理性评估

3.1 限制规则

// 每日触发次数限制100次
if todayCount >= 100 {
    return errors.New(errors.CodeInvalidParam, "已达到每日触发次数上限")
}

// 单次卡数限制1000张
if len(cardIDs) > 1000 {
    return errors.New(errors.CodeInvalidParam, "单次最多触发1000张卡")
}

3.2 限制的问题

问题1每日100次限制不合理

场景分析

场景A故障恢复
- 实际需求1-5次/天
- 限制100次/天
- 评价:过高,浪费

场景B数据修复
- 实际需求:<1次/周
- 限制100次/天
- 评价:过高,浪费

场景C业务系统集成
- 实际需求:可能>100次/天
- 限制100次/天
- 评价:过低,无法满足

场景D性能测试
- 实际需求:可能>100次/天
- 限制100次/天
- 评价:过低,无法满足

结论

  • 如果是人工运维操作100次/天 过高
  • 如果是自动化系统调用100次/天 过低
  • 限制设置没有明确的业务依据

问题2单次1000张卡限制

分析

// 单次最多1000张卡
if len(cardIDs) > 1000 {
    return nil, errors.New(errors.CodeInvalidParam, "单次最多触发1000张卡")
}

// 但批量处理时每100卡更新一次进度
if processedCount%100 == 0 {
    _ = s.logStore.UpdateProgress(ctx, logID, processedCount, successCount, failedCount)
}

问题

  • 1000张卡的限制来自哪里没有文档说明
  • 是否考虑了数据库查询性能?
  • 是否考虑了 Redis 操作性能?
  • 是否考虑了 Asynq 队列容量?

问题3去重机制的时间不对齐

// 去重 key 1小时过期
s.redis.Expire(ctx, dedupeKey, time.Hour)

// 但日限制是按天计算
if todayCount >= 100 {
    return errors.New(...)
}

问题

  • 去重 key 在1小时后过期但日限制是按天计算
  • 如果在 23:00 触发一张卡1小时后00:00去重 key 过期
  • 同一张卡可能在同一天内被触发两次(如果去重 key 过期了)
  • 去重机制与日限制不一致

3.3 限制的实现问题

问题1CountTodayTriggers 的性能

func (s *PollingManualTriggerLogStore) CountTodayTriggers(ctx context.Context, triggeredBy uint) (int64, error) {
    var count int64
    if err := s.db.WithContext(ctx).Model(&model.PollingManualTriggerLog{}).
        Where("triggered_by = ? AND DATE(triggered_at) = CURRENT_DATE", triggeredBy).
        Count(&count).Error; err != nil {
        return 0, err
    }
    return count, nil
}

问题

  • 每次触发都要查询数据库
  • 没有索引优化(triggered_by + triggered_at
  • 没有缓存(可以用 Redis 计数器)
  • 在高频触发场景下,这个查询会成为瓶颈

问题2去重 Set 的内存占用

// 每个任务类型一个去重 Set
dedupeKey := constants.RedisPollingManualDedupeKey(taskType)
added, err := s.redis.SAdd(ctx, dedupeKey, cardID).Result()

问题

  • 如果每日触发1000张卡去重 Set 会有1000个元素
  • 4个任务类型 × 1000张卡 = 4000个元素
  • 1小时过期后清空但在高峰期可能占用大量内存
  • 没有考虑 Redis 内存限制

4. 代码实现复杂度分析

4.1 代码规模

总代码行数884 行
├── Service 层469 行53%
├── Handler 层307 行35%
└── Store 层108 行12%

功能点数:
├── 3 种触发方式
├── 权限检查3 个函数)
├── 进度追踪(异步处理)
├── 任务取消
├── 历史查询
└── 状态查询

4.2 复杂度评估

权限检查的复杂性

// canManageCard单卡权限检查
// - 用户类型判断3种
// - 卡信息查询
// - 店铺权限检查
// 复杂度O(1) + O(1) = O(1)

// canManageCards批量权限检查
// - 用户类型判断3种
// - 批量查询卡信息
// - 逐卡权限检查
// 复杂度O(n) 其中 n = 卡数量

// applyShopPermissionFilter条件筛选权限过滤
// - 用户类型判断3种
// - 店铺权限检查
// 复杂度O(1)

问题

  • canManageCards 的 O(n) 复杂度在 n=1000 时可能有性能问题
  • 没有批量查询的优化(如分页查询)

异步处理的复杂性

// processBatchTrigger异步处理批量触发
// - 逐卡检查去重O(n)
// - 逐卡加入队列O(n)
// - 每100卡更新一次进度O(n/100)
// 复杂度O(n)

// 问题:
// 1. 没有错误处理(如果 Redis 操作失败)
// 2. 没有超时控制(如果处理时间过长)
// 3. 没有并发控制(如果同时有多个批量触发)

4.3 维护成本

方面 成本 说明
代码理解 需要理解权限模型、队列机制、异步处理
测试覆盖 需要测试6个接口、3种权限、3种触发方式
故障排查 涉及数据库、Redis、Asynq 三个系统
性能优化 需要优化数据库查询、Redis 操作
功能扩展 添加新的触发方式或限制规则需要修改多个地方

5. 与自动轮询的必要性分析

5.1 自动轮询的能力

自动轮询配置示例:
┌─────────────────────────────────────────┐
│ 配置1未实名卡                          │
│ - 实名检查间隔60秒                     │
│ - 流量检查间隔NULL不检查           │
│ - 套餐检查间隔NULL不检查           │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ 配置2已实名卡                          │
│ - 实名检查间隔3600秒1小时          │
│ - 流量检查间隔1800秒30分钟         │
│ - 套餐检查间隔1800秒30分钟         │
└─────────────────────────────────────────┘

5.2 手动触发的优势

场景 自动轮询 手动触发 必要性
故障恢复 需要等待下次轮询 立即执行
数据修复 需要等待下次轮询 立即执行
性能测试 无法控制 可以控制
日常运维 足够 可选 ⚠️
业务集成 足够 可选 ⚠️

5.3 可替代方案

方案1调整轮询配置

// 不需要手动触发,直接调整配置
// 原配置:实名检查间隔 3600秒
// 新配置:实名检查间隔 60秒故障期间
// 恢复:实名检查间隔 3600秒故障解决后

优点
- 简单不需要额外代码
- 自动应用到所有卡
- 易于理解和维护

缺点
- 需要手动修改配置
- 可能影响其他卡的轮询频率

方案2优先级队列

// 不需要手动触发,使用优先级队列
// 自动轮询队列Sorted Set按时间戳排序
// 优先级队列Sorted Set按优先级排序

优点
- 灵活可以动态调整优先级
- 不需要额外的队列机制

缺点
- 需要修改调度器逻辑
- 需要定义优先级规则

方案3保留手动触发当前方案

优点
- 灵活支持多种触发方式
- 不影响自动轮询配置
- 可以精确控制触发范围

缺点
- 代码复杂度高884
- 维护成本高
- 频次限制设置不合理

5.4 结论

手动触发功能的必要性:中等

  • 对于故障恢复和数据修复场景,手动触发是有价值的
  • ⚠️ 对于日常运维和业务集成,自动轮询已经足够
  • 当前的实现过于复杂,频次限制不合理

6. 过度设计问题

6.1 功能过度设计

功能 使用频率 必要性 说明
单卡触发 用于快速测试
批量触发 用于批量修复
条件筛选触发 ⚠️ 可以用 API 查询后再批量触发
进度追踪 ⚠️ 异步处理,用户无法实时看到
任务取消 极低 取消后卡已经在队列中,无法真正取消
历史查询 ⚠️ 可以用日志系统查询

6.2 权限检查过度设计

// 当前实现3个权限检查函数
- canManageCard(ctx, cardID)      // 单卡权限检查
- canManageCards(ctx, cardIDs)    // 批量权限检查
- applyShopPermissionFilter(...)  // 条件筛选权限过滤

// 问题:
// 1. 代码重复(都是检查 shop_id
// 2. 逻辑复杂(需要理解权限模型)
// 3. 易出错(权限检查不一致)

// 可以简化为:
- canManageCards(ctx, cardIDs)    // 统一的权限检查

6.3 数据库设计过度设计

// 当前表结构tb_polling_manual_trigger_log
type PollingManualTriggerLog struct {
    ID              uint       // 日志ID
    TaskType        string     // 任务类型
    TriggerType     string     // 触发类型single/batch/by_condition
    CardIDs         string     // 卡ID列表JSON
    ConditionFilter string     // 筛选条件JSON
    TotalCount      int        // 总卡数
    ProcessedCount  int        // 已处理数
    SuccessCount    int        // 成功数
    FailedCount     int        // 失败数
    Status          string     // 状态
    TriggeredBy     uint       // 触发人ID
    TriggeredAt     time.Time  // 触发时间
    CompletedAt     *time.Time // 完成时间
}

// 问题:
// 1. CardIDs 和 ConditionFilter 都是 JSON 字符串,难以查询
// 2. ProcessedCount/SuccessCount/FailedCount 在异步处理中更新,可能不准确
// 3. Status 字段的状态转移不清楚pending -> processing -> completed

// 可以简化为:
// - 只记录触发信息TaskType, TriggerType, TotalCount, TriggeredBy, TriggeredAt
// - 不记录进度信息ProcessedCount, SuccessCount, FailedCount
// - 不记录详细条件CardIDs, ConditionFilter

7. 实际使用价值评估

7.1 使用场景评分

场景 频率 价值 复杂度 综合评分
故障恢复
数据修复
性能测试
日常运维
业务集成

7.2 成本收益分析

成本:
- 代码行数884 行
- 维护成本:中等(需要理解权限、队列、异步处理)
- 测试成本6个接口多种权限多种触发方式
- 性能成本:低(不影响自动轮询)

收益:
- 故障恢复:高(可以快速重试失败的卡)
- 数据修复:高(可以快速重新检查修复的卡)
- 性能测试:中(可以控制测试场景)
- 日常运维:低(自动轮询已经足够)
- 业务集成:低(频次限制过低)

综合评估:
- 如果只用于故障恢复和数据修复,成本收益比 = 低成本 / 高收益 = ✅ 值得
- 如果用于日常运维和业务集成,成本收益比 = 中成本 / 低收益 = ❌ 不值得

7.3 建议

短期建议(保留当前功能)

  1. 明确使用场景

    • 文档化手动触发的使用场景
    • 定义频次限制的业务依据
    • 添加使用指南
  2. 优化频次限制

    • 每日限制改为 1000 次(支持更多使用场景)
    • 单次限制改为 10000 张卡(支持大规模修复)
    • 或者完全移除限制(由业务层控制)
  3. 改进实现

    • 修复去重 key 过期时间与日限制的不对齐
    • 添加 Redis 缓存优化 CountTodayTriggers 查询
    • 改进异步处理的错误处理和超时控制

长期建议(重新设计)

  1. 简化功能

    • 移除条件筛选触发(可以用 API 查询后再批量触发)
    • 移除进度追踪(异步处理,用户无法实时看到)
    • 移除任务取消(无法真正取消已入队的任务)
  2. 改进设计

    • 统一权限检查逻辑
    • 简化数据库表结构
    • 使用事件驱动而不是 API 驱动
  3. 考虑替代方案

    • 使用优先级队列替代手动触发队列
    • 使用配置调整替代手动触发
    • 使用事件系统替代 API 调用

8. 总结

8.1 核心发现

  1. 功能完整性高:支持多种触发方式、权限控制、进度追踪
  2. 使用价值存疑:缺乏明确的业务需求支撑
  3. 频次限制不合理每日100次、1000张卡的限制没有业务依据
  4. 过度设计风险:代码复杂度与实际使用场景不匹配
  5. 维护成本高884行代码涉及多个系统数据库、Redis、Asynq

8.2 建议

保留手动触发功能,但需要改进

  1. 明确使用场景和频次限制的业务依据
  2. 优化频次限制(提高或移除)
  3. 改进实现(修复 bug优化性能
  4. 简化功能(移除不必要的功能)
  5. 改进文档(添加使用指南和最佳实践)

不建议完全移除,因为:

  • 故障恢复和数据修复场景有实际价值
  • 代码已经实现,移除成本高
  • 可以通过改进来提高价值