# 任务中心领取 Bug 修复计划 ## 问题描述 小程序(bindbox-mini)任务中心,活动前端领取不了但是可以看到。 ## 根因分析 ### 核心问题:前后端进度数据源不一致 **后端 `ClaimTier`**(service.go:738)使用 `TierProgressMap`(基于窗口化、受任务时间约束的进度)来校验是否达标: ```go if tp, ok := progress.TierProgressMap[tierID]; ok { currentOrderCount = tp.OrderCount // 窗口化进度,受任务 StartTime 约束 } ``` **前端 `isTierClaimable`**(index.vue:387-437)使用的是 `subProgress`(活动级别汇总)或全局进度(`orderCount` / `orderAmount`),**完全没有使用 `tier_progress_map`**。 ### 触发条件 最近提交 `e0db875` 修改了 `computeTimeWindow`: - **修改前**:`lifetime` / `since_registration` / 空窗口 → `return nil, nil`(不限时间) - **修改后**:`lifetime` / 默认窗口 → `return taskStart, taskEnd`(受任务时间约束) 这导致 `TierProgressMap` 中的进度值被任务时间限制,但 API 返回的 `order_count` / `sub_progress` 全局进度仍然不受时间限制(service.go:618-622 用 `nil, nil` 查询)。 ### 不一致的结果 | 数据源 | 时间约束 | 进度值 | 使用方 | |--------|---------|--------|--------| | `TierProgressMap` | 受任务 StartTime/EndTime 约束 | 较小 | 后端 ClaimTier | | `SubProgress` / 全局进度 | 无时间约束 | 较大 | 前端 isTierClaimable | | API 返回的 `order_count` | 无时间约束(或活动级别) | 较大 | 前端 isTierClaimable | **场景举例**: - 用户在任务创建前有 5 笔历史订单,任务创建后有 2 笔新订单 - 任务档位要求 `order_count >= 3` - 前端看到全局 `orderCount = 7`(不限时间) → 显示"领取"按钮 - 后端 `TierProgressMap.OrderCount = 2`(只统计任务开始后) → 返回"任务条件未达成" **或者反过来**: - 前端也使用 `subProgress` 做判断,但 `subProgress` 的统计可能不包含某些场景的数据 - 导致前端 `isTierClaimable` 返回 `false`,按钮不出现 - 用户看到任务但无法领取 ## 任务类型 - [x] 后端 (→ 后端逻辑修复) - [x] 前端 (→ 前端判断修复) ## 技术方案 **方案 A(推荐):后端 API 返回 `tier_progress_map`,前端使用** 让前后端使用同一份进度数据源(`TierProgressMap`),确保判断一致。 ### 实施步骤 #### Step 1:后端 - API Response 增加 `tier_progress_map` 字段 **文件**: `internal/api/task_center/tasks_app.go` 1. 在 `taskProgressResponse` 结构体中添加 `TierProgressMap` 字段: ```go type tierProgressItem struct { TierID int64 `json:"tier_id"` OrderCount int64 `json:"order_count"` OrderAmount int64 `json:"order_amount"` InviteCount int64 `json:"invite_count"` FirstOrder bool `json:"first_order"` } type taskProgressResponse struct { // ... existing fields ... TierProgress []tierProgressItem `json:"tier_progress"` // 新增 } ``` 2. 在 `GetTaskProgressForApp` handler 中填充该字段。 #### Step 2:前端 - `isTierClaimable` 优先使用 `tier_progress` **文件**: `bindbox-mini/pages-user/tasks/index.vue` 1. 在 `fetchData` 中解析并存储 `tier_progress` 到 `taskProgress[taskId]` 2. 修改 `isTierClaimable` 函数,优先从 `tierProgress` 中查找对应 tier 的进度 3. 修改 `getTierProgressText` 和 `getTierProgressPercent`,同步使用新数据源 ```js function isTierClaimable(task, tier) { const progress = taskProgress[task.id] || {} // 优先使用 tier 级别窗口化进度(与后端 ClaimTier 保持一致) if (progress.tierProgress) { const tp = progress.tierProgress.find(t => t.tier_id === tier.id) if (tp) { const metric = tier.metric || '' const threshold = tier.threshold || 0 const operator = tier.operator || '>=' let current = 0 if (metric === 'first_order') return tp.first_order || false else if (metric === 'order_count') current = tp.order_count || 0 else if (metric === 'order_amount') current = tp.order_amount || 0 else if (metric === 'invite_count') current = tp.invite_count || 0 if (operator === '>=') return current >= threshold if (operator === '==') return current === threshold if (operator === '>') return current > threshold return current >= threshold } } // fallback: 原有逻辑 // ... } ``` #### Step 3:同步修改进度显示 修改 `getTierProgressText` 和 `getTierProgressPercent` 也优先使用 `tierProgress` 数据,确保用户看到的进度和可领取状态一致。 ### 关键文件 | 文件 | 操作 | 说明 | |------|------|------| | `internal/api/task_center/tasks_app.go:106-170` | 修改 | 添加 tier_progress 到响应体 | | `bindbox-mini/pages-user/tasks/index.vue:387-437` | 修改 | isTierClaimable 使用 tier_progress | | `bindbox-mini/pages-user/tasks/index.vue:440-478` | 修改 | getTierProgressText 使用 tier_progress | | `bindbox-mini/pages-user/tasks/index.vue:590-630` | 修改 | getTierProgressPercent 使用 tier_progress | | `bindbox-mini/pages-user/tasks/index.vue:551-576` | 修改 | fetchData 解析 tier_progress | ### 风险与缓解 | 风险 | 缓解措施 | |------|----------| | 前端旧版本未使用 `tier_progress` 字段 | 保持原有 `order_count`、`sub_progress` 字段不变,`tier_progress` 为新增字段,向后兼容 | | `tier_progress_map` 为空(数据库无 tiers 配置) | 前端 fallback 到原有 `subProgress` / 全局进度逻辑 | | 已部署但未刷新前端的用户 | `tier_progress` 是附加字段,不影响旧逻辑 | ### SESSION_ID(供 /ccg:execute 使用) - CODEX_SESSION: N/A(未使用外部模型) - GEMINI_SESSION: N/A(未使用外部模型)