From 0df14036cf21ac7b38001acd6d2d3458bd9e6344 Mon Sep 17 00:00:00 2001 From: toom1996 <23cm.cn@gmail.com> Date: Sat, 22 Aug 2026 01:51:42 +0800 Subject: [PATCH] UPDATE --- .env.example | 13 +- Dockerfile | 19 +- docker-compose.yml | 14 +- nginx.conf | 8 +- src/components/Index.astro | 97 +- src/components/RunwayLooks.astro | 37 +- src/components/RunwayLooksDetail.astro | 58 +- src/components/pages/articles.astro | 10 +- src/components/pages/brands.astro | 34 +- src/i18n/dictionary.ts | 11 +- src/layouts/Layout.astro | 4 +- src/lib/api.ts | 10 +- src/lib/data.ts | 17 +- src/lib/ssg.ts | 97 ++ .../en/{runway-looks => items}/[id].astro | 6 +- src/pages/en/shows.astro | 858 ------------------ 16 files changed, 270 insertions(+), 1023 deletions(-) create mode 100644 src/lib/ssg.ts rename src/pages/en/{runway-looks => items}/[id].astro (76%) delete mode 100644 src/pages/en/shows.astro diff --git a/.env.example b/.env.example index 5a91d60..f77dba8 100644 --- a/.env.example +++ b/.env.example @@ -9,7 +9,16 @@ DB_NAME=fashionadmin # ---- 后端 JWT 密钥(生产务必改为随机长字符串)---- JWT_SECRET=change-me-in-prod +# ---- 公开 ID 混淆盐值(生产务必改为随机串)---- +# 决定对外 id/brand_id 编码结果;变更会使线上旧链接全部失效,一旦上生产请固定。 +HASHID_SECRET=change-me-in-prod-hashid + +# ---- SSG 内部接口令牌(可选)---- +# SSG_TOKEN=some-long-random-token + # 说明: -# - 前端构建期通过 compose 的 build.args.API_SSR=http://localhost:8090 拉数据 # - 前端运行期走同源 /api(nginx 反代到 backend:8090),PUBLIC_API_BASE 在 Dockerfile 中置空 -# - backend 监听 8090;生产建议去掉 ports 暴露,仅由前端 nginx 反代访问 +# - 前端构建期(SSG)通过 compose 的 build.args.SSG_API=http://localhost:8091 拉数据, +# 指向后端独立的 SSG 内部端口(只绑宿主回环,nginx 不反代) +# - 若后端 SSG_TOKEN 非空,前端构建需传相同 SSG_TOKEN(compose 已对齐) +# - backend 监听 8090 对外 + 8091 对内;生产建议去掉 8090 的 ports 暴露 diff --git a/Dockerfile b/Dockerfile index 1d13c03..9eac3e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,23 @@ FROM node:22-alpine AS build WORKDIR /app -# 构建期 SSR 拉数据用的「后端内部地址」。 -# 在 docker-compose 里通过 build.args.API_SSR 传入(= http://localhost:8090, -# 配合 build.network: host,让前端构建容器能访问已启动的 backend 容器)。 -ARG API_SSR=http://localhost:8090 +# 构建期 SSG 拉数据用的「后端 SSG 内部地址」。 +# 在 docker-compose 里通过 build.args.SSG_API 传入(= http://localhost:8091, +# 配合 build.network: host,让前端构建容器能访问已启动的 backend 容器的 SSG 内部端口)。 +# 该端口只绑宿主回环(127.0.0.1:8091),nginx 绝不反代,因此 SSG 全量数据不会外泄。 +# SSG_API 与运行期 PUBLIC_API_BASE(8090)完全分离,二者指向不同端口。 +ARG SSG_API=http://localhost:8091 +ENV SSG_API=$SSG_API + +# 兼容旧命名:API_SSR 仍可作为 SSG 基址(lib/ssg.ts 优先读 SSG_API)。 +ARG API_SSR=http://localhost:8091 ENV API_SSR=$API_SSR +# SSG 内部端口的可选访问令牌(防御纵深)。仅构建期 node fetch 使用,且不带 PUBLIC_ 前缀, +# 故绝不会进入前端 bundle。置空时后端不校验(依赖回环绑定即可)。 +ARG SSG_TOKEN="" +ENV SSG_TOKEN=$SSG_TOKEN + # 运行期浏览器调用地址:置空 = 同源相对路径 /api(由下方 nginx 反代到 backend:8090) ENV PUBLIC_API_BASE="" diff --git a/docker-compose.yml b/docker-compose.yml index 0549bc1..7361943 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ # 生产部署 compose(放在前端仓库根,服务器结构见 DEPLOY.md) # 前端仓: /opt/fashion/test (本文件所在) -# 后端仓: /opt/fashion/admin/backend +# 后端仓: /opt/fashion/backend (注意:已对齐到当前活跃开发的后端,原 admin/backend 为旧扁平版) services: # ---- 数据库 ---- db: @@ -20,10 +20,13 @@ services: # ---- 后端 (Go) ---- backend: build: - context: ../admin/backend + context: ../backend restart: unless-stopped environment: SERVER_PORT: 8090 + SSG_PORT: 8091 # SSG 内部端口(见下 ports 绑定) + SSG_TOKEN: ${SSG_TOKEN:-} # 可选:非空则 SSG 接口需带 token(防御纵深) + HASHID_SECRET: ${HASHID_SECRET:-} # 公开 ID 混淆盐值:生产务必注入随机串(改值会使旧链接失效,谨慎变更) GIN_MODE: release JWT_SECRET: ${JWT_SECRET:-change-me-in-prod} DB_HOST: db @@ -32,7 +35,8 @@ services: DB_PASSWORD: ${DB_PASSWORD:-root} DB_NAME: ${DB_NAME:-fashionadmin} ports: - - "8090:8090" # 仅构建期前端拉数据用;生产可去掉,改由前端 nginx 反代 + - "8090:8090" # 对外公开 API(运行期由 nginx 反代);生产可去掉此发布,改由前端 nginx 直接反代 backend:8090 + - "127.0.0.1:8091:8091" # ⚠️ SSG 内部端口:只绑宿主回环,外部网络不可达;nginx 也绝不反代它 depends_on: db: condition: service_healthy @@ -47,7 +51,9 @@ services: build: context: . args: - API_SSR: http://localhost:8090 # 构建期 SSR 拉数据:配合 network: host 访问已起的 backend + SSG_API: http://localhost:8091 # 构建期 SSG 拉数据:指向 backend 的 SSG 内部端口(配合 network: host 访问回环绑定的 8091) + API_SSR: http://localhost:8091 # 兼容旧命名(效果同 SSG_API) + SSG_TOKEN: ${SSG_TOKEN:-} # 与 backend 的 SSG_TOKEN 保持一致;为空则构建期无需携带 token network: host restart: unless-stopped ports: diff --git a/nginx.conf b/nginx.conf index 1254516..b6a1028 100644 --- a/nginx.conf +++ b/nginx.conf @@ -10,7 +10,13 @@ server { try_files $uri $uri/ /index.html; } - # 反向代理后端 API:浏览器走同源 /api,由 nginx 转发到 backend 容器 + # 网关隔离:SSG 内部接口只存在于 backend 的 8091 端口(回环绑定,nginx 不反代)。 + # 若有人从公网域名探测 /api/ssg/*,此处直接 404,确保构建期全量数据绝不从公网出口。 + location /api/ssg/ { + return 404; + } + + # 反向代理后端 API:浏览器走同源 /api,由 nginx 转发到 backend 容器(仅 8090 公开端口) location /api/ { proxy_pass http://backend:8090/; proxy_set_header Host $host; diff --git a/src/components/Index.astro b/src/components/Index.astro index f55bc2c..f606121 100644 --- a/src/components/Index.astro +++ b/src/components/Index.astro @@ -1,37 +1,34 @@ --- import ComingSoon from "@/components/ComingSoon.astro" -import { getShows, seriesLabel } from "@/lib/data" +import { toAbs } from "@/lib/api" import { getI18n } from "@/i18n/utils" import { getRelativeLocaleUrl } from 'astro:i18n' - - -const shows = await getShows(20) +import { getSsgPopularBrands } from "@/lib/ssg" // 国际化:一行取当前语言 + 翻译函数 const { locale, t } = getI18n(Astro) -type GalleryItem = { - src: string; - brand: string; - articleId: number; - year: number; - title: string; - collection_type?: string +// 首页「热门品牌」区块:构建期(SSG 内部接口,server-only)拉取 20 个热门品牌, +// 封面/标题取自每个品牌代表走秀,直接烧入静态 HTML,运行期不再请求;后端不可达则静默留空(区块隐藏)。 +// 具体 base 地址 + token 拼接已在 @/lib/ssg 内封装。 +const popular = await getSsgPopularBrands(20) + +type GalleryItem = { + src: string + brand: string + articleId: string // 代表走秀 hashid 编码串(后端返回 string) + title: string } -const galleryItems: GalleryItem[] = [] -for (const s of shows) { - if (!s.img) continue - galleryItems.push({ - src: s.img, - brand: s.brand, - articleId: s.id, - year: s.year, - title: s.title, - collection_type: s.collection_type, - }) -} +const galleryItems: GalleryItem[] = popular + .filter((b) => b.cover && b.article_id) + .map((b) => ({ + src: toAbs(b.cover || ""), + brand: b.brand, + articleId: b.article_id, + title: b.title, + })) ---
- {error === "missing" ? lbl.missing : lbl.failed} + {error === "missing" ? t('No article ID specified') : t('Failed to load article. Please refresh or try later.')}
- ← {backLabel} + ← {t('Back to Index')}