}>
@@ -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