归档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s

This commit is contained in:
2026-04-13 15:03:02 +08:00
parent c1456f3c54
commit 7308afe801
17 changed files with 2526 additions and 6 deletions

View File

@@ -1,8 +1,12 @@
{
"active_plan": "/Users/break/csxjProject/junhong_cmp_fiber/.sisyphus/plans/add-gateway-admin-api.md",
"started_at": "2026-02-02T09:24:48.582Z",
"active_plan": "/Users/break/csxjProject/junhong_cmp_fiber/.sisyphus/plans/realname-trigger-priority-enhancement.md",
"started_at": "2026-04-13T06:40:48.711Z",
"session_ids": [
"ses_3e254bedbffeBTwWDP2VQqDr7q"
"ses_27a6dbcacffe7LBhCAdrKJVgDi"
],
"plan_name": "add-gateway-admin-api"
"session_origins": {
"ses_27a6dbcacffe7LBhCAdrKJVgDi": "direct"
},
"plan_name": "realname-trigger-priority-enhancement",
"agent": "atlas"
}

View File

@@ -0,0 +1,27 @@
# 架构决策记录
## goroutine context 设计(已确认)
- 必须使用 `context.WithTimeout(context.Background(), 3*time.Second)` 创建独立 context
- 禁止复用 `c.UserContext()`Fiber 请求 context 在 handler 返回后立即失效)
- goroutine 只接受标量参数uint、string不捕获 *fiber.Ctx 或任何指针
## 系统用户身份(已确认)
- 使用 `UserTypePlatform` 而非 `UserTypeSuperAdmin`
- Platform 用户仍受 500次/天日限制约束
- SuperAdmin 不受日限制约束(不符合要求)
## 配置结构体命名(已确认)
- 新增 `PollingAutoTriggerConfig` 结构体
- Config 中字段名:`PollingAutoTrigger PollingAutoTriggerConfig`
- YAML key: `polling_auto_trigger`
- 环境变量前缀:`JUNHONG_POLLING_AUTO_TRIGGER_`
## 配置验证(已确认)
- 不在 Validate() 中添加校验
- 零值false/0是合法默认值
## 原子提交策略(已确认)
- Commit 1: config 文件
- Commit 2: service bug fix + redis 注释
- Commit 3: handler 集成(此时全量 build 暂时报错)
- Commit 4: DI 接线(修复全量 build

View File

@@ -0,0 +1,22 @@
# 已知问题和注意事项
## assetService.ResolvedAsset 类型名已确认(重要修正)
- 计划中写的 `assetService.ResolvedAsset` 是错误的
- 实际类型:`*dto.AssetResolveResponse`(在 `internal/model/dto/asset_dto.go` 定义)
- `h.assetService.Resolve()` 返回 `(*dto.AssetResolveResponse, error)`
- 字段:`.AssetType` (string), `.AssetID` (uint), `.VirtualNo` (string)
- `resolveTargetCard` 函数签名应为:
`func (h *ClientRealnameHandler) resolveTargetCard(c *fiber.Ctx, asset *dto.AssetResolveResponse, iccid string) (*model.IotCard, error)`
## import 别名冲突
- client_realname.go 已导入 `internal/middleware` (for GetCustomerID)
- 需要额外导入 `pkg/middleware` (for SetUserContext) 时需使用别名
- 建议: `pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"`
## processBatchTrigger 中也有 TTL Bug
- 不仅 TriggerSingle (行72)processBatchTrigger (行192) 也有相同 Bug
- 两处都需修复,不能漏
## 功能开关检查逻辑
- `config.Get().PollingAutoTrigger.EnableAutoTrigger` 为 false 时跳过 goroutine 启动
- 零值false/0是合法默认值不需要在 Validate() 中校验

View File

@@ -0,0 +1,37 @@
# 项目约定和模式
## 模块路径
- 项目模块路径:`github.com/break/junhong_cmp_fiber`
## 关键包路径(区分 pkg vs internal
- `pkg/middleware` - 包含 `SetUserContext`, `UserContextInfo`, `GetUserTypeFromContext`
- `internal/middleware` - 包含 `GetCustomerID(c *fiber.Ctx)` (C端个人客户认证)
- `internal/service/polling` - ManualTriggerService 所在包
- `pkg/constants` - UserTypePlatform, TaskTypePollingRealname 等常量
## 重要发现
- `SetUserContext``pkg/middleware` 包,不在 `internal/middleware`
- `UserContextInfo` 字段UserID(uint), UserType(int), ShopID(uint), EnterpriseID(uint), CustomerID(uint)
- `GetCustomerID(c *fiber.Ctx)``internal/middleware/personal_auth.go`,返回 (uint, bool)
- manual_trigger_service.go 当前 469 行,进行重构
## 当前待修复 Bug
- `todayCount >= 100` 在 3 处行58, 129, 238需改为 `>= 500`
- `time.Hour` TTL 在 2 处行72, 192需改为 `24*time.Hour`
- `RedisPollingManualDedupeKey` 注释写的"1小时"需改为"24小时"
## client_realname.go 重构要点
- 当前 `GetRealnameLink` 函数 127 行,加新代码会超 100 行限制
- 需提取 `resolveTargetCard(c *fiber.Ctx, asset, iccid)` - 资产类型 switch
- 需提取 `buildRealnameResponse(ctx, card, carrier)` - 运营商 URL 调度
- 注意:`findCardInDeviceBindings``findFirstBoundCard` 已存在,可从 resolveTargetCard 调用
- goroutine 传标量值uint, string不能捕获 `c *fiber.Ctx`
## 依赖注入
- `handlers.go` 第 66 行:`ClientRealname: app.NewClientRealnameHandler(...)` 需加第8个参数
- `docs.go``gendocs/main.go` 各有一处 `NewClientRealnameHandler(nil*7)` 需改为 8个nil
- `svc.PollingManualTrigger` 已在 services.go:210 初始化,可直接使用
## Commit 3 后临时编译失败
- Task 4 修改构造函数后、Task 5 接线前,全量编译报错是预期行为
- 只做文件级 LSP 检查,不跑全量 build

View File

@@ -0,0 +1,691 @@
# 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.go`、`cmd/api/docs.go`、`cmd/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 结构体附近):
```go
// 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 字段之后):
```go
PollingAutoTrigger PollingAutoTriggerConfig `mapstructure:"polling_auto_trigger"`
```
**陷阱**:不要在 `Validate()` 中添加校验——零值false/0是合法默认值
**验收**结构体定义正确mapstructure tag 与 yaml key 对应
---
### Task 2.2 — 更新默认配置 YAML
**文件**`pkg/config/defaults/config.yaml`
在文件末尾添加(保留尾部换行):
```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_diagnostics``pkg/config/config.go`
**验收**0 error
---
## Phase 2 — 手动触发服务 Bug 修复
### Task 3.1 — 日限制 100 → 5003处
**文件**`internal/service/polling/manual_trigger_service.go`
| 行号 | 修改内容 |
|------|---------|
| 58 | `todayCount >= 100``todayCount >= 500`,同步更新行内注释 `// 每日最多触发100次``// 每日最多触发500次` |
| 129 | `todayCount >= 100``todayCount >= 500`,补充注释 `// 每日最多触发500次` |
| 238 | `todayCount >= 100``todayCount >= 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.Hour``24*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`
`canManageCard``canManageCards``applyShopPermissionFilter` 三处提取重复的用户类型检查,新增私有方法:
```go
// 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
}
```
三个调用方改为:
```go
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_by``task_type``error` |
| Redis去重操作失败 | `card_id``task_type``error` |
| 创建触发日志失败 | `card_id``triggered_by``error` |
| 写入队列失败 | `card_id``task_type``error` |
**验收**:每条 return err 路径有对应日志,级别为 Error
---
### Task 3.6 — LSP 检查
**操作**`lsp_diagnostics``internal/service/polling/manual_trigger_service.go`
**验收**0 error0 新增 warning
---
### Task 3.7 — 手动验证去重 + 限制
**操作**(通过 Redis CLI 或 PostgreSQL MCP
```bash
# 1. 触发某张卡,验证 TTL 为 24小时
redis-cli TTL polling:manual:dedupe:polling:realname
# 期望:≈ 86400
# 2. 再次触发同一张卡,验证被拒绝
# 期望返回:"该卡已在手动触发队列中"
# 3. 删除 dedupe key 后重新触发,验证成功
redis-cli DEL polling:manual:dedupe:polling:realname
```
```sql
-- 验证今日触发次数计数正确
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/polling``internal/polling` 是不同包):
```go
pollingSvc "github.com/xxx/junhong_cmp_fiber/internal/service/polling"
```
`ClientRealnameHandler` 结构体新增字段:
```go
manualTriggerSvc *pollingSvc.ManualTriggerService // 手动触发服务可为nilnil时跳过自动触发
```
**验收**结构体字段添加正确import 路径正确
---
### Task 4.2 — 修改构造函数第8个参数
**文件**`internal/handler/app/client_realname.go`
`NewClientRealnameHandler` 末尾新增参数:
```go
func NewClientRealnameHandler(
// ...现有7个参数保持不变...
manualTriggerSvc *pollingSvc.ManualTriggerService, // 可为nil
) *ClientRealnameHandler {
return &ClientRealnameHandler{
// ...现有字段初始化...
manualTriggerSvc: manualTriggerSvc,
}
}
```
**陷阱**:修改后 `docs.go``gendocs/main.go``bootstrap/handlers.go` 会出现编译错误(构造函数参数不一致),待 Task 5 修复。此阶段**只做文件级** LSP 检查,不跑全量 build
**验收**`client_realname.go` 本身 LSP 0 error
---
### Task 4.3 — 提取辅助方法(解决 GetRealnameLink 行数超限问题)
**文件**`internal/handler/app/client_realname.go`
**背景**:当前 `GetRealnameLink` 127行加 goroutine 代码约133行违反 ≤100行规范。需提取两个私有方法。
**提取1`resolveTargetCard`**(资产类型 switch 分支约26行
```go
// 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, "不支持的资产类型")
}
}
```
**提取2`buildRealnameResponse`**(运营商 URL 调度约32行
```go
// 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
- `buildRealnameResponse``ctx 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)` 之前插入:
```go
// 异步触发实名检查,提升检测优先级;失败不影响主流程
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"`
```go
// 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 — 日志字段核查
**检查清单**
- [x] WARN 路径包含:`customer_id``iccid``card_id``error`
- [x] INFO 路径包含:`customer_id``iccid``card_id`
- [x] 无 nil panic 风险err 已检查)
- [x] goroutine 无 panic 风险manualTriggerSvc 已在调用前检查非 nil
**验收**:所有字段齐全,无潜在 panic
---
### Task 4.7 — LSP 检查 Handler 文件
**操作**`lsp_diagnostics``internal/handler/app/client_realname.go`
**注意**:此时 `bootstrap/handlers.go``docs.go``gendocs/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` 参数:
```go
// 修改前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` 已有:
```go
PollingManualTrigger: pollingSvc.NewManualTriggerService(...)
```
**无需变更**,仅确认
**验收**`svc.PollingManualTrigger` 非 nil
---
### Task 5.3 — 更新 docs.go 和 gendocs/main.go
**文件**`cmd/api/docs.go``cmd/gendocs/main.go`
两处均将 `NewClientRealnameHandler` 调用从 7个nil 改为 8个nil
```go
// 修改前
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 启动
```bash
curl -H "Authorization: Bearer <token>" \
"http://localhost:3000/api/c/v1/realname/link?identifier=<iccid>"
```
**查看 app.log** 确认出现:
```json
{"level":"info","msg":"自动触发实名检查成功","customer_id":...,"iccid":"...","card_id":...}
```
**验收**响应正常返回实名链接日志在响应后3秒内出现
---
### Task 6.2 — DB 验证触发记录
```sql
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 = 1`SuperAdmin`task_type = 'polling:realname'``total_count = 1`
**验收**:记录存在且字段正确
---
### Task 6.3 — Redis 队列验证
```bash
redis-cli LRANGE polling:manual:polling:realname 0 -1
```
**期望**:卡 ID 存在于队列中
**验收**:队列条目可见
---
### Task 6.4 — 日限制计数验证
```sql
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 验证
```bash
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并更新此值** |
**验收**:文档中有上述两个环境变量的说明
---
### Task 7.3 — GetRealnameLink 行数确认
统计 `GetRealnameLink` 函数体行数(从 `func` 到最后一个 `}`
**验收**:≤ 100 行
---
## 原子提交策略
| 提交顺序 | 涵盖文件 | Commit Message |
|---------|---------|---------------|
| Commit 1 | `pkg/config/config.go``pkg/config/defaults/config.yaml` | `feat: 新增轮询自动触发配置EnableAutoTrigger、AutoTriggerSystemUserID` |
| Commit 2 | `internal/service/polling/manual_trigger_service.go``pkg/constants/redis.go` | `fix: 修复手动触发去重TTL和日限制次数优化权限检查和错误日志` |
| Commit 3 | `internal/handler/app/client_realname.go` | `feat: ClientRealnameHandler 集成异步触发实名检查` |
| Commit 4 | `internal/bootstrap/handlers.go``cmd/api/docs.go``cmd/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 提前勾选。