@kunlv/atdc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/AGENTS.md +58 -0
  2. package/GO.md +245 -0
  3. package/bin/add-command.py +144 -0
  4. package/bin/atdc.js +460 -0
  5. package/bin/cls.js +98 -0
  6. package/central/link.sh +60 -0
  7. package/central/ticket_center_server/.handoffs/TASK-238389_QYinVipLimit.md +128 -0
  8. package/central/ticket_center_server/.test_plans/TASK-227254-SupportMembership-self-test-plan.md +566 -0
  9. package/central/ticket_center_server/.test_plans/task-238389-qyin-vip-limit-self-test-plan.md +210 -0
  10. package/commands/claude/atd.md +3 -0
  11. package/commands/claude/atdc/1-spec.md +5 -0
  12. package/commands/claude/atdc/1-update.md +5 -0
  13. package/commands/claude/atdc/2-qa.md +3 -0
  14. package/commands/claude/atdc/3-impl-skip-spec.md +5 -0
  15. package/commands/claude/atdc/3-impl.md +3 -0
  16. package/commands/claude/atdc/4-verify.md +3 -0
  17. package/commands/claude/atdc/5-done.md +3 -0
  18. package/commands/claude/atdc/add-command.md +4 -0
  19. package/commands/claude/atdc/add-to-hc.md +3 -0
  20. package/commands/claude/atdc/analysis-cls-log.md +4 -0
  21. package/commands/claude/atdc/article.md +6 -0
  22. package/commands/claude/atdc/bugfix.md +4 -0
  23. package/commands/claude/atdc/code-simplifier.md +8 -0
  24. package/commands/claude/atdc/commit.md +4 -0
  25. package/commands/claude/atdc/compound.md +5 -0
  26. package/commands/claude/atdc/context.md +3 -0
  27. package/commands/claude/atdc/diagnose.md +8 -0
  28. package/commands/claude/atdc/git-guardrails.md +18 -0
  29. package/commands/claude/atdc/grill-with-docs.md +8 -0
  30. package/commands/claude/atdc/handoff.md +8 -0
  31. package/commands/claude/atdc/health.md +3 -0
  32. package/commands/claude/atdc/learn.md +4 -0
  33. package/commands/claude/atdc/review.md +10 -0
  34. package/commands/claude/atdc/search.md +4 -0
  35. package/commands/claude/atdc/strategy.md +3 -0
  36. package/commands/claude/atdc/table2xlsx.md +4 -0
  37. package/commands/claude/atdc/test-plan.md +5 -0
  38. package/commands/claude/atdc/travel-planner.md +4 -0
  39. package/commands/claude/atdc/xlsx2sql-insert.md +4 -0
  40. package/commands/claude/atdc/xlsx2sql-update.md +4 -0
  41. package/commands/claude/atdc/zoom-out.md +8 -0
  42. package/commands/codex/atdc/SKILL.md +8 -0
  43. package/commands/codex/atdc-1-spec/SKILL.md +8 -0
  44. package/commands/codex/atdc-1-update/SKILL.md +8 -0
  45. package/commands/codex/atdc-2-qa/SKILL.md +8 -0
  46. package/commands/codex/atdc-3-impl/SKILL.md +8 -0
  47. package/commands/codex/atdc-3-impl-skip-spec/SKILL.md +8 -0
  48. package/commands/codex/atdc-4-verify/SKILL.md +8 -0
  49. package/commands/codex/atdc-5-done/SKILL.md +8 -0
  50. package/commands/codex/atdc-add-command/SKILL.md +7 -0
  51. package/commands/codex/atdc-add-to-hc/SKILL.md +8 -0
  52. package/commands/codex/atdc-analysis-cls-log/SKILL.md +5 -0
  53. package/commands/codex/atdc-article/SKILL.md +8 -0
  54. package/commands/codex/atdc-bugfix/SKILL.md +5 -0
  55. package/commands/codex/atdc-code-simplifier/SKILL.md +7 -0
  56. package/commands/codex/atdc-commit/SKILL.md +7 -0
  57. package/commands/codex/atdc-compound/SKILL.md +8 -0
  58. package/commands/codex/atdc-context/SKILL.md +8 -0
  59. package/commands/codex/atdc-diagnose/SKILL.md +9 -0
  60. package/commands/codex/atdc-git-guardrails/SKILL.md +19 -0
  61. package/commands/codex/atdc-grill-with-docs/SKILL.md +9 -0
  62. package/commands/codex/atdc-handoff/SKILL.md +9 -0
  63. package/commands/codex/atdc-health/SKILL.md +8 -0
  64. package/commands/codex/atdc-learn/SKILL.md +7 -0
  65. package/commands/codex/atdc-review/SKILL.md +49 -0
  66. package/commands/codex/atdc-search/SKILL.md +5 -0
  67. package/commands/codex/atdc-strategy/SKILL.md +8 -0
  68. package/commands/codex/atdc-table2xlsx/SKILL.md +8 -0
  69. package/commands/codex/atdc-test-plan/SKILL.md +13 -0
  70. package/commands/codex/atdc-travel-planner/SKILL.md +7 -0
  71. package/commands/codex/atdc-xlsx2sql-insert/SKILL.md +8 -0
  72. package/commands/codex/atdc-xlsx2sql-update/SKILL.md +8 -0
  73. package/commands/codex/atdc-zoom-out/SKILL.md +9 -0
  74. package/commands/cursor/atdc/SKILL.md +8 -0
  75. package/commands/cursor/atdc-1-spec/SKILL.md +8 -0
  76. package/commands/cursor/atdc-1-update/SKILL.md +8 -0
  77. package/commands/cursor/atdc-2-qa/SKILL.md +8 -0
  78. package/commands/cursor/atdc-3-impl/SKILL.md +8 -0
  79. package/commands/cursor/atdc-3-impl-skip-spec/SKILL.md +8 -0
  80. package/commands/cursor/atdc-4-verify/SKILL.md +8 -0
  81. package/commands/cursor/atdc-5-done/SKILL.md +8 -0
  82. package/commands/cursor/atdc-add-command/SKILL.md +7 -0
  83. package/commands/cursor/atdc-add-to-hc/SKILL.md +8 -0
  84. package/commands/cursor/atdc-analysis-cls-log/SKILL.md +5 -0
  85. package/commands/cursor/atdc-article/SKILL.md +8 -0
  86. package/commands/cursor/atdc-bugfix/SKILL.md +5 -0
  87. package/commands/cursor/atdc-code-simplifier/SKILL.md +7 -0
  88. package/commands/cursor/atdc-commit/SKILL.md +7 -0
  89. package/commands/cursor/atdc-compound/SKILL.md +8 -0
  90. package/commands/cursor/atdc-context/SKILL.md +8 -0
  91. package/commands/cursor/atdc-diagnose/SKILL.md +8 -0
  92. package/commands/cursor/atdc-git-guardrails/SKILL.md +18 -0
  93. package/commands/cursor/atdc-grill-with-docs/SKILL.md +8 -0
  94. package/commands/cursor/atdc-handoff/SKILL.md +8 -0
  95. package/commands/cursor/atdc-health/SKILL.md +8 -0
  96. package/commands/cursor/atdc-learn/SKILL.md +7 -0
  97. package/commands/cursor/atdc-review/SKILL.md +49 -0
  98. package/commands/cursor/atdc-search/SKILL.md +5 -0
  99. package/commands/cursor/atdc-strategy/SKILL.md +8 -0
  100. package/commands/cursor/atdc-table2xlsx/SKILL.md +8 -0
  101. package/commands/cursor/atdc-test-plan/SKILL.md +7 -0
  102. package/commands/cursor/atdc-travel-planner/SKILL.md +7 -0
  103. package/commands/cursor/atdc-xlsx2sql-insert/SKILL.md +8 -0
  104. package/commands/cursor/atdc-xlsx2sql-update/SKILL.md +8 -0
  105. package/commands/cursor/atdc-zoom-out/SKILL.md +8 -0
  106. package/commands/opencode/atd.md +6 -0
  107. package/commands/opencode/atdc/1-spec.md +8 -0
  108. package/commands/opencode/atdc/1-update.md +8 -0
  109. package/commands/opencode/atdc/2-qa.md +6 -0
  110. package/commands/opencode/atdc/3-impl-skip-spec.md +8 -0
  111. package/commands/opencode/atdc/3-impl.md +6 -0
  112. package/commands/opencode/atdc/4-verify.md +6 -0
  113. package/commands/opencode/atdc/5-done.md +6 -0
  114. package/commands/opencode/atdc/add-command.md +6 -0
  115. package/commands/opencode/atdc/add-to-hc.md +6 -0
  116. package/commands/opencode/atdc/analysis-cls-log.md +6 -0
  117. package/commands/opencode/atdc/article.md +6 -0
  118. package/commands/opencode/atdc/bugfix.md +6 -0
  119. package/commands/opencode/atdc/code-simplifier.md +8 -0
  120. package/commands/opencode/atdc/commit.md +6 -0
  121. package/commands/opencode/atdc/compound.md +8 -0
  122. package/commands/opencode/atdc/context.md +6 -0
  123. package/commands/opencode/atdc/diagnose.md +8 -0
  124. package/commands/opencode/atdc/git-guardrails.md +18 -0
  125. package/commands/opencode/atdc/grill-with-docs.md +8 -0
  126. package/commands/opencode/atdc/handoff.md +8 -0
  127. package/commands/opencode/atdc/health.md +6 -0
  128. package/commands/opencode/atdc/learn.md +6 -0
  129. package/commands/opencode/atdc/review.md +7 -0
  130. package/commands/opencode/atdc/search.md +6 -0
  131. package/commands/opencode/atdc/strategy.md +6 -0
  132. package/commands/opencode/atdc/table2xlsx.md +6 -0
  133. package/commands/opencode/atdc/test-plan.md +6 -0
  134. package/commands/opencode/atdc/travel-planner.md +6 -0
  135. package/commands/opencode/atdc/xlsx2sql-insert.md +6 -0
  136. package/commands/opencode/atdc/xlsx2sql-update.md +6 -0
  137. package/commands/opencode/atdc/zoom-out.md +8 -0
  138. package/config/cls-defaults.json +19 -0
  139. package/hooks/atd-archive.sh +86 -0
  140. package/hooks/atd-state.sh +165 -0
  141. package/hooks/evidence-hook.sh +88 -0
  142. package/hooks/evidence-log.sh +54 -0
  143. package/hooks/guardrails.sh +65 -0
  144. package/hooks/handoff-compress.sh +104 -0
  145. package/hooks/phase-guard.sh +134 -0
  146. package/hooks/phase-write-guard.sh +96 -0
  147. package/hooks/ponytail-fix.sh +179 -0
  148. package/hooks/ponytail-guard.sh +66 -0
  149. package/hooks/ponytail-scan.sh +62 -0
  150. package/install.sh +276 -0
  151. package/package.json +20 -0
  152. package/packages/cls/bin/cls.js +146 -0
  153. package/packages/cls/config/defaults.json +17 -0
  154. package/packages/cls/mcp/server.js +142 -0
  155. package/packages/cls/package.json +29 -0
  156. package/packages/cls/scripts/postinstall.js +19 -0
  157. package/packages/cls/src/client.js +262 -0
  158. package/prompts/1-spec.md +271 -0
  159. package/prompts/1-update.md +198 -0
  160. package/prompts/2-qa.md +164 -0
  161. package/prompts/3-impl-bugfix.md +271 -0
  162. package/prompts/3-impl.md +419 -0
  163. package/prompts/4-verify.md +160 -0
  164. package/prompts/5-done.md +227 -0
  165. package/prompts/6-article.md +571 -0
  166. package/prompts/X-add-command.md +60 -0
  167. package/prompts/X-add-to-hc.md +178 -0
  168. package/prompts/X-analysis-cls-log.md +68 -0
  169. package/prompts/X-bugfix.md +10 -0
  170. package/prompts/X-code-simplifier.md +38 -0
  171. package/prompts/X-commit.md +116 -0
  172. package/prompts/X-compound.md +188 -0
  173. package/prompts/X-context-scan.md +101 -0
  174. package/prompts/X-diagnose.md +116 -0
  175. package/prompts/X-grill-with-docs.md +120 -0
  176. package/prompts/X-handoff.md +80 -0
  177. package/prompts/X-health.md +78 -0
  178. package/prompts/X-learn.md +151 -0
  179. package/prompts/X-review.md +417 -0
  180. package/prompts/X-search.md +74 -0
  181. package/prompts/X-strategy.md +95 -0
  182. package/prompts/X-table2xlsx.md +46 -0
  183. package/prompts/X-test-plan.md +436 -0
  184. package/prompts/X-travel-planner.md +22 -0
  185. package/prompts/X-xlsx2sql-insert.md +34 -0
  186. package/prompts/X-xlsx2sql-update.md +40 -0
  187. package/prompts/X-zoom-out.md +119 -0
  188. package/reference/diagnose-loop.md +66 -0
  189. package/reference/gates.md +90 -0
  190. package/reference/git-guardrails.md +71 -0
  191. package/reference/guards.md +275 -0
  192. package/reference/learn.md +100 -0
  193. package/reference/memory.md +190 -0
  194. package/reference/risk.md +74 -0
  195. package/reference/tdd.md +40 -0
  196. package/scripts/svg2png.js +46 -0
  197. package/src/cls/client.js +244 -0
  198. package/src/constants.js +49 -0
  199. package/src/evidence/ai-draft-metrics.js +157 -0
  200. package/src/evidence/canvas.js +184 -0
  201. package/src/evidence/index.js +98 -0
  202. package/src/evidence/sink-events.js +165 -0
  203. package/src/evidence/stage-timing.js +112 -0
  204. package/src/installers/claude.js +79 -0
  205. package/src/installers/codex.js +62 -0
  206. package/src/installers/opencode.js +72 -0
  207. package/src/manifest/ponytail-patterns.json +185 -0
  208. package/src/mcp/cls-mcp-server.js +142 -0
  209. package/src/utils.js +79 -0
  210. package/src/validators/state-validate.js +123 -0
  211. package/templates/.phase-state.json +13 -0
  212. package/templates/adr.md +62 -0
  213. package/templates/changelog.md +21 -0
  214. package/templates/concepts.md +35 -0
  215. package/templates/design.md +47 -0
  216. package/templates/handoff.md +64 -0
  217. package/templates/learn-skill.md +43 -0
  218. package/templates/progress.json +17 -0
  219. package/templates/proposal.md +29 -0
  220. package/templates/solution.md +36 -0
  221. package/templates/strategy.md +39 -0
  222. package/templates/tasks.md +41 -0
