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

24 KiB
Raw Blame History

realname-trigger-priority-enhancement 执行计划

TL;DR

快速摘要修复手动触发去重TTL不对齐、日限制过低两个Bug并在C端实名链接接口中异步触发优先级检查减少用户等待时间。

交付物

  • pkg/config/ 新增 PollingAutoTriggerConfig 配置结构
  • internal/service/polling/manual_trigger_service.go 修复3处日限制(100→500)、2处TTL(1h→24h)、提取权限检查公共函数、增强错误日志
  • internal/handler/app/client_realname.go 新增异步触发逻辑含2个辅助方法提取
  • internal/bootstrap/handlers.gocmd/api/docs.gocmd/gendocs/main.go 依赖注入接线
  • pkg/constants/redis.go 注释同步更新

预计工作量60-90 分钟 关键路径:配置 → ManualTriggerService修复 → Handler集成 → 依赖注入接线 → 手动验证


上下文

原始需求

用户在C端主动获取实名跳转链接后需等待轮询系统定时检查30秒-5分钟间隔才能检测到实名状态变化。同时手动触发功能存在两个Bug去重key TTL1小时与日限制周期24小时不对齐以及日限制次数100次过低导致正常使用受限。

代码库现状(已验证)

项目 状态
constants.TaskTypePollingRealname 已存在:"polling:realname"constants.go:55
svc.PollingManualTrigger in bootstrap 已初始化services.go:210
日限制 >= 100 位置 3处manual_trigger_service.go:58, 129, 238
Expire(..., time.Hour) 位置 2处manual_trigger_service.go:72TriggerSingle和 :192processBatchTrigger
NewClientRealnameHandler 当前参数数 7个 → 改后8个
docs.go / gendocs/main.go 调用处 各1处7个nil → 8个nil
GetRealnameLink 当前行数 127行已超100行限制需提取辅助方法
系统用户 暂用 SuperAdmin ID=1使用 UserTypePlatform 身份

关键设计决策

  1. goroutine 异步触发:使用 context.WithTimeout(context.Background(), 3s) 独立 context禁止复用 c.UserContext()Fiber 请求 context 在 handler 返回后失效)
  2. 系统用户身份UserTypePlatform(非 UserTypeSuperAdmin既绕过卡归属权限检查又仍受500次/天限制约束
  3. goroutine 参数只传标量值uint、string绝对不捕获 c *fiber.Ctx 或任何指针
  4. GetRealnameLink 行数当前127行已超限需在 Task 4 中额外提取两个 helper已批准
  5. processBatchTrigger TTL:两处都修(已确认)
  6. Commit 3 临时编译失败Task 4 修改构造函数后、Task 5 接线前,全量编译会报错,只做文件级 LSP 检查

Phase 0 — 现状分析

Task 1.1 — LSP 基线诊断

操作:对以下两个文件运行 lsp_diagnostics

  • internal/handler/app/client_realname.go
  • internal/service/polling/manual_trigger_service.go

目的:建立干净基线,区分预存问题与本次引入的问题

验收:记录所有预存 warning确认无预存 error


Task 1.2 — 依赖关系确认

操作

  • 确认 handler/app → service/polling 无循环依赖polling service 不导入 handler 包)
  • 确认 internal/bootstrap/handlers.go 已有 pollingSvcPkg import alias

验收:无循环依赖,svc.PollingManualTrigger 在 bootstrap 中可访问


Phase 1 — 配置管理

Task 2.1 — 新增配置结构体和字段

文件pkg/config/config.go

新增结构体(放在其他 Config 结构体附近):

// PollingAutoTriggerConfig 轮询自动触发配置
type PollingAutoTriggerConfig struct {
    // EnableAutoTrigger 是否启用C端实名自动触发
    // 环境变量JUNHONG_POLLING_AUTO_TRIGGER_ENABLE_AUTO_TRIGGER
    EnableAutoTrigger bool `mapstructure:"enable_auto_trigger"`
    // AutoTriggerSystemUserID 自动触发使用的系统用户ID暂用 SuperAdmin ID=1
    // 环境变量JUNHONG_POLLING_AUTO_TRIGGER_AUTO_TRIGGER_SYSTEM_USER_ID
    AutoTriggerSystemUserID int `mapstructure:"auto_trigger_system_user_id"`
}

