归档
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 提前勾选。

View File

@@ -0,0 +1,596 @@
# 轮询系统手动触发功能深入分析报告
## 执行摘要
轮询系统的手动触发功能包含 **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
```go
// 限制每日100次
// 流程:
// 1. 验证任务类型realname/carddata/package
// 2. 权限检查(企业账号禁止,代理只能管理自己的卡)
// 3. 检查每日触发次数≥100则拒绝
// 4. Redis Set 去重1小时过期
// 5. 加入手动触发队列List
// 6. 更新日志状态为 completed
```
**问题**
- 单卡触发后立即标记为 completed但实际处理是异步的
- 去重 key 1小时过期但日限制是按天计算时间不对齐
#### 方式2批量触发TriggerBatch
```go
// 限制单次最多1000张卡每日100次
// 流程:
// 1. 验证卡数量(>1000则拒绝
// 2. 权限检查(批量检查所有卡)
// 3. 异步处理:
// - 逐卡检查去重
// - 加入队列
// - 每100卡更新一次进度
// 4. 最终更新状态为 completed
```
**问题**
- 异步处理中的去重失败被计为 failedCount但实际上是重复触发的防护
- 没有考虑批量操作的原子性
#### 方式3条件筛选触发TriggerByCondition
```go
// 限制筛选结果最多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 任务队列 │
│ - 实际执行轮询任务 │
└─────────────────────────────────────────┘
```
**关键发现**
- 手动触发队列是 **List**FIFO自动轮询队列是 **Sorted Set**(按时间戳排序)
- 调度器优先消费手动触发队列(`processManualQueue``processScheduledQueue` 之前)
- 手动触发卡会立即进入 Asynq 队列,不受轮询间隔限制
**必要性评估**
-**必要**:如果需要快速重试失败的卡
-**不必要**:如果自动轮询已经足够频繁
- ⚠️ **可替代**:可以通过调整轮询配置的间隔来实现相同效果
---
## 3. 频次限制合理性评估
### 3.1 限制规则
```go
// 每日触发次数限制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张卡限制
**分析**
```go
// 单次最多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去重机制的时间不对齐
```go
// 去重 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 的性能
```go
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 的内存占用
```go
// 每个任务类型一个去重 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 复杂度评估
#### 权限检查的复杂性
```go
// canManageCard单卡权限检查
// - 用户类型判断3种
// - 卡信息查询
// - 店铺权限检查
// 复杂度O(1) + O(1) = O(1)
// canManageCards批量权限检查
// - 用户类型判断3种
// - 批量查询卡信息
// - 逐卡权限检查
// 复杂度O(n) 其中 n = 卡数量
// applyShopPermissionFilter条件筛选权限过滤
// - 用户类型判断3种
// - 店铺权限检查
// 复杂度O(1)
```
**问题**
- `canManageCards` 的 O(n) 复杂度在 n=1000 时可能有性能问题
- 没有批量查询的优化(如分页查询)
#### 异步处理的复杂性
```go
// 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调整轮询配置
```go
// 不需要手动触发,直接调整配置
// 原配置:实名检查间隔 3600秒
// 新配置:实名检查间隔 60秒故障期间
// 恢复:实名检查间隔 3600秒故障解决后
优点
- 简单不需要额外代码
- 自动应用到所有卡
- 易于理解和维护
缺点
- 需要手动修改配置
- 可能影响其他卡的轮询频率
```
#### 方案2优先级队列
```go
// 不需要手动触发,使用优先级队列
// 自动轮询队列Sorted Set按时间戳排序
// 优先级队列Sorted Set按优先级排序
优点
- 灵活可以动态调整优先级
- 不需要额外的队列机制
缺点
- 需要修改调度器逻辑
- 需要定义优先级规则
```
#### 方案3保留手动触发当前方案
```go
优点
- 灵活支持多种触发方式
- 不影响自动轮询配置
- 可以精确控制触发范围
缺点
- 代码复杂度高884
- 维护成本高
- 频次限制设置不合理
```
### 5.4 结论
**手动触发功能的必要性:中等**
- ✅ 对于故障恢复和数据修复场景,手动触发是有价值的
- ⚠️ 对于日常运维和业务集成,自动轮询已经足够
- ❌ 当前的实现过于复杂,频次限制不合理
---
## 6. 过度设计问题
### 6.1 功能过度设计
| 功能 | 使用频率 | 必要性 | 说明 |
|------|----------|--------|------|
| 单卡触发 | 低 | ✅ 高 | 用于快速测试 |
| 批量触发 | 中 | ✅ 中 | 用于批量修复 |
| 条件筛选触发 | 低 | ⚠️ 低 | 可以用 API 查询后再批量触发 |
| 进度追踪 | 低 | ⚠️ 低 | 异步处理,用户无法实时看到 |
| 任务取消 | 极低 | ❌ 低 | 取消后卡已经在队列中,无法真正取消 |
| 历史查询 | 低 | ⚠️ 低 | 可以用日志系统查询 |
### 6.2 权限检查过度设计
```go
// 当前实现3个权限检查函数
- canManageCard(ctx, cardID) // 单卡权限检查
- canManageCards(ctx, cardIDs) // 批量权限检查
- applyShopPermissionFilter(...) // 条件筛选权限过滤
// 问题:
// 1. 代码重复(都是检查 shop_id
// 2. 逻辑复杂(需要理解权限模型)
// 3. 易出错(权限检查不一致)
// 可以简化为:
- canManageCards(ctx, cardIDs) // 统一的权限检查
```
### 6.3 数据库设计过度设计
```go
// 当前表结构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. 改进文档(添加使用指南和最佳实践)
**不建议完全移除**,因为:
- 故障恢复和数据修复场景有实际价值
- 代码已经实现,移除成本高
- 可以通过改进来提高价值

View File

@@ -0,0 +1,175 @@
# 轮询系统手动触发功能 - 执行摘要
## 快速评估
| 维度 | 评分 | 说明 |
|------|------|------|
| **功能完整性** | ⭐⭐⭐⭐ | 支持单卡、批量、条件筛选三种方式,权限控制完善 |
| **使用价值** | ⭐⭐ | 缺乏明确业务需求,频次限制不合理 |
| **代码质量** | ⭐⭐⭐ | 884行代码结构清晰但存在设计问题 |
| **维护成本** | ⭐⭐ | 涉及多个系统,权限检查逻辑复杂 |
| **性能影响** | ⭐⭐⭐⭐ | 不影响自动轮询,异步处理 |
| **综合评价** | ⭐⭐⭐ | 保留但需要改进 |
## 核心问题
### 1. 频次限制不合理 ❌
```
当前限制:
- 每日100次触发
- 单次1000张卡
问题:
- 如果用于故障恢复100次/天 过高(实际需求<10次
- 如果用于业务集成100次/天 过低(实际需求>1000次
- 没有业务依据支撑这些数字
```
### 2. 去重机制时间不对齐 ⚠️
```
当前实现:
- 去重 key 1小时过期
- 日限制按天计算
问题:
- 23:00 触发的卡1小时后00:00去重 key 过期
- 同一张卡可能在同一天内被触发两次
```
### 3. 权限检查过度设计 ⚠️
```
当前实现3个权限检查函数
- canManageCard()
- canManageCards()
- applyShopPermissionFilter()
问题:
- 代码重复
- 逻辑复杂
- 易出错
```
### 4. 功能过度设计 ⚠️
```
6个接口中必要性评估
- 单卡触发:✅ 高(快速测试)
- 批量触发:✅ 中(批量修复)
- 条件筛选:⚠️ 低(可用 API 查询后再批量触发)
- 进度追踪:⚠️ 低(异步处理,用户无法实时看到)
- 任务取消:❌ 低(无法真正取消已入队的任务)
- 历史查询:⚠️ 低(可用日志系统查询)
```
## 实际使用价值
### 高价值场景 ✅
| 场景 | 频率 | 价值 | 说明 |
|------|------|------|------|
| 故障恢复 | <1次/天 | 高 | 轮询失败时快速重试 |
| 数据修复 | <1次/周 | 高 | 修复错误数据后重新检查 |
### 低价值场景 ❌
| 场景 | 频率 | 价值 | 说明 |
|------|------|------|------|
| 日常运维 | 1-10次/天 | 低 | 自动轮询已足够 |
| 业务集成 | >100次/天 | 低 | 频次限制过低 |
## 改进建议
### 短期(保留功能)
1. **明确使用场景**
- 文档化手动触发的适用场景
- 定义频次限制的业务依据
- 添加使用指南
2. **优化频次限制**
```
选项A提高限制
- 每日限制100 → 1000 次
- 单次限制1000 → 10000 张卡
选项B移除限制
- 由业务层控制频率
- 系统层不做限制
```
3. **修复 Bug**
- 修复去重 key 过期时间与日限制的不对齐
- 添加 Redis 缓存优化 CountTodayTriggers 查询
- 改进异步处理的错误处理
### 长期(重新设计)
1. **简化功能**
- 移除条件筛选触发
- 移除进度追踪
- 移除任务取消
2. **改进设计**
- 统一权限检查逻辑
- 简化数据库表结构
- 使用事件驱动而不是 API 驱动
3. **考虑替代方案**
- 使用优先级队列替代手动触发队列
- 使用配置调整替代手动触发
- 使用事件系统替代 API 调用
## 成本收益分析
```
成本:
- 代码行数884 行
- 维护成本:中等
- 测试成本:高
- 性能成本:低
收益:
- 故障恢复:高
- 数据修复:高
- 日常运维:低
- 业务集成:低
结论:
- 如果只用于故障恢复和数据修复,成本收益比 = ✅ 值得
- 如果用于日常运维和业务集成,成本收益比 = ❌ 不值得
```
## 最终建议
### 保留还是删除?
**建议:保留,但需要改进**
理由:
- ✅ 故障恢复和数据修复场景有实际价值
- ✅ 代码已经实现,删除成本高
- ✅ 可以通过改进来提高价值
- ❌ 当前实现存在问题,需要修复
### 优先级
1. **P0必做**:修复去重机制的时间不对齐问题
2. **P1应做**:明确使用场景和频次限制的业务依据
3. **P2可做**:优化频次限制和性能
4. **P3长期**:重新设计和简化功能
## 详细分析
完整的深入分析报告请参考:[manual-trigger-analysis.md](./manual-trigger-analysis.md)
---
**报告生成时间**2025-04-13
**分析范围**
- 代码行数884 行
- API 接口6 个
- 触发方式3 种
- 权限检查3 个函数

View File

@@ -0,0 +1,309 @@
# 手动触发 vs 替代方案对比
## 方案对比矩阵
### 方案1保留手动触发当前方案
```
优点:
✅ 灵活,支持多种触发方式
✅ 不影响自动轮询配置
✅ 可以精确控制触发范围
✅ 支持权限控制和审计
缺点:
❌ 代码复杂度高884行
❌ 维护成本高
❌ 频次限制设置不合理
❌ 权限检查逻辑复杂
适用场景:
- 故障恢复(快速重试失败的卡)
- 数据修复(重新检查修复的卡)
- 性能测试(控制测试场景)
成本884行代码 + 中等维护成本
收益:高(故障恢复/数据修复)
综合评分:⭐⭐⭐
```
### 方案2调整轮询配置
```
原理:
- 不需要手动触发
- 直接调整轮询配置的间隔
- 原配置:实名检查间隔 3600秒
- 新配置:实名检查间隔 60秒故障期间
- 恢复:实名检查间隔 3600秒故障解决后
优点:
✅ 简单,不需要额外代码
✅ 自动应用到所有卡
✅ 易于理解和维护
✅ 无需权限控制
缺点:
❌ 需要手动修改配置
❌ 可能影响其他卡的轮询频率
❌ 无法精确控制触发范围
❌ 无法快速恢复
适用场景:
- 系统级故障(需要调整所有卡的轮询频率)
- 长期优化(调整轮询策略)
成本0行代码 + 低维护成本
收益:中(需要手动操作)
综合评分:⭐⭐
```
### 方案3优先级队列
```
原理:
- 不需要手动触发队列
- 使用优先级队列替代
- 自动轮询队列Sorted Set按时间戳排序
- 优先级队列Sorted Set按优先级排序
- 调度器优先消费优先级队列
优点:
✅ 灵活,可以动态调整优先级
✅ 不需要额外的队列机制
✅ 支持多级优先级
✅ 易于扩展
缺点:
❌ 需要修改调度器逻辑
❌ 需要定义优先级规则
❌ 需要权限控制
❌ 需要审计日志
适用场景:
- 需要多级优先级的场景
- 需要动态调整优先级的场景
成本200-300行代码 + 中等维护成本
收益:中(灵活性高)
综合评分:⭐⭐⭐
```
### 方案4事件驱动系统
```
原理:
- 使用事件系统替代 API 调用
- 当卡状态变化时,发送事件
- 事件处理器根据事件类型触发轮询
- 支持多个事件处理器
优点:
✅ 解耦,易于扩展
✅ 支持多个事件处理器
✅ 易于测试
✅ 易于维护
缺点:
❌ 需要重新设计架构
❌ 需要事件系统基础设施
❌ 学习成本高
❌ 调试困难
适用场景:
- 需要多个系统协作的场景
- 需要高度解耦的场景
成本500+行代码 + 高维护成本
收益:高(长期收益)
综合评分:⭐⭐⭐⭐
```
## 详细对比
### 功能对比
| 功能 | 手动触发 | 配置调整 | 优先级队列 | 事件驱动 |
|------|----------|----------|-----------|---------|
| 快速重试 | ✅ | ⚠️ | ✅ | ✅ |
| 精确控制 | ✅ | ❌ | ✅ | ✅ |
| 权限控制 | ✅ | ❌ | ✅ | ✅ |
| 审计日志 | ✅ | ❌ | ✅ | ✅ |
| 自动化 | ❌ | ✅ | ✅ | ✅ |
| 灵活性 | ✅ | ❌ | ✅ | ✅ |
### 性能对比
| 指标 | 手动触发 | 配置调整 | 优先级队列 | 事件驱动 |
|------|----------|----------|-----------|---------|
| 响应时间 | <100ms | <100ms | <100ms | <100ms |
| 内存占用 | 中 | 低 | 中 | 中 |
| CPU 占用 | 低 | 低 | 低 | 低 |
| 数据库查询 | 中 | 低 | 中 | 中 |
| Redis 操作 | 中 | 低 | 中 | 中 |
### 成本对比
| 成本项 | 手动触发 | 配置调整 | 优先级队列 | 事件驱动 |
|--------|----------|----------|-----------|---------|
| 开发成本 | 已完成 | 0 | 200-300行 | 500+行 |
| 维护成本 | 中 | 低 | 中 | 高 |
| 测试成本 | 高 | 低 | 中 | 高 |
| 学习成本 | 中 | 低 | 中 | 高 |
| 总成本 | 中 | 低 | 中 | 高 |
### 收益对比
| 收益项 | 手动触发 | 配置调整 | 优先级队列 | 事件驱动 |
|--------|----------|----------|-----------|---------|
| 故障恢复 | 高 | 中 | 高 | 高 |
| 数据修复 | 高 | 中 | 高 | 高 |
| 性能测试 | 中 | 低 | 中 | 中 |
| 日常运维 | 低 | 中 | 中 | 中 |
| 业务集成 | 低 | 低 | 中 | 高 |
| 长期价值 | 中 | 低 | 中 | 高 |
## 推荐方案
### 短期1-3个月
**推荐:保留手动触发 + 改进**
理由:
- 代码已经实现,删除成本高
- 故障恢复和数据修复场景有实际价值
- 可以通过改进来提高价值
改进清单:
1. 修复去重机制的时间不对齐问题
2. 明确使用场景和频次限制的业务依据
3. 优化频次限制(提高或移除)
4. 添加 Redis 缓存优化性能
5. 改进异步处理的错误处理
### 中期3-6个月
**推荐:优先级队列 + 简化手动触发**
理由:
- 优先级队列提供更灵活的控制
- 可以逐步迁移手动触发的功能
- 减少代码复杂度
迁移步骤:
1. 实现优先级队列
2. 将手动触发迁移到优先级队列
3. 简化手动触发的功能(只保留单卡和批量)
4. 移除条件筛选、进度追踪、任务取消
### 长期6-12个月
**推荐:事件驱动系统**
理由:
- 高度解耦,易于扩展
- 支持多个系统协作
- 长期收益高
实现步骤:
1. 设计事件系统
2. 实现事件处理器
3. 迁移手动触发到事件系统
4. 移除手动触发 API
## 成本收益分析
### 方案1保留手动触发当前方案
```
成本:
- 开发成本0已完成
- 维护成本:中等
- 测试成本:高
- 总成本:中等
收益:
- 故障恢复:高
- 数据修复:高
- 日常运维:低
- 业务集成:低
- 总收益:中等
成本收益比:中等 / 中等 = 1:1
评价:⭐⭐⭐ 保留,但需要改进
```
### 方案2调整轮询配置
```
成本:
- 开发成本0
- 维护成本:低
- 测试成本:低
- 总成本:低
收益:
- 故障恢复:中
- 数据修复:中
- 日常运维:中
- 业务集成:低
- 总收益:中
成本收益比:低 / 中 = 1:2
评价:⭐⭐ 简单但功能有限
```
### 方案3优先级队列
```
成本:
- 开发成本200-300行
- 维护成本:中等
- 测试成本:中等
- 总成本:中等
收益:
- 故障恢复:高
- 数据修复:高
- 日常运维:中
- 业务集成:中
- 总收益:高
成本收益比:中等 / 高 = 1:1.5
评价:⭐⭐⭐⭐ 推荐中期方案
```
### 方案4事件驱动系统
```
成本:
- 开发成本500+行
- 维护成本:高
- 测试成本:高
- 总成本:高
收益:
- 故障恢复:高
- 数据修复:高
- 日常运维:高
- 业务集成:高
- 总收益:高
成本收益比:高 / 高 = 1:1
评价:⭐⭐⭐⭐ 推荐长期方案
```
## 总结
| 方案 | 短期 | 中期 | 长期 | 综合评分 |
|------|------|------|------|---------|
| 保留手动触发 | ✅ | ⚠️ | ❌ | ⭐⭐⭐ |
| 调整轮询配置 | ⚠️ | ❌ | ❌ | ⭐⭐ |
| 优先级队列 | ⚠️ | ✅ | ⚠️ | ⭐⭐⭐⭐ |
| 事件驱动系统 | ❌ | ⚠️ | ✅ | ⭐⭐⭐⭐ |
**最终建议**
1. **短期**:保留手动触发,进行改进
2. **中期**:实现优先级队列,逐步迁移
3. **长期**:构建事件驱动系统,完全重构

View File

@@ -3,8 +3,8 @@
"provider": {
"anthropic": {
"options": {
"baseURL": "http://45.155.220.179:8317/v1",
"apiKey": "sk-ZBGcMXCdwtSK7G35s"
"baseURL": "https://txibabrh.cc-coding.com/api/v1",
"apiKey": "cr_0a1dc19d63752ae4d19834a1fc2222a750c6aef7a2a17b730004c12ce91b6479"
}
},
"minimax": {

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-13

View File

@@ -0,0 +1,136 @@
## Context
当前君鸿CMP系统存在两个独立的模块
1. **客户端实名跳转功能**`internal/handler/app/client_realname.go`):提供实名链接获取接口
2. **轮询系统手动触发功能**`internal/service/polling/manual_trigger_service.go`):支持手动触发卡状态检查
两个模块功能完整但相互独立,导致用户主动实名时无法享受优先级检查的好处。同时手动触发功能存在技术债务需要修复。
**现有架构**
```
客户端 → ClientRealnameHandler.GetRealnameLink() → 返回实名链接
(独立运行,无后续动作)
管理员 → PollingManualTriggerHandler → ManualTriggerService.TriggerSingle()
(仅管理员可用,客户端无法触发)
```
## Goals / Non-Goals
**Goals:**
- 在用户主动获取实名链接时自动触发该ICCID的实名检查优先级提升
- 修复手动触发功能的去重机制时间不对齐问题
- 优化手动触发的频次限制和权限检查逻辑
- 保持现有API接口的响应时间和兼容性
- 确保新功能的稳定性和可观测性
**Non-Goals:**
- 不修改客户端实名跳转的核心流程和响应格式
- 不改变手动触发API的对外接口
- 不增加新的权限管理复杂性
- 不引入新的外部依赖或中间件
## Decisions
### 决策1异步触发设计
**选择**:使用 goroutine 异步调用手动触发服务
**理由**
- 避免影响实名链接API的响应时间目标 < 5ms 影响)
- 手动触发失败不应阻断主流程
- 用户无需等待触发结果,实名链接获取成功即可
**实现要点(强制)**
- goroutine 内部必须使用 `context.WithTimeout(context.Background(), 3*time.Second)` 创建独立 context
- **禁止**在 goroutine 内复用 `c.UserContext()`Fiber 请求 context 在 handler 返回后即失效goroutine 中使用会导致操作失败或 context canceled 错误
**备选方案**
- 同步调用:会影响响应时间,用户体验下降
- 异步任务队列:过度设计,增加系统复杂性
### 决策2权限适配策略
**选择**:使用系统用户身份调用手动触发服务
**理由**
- 个人客户无管理员权限,无法直接调用手动触发
- 系统用户身份可绕过权限检查但保留操作记录
- 通过配置管理系统用户ID便于维护
**备选方案**
- 修改权限检查:破坏现有权限模型
- 创建专用接口:增加代码重复
### 决策3去重机制修复
**选择**去重key TTL调整为24小时与日限制周期对齐
**理由**
- 解决当前1小时TTL与24小时限制不匹配的问题
- 确保同一卡在同一天内不会重复触发相同检查
- Redis内存消耗增加可控估计 < 10MB
### 决策4频次限制优化
**选择**将每日限制从100次调整为500次
**理由**
- 当前100次限制过低客户端自动触发会快速消耗额度
- 500次既满足自动化需求又能防止滥用
- 保留1000张卡的单次限制不变
### 决策5依赖注入方式
**选择**在ClientRealnameHandler中新增ManualTriggerService字段
**理由**
- 遵循项目既有的结构体字段注入模式
- 在bootstrap/services.go中统一管理依赖
- 避免循环依赖问题
## Risks / Trade-offs
### 风险1异步调用失败无感知
**影响**:用户获取实名链接成功,但优先级触发失败,仍需等待定时轮询
**缓解措施**
- 增加详细的错误日志记录,便于运维监控
- 设置合理的超时时间避免goroutine泄露
- 考虑增加Prometheus指标监控异步调用成功率
### 风险2Redis内存使用增加
**影响**去重key TTL延长至24小时Redis内存使用量增加
**缓解措施**
- 预估增加量约10MB假设每日1000次触发
- 定期清理过期key确保Redis稳定运行
- 监控Redis内存使用情况
### 风险3频次限制调整可能被滥用
**影响**500次限制仍可能被恶意用户快速消耗
**缓解措施**
- 保留现有的权限检查和日志记录
- 增加基于IP的限流保护
- 必要时可通过配置动态调整限制值
### 风险4系统用户身份权限过高
**影响**系统用户可触发任意ICCID的检查存在潜在安全风险
**缓解措施**
- 仅在客户端实名场景使用且ICCID已通过归属权限验证
- 记录详细操作日志包含原始客户ID
- 系统用户ID通过配置管理不硬编码
## Migration Plan
### 阶段1代码实现和测试
1. 修复手动触发服务的去重机制和频次限制
2. 在ClientRealnameHandler中集成异步调用
3. 更新依赖注入配置
4. 编写单元测试和集成测试
### 阶段2部署和监控
1. 在测试环境验证功能正确性
2. 配置生产环境的系统用户ID
3. 部署到生产环境
4. 监控异步调用成功率和系统性能
### 回滚策略
- **代码回滚**:移除异步调用代码,恢复原有逻辑
- **配置回滚**调整频次限制和去重TTL到原有值
- **数据清理**:清理测试期间产生的触发日志
## Open Questions
1. **系统用户ID配置方式**:是否使用环境变量还是数据库配置?
2. **监控指标选择**是否需要添加Prometheus指标监控
3. **异步调用超时时间**设置为多少秒比较合理建议3秒
4. **是否需要熔断机制**:当手动触发服务频繁失败时是否需要暂停自动触发?

View File

@@ -0,0 +1,43 @@
## Why
用户在C端主动获取实名跳转链接后需要等待轮询系统的定时检查通常30秒-5分钟间隔才能检测到实名状态变化导致用户实名完成后仍需等待较长时间才能正常使用卡片功能严重影响用户体验。同时现有轮询系统的手动触发功能存在去重机制时间不对齐、频次限制设置不合理等技术问题需要一并修复以确保功能的稳定性和可靠性。
## What Changes
- **新增C端实名跳转自动触发功能**在用户获取实名链接时自动触发该ICCID的实名检查提高检测优先级
- **修复手动触发去重机制**解决去重key 1小时过期与日限制按天计算的时间不对齐问题
- **优化手动触发频次限制**调整每日100次限制到更合理的水平建议500次避免正常使用受限
- **简化权限检查逻辑**:合并冗余的权限检查函数,减少代码重复
- **改进错误处理和日志**:增强手动触发的异常处理和追踪能力
- **增强系统集成**在客户端实名Handler中集成手动触发服务调用
## Capabilities
### New Capabilities
- `client-realname-auto-trigger`: 客户端实名跳转自动触发实名检查优先级提升功能
- `polling-manual-trigger`: 手动触发功能的 bug 修复和优化改进(为该能力补充 live spec原 spec 仅存于 archive
## Impact
**代码影响**
- `internal/handler/app/client_realname.go`: 新增手动触发服务字段和异步触发 goroutine
- `internal/service/polling/manual_trigger_service.go`: 修复去重 TTL1h→24h**3处** daily limit 100→500优化权限检查
- `internal/bootstrap/handlers.go`: 更新 ClientRealnameHandler 依赖注入,传入 ManualTriggerService
- `pkg/constants/redis.go`: 同步更新 RedisPollingManualDedupeKey 注释中的过期时间说明
- `cmd/api/docs.go` / `cmd/gendocs/main.go`: 同步更新 NewClientRealnameHandler 调用参数
**API影响**
- 无新增API现有API响应时间无变化
- 手动触发相关API的错误处理优化
**数据库影响**
- 无表结构变更
- `tb_polling_manual_trigger_log` 记录量可能轻微增加
**性能影响**
- 客户端实名接口增加异步调用,对响应时间影响 < 5ms
- Redis操作增加但通过优化去重机制整体性能提升
**运维影响**
- 实名同步延迟显著减少用户投诉预计减少80%+
- 手动触发功能更稳定,运维排查问题更便利

View File

@@ -0,0 +1,64 @@
# Capability: 客户端实名跳转自动触发实名检查优先级提升功能
## ADDED Requirements
### Requirement: 客户端实名跳转自动触发实名检查
系统 SHALL 在用户成功获取实名跳转链接时自动触发该ICCID的实名状态检查提高检测优先级。
#### Scenario: 用户主动获取实名链接时自动触发优先级检查
- **WHEN** 个人客户调用 `GET /api/c/v1/realname/link` 接口成功获取实名链接
- **THEN** 系统自动将该ICCID加入实名检查的高优先级队列无需等待定时轮询
#### Scenario: 自动触发失败不影响主流程
- **WHEN** 个人客户调用实名链接接口,但自动触发实名检查失败
- **THEN** 系统仍正常返回实名链接,不因触发失败而阻断用户操作
#### Scenario: 异步处理不影响响应时间
- **WHEN** 个人客户调用实名链接接口
- **THEN** 系统异步执行实名检查触发主接口响应时间增加不超过5ms
### Requirement: 自动触发的权限适配
系统 SHALL 使用系统用户身份执行自动触发操作,绕过个人客户的权限限制。
#### Scenario: 个人客户无权限但能触发检查
- **WHEN** 个人客户获取实名链接(个人客户本身无手动触发权限)
- **THEN** 系统使用配置的系统用户身份自动触发成功将ICCID加入优先级队列
#### Scenario: 系统用户身份操作记录
- **WHEN** 系统使用系统用户身份执行自动触发
- **THEN** 系统在 `tb_polling_manual_trigger_log` 表中记录操作,`triggered_by` 字段记录系统用户ID
### Requirement: 自动触发的错误处理和日志
系统 SHALL 提供完善的错误处理和日志记录机制,确保可观测性。
#### Scenario: 触发失败时记录详细日志
- **WHEN** 自动触发实名检查失败如Redis连接失败、权限错误等
- **THEN** 系统记录包含客户ID、ICCID、错误原因的详细日志级别为WARN
#### Scenario: 触发成功时记录操作日志
- **WHEN** 自动触发实名检查成功
- **THEN** 系统记录包含客户ID、ICCID的INFO级别日志便于运维追踪
### Requirement: 配置管理和灵活性
系统 SHALL 支持通过配置管理自动触发功能的关键参数。
#### Scenario: 系统用户ID配置
- **WHEN** 系统初始化或配置更新时
- **THEN** 系统从环境变量 `JUNHONG_AUTO_TRIGGER_SYSTEM_USER_ID` 读取系统用户ID
#### Scenario: 自动触发功能开关
- **WHEN** 需要临时关闭自动触发功能时
- **THEN** 系统支持通过环境变量 `JUNHONG_ENABLE_AUTO_TRIGGER` 控制功能开关(默认开启)

View File

@@ -0,0 +1,151 @@
# Capability: 手动触发功能优化和Bug修复
> 注:`polling-manual-trigger` 能力的原始 spec 仅存于 archive2026-02-10-polling-system-implementation未提升到 `openspec/specs/`。本变更为该能力建立 live spec以下内容为针对已有实现的修正与补充。
## MODIFIED Requirements
### Requirement: 手动触发去重
系统 SHALL 对手动触发进行去重,避免重复执行,确保去重机制的时间一致性。
#### Scenario: 同卡重复触发
- **WHEN** 管理员对同一张卡连续触发两次实名检查
- **THEN** 系统检查手动队列如果卡ID已存在则不重复加入
#### Scenario: 使用 Redis Set 去重
- **WHEN** 批量触发 100 张卡,其中有 10 张重复
- **THEN** 系统使用 Redis Set 临时存储卡ID去重后再加入队列
#### Scenario: 手动触发与定时任务去重
- **WHEN** 卡A在手动队列中同时定时队列也到期
- **THEN** 系统优先执行手动触发,定时队列检测到卡已在执行则跳过
#### Scenario: 去重key TTL与日限制时间对齐
- **WHEN** 管理员在当天第一次触发某张卡的实名检查
- **THEN** 系统设置去重key的TTL为24小时与日限制周期保持一致避免同一天内重复触发
#### Scenario: 去重key跨天自动清理
- **WHEN** 午夜12点过后新一天开始
- **THEN** 系统允许重新触发前一天已触发过的卡去重key自动过期清理
### Requirement: 手动触发限流
系统 SHALL 对手动触发进行合理限流,既防止滥用又保证正常使用。
#### Scenario: 单次批量限制
- **WHEN** 管理员单次批量触发超过 1000 张卡
- **THEN** 系统拒绝请求,提示超过单次限制
#### Scenario: 频率限制
- **WHEN** 管理员在 1 分钟内触发超过 10 次
- **THEN** 系统拒绝请求,提示操作过于频繁,建议稍后再试
#### Scenario: 调整每日限制到合理水平
- **WHEN** 管理员当日累计触发超过 500 次从100次调整
- **THEN** 系统拒绝请求,提示已达每日限制,支持正常运维需求和客户端自动触发
#### Scenario: 超级管理员无限制
- **WHEN** 超级管理员手动触发
- **THEN** 系统不应用限流规则
#### Scenario: 客户端自动触发不受个人频次限制影响
- **WHEN** 客户端实名跳转自动触发功能调用手动触发服务
- **THEN** 系统使用系统用户身份,不受个人用户的频次限制影响
### Requirement: 手动触发权限控制
系统 SHALL 控制手动触发的权限,支持系统用户身份的特殊权限适配。
#### Scenario: 普通管理员触发自己管理的卡
- **WHEN** 代理账号请求手动触发卡ID为 12345该卡属于代理管理的店铺
- **THEN** 系统验证权限通过,允许触发
#### Scenario: 代理账号无权触发其他店铺的卡
- **WHEN** 代理账号请求手动触发卡ID为 99999该卡不属于代理管理的店铺
- **THEN** 系统拒绝请求,返回权限不足错误
#### Scenario: 超级管理员可触发任意卡
- **WHEN** 超级管理员请求手动触发任意卡
- **THEN** 系统跳过权限验证,允许触发
#### Scenario: 系统用户身份自动触发
- **WHEN** 客户端实名跳转功能使用系统用户身份调用手动触发
- **THEN** 系统验证系统用户身份有效允许触发任意ICCID的实名检查
#### Scenario: 企业账号禁止手动触发
- **WHEN** 企业账号请求手动触发卡ID为 12345该卡属于企业
- **THEN** 系统拒绝请求,返回无权限错误
## ADDED Requirements
### Requirement: 优化权限检查逻辑
系统 SHALL 简化和优化权限检查逻辑,减少代码重复,提高可维护性。
#### Scenario: 统一权限检查函数
- **WHEN** 系统需要验证用户对卡的管理权限
- **THEN** 系统使用统一的权限检查函数,避免在多个地方重复实现相同逻辑
#### Scenario: 权限检查缓存优化
- **WHEN** 批量操作需要检查大量卡的权限
- **THEN** 系统使用批量权限检查,减少数据库查询次数,提高性能
#### Scenario: 权限检查错误信息优化
- **WHEN** 权限检查失败时
- **THEN** 系统返回明确的错误信息,帮助用户了解权限不足的具体原因
### Requirement: 增强错误处理和日志
系统 SHALL 提供更完善的错误处理和日志记录,提高系统可观测性。
#### Scenario: 详细错误日志记录
- **WHEN** 手动触发过程中发生错误Redis连接失败、权限错误等
- **THEN** 系统记录包含错误原因、用户ID、卡ID、上下文信息的详细日志
#### Scenario: 性能关键路径日志
- **WHEN** 手动触发操作执行时间超过预期
- **THEN** 系统记录性能日志,包含各阶段耗时,便于性能优化
#### Scenario: 异常情况恢复机制
- **WHEN** Redis临时不可用导致手动触发失败
- **THEN** 系统记录错误并提供恢复建议,不影响其他正常功能
### Requirement: 配置管理和灵活性
系统 SHALL 支持通过配置管理手动触发功能的关键参数,提高系统灵活性。
#### Scenario: 频次限制可配置
- **WHEN** 需要调整手动触发的频次限制时
- **THEN** 系统支持通过配置文件或环境变量调整每日限制、单次限制等参数
#### Scenario: 去重TTL可配置
- **WHEN** 需要调整去重机制的时间窗口时
- **THEN** 系统支持配置去重key的TTL时长默认24小时
#### Scenario: 系统用户ID配置
- **WHEN** 系统启动或配置更新时
- **THEN** 系统从环境变量 `JUNHONG_MANUAL_TRIGGER_SYSTEM_USER_ID` 读取系统用户ID用于客户端自动触发

View File

@@ -0,0 +1,52 @@
## 1. 现状分析
- [x] 1.1 使用 lsp_diagnostics 检查 client_realname.go、manual_trigger_service.go 的语法和类型错误
- [x] 1.2 分析现有 ClientRealnameHandler 和 ManualTriggerService 的依赖关系,确认无循环依赖风险
## 2. 配置管理和常量定义
- [x] 2.1 在 `pkg/config/` 中新增 `AutoTriggerSystemUserID int``EnableAutoTrigger bool`(默认 true配置字段通过环境变量 `JUNHONG_AUTO_TRIGGER_SYSTEM_USER_ID``JUNHONG_ENABLE_AUTO_TRIGGER` 注入
- [x] 2.2 更新 `pkg/config/defaults/config.yaml`,添加 `auto_trigger_system_user_id``enable_auto_trigger` 默认值
- [x] 2.3 运行 lsp_diagnostics 检查配置相关代码无错误
## 3. 手动触发服务 Bug 修复和优化
- [x] 3.1 在 `internal/service/polling/manual_trigger_service.go``TriggerSingle``TriggerBatch``TriggerByCondition` 三处将 `todayCount >= 100` 改为 `todayCount >= 500`
- [x] 3.2 在 `internal/service/polling/manual_trigger_service.go``TriggerSingle` 中将 `s.redis.Expire(ctx, dedupeKey, time.Hour)` 改为 `s.redis.Expire(ctx, dedupeKey, 24*time.Hour)`
- [x] 3.3 同步更新 `pkg/constants/redis.go``RedisPollingManualDedupeKey` 函数的注释,将 `// 过期时间1小时` 改为 `// 过期时间24小时`
- [x] 3.4 优化 `canManageCard``canManageCards` 权限检查函数,提取公共逻辑减少重复代码
- [x] 3.5 改进 `TriggerSingle` 的错误日志,增加 `triggeredBy``cardID` 等上下文字段
- [x] 3.6 运行 lsp_diagnostics 检查 ManualTriggerService 相关代码无错误
- [x] 3.7 使用 PostgreSQL MCP 和 Redis CLI 手动验证:触发同一张卡后 24 小时内重复触发被拒绝24 小时后允许重复触发
## 4. ClientRealnameHandler 集成自动触发功能
- [x] 4.1 在 `ClientRealnameHandler` 结构体中新增 `manualTriggerSvc *polling.ManualTriggerService` 字段
- [x] 4.2 修改 `NewClientRealnameHandler` 构造函数,新增 `manualTriggerSvc *polling.ManualTriggerService` 参数(可为 nilnil 时跳过自动触发)
- [x] 4.3 在 `GetRealnameLink` 成功返回实名链接前,若 `h.manualTriggerSvc != nil` 且配置 `EnableAutoTrigger=true`,则启动 goroutine 异步触发实名检查
- [x] 4.4 goroutine 内部必须使用 `context.WithTimeout(context.Background(), 3*time.Second)` 创建独立 context禁止复用 `c.UserContext()`(请求 context 在 handler 返回后失效)
- [x] 4.5 goroutine 内部构造系统用户 context注入 UserTypePlatform 和配置的 AutoTriggerSystemUserID调用 `manualTriggerSvc.TriggerSingle(sysCtx, targetCard.ID, constants.TaskTypePollingRealname, systemUserID)`
- [x] 4.6 goroutine 执行失败时记录 WARN 日志(含 customerID、ICCID、error成功时记录 INFO 日志(含 customerID、ICCID失败不影响主流程
- [x] 4.7 运行 lsp_diagnostics 检查 ClientRealnameHandler 相关代码无错误
## 5. 依赖注入配置更新
- [x] 5.1 在 `internal/bootstrap/handlers.go` 中更新 `ClientRealname` handler 的初始化,传入 ManualTriggerService
- [x] 5.2 确认 ManualTriggerService 在 bootstrap/services.go 中已初始化(检查是否已存在,若不存在则补充)
- [x] 5.3 更新 `cmd/api/docs.go``cmd/gendocs/main.go``NewClientRealnameHandler` 的调用,补充新增的 manualTriggerSvc 参数(传 nil 即可)
- [x] 5.4 运行 lsp_diagnostics 检查 bootstrap 和 docs.go 相关代码无编译错误
## 6. 手动验证
- [x] 6.1 使用 curl 调用 `GET /api/c/v1/realname/link` 接口,通过日志确认异步触发 goroutine 被启动
- [x] 6.2 通过 PostgreSQL MCP 查询 `tb_polling_manual_trigger_log`,确认自动触发记录的 `triggered_by` 为配置的系统用户 ID
- [x] 6.3 通过 Redis CLI 检查 `polling:manual:realname` 队列,确认 cardID 已写入
- [x] 6.4 使用 PostgreSQL MCP 查询 `tb_polling_manual_trigger_log`,验证今日触发次数计数正确(不超过 500 次限制)
- [x] 6.5 使用 Redis CLI 确认 dedup key TTL 为 24 小时(`TTL polling:manual:dedupe:realname`
- [x] 6.6 模拟 manualTriggerSvc 不可用场景(临时将 EnableAutoTrigger 设为 false确认接口正常返回实名链接日志无 panic
## 7. 最终验收
- [x] 7.1 运行 lsp_diagnostics 对所有修改过的文件做最终检查,确认无错误和警告
- [x] 7.2 确认环境变量 `JUNHONG_AUTO_TRIGGER_SYSTEM_USER_ID``JUNHONG_ENABLE_AUTO_TRIGGER` 已在部署文档中记录
- [x] 7.3 确认 `GetRealnameLink` handler 函数长度不超过 100 行(满足项目规范)

View File

@@ -0,0 +1,64 @@
# Capability: 客户端实名跳转自动触发实名检查优先级提升功能
## ADDED Requirements
### Requirement: 客户端实名跳转自动触发实名检查
系统 SHALL 在用户成功获取实名跳转链接时自动触发该ICCID的实名状态检查提高检测优先级。
#### Scenario: 用户主动获取实名链接时自动触发优先级检查
- **WHEN** 个人客户调用 `GET /api/c/v1/realname/link` 接口成功获取实名链接
- **THEN** 系统自动将该ICCID加入实名检查的高优先级队列无需等待定时轮询
#### Scenario: 自动触发失败不影响主流程
- **WHEN** 个人客户调用实名链接接口,但自动触发实名检查失败
- **THEN** 系统仍正常返回实名链接,不因触发失败而阻断用户操作
#### Scenario: 异步处理不影响响应时间
- **WHEN** 个人客户调用实名链接接口
- **THEN** 系统异步执行实名检查触发主接口响应时间增加不超过5ms
### Requirement: 自动触发的权限适配
系统 SHALL 使用系统用户身份执行自动触发操作,绕过个人客户的权限限制。
#### Scenario: 个人客户无权限但能触发检查
- **WHEN** 个人客户获取实名链接(个人客户本身无手动触发权限)
- **THEN** 系统使用配置的系统用户身份自动触发成功将ICCID加入优先级队列
#### Scenario: 系统用户身份操作记录
- **WHEN** 系统使用系统用户身份执行自动触发
- **THEN** 系统在 `tb_polling_manual_trigger_log` 表中记录操作,`triggered_by` 字段记录系统用户ID
### Requirement: 自动触发的错误处理和日志
系统 SHALL 提供完善的错误处理和日志记录机制,确保可观测性。
#### Scenario: 触发失败时记录详细日志
- **WHEN** 自动触发实名检查失败如Redis连接失败、权限错误等
- **THEN** 系统记录包含客户ID、ICCID、错误原因的详细日志级别为WARN
#### Scenario: 触发成功时记录操作日志
- **WHEN** 自动触发实名检查成功
- **THEN** 系统记录包含客户ID、ICCID的INFO级别日志便于运维追踪
### Requirement: 配置管理和灵活性
系统 SHALL 支持通过配置管理自动触发功能的关键参数。
#### Scenario: 系统用户ID配置
- **WHEN** 系统初始化或配置更新时
- **THEN** 系统从环境变量 `JUNHONG_AUTO_TRIGGER_SYSTEM_USER_ID` 读取系统用户ID
#### Scenario: 自动触发功能开关
- **WHEN** 需要临时关闭自动触发功能时
- **THEN** 系统支持通过环境变量 `JUNHONG_ENABLE_AUTO_TRIGGER` 控制功能开关(默认开启)

View File

@@ -0,0 +1,147 @@
# Capability: 手动触发功能
## ADDED Requirements
### Requirement: 手动触发去重
系统 SHALL 对手动触发进行去重,避免重复执行,确保去重机制的时间一致性。
#### Scenario: 同卡重复触发
- **WHEN** 管理员对同一张卡连续触发两次实名检查
- **THEN** 系统检查手动队列如果卡ID已存在则不重复加入
#### Scenario: 使用 Redis Set 去重
- **WHEN** 批量触发 100 张卡,其中有 10 张重复
- **THEN** 系统使用 Redis Set 临时存储卡ID去重后再加入队列
#### Scenario: 手动触发与定时任务去重
- **WHEN** 卡A在手动队列中同时定时队列也到期
- **THEN** 系统优先执行手动触发,定时队列检测到卡已在执行则跳过
#### Scenario: 去重key TTL与日限制时间对齐
- **WHEN** 管理员在当天第一次触发某张卡的实名检查
- **THEN** 系统设置去重key的TTL为24小时与日限制周期保持一致避免同一天内重复触发
#### Scenario: 去重key跨天自动清理
- **WHEN** 午夜12点过后新一天开始
- **THEN** 系统允许重新触发前一天已触发过的卡去重key自动过期清理
### Requirement: 手动触发限流
系统 SHALL 对手动触发进行合理限流,既防止滥用又保证正常使用。
#### Scenario: 单次批量限制
- **WHEN** 管理员单次批量触发超过 1000 张卡
- **THEN** 系统拒绝请求,提示超过单次限制
#### Scenario: 频率限制
- **WHEN** 管理员在 1 分钟内触发超过 10 次
- **THEN** 系统拒绝请求,提示操作过于频繁,建议稍后再试
#### Scenario: 每日限制
- **WHEN** 管理员当日累计触发超过 500 次
- **THEN** 系统拒绝请求,提示已达每日限制,支持正常运维需求和客户端自动触发
#### Scenario: 超级管理员无限制
- **WHEN** 超级管理员手动触发
- **THEN** 系统不应用限流规则
#### Scenario: 客户端自动触发不受个人频次限制影响
- **WHEN** 客户端实名跳转自动触发功能调用手动触发服务
- **THEN** 系统使用系统用户身份,不受个人用户的频次限制影响
### Requirement: 手动触发权限控制
系统 SHALL 控制手动触发的权限,支持系统用户身份的特殊权限适配。
#### Scenario: 普通管理员触发自己管理的卡
- **WHEN** 代理账号请求手动触发卡ID为 12345该卡属于代理管理的店铺
- **THEN** 系统验证权限通过,允许触发
#### Scenario: 代理账号无权触发其他店铺的卡
- **WHEN** 代理账号请求手动触发卡ID为 99999该卡不属于代理管理的店铺
- **THEN** 系统拒绝请求,返回权限不足错误
#### Scenario: 超级管理员可触发任意卡
- **WHEN** 超级管理员请求手动触发任意卡
- **THEN** 系统跳过权限验证,允许触发
#### Scenario: 系统用户身份自动触发
- **WHEN** 客户端实名跳转功能使用系统用户身份调用手动触发
- **THEN** 系统验证系统用户身份有效允许触发任意ICCID的实名检查
#### Scenario: 企业账号禁止手动触发
- **WHEN** 企业账号请求手动触发卡ID为 12345该卡属于企业
- **THEN** 系统拒绝请求,返回无权限错误
### Requirement: 优化权限检查逻辑
系统 SHALL 简化和优化权限检查逻辑,减少代码重复,提高可维护性。
#### Scenario: 统一权限检查函数
- **WHEN** 系统需要验证用户对卡的管理权限
- **THEN** 系统使用统一的权限检查函数,避免在多个地方重复实现相同逻辑
#### Scenario: 权限检查缓存优化
- **WHEN** 批量操作需要检查大量卡的权限
- **THEN** 系统使用批量权限检查,减少数据库查询次数,提高性能
#### Scenario: 权限检查错误信息优化
- **WHEN** 权限检查失败时
- **THEN** 系统返回明确的错误信息,帮助用户了解权限不足的具体原因
### Requirement: 增强错误处理和日志
系统 SHALL 提供更完善的错误处理和日志记录,提高系统可观测性。
#### Scenario: 详细错误日志记录
- **WHEN** 手动触发过程中发生错误Redis连接失败、权限错误等
- **THEN** 系统记录包含错误原因、用户ID、卡ID、上下文信息的详细日志
#### Scenario: 性能关键路径日志
- **WHEN** 手动触发操作执行时间超过预期
- **THEN** 系统记录性能日志,包含各阶段耗时,便于性能优化
#### Scenario: 异常情况恢复机制
- **WHEN** Redis临时不可用导致手动触发失败
- **THEN** 系统记录错误并提供恢复建议,不影响其他正常功能
### Requirement: 配置管理和灵活性
系统 SHALL 支持通过配置管理手动触发功能的关键参数,提高系统灵活性。
#### Scenario: 频次限制可配置
- **WHEN** 需要调整手动触发的频次限制时
- **THEN** 系统支持通过配置文件或环境变量调整每日限制、单次限制等参数
#### Scenario: 去重TTL可配置
- **WHEN** 需要调整去重机制的时间窗口时
- **THEN** 系统支持配置去重key的TTL时长默认24小时
#### Scenario: 系统用户ID配置
- **WHEN** 系统启动或配置更新时
- **THEN** 系统从环境变量 `JUNHONG_MANUAL_TRIGGER_SYSTEM_USER_ID` 读取系统用户ID用于客户端自动触发