151
.workbuddy/memory/2026-08-26.md
Normal file
151
.workbuddy/memory/2026-08-26.md
Normal file
@ -0,0 +1,151 @@
|
||||
# 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`,别再踩。
|
||||
@ -1,58 +1,37 @@
|
||||
# 项目长期记忆(Project MEMORY.md)
|
||||
|
||||
## 模块策略(重要)
|
||||
- 站点按模块逐个完善。当前只上线「发布会模块」:首页 index + 品牌索引 brands + 走秀列表 articles + 走秀详情 article。
|
||||
- **所有走秀页 `/shows`**(新建 2026-08-08):全局走秀档案(区别于 `/articles` 的品牌档案视图 ?brand_id)。基于 articles.astro,标题「All Runway」,保留系列/季节/品牌筛选 + 三视图(Grid/Magazine/Timeline),新增分页 Load More(page/total/busy,append 模式,后端 `?page=&size=30&with_images=5`)。首页 sec-more「全部走秀 →」指向 /shows。
|
||||
- 其余模块(Portfolio / Latest Projects / About / Contact / 登录注册)为「暂未上线」占位页:直接访问旧 URL 或点击导航旧链接均渲染 ComingSoon 占位;**导航(header)保持原样未改动**(用户 2026-08-07 明确「header 别动」),门禁完全靠占位页实现。
|
||||
- **首页 tabbar**(index.astro, id="tabbar"):仅「发布会(backstage)」上线(默认激活 + Live 徽标,承载 getShows(8) SSR 真实卡片);「灵感(shows)/社媒(events)/街拍(street)」标 `data-soon="true"` + Soon 灰徽标,点击仅显示 ComingSoon 占位、不再请求后端;「品牌」tab 仍为指向 /brands 的有效链接(属已上线模块,未屏蔽)。
|
||||
- 占位实现:src/components/ComingSoon.astro(编辑风,props: module / moduleZh / note;两个按钮「浏览走秀档案 →」「返回首页」)。portfolio / latest-projects / about / contact 仍为 ComingSoon 占位页。
|
||||
- **登录/注册(2026-08-25 更新)**:`/en/login`、`/en/register` 已做成真实编辑风页面(套 Layout + 新建 `src/components/pages/AuthForm.astro` 通用表单组件,`mode:'login'|'register'`,客户端 POST `/api/auth/login|register`,用 `saveSession` 写 `fa_token`/`fa_user`)。根 `/login`(src/pages/login.astro) 与 `/cn/login` 等**仍是 ComingSoon 占位**(未动,属当时范围外);导航 Login/Signup 在 en 语境下现指向真实的 /en/login、/en/register。
|
||||
- 站点按模块逐个完善。当前上线「发布会模块」:首页 index + 走秀列表/详情(RunwayLooks = 走秀档案页,articles/shows 页面已在 i18n 重构中重组)。
|
||||
- **品牌索引页已下线**(2026-08-26 确认):`src/pages/brands.astro` 及孤儿组件 `src/components/pages/brands.astro` 均已删除;品牌数据现在只通过走秀页侧栏 filter + 品牌弹窗呈现(热门 30 条走 SSG 接口,搜索/字母走运行时 `/api/v1/public/brands`)。
|
||||
- 其余模块(Portfolio / Latest Projects / About / Contact / 根路径登录注册)为 ComingSoon 占位页;**导航(header)保持原样未改动**(用户 2026-08-07 明确「header 别动」),门禁完全靠占位页实现。`/en/login`、`/en/register` 已是真实页面(`AuthForm.astro`,mode login/register)。
|
||||
|
||||
## 架构总览
|
||||
- 前端:/d/project/test/test — Astro SSG + Tailwind v4。构建 `PUBLIC_API_BASE=http://localhost:8090 npm run build`;预览 `python -m http.server` 托管 dist/。
|
||||
- 后端:/d/project/admin/backend — Go + Gin + GORM(MySQL),:8090。路由分后台 /api/* 与公开只读 /api/public/*。
|
||||
- 数据层:前端 src/lib/data.ts 统一从 /api/public/* 拉数据;后端不可达时回落内置示例。
|
||||
- 前端:**d:/project/frontend_v2** — Astro SSG + Tailwind v4 + Alpine.js。`npm run build`(SSG 取数走 ssgRequest,失败回落空/假数据不影响构建)。
|
||||
- 后端:**d:/project/backend_v2** — Go + Gin + GORM(MySQL),公开引擎 :8090(`/api/v1/public/*`、`/api/v1/auth/*`),SSG 内部引擎 :8091(仅 127.0.0.1,`/api/v1/ssg/*`,SSGToken 中间件,nginx 不反代)。
|
||||
- **SSG 分组现状(2026-08-26)**:只剩两个端点——`/api/v1/ssg/index/runway`(首页 runway 区块,`IndexService.Runway`,hotBrand 20 条)+ `/api/v1/ssg/brands/hot`(走秀页热门品牌,`BrandService.Hot`,固定 30 条、按图片总数热度顺序,零参数)。`/ssg/brands`(全量 A-Z)与 `/ssg/articles/ids` 均已删除。
|
||||
- **接口 i18n 约定**:后端按 `?locale=cn|en` 选列返回(pickLocale 缺翻译回落另一语言);前端取数统一复用带 locale 的原语(request/ssgRequest 等),**绝不裸写 fetch;语言以 URL 前缀为准**。
|
||||
- 走秀标题 `title_cn` 已规则回填(backend_v2 `scripts/backfill_title_cn/main.go`,幂等);`description_cn` 为空白属预期,中文页回落英文。
|
||||
|
||||
## 关键约定
|
||||
- 视觉语言:黑白极简编辑风(monospace + Georgia 衬线大标题 + 细线),HBX / 时尚档案调性。
|
||||
- 字体:全站走 Tailwind 体系(font-sans / font-mono / font-serif),禁止自写截断字体栈。新增页面复用项目字体变量。
|
||||
- CSS 写法:能用 @apply 就 @apply;`<style>` 顶部加 `@reference "tailwindcss";`。仅保留手写:@keyframes、渐变 shimmer、!important、双时长 transition。Tailwind v4 已移除零值类(mt-0 等),零值边距保持手写。
|
||||
- 导航(Layout.astro):**保持原样未改动**(Portfolio / Latest Projects / About / Contact / Login / Signup),含页脚 Categories + Color Studio Tools + About/Contact 链接。门禁仅由 6 个 ComingSoon 占位页承担,导航不删任何链接(用户要求 header 别动)。
|
||||
- 视觉语言:黑白极简编辑风(monospace + Georgia 衬线大标题 + 细线),HBX / 时尚档案调性。字体全走 Tailwind 体系,禁止自写字体栈。
|
||||
- **品牌名前端一律只显示英文**(用户偏好 2026-08-26:「前端品牌不要展示中文名称,标题可以留着」):api.ts 映射 `display = nameEn`;走秀标题/描述仍按 locale 选列。
|
||||
- **API 命名哲学(用户原则)**:「每一个地方尽可能不要通过参数来复用接口,尽量一个功能一个接口」。查询参数仅限「同一资源的列表筛选/分页」;跨资源/独立功能一律独立路径(如 `brands/hot` 而非 `?featured=1`)。待办:公开侧 `/api/v1/public/brands` 的 featured/keyword/letter 参数拆分、`/articles` 的 `brand_id` 拆 `brands/:id/shows`、资源命名 `shows` vs `articles`、`popular` vs `featured`(目标清单见 artifact `api_endpoints_catalog.md`)。
|
||||
- CSS:能 @apply 就 @apply;`<style>` 顶部 `@reference "tailwindcss";`。Tailwind v4 无零值类。
|
||||
- 导航(Layout.astro)保持原样,门禁仅由占位页承担。
|
||||
|
||||
## Layout 陷阱(重要,2026-08-12 踩坑)
|
||||
- **Layout.astro** 把 `<slot />` 包裹在 `<div class="flex-1 flex flex-col">`(flex column 容器)。
|
||||
- **禁用 `m-auto`** 作为页内根容器(`<div class="xxx m-auto">`):它在 flex column 容器中作为 flex item 的 cross-axis `margin: auto`,会触发 **shrink-to-fit**(content 决定宽度),导致:
|
||||
- 内容空/少时根容器收缩到极窄(骨架 24 张空 div → .showsroot 仅 626px / 主区 350px / 卡片 164px)。
|
||||
- 内容多时根容器被撑大(真实卡片有 img/文字 → 1042px 「看起来正常」)。
|
||||
- 同页面在不同内容状态下宽度剧烈变化 = 抖动。
|
||||
- **正确做法**:用 `w-full`(强制 100% 父容器,常配 `max-w-[xxx]`),或 `w-full mx-auto`(保留大屏居中)。
|
||||
- 适用范围:所有 Layout 内的页内根容器(包括 showsroot / blogs / about 等),**避免 `m-auto` 单独使用**。
|
||||
## Layout 陷阱(2026-08-12)
|
||||
- Layout 把 `<slot />` 包在 `flex-1 flex flex-col` 容器里;页内根容器**禁用 `m-auto`**(cross-axis margin:auto 触发 shrink-to-fit → 内容多少决定宽度、页面抖动)。用 `w-full`(可配 `max-w-[xxx]` + `mx-auto`)。
|
||||
|
||||
## Astro frontmatter 与 `<script>` 作用域隔离(通用坑)
|
||||
- **frontmatter 里的 `import` / 变量,在 `<script>`(客户端打包脚本)里拿不到**。两者是分别打包的:frontmatter 是服务端渲染期,`<script>` 是独立客户端 bundle。脚本里直接用 frontmatter 引的 `ROUTES` / `clientHref` 等,esbuild 不报错(当全局变量),但 **TS 语言服务报 `ts(2304) 找不到名称`**,运行时更是 `ReferenceError`。
|
||||
- **正确做法**:脚本里要用什么,就在该 `<script>` 顶部重新 `import`。例:`import { clientHref, ROUTES } from "@/lib/routes"`。
|
||||
- 排查方法:在 `<script>` 块里 `import` 行 grep,确认用到的标识符都已在脚本作用域内 import。本仓库曾在 RunwayLooks.astro、pages/shows.astro、pages/cn/shows.astro 三处脚本用了 `clientHref`/`ROUTES` 却只在 frontmatter import,统一补 import 修复(2026-08-24)。
|
||||
## Astro `<script>` 两大坑
|
||||
- **作用域隔离**:frontmatter 的 import/变量在打包型 `<script>` 里拿不到(运行时 ReferenceError);脚本里要用什么就在脚本顶部重新 import。
|
||||
- **不做模板插值**:打包型 `<script>` 里 `{t('...')}` 不会被执行 → `t is not defined`。预翻译用 `define:vars={{ hiLabel }}` 注入;模板区 `{t()}` 正常。
|
||||
- 打包后脚本在 `_astro/*.js` 而非内联;查 dist 产物用 search_content(ripgrep),PowerShell 无 `Select-String -Recurse`。
|
||||
|
||||
## i18n 脚本内翻译陷阱(2026-08-23 踩坑)
|
||||
- **Astro 对「打包型 `<script>`(module)不做 `{...}` 模板插值**。若在前端 `<script>` 里写 `slot.innerHTML = \`...{t('Hi')}...\``,构建后 `t('Hi')` 会原样留存在打包 JS 中 → 运行时 `t is not defined` 报错、脚本中断。
|
||||
- **正确做法**:在 frontmatter 用 `const hiLabel = t('Hi')` 预翻译,再用 `<script define:vars={{ hiLabel, logoutLabel }}>` 把字符串注入脚本,脚本内用 `${hiLabel}` 引用。define:vars 会把 `const hiLabel="..."` 注入到该 page 的脚本作用域(en 得英文、cn 得中文)。
|
||||
- 模板(`<template>` HTML 区,非 `<script>`)里的 `{t('...')}` 插值正常可用,仅 `<script>` 受限。
|
||||
- 验证坑:`Select-String -Recurse` 在 PowerShell 不存在(参数名不符);查 dist 产物改用 ripgrep 工具(search_content,递归默认开启)或 `Get-ChildItem -Recurse | Select-String`。
|
||||
- 注意:打包后 `<script>` 内容不在 HTML 内联,而在 `_astro/*.js`;用 `--` 类 UX 标志(如 `activeThemePalette` 控制的 Reset B&W Theme)在对应 prop 为空时不会渲染,静态 HTML 搜不到属正常。
|
||||
## 走秀数据分类(brand_runway)
|
||||
- 三维分类字段:`collection_type`(rtw/menswear/couture/resort/pre_fall)、`season`(spring/fall/null)、`season_code`(SS26/FW25…派生)。靠 title 规则回填(幂等,25,206 行)。
|
||||
- 公开列表 `?collection_type=&season=&season_code=&year=` 筛选;前端走秀页有系列/季节/年份 chips。
|
||||
- 地理/城市维度:用户明确暂不做。
|
||||
- 分布:rtw 15203 / menswear 4061 / couture 672 / resort 2894 / pre_fall 2376。
|
||||
|
||||
## 部署(Docker + Gitea Actions)
|
||||
- 仅 brands.astro 构建期 SSR 取数(用 API_SSR);其余页面运行时相对 /api 拉取(nginx 反代 backend:8090)。
|
||||
- 交付文件:前端 Dockerfile / nginx.conf / docker-compose.yml / deploy.sh / .gitea/workflows/deploy.yml / .env.example / DEPLOY.md;后端 Dockerfile。
|
||||
|
||||
## 走秀数据分类(brand_runway,已实现 2026-08-08)
|
||||
- **三维分类字段**(已加,靠 title 解析回填,无需外部数据):
|
||||
- `collection_type`:rtw / menswear / couture / resort / pre_fall
|
||||
- `season`:spring / fall / null(Resort/Pre-Fall 为单季,season=null)
|
||||
- `season_code`:SS26 / FW25 / RES26 / PF25(派生,用于排序/展示)
|
||||
- **回填**:main.go `backfillRunwayClassification()` 单条 `UPDATE ... CASE` 解析 title(Vogue 格式 `Spring/Fall 2016 Ready-to-Wear`、`Pre-Fall 2016`、`Resort 2017`、`Fall 2016 Couture`、`Spring 2005 Menswear`),仅处理 `collection_type=''` 行(幂等)。实测 25,206 行 ~338ms 完成。
|
||||
- **接口筛选**:`GET /api/public/articles` 新增 `?collection_type=&season=&season_code=&year=`,响应含三字段。
|
||||
- **前端筛选**:articles.astro 增加「系列类型 / 季节」两组 chips(`#typefilter` / `#seasonfilter`),点击经 `loadBackend()` 带参重拉。
|
||||
- **首页 feat 改竖版编辑拼图**(index.astro, 2026-08-08):原 4:3 横图 hero 改为「左侧大竖图(feat-main, aspect 3/4) + 右侧 3 张竖版 rail(feat-rail-item, grid-rows-3)」,**用同一场秀自己的 gallery(最多5张)做多图拼图**(避免横图裁切竖版),保留系列/季节 chip(feat-fill 渐变浮层)。`feat` 整块带 `data-ct` 供系列筛选隐藏。
|
||||
- **首页「查看更多」内联加载**(index.astro, 2026-08-08):网格下加 `#showmore` 按钮,JS 分页 `fetch ?page=2..&size=8&with_images=5` 追加到 `#showgrid`,带 `homeFType` 系列筛选参数;无更多显示「没有更多了」。系列筛选 IIFE 重构为 `seriesApply()` 暴露 `homeFType`。
|
||||
- **地理/城市维度:用户明确暂不做**(爬 Vogue 页不可靠;城市方案后续可用「品牌→所属时装周城市」映射表,Resort/Pre-Fall 归巡演桶)。
|
||||
- 分布实测:rtw 15203 / menswear 4061 / couture 672 / resort 2894 / pre_fall 2376;spring 9140 / fall 9049。
|
||||
- 交付文件:前端 Dockerfile / nginx.conf / docker-compose.yml / deploy.sh / .gitea/workflows/deploy.yml / .env.example / DEPLOY.md;后端 Dockerfile。nginx 拦截 `/api/v1/ssg/` 404;健康检查 `/api/v1/public/brands`。
|
||||
|
||||
Reference in New Issue
Block a user