Files
frontend_v2/.workbuddy/memory/2026-08-26.md
toom1996 eee2265e15
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-08-26 19:42:10 +08:00

152 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 2026-08-26 工作日志
## 新增前端 debug mode(本地联调开关)
- 用户要求:加一个 debug mode,启用后 SSR 用 8090、SSG 用 8091。
- 背景:本地 `npm run build` 时 SSG 走 `internal:8091`(Docker 网络别名)在本地解析不到,且 `astro dev` 时 `getBase()` 硬写线上域名 `seo.23cm.cn`,无法联调本地后端。本地后端实际 `8090`(公开 API) 与 `8091`(SSG 内部端口) 都在 localhost 上。
- 实现(`src/lib/api.ts`):
1. 新增 `isDebugMode()`:读 `import.meta.env.PUBLIC_DEBUG` 或 `process.env.PUBLIC_DEBUG`(`"1"/"true"` 为真),得到顶层常量 `DEBUG`(并 `export const DEBUG_MODE`)。
2. `getBase()`:`if (DEBUG) return "http://localhost:8090"` 置于 `import.meta.env.DEV` 之前(debug 盖掉线上域名)。
3. `publicBase()` / `ssrBase()`:`if (DEBUG) return "http://localhost:8090"`。
4. `BASE_API_URL`(fetchMe 用):`DEBUG ? "http://localhost:8090" : (BASE_API || "")`。
5. `ssgBase()`:`if (DEBUG) return "http://localhost:8091"`(否则 `BASE_API_SSG || http://localhost:8091`)。
- 效果:debug 开启后,SSR/运行期 → localhost:8090、SSG 构建期 → localhost:8091;关闭(默认)走生产配置(DEV→seo.23cm.cn / BASE_API / BASE_API_SSG)。
- 启用方式:构建/开发时设置环境变量 `PUBLIC_DEBUG=1`,如 `PUBLIC_DEBUG=1 npm run build` 或 `PUBLIC_DEBUG=1 npm run dev`。已在 `.env.example` 补 `#PUBLIC_DEBUG=1` 注释。
- 验证(PowerShell):
- `read_lints` api.ts 0 错误;默认构建 `npm run build` BUILD_EXIT=0(无回归)。
- SSG 开关实证:设 `BASE_API_SSG=http://127.0.0.1:9`(死端口) 构建——
- debug 开:`hot-chanel`(FAKE 假 id) 命中 0、真实 hashid `003xOZ9b` 命中 4 → 证明 SSG 强制走 localhost:8091 拉到真实数据;
- debug 关:`hot-chanel` 命中 3 → 证明回落 FAKE(死端口生效)。
- 因 getBase/publicBase/ssrBase/BASE_API_URL 共用同一 `DEBUG` 判定,SSG 侧已证 `DEBUG` 在构建期为真,故 SSR→8090 同步生效。
- ⚠️ SSR(8090) 当前是否运行取决于用户本地后端;debug 构建时若 8090 没跑,SSR 数据取数会 try/catch 回落样例(页面不崩)。SSG(8091) 本地实测可达。
## 前端接口请求接入 i18n(?locale= 透传,13:10)
- 用户疑问:接口返回数据没有语言标识、没法判断该渲染哪种语言。排查结论:**后端早已实现 i18n**,无需前端改契约。
- 后端现状(d:/project/backend_v2):DB 已加 `brand.name_en/name_cn`、`brand_runway.title_en/title_cn/description_en/description_cn`(scripts/i18n/main.go 迁移回填);`service/locale.go` 的 `pickLocale(en,cn,locale)` 按 locale 选列、缺翻译回落另一语言、出参仍是单字段(name/title/brand_name 不变);`handler` 的 `parseLocale(c)` 优先级:`?locale=cn|en` > `Accept-Language` 头 > 默认 `en`。公开接口(/api/public/articles、/articles/:id、/brands) 与 SSG 内部接口(/api/ssg/brands、/brands/popular) 全部读 locale。
- 前端改动(`src/lib/api.ts`):
1. 新增 `setApiLocale(locale)` + 模块级 `_ssrLocale` + `currentLocale()`(浏览器端用 `getLocaleFromUrl(window.location.pathname)` 从 URL path 推断 `/cn/`→cn,其余→en;服务端回落 `_ssrLocale` 或默认 `en`);`withLocale(params, override?)` 统一把 `?locale=` 追加进 query。
2. 所有请求原语统一带 locale:`request()`(getBrands/getArticlesRaw)、`fetchArticleById(id, base?, locale?)`、`getSsrArticle(id, locale?)`、`fetchArticlesList`(客户端)、`fetchColors`(客户端)、`fetchMe`、`ssgRequest`(构建期,token+locale 组合)。删除已内联的 `withToken` 死代码。
3. `currentLocale()` 客户端分支依赖 `astro:i18n` 的 `getLocaleByPath`(经 `@/i18n/utils.getLocaleFromUrl`),客户端 bundle 可用(已 build 通过验证)。
- 注入点:
- `src/layouts/Layout.astro` frontmatter 顶部 `setApiLocale(locale)`(每页经 Layout 渲染,覆盖所有子组件服务端取数)。
- `src/pages/shows.astro` 自身 frontmatter 在取数前 `setApiLocale(locale)`(该页在前置 frontmatter 就 `getBrands`/`getSsgPopularBrands`,早于 Layout,必须单独注入)。
- 4 个 SSR 详情页 `src/pages/{en,cn}/article.astro`、`src/pages/{en,cn}/item/[id].astro`:`getSsrArticle(id, Astro.currentLocale)` 显式传 locale(按需渲染并发安全,避免全局 `_ssrLocale` 串语言)。
- 验证:
- `read_lints` api.ts / Layout.astro / shows.astro 均 0 错误;默认 `npm run build` BUILD_EXIT=0。
- 后端契约实证:本地 8090 品牌接口 `?locale=cn`→`阿德达舍`、默认→`A Detacher`;本地 8091 SSG popular `?locale=cn`→`香奈儿 安普里奥·阿玛尼 克里斯汀·迪奥`、默认→`CHANEL Emporio Armani Christian Dior`。链路打通。
- 约定(重要,跨会话):**全站 API 请求一律带 `?locale=`,语言由 URL path 决定,不依赖浏览器 Accept-Language**(因站点语言由 URL 前缀决定,与浏览器语言无关)。新增任何取数函数都必须经 `request()`/`ssgRequest()`/`fetchArticlesList` 等已带 locale 的原语,不要再裸写 fetch。
## 后端 title_cn 规则回填(14:50)
- 用户批准:把 `brand_runway.title_cn` 从空补齐,让中文走秀页有中文标题(之前 `title_cn` 在迁移里被故意留空,中文页 `pickLocale` 回落英文 title)。
- 探查(脚本 scripts/diag_title,已删):title 本质是「季节/品类描述」(Vogue 格式),品牌名在 JOIN 的 brand_name 单独给;`collection_type`/`season`/`year` 早已解析回填。分布干净:无 year=0、无空 collection_type、无异常 season;合计 25,206 行。
- 方案:**纯规则生成,零外部翻译 API,可幂等重跑**。映射:
- resort→`{year} 度假系列`,pre_fall→`{year} 早秋系列`;
- rtw→`{year} {春夏|秋冬}成衣`,menswear→`{year} {春夏|秋冬}男装`,couture→`{year} {春夏|秋冬}高定`(无 season 的 rtw 回落 `{year} 成衣`)。
- 实现:新增 `d:/project/backend_v2/scripts/backfill_title_cn/main.go`(单条 `UPDATE ... CASE CONCAT(year,...)`,WHERE is_deleted=0,整表覆盖重跑无害);并把该 CASE 语句补进 `scripts/sql/002_i18n_columns.sql` 作为「第 5 步」评审底稿。
- 执行实证:本地 MySQL(root:root@127.0.0.1:3306/db) 跑通,updated rows = 25206,title_cn 非空率 0→100%。
- 端到端验证:临时 `go build` 启动 backend_v2(cmd/server) 于 8090,打 `/api/public/articles`——
- `?locale=cn`:`brand=芬迪 | title=2016 春夏成衣 / 2016 早秋系列 / 2016 秋冬男装 / 2016 秋冬成衣 / 2017 度假系列 / 2017 春夏男装`;
- 默认:`brand=FENDI | title=Spring 2016 Ready-to-Wear / Pre-Fall 2016 / ...`。
- 证明 `article_service.go` 的 `pickLocale(r.TitleEn, r.TitleCn, locale)` 已自动生效,前端 cn 页(articles/article.astro,已带 ?locale=)将直接显示中文标题,无需前端改动。验证后已停掉临时后端、释放 8090。
- 未做(范围外):`description_cn` 仍是空 → 中文页摘要/描述回落英文(描述是散文,规则无法生成,需真实翻译)。
## 品牌搜索「中文昵称搜不到」修复(15:10)
- 现象:中文界面搜「猿人头」找不到 A Bathing Ape。用户期望展示「A Bathing Ape(猿人头)」。
- 根因(已用一次性脚本 scripts/diag_brand 实查,已删):数据其实有 `name_cn="猿人头"`(brand id=3100,`show_name="A Bathing Ape/猿人头"`);后端 `brand_repository.go` 搜索本就匹配 `name_en OR name_cn`。真正卡点:**默认 `only_with_articles=1`** 先用子查询把「无走秀档案的品牌」全滤掉,而 brand 3100 的 `brand_runway` 档案数=0 → 关键词搜索落空。
- 修复(前端+后端):
1. 后端 `brand_repository.go` `brandFilterScope`:关键词搜索时跳过 only_with_articles 子查询(`if q.OnlyWithArticles && q.Keyword == "" && q.Letter != "OTHER"`)。语义:按名字找品牌本就不该被「必须有档案」限制。
2. 前端 `src/lib/api.ts` `getBrands`:关键词搜索时强制 `only_with_articles=0`(双保险,使未重启的后端也立即生效);并改映射返回 `nameCn` / `display`。
3. 后端 `dto.PublicBrand` 新增 `name_en` / `name_cn` 两字段(service 填充 `b.NameEn`/`b.NameCn`),因原 `name` 是 `pickLocale` 二选一、不含双语,前端拼不出「英文(中文)」。
4. 前端 `RunwayLooks.astro` 弹窗品牌名改用 `b.display || b.name`,`display`=中文语境下 `name_en + " (" + name_cn + ")"`,否则 `name_en`。
- 端到端验证:临时 `go build` 起 backend_v2 于 8090,请求 `/api/public/brands?keyword=猿人头&only_with_articles=0&locale=cn` → 返回 `{"name":"猿人头","name_en":"A Bathing Ape","name_cn":"猿人头","article_count":0}`,即搜得到且字段齐备。验证完毕已停后端、释放 8090、清理临时脚本与二进制。
- 注意:brand 3100 档案数=0 是数据现实(爬取遗漏/该品牌无走秀收录),搜索能找到它,但选定后走秀列表会空——属数据层问题,不在本次范围。全站 23,375 品牌仅 2,025 个(8.7%)有 `name_cn`。
## 品牌名不再展示中文(15:49)
- 用户反馈:「前端品牌还是不要展示中文名称了吧,感觉有点low… 标题什么的倒是可以留着」。即品牌名保持英文观感,走秀标题(中文页 title_cn)保留中文。
- 根因:之前品牌名也被后端 `pickLocale` 随 locale 本地化了(`article_service.go` 的 `BrandName`、`brand_service.go` 的 `Brand` 在 `?locale=cn` 时返回中文;前端 `getBrands` 的 `display` 又拼了「英文名 (中文名)」双语)。导致 `/cn/` 页文章卡片、精选品牌、RunwayLooks 弹窗都出现中文品牌名。
- 修改:
1. 后端 `d:/project/backend_v2/internal/service/article_service.go`:列表(`:54`)与详情(`:114`)的 `BrandName` 由 `pickLocale(En, Cn, locale)` 改为 `pickLocale(En, Cn, "en")`——强制优先英文,仅英文缺失时回落中文避免留空;`Title/Summary/Description` 维持按 locale。
2. 后端 `d:/project/backend_v2/internal/service/brand_service.go`:精选 `Brand`(`:87`) 同改为 `pickLocale(En, Cn, "en")`。
3. 前端 `d:/project/frontend_v2/src/lib/api.ts`:`getBrands` 的 `display` 改为 `display = nameEn`(去掉双语拼接),并删除已无用的 `const loc = currentLocale()`。
- 验证:后端 `go build ./internal/service/` 通过(exit 0);前端 `read_lints` api.ts 0 错误。品牌名全站统一英文;标题仍随 locale(中文页显示 title_cn)。
- 边界:`dto.PublicBrand.Name`(品牌列表 `name` 字段)仍为 `pickLocale` 二选一,但前端 `getBrands` 始终用 `nameEn` 覆盖,不显示中文,故未改该字段(避免扩大 API 契约改动面)。
## 后端接口命名复盘(15:59,仅分析未改动)
- 用户觉得后端接口命名乱,要求按现有网站设计给命名方案。已产出提案文档(artifact: api_naming_proposal.md),未改任何代码。
- 现状路由:公开 :8090=`/api/health`、`/api/public/articles`、`/api/public/articles/:id`、`/api/public/brands`、`/api/auth/{register,login,logout,me}`;构建期内部 :8091=`/api/ssg/brands`、`/api/ssg/brands/popular`、`/api/ssg/articles/ids`。
- 识别出的 6 个痛点:① 资源名前后端错位——后端 `articles`=走秀,前端页面已叫 `/shows`;② `public` vs `ssg` 把同类公开数据拆两世界,`ssg` 泄漏 Astro 构建机制;③ 同概念双命名 `popular`(SSG 子路径) vs `featured=1`(public 参数);④ 幽灵接口 `/api/public/colors` 后端无实现、前端靠降级兜底;⑤ 一个 `/public/articles` 端点有 4 个前端 getter(getArticlesRaw/getArticles/fetchArticlesList/getYears);⑥ 缺 `brands/:id` 详情与缺 `/api/v1` 版本前缀。
- 推荐方案:资源名对齐前端路由(`articles`→`shows`)、`ssg`→`internal`、`popular`统一为`featured`、`/api/v1` 前缀、抽 `ENDPOINTS` 常量表、合并 articles getter、删/实现 colors。待用户拍板 5 个问题后落地。
## 落地 /api/v1 前缀(16:13)
- 用户要求:先给所有接口加 `/api/v1` 前缀;并确立命名哲学「一个功能一个接口,尽量不通过参数复用接口」,让我把接口全集确定出来,他逐条确认。
- 改动:
- 后端 `internal/router/router.go`:`r.Group("/api")` → `/api/v1`(连带 health/public/*/auth/* 全部前缀)。
- 后端 `internal/router/ssg.go`:`/api/ssg` → `/api/v1/ssg`。
- 前端 `src/lib/api.ts`:`request()` 原语 `getBase()+"/api"` → `"/api/v1"`;`fetchArticleById`/`fetchArticlesList`/`fetchMe` 3 处手动 fetch 路径加 v1;`getSsgPopularBrands`/`getSsgBrands`/`getSsgArticleIds` 3 处字面量 `/api/ssg` → `/api/v1/ssg`;同步头部/分区注释。
- 前端 `src/components/pages/AuthForm.astro`:登录注册提交 `/api/auth/*` → `/api/v1/auth/*`。
- 部署:`nginx.conf` 404 拦截 `/api/ssg/` → `/api/v1/ssg/`;`docker-compose.yml` 健康检查 `…/api/public/brands` → `…/api/v1/public/brands`;`DEPLOY.md` 同步说明。
- 验证:后端 `go build ./...` 通过(BUILD_EXIT=0);前端 `read_lints` api.ts / AuthForm.astro 均 0 错误。
- 已产出目标接口全集清单(artifact: api_endpoints_catalog.md),按「一功能一接口」原则拆出 shows/brands/内部 各独立端点(A1–A6/B1–B5/C1/D1–D4/E1–E4/F1),并标注旧名作废映射。待用户确认 3 个总决命名(shows vs articles、internal vs ssg、featured vs popular)后逐条落地。
## 前端未用接口清理(16:20)
- 用户要求:先查前端实际没用到的接口,在后端去掉,再列精简清单确认。
- 排查结论(前端调用点全量核对):
- `/api/v1/ssg/articles/ids`:仅 `getSsgArticleIds` 定义,全站无任何 `getStaticPaths`/页面调用(详情页走 `getSsrArticle` 按需 SSR)→ 死端点。
- `/api/v1/auth/logout`:前端退出按钮只做 `clearSession()`+刷新,从不 POST 该接口 → 死端点。
- 附带死代码:前端 `getYears`、`getSsgArticleIds`、`fetchColors`(colors 后端本无实现,已不在 api.ts)三个 getter 从未被调用。
- 已删除:后端 `router.go` 的 `auth.POST("/logout")`、`ssg.go` 的 `ssg.GET("/articles/ids")`;前端 `api.ts` 的 `getYears` 与 `getSsgArticleIds` 函数。
- 验证:后端 `go build ./...` 通过(BUILD_EXIT=0);前端 `read_lints` api.ts 0 错误。
- 剩余后端接口(供用户确认):`/api/v1/health`(infra) + `/api/v1/public/articles`、`/api/v1/public/articles/:id`、`/api/v1/public/brands`、`/api/v1/auth/register|login|me`、`/api/v1/ssg/brands`、`/api/v1/ssg/brands/popular`,共 9 个功能端点。
- 注:后端 `AuthHandler.Logout` 与 `SSGHandler.ArticleIDs` 两个 handler 方法因路由删除已成为孤儿(Go 编译无误),可后续一并删除。
## 重构 SSG 首页接口为 /api/v1/ssg/index/runway(16:48)
- 用户要求:把首页 runway 的 SSG 数据接口从「按品牌热门」改为「首页(/index/)runway 区块多模块数据」,已命名为 `/api/v1/ssg/index/runway`,hotBrand 为其中一个模块;接口尽量写死、避免多余参数。
- 后端:
- `ssg.go`:`ssg.GET("/brands/popular", opt.SSG.Popular)` → `ssg.GET("/index/runway", opt.SSG.IndexRunway)`。
- `ssg_handler.go`:删 `Popular` 方法,新增 `IndexRunwayData{HotBrand []dto.PopularBrand json:"hotBrand"}` 结构体 + `IndexRunway` 方法;写死 `hotBrandLimit=20`、去掉 `limit` 查询参数、不读任何入参;调用 `h.brands.Popular(ctx, 20, parseLocale(c))`。`dto.PopularBrand` 字段 id/article_id/brand/cover/title 与前端 Index.astro 用法吻合。
- 前端:
- `api.ts`:`getSsgPopularBrands(limit)` → 新增 `getSsgIndexRunway(): Promise<{hotBrand: SsgPopularBrand[]}>`,返回 `j?.data ?? {hotBrand:[]}`(沿用 response.Data 的 data 信封)。
- `Index.astro`:改用 `getSsgIndexRunway().hotBrand` 作为 `popular`(galleryItems 映射 b.cover/article_id/brand/title 不变)。
- `RunwayLooks.astro`(/runway-looks 独立页,非首页):因该 SSG 接口已限定 /index/ 专属,改为复用侧栏已拉取的 featured 品牌 `sidebarBrands.slice(0,12)` 派生 hotBrands,移除对首页 SSG 接口的跨页耦合与 `getSsgPopularBrands` 导入。
- 删除旧 `getSsgPopularBrands`(全仓 0 引用)。
- 验证:`go build ./...` 通过(BUILD_EXIT=0);前端 `read_lints` api.ts / RunwayLooks.astro 均 0 错误;全仓 grep 无 `getSsgPopularBrands` / 旧路径残留。
- 设计约定(重要):`/api/v1/ssg/index/runway` 为「首页 runway 区块数据」聚合端点,按模块拆字段(当前仅 hotBrand);后续新增首页 runway 模块(如 featuredShows/latestShows)直接在 `IndexRunwayData` 加字段,前端按需读取,互不干扰。该端点零查询参数、数量写死。
## 删除孤儿 handler(16:32)
- 用户确认「没用到的都删掉」:删掉 `AuthHandler.Logout`(auth_handler.go)与 `SSGHandler.ArticleIDs`(ssg_handler.go)两个方法体及其注释,logout 不恢复。
- 全仓 grep 确认无测试/他处引用后删除;`go build ./...` 通过(BUILD_EXIT=0)。
- 余留:`SSGHandler.articles` 字段在 ArticleIDs 删除后不再被读取(Go 允许未用结构体字段,编译无碍)。如需极致干净,可后续移除该字段及 `NewSSGHandler` 的 articles 参数与 main 装配——属可选清理,未动以免扩大改动面。
## 抽出 IndexService(17:00)
- 用户质疑:`/api/v1/ssg/index/runway` 的实现挂在 `service.BrandService.Popular` 上(ssg_handler.go 调 `h.brands.Popular(...)`),首页区块数据不该借用品牌服务。要求新建专属 service。
- 命名:采用 `IndexService`(对应 URL 的 `/index/` 段「首页区块数据聚合服务」;备选 `HomeService` 偏口语被否)。文件 `internal/service/index_service.go`。
- 改动:
- 新建 `service/index_service.go`:`IndexService` 接口(`Runway(ctx, locale) (IndexRunwayData, error)`)+ `IndexRunwayData{HotBrand []dto.PopularBrand}`(从 handler 迁入)+ `indexService` 结构体(持 `repository.BrandRepository`)+ `NewIndexService(brands)`;`Runway` 写死 `hotBrandLimit=20`,复用 `brands.PopularWithCover` 并保持「品牌名强制英文 / 标题按 locale」映射。
- `handler/ssg_handler.go`:SSGHandler 增 `index service.IndexService` 字段、构造签名 `NewSSGHandler(brands, index)`;删 handler 内 `IndexRunwayData` 定义;`IndexRunway` 改为 `h.index.Runway(...)`;并顺手删掉自 ArticleIDs 删除后空置的 `articles` 字段与参数。
- `service/brand_service.go`:从接口与实现中移除 `Popular`(已无调用方,逻辑整体迁至 IndexService)。
- `cmd/server/main.go`:新增 `indexSvc = service.NewIndexService(brandRepo)`;`NewSSGHandler(brandSvc, articleSvc)` → `NewSSGHandler(brandSvc, indexSvc)`(`articleSvc` 仍被 `NewArticleHandler` 使用,未变未用)。
- 验证:`go build ./...` 通过(exit 0);`read_lints` ssg_handler.go 0 错误;全仓 grep 仅 `index_service.go` 含 `IndexRunwayData`/`Popular` 残留(repository.PopularWithCover 仍被新 service 复用)。
- 架构约定(稳定):首页区块数据由 `IndexService` 专属承载,未来 `/index/*` 各区块(runway/inspiration…)都归它;`IndexRunwayData` 按模块加字段扩展,handler 只做转发。`SSGHandler` 现仅依赖 `BrandService`(供 brands 索引)+ `IndexService`(供 index/runway)。
## SSG 品牌接口重做:/ssg/brands → /ssg/brands/hot(17:45)
- 背景:用户发现 `/api/v1/ssg/brands` 唯一消费者(brands.astro 页面→组件)在 i18n 重构中已成孤儿链 → 确认品牌索引页下线,死端点不改造、直接删。用户改为要求:新写一个 SSG 方法取热门前 30 品牌,替换 RunwayLooks 里的 getBrands;侧栏 filter 展示前 10,弹窗 HOT 展示全部;顺带清掉没用到的 JS 函数。
- 后端(backend_v2):
- `service/brand_service.go`:新增 `Hot(ctx, limit, locale)`——FeaturedIDs("images", limit) 取热度排名 → repo.List(按 id 集合) → **按热度排名重排**(repo.List 固定 name_en ASC,必须重排否则「前 10」不是真前 10 热)→ 映射。抽出公共 `toPublicBrands(items, counts, locale)` helper,List 复用。
- `handler/ssg_handler.go`:删 `Brands`(连带 strings/dto import),新增 `HotBrands`——固定 `hotBrandLimit=30` 写死,**零查询参数**(符合「一个功能一个接口」原则),`response.Data` 返回。
- `router/ssg.go`:`/brands` → `/brands/hot`。SSG 分组现仅 `brands/hot` + `index/runway`。
- 前端(frontend_v2):
- `api.ts`:删 `SsgBrand` + `getSsgBrands`;新增 `getSsgHotBrands(): Promise<BrandEntry[]>`(ssgRequest `/api/v1/ssg/brands/hot`,display=nameEn 保持英文观感)。`getBrands` 保留——弹窗搜索(modalSearch)/字母(modalPickLetter)运行时仍用。
- `RunwayLooks.astro`:frontmatter 数据源换 `getSsgHotBrands()`(30 条热度顺序),删 sidebarBrands/allBrands 数据岛(`showsbrands-data`);删死代码 `visibleBrandItems`/`hasMoreBrands`/`brandsExpanded`(模板零引用);pills findName 改 hotBrands + modalResults 兜底;modalLoadHot 去掉 allBrands.slice(0,50) 兜底直接展示 30 条;侧栏 filter 模板原有 `hotBrands.slice(0,10)` 不变。
- 删除孤儿组件 `src/components/pages/brands.astro`。
- 验证:`go build ./...` exit 0;前端 `npm run build` exit 0;read_lints api.ts / RunwayLooks.astro 均 0;全仓 grep 无 allBrands/getSsgBrands/SsgBrand/sidebarBrands/visibleBrandItems 残留。本地 :8091 跑的是旧进程(新路由 404),重启后端后生效。
- MEMORY.md 本次同步精简:品牌索引页下线、SSG 分组现状、前后端路径修正(frontend_v2 / backend_v2)。
## 修复 Hot 未按热度排名的 bug(19:15)
- 现象:`/api/v1/ssg/brands/hot` 返回 A-Z 字母序、article_count 全 0(用户发现「没按热度排」)。
- 根因:`Hot` 借道 `repo.List(q, restrictIDs)`,但仓储 `brandFilterScope` 里 **restrictIDs 仅在 `q.Featured==true` 时才拼进 WHERE**;Hot 构造的 BrandQuery 没设 Featured → `id IN (热门30)` 完全没生效,实际返回全量品牌字母序第一页;counts map 只含热门品牌 id → 查全空 → article_count 全 0。
- 排查路径:先写 `scripts/diag_hot/main.go` 复现 FeaturedIDs SQL 验证数据源正常(CHANEL 11106 图第一,梯度清晰,排除数据问题)→ 再 curl 8091 实测发现返回与热度排名完全对不上 → 定位到 scope 的隐藏耦合。
- 修复(符合「一个功能一个接口」哲学,不用 featured 假旗标复用 List):
- `repository/brand_repository.go`:接口+实现新增 `ListByIDs(ctx, ids)`(`WHERE is_deleted=0 AND id IN ?`,不保证顺序,调用方重排)。
- `service/brand_service.go` `Hot`:改用 `ListByIDs` 取行 → 按FeaturedIDs 顺序 map 重排 → 映射。删掉 BrandQuery 借道逻辑。
- 验证:`go build ./...` exit 0;重启 8091 dev 进程(go run 临时 exe,杀旧 PID + `Start-Process go run ./cmd/server` 后台拉起,日志 dev-ssg.log/err)后实测返回 CHANEL(151)→Emporio Armani(110)→Dior(147)→Valentino(107)→GUCCI(109)…与热度排名完全一致;8090 公开引擎 200 正常。
- 教训:repo.List 的 restrictIDs 是 featured 专属语义,其他场景要按 id 集合取行一律走 `ListByIDs`,别再踩。