From bc2909d6f2d1428adc62e0faf43d60386cf4baab Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 16 Aug 2026 16:08:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E6=89=8B=E7=AB=AF=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=E7=8B=AC=E7=AB=8B=E5=AD=90=E5=9F=9F=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.mac-docker.example | 3 +- .env.server.example | 1 + README.md | 3 +- .../src/config/env-documentation.test.ts | 2 + apps/frontend/package.json | 1 + apps/frontend/src/App.tsx | 67 +++++++++++++++---- apps/frontend/src/layouts/WorkerLayout.tsx | 20 +++--- apps/frontend/src/lib/http.ts | 8 +-- .../src/pages/worker/WorkerLoginPage.tsx | 4 +- .../src/pages/worker/WorkerProfilePage.tsx | 4 +- apps/frontend/src/utils/site.ts | 22 ++++++ apps/frontend/src/utils/worker-routes.ts | 15 +++++ apps/frontend/tests/worker-routes.test.ts | 20 ++++++ apps/frontend/vite.config.ts | 1 + deploy/caddy/Caddyfile | 44 +++++++----- deploy/docker/frontend.Dockerfile | 2 + deploy/ubuntu-deploy.sh | 4 +- docker-compose.dev.yml | 1 + docker-compose.yml | 2 + docs/部署启动与数据库重建.md | 1 + 20 files changed, 173 insertions(+), 52 deletions(-) create mode 100644 apps/frontend/src/utils/site.ts create mode 100644 apps/frontend/src/utils/worker-routes.ts create mode 100644 apps/frontend/tests/worker-routes.test.ts diff --git a/.env.mac-docker.example b/.env.mac-docker.example index 94694081..8a942e1f 100644 --- a/.env.mac-docker.example +++ b/.env.mac-docker.example @@ -1,9 +1,10 @@ # Mac Docker 本机联调模板。复制为 .env 后按需修改。 -# 如果使用 ngrok / cloudflared 等公网地址,只需要同步修改 APP_DOMAIN / CADDY_SITE_ADDR / CLAIM_BASE_URL。 +# 如果使用 ngrok / cloudflared 等公网地址,只需要同步修改 APP_DOMAIN / CADDY_SITE_ADDR / WORKER_SITE_ADDR / CLAIM_BASE_URL。 # App / Caddy APP_DOMAIN=localhost CADDY_SITE_ADDR=http://localhost +WORKER_SITE_ADDR=http://worker.localhost TZ=Asia/Shanghai BACKEND_PORT=3000 diff --git a/.env.server.example b/.env.server.example index 242463fa..21c62876 100644 --- a/.env.server.example +++ b/.env.server.example @@ -4,6 +4,7 @@ # App / Caddy APP_DOMAIN=order.khhao.com CADDY_SITE_ADDR=https://order.khhao.com +WORKER_SITE_ADDR=https://worker.khhao.com TZ=Asia/Shanghai BACKEND_PORT=3000 diff --git a/README.md b/README.md index ab0bea7a..a9383777 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,8 @@ cp .env.server.example .env | 变量 | 说明 | | --- | --- | -| `APP_DOMAIN` / `CADDY_SITE_ADDR` | 对外域名与 Caddy 监听地址 | +| `APP_DOMAIN` / `CADDY_SITE_ADDR` | 后台与领取页域名、Caddy 主站监听地址 | +| `WORKER_SITE_ADDR` | 打手端独立域名,如 `https://worker.khhao.com` | | `CLAIM_BASE_URL` | 领取页完整前缀,如 `https://域名/#/claim` | | `CLAIM_TOKEN_TTL_HOURS` | 领取链接有效期(小时);`0` 表示长期有效,正整数表示限时 | | `DATABASE_URL` | PostgreSQL 连接串(容器内主机名用 `postgres`) | diff --git a/apps/backend/src/config/env-documentation.test.ts b/apps/backend/src/config/env-documentation.test.ts index 9871ad22..0d1510ae 100644 --- a/apps/backend/src/config/env-documentation.test.ts +++ b/apps/backend/src/config/env-documentation.test.ts @@ -30,6 +30,8 @@ const DEPLOYMENT_ONLY_ENV_NAMES = [ 'TZ', 'VITE_API_TARGET', 'VITE_ALLOWED_HOSTS', + 'VITE_WORKER_SITE_URL', + 'WORKER_SITE_ADDR', ] const KNOWN_ENV_NAMES = new Set([ diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 81926951..a05c4c00 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "test": "node --experimental-strip-types --test tests/*.test.ts", "typecheck": "tsc -b --noEmit", "preview": "vite preview" }, diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index a2fd618a..65bc666a 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,9 +1,19 @@ -import { Suspense, lazy } from 'react' +import { Suspense, lazy, useEffect } from 'react' import { Spin } from 'antd' -import { HashRouter, Navigate, Outlet, Route, Routes, useLocation } from 'react-router' +import { + BrowserRouter, + HashRouter, + Navigate, + Outlet, + Route, + Routes, + useLocation, +} from 'react-router' import { getAdminRole, hasAdminSession } from '@/utils/admin-auth' +import { isWorkerSite, WORKER_SITE_ORIGIN } from '@/utils/site' import { hasWorkerSession } from '@/utils/worker-auth' +import { resolveLegacyWorkerPath } from '@/utils/worker-routes' import AdminLayout from '@/layouts/AdminLayout' import WorkerLayout from '@/layouts/WorkerLayout' @@ -63,13 +73,27 @@ function RequireWorker() { const location = useLocation() if (!hasWorkerSession()) { - return + return } return } export default function App() { + const legacyWorkerPath = resolveLegacyWorkerPath(window.location.hash) + + if (legacyWorkerPath) { + return + } + + if (isWorkerSite()) { + return + } + + return +} + +function AdminApplication() { return ( }> @@ -96,15 +120,6 @@ export default function App() { - } /> - }> - }> - } /> - } /> - } /> - } /> - - } /> } /> } /> @@ -114,6 +129,34 @@ export default function App() { ) } +function WorkerApplication() { + return ( + + }> + + } /> + }> + }> + } /> + } /> + } /> + + + } /> + + + + ) +} + +function WorkerSiteRedirect({ path }: { path: string }) { + useEffect(() => { + window.location.replace(`${WORKER_SITE_ORIGIN}${path}`) + }, [path]) + + return +} + function RouteLoading() { return (
diff --git a/apps/frontend/src/layouts/WorkerLayout.tsx b/apps/frontend/src/layouts/WorkerLayout.tsx index 8522b72a..c1eea433 100644 --- a/apps/frontend/src/layouts/WorkerLayout.tsx +++ b/apps/frontend/src/layouts/WorkerLayout.tsx @@ -13,15 +13,15 @@ import { useIsMobile } from '@/utils/use-is-mobile' import { clearWorkerSession, getWorkerStatus, getWorkerUsername } from '@/utils/worker-auth' const menuItems: MenuProps['items'] = [ - { key: '/worker/hall', icon: , label: '抢单大厅' }, - { key: '/worker/orders', icon: , label: '我的订单' }, - { key: '/worker/profile', icon: , label: '个人中心' }, + { key: '/hall', icon: , label: '抢单大厅' }, + { key: '/orders', icon: , label: '我的订单' }, + { key: '/profile', icon: , label: '个人中心' }, ] const tabItems = [ - { key: '/worker/hall', icon: ShoppingOutlined, label: '抢单大厅' }, - { key: '/worker/orders', icon: ContainerOutlined, label: '我的订单' }, - { key: '/worker/profile', icon: UserOutlined, label: '个人中心' }, + { key: '/hall', icon: ShoppingOutlined, label: '抢单大厅' }, + { key: '/orders', icon: ContainerOutlined, label: '我的订单' }, + { key: '/profile', icon: UserOutlined, label: '个人中心' }, ] export default function WorkerLayout() { @@ -41,7 +41,7 @@ export default function WorkerLayout() { } finally { clearWorkerSession() message.success('已退出登录') - navigate('/worker/login', { replace: true }) + navigate('/', { replace: true }) } } @@ -144,9 +144,9 @@ export default function WorkerLayout() { } function resolveSelectedKey(pathname: string) { - if (pathname.startsWith('/worker/orders')) return '/worker/orders' - if (pathname.startsWith('/worker/profile')) return '/worker/profile' - return '/worker/hall' + if (pathname.startsWith('/orders')) return '/orders' + if (pathname.startsWith('/profile')) return '/profile' + return '/hall' } function formatWorkerStatus(status: string) { diff --git a/apps/frontend/src/lib/http.ts b/apps/frontend/src/lib/http.ts index b9df455a..f5cc9ddc 100644 --- a/apps/frontend/src/lib/http.ts +++ b/apps/frontend/src/lib/http.ts @@ -1,5 +1,6 @@ import axios from 'axios' import { clearAdminSession, getAdminToken } from '@/utils/admin-auth' +import { isWorkerSite } from '@/utils/site' import { clearWorkerSession, getWorkerToken } from '@/utils/worker-auth' export interface ApiEnvelope { @@ -84,11 +85,8 @@ http.interceptors.response.use( if (status === 401 && shouldClearWorkerSession(errorCode)) { clearWorkerSession() - if ( - window.location.hash.startsWith('#/worker') && - !window.location.hash.startsWith('#/worker/login') - ) { - window.location.hash = '#/worker/login' + if (isWorkerSite() && window.location.pathname !== '/') { + window.location.replace('/') } } } diff --git a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx index 1027bf76..3668d9bf 100644 --- a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx @@ -48,7 +48,7 @@ export default function WorkerLoginPage() { }, [countdown]) if (hasWorkerSession()) { - return + return } async function submitLogin(values: { username: string; password: string }) { @@ -57,7 +57,7 @@ export default function WorkerLoginPage() { const response = await loginWorker(values) setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker) message.success('登录成功') - navigate('/worker/hall', { replace: true }) + navigate('/hall', { replace: true }) } catch (error) { message.error(error instanceof Error ? error.message : '登录失败') } finally { diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx index 7d0ac20b..68149d2d 100644 --- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx @@ -172,7 +172,7 @@ export default function WorkerProfilePage() { } finally { clearWorkerSession() message.success('已退出登录') - navigate('/worker/login', { replace: true }) + navigate('/', { replace: true }) } } @@ -256,7 +256,7 @@ export default function WorkerProfilePage() { }) message.success('密码已修改,请重新登录') clearWorkerSession() - navigate('/worker/login', { replace: true }) + navigate('/', { replace: true }) } catch (error) { message.error(error instanceof Error ? error.message : '修改密码失败') } finally { diff --git a/apps/frontend/src/utils/site.ts b/apps/frontend/src/utils/site.ts new file mode 100644 index 00000000..11eca5b0 --- /dev/null +++ b/apps/frontend/src/utils/site.ts @@ -0,0 +1,22 @@ +const DEFAULT_WORKER_SITE_URL = 'https://worker.khhao.com' + +export const WORKER_SITE_ORIGIN = resolveWorkerSiteOrigin() + +export function isWorkerSite() { + const configuredHostname = new URL(WORKER_SITE_ORIGIN).hostname + + return ( + window.location.hostname === configuredHostname || + (import.meta.env.DEV && window.location.hostname === 'worker.localhost') + ) +} + +function resolveWorkerSiteOrigin() { + const configuredUrl = String(import.meta.env.VITE_WORKER_SITE_URL || '').trim() + + try { + return new URL(configuredUrl || DEFAULT_WORKER_SITE_URL).origin + } catch { + return DEFAULT_WORKER_SITE_URL + } +} diff --git a/apps/frontend/src/utils/worker-routes.ts b/apps/frontend/src/utils/worker-routes.ts new file mode 100644 index 00000000..3802f658 --- /dev/null +++ b/apps/frontend/src/utils/worker-routes.ts @@ -0,0 +1,15 @@ +export function resolveLegacyWorkerPath(hash: string) { + const value = hash.startsWith('#') ? hash.slice(1) : hash + const queryIndex = value.indexOf('?') + const pathname = queryIndex >= 0 ? value.slice(0, queryIndex) : value + const search = queryIndex >= 0 ? value.slice(queryIndex) : '' + + if (pathname === '/worker' || pathname === '/worker/' || pathname === '/worker/login') { + return `/${search}` + } + if (pathname === '/worker/hall') return `/hall${search}` + if (pathname === '/worker/orders') return `/orders${search}` + if (pathname === '/worker/profile') return `/profile${search}` + + return '' +} diff --git a/apps/frontend/tests/worker-routes.test.ts b/apps/frontend/tests/worker-routes.test.ts new file mode 100644 index 00000000..bdf6b1c1 --- /dev/null +++ b/apps/frontend/tests/worker-routes.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { resolveLegacyWorkerPath } from '../src/utils/worker-routes.ts' + +test('旧打手地址映射到子域名短路径', () => { + assert.equal(resolveLegacyWorkerPath('#/worker/login'), '/') + assert.equal(resolveLegacyWorkerPath('#/worker/hall'), '/hall') + assert.equal(resolveLegacyWorkerPath('#/worker/orders'), '/orders') + assert.equal(resolveLegacyWorkerPath('#/worker/profile'), '/profile') +}) + +test('旧打手地址保留查询参数', () => { + assert.equal(resolveLegacyWorkerPath('#/worker/orders?status=in_progress'), '/orders?status=in_progress') +}) + +test('非打手地址不触发跨域跳转', () => { + assert.equal(resolveLegacyWorkerPath('#/admin/dashboard'), '') + assert.equal(resolveLegacyWorkerPath('#/claim/example'), '') +}) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 6b3fae78..dd1bf436 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ // 隧道/自定义域名访问 dev server 时需加入白名单,否则 Vite 会拦截 Host allowedHosts: [ 'localhost', + '.localhost', '127.0.0.1', '221329.cc.cd', '.cc.cd', diff --git a/deploy/caddy/Caddyfile b/deploy/caddy/Caddyfile index 7093a052..4c34b0f0 100644 --- a/deploy/caddy/Caddyfile +++ b/deploy/caddy/Caddyfile @@ -1,24 +1,32 @@ +(order-site) { + route { + handle /api/* { + reverse_proxy backend:3000 + } + + handle /health* { + reverse_proxy backend:3000 + } + + handle /s/* { + reverse_proxy backend:3000 + } + + encode zstd gzip + root * /srv + try_files {path} /index.html + file_server + } +} + {$CADDY_SITE_ADDR:http://localhost} { - route { - handle /api/* { - reverse_proxy backend:3000 - } + import order-site +} - handle /health* { - reverse_proxy backend:3000 - } - - handle /s/* { - reverse_proxy backend:3000 - } - - encode zstd gzip - root * /srv - try_files {path} /index.html - file_server - } +{$WORKER_SITE_ADDR:https://worker.khhao.com} { + import order-site } live.khhao.com { - reverse_proxy douyu-login:8800 + reverse_proxy douyu-login:8800 } diff --git a/deploy/docker/frontend.Dockerfile b/deploy/docker/frontend.Dockerfile index d6a62a4b..e18d4f44 100644 --- a/deploy/docker/frontend.Dockerfile +++ b/deploy/docker/frontend.Dockerfile @@ -1,9 +1,11 @@ FROM docker.m.daocloud.io/library/node:22-bookworm AS builder ARG NPM_REGISTRY=https://registry.npmmirror.com +ARG VITE_WORKER_SITE_URL=https://worker.khhao.com ENV TZ=Asia/Shanghai ENV NPM_CONFIG_REGISTRY=${NPM_REGISTRY} +ENV VITE_WORKER_SITE_URL=${VITE_WORKER_SITE_URL} WORKDIR /app diff --git a/deploy/ubuntu-deploy.sh b/deploy/ubuntu-deploy.sh index b902abc8..b950a4af 100755 --- a/deploy/ubuntu-deploy.sh +++ b/deploy/ubuntu-deploy.sh @@ -119,9 +119,11 @@ validate_env_file() { [[ "${READY_TIMEOUT}" =~ ^[1-9][0-9]*$ ]] || fail "READY_TIMEOUT 必须是正整数秒数,当前值:${READY_TIMEOUT}" - local site_addr + local site_addr worker_site_addr site_addr="$(read_env_value CADDY_SITE_ADDR)" [[ -n "${site_addr}" ]] || fail ".env 缺少 CADDY_SITE_ADDR。" + worker_site_addr="$(read_env_value WORKER_SITE_ADDR)" + [[ -n "${worker_site_addr}" ]] || fail ".env 缺少 WORKER_SITE_ADDR。" HEALTH_URL="${site_addr%/}/health/ready" validate_database_url_for_compose diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e97d531b..d4ed89e5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -105,6 +105,7 @@ services: NPM_CONFIG_REGISTRY: https://registry.npmmirror.com VITE_API_TARGET: http://backend:3000 VITE_ALLOWED_HOSTS: ${VITE_ALLOWED_HOSTS:-localhost,127.0.0.1} + VITE_WORKER_SITE_URL: ${WORKER_SITE_ADDR:-http://worker.localhost} volumes: - ./apps/frontend:/app - frontend_node_modules:/app/node_modules diff --git a/docker-compose.yml b/docker-compose.yml index 84e639f7..5d78203a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,6 +91,7 @@ services: dockerfile: deploy/docker/frontend.Dockerfile args: APP_DOMAIN: ${APP_DOMAIN:-localhost} + VITE_WORKER_SITE_URL: ${WORKER_SITE_ADDR:-https://worker.khhao.com} ALPINE_MIRROR: ${ALPINE_MIRROR:-mirrors.aliyun.com} NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmmirror.com} restart: unless-stopped @@ -105,6 +106,7 @@ services: TZ: ${TZ:-Asia/Shanghai} APP_DOMAIN: ${APP_DOMAIN:-localhost} CADDY_SITE_ADDR: ${CADDY_SITE_ADDR:-http://localhost} + WORKER_SITE_ADDR: ${WORKER_SITE_ADDR:-https://worker.khhao.com} volumes: - caddy_data:/data - caddy_config:/config diff --git a/docs/部署启动与数据库重建.md b/docs/部署启动与数据库重建.md index d5b96bfe..843e16d6 100644 --- a/docs/部署启动与数据库重建.md +++ b/docs/部署启动与数据库重建.md @@ -146,6 +146,7 @@ cp .env.server.example .env ```text CADDY_SITE_ADDR +WORKER_SITE_ADDR CLAIM_BASE_URL POSTGRES_PASSWORD DATABASE_URL