@@ -0,0 +1,128 @@
1
+ # TASK-238389_QYinVipLimit 交接文档
2
+
3
+ > 生成时间:2026-07-17 12:00
4
+ > 生成方式:/atd:handoff
5
+ > 源交接:`ticket_center_server.TASK-fxiQyin/docs/handoffs/TASK-238389_QYinVipLimit.md`
6
+ > 当前工作区:`/Users/lvkun/Documents/Codes/ticket_center_server.TASK-238389_QYinVipLimit`(分支 `TASK-238389_QYinVipLimit`)
7
+ > 会话来源:Q音绿钻发货失败静默成功排查 → 存量补发 → 错误语义修正 → `rights_grant_check` 并发/限流分析 → **切至此分支做限流改造**
8
+
9
+ ---
10
+
11
+ ## 做了什么
12
+
13
+ **动机**:Q音绿钻发货失败(如未映射码 `10101`)曾被当成功吞掉导致不重试/失货;线上 `rights_grant_check` 多 job 并发打满对方限流 300;需在本分支做 Q音 VIP 发放限流改造。
14
+
15
+ **结果**:前序 worktree(`TASK-fxiQyin`)已厘清失败语义与补单路径、补发 CLI/名单、64 路并发打到 300 的机制,并落地 Retry vs 永久失败语义;**本分支尚未开始限流代码/openspec**,当前即为限流改造起点。
16
+
17
+ ---
18
+
19
+ ## 关键决策
20
+
21
+ | # | 决策 | 选了 | 排除了 | 原因 |
22
+ |---|------|------|--------|------|
23
+ | 1 | 发货失败是否一律 `return error` | 仅 `DeliveryStatusRetry(=3)` 返回 error | 所有非 Shipped 都 return error | `DeliveryQQVipStatusFail(=4)`「不允许发货」重试无意义,会打爆补单/限流 |
24
+ | 2 | 存量 800+ 单补发顺序 | 先补发再改 bug | 先上线错误语义再补发 | 业务要求先止血到账;补发以权益表验收,不信 `SendSvip` 返回值 |
25
+ | 3 | 补发方式 | CLI `resend_qmusic_svip` 读 CSV + 查 `user_ticket_rights` | 仅把 `sync_status` 打回 0 跑 `rights_check` | 旧逻辑失败也 `nil`,rights_check 会误推 sync 成功 |
26
+ | 4 | 限流改造落点 | 本分支 worktree `TASK-238389_QYinVipLimit` | 在 `TASK-fxiQyin` 上继续改 | 独立需求/分支,避免串工作区 |
27
+
28
+ ### 状态语义(务必保持,限流改造勿破坏)
29
+
30
+ | Status | 常量 | 含义 | `handleQMusicGreenDiamond` |
31
+ |--------|------|------|----------------------------|
32
+ | 2 | `DeliveryStatusShipped` | 真成功 | 落库 + 短信 |
33
+ | 3 | `DeliveryStatusRetry` | 未映射/可重试(含原 10101 类) | `return error` 触发重试 |
34
+ | 4 | `DeliveryQQVipStatusFail` | 不允许发货(白名单码) | `return nil`,不重试、不落成功态 |
35
+
36
+ `parseDeliveryResponse`:成功→2;`isQMusicDeliveryNotAllowed`→4;其余未知码→3。
37
+
38
+ 本分支已确认存在:`add_right_impl.go` 中 Retry→`fmt.Errorf(...)`;`qmusicResultStatus` 将 status=4 映射为 `ResultStatusFailed`。
39
+
40
+ ---
41
+
42
+ ## 受影响模块
43
+
44
+ ```
45
+ # 前序已改(来自 TASK-fxiQyin,本分支应已包含语义修复)
46
+ service/biz/rights/
47
+ └── add_right_impl.go — Retry 用响应字段构造 error(勿 wrap nil err);永久失败 return nil
48
+ service/biz/rights/member/
49
+ └── qmusic_provider.go — status=4 → ResultStatusFailed
50
+ service/rpc/
51
+ └── qmusic_direct_vip_impl.go — 未知码→DeliveryStatusRetry;callTencentMusicAPI 为限流落点候选
52
+
53
+ cli/cmd/rights/
54
+ ├── resend_qmusic_svip.go — CSV 补发;以 QY right_type=4 + status=1 验收
55
+ ├── rights_grant_check.go — 延迟发放扫表;8 库×每库8表;无客户端限流(问题源)
56
+ └── rights_check.go — sync_status=0 补全链路;SendSvip nil 会推进 sync=1
57
+ cli/
58
+ └── register.go — 注册 resend_qmusic_svip
59
+
60
+ # 本分支待改(限流)
61
+ service/redis/
62
+ ├── redis_rate.go — 已有 RateLimiter.AllowPerSecond(可复用)
63
+ └── redis_limit.go — 计数限流备选
64
+ service/rpc/
65
+ └── qmusic_direct_vip_impl.go / local.go(SendSvip) — 限流挂载点待定
66
+
67
+ docs/bugfix/
68
+ └── ticket-QQ音乐绿钻发货静默失败-ISSUE-0027.md
69
+ ```
70
+
71
+ ### 相关路径 / 资产
72
+
73
+ | 项 | 路径 |
74
+ |----|------|
75
+ | **限流改造 worktree(当前)** | `/Users/lvkun/Documents/Codes/ticket_center_server.TASK-238389_QYinVipLimit` |
76
+ | 前序语义修复 worktree | `/Users/lvkun/Documents/Codes/ticket_center_server.TASK-fxiQyin` |
77
+ | 补发名单 | `/Users/lvkun/Documents/kugou/userid_orderno.csv`(872 行,已校验可解析) |
78
+ | openspec change | **尚无** `openspec/changes/*238389*` / `*QYinVip*` — 需新建 |
79
+
80
+ ---
81
+
82
+ ## 测试状态
83
+
84
+ | 覆盖范围 | 状态 | 说明 |
85
+ |---------|------|------|
86
+ | 单元测试 | ⚠️ | Retry wrap-nil / 永久失败不重试等语义变更未见专门单测补齐 |
87
+ | CSV 解析 | ✅ | 872 行 PARSE_OK,无重复/坏行 |
88
+ | 存量补发 | ⚠️ | CLI 已就绪;是否全量跑完、失败清单需接手确认 |
89
+ | 限流改造 | ❌ | 本分支目标功能,**尚未开始**(无 openspec、无代码 diff) |
90
+
91
+ ---
92
+
93
+ ## 未解决问题
94
+
95
+ - **Q音侧限流 300(本分支主目标)**:`rights_grant_check` 8 job×每库 8 协程 = **64 路并发**,无本地限流;对方 RTT~200ms 时理论 QPS≈320,已实锤可触发限流。应加共享令牌桶/间隔或降并发,目标峰值 **<300**(建议 200~250)。
96
+ - **永久失败 return nil 与 RightsCheck**:`status=4` 不重试合理,但 `rights_check` 若走 SendSvip 得 nil 仍可能把订单 `sync_status` 推成成功——会员永久失败时其它权益仍前进;是否要落「失败态权益记录」便于对账未定。
97
+ - **aiconsult #366882**:审查建议「非成功一律 return error」与业务决策冲突;已按「仅 Retry 抛错」落地,相关 MR 需说明。
98
+ - **补发 CLI 内对 SendSvip error 仅 Warn**:验收靠权益表;限流改造后建议失败也写入 fail-out 并可选尊重 error。
99
+ - **存量补发是否跑完**:需查 `resend_ok/fail/skip` CSV,失败单用 fail 名单重跑。
100
+
101
+ ---
102
+
103
+ ## 下一步
104
+
105
+ 1. **本目录已是正确 worktree**(`TASK-238389_QYinVipLimit`)— 勿再切回 `TASK-fxiQyin` 写限流。
106
+ 2. **按 openspec 流程起规格**:`/opsx:propose`(或项目 flow)写清限流方案;候选挂载点:`callTencentMusicAPI` / `SendSvip` / `processPendingRight`;优先复用 `service/redis/redis_rate.go` 的 `RateLimiter`(跨 job 全局生效)。
107
+ 3. **并发调参备选(改代码前可先运维止血)**:减 job 或降 `-w`(例如每库 `-w=2` → 总并发≈16);代码侧共享限流器更稳。
108
+ 4. **确认存量补发**:查 `resend_ok/fail/skip`;失败单重跑。
109
+ 5. **可选**:为 `handleQMusicGreenDiamond` Retry/永久失败分支补单测,防止再次把 4 当成可重试。
110
+
111
+ ### `rights_grant_check` 并发速查(线上)
112
+
113
+ ```
114
+ 命令:bin/cli rights_grant_check -t=1000 -i={0..7} -w=10
115
+ 配置:ticket_user sharding_db=8, sharding_table=64 → 每库 8 表
116
+ 实际:每 job 8 协程(不是 10);8 job 合计 64 协程;表内串行
117
+ 峰值 QPS ≈ 64 / 单次耗时
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 相关资源
123
+
124
+ - Bugfix:`docs/bugfix/ticket-QQ音乐绿钻发货静默失败-ISSUE-0027.md`
125
+ - 审查事件:aiconsult #168343 / Bug 366873(wrap nil err)、366882(非成功 return nil)
126
+ - 样本订单:`12607131000330004220001010395` / userid `2534695307`
127
+ - 已有限流设施:`service/redis/redis_rate.go`(`RateLimiter.AllowPerSecond`)、`service/redis/redis_limit.go`
128
+ - 源交接(前序会话):`/Users/lvkun/Documents/Codes/ticket_center_server.TASK-fxiQyin/docs/handoffs/TASK-238389_QYinVipLimit.md`
@@ -0,0 +1,566 @@
1
+ # TASK-227254-SupportMembership 自测计划(可执行版)
2
+
3
+ > 所有命令可直接复制运行。变量 `<HOST>` 替换为测试环境地址(如 `http://ticketcenter-test.kgidc.cn`)。
4
+
5
+ ---
6
+
7
+ ## 第零步:测试前准备
8
+
9
+ ```bash
10
+ # 1. 预置 SKU 数据
11
+ mysql> INSERT INTO membership_product_sku (sku_code, name, icon, platform, member_type, vip_type, list_price, sale_price, status, sort, sale_start_time, sale_end_time, create_time, update_time)
12
+ VALUES ('test-kg-vip-month', '酷狗VIP月卡', 'https://img.example.com/kg.png', 101, 1, 1001, 1800, 1500, 1, 100, '2025-01-01', '2027-12-31', NOW(), NOW());
13
+ INSERT INTO membership_product_sku (sku_code, name, icon, platform, member_type, vip_type, list_price, sale_price, status, sort, sale_start_time, sale_end_time, create_time, update_time)
14
+ VALUES ('test-qm-vip-month', 'Q音绿钻月卡', 'https://img.example.com/qm.png', 102, 1, 2001, 1800, 1500, 1, 100, '2025-01-01', '2027-12-31', NOW(), NOW());
15
+ INSERT INTO membership_product_sku (sku_code, name, icon, platform, member_type, vip_type, list_price, sale_price, status, sort, sale_start_time, sale_end_time, create_time, update_time)
16
+ VALUES ('test-kw-vip-month', '酷我VIP月卡', 'https://img.example.com/kw.png', 103, 1, 3001, 1800, 1500, 1, 100, '2025-01-01', '2027-12-31', NOW(), NOW());
17
+
18
+ # 2. 记录测试 userid(后续命令会用 ***)
19
+ # 酷狗 userid: XXXXXXXXX(需替换)
20
+ # Q音 userid: YYYYYYYYY(需替换)
21
+ # 酷我 userid: ZZZZZZZZZ(需替换)
22
+
23
+ # 3. 记下一个全局变量方便后续命令复用
24
+ export HOST=http://127.0.0.1:8080
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 链 1:商品查询
30
+
31
+ ### 1.1 正常查询
32
+ ```bash
33
+ curl -s "${HOST}/membership/product/list?performance_id=474134&app_type=1" | jq '.list | length'
34
+ # 预期:>0,返回酷狗平台 SKU
35
+ ```
36
+
37
+ ### 1.2 Q音平台过滤
38
+ ```bash
39
+ curl -s "${HOST}/membership/product/list?performance_id=474134&app_type=2" | jq '.list[] | select(.platform==101)'
40
+ # 预期:空(不返回酷狗 SKU)
41
+ ```
42
+
43
+ ### 1.3 无匹配演出
44
+ ```bash
45
+ curl -s "${HOST}/membership/product/list?performance_id=99999999&app_type=1" | jq '.list'
46
+ # 预期:[],popup 字段存在
47
+ ```
48
+
49
+ ### 1.4 Redis 缓存验证
50
+ ```bash
51
+ # 连续调两次,查看服务日志
52
+ curl -s "${HOST}/membership/product/list?performance_id=474134&app_type=1" > /dev/null
53
+ sleep 1
54
+ curl -s "${HOST}/membership/product/list?performance_id=474134&app_type=1" > /dev/null
55
+ # 查日志:第一次有 "membership GetProductList loaded, platform:xxx count:N"
56
+ # 第二次无 DB 查询日志(命中缓存)
57
+ ```
58
+
59
+ ### 1.5 售卖窗口外
60
+ ```sql
61
+ UPDATE membership_product_sku SET sale_end_time = '2025-01-02 00:00:00' WHERE sku_code = 'test-kg-vip-month';
62
+ ```
63
+ ```bash
64
+ curl -s "${HOST}/membership/product/list?performance_id=474134&app_type=1" | jq '.list[] | select(.sku_code=="test-kg-vip-month")'
65
+ # 预期:空
66
+ ```
67
+ ```sql
68
+ -- 恢复
69
+ UPDATE membership_product_sku SET sale_end_time = '2027-12-31 00:00:00' WHERE sku_code = 'test-kg-vip-month';
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 链 2:订单创建
75
+
76
+ ### 2.1 正常下单(记录 orderNo 供后续使用)
77
+ ```bash
78
+ # 酷狗用户下单
79
+ curl -s -X POST "${HOST}/membership/order/create" \
80
+ -H 'Content-Type: application/json' \
81
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"test-kg-vip-month","num":1,"ext":"{}"}' | jq .
82
+ # 预期:{"order_no":"...", "price_total":1500, "expire_time":"2026-..."}
83
+ # ⚠️ 记下 order_no → 暴露 ORD_KG_1="..."
84
+ ```
85
+ ```bash
86
+ # Q音用户下单
87
+ curl -s -X POST "${HOST}/membership/order/create" \
88
+ -H 'Content-Type: application/json' \
89
+ -d '{"userid":YYYYYYYYY,"app_type":2,"sku_code":"test-qm-vip-month","num":1,"ext":"{\"c_field\":\"1_2\"}"}' | jq .
90
+ # ⚠️ 记下 order_no → 暴露 ORD_QM_1="..."
91
+ ```
92
+ ```bash
93
+ # 酷我用户下单
94
+ curl -s -X POST "${HOST}/membership/order/create" \
95
+ -H 'Content-Type: application/json' \
96
+ -d '{"userid":ZZZZZZZZZ,"app_type":3,"sku_code":"test-kw-vip-month","num":1,"ext":""}' | jq .
97
+ # ⚠️ 记下 order_no → 暴露 ORD_KW_1="..."
98
+ ```
99
+
100
+ ### 2.2 多数量下单
101
+ ```bash
102
+ curl -s -X POST "${HOST}/membership/order/create" \
103
+ -H 'Content-Type: application/json' \
104
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"test-kg-vip-month","num":3}' | jq .
105
+ # 预期:price_total=4500(=1500*3)
106
+ # ⚠️ 记下 order_no → 暴露 ORD_KG_NUM3="..."
107
+ ```
108
+ ```sql
109
+ SELECT order_no, num, price_payable FROM membership_order WHERE order_no='${ORD_KG_NUM3}';
110
+ -- 预期:num=3, price_payable=4500
111
+ ```
112
+
113
+ ### 2.3 Ext 透传
114
+ ```sql
115
+ SELECT ext FROM membership_order WHERE order_no='${ORD_QM_1}';
116
+ -- 预期:{"c_field":"1_2"}
117
+ ```
118
+
119
+ ### 2.4 SKU 不存在
120
+ ```bash
121
+ curl -s -X POST "${HOST}/membership/order/create" \
122
+ -H 'Content-Type: application/json' \
123
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"bad-sku-not-exist"}' | jq .
124
+ # 预期:返回 20504(MembershipSkuNotAvailableErr)
125
+ ```
126
+
127
+ ### 2.5 平台不匹配
128
+ ```bash
129
+ # 用酷狗用户买 Q音 SKU
130
+ curl -s -X POST "${HOST}/membership/order/create" \
131
+ -H 'Content-Type: application/json' \
132
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"test-qm-vip-month"}' | jq .
133
+ # 预期:返回 20504(MembershipSkuNotAvailableErr,sku platform=102 ≠ app_type=1)
134
+ ```
135
+
136
+ ### 2.6 重复下单保护
137
+ ```bash
138
+ # 先完成一次下单
139
+ curl -s -X POST "${HOST}/membership/order/create" \
140
+ -H 'Content-Type: application/json' \
141
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"test-kg-vip-month"}' | jq .
142
+ # 记下 order_no → ORD_KG_DUP="..."
143
+ ```
144
+ ```sql
145
+ -- 手工制造"已支付"假数据
146
+ UPDATE membership_order SET order_status=2, pay_status=2, version=1, price_paid=1500, paid_at=NOW() WHERE order_no='${ORD_KG_DUP}';
147
+ ```
148
+ ```bash
149
+ # 再次检查重复
150
+ curl -s "${HOST}/membership/order/repeat_check?userid=XXXXXXXXX&app_type=1" | jq .
151
+ # 预期:need_block=true, product_name 非空
152
+ ```
153
+
154
+ ### 2.7 参数校验
155
+ ```bash
156
+ # userid=0
157
+ curl -s -X POST "${HOST}/membership/order/create" \
158
+ -H 'Content-Type: application/json' \
159
+ -d '{"userid":0,"app_type":1,"sku_code":"test-kg-vip-month"}' | jq .
160
+ ```
161
+ ```bash
162
+ # 不传 sku_code
163
+ curl -s -X POST "${HOST}/membership/order/create" \
164
+ -H 'Content-Type: application/json' \
165
+ -d '{"userid":XXXXXXXXX,"app_type":1}' | jq .
166
+ # 两个预期都返回 ParamErr
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 链 3:支付
172
+
173
+ ### 3.1 发起支付
174
+ ```bash
175
+ curl -s -X POST "${HOST}/membership/order/pay" \
176
+ -H 'Content-Type: application/json' \
177
+ -d '{"userid":XXXXXXXXX,"app_type":1,"order_no":"${ORD_KG_1}","pay_type":1,"openid":"test-openid-xxx"}' | jq .
178
+ # 预期:返回 pay_data 对象
179
+ ```
180
+
181
+ ### 3.2 iOS → 小程序支付
182
+ ```bash
183
+ # 需要 middleware 注入 client_code 到 context,如果本地无法注入,改为直接调用 inner 接口验证日志:
184
+ # 把订单状态重置为待支付
185
+ mysql> UPDATE membership_order SET order_status=1, pay_status=1, version=version+1 WHERE order_no='${ORD_KG_1}';
186
+ # 修改 model 验证代码后通过日志验证:
187
+ # grep "PayOrder WxMiniprogramPay req" /path/to/log
188
+ ```
189
+
190
+ ### 3.5 iOS 无 Openid 报错
191
+ ```bash
192
+ # 需要 client_code 含 "ios"。本地不好模拟,可以在 PayOrder 方法里打 log 确认分支。
193
+ # 替代验证:修改代码临时让 isIOS=true,不传 openid,应返回 ParamErr
194
+ ```
195
+
196
+ ### 3.6 Q音 BizAppID
197
+ ```bash
198
+ curl -s -X POST "${HOST}/membership/order/pay" \
199
+ -H 'Content-Type: application/json' \
200
+ -d '{"userid":YYYYYYYYY,"app_type":2,"order_no":"${ORD_QM_1}","pay_type":1,"openid":"test-openid-xxx"}' | jq .
201
+ # 日志关键词:grep "qqmusic_biz_key"
202
+ ```
203
+
204
+ ### 3.7 酷狗 BizAppID
205
+ ```bash
206
+ curl -s -X POST "${HOST}/membership/order/pay" \
207
+ -H 'Content-Type: application/json' \
208
+ -d '{"userid":XXXXXXXXX,"app_type":1,"order_no":"${ORD_KG_1}","pay_type":1,"openid":"test-openid-xxx"}' | jq .
209
+ # 日志关键词:grep "kugou_biz_key"
210
+ ```
211
+
212
+ ### 3.8 支付回调
213
+ ```bash
214
+ # 先确保订单 order_status=1(待支付)
215
+ mysql> UPDATE membership_order SET order_status=1, pay_status=1, version=version+1 WHERE order_no='${ORD_KG_1}';
216
+ ```
217
+ ```bash
218
+ curl -s "${HOST}/inner/v1/membership/order/pay_notify?order_no=${ORD_KG_1}&appid=123&sign=abc&out_trade_no=TEST_TRADE_001&trade_status=1"
219
+ # 预期:返回 "success"
220
+ ```
221
+ ```sql
222
+ SELECT order_status, pay_status, price_paid FROM membership_order WHERE order_no='${ORD_KG_1}';
223
+ -- 预期:order_status=2(Paid), pay_status=2
224
+ ```
225
+
226
+ ### 3.9 重复回调幂等
227
+ ```bash
228
+ curl -s "${HOST}/inner/v1/membership/order/pay_notify?order_no=${ORD_KG_1}&appid=123&sign=abc&out_trade_no=TEST_TRADE_001&trade_status=1"
229
+ # 预期:仍返回 "success",不报错
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 链 4:履约发放
235
+
236
+ ### 4.1 手动触发履约
237
+ ```bash
238
+ # 酷狗订单(先确认已支付:order_status=2)
239
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
240
+ -H 'Content-Type: application/json' \
241
+ -d "{\"order_no\":\"${ORD_KG_1}\"}"
242
+ # 预期:返回成功
243
+ ```
244
+ ```sql
245
+ SELECT order_status, completed_at FROM membership_order WHERE order_no='${ORD_KG_1}';
246
+ -- 预期:order_status=4(Completed)
247
+ ```
248
+
249
+ ### 4.2 酷狗会员发放日志
250
+ ```bash
251
+ grep "kugou Grant VipCreateOrder req" /path/to/log | tail -1
252
+ grep "kugou Grant VipCreateOrder resp" /path/to/log | tail -1
253
+ ```
254
+
255
+ ### 4.3 Q音会员发放日志
256
+ ```bash
257
+ # 先让 Q音订单也变成已支付再调 fulfill
258
+ mysql> UPDATE membership_order SET order_status=2, pay_status=2, version=version+1, price_paid=1500, paid_at=NOW() WHERE order_no='${ORD_QM_1}';
259
+ ```
260
+ ```bash
261
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
262
+ -H 'Content-Type: application/json' \
263
+ -d "{\"order_no\":\"${ORD_QM_1}\"}"
264
+ grep "qmusic Grant" /path/to/log | tail -2
265
+ ```
266
+
267
+ ### 4.4 酷我会员发放日志
268
+ ```bash
269
+ mysql> UPDATE membership_order SET order_status=2, pay_status=2, version=version+1, price_paid=1500, paid_at=NOW() WHERE order_no='${ORD_KW_1}';
270
+ ```
271
+ ```bash
272
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
273
+ -H 'Content-Type: application/json' \
274
+ -d "{\"order_no\":\"${ORD_KW_1}\"}"
275
+ grep "kuwo Grant" /path/to/log | tail -2
276
+ ```
277
+
278
+ ### 4.5 发放幂等
279
+ ```bash
280
+ # 再调一次同一个 order_no
281
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
282
+ -H 'Content-Type: application/json' \
283
+ -d "{\"order_no\":\"${ORD_KG_1}\"}"
284
+ grep "fulfillment skipped by idempotency" /path/to/log | tail -1
285
+ ```
286
+
287
+ ### 4.6 数量×发放周期
288
+ ```bash
289
+ # 用多数量订单 ORD_KG_NUM3(num=3, member_type=1 月卡)
290
+ mysql> UPDATE membership_order SET order_status=2, pay_status=2, version=version+1, price_paid=4500, paid_at=NOW() WHERE order_no='${ORD_KG_NUM3}';
291
+ ```
292
+ ```bash
293
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
294
+ -H 'Content-Type: application/json' \
295
+ -d "{\"order_no\":\"${ORD_KG_NUM3}\"}"
296
+ grep "kugou Grant VipCreateOrder req" /path/to/log | tail -1 | grep "month"
297
+ # 预期:month=3(月卡×3 份=3 个月)
298
+ ```
299
+
300
+ ### 4.7 发放失败重试
301
+ ```bash
302
+ # 构造一条新订单并支付
303
+ curl -s -X POST "${HOST}/membership/order/create" \
304
+ -H 'Content-Type: application/json' \
305
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"test-kg-vip-month","num":1}' | jq -r '.order_no'
306
+ # 记下 → EXPORT ORD_KG_FAIL1="..."
307
+ mysql> UPDATE membership_order SET order_status=2, pay_status=2, version=version+1, price_paid=1500, paid_at=NOW() WHERE order_no='${ORD_KG_FAIL1}';
308
+ ```
309
+ ```bash
310
+ # 触发履约(如酷狗 RPC 不可达会失败)
311
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
312
+ -H 'Content-Type: application/json' \
313
+ -d "{\"order_no\":\"${ORD_KG_FAIL1}\"}"
314
+ ```
315
+ ```sql
316
+ SELECT o.order_status, f.fulfillment_status, f.retry_strategy, f.next_retry_at
317
+ FROM membership_order o
318
+ JOIN membership_order_fulfillment f ON o.order_no = f.order_no
319
+ WHERE o.order_no = '${ORD_KG_FAIL1}';
320
+ -- 预期:fulfillment_status=3, retry_strategy=1, next_retry_at > NOW()
321
+ ```
322
+
323
+ ### 4.8 履约状态锁
324
+ ```bash
325
+ # 同一订单同时发两个 fulfill 请求(用 & 后台)
326
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
327
+ -H 'Content-Type: application/json' \
328
+ -d "{\"order_no\":\"${ORD_KG_1}\"}" &
329
+ curl -s -X POST "${HOST}/inner/v1/membership/order/fulfill" \
330
+ -H 'Content-Type: application/json' \
331
+ -d "{\"order_no\":\"${ORD_KG_1}\"}" &
332
+ wait
333
+ # 日志中一个 "state locked for grant",另一个 "already processing and not timeout"
334
+ ```
335
+
336
+ ---
337
+
338
+ ## 链 5:履约失败重试 Cron
339
+
340
+ ### 5.1 重试扫描
341
+ ```bash
342
+ # 确保 4.7 留下了失败记录,然后手动改 next_retry_at 为过去
343
+ mysql> UPDATE membership_order_fulfillment SET next_retry_at = '2020-01-01 00:00:00', retry_strategy=1 WHERE order_no='${ORD_KG_FAIL1}';
344
+ ```
345
+ ```bash
346
+ # 启动 cron
347
+ ./cli membership_fulfill_retry -l=10 -k=lock_test_fulfill_retry 2>&1 | tail -5
348
+ # 预期:日志 "membership fulfill retry success" 或 "deferred again"
349
+ ```
350
+
351
+ ### 5.2 处理中超时
352
+ ```sql
353
+ -- 模拟一条卡在 Processing 且超时(>15 分钟前开始)的记录
354
+ UPDATE membership_order_fulfillment
355
+ SET fulfillment_status = 1, processing_started_at = DATE_SUB(NOW(), INTERVAL 20 MINUTE)
356
+ WHERE order_no = '${ORD_KG_FAIL1}';
357
+ ```
358
+ ```bash
359
+ ./cli membership_fulfill_retry -l=10 -k=lock_test_fulfill_retry 2>&1 | tail -3
360
+ # 预期:该记录被重新触发
361
+ ```
362
+
363
+ ### 5.3 未到重试时间
364
+ ```sql
365
+ UPDATE membership_order_fulfillment SET next_retry_at = DATE_ADD(NOW(), INTERVAL 1 HOUR), fulfillment_status=3 WHERE order_no='${ORD_KG_FAIL1}';
366
+ ```
367
+ ```bash
368
+ ./cli membership_fulfill_retry -l=10 -k=lock_test_fulfill_retry 2>&1 | tail -3
369
+ # 预期:日志 "finished, retryable: 0"
370
+ ```
371
+
372
+ ---
373
+
374
+ ## 链 6:业务方通知
375
+
376
+ ### 6.1 履约成功后通知
377
+ ```sql
378
+ -- 找一个已完成履约的订单
379
+ SELECT order_no, biz_notify_status FROM membership_order WHERE order_no='${ORD_KG_1}';
380
+ -- 预期:biz_notify_status=1(只要通知接口可达)
381
+ ```
382
+
383
+ ### 6.2 通知失败吞错误
384
+ ```bash
385
+ # 构造新订单走完 链2→链3→链4 流程,如果回调接口失败(本地环境降级验证):
386
+ ```
387
+ ```sql
388
+ -- 手工修改为"已完成但未通知"
389
+ UPDATE membership_order SET biz_notify_status=0, biz_notify_retry_count=0, biz_notify_next_retry_at='1970-01-01 00:00:00' WHERE order_no='${ORD_KG_1}';
390
+ ```
391
+ ```bash
392
+ # 通过 RetryBizNotify 触发的日志验证:
393
+ grep "biz notify callback failed" /path/to/log | tail -1
394
+ ```
395
+
396
+ ### 6.3 通知重试 cron
397
+ ```sql
398
+ -- 制造一条待通知订单
399
+ UPDATE membership_order SET biz_notify_status=0, biz_notify_retry_count=0, biz_notify_next_retry_at='2020-01-01 00:00:00' WHERE order_status=4 AND order_no='${ORD_KG_1}';
400
+ ```
401
+ ```bash
402
+ ./cli membership_biz_notify_retry -l=10 2>&1 | tail -5
403
+ # 预期:日志 "biz notify retry finished, count:1"
404
+ ```
405
+
406
+ ### 6.4 重试间隔递增
407
+ ```sql
408
+ -- 模拟第一次重试失败,把 next_retry_at 改过去
409
+ UPDATE membership_order SET biz_notify_retry_count=0, biz_notify_next_retry_at='2020-01-01 00:00:00', biz_notify_status=0 WHERE order_no='${ORD_KG_1}';
410
+ ```
411
+ ```bash
412
+ ./cli membership_biz_notify_retry -l=10 2>&1 | tail -3
413
+ ```
414
+ ```sql
415
+ SELECT biz_notify_retry_count, biz_notify_next_retry_at FROM membership_order WHERE order_no='${ORD_KG_1}';
416
+ -- 预期:retry_count=1, next_retry_at ≈ NOW()+30s(Apollo 默认间隔)
417
+ ```
418
+
419
+ ### 6.5 超过最大重试次数
420
+ ```sql
421
+ -- 手动设满
422
+ UPDATE membership_order SET biz_notify_retry_count=99, biz_notify_next_retry_at='2020-01-01 00:00:00' WHERE order_no='${ORD_KG_1}';
423
+ ```
424
+ ```bash
425
+ ./cli membership_biz_notify_retry -l=10 2>&1 | tail -3
426
+ ```
427
+ ```sql
428
+ SELECT biz_notify_retry_count FROM membership_order WHERE order_no='${ORD_KG_1}';
429
+ -- 预期:count 不变(≥ 5 = Apollo max,不再重试)
430
+ ```
431
+
432
+ ### 6.6 通知请求体映射
433
+ ```bash
434
+ grep "biz notify request built" /path/to/log | tail -1
435
+ # 预期日志:user_id=XXXXXXXXX, quantity=1, product_id=test-kg-vip-month
436
+ ```
437
+
438
+ ### 6.7 Extend 原样透传
439
+ ```bash
440
+ grep "MembershipBizNotifyCallback" /path/to/log | tail -3
441
+ # 检查请求 body 中的 Extend 字段是否和 order.Ext 一致
442
+ ```
443
+
444
+ ---
445
+
446
+ ## 链 7:订单管理
447
+
448
+ ### 7.1 查询状态
449
+ ```bash
450
+ curl -s "${HOST}/membership/order/query_status?userid=XXXXXXXXX&app_type=1&order_no=${ORD_KG_1}" | jq .
451
+ # 预期:{"status":4}
452
+ ```
453
+
454
+ ### 7.2 取消订单(需先有未支付订单)
455
+ ```bash
456
+ # 创建一条新订单
457
+ ORD_KG_CANCEL=$(curl -s -X POST "${HOST}/membership/order/create" \
458
+ -H 'Content-Type: application/json' \
459
+ -d '{"userid":XXXXXXXXX,"app_type":1,"sku_code":"test-kg-vip-month","num":1}' | jq -r '.order_no')
460
+ echo "ORD_KG_CANCEL=${ORD_KG_CANCEL}"
461
+ ```
462
+ ```bash
463
+ curl -s -X POST "${HOST}/membership/order/cancel" \
464
+ -H 'Content-Type: application/json' \
465
+ -d "{\"userid\":XXXXXXXXX,\"app_type\":1,\"order_no\":\"${ORD_KG_CANCEL}\"}" | jq .
466
+ ```
467
+ ```sql
468
+ SELECT order_status, pay_status FROM membership_order WHERE order_no='${ORD_KG_CANCEL}';
469
+ -- 预期:order_status=5, pay_status=4
470
+ ```
471
+
472
+ ### 7.3 已支付不能取消
473
+ ```bash
474
+ curl -s -X POST "${HOST}/membership/order/cancel" \
475
+ -H 'Content-Type: application/json' \
476
+ -d "{\"userid\":XXXXXXXXX,\"app_type\":1,\"order_no\":\"${ORD_KG_1}\"}" | jq .
477
+ # 预期:返回 MembershipOrderStatusInvalidErr
478
+ ```
479
+
480
+ ### 7.4 订单详情
481
+ ```bash
482
+ curl -s "${HOST}/membership/order/detail?userid=XXXXXXXXX&app_type=1&order_no=${ORD_KG_1}" | jq .
483
+ # 预期:product_name、icon、price_total、order_status 都和 DB 一致
484
+ ```
485
+
486
+ ### 7.5 订单列表
487
+ ```bash
488
+ curl -s "${HOST}/membership/order/list?userid=XXXXXXXXX&app_type=1&page=1&page_size=5" | jq '{has_more,count: (.list|length)}'
489
+ # 预期:有记录,has_more 正确
490
+ ```
491
+
492
+ ### 7.6 数量展示
493
+ ```bash
494
+ curl -s "${HOST}/membership/order/detail?userid=XXXXXXXXX&app_type=1&order_no=${ORD_KG_NUM3}" | jq '.order | {quantity, price_total}'
495
+ # 预期:quantity=3
496
+ ```
497
+
498
+ ---
499
+
500
+ ## 链 8:会员优先购权限
501
+
502
+ ### 8.1 未登录
503
+ ```bash
504
+ curl -s "${HOST}/v1/performance/detail?performance_id=474134&app_type=1&userid=0" | jq '.performance_vip_sale_permission'
505
+ # 预期:{"eligible":false,"reason":"not_logged_in"} 或 null(无配置时)
506
+ ```
507
+
508
+ ### 8.2 平台不匹配
509
+ ```bash
510
+ curl -s "${HOST}/v1/performance/detail?performance_id=474134&app_type=1&userid=XXXXXXXXX" | jq '.performance_vip_sale_permission'
511
+ # 检查 reason 字段(有配置时可能是 eligible 或 platform_mismatch)
512
+ ```
513
+
514
+ ### 8.3 非 VIP 用户
515
+ ```bash
516
+ # 用一个不是 VIP 的普通用户查
517
+ curl -s "${HOST}/v1/performance/detail?performance_id=474134&app_type=1&userid=999999999" | jq '.performance_vip_sale_permission'
518
+ # 预期:reason 为 not_eligible 或 check_degraded(取决于 RPC 是否可达)
519
+ ```
520
+
521
+ ### 8.4 无 VIP 配置演出
522
+ ```bash
523
+ curl -s "${HOST}/v1/performance/detail?performance_id=474134&app_type=1&userid=XXXXXXXXX" | jq 'keys | contains(["performance_vip_sale_config"])'
524
+ # 如果演出未配置优先购,预期:false(字段不下发)
525
+ ```
526
+
527
+ ---
528
+
529
+ ## 链 9:日志验证
530
+
531
+ ```bash
532
+ # 全部链跑完后,汇总 grep 检查
533
+ echo "=== 下单 ==="
534
+ grep "CreateOrder success" /path/to/log | tail -3
535
+
536
+ echo "=== 支付 RPC ==="
537
+ grep "PayOrder.*Pay.*req\|PayOrder.*Pay.*resp" /path/to/log | tail -4
538
+
539
+ echo "=== 履约发放 RPC ==="
540
+ grep "Grant .*req\|Grant .*resp" /path/to/log | tail -6
541
+
542
+ echo "=== 履约步骤 ==="
543
+ grep "fulfillment prepare start\|fulfillment prepared\|fulfillment finalize success" /path/to/log | tail -5
544
+
545
+ echo "=== 业务通知 ==="
546
+ grep "biz notify" /path/to/log | tail -5
547
+
548
+ echo "=== Cron 重试 ==="
549
+ grep "membership.*retry finished" /path/to/log | tail -3
550
+ ```
551
+
552
+ ---
553
+
554
+ ## 测试完成检查
555
+
556
+ ```bash
557
+ # 最终编译验证
558
+ go build ./...
559
+
560
+ # 最终单元测试
561
+ go test ./service/biz/membership/... -count=1
562
+
563
+ # ⚠️ 清理测试数据(可选)
564
+ mysql> DELETE FROM membership_order WHERE order_no LIKE 'MC%' AND user_id IN (XXXXXXXXX, YYYYYYYYY, ZZZZZZZZZ);
565
+ mysql> DELETE FROM membership_order_fulfillment WHERE order_no LIKE 'MC%';
566
+ ```