Skip to content

feat(api-go): 完成 user-settings 只读域 Go 接管 - #18

Open
wei500L wants to merge 11 commits into
mainfrom
refactor/go-user-settings-read-takeover
Open

wei500L wants to merge 11 commits into
mainfrom
refactor/go-user-settings-read-takeover

Conversation

@wei500L

@wei500L wei500L commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Go-批3B:user-settings 六个只读 GET 统一 Go 接管

核心变更

六个只读 GET 在 pilot Go 模式(API_GO_USER_SETTINGS_READ_MODE=go)下全部由 Go 独立完成——不请求 NestJS、不读 JWT permissions claim、NestJS 停止后仍可用:

端点 之前 本轮
GET /api/user-settings/ui/onboarding Go(批3A 单端点) 统一 handler
GET /api/user-settings/ui/rss-reader Shadow Go 接管
GET /api/user-settings/ui/spacetime-timeline Shadow Go 接管
GET /api/user-settings/ui/war-map Legacy 真实 repository + normalization + Go 接管
GET /api/user-settings/ui/newsnow Legacy 真实 repository + normalization + Go 接管
GET /api/user-settings/ui/situation-monitor Legacy 三 key 聚合 + Go 接管

六个 PUT 仍全部由 NestJS 单写(exact path + GET method 白名单)。

实现要点

  • repository:8 个编译期固定 key(封闭集合,无任意 key API);五个单 key 端点共用同一条参数化查询;situation-monitor 一次聚合查询三 key(对齐 NestJS findMany)
  • normalization:War Map 完整移植 war-map-contract.ts(46 layer + legacy key 映射 + viewState clamp + bearing/pitch 归零);Situation Monitor 三段聚合;NewsNow 有序对象(Object.entries 顺序 + 200/32/300 上限 + JS Boolean 真值,流式有序解码而非 map 随机遍历)
  • 统一 handlerinternal/usersettingsread):六端点共享同一鉴权链装配与连接池,编译期绑定表;旧 internal/onboarding 删除(不并存)
  • 模式配置API_GO_USER_SETTINGS_READ_MODE=shadow|go,空=兼容旧行为(批3A 变量继续控制 onboarding);非法值启动失败
  • smoke 四阶段:Phase A 六 PUT(NestJS 单写,8 key 落库)→ Phase B shadow(executed 精确 +6,diffs 零)→ Phase C go 契约对比(六端点逐字段全等)→ Phase D 停止 NestJS 后六 GET 仍 200、PUT/未迁移 GET 502

边界(不变)

登录/refresh/logout/MFA/OIDC/机器令牌仍全部 NestJS;canary 未激活;默认生产入口未切换;本轮是远端真实栈验证,不是生产/预发流量验收。

无新增 Go 依赖(go.mod/go.sum 零变化);NestJS 契约零修改(OpenAPI/Auth Matrix/GraphQL 基线应零漂移)。

- repository:新增 WarMapKey/NewsnowKey/SituationMonitor{Monitors,Layout,Settings}Key
  编译期常量;FindWarMap/FindNewsnow 复用既有单 key 参数化查询;
  FindSituationMonitor 一次聚合查询三个固定 key(对齐 NestJS findMany,
  WHERE orgId+userId+key IN 三个编译期常量——无任意 key API)
- situationmonitor.go:三段聚合响应(updatedAt 各段 omitempty、无记录 null)
  + monitors(trim/截断/keywords 先截 30 再去重/color/location/最多 20)
  + layout(五断点/legacy lg 回退/取整与最小值/visibility 64 项有序上限)
  + settings(windowHours 6/24/72、scope、三个布尔回退)
- warmap.go:完整移植 packages/utils war-map-contract.ts——46 layer 固定
  struct 序列化、legacy key 映射、viewState clamp、bearing/pitch 归零、
  枚举回退、根对象 vs 嵌套 layerVisibility 差异
- newsnow.go:source id pattern/trim/去重/上限(200/32/300)、
  hideCrossSourceDuplicates 的 JS Boolean 真值、smart→personalized、
  clamp+round;columnOrders/sourceAffinity 用 orderedEntries 流式有序解码
  (Object.entries 顺序 + 前N项语义,map 随机遍历不可用)+ 自定义
  MarshalJSON 保持对象形态与字段顺序