Config struct 中添加字段位置Gateway 字段之后):

PollingAutoTrigger PollingAutoTriggerConfig `mapstructure:"polling_auto_trigger"`

陷阱:不要在 Validate() 中添加校验——零值false/0是合法默认值

验收结构体定义正确mapstructure tag 与 yaml key 对应


Task 2.2 — 更新默认配置 YAML

文件pkg/config/defaults/config.yaml

在文件末尾添加(保留尾部换行):

# 轮询自动触发配置
polling_auto_trigger:
  enable_auto_trigger: true
  auto_trigger_system_user_id: 1  # 暂用 SuperAdmin生产环境建议创建专用平台账号并更新此值

对应环境变量:

  • JUNHONG_POLLING_AUTO_TRIGGER_ENABLE_AUTO_TRIGGER
  • JUNHONG_POLLING_AUTO_TRIGGER_AUTO_TRIGGER_SYSTEM_USER_ID

验收config.Get().PollingAutoTrigger.EnableAutoTrigger 默认为 true


Task 2.3 — LSP 检查配置

操作lsp_diagnosticspkg/config/config.go

验收0 error


Phase 2 — 手动触发服务 Bug 修复

Task 3.1 — 日限制 100 → 5003处

文件internal/service/polling/manual_trigger_service.go

行号 修改内容
58 todayCount >= 100todayCount >= 500,同步更新行内注释 // 每日最多触发100次// 每日最多触发500次
129 todayCount >= 100todayCount >= 500,补充注释 // 每日最多触发500次
238 todayCount >= 100todayCount >= 500,补充注释 // 每日最多触发500次

验收grep -n "100" manual_trigger_service.go 无日限制相关残留


Task 3.2 — 去重 TTL 1h → 24h2处

文件internal/service/polling/manual_trigger_service.go

行号 修改内容
71-72 注释改为 // 设置去重 key 过期时间24小时与日限制周期对齐time.Hour24*time.Hour
191-192 同上processBatchTrigger 中,存在相同 Bug一并修复

验收grep -n "time.Hour" manual_trigger_service.go 无残留


Task 3.3 — 同步 Redis 常量注释

文件pkg/constants/redis.go

找到 RedisPollingManualDedupeKey 函数注释,将:

// 过期时间1小时

改为:

// 过期时间24小时与日限制周期对齐

验收:注释与实际 TTL 一致


Task 3.4 — 提取公共权限检查函数

文件internal/service/polling/manual_trigger_service.go

canManageCardcanManageCardsapplyShopPermissionFilter 三处提取重复的用户类型检查,新增私有方法:

// checkUserTypePermission 检查用户类型是否有手动触发权限
// 返回 skip=true 表示超级管理员/平台用户直接放行
// 返回 err!=nil 表示企业账号无权限
// 返回 skip=false, err=nil 表示代理账号需继续细粒度检查
func (s *ManualTriggerService) checkUserTypePermission(ctx context.Context) (skip bool, err error) {
    userType := middleware.GetUserTypeFromContext(ctx)
    if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform {
        return true, nil
    }
    if userType == constants.UserTypeEnterprise {
        return false, errors.New(errors.CodeForbidden, "企业账号无权限手动触发轮询")
    }
    return false, nil
}

三个调用方改为:

skip, err := s.checkUserTypePermission(ctx)
if err != nil || skip {
    return err  // canManageCard 签名
}

陷阱applyShopPermissionFilter 中超管分支有 filter 对象逻辑,只提取类型检查部分,不删除 filter 修改逻辑

验收三个函数各减少约5行重复代码行为不变


Task 3.5 — 改进 TriggerSingle 错误日志

文件internal/service/polling/manual_trigger_service.go

TriggerSingle 每条 return err 路径前补充结构化日志,字段要求:

错误场景 必须包含字段
查询今日触发次数失败 triggered_bytask_typeerror
Redis去重操作失败 card_idtask_typeerror
创建触发日志失败 card_idtriggered_byerror
写入队列失败 card_idtask_typeerror

