From 8d2e8faa843e5fef98da2eb2d931405ea2414e58 Mon Sep 17 00:00:00 2001 From: huang Date: Thu, 23 Apr 2026 11:30:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=80=E6=AC=BEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/systematic-debugging/SKILL.md | 260 ----------------- .../skills/systematic-debugging/SKILL.md | 265 ------------------ AGENTS.md | 1 - CLAUDE.md | 1 - .../service/package/activation_service.go | 112 ++++++++ internal/service/refund/service.go | 17 +- opencode.json | 6 +- 7 files changed, 128 insertions(+), 534 deletions(-) delete mode 100644 .claude/skills/systematic-debugging/SKILL.md delete mode 100644 .opencode/skills/systematic-debugging/SKILL.md diff --git a/.claude/skills/systematic-debugging/SKILL.md b/.claude/skills/systematic-debugging/SKILL.md deleted file mode 100644 index a052e9b..0000000 --- a/.claude/skills/systematic-debugging/SKILL.md +++ /dev/null @@ -1,260 +0,0 @@ ---- -name: systematic-debugging -description: 遇到任何 bug、异常行为、报错时必须使用。在提出任何修复方案之前,强制执行根因分析流程。适用于 API 报错、数据异常、业务逻辑错误、性能问题等所有技术问题。 ---- - -# 系统化调试方法论 - -## 铁律 - -``` -没有找到根因,禁止提出任何修复方案。 -``` - -改之前先搞懂为什么坏了。猜测不是调试,验证假设才是。 - ---- - -## 什么时候用 - -**所有技术问题都用这个流程**: -- API 接口报错(4xx / 5xx) -- 业务数据异常(金额不对、状态流转错误) -- 性能问题(接口慢、数据库慢查询) -- 异步任务失败(Asynq 任务报错/卡住) -- 构建失败、启动失败 - -**尤其是以下场景**: -- 时间紧迫(越急越不能瞎猜) -- "很简单的问题"(简单问题也有根因) -- 已经试了一次修复但没解决 -- 不完全理解为什么出问题 - ---- - -## 四阶段流程 - -必须按顺序完成每个阶段,不可跳过。 - -### 阶段一:根因调查 - -**这是最重要的阶段,占整个调试时间的 60%。没完成本阶段,禁止进入阶段二。** - -#### 1. 仔细阅读错误信息 - -- 完整阅读 stack trace,不要跳过 -- 注意行号、文件路径、错误码 -- 很多时候答案就在错误信息里 -- 检查 `logs/app.log` 和 `logs/access.log` 中的上下文 - -#### 2. 稳定复现 - -- 能稳定触发吗?精确的请求参数是什么? -- 用 curl 或 Postman 复现,记录完整的请求和响应 -- 不能复现 → 收集更多数据(检查日志、Redis 状态、数据库记录),**不要瞎猜** - -#### 3. 检查最近改动 - -- `git diff` / `git log --oneline -10` 看最近改了什么 -- 新加了什么依赖?改了什么配置?改了什么 SQL? -- 对比改动前后的行为差异 - -#### 4. 逐层诊断(针对本项目架构) - -本项目有明确的分层架构,问题一定出在某一层的边界: - -``` -请求 → Fiber Middleware → Handler → Service → Store → PostgreSQL/Redis - ↑ ↑ ↑ ↑ ↑ - 认证/限流 参数解析 业务逻辑 SQL/缓存 数据本身 -``` - -**在每个层边界确认数据是否正确**: - -```go -// Handler 层 — 请求进来的参数对不对? -logger.Info("Handler 收到请求", - zap.Any("params", req), - zap.String("request_id", requestID), -) - -// Service 层 — 传给业务逻辑的数据对不对? -logger.Info("Service 开始处理", - zap.Uint("user_id", userID), - zap.Any("input", input), -) - -// Store 层 — SQL 查询/写入的数据对不对? -// 开启 GORM Debug 模式查看实际 SQL -db.Debug().Where(...).Find(&result) - -// Redis 层 — 缓存的数据对不对? -// 用 redis-cli 直接检查 key 的值 -// GET auth:token:{token} -// GET sim:status:{iccid} -``` - -**跑一次 → 看日志 → 找到断裂的那一层 → 再深入该层排查。** - -#### 5. 追踪数据流 - -如果错误深藏在调用链中: -- 坏数据从哪来的? -- 谁调用了这个函数,传了什么参数? -- 一直往上追,直到找到数据变坏的源头 -- **修源头,不修症状** - ---- - -### 阶段二:模式分析 - -**找到参照物,对比差异。** - -#### 1. 找能用的参照 - -项目里有没有类似的、能正常工作的代码? - -| 如果问题在... | 参照物在... | -|-------------|-----------| -| Handler 参数解析 | 其他 Handler 的相同模式 | -| Service 业务逻辑 | 同模块其他方法的实现 | -| Store SQL 查询 | 同 Store 文件中类似的查询 | -| Redis 操作 | `pkg/constants/redis.go` 中的 Key 定义 | -| 异步任务 | `internal/task/` 中其他任务处理器 | -| GORM Callback | `pkg/database/` 中的 callback 实现 | - -#### 2. 逐行对比 - -完整阅读参考代码,不要跳读。列出每一处差异。 - -#### 3. 不要假设"这个不重要" - -小差异经常是 bug 的根因: -- 字段标签 `gorm:"column:xxx"` 拼写不对 -- `errors.New()` 用了错误的错误码 -- Redis Key 函数参数传反了 -- Context 里的 UserID 没取到(中间件没配) - ---- - -### 阶段三:假设和验证 - -**科学方法:一次只验证一个假设。** - -#### 1. 形成单一假设 - -明确写下: - -> "我认为根因是 X,因为 Y。验证方法是 Z。" - -#### 2. 最小化验证 - -- 只改一个地方 -- 一次只验证一个变量 -- 不要同时修多处 - -#### 3. 验证结果 - -- 假设成立 → 进入阶段四 -- 假设不成立 → 回到阶段一,用新信息重新分析 -- **绝对不能在失败的修复上再叠加修复** - -#### 4. 三次失败 → 停下来 - -如果连续 3 次假设都不成立: - -**这不是 bug,是架构问题。** - -- 停止一切修复尝试 -- 整理已知信息 -- 向用户说明情况,讨论是否需要重构 -- 不要再试第 4 次 - ---- - -### 阶段四:实施修复 - -**确认根因后,一次性修好。** - -#### 1. 修根因,不修症状 - -``` -❌ 症状修复:在 Handler 里加个 if 把坏数据过滤掉 -✅ 根因修复:修 Service 层生成坏数据的逻辑 -``` - -#### 2. 一次只改一个地方 - -- 不搞"顺手优化" -- 不在修 bug 的同时重构代码 -- 修完 bug 就停 - -#### 3. 验证修复 - -- `go build ./...` 编译通过 -- `lsp_diagnostics` 无新增错误 -- 用原来复现 bug 的请求再跑一次,确认修好了 -- 用 PostgreSQL MCP 工具检查数据库中的数据状态 - -#### 4. 清理诊断代码 - -- 删除阶段一加的临时诊断日志(除非它们本身就该保留) -- 确保没有 `db.Debug()` 残留在代码里 - ---- - -## 本项目常见调试场景速查 - -| 场景 | 首先检查 | -|------|---------| -| API 返回 401 | `logs/access.log` 中该请求的 token → Redis 中 `auth:token:{token}` 是否存在 | -| API 返回 403 | 用户类型是什么 → GORM Callback 自动过滤的条件对不对 → `middleware.CanManageShop()` 的参数 | -| 数据查不到 | GORM 数据权限过滤有没有生效 → `shop_id` / `enterprise_id` 是否正确 → 是否需要 `SkipDataPermission` | -| 金额/余额不对 | 乐观锁 version 字段 → `RowsAffected` 是否为 0 → 并发场景下的锁竞争 | -| 状态流转错误 | `WHERE status = expected` 条件更新 → 状态机是否有遗漏的路径 | -| 异步任务不执行 | Asynq Dashboard → `RedisTaskLockKey` 有没有残留 → Worker 日志 | -| 异步任务重复执行 | `RedisTaskLockKey` 的 TTL → 任务幂等性检查 | -| 分佣计算错误 | 佣金类型(差价/一次性) → 套餐级别的佣金率 → 设备级防重复分佣 | -| 套餐激活异常 | 卡状态 → 实名状态 → 主套餐排队逻辑 → 加油包绑定关系 | -| Redis 缓存不一致 | Key 的 TTL → 缓存更新时机 → 是否有手动 `Del` 清除 | -| 微信支付回调失败 | 签名验证 → 幂等性处理 → 回调 URL 是否可达 | -| GORM 查询慢 | `db.Debug()` 看实际 SQL → 是否 N+1 → 是否缺少索引 | - ---- - -## 红线规则 - -如果你发现自己在想以下任何一条,**立刻停下来,回到阶段一**: - -| 想法 | 为什么是错的 | -|------|------------| -| "先快速修一下,回头再查" | 快速修 = 猜测。猜测 = 浪费时间。 | -| "试试改这个看看行不行" | 一次只验证一个假设,不是随机改。 | -| "大概是 X 的问题,我直接改了" | "大概"不是根因。先验证再改。 | -| "这个很简单,不用走流程" | 简单问题走流程只需要 5 分钟。不走流程可能浪费 2 小时。 | -| "我不完全理解但这应该行" | 不理解 = 没找到根因。回阶段一。 | -| "再试一次"(已经失败 2 次) | 3 次失败 = 架构问题。停下来讨论。 | -| "同时改这几个地方应该能修好" | 改多处 = 无法确认哪个是根因。一次只改一处。 | - ---- - -## 常见借口和真相 - -| 借口 | 真相 | -|------|------| -| "问题很简单,不需要走流程" | 简单问题也有根因。走流程对简单问题只花 5 分钟。 | -| "太紧急了,没时间分析" | 系统化调试比乱猜快 3-5 倍。越急越要走流程。 | -| "先改了验证一下" | 这叫猜测,不叫验证。先确认根因再改。 | -| "我看到问题了,直接修" | 看到症状 ≠ 理解根因。症状修复是技术债。 | -| "改了好几个地方,反正能用了" | 不知道哪个改动修的,下次还会出问题。 | - ---- - -## 快速参考 - -| 阶段 | 核心动作 | 完成标准 | -|------|---------|---------| -| **一、根因调查** | 读错误日志、复现、检查改动、逐层诊断、追踪数据流 | 能说清楚"因为 X 所以 Y" | -| **二、模式分析** | 找参照代码、逐行对比、列出差异 | 知道正确的应该长什么样 | -| **三、假设验证** | 写下假设、最小改动、单变量验证 | 假设被证实或推翻 | -| **四、实施修复** | 修根因、编译检查、请求验证、清理诊断代码 | bug 消失,无新增问题 | diff --git a/.opencode/skills/systematic-debugging/SKILL.md b/.opencode/skills/systematic-debugging/SKILL.md deleted file mode 100644 index 44ba4e7..0000000 --- a/.opencode/skills/systematic-debugging/SKILL.md +++ /dev/null @@ -1,265 +0,0 @@ ---- -name: systematic-debugging -description: 遇到任何 bug、异常行为、报错时必须使用。在提出任何修复方案之前,强制执行根因分析流程。适用于 API 报错、数据异常、业务逻辑错误、性能问题等所有技术问题。 -license: MIT -metadata: - author: junhong - version: "1.0" - source: "adapted from obra/superpowers systematic-debugging" ---- - -# 系统化调试方法论 - -## 铁律 - -``` -没有找到根因,禁止提出任何修复方案。 -``` - -改之前先搞懂为什么坏了。猜测不是调试,验证假设才是。 - ---- - -## 什么时候用 - -**所有技术问题都用这个流程**: -- API 接口报错(4xx / 5xx) -- 业务数据异常(金额不对、状态流转错误) -- 性能问题(接口慢、数据库慢查询) -- 异步任务失败(Asynq 任务报错/卡住) -- 构建失败、启动失败 - -**尤其是以下场景**: -- 时间紧迫(越急越不能瞎猜) -- "很简单的问题"(简单问题也有根因) -- 已经试了一次修复但没解决 -- 不完全理解为什么出问题 - ---- - -## 四阶段流程 - -必须按顺序完成每个阶段,不可跳过。 - -### 阶段一:根因调查 - -**这是最重要的阶段,占整个调试时间的 60%。没完成本阶段,禁止进入阶段二。** - -#### 1. 仔细阅读错误信息 - -- 完整阅读 stack trace,不要跳过 -- 注意行号、文件路径、错误码 -- 很多时候答案就在错误信息里 -- 检查 `logs/app.log` 和 `logs/access.log` 中的上下文 - -#### 2. 稳定复现 - -- 能稳定触发吗?精确的请求参数是什么? -- 用 curl 或 Postman 复现,记录完整的请求和响应 -- 不能复现 → 收集更多数据(检查日志、Redis 状态、数据库记录),**不要瞎猜** - -#### 3. 检查最近改动 - -- `git diff` / `git log --oneline -10` 看最近改了什么 -- 新加了什么依赖?改了什么配置?改了什么 SQL? -- 对比改动前后的行为差异 - -#### 4. 逐层诊断(针对本项目架构) - -本项目有明确的分层架构,问题一定出在某一层的边界: - -``` -请求 → Fiber Middleware → Handler → Service → Store → PostgreSQL/Redis - ↑ ↑ ↑ ↑ ↑ - 认证/限流 参数解析 业务逻辑 SQL/缓存 数据本身 -``` - -**在每个层边界确认数据是否正确**: - -```go -// Handler 层 — 请求进来的参数对不对? -logger.Info("Handler 收到请求", - zap.Any("params", req), - zap.String("request_id", requestID), -) - -// Service 层 — 传给业务逻辑的数据对不对? -logger.Info("Service 开始处理", - zap.Uint("user_id", userID), - zap.Any("input", input), -) - -// Store 层 — SQL 查询/写入的数据对不对? -// 开启 GORM Debug 模式查看实际 SQL -db.Debug().Where(...).Find(&result) - -// Redis 层 — 缓存的数据对不对? -// 用 redis-cli 直接检查 key 的值 -// GET auth:token:{token} -// GET sim:status:{iccid} -``` - -**跑一次 → 看日志 → 找到断裂的那一层 → 再深入该层排查。** - -#### 5. 追踪数据流 - -如果错误深藏在调用链中: -- 坏数据从哪来的? -- 谁调用了这个函数,传了什么参数? -- 一直往上追,直到找到数据变坏的源头 -- **修源头,不修症状** - ---- - -### 阶段二:模式分析 - -**找到参照物,对比差异。** - -#### 1. 找能用的参照 - -项目里有没有类似的、能正常工作的代码? - -| 如果问题在... | 参照物在... | -|-------------|-----------| -| Handler 参数解析 | 其他 Handler 的相同模式 | -| Service 业务逻辑 | 同模块其他方法的实现 | -| Store SQL 查询 | 同 Store 文件中类似的查询 | -| Redis 操作 | `pkg/constants/redis.go` 中的 Key 定义 | -| 异步任务 | `internal/task/` 中其他任务处理器 | -| GORM Callback | `pkg/database/` 中的 callback 实现 | - -#### 2. 逐行对比 - -完整阅读参考代码,不要跳读。列出每一处差异。 - -#### 3. 不要假设"这个不重要" - -小差异经常是 bug 的根因: -- 字段标签 `gorm:"column:xxx"` 拼写不对 -- `errors.New()` 用了错误的错误码 -- Redis Key 函数参数传反了 -- Context 里的 UserID 没取到(中间件没配) - ---- - -### 阶段三:假设和验证 - -**科学方法:一次只验证一个假设。** - -#### 1. 形成单一假设 - -明确写下: - -> "我认为根因是 X,因为 Y。验证方法是 Z。" - -#### 2. 最小化验证 - -- 只改一个地方 -- 一次只验证一个变量 -- 不要同时修多处 - -#### 3. 验证结果 - -- 假设成立 → 进入阶段四 -- 假设不成立 → 回到阶段一,用新信息重新分析 -- **绝对不能在失败的修复上再叠加修复** - -#### 4. 三次失败 → 停下来 - -如果连续 3 次假设都不成立: - -**这不是 bug,是架构问题。** - -- 停止一切修复尝试 -- 整理已知信息 -- 向用户说明情况,讨论是否需要重构 -- 不要再试第 4 次 - ---- - -### 阶段四:实施修复 - -**确认根因后,一次性修好。** - -#### 1. 修根因,不修症状 - -``` -❌ 症状修复:在 Handler 里加个 if 把坏数据过滤掉 -✅ 根因修复:修 Service 层生成坏数据的逻辑 -``` - -#### 2. 一次只改一个地方 - -- 不搞"顺手优化" -- 不在修 bug 的同时重构代码 -- 修完 bug 就停 - -#### 3. 验证修复 - -- `go build ./...` 编译通过 -- `lsp_diagnostics` 无新增错误 -- 用原来复现 bug 的请求再跑一次,确认修好了 -- 用 PostgreSQL MCP 工具检查数据库中的数据状态 - -#### 4. 清理诊断代码 - -- 删除阶段一加的临时诊断日志(除非它们本身就该保留) -- 确保没有 `db.Debug()` 残留在代码里 - ---- - -## 本项目常见调试场景速查 - -| 场景 | 首先检查 | -|------|---------| -| API 返回 401 | `logs/access.log` 中该请求的 token → Redis 中 `auth:token:{token}` 是否存在 | -| API 返回 403 | 用户类型是什么 → GORM Callback 自动过滤的条件对不对 → `middleware.CanManageShop()` 的参数 | -| 数据查不到 | GORM 数据权限过滤有没有生效 → `shop_id` / `enterprise_id` 是否正确 → 是否需要 `SkipDataPermission` | -| 金额/余额不对 | 乐观锁 version 字段 → `RowsAffected` 是否为 0 → 并发场景下的锁竞争 | -| 状态流转错误 | `WHERE status = expected` 条件更新 → 状态机是否有遗漏的路径 | -| 异步任务不执行 | Asynq Dashboard → `RedisTaskLockKey` 有没有残留 → Worker 日志 | -| 异步任务重复执行 | `RedisTaskLockKey` 的 TTL → 任务幂等性检查 | -| 分佣计算错误 | 佣金类型(差价/一次性) → 套餐级别的佣金率 → 设备级防重复分佣 | -| 套餐激活异常 | 卡状态 → 实名状态 → 主套餐排队逻辑 → 加油包绑定关系 | -| Redis 缓存不一致 | Key 的 TTL → 缓存更新时机 → 是否有手动 `Del` 清除 | -| 微信支付回调失败 | 签名验证 → 幂等性处理 → 回调 URL 是否可达 | -| GORM 查询慢 | `db.Debug()` 看实际 SQL → 是否 N+1 → 是否缺少索引 | - ---- - -## 红线规则 - -如果你发现自己在想以下任何一条,**立刻停下来,回到阶段一**: - -| 想法 | 为什么是错的 | -|------|------------| -| "先快速修一下,回头再查" | 快速修 = 猜测。猜测 = 浪费时间。 | -| "试试改这个看看行不行" | 一次只验证一个假设,不是随机改。 | -| "大概是 X 的问题,我直接改了" | "大概"不是根因。先验证再改。 | -| "这个很简单,不用走流程" | 简单问题走流程只需要 5 分钟。不走流程可能浪费 2 小时。 | -| "我不完全理解但这应该行" | 不理解 = 没找到根因。回阶段一。 | -| "再试一次"(已经失败 2 次) | 3 次失败 = 架构问题。停下来讨论。 | -| "同时改这几个地方应该能修好" | 改多处 = 无法确认哪个是根因。一次只改一处。 | - ---- - -## 常见借口和真相 - -| 借口 | 真相 | -|------|------| -| "问题很简单,不需要走流程" | 简单问题也有根因。走流程对简单问题只花 5 分钟。 | -| "太紧急了,没时间分析" | 系统化调试比乱猜快 3-5 倍。越急越要走流程。 | -| "先改了验证一下" | 这叫猜测,不叫验证。先确认根因再改。 | -| "我看到问题了,直接修" | 看到症状 ≠ 理解根因。症状修复是技术债。 | -| "改了好几个地方,反正能用了" | 不知道哪个改动修的,下次还会出问题。 | - ---- - -## 快速参考 - -| 阶段 | 核心动作 | 完成标准 | -|------|---------|---------| -| **一、根因调查** | 读错误日志、复现、检查改动、逐层诊断、追踪数据流 | 能说清楚"因为 X 所以 Y" | -| **二、模式分析** | 找参照代码、逐行对比、列出差异 | 知道正确的应该长什么样 | -| **三、假设验证** | 写下假设、最小改动、单变量验证 | 假设被证实或推翻 | -| **四、实施修复** | 修根因、编译检查、请求验证、清理诊断代码 | bug 消失,无新增问题 | diff --git a/AGENTS.md b/AGENTS.md index 4655442..5d73b02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,6 @@ | 测试接口/验证数据 | `db-validation` | PostgreSQL MCP 使用方法和验证示例 | | 数据库迁移 | `db-migration` | 迁移命令、文件规范、执行流程、失败处理 | | 维护规范文档 | `doc-management` | 规范文档流程和维护规则 | -| 调试 bug / 排查异常 | `systematic-debugging` | 四阶段根因分析流程、逐层诊断、场景速查表 | | 编写 Go 代码注释/文档注释 | `comment-standards` | 包/结构体/接口/函数/内联注释完整规范与示例 | | 创建涉及接口的 OpenSpec 提案 | `openspec-api-contract` | 探索阶段引导清单、提案必填章节与完成标准 | diff --git a/CLAUDE.md b/CLAUDE.md index 4655442..5d73b02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,6 @@ | 测试接口/验证数据 | `db-validation` | PostgreSQL MCP 使用方法和验证示例 | | 数据库迁移 | `db-migration` | 迁移命令、文件规范、执行流程、失败处理 | | 维护规范文档 | `doc-management` | 规范文档流程和维护规则 | -| 调试 bug / 排查异常 | `systematic-debugging` | 四阶段根因分析流程、逐层诊断、场景速查表 | | 编写 Go 代码注释/文档注释 | `comment-standards` | 包/结构体/接口/函数/内联注释完整规范与示例 | | 创建涉及接口的 OpenSpec 提案 | `openspec-api-contract` | 探索阶段引导清单、提案必填章节与完成标准 | diff --git a/internal/service/package/activation_service.go b/internal/service/package/activation_service.go index 2050de6..f1e60b5 100644 --- a/internal/service/package/activation_service.go +++ b/internal/service/package/activation_service.go @@ -491,6 +491,118 @@ func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx * } } +// InvalidatePackagesForRefund 退款精准失效套餐 +// 规则: +// 1. 优先按 packageUsageID 精准失效单条套餐(且必须属于当前退款订单) +// 2. 未传 packageUsageID 时,按 orderID 失效该订单生成的套餐 +// 3. 若命中主套餐,级联失效其加油包,避免残留挂靠关系 +func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, assetType string, assetID uint, orderID uint, packageUsageID *uint) error { + if orderID == 0 { + return errors.New(errors.CodeInvalidParam, "无效的订单ID") + } + + validStatuses := []int{ + constants.PackageUsageStatusPending, + constants.PackageUsageStatusActive, + constants.PackageUsageStatusDepleted, + } + + baseQuery := s.db.WithContext(ctx).Model(&model.PackageUsage{}) + switch assetType { + case "iot_card": + baseQuery = baseQuery.Where("iot_card_id = ?", assetID) + case "device": + baseQuery = baseQuery.Where("device_id = ?", assetID) + default: + return errors.New(errors.CodeInvalidParam, "无效的资产类型") + } + + var targets []model.PackageUsage + if packageUsageID != nil && *packageUsageID > 0 { + var usage model.PackageUsage + err := baseQuery. + Where("id = ? AND order_id = ?", *packageUsageID, orderID). + Where("status IN ?", validStatuses). + First(&usage).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + s.logger.Info("退款精准失效:未命中可失效套餐", + zap.String("asset_type", assetType), + zap.Uint("asset_id", assetID), + zap.Uint("order_id", orderID), + zap.Uint("package_usage_id", *packageUsageID), + ) + return nil + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联套餐失败") + } + targets = append(targets, usage) + } else { + if err := baseQuery. + Where("order_id = ?", orderID). + Where("status IN ?", validStatuses). + Find(&targets).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询退款订单套餐失败") + } + if len(targets) == 0 { + s.logger.Info("退款精准失效:订单无可失效套餐", + zap.String("asset_type", assetType), + zap.Uint("asset_id", assetID), + zap.Uint("order_id", orderID), + ) + return nil + } + } + + targetIDSet := make(map[uint]struct{}, len(targets)) + mainUsageIDs := make([]uint, 0, len(targets)) + for _, usage := range targets { + targetIDSet[usage.ID] = struct{}{} + if usage.MasterUsageID == nil { + mainUsageIDs = append(mainUsageIDs, usage.ID) + } + } + + if len(mainUsageIDs) > 0 { + var addons []model.PackageUsage + if err := baseQuery. + Where("master_usage_id IN ?", mainUsageIDs). + Where("status IN ?", validStatuses). + Find(&addons).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐关联加油包失败") + } + for _, addon := range addons { + targetIDSet[addon.ID] = struct{}{} + } + } + + targetIDs := make([]uint, 0, len(targetIDSet)) + for id := range targetIDSet { + targetIDs = append(targetIDs, id) + } + if len(targetIDs) == 0 { + return nil + } + + result := s.db.WithContext(ctx). + Model(&model.PackageUsage{}). + Where("id IN ?", targetIDs). + Update("status", constants.PackageUsageStatusInvalidated) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "退款失效套餐失败") + } + + s.logger.Info("退款精准失效套餐完成", + zap.String("asset_type", assetType), + zap.Uint("asset_id", assetID), + zap.Uint("order_id", orderID), + zap.Int("target_count", len(targetIDs)), + zap.Int64("affected", result.RowsAffected), + ) + + return nil +} + // InvalidateAllPackagesByAsset 批量失效资产关联的所有有效套餐 // 退款时调用:将该资产下状态为待生效(0)、生效中(1)、已用完(2)的套餐全部标记为已失效(4) func (s *ActivationService) InvalidateAllPackagesByAsset(ctx context.Context, assetType string, assetID uint) error { diff --git a/internal/service/refund/service.go b/internal/service/refund/service.go index f0b83ca..23ce474 100644 --- a/internal/service/refund/service.go +++ b/internal/service/refund/service.go @@ -423,7 +423,7 @@ func (s *Service) deductSingleCommission(ctx context.Context, refund *model.Refu } // handleAssetReset 异步处理资产重置 -// 包括:套餐失效、停机、世代重置(generation+1)、钱包重建、订单状态更新 +// 包括:退款套餐精准失效、停机、世代重置(generation+1)、钱包重建、订单状态更新 // 失败仅记录日志,不影响审批结果 func (s *Service) handleAssetReset(ctx context.Context, refundID uint) { logger := s.logger @@ -465,10 +465,19 @@ func (s *Service) handleAssetReset(ctx context.Context, refundID uint) { return } - // 1. 失效所有套餐 + // 1. 按退款单精准失效套餐(仅处理本次退款订单关联套餐) if s.packageActivationService != nil { - if err := s.packageActivationService.InvalidateAllPackagesByAsset(ctx, assetType, assetID); err != nil { - logger.Error("资产重置:失效套餐失败", zap.String("asset_type", assetType), zap.Uint("asset_id", assetID), zap.Error(err)) + if err := s.packageActivationService.InvalidatePackagesForRefund(ctx, assetType, assetID, order.ID, refund.PackageUsageID); err != nil { + fields := []zap.Field{ + zap.String("asset_type", assetType), + zap.Uint("asset_id", assetID), + zap.Uint("order_id", order.ID), + zap.Error(err), + } + if refund.PackageUsageID != nil { + fields = append(fields, zap.Uint("package_usage_id", *refund.PackageUsageID)) + } + logger.Error("资产重置:退款套餐精准失效失败", fields...) } } diff --git a/opencode.json b/opencode.json index 28e892e..8bf5b43 100644 --- a/opencode.json +++ b/opencode.json @@ -3,11 +3,11 @@ "provider": { "anthropic": { "options": { - "baseURL": "https://txibabrh.cc-coding.com/api/v1", - "apiKey": "cr_0a1dc19d63752ae4d19834a1fc2222a750c6aef7a2a17b730004c12ce91b6479" + "baseURL": "http://45.155.220.179:8317/v1", + "apiKey": "sk-ZBGcMXCdwtSK7G35s" } }, - "minimax": { + "openai": { "options": { "baseURL": "http://45.155.220.179:8317/v1", "apiKey": "sk-ZBGcMXCdwtSK7G35s"