@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.05acdd1

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 (65) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/charts/SKILL.md +4 -0
  3. package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
  4. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  5. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
  6. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  7. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2831 -293
  8. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
  9. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
  10. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
  11. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
  12. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  13. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
  25. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
  26. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
  27. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
  28. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
  29. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
  30. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
  31. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
  32. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
  33. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
  34. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
  35. package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
  36. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +265 -0
  37. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
  38. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
  39. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
  40. package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
  41. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
  42. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +77 -0
  43. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
  44. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +205 -0
  45. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +206 -0
  46. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
  47. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
  48. package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
  49. package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
  50. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  51. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
  52. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  53. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  54. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
  55. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  56. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  57. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
  58. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
  59. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
  60. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  61. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  62. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
  63. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
  64. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
  65. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
@@ -0,0 +1,265 @@
1
+ ---
2
+ name: nestjs-cache
3
+ description: Use when planning, adding, debugging, or reviewing NestJS cache logic, especially for performance optimization, repeated data access, external API or database load reduction, multi-instance FaaS cache architecture, or shared versus in-memory cache choices. 触发词:缓存方案, 缓存设计, 性能优化, 重复数据访问, 多实例缓存, FaaS 缓存, 共享缓存, 内存缓存, cache architecture, cache, caching, cache-manager, CacheInterceptor, CACHE_MANAGER, cache.wrap, CacheKey, CacheTTL, TTL, 缓存穿透, 缓存击穿, cache miss, cache stampede, single-flight, 热点缓存
4
+ steering: true
5
+ steering-topic: nestjs_cache
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
8
+
9
+ # NestJS Caching 使用指南
10
+
11
+ 本 Skill 用于妙搭 NestJS 后端的缓存评估、接入、优化、审查和排障。先判断数据能否复用、谁可以读取、允许多旧,再确定 Key、TTL、失效方式和实现。平台已注入 `CACHE_MANAGER`,业务直接使用。触发本 Skill 只表示需要评估缓存;收益不足或正确性条件不满足时,说明原因并给出合适的处理方向。
12
+
13
+ ## 前置约束:不要自己装配 `CacheModule`
14
+
15
+ 平台已经在根级注册好全局 `CacheModule` 并暴露 `CACHE_MANAGER` provider,业务模块**直接 `@Inject(CACHE_MANAGER)` 即可**。
16
+
17
+ **不要**在业务 `Module` 里再 `imports: [CacheModule.register()]` 或 `CacheModule.registerAsync(...)`:
18
+
19
+ ```typescript
20
+ // BAD — 会创建新的独立 store,不共享连接池 / token / fail-open 保护
21
+ @Module({
22
+ imports: [CacheModule.register()], // ← 删掉;直接 @Inject(CACHE_MANAGER) 即可(见下方姿势 A)
23
+ controllers: [BookController],
24
+ providers: [BookService],
25
+ })
26
+ export class BookModule {}
27
+ ```
28
+
29
+ 自己 `register()` 出的 store 是内存 store 且脱离平台管理链路,**不走 Redis、不受 fail-open 保护、不上报可观测**——症状是"本地看着 hit 都对,上线后集群不共享 / 重启就丢"。
30
+
31
+ ## 禁止使用内存缓存(CRITICAL)
32
+
33
+ 应用采用多 FaaS 实例部署,进程内缓存只对当前实例生效,无法被其他实例共享。因此,**禁止**使用 `new Map()`、模块级变量、全局单例或自行注册的内存 store 实现缓存;统一使用平台注入的 `CACHE_MANAGER`。
34
+
35
+ ## 依赖版本(CRITICAL)
36
+
37
+ **必须使用 `cache-manager` v6**,禁止安装或按照 v4 / v5 / v7 的 API 语义编写缓存代码。
38
+
39
+ ## 什么时候该加缓存(先决策再动手)
40
+
41
+ ```
42
+ 决策树:加缓存吗?
43
+ ├─ 是否满足以下任一"值得缓存"信号?
44
+ │ a) 读得频繁(同一份数据反复被查,非一次性接口)
45
+ │ b) 读得慢(源侧要跨服务/跨库/跑聚合,用户能感觉到等待)
46
+ │ c) 读得贵(外部 HTTP 服务、多维表格等插件、AI 模型调用、高负载 RPC / 大结果集 SQL)
47
+ │ ├─ 都不满足 → 不加。加了维护成本 > 收益
48
+ │ └─ 满足任一 → 下一步
49
+ ├─ 返回数据是否受身份或权限影响?
50
+ │ ├─ 否 → 下一步
51
+ │ └─ 是 → 每次请求仍须执行必要鉴权;缓存命中不能跳过方法内的权限检查
52
+ │ 身份相关结果必须隔离,权限变化后旧结果不得越权访问
53
+ │ 无法满足时 → 不缓存该结果;继续评估可独立复用的公共数据
54
+ ├─ 业务可容忍 TTL 期内数据轻微陈旧(读到几秒前的旧值不影响正确性)?
55
+ │ ├─ 否(要强一致,如库存/余额/权限当次判定)→ 不加
56
+ │ └─ 是 → 下一步
57
+ ├─ 数据复用收益是否高于缓存维护成本?
58
+ │ 结合重复读取量、更新频率、允许的陈旧时间和失效成本判断;读多写少是优先信号
59
+ │ ├─ 否,或无法满足一致性要求 → 不加
60
+ │ └─ 是 → 可加;写入较频繁时,仍需确认失效方式可维护、预期命中率有价值
61
+ └─ 是否存在同一 Key 的并发回源?
62
+ └─ 是 → 使用 `cache.wrap` 合并单进程内并发;多实例仍可能分别回源,继续评估源侧承载能力
63
+ ```
64
+
65
+ **判断技巧**:先确认相同输入是否会重复读取,再检查身份隔离、允许的陈旧时间和失效方式。连续请求可以返回相同结果,只是复用信号,不能替代这些检查。接口慢时也要检查索引、重复查询等源侧问题。
66
+
67
+ **优先评估**:外部 API 元数据、公共字典、配置、热点详情、允许陈旧的报表,以及满足身份隔离和权限变化要求的用户资料。一个接口包含多步读取时,分别评估各步骤。
68
+
69
+ **不使用缓存替代**:库存扣减、余额校验、授权决定等当次权威判断,以及事务型写入。历史记录和统计展示可单独评估;参数不重复、复用收益低的查询通常不加。
70
+
71
+ ## Quick Reference
72
+
73
+ | 场景 | 用法 |
74
+ |------|------|
75
+ | 单次读缓存、未命中回源 | `get` → 未命中时读取源数据 → `set`;需并发合并时用 `wrap` |
76
+ | 公共配置、字典、热点数据 | 使用平台共享缓存;`wrap` 仅合并单进程内对同一 Key 的并发读取 |
77
+ | 外部 HTTP / 多维表格等插件 / AI 模型调用结果 | 只有 Server 侧调用能用 `CACHE_MANAGER`,调用放在哪一侧按 plugin-guide 决定;Key 用规范化后的完整请求参数(AI 另加实例 id 与模型参数);只缓存成功的最终结果,超时、错误、流式过程不缓存;缓存响应数据,不缓存凭证;外部数据靠 TTL 发现变化,AI 结果仅在相同输入可复用、且不是「重新生成」流程时缓存 |
78
+ | 可缓存的 GET 返回值 | `CacheInterceptor` + `CacheTTL`;静态 Key 仅用于所有相关请求结果相同的场景 |
79
+ | 用户或租户维度的数据 | 先确认鉴权、身份隔离和权限变化要求,再选择手动缓存或受支持的动态 Key |
80
+ | 列表、筛选、分页、报表 | Key 纳入影响结果的身份与有效查询参数;估算容量并明确失效方式 |
81
+ | 反复读取不存在的资源 | 满足新数据可见性要求时缓存带类型标记的空结果;不缓存暂时故障 |
82
+ | 一个接口包含多步读取 | 分别评估公共数据、用户数据和计算结果,只缓存满足条件的部分 |
83
+ | 写后失效 | 业务数据提交成功后 `del` 相关 Key,并评估并发旧值回填 |
84
+ | TTL 单位 | 毫秒;同时遵循平台允许的范围 |
85
+
86
+ ## 三种使用姿势
87
+
88
+ ### A. 手动 `CACHE_MANAGER`(最常用,最灵活)
89
+
90
+ ```typescript
91
+ import { Inject, Injectable } from '@nestjs/common';
92
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
93
+ import type { Cache } from 'cache-manager';
94
+
95
+ @Injectable()
96
+ export class ProfileService {
97
+ constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {}
98
+
99
+ async getProfile(userId: string) {
100
+ const key = `profile:${userId}`;
101
+ const hit = await this.cache.get<Profile>(key);
102
+ if (hit !== null) return hit; // falsy 值(0/''/false)也是有效命中,别用 if (hit)
103
+
104
+ const fresh = await this.fetchFromRemote(userId);
105
+ await this.cache.set(key, fresh, 60_000); // TTL ms
106
+ return fresh;
107
+ }
108
+
109
+ async updateProfile(userId: string, patch: Partial<Profile>) {
110
+ const next = await this.saveToRemote(userId, patch);
111
+ await this.cache.del(`profile:${userId}`); // 写路径主动失效
112
+ return next;
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### B. `cache.wrap`(防击穿 / single-flight)
118
+
119
+ 对同一缓存实例和 Key,`cache.wrap` 会合并同一进程内重叠的读取:只有一个 loader 执行,其他请求等待结果。热点或冷启动出现并发回源时优先使用。
120
+
121
+ > **边界**:不同 FaaS 实例各自合并,`wrap` 不保证全局只回源一次。若源侧仍无法承受多实例并发,应根据平台已有能力选择进一步措施;不要假设 `CACHE_MANAGER` 自带分布式锁。
122
+
123
+ ```typescript
124
+ async getConfig(appApiName: string) {
125
+ return this.cache.wrap(
126
+ `app-config:${appApiName}`,
127
+ () => this.loadFromDB(appApiName),
128
+ 5 * 60_000, // TTL 5min
129
+ );
130
+ }
131
+ ```
132
+
133
+ ### C. `CacheInterceptor` + 装饰器(Controller 方法级)
134
+
135
+ 仅用于可缓存的 GET,返回值须为 JSON-safe,缓存命中不能跳过必要鉴权或方法内的权限检查。默认按 URL 生成 Key,不会自动包含登录身份。身份相关结果须满足前述隔离与权限变化要求,再选择手动缓存或动态 Key。
136
+
137
+ 当前 `@nestjs/cache-manager@3.1.3` 的 `@CacheKey` 支持字符串和 Key 工厂函数。字符串 Key 是固定值,仅适用于相关请求结果相同的场景;动态 Key 应包含影响结果的路由、查询与身份维度。无身份等额外差异时,可不写 `@CacheKey` 并沿用 URL Key。已有项目先确认版本支持;示例中的 URL 方案仍可使用。
138
+
139
+ ```typescript
140
+ import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
141
+
142
+ @Controller('apps')
143
+ @UseInterceptors(CacheInterceptor)
144
+ export class AppsController {
145
+ // 不写 @CacheKey:按 GET /apps/:apiName/meta 完整 URL 自动分 key
146
+ @Get(':apiName/meta')
147
+ @CacheTTL(60_000) // ms
148
+ getMeta(@Param('apiName') apiName: string) { /* ... */ }
149
+ }
150
+ ```
151
+
152
+ ## Key 命名约定
153
+
154
+ | 规则 | 例子 | 理由 |
155
+ |------|------|------|
156
+ | 业务前缀必需 | `profile:${userId}` | 避免多应用共用 Redis 时 key 冲突 |
157
+ | 冒号分层 | `app:${apiName}:config:${env}` | 便于排障时 SCAN |
158
+ | 参数拼接稳定且完整 | 筛选、排序、分页等有效参数按固定字段顺序编码;按需加入可信的身份维度 | 相同条件得到相同 Key,不同结果不能因遗漏参数而共用 Key |
159
+ | 禁用大对象作 key | 不要 `JSON.stringify(user)` 当 key | key 过长 / 特殊字符转义踩坑 |
160
+ | Key 长度硬上限 | 必须 ≤ 512 字符 | 平台校验,超过直接跳过缓存 |
161
+
162
+ ## 平台校验限制(硬门槛)
163
+
164
+ 以下是当前平台规定的 Key、TTL、数量和 Value 边界,业务代码按这些限制设计。缓存失败或拒写时,业务应使用正常回源结果;具体拒绝行为和 Trace 字段以部署版本的 SDK 与服务端契约为准。排障时核对实际错误,不仅凭本表推断原因。
165
+
166
+ | 限制项 | 边界 | 超限行为 | error_code |
167
+ |--------|------|----------|------------|
168
+ | 业务 key 为空 | 不能是空串 | 本次不用缓存,直接跑原逻辑 | `CACHE_KEY_INVALID` |
169
+ | 业务 key 长度 | ≤ 512 字符 | 本次不用缓存,直接跑原逻辑 | `CACHE_KEY_TOO_LONG` |
170
+ | TTL 范围 | **30 秒 - 24 小时**(30_000 - 86_400_000 ms) | 本次不用缓存,直接跑原逻辑 | `CACHE_TTL_OUT_OF_RANGE` |
171
+ | 单应用单环境 key 数量 | ≤ 10,000 个有效 key | 已存在的 key 可正常刷新;新 key 不再写入 | `CACHE_KEY_QUOTA_EXCEEDED` |
172
+ | 单 key value 大小 | 序列化后 ≤ 1 MB | 不写入缓存,原读取结果照常返回 | `CACHE_VALUE_TOO_LARGE` |
173
+
174
+ **写代码前的最小心智模型**:把这 5 条边界当作**写 key / 传 TTL / set value 前的心跳检查**——具体反例见文末 [Common Mistakes](#common-mistakes) 表格。
175
+
176
+ ## Fail-Open 语义(重要)
177
+
178
+ **缓存不可用不应该让业务请求失败**,平台已保证:
179
+
180
+ ```
181
+ cache.get(key) 场景
182
+ ├─ 缓存正常 + hit → 返回值
183
+ ├─ 缓存正常 + miss → 返回 null
184
+ ├─ 缓存后端不可达 → 返回 null(不抛异常)
185
+ └─ 短暂网络抖动 → 返回 null,业务侧当 miss 处理即可
186
+ ```
187
+
188
+ 业务代码永远只需要 `if (hit === null) { fetch fresh }`(**别用 `if (!hit)`**——缓存值为 `0` / `''` / `false` 时会被误判成 miss)。**不要包 try-catch**——反例见 [Common Mistakes](#common-mistakes)。
189
+
190
+ ## Common Mistakes
191
+
192
+ 按主题分组,涵盖 API 姿势、Key、TTL、Value、防御式代码 5 类。
193
+
194
+ ### Key 相关
195
+
196
+ | 错误 | 正确做法 |
197
+ |------|----------|
198
+ | Key 不加业务前缀 | 必须 `${业务域}:${resource}:${id}` 分层,避免多应用 / 多资源撞车 |
199
+ | 空字符串 / `null` / `undefined` 当 key | 平台校验空 key,本次跳过缓存(`CACHE_KEY_INVALID`);调用前先判空 |
200
+ | 用户输入直接拼 Key,或截断后作为 Key | 先规范化影响结果的完整参数,再计算摘要;不要用截断代替摘要。摘要只缩短 Key,不减少不同参数组合的数量,仍需评估平台配额 |
201
+ | 把大对象 / JSON / SQL 塞进 key | 长度爆炸 + 特殊字符转义踩坑;用结构化 id 或摘要 |
202
+ | Key 里放 traceId / timestamp / requestId | key 空间爆炸,撞 10,000 上限后新 key 不再写入(`CACHE_KEY_QUOTA_EXCEEDED`) |
203
+ | 多参数拼接顺序不稳定或遗漏有效参数 | 固定字段顺序,包含所有影响结果的参数;规范化不能改变查询含义,例如有顺序语义的数组不能随意排序 |
204
+
205
+ ### TTL 相关(v6 单位是**毫秒**)
206
+
207
+ | 错误 | 正确做法 |
208
+ |------|----------|
209
+ | TTL 单位误传为秒,例如用 `60` 表示 1 分钟 | `cache-manager` v5、v6 的 TTL 均以毫秒计,1 分钟应传 `60_000`。当前平台仍要求 TTL 在 `30_000 - 86_400_000` ms 内,`60` 不在允许范围内 |
210
+ | TTL 传 `0` 想表达永久 | v6 `0` = 不过期,但平台也拒;接近永久用 `24 * 3600_000`(24h 上限) |
211
+ | TTL 短于 30s | 平台拒(`CACHE_TTL_OUT_OF_RANGE`),本次不缓存;实际范围 `30_000 - 86_400_000` |
212
+
213
+ ### Value 相关
214
+
215
+ | 错误 | 正确做法 |
216
+ |------|----------|
217
+ | 直接缓存 Buffer / Date / 循环引用对象 | 转成 JSON-safe DTO,并明确缓存命中与回源返回相同的数据结构;日期可转为约定的字符串,二进制使用文件地址 |
218
+ | 缓存 `null` 表示“资源不存在” | 用带类型标记的结构,如 `{ kind: 'not-found' }`,与 miss 区分。只有允许延迟发现新数据时才缓存;TTL 遵循平台范围,超时和服务错误不能写成“不存在” |
219
+ | 单 value 接近或超过 1 MB | 先裁剪字段或按分页 Key 拆分;大文件用 CDN / 对象存储。分页 Key 仍需纳入查询维度并估算数量 |
220
+
221
+ ### API 姿势相关
222
+
223
+ | 错误 | 正确做法 |
224
+ |------|----------|
225
+ | 业务模块里再 `CacheModule.register()` / `registerAsync(...)` | 平台已全局注册,直接 `@Inject(CACHE_MANAGER)`;重复注册会创建独立内存 store,不走 Redis、不 fail-open |
226
+ | 用 `CacheInterceptor` 缓存写请求 | Interceptor 只适合幂等 GET;写请求用 A 方案,配 `cache.del(key)` |
227
+ | 不同路由、查询或身份结果共用一个静态 `@CacheKey` | 静态 Key 不会区分这些结果;无额外身份差异时可用 URL Key,否则使用包含必要维度的动态 Key 或手动缓存,并满足鉴权要求 |
228
+ | 高 QPS 热点用 A 方案而非 `wrap` | 击穿风险,冷启动 loader 会被并发触发 N 次;换 B 方案拿 single-flight(注意仍是单实例内去重) |
229
+ | 用 `cache.wrap` 包业务写入 | `wrap` 仅用于读取和回填。业务数据提交成功后,再 `cache.del(key)` 失效相关缓存;涉及详情和列表时分别确定相关 Key。删除缓存不能保证与并发读取强一致 |
230
+ | 把 `await cache.set()` 等同于业务数据提交,或默认它一定不等待写入 | 当前锁定 SDK 会等待底层 `set`;服务端写入可见性取决于后端契约。缓存不能作为业务提交依据;写后直接使用本次计算或保存得到的结果,不用立即 `get` 来证明业务成功 |
231
+
232
+ ### 授权 / 身份相关(可能导致越权)
233
+
234
+ | 错误 | 正确做法 |
235
+ |------|----------|
236
+ | 看到 `@CanRole` / `@Can` 就直接拒绝缓存 | 每次请求保留必要鉴权;按返回数据的共享范围评估,命中不能跳过方法内的权限检查 |
237
+ | 用默认 URL Key 缓存按当前身份过滤的结果 | URL 不能表达身份;需从可信请求上下文取得租户、用户等必要维度,隔离结果并满足权限变化要求 |
238
+ | 认为 Key 带 `tenantId` / `userId` 就足够安全 | 身份隔离不能解决权限撤销;无法确保旧结果仍有权访问时,不缓存该结果。Key 数量仍须遵循平台配额 |
239
+
240
+ ### 防御式代码
241
+
242
+ | 错误 | 正确做法 |
243
+ |------|----------|
244
+ | Try-catch 包 `cache.get` / `cache.set` | 平台已 fail-open,`get` 不抛、`set` 失败原逻辑照走;try-catch 会掩盖真实业务错误 |
245
+ | 手动检测缓存后端是否可达再决定 get | 直接调;miss / 后端挂了都返回 `null`,业务侧按 miss 处理 |
246
+
247
+ ## 排障速查
248
+
249
+ | 症状 | 优先怀疑 | 验证方法 |
250
+ |------|----------|----------|
251
+ | 命中率长期 0 | Key 里含请求 ID / 时间戳,每次都新 key | 打印 key 名,对齐同一业务参数应得同 key |
252
+ | 本地测缓存正常,线上不共享 / 重启就丢 | 业务模块里自己 `CacheModule.register()` 了,跑的是内存 store | 检查 `imports:` 是否含 `CacheModule.*`;删掉,直接 `@Inject(CACHE_MANAGER)` |
253
+ | Interceptor 不生效 | 忘 `@UseInterceptors(CacheInterceptor)` 或 Controller 没注册 | 在 loader 加日志,观察是否每次请求都被调 |
254
+ | TTL "不生效" | 传成秒、传 `0`、或超出 `30s - 24h` 平台范围 | Trace 里出现 `CACHE_TTL_OUT_OF_RANGE` 就是超范围 |
255
+ | 数据陈旧 | 写后未失效、删除失败、遗漏相关 Key,或旧的并发读取在删除后又回填 | 核对写入提交、删除和回填的先后顺序,检查缓存错误与 Key 覆盖范围;需要强一致的读取不要依赖缓存 |
256
+ | Trace 出现 `CACHE_KEY_TOO_LONG` | key 里塞了整段 SQL / 用户输入 / JSON | 用 hash 或结构化 id 替换 |
257
+ | Trace 出现 `CACHE_VALUE_TOO_LARGE` | 整表 / 大 list 直接缓存 | 分页缓存或投影裁剪,把单个 value 压到 1 MB 以内 |
258
+ | Trace 出现 `CACHE_KEY_QUOTA_EXCEEDED` | key 空间失控(含 traceId / timestamp / 用户输入) | 收敛 key 模板,用有限的枚举维度 |
259
+ | 并发压测下源侧被打爆 | 同一 Key 并发回源、Key 不稳定、缓存不可用或多实例同时冷启动 | 先核对 Key、命中与实例数;`wrap` 只合并单进程内并发,多实例压力需结合平台能力另行处理 |
260
+
261
+ ## 验证缓存行为
262
+
263
+ 按本次改动验证相关场景:命中时减少回源,未命中正常读取,`0` / `false` / 空字符串不被误判;影响结果的参数和身份维度正确隔离;写入后按约定失效;缓存不可用时正常回源且业务错误仍能暴露。
264
+
265
+ 涉及身份数据时,验证缓存命中不会跳过鉴权,并检查权限撤销后旧结果不可越权访问。涉及热点时区分单进程并发合并与多实例分别回源。无法运行的检查应说明原因,不将方案预期写成已验证结果。