验收:每条 return err 路径有对应日志,级别为 Error


Task 3.6 — LSP 检查

操作lsp_diagnosticsinternal/service/polling/manual_trigger_service.go

验收0 error0 新增 warning


Task 3.7 — 手动验证去重 + 限制

操作(通过 Redis CLI 或 PostgreSQL MCP

# 1. 触发某张卡,验证 TTL 为 24小时
redis-cli TTL polling:manual:dedupe:polling:realname
# 期望:≈ 86400

# 2. 再次触发同一张卡,验证被拒绝
# 期望返回:"该卡已在手动触发队列中"

# 3. 删除 dedupe key 后重新触发,验证成功
redis-cli DEL polling:manual:dedupe:polling:realname
-- 验证今日触发次数计数正确
SELECT COUNT(*) as today_count
FROM tb_polling_manual_trigger_log
WHERE DATE(triggered_at) = CURRENT_DATE;

验收TTL ≈ 86400重复触发被拒绝DB 计数正确


Phase 3 — Handler 集成

Task 4.1 — 结构体新增字段

文件internal/handler/app/client_realname.go

添加 import注意区分 internal/service/pollinginternal/polling 是不同包):

pollingSvc "github.com/xxx/junhong_cmp_fiber/internal/service/polling"

ClientRealnameHandler 结构体新增字段:

manualTriggerSvc *pollingSvc.ManualTriggerService // 手动触发服务可为nilnil时跳过自动触发

验收结构体字段添加正确import 路径正确


Task 4.2 — 修改构造函数第8个参数

文件internal/handler/app/client_realname.go

NewClientRealnameHandler 末尾新增参数:

func NewClientRealnameHandler(
    // ...现有7个参数保持不变...
    manualTriggerSvc *pollingSvc.ManualTriggerService, // 可为nil
) *ClientRealnameHandler {
    return &ClientRealnameHandler{
        // ...现有字段初始化...
        manualTriggerSvc: manualTriggerSvc,
    }
}

陷阱:修改后 docs.gogendocs/main.gobootstrap/handlers.go 会出现编译错误(构造函数参数不一致),待 Task 5 修复。此阶段只做文件级 LSP 检查,不跑全量 build

验收client_realname.go 本身 LSP 0 error


文件internal/handler/app/client_realname.go

背景:当前 GetRealnameLink 127行加 goroutine 代码约133行违反 ≤100行规范。需提取两个私有方法。

提取1resolveTargetCard(资产类型 switch 分支约26行

// resolveTargetCard 根据资产类型和ICCID定位目标卡
// 支持三条路径:直接卡资产、设备+指定ICCID、设备取第一张绑定卡
func (h *ClientRealnameHandler) resolveTargetCard(c *fiber.Ctx, asset *assetService.ResolvedAsset, iccid string) (*model.IotCard, error) {
    switch {
    case asset.AssetType == "card":
        // ...
    case asset.AssetType == "device" && iccid != "":
        // ...
    case asset.AssetType == "device":
        // ...
    default:
        return nil, errors.New(errors.CodeInvalidParam, "不支持的资产类型")
    }
}

提取2buildRealnameResponse(运营商 URL 调度约32行

// buildRealnameResponse 根据运营商实名链接类型构建响应
func (h *ClientRealnameHandler) buildRealnameResponse(ctx context.Context, card *model.IotCard, carrier *model.Carrier) (*dto.RealnimeLinkResponse, error) {
    // switch carrier.RealnameLinkType { ... }
}

提取后 GetRealnameLink ≈70行加 goroutine 后 ≈76行

陷阱

  • resolveTargetCard 需要 c *fiber.Ctx(子函数调用需要 Fiber context
  • buildRealnameResponsectx context.Context(只调 gatewayClient
  • 确认 assetService.ResolvedAsset 的实际类型名称(读文件确认后再写)

验收GetRealnameLink 函数体 ≤100行两个新方法编译正常逻辑不变


Task 4.4 — 添加异步触发调用

文件internal/handler/app/client_realname.go

添加 import"github.com/xxx/junhong_cmp_fiber/pkg/config"

GetRealnameLink 中,成功构建响应之后、response.Success(c, resp) 之前插入:

// 异步触发实名检查,提升检测优先级;失败不影响主流程
if h.manualTriggerSvc != nil && config.Get().PollingAutoTrigger.EnableAutoTrigger {
    systemUserID := uint(config.Get().PollingAutoTrigger.AutoTriggerSystemUserID)
    go h.triggerRealnameCheck(targetCard.ID, customerID, targetCard.ICCID, systemUserID)
}

强制约束goroutine 只传标量值uint、string绝对不捕获 c *fiber.Ctx 或任何指针类型

验收goroutine 启动代码不捕获 Fiber context 相关变量


Task 4.5 — 实现 triggerRealnameCheck 私有方法

文件internal/handler/app/client_realname.go

添加 imports"context""time""github.com/xxx/junhong_cmp_fiber/pkg/middleware"

// triggerRealnameCheck 异步触发单卡实名检查
// 在独立 goroutine 中调用,使用独立 context 避免 Fiber 请求 context 失效问题
// 参数全部为值类型,不捕获请求相关指针
func (h *ClientRealnameHandler) triggerRealnameCheck(cardID, customerID uint, iccid string, systemUserID uint) {
    // 必须使用独立 context禁止复用 Fiber 请求 context请求返回后即失效
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    // 使用平台用户身份构建 context绕过卡归属权限检查
    // 注意:使用 UserTypePlatform 而非 UserTypeSuperAdminSuperAdmin 不受日限制约束
    sysCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
        UserID:   systemUserID,
        UserType: constants.UserTypePlatform,
    })

    err := h.manualTriggerSvc.TriggerSingle(sysCtx, cardID, constants.TaskTypePollingRealname, systemUserID)
    if err != nil {
        h.logger.Warn("自动触发实名检查失败",
            zap.Uint("customer_id", customerID),
            zap.String("iccid", iccid),
            zap.Uint("card_id", cardID),
            zap.Error(err))
        return
    }
    h.logger.Info("自动触发实名检查成功",
        zap.Uint("customer_id", customerID),
        zap.String("iccid", iccid),
        zap.Uint("card_id", cardID))
}

验收:方法 ≤30行使用 context.Background()context 注入 UserTypePlatform日志字段完整


Task 4.6 — 日志字段核查

检查清单

  • WARN 路径包含:customer_idiccidcard_iderror
  • INFO 路径包含:customer_idiccidcard_id
  • 无 nil panic 风险err 已检查)
  • goroutine 无 panic 风险manualTriggerSvc 已在调用前检查非 nil

验收:所有字段齐全,无潜在 panic


Task 4.7 — LSP 检查 Handler 文件

操作lsp_diagnosticsinternal/handler/app/client_realname.go

注意:此时 bootstrap/handlers.godocs.gogendocs/main.go 仍有编译错误(待 Task 5只关注 client_realname.go 自身

验收client_realname.go 自身 0 error


Phase 4 — 依赖注入接线

Task 5.1 — 更新 bootstrap/handlers.go

文件internal/bootstrap/handlers.go

找到 ClientRealname 初始化行,在末尾补充 svc.PollingManualTrigger 参数:

// 修改前7个参数
ClientRealname: app.NewClientRealnameHandler(
    svc.Asset, personalCustomerDeviceStore, iotCardStore,
    deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger,
),

// 修改后8个参数
ClientRealname: app.NewClientRealnameHandler(
    svc.Asset, personalCustomerDeviceStore, iotCardStore,
    deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger,
    svc.PollingManualTrigger,
),

验收svc.PollingManualTrigger 类型匹配(已在 services.go:210 初始化)


Task 5.2 — 确认 ManualTriggerService 初始化(只读)

操作:确认 internal/bootstrap/services.go:210 已有:

PollingManualTrigger: pollingSvc.NewManualTriggerService(...)

无需变更,仅确认

验收svc.PollingManualTrigger 非 nil


Task 5.3 — 更新 docs.go 和 gendocs/main.go

文件cmd/api/docs.gocmd/gendocs/main.go

两处均将 NewClientRealnameHandler 调用从 7个nil 改为 8个nil

// 修改前
handlers.ClientRealname = apphandler.NewClientRealnameHandler(nil, nil, nil, nil, nil, nil, nil)

// 修改后
handlers.ClientRealname = apphandler.NewClientRealnameHandler(nil, nil, nil, nil, nil, nil, nil, nil)

验收:两个文件均编译通过


Task 5.4 — LSP 检查依赖注入相关文件

操作lsp_diagnostics 对以下3个文件

  • internal/bootstrap/handlers.go
  • cmd/api/docs.go
  • cmd/gendocs/main.go

验收:全部 0 error


Phase 5 — 手动验证

Task 6.1 — 接口调用验证 goroutine 启动

curl -H "Authorization: Bearer <token>" \
  "http://localhost:3000/api/c/v1/realname/link?identifier=<iccid>"

查看 app.log 确认出现:

{"level":"info","msg":"自动触发实名检查成功","customer_id":...,"iccid":"...","card_id":...}

验收响应正常返回实名链接日志在响应后3秒内出现


Task 6.2 — DB 验证触发记录

SELECT triggered_by, task_type, total_count, status, created_at
FROM tb_polling_manual_trigger_log
WHERE triggered_by = 1
ORDER BY id DESC LIMIT 5;

期望triggered_by = 1SuperAdmintask_type = 'polling:realname'total_count = 1

验收:记录存在且字段正确


Task 6.3 — Redis 队列验证

redis-cli LRANGE polling:manual:polling:realname 0 -1

期望:卡 ID 存在于队列中

验收:队列条目可见


Task 6.4 — 日限制计数验证

SELECT COUNT(*) as today_count
FROM tb_polling_manual_trigger_log
WHERE triggered_by = 1
  AND DATE(triggered_at) = CURRENT_DATE;

期望计数正确未超过500

验收:计数准确


Task 6.5 — Redis 去重 TTL 验证

redis-cli TTL polling:manual:dedupe:polling:realname

期望:≈ 8640024小时

验收TTL 约等于 86400


Task 6.6 — 功能开关验证

设置 JUNHONG_POLLING_AUTO_TRIGGER_ENABLE_AUTO_TRIGGER=false 后重启服务,调用接口:

期望正常返回实名链接app.log 无自动触发相关日志DB 无新增触发记录,无 panic

验收:功能开关有效,主流程不受影响


Phase 6 — 最终验收

Task 7.1 — 全文件 LSP 最终扫描

对所有修改过的文件运行 lsp_diagnostics

  • internal/handler/app/client_realname.go
  • internal/service/polling/manual_trigger_service.go
  • internal/bootstrap/handlers.go
  • pkg/constants/redis.go
  • pkg/config/config.go
  • cmd/api/docs.go
  • cmd/gendocs/main.go

验收:全部 0 error0 新增 warning


Task 7.2 — 环境变量文档记录

确认以下环境变量已在部署文档中记录:

环境变量 默认值 说明
JUNHONG_POLLING_AUTO_TRIGGER_ENABLE_AUTO_TRIGGER true 自动触发开关,可临时关闭
JUNHONG_POLLING_AUTO_TRIGGER_AUTO_TRIGGER_SYSTEM_USER_ID 1 暂用 SuperAdmin。生产环境建议创建专用平台账号user_type=Platform并更新此值

验收:文档中有上述两个环境变量的说明


统计 GetRealnameLink 函数体行数(从 func 到最后一个 }

验收:≤ 100 行


原子提交策略

提交顺序 涵盖文件 Commit Message
Commit 1 pkg/config/config.gopkg/config/defaults/config.yaml feat: 新增轮询自动触发配置EnableAutoTrigger、AutoTriggerSystemUserID
Commit 2 internal/service/polling/manual_trigger_service.gopkg/constants/redis.go fix: 修复手动触发去重TTL和日限制次数优化权限检查和错误日志
Commit 3 internal/handler/app/client_realname.go feat: ClientRealnameHandler 集成异步触发实名检查
Commit 4 internal/bootstrap/handlers.gocmd/api/docs.gocmd/gendocs/main.go feat: 更新依赖注入,传入 ManualTriggerService

⚠️ Commit 3 之后、Commit 4 完成之前,全量编译会报错(构造函数参数不一致)。只做文件级 LSP 检查,不跑全量 build。


风险清单

风险 级别 对策
goroutine 捕获 c *fiber.Ctx 导致 use-after-free 致命 triggerRealnameCheck 只接受标量参数uint、string
GetRealnameLink 超 100 行 Task 4.3 提取两个 helper已批准
processBatchTrigger TTL 漏修 Task 3.2 明确修复两处(已确认)
SuperAdmin(ID=1) 与其他系统进程共享日限额 生产环境创建专用账号后更新配置
Commit 3 后临时全量编译失败 预期行为 文件级 LSP 即可Task 5 完成后全量恢复
assetService.ResolvedAsset 实际类型名称不确定 Task 4.3 执行前先读文件确认类型名

提案 tasks.md 更新规则

文件openspec/changes/realname-trigger-priority-enhancement/tasks.md

每完成一个 Phase 后,必须将对应条目从 - [ ] 改为 - [x]。对应关系如下:

本计划 Phase/Task 提案 tasks.md 条目
Task 1.1 完成 1.1 使用 lsp_diagnostics 检查...
Task 1.2 完成 1.2 分析现有 ClientRealnameHandler...
Task 2.1 完成 2.1 在 pkg/config/ 中新增...
Task 2.2 完成 2.2 更新 pkg/config/defaults/config.yaml...
Task 2.3 完成 2.3 运行 lsp_diagnostics 检查配置...
Task 3.1 完成 3.1 ...三处将 todayCount >= 100 改为...
Task 3.2 完成 3.2 ...将 s.redis.Expire(ctx, dedupeKey, time.Hour) 改为...
Task 3.3 完成 3.3 同步更新 pkg/constants/redis.go...
Task 3.4 完成 3.4 优化 canManageCard 和 canManageCards...
Task 3.5 完成 3.5 改进 TriggerSingle 的错误日志...
Task 3.6 完成 3.6 运行 lsp_diagnostics 检查 ManualTriggerService...
Task 3.7 完成 3.7 使用 PostgreSQL MCP 和 Redis CLI 手动验证...
Task 4.14.2 完成 4.1 在 ClientRealnameHandler 结构体中新增... + 4.2 修改 NewClientRealnameHandler...
Task 4.3 完成(提取 helper — (本计划新增步骤,提案无对应条目)
Task 4.44.5 完成 4.3 在 GetRealnameLink 成功返回... + 4.4 goroutine 内部必须使用... + 4.5 goroutine 内部构造系统用户 context...
Task 4.6 完成 4.6 goroutine 执行失败时记录 WARN 日志...
Task 4.7 完成 4.7 运行 lsp_diagnostics 检查 ClientRealnameHandler...
Task 5.1 完成 5.1 在 internal/bootstrap/handlers.go 中更新...
Task 5.2 完成 5.2 确认 ManualTriggerService 在 bootstrap/services.go 中已初始化...
Task 5.3 完成 5.3 更新 cmd/api/docs.go 和 cmd/gendocs/main.go...
Task 5.4 完成 5.4 运行 lsp_diagnostics 检查 bootstrap 和 docs.go...
Task 6.1 完成 6.1 使用 curl 调用...
Task 6.2 完成 6.2 通过 PostgreSQL MCP 查询 tb_polling_manual_trigger_log...
Task 6.3 完成 6.3 通过 Redis CLI 检查 polling:manual:realname 队列...
Task 6.4 完成 6.4 使用 PostgreSQL MCP 查询今日触发次数...
Task 6.5 完成 6.5 使用 Redis CLI 确认 dedup key TTL 为 24 小时...
Task 6.6 完成 6.6 模拟 manualTriggerSvc 不可用场景...
Task 7.1 完成 7.1 运行 lsp_diagnostics 对所有修改过的文件...
Task 7.2 完成 7.2 确认环境变量...已在部署文档中记录
Task 7.3 完成 7.3 确认 GetRealnameLink handler 函数长度不超过 100 行

更新方式:直接编辑 tasks.md,将对应行的 - [ ] 替换为 - [x]。每个 Phase 执行完毕后集中更新该 Phase 所有条目,不要跨 Phase 提前勾选。