diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json index b685c21..4388c06 100644 --- a/.sisyphus/boulder.json +++ b/.sisyphus/boulder.json @@ -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" } \ No newline at end of file diff --git a/.sisyphus/notepads/realname-trigger-priority-enhancement/decisions.md b/.sisyphus/notepads/realname-trigger-priority-enhancement/decisions.md new file mode 100644 index 0000000..6741ba6 --- /dev/null +++ b/.sisyphus/notepads/realname-trigger-priority-enhancement/decisions.md @@ -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) diff --git a/.sisyphus/notepads/realname-trigger-priority-enhancement/issues.md b/.sisyphus/notepads/realname-trigger-priority-enhancement/issues.md new file mode 100644 index 0000000..57423bc --- /dev/null +++ b/.sisyphus/notepads/realname-trigger-priority-enhancement/issues.md @@ -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() 中校验 diff --git a/.sisyphus/notepads/realname-trigger-priority-enhancement/learnings.md b/.sisyphus/notepads/realname-trigger-priority-enhancement/learnings.md new file mode 100644 index 0000000..95cef85 --- /dev/null +++ b/.sisyphus/notepads/realname-trigger-priority-enhancement/learnings.md @@ -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 diff --git a/.sisyphus/plans/realname-trigger-priority-enhancement.md b/.sisyphus/plans/realname-trigger-priority-enhancement.md new file mode 100644 index 0000000..a1dac63 --- /dev/null +++ b/.sisyphus/plans/realname-trigger-priority-enhancement.md @@ -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 TTL(1小时)与日限制周期(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:72(TriggerSingle)和 :192(processBatchTrigger) | +| `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 → 500(3处) + +**文件**:`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 → 24h(2处) + +**文件**:`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 error,0 新增 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 // 手动触发服务(可为nil,nil时跳过自动触发) +``` + +**验收**:结构体字段添加正确,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 而非 UserTypeSuperAdmin,SuperAdmin 不受日限制约束 + 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 " \ + "http://localhost:3000/api/c/v1/realname/link?identifier=" +``` + +**查看 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 +``` + +**期望**:≈ 86400(24小时) + +**验收**: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 error,0 新增 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.1–4.2 完成 | `4.1 在 ClientRealnameHandler 结构体中新增...` + `4.2 修改 NewClientRealnameHandler...` | +| Task 4.3 完成(提取 helper) | — (本计划新增步骤,提案无对应条目) | +| Task 4.4–4.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 提前勾选。 diff --git a/docs/polling-system/manual-trigger-analysis.md b/docs/polling-system/manual-trigger-analysis.md new file mode 100644 index 0000000..4d475e2 --- /dev/null +++ b/docs/polling-system/manual-trigger-analysis.md @@ -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 限制的实现问题 + +#### 问题1:CountTodayTriggers 的性能 + +```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. 改进文档(添加使用指南和最佳实践) + +**不建议完全移除**,因为: +- 故障恢复和数据修复场景有实际价值 +- 代码已经实现,移除成本高 +- 可以通过改进来提高价值 + diff --git a/docs/polling-system/manual-trigger-summary.md b/docs/polling-system/manual-trigger-summary.md new file mode 100644 index 0000000..4497ddf --- /dev/null +++ b/docs/polling-system/manual-trigger-summary.md @@ -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 个函数 diff --git a/docs/polling-system/manual-trigger-vs-alternatives.md b/docs/polling-system/manual-trigger-vs-alternatives.md new file mode 100644 index 0000000..1f5e626 --- /dev/null +++ b/docs/polling-system/manual-trigger-vs-alternatives.md @@ -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. **长期**:构建事件驱动系统,完全重构 + diff --git a/opencode.json b/opencode.json index 83fe564..28e892e 100644 --- a/opencode.json +++ b/opencode.json @@ -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": { diff --git a/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/.openspec.yaml b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/.openspec.yaml new file mode 100644 index 0000000..e14f322 --- /dev/null +++ b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-13 diff --git a/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/design.md b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/design.md new file mode 100644 index 0000000..5d448d1 --- /dev/null +++ b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/design.md @@ -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指标监控异步调用成功率 + +### 风险2:Redis内存使用增加 +**影响**:去重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. **是否需要熔断机制**:当手动触发服务频繁失败时是否需要暂停自动触发? \ No newline at end of file diff --git a/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/proposal.md b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/proposal.md new file mode 100644 index 0000000..17765d4 --- /dev/null +++ b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/proposal.md @@ -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`: 修复去重 TTL(1h→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%+ +- 手动触发功能更稳定,运维排查问题更便利 \ No newline at end of file diff --git a/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/specs/client-realname-auto-trigger/spec.md b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/specs/client-realname-auto-trigger/spec.md new file mode 100644 index 0000000..5235491 --- /dev/null +++ b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/specs/client-realname-auto-trigger/spec.md @@ -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` 控制功能开关(默认开启) \ No newline at end of file diff --git a/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/specs/polling-manual-trigger/spec.md b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/specs/polling-manual-trigger/spec.md new file mode 100644 index 0000000..a26a444 --- /dev/null +++ b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/specs/polling-manual-trigger/spec.md @@ -0,0 +1,151 @@ +# Capability: 手动触发功能优化和Bug修复 + +> 注:`polling-manual-trigger` 能力的原始 spec 仅存于 archive(2026-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用于客户端自动触发 \ No newline at end of file diff --git a/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/tasks.md b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/tasks.md new file mode 100644 index 0000000..f7d921b --- /dev/null +++ b/openspec/changes/archive/2026-04-13-realname-trigger-priority-enhancement/tasks.md @@ -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` 参数(可为 nil,nil 时跳过自动触发) +- [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 行(满足项目规范) diff --git a/openspec/specs/client-realname-auto-trigger/spec.md b/openspec/specs/client-realname-auto-trigger/spec.md new file mode 100644 index 0000000..62ed119 --- /dev/null +++ b/openspec/specs/client-realname-auto-trigger/spec.md @@ -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` 控制功能开关(默认开启) diff --git a/openspec/specs/polling-manual-trigger/spec.md b/openspec/specs/polling-manual-trigger/spec.md new file mode 100644 index 0000000..97b7d4e --- /dev/null +++ b/openspec/specs/polling-manual-trigger/spec.md @@ -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用于客户端自动触发