From 4940278ea54423b54def0c591145b68e56d7725f Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 7 Jul 2026 17:33:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(migrate):=20=E7=94=A8=E6=88=B7=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E8=BF=81=E5=88=B0=20pangolin.yanmeiai.com/user/=20+?= =?UTF-8?q?=20=E5=9F=9F=E5=90=8D=E9=85=8D=E7=BD=AE=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原独立子域 app.yanmeiai.com → 主站子路径 /user/(用户选停用旧域名): - 域名配置化(去硬编码):客户端 kWebUserCenterBaseUrl 收进 api_config.dart (dart-define 可覆盖);官网 site.ts、服务端 CORS_ORIGINS 本就是配置 - 用户中心 next.config basePath=/user;layout.tsx 的 /colors_and_type.css 手动 拼 basePath(public 根绝对资源不自动加前缀,否则 404) - CI 合并部署:compile-site + compile-usercenter + combine-site(用户中心并入 dist/user/ + _headers 按 /user/* 分域:官网严格 CSP,用户中心 unsafe-inline+ connect-src https)→ 单次部署 pangolin-site;删独立 pangolin-usercenter 部署 - 服务端 CORS 默认 origin app.yanmeiai.com → pangolin.yanmeiai.com(+ 测试) - 客户端 web_launch 走新址 → 随 client-v1.0.62;go test CORS 过、flutter analyze 净 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u --- .gitea/workflows/deploy-site.yml | 21 ++++++++-------- client/lib/services/api_config.dart | 8 +++++++ client/lib/services/web_launch.dart | 4 +--- scripts/ci/combine-site.sh | 36 ++++++++++++++++++++++++++++ scripts/ci/compile-usercenter.sh | 5 ++-- scripts/ci/deploy-usercenter.sh | 28 ---------------------- server/cmd/server/main.go | 2 +- server/internal/httpapi/cors.go | 4 +++- server/internal/httpapi/cors_test.go | 8 +++---- web/usercenter/app/layout.tsx | 6 ++++- web/usercenter/next.config.mjs | 4 ++++ web/website/src/config/site.ts | 2 +- 12 files changed, 76 insertions(+), 52 deletions(-) create mode 100755 scripts/ci/combine-site.sh delete mode 100755 scripts/ci/deploy-usercenter.sh diff --git a/.gitea/workflows/deploy-site.yml b/.gitea/workflows/deploy-site.yml index 201b5d0..756fa65 100644 --- a/.gitea/workflows/deploy-site.yml +++ b/.gitea/workflows/deploy-site.yml @@ -24,19 +24,18 @@ jobs: SITE_URL: https://pangolin.yanmeiai.com run: bash scripts/ci/compile-site.sh + # 用户中心(web/usercenter,basePath=/user,http 模式)构建 → out/。 + - name: Compile (用户中心 Next.js) + run: bash scripts/ci/compile-usercenter.sh + + # 合并:用户中心并入官网 dist/user/ + _headers 按 /user/* 分域(见脚本)。 + # 域名迁移:原独立子域 app.yanmeiai.com → 主站子路径 pangolin.yanmeiai.com/user/。 + - name: Combine (官网 + 用户中心) + run: bash scripts/ci/combine-site.sh + + # 单次部署合并产物到 pangolin-site:/ 官网、/user/ 用户中心。 - name: Deploy → Cloudflare Pages env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: bash scripts/ci/deploy-site.sh - - # 用户中心(web/usercenter → CF Pages pangolin-usercenter / app.yanmeiai.com)。 - # 与官网同一发版(site-v*),避免两处各自漂移。⚠️ 必须 http 模式构建(见脚本)。 - - name: Compile (用户中心 Next.js) - run: bash scripts/ci/compile-usercenter.sh - - - name: Deploy usercenter → Cloudflare Pages - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: bash scripts/ci/deploy-usercenter.sh diff --git a/client/lib/services/api_config.dart b/client/lib/services/api_config.dart index c292d97..16ac817 100644 --- a/client/lib/services/api_config.dart +++ b/client/lib/services/api_config.dart @@ -8,3 +8,11 @@ const String kApiBaseUrl = String.fromEnvironment( 'PANGOLIN_API_URL', defaultValue: 'https://api.yanmeiai.com', ); + +/// 用户中心(网页)基址(单源)。已从独立子域 app.yanmeiai.com 迁到主站子路径 +/// pangolin.yanmeiai.com/user/(旧子域已停用)。末尾不带斜杠;调用方自行拼 path。 +/// 本地联调可 --dart-define=PANGOLIN_USERCENTER_URL=http://127.0.0.1:3000 覆盖。 +const String kWebUserCenterBaseUrl = String.fromEnvironment( + 'PANGOLIN_USERCENTER_URL', + defaultValue: 'https://pangolin.yanmeiai.com/user', +); diff --git a/client/lib/services/web_launch.dart b/client/lib/services/web_launch.dart index 26645a8..d26c745 100644 --- a/client/lib/services/web_launch.dart +++ b/client/lib/services/web_launch.dart @@ -8,9 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; import '../state/account_providers.dart'; - -/// 用户中心(网页版)基址。 -const String kWebUserCenterBaseUrl = 'https://app.yanmeiai.com'; +import 'api_config.dart'; /// 打开用户中心网页版 [path](默认首页),尝试免登录(SSO 换票)。 Future openWebUserCenter(WidgetRef ref, {String path = '/'}) async { diff --git a/scripts/ci/combine-site.sh b/scripts/ci/combine-site.sh new file mode 100755 index 0000000..594fd06 --- /dev/null +++ b/scripts/ci/combine-site.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# combine-site.sh — 把用户中心(web/usercenter/out/)并入官网产物,供**单次** CF Pages +# 部署到 pangolin-site:官网在 /、用户中心在 /user/(域名迁移:原独立子域 +# app.yanmeiai.com → pangolin.yanmeiai.com/user/)。 +# +# 关键:两个 app 的 CSP 不同(官网用脚本哈希 + connect-src 'self';用户中心要 +# 'unsafe-inline' + connect-src https 调 api)。CF Pages 只读产物根 _headers,故把 +# 用户中心的 _headers 规则前缀重定到 /user/*,追加在官网规则之后 —— CF Pages 对同 +# 一路径「后写规则覆盖先写」,于是 /user/* 命中用户中心 CSP,其余走官网 CSP。 +# +# 由 compile-site.sh + compile-usercenter.sh 先产出 dist/ 与 out/;从 repo 根调用。 +set -euo pipefail + +DIST=web/website/dist +UC=web/usercenter/out + +[ -d "$DIST" ] || { echo "==> combine-site: $DIST 不存在(先跑 compile-site.sh)" >&2; exit 1; } +[ -d "$UC" ] || { echo "==> combine-site: $UC 不存在(先跑 compile-usercenter.sh)" >&2; exit 1; } + +# 1) 用户中心产物挂到 /user/。 +rm -rf "${DIST}/user" +mkdir -p "${DIST}/user" +cp -R "${UC}/." "${DIST}/user/" + +# 2) 合并 _headers:用户中心路径规则前缀 /user,追加到官网 _headers 之后。 +# 删掉并入产物里那份会被 CF 忽略的 /user/_headers,避免误导。 +rm -f "${DIST}/user/_headers" +{ + echo "" + echo "# ==== 用户中心(/user/*)独立 CSP,combine-site.sh 从 usercenter/_headers 重定 ====" + # 行首 '/'(路径规则)前缀 /user;'#' 注释与缩进的 header 行不动。 + sed -E 's#^/#/user/#' "${UC}/_headers" +} >> "${DIST}/_headers" + +echo "==> combine-site: done — 用户中心并入 ${DIST}/user/,_headers 已按 /user/* 分域合并" +ls -la "${DIST}/user" | head diff --git a/scripts/ci/compile-usercenter.sh b/scripts/ci/compile-usercenter.sh index b7d6202..4d4c7c7 100755 --- a/scripts/ci/compile-usercenter.sh +++ b/scripts/ci/compile-usercenter.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # compile-usercenter.sh — build the Next.js 用户中心 (web/usercenter) as a static -# export (output: web/usercenter/out/). Deployed to Cloudflare Pages -# (project pangolin-usercenter, 自定义域 app.yanmeiai.com) by deploy-usercenter.sh. +# export (output: web/usercenter/out/, basePath=/user)。由 combine-site.sh 并入官网 +# dist/user/,随官网一起部署到 CF Pages pangolin-site(pangolin.yanmeiai.com/user/)。 +# 域名迁移:原独立子域 app.yanmeiai.com 已停用。 # # ⚠️ 必须以「真实 API」模式构建:NEXT_PUBLIC_API_MODE=http。否则 lib/api/client.ts # 默认落到 mock(内存假数据),部署上去登录/订阅全是演示数据。API 域名是公开信息 diff --git a/scripts/ci/deploy-usercenter.sh b/scripts/ci/deploy-usercenter.sh deleted file mode 100755 index fc6f337..0000000 --- a/scripts/ci/deploy-usercenter.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# deploy-usercenter.sh — 部署 Next.js 用户中心 (web/usercenter/out/) 到 Cloudflare -# Pages(项目 pangolin-usercenter,自定义域 app.yanmeiai.com)。纯静态导出,全程 -# HTTPS,不落 VPS,与节点 :443(sing-box)无冲突。 -# -# 需环境变量: -# CLOUDFLARE_API_TOKEN 带 Account > Cloudflare Pages > Edit 权限的 CF token -# CLOUDFLARE_ACCOUNT_ID CF 账户 ID -# 由 compile-usercenter.sh 先产出 web/usercenter/out/;从 repo 根调用。 -set -euo pipefail - -if [ ! -d web/usercenter/out ]; then - echo "==> deploy-usercenter: web/usercenter/out/ 不存在 — 拒绝部署" >&2 - exit 1 -fi -if ! find web/usercenter/out -mindepth 1 -print -quit | grep -q .; then - echo "==> deploy-usercenter: web/usercenter/out/ 为空 — 拒绝部署" >&2 - exit 1 -fi - -: "${CLOUDFLARE_API_TOKEN:?deploy-usercenter: CLOUDFLARE_API_TOKEN 未设(需带 Pages:Edit)}" -: "${CLOUDFLARE_ACCOUNT_ID:?deploy-usercenter: CLOUDFLARE_ACCOUNT_ID 未设}" - -echo "==> deploy-usercenter: wrangler pages deploy → project pangolin-usercenter (branch main)" -npx --yes wrangler@4 pages deploy web/usercenter/out \ - --project-name=pangolin-usercenter --branch=main --commit-dirty=true - -echo "==> deploy-usercenter: done" diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 88933ce..9342c7e 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -123,7 +123,7 @@ func main() { r.Use(chimw.Logger) r.Use(chimw.Recoverer) r.Use(apierr.Middleware) - // CORS:Web 用户中心(app.yanmeiai.com)跨域调 /v1/*;原生端不受影响。 + // CORS:Web 用户中心(pangolin.yanmeiai.com/user)跨域调 /v1/*;原生端不受影响。 r.Use(httpapi.NewCORS()) r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { diff --git a/server/internal/httpapi/cors.go b/server/internal/httpapi/cors.go index a56745a..752f046 100644 --- a/server/internal/httpapi/cors.go +++ b/server/internal/httpapi/cors.go @@ -16,7 +16,9 @@ import ( func NewCORS() func(http.Handler) http.Handler { raw := os.Getenv("CORS_ORIGINS") if raw == "" { - raw = "https://app.yanmeiai.com,https://pangolin-usercenter.pages.dev" + // 用户中心已迁到主站子路径 pangolin.yanmeiai.com/user/,其 origin 即主站域名 + // (旧 app.yanmeiai.com 已停用)。pages.dev 为 CF Pages 直连兜底。 + raw = "https://pangolin.yanmeiai.com,https://pangolin-site.pages.dev" } allowed := map[string]bool{} for _, o := range strings.Split(raw, ",") { diff --git a/server/internal/httpapi/cors_test.go b/server/internal/httpapi/cors_test.go index 55fc407..78727f0 100644 --- a/server/internal/httpapi/cors_test.go +++ b/server/internal/httpapi/cors_test.go @@ -7,17 +7,17 @@ import ( ) func TestCORS(t *testing.T) { - t.Setenv("CORS_ORIGINS", "https://app.yanmeiai.com") + t.Setenv("CORS_ORIGINS", "https://pangolin.yanmeiai.com") mw := NewCORS() next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) h := mw(next) // 允许的 Origin:补 Allow-Origin,普通请求继续。 r := httptest.NewRequest("POST", "/v1/auth/login", nil) - r.Header.Set("Origin", "https://app.yanmeiai.com") + r.Header.Set("Origin", "https://pangolin.yanmeiai.com") w := httptest.NewRecorder() h.ServeHTTP(w, r) - if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.yanmeiai.com" { + if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://pangolin.yanmeiai.com" { t.Fatalf("allowed origin: want header, got %q", got) } if w.Code != 200 { @@ -26,7 +26,7 @@ func TestCORS(t *testing.T) { // OPTIONS 预检:204,不落到业务。 r = httptest.NewRequest("OPTIONS", "/v1/auth/login", nil) - r.Header.Set("Origin", "https://app.yanmeiai.com") + r.Header.Set("Origin", "https://pangolin.yanmeiai.com") w = httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusNoContent { diff --git a/web/usercenter/app/layout.tsx b/web/usercenter/app/layout.tsx index 032ad32..f16963a 100644 --- a/web/usercenter/app/layout.tsx +++ b/web/usercenter/app/layout.tsx @@ -12,13 +12,17 @@ export const viewport: Viewport = { initialScale: 1, }; +// basePath 前缀(与 next.config.mjs 同源)。public/ 的根绝对资源(colors_and_type.css) +// 不会被 Next 自动加 basePath,需手动拼,否则挂到 /user/ 下会 404。 +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '/user'; + export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {/* 设计令牌单一真相源,原样链入(SRI 由构建期注入) */} {/* eslint-disable-next-line @next/next/no-css-tags */} - + {children} diff --git a/web/usercenter/next.config.mjs b/web/usercenter/next.config.mjs index dc79e52..45fba95 100644 --- a/web/usercenter/next.config.mjs +++ b/web/usercenter/next.config.mjs @@ -5,6 +5,10 @@ const nextConfig = { output: 'export', reactStrictMode: true, trailingSlash: true, + // 用户中心从独立子域 app.yanmeiai.com 迁到主站子路径:与官网合并部署到同一 + // CF Pages(pangolin-site),挂在 pangolin.yanmeiai.com/user/。basePath 让所有 + // 路由/资源前缀带 /user;可用 NEXT_PUBLIC_BASE_PATH 覆盖(如本地 dev 置空)。 + basePath: process.env.NEXT_PUBLIC_BASE_PATH ?? '/user', // 静态导出无图片优化服务,关闭以保证产物无服务端依赖。 images: { unoptimized: true }, }; diff --git a/web/website/src/config/site.ts b/web/website/src/config/site.ts index 8990a28..0f4362f 100644 --- a/web/website/src/config/site.ts +++ b/web/website/src/config/site.ts @@ -21,7 +21,7 @@ export const SITE = { store: { label: 'shop.pangolin.vpn' }, /** Web 用户中心(web/usercenter,Next.js 静态导出 → CF Pages)。登录/订阅/设备管理入口。 */ - usercenter: 'https://app.yanmeiai.com', + usercenter: 'https://pangolin.yanmeiai.com/user/', /** * 客户端安装包直链。控制面 pangolin-server 通过 Cloudflare Tunnel 对外暴露