- internal/usersettingsread:六个 GET 共享一个 handler——同一
  authhttp.Authenticator 鉴权链装配(Bearer→JWT 验签→Redis blacklist→
  MySQL membership/RBAC)、同一 usersettings.Repository、同一连接池;
  端点差异收敛为编译期绑定表(query+build 闭包对),不复制六份 handler
- 请求链固定:items.read 判定 → 固定 UserSetting 查询 → normalization →
  契约响应(Cache-Control: no-store、authhttp 错误契约、不追加换行)
- 旧 internal/onboarding 已被完全替代,删除(任务书:不得新旧并存);
  onboarding 端点改由统一 handler 的绑定表承载
- config:新变量 API_GO_USER_SETTINGS_READ_MODE=shadow|go(非法值启动
  失败);空=兼容旧行为(API_GO_ONBOARDING_MODE 继续控制 onboarding,
  rss/spacetime shadow,war-map/newsnow/situation-monitor legacy);
  go 模式要求 JWT_SECRET/DATABASE_URL/REDIS_HOST 齐备(与批3A 变量
  取或触发同一校验)
- legacyproxy.DefaultRules(onboardingMode, readMode):六个 user-settings
  GET 的模式由 readMode 统一决定(设置时优先级高于 onboardingMode);
  全部 exact path + GET method 白名单
- main.go:go 接管时装配 usersettingsread.NewHandler 并注册六个 path;
  shadow 单元表扩展到六端点(war-map/newsnow 单 key + situation-monitor
  三 key 聚合 executant);go 接管端点按路由表 ModeGo 集合从 shadow
  单元表过滤;healthz 自省改报 userSettingsRead.{mode,database}
- 新增唯一测试文件 batch3b_test.go(4 个顶层函数,未超过 5 个上限):
  Situation Monitor(三段聚合/部分记录/代表性规整)、War Map(默认/
  legacy key/clamp/嵌套差异)、NewsNow(有序对象/真值/clamp/去重/
  smart 归一 + 序列化形态)、situation visibility 64 项有序上限
- main_test.go:shadow identity gate 表格扩展六端点(新三端点 GET 执行
  一次/PUT 零执行);countingRepo 扩展六方法 + 聚合计数;新增
  TestUserSettingsReadModeRouting(readMode 三态路由表矩阵 + go 模式
  下 PUT/POST/HEAD/相似路径回落 legacy 的 exact-method 边界)
- config_test.go:read mode 三值语义 + go 模式依赖前置(合并表格)
- mysql_integration_test.go:扩展到 8 个固定 key——war-map/newsnow 真实
  读取与 normalization 抽查;situation-monitor 部分记录聚合(只有
  monitors)与全三 key 聚合、租户隔离
…立性证明

- Phase A:六个 PUT(含 situation-monitor 三段一次写入)经 api-go 由
  NestJS 单写;MySQL 直查 8 个固定 key 均存在;PUT 前后 shadow executed
  不变;相似路径 404 不误命中
- Phase B:API_GO_USER_SETTINGS_READ_MODE=shadow 启动——六个 GET 全部
  NestJS 响应 + Go 真实旁路差分(executed 精确 +7 含 healthz/live、
  diffs/dropped 六类零增量、inflight 归零;不扩大 ignore 集)
- Phase C:重启为 readMode=go——六端点与 NestJS 直连逐字段契约对比
 (status/Cache-Control/content-type/JSON/trace header、各端点读自己
  key、situation-monitor 三段 updatedAt 对应);Go 请求零 shadow 执行
- Phase D:停止 NestJS 并确认 4000 关闭——六个 GET 仍 200(Phase A
  持久化数据);代表性 PUT(war-map)502 证明写路径未迁入 Go;未迁移
  GET(/api/items)502 证明不伪装成功;容器 healthcheck 保持健康
- 共享鉴权链负向用例只保留代表性端点(onboarding)——批3A 已验证的
  JWT/RBAC 行为不重复铺矩阵
- compose api-go pilot:API_GO_USER_SETTINGS_READ_MODE=go(优先级高于
  批3A 变量,后者保留 go 兼容);.env.example/.env.sample 登记新变量
  与兼容优先级
- apps/api-go/README:统一接管章节(替换批3A 单端点叙述——不并存)、
  配置表新变量、四态路由表、四阶段 smoke 描述、回滚(shadow 或删除
  变量回兼容行为)
- 根 README:pilot 章节、流量去向、验证状态(四阶段 + 明确生产/预发
  流量未切换、不代表 NestJS 迁移完成)
- ADR §4.4:统一接管决策记录(绑定表非注册框架、8 key 封闭集合、
  有序对象语义、四阶段验收、边界如实登记)
- four-mode/contract-inventory/roadmap:六 GET=ModeGo、六 PUT=Legacy、
  API_GO_USER_SETTINGS_READ_MODE 优先级与兼容语义
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

CI 根因(run 34112159439,verify lint / user-settings-integration 同源):
NormalizeSituationMonitors 的循环变量是 any(asJSONObjectArray 返回
[]any),直接下标索引编译失败(cannot index record)。修复:循环内先
断言为 map[string]any(非对象跳过——与 NestJS !entry || typeof !==
object || Array.isArray 跳过语义一致)。

同时修正 smoke fixture 的 fallback 断言:Bad location 监控项缺失
id/createdAt 时 NestJS 回填 randomUUID()/Date.now()(非确定值)——
改用 @exists: 哨兵断言存在性与形态(id 以 sm- 开头、createdAt 为毫秒
时间戳),不是把随机字段加入 ignore 集。Phase A 与 Phase D 同步。
CI 根因(run 34112587502)三处独立:
1. warmap.go:warMapLayerCount 常量误写 46(WAR_MAP_LAYER_IDS 实际
   45 项)——[46]T 数组声明 + 45 个初始化器会零值填充第 46 个空
   layer id,defaultWarMapVisibility 遍历 46 个下标时 fields[45] 越界
   panic。修正常量并同步文档口径。
2. batch3b_test.go:monitors 数量断言错——fixture 4 条中 #2(name 空)
   与 #3(无 keywords)整条丢弃,保留 2 条(不是 3);Bad location
   的索引也相应是 [1] 不是 [2]。
3. mysql_integration_test.go:步骤 4 的「其他 key 不串读」占位插入用了
   ui:war-map:settings:v1——批3B 步骤 7 的真实 WarMapKey 插入撞
   orgId+userId+key 唯一键(1062)。占位改用非业务 key。
CI 根因(run 34115328067 verify Go tests)三处均为 fixture/断言错误:
1. cables 断言方向反了——if Cables 触发的是 want-true 场景(新 key
   true 应被采用),应为 !Cables 才报错。
2. situation settings autoRefresh:输入 0 是数字非布尔——NestJS
   normalizeBoolean 回退默认 true(不是 false)。
3. visibility 64 项上限:NestJS 对 key 只要求 trim 后非空(无 pattern
   检查)——'bad-key!' 也占位。fixture 67 个非空 key 前 64 个存活 =
   v001…v063 + bad-key!;v064 起丢弃(此前误以为非法 key 不占位)。
   同步修正误导性注释(fixture 无 not-a-bool 项)。
smoke run 34117253953 根因:Phase A 的 PUT 回显断言用 "settings" in doc
判别响应形态——situation-monitor 的响应顶层三段(monitors/layout/
settings)同样含顶层 settings 键,被误判为单 key 端点的 {settings:{…}}
包裹形态,monitors 断言拿到 None。改以 monitors 段判别(仅
situation-monitor 有)。
smoke run 34118687382 根因:Phase D 的 _partial 断言直接读响应顶层字段,
而 war-map 的 viewState/activePreset 等在 settings 段内(单 key 端点的
{settings:{…}} 包裹形态)——顶层取到 None。改为先断言 settings 非 null
(war-map 数据确实存在),再从 settings 段读 partial 字段。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant