fix(distribution): 修正分销后台入口地址并收口主站后台隐藏路径
- 新增前端 distributionAdmin 工具:优先 VITE_DISTRIBUTION_ADMIN_URL, 本地开发回退 5190,生产按 distribution.<主站域名> 推导 - 替换主站后台「打开独立分销后台」按钮与开通凭证中硬编码的 :5190 - 分销后台复制凭证改用 window.location.origin,去掉硬编码 http - Caddyfile 中 /manage-7x9k2p 仅主站域名放行,分站域名统一 404 - 构建/部署链路透传 VITE_DISTRIBUTION_ADMIN_URL 与 ADMIN_BASE_PATH
This commit is contained in:
@@ -23,6 +23,8 @@
|
|||||||
- 分销管理后台:`http://localhost:5190`(在主站后台“分销站点”开通站点并配置分销操作员登录)
|
- 分销管理后台:`http://localhost:5190`(在主站后台“分销站点”开通站点并配置分销操作员登录)
|
||||||
- 分销专属前台:`http://localhost:5191`(独立端口隔离,支持 Host 域名与 X-Distribution-Site 头识别站点)
|
- 分销专属前台:`http://localhost:5191`(独立端口隔离,支持 Host 域名与 X-Distribution-Site 头识别站点)
|
||||||
|
|
||||||
|
> 生产环境不使用上述端口:Caddy 按域名分流,分销管理后台固定为 `https://distribution.<CADDY_DOMAIN>`(见 `deploy/caddy/Caddyfile`)。主站后台的「打开独立分销后台」与开通凭证通过构建期 `VITE_DISTRIBUTION_ADMIN_URL` 注入该地址;未配置时按当前主站域名推导。
|
||||||
|
|
||||||
|
|
||||||
如需拆分调试,可手动执行:
|
如需拆分调试,可手动执行:
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ JWT_SECRET=change-to-a-long-random-secret
|
|||||||
|
|
||||||
# 前端后台页面入口;生产请改成不公开的单段路径,不要使用 /admin。
|
# 前端后台页面入口;生产请改成不公开的单段路径,不要使用 /admin。
|
||||||
VITE_ADMIN_BASE_PATH=/manage-7x9k2p
|
VITE_ADMIN_BASE_PATH=/manage-7x9k2p
|
||||||
|
# 分销(分站)管理后台地址,供主站后台「打开独立分销后台」按钮和开通凭证使用。
|
||||||
|
# 留空时前端按 https://distribution.<CADDY_DOMAIN> 推导,与 deploy/caddy/Caddyfile 一致;
|
||||||
|
# 只有分销后台改用其他域名时才需要显式填写。
|
||||||
|
# VITE_DISTRIBUTION_ADMIN_URL=https://distribution.hfb.221329.cc.cd
|
||||||
|
|
||||||
# 首次启动且管理员表为空时创建首个超级管理员;生产密码至少 12 位且包含字母和数字。
|
# 首次启动且管理员表为空时创建首个超级管理员;生产密码至少 12 位且包含字母和数字。
|
||||||
# 创建成功后建议从 .env 移除,后续管理员通过后台维护。
|
# 创建成功后建议从 .env 移除,后续管理员通过后台维护。
|
||||||
|
|||||||
@@ -63,6 +63,13 @@
|
|||||||
|
|
||||||
import public_api
|
import public_api
|
||||||
|
|
||||||
|
# 主站后台隐藏入口:storefront 片段同时服务主站和所有分销站点域名,
|
||||||
|
# 这里统一 404,仅主站域名在下方主站 block 用 @main_admin 放行。
|
||||||
|
@platform_admin path {$ADMIN_BASE_PATH:/manage-7x9k2p} {$ADMIN_BASE_PATH:/manage-7x9k2p}/*
|
||||||
|
handle @platform_admin {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
|
||||||
# 生产后台页面使用构建时配置的隐藏入口,旧 /admin 入口直接返回 404。
|
# 生产后台页面使用构建时配置的隐藏入口,旧 /admin 入口直接返回 404。
|
||||||
@legacy_admin path /admin /admin/*
|
@legacy_admin path /admin /admin/*
|
||||||
handle @legacy_admin {
|
handle @legacy_admin {
|
||||||
@@ -80,6 +87,14 @@
|
|||||||
# 主站与过渡子域名共享同一套配置:域名迁移期间新旧域名同时可访问,
|
# 主站与过渡子域名共享同一套配置:域名迁移期间新旧域名同时可访问,
|
||||||
# 避免 DNS 缓存未生效导致部分用户打不开。迁移完成后可移除 h5.{$CADDY_DOMAIN}。
|
# 避免 DNS 缓存未生效导致部分用户打不开。迁移完成后可移除 h5.{$CADDY_DOMAIN}。
|
||||||
{$CADDY_DOMAIN}, h5.{$CADDY_DOMAIN} {
|
{$CADDY_DOMAIN}, h5.{$CADDY_DOMAIN} {
|
||||||
|
# 主站后台隐藏入口只在本域名提供静态资源;分销站点域名由 storefront 片段统一 404。
|
||||||
|
@main_admin path {$ADMIN_BASE_PATH:/manage-7x9k2p} {$ADMIN_BASE_PATH:/manage-7x9k2p}/*
|
||||||
|
handle @main_admin {
|
||||||
|
root * /srv/dist
|
||||||
|
try_files {path} /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
import storefront
|
import storefront
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ FROM node:24-alpine AS build
|
|||||||
|
|
||||||
ARG VITE_ADMIN_BASE_PATH=/manage-7x9k2p
|
ARG VITE_ADMIN_BASE_PATH=/manage-7x9k2p
|
||||||
ENV VITE_ADMIN_BASE_PATH=${VITE_ADMIN_BASE_PATH}
|
ENV VITE_ADMIN_BASE_PATH=${VITE_ADMIN_BASE_PATH}
|
||||||
|
# 主站后台里「打开独立分销后台」按钮与开通凭证使用的分销后台地址。
|
||||||
|
# 留空时前端按 distribution.<当前主站域名> 推导,显式配置可覆盖(如独立后台子域改名)。
|
||||||
|
ARG VITE_DISTRIBUTION_ADMIN_URL=
|
||||||
|
ENV VITE_DISTRIBUTION_ADMIN_URL=${VITE_DISTRIBUTION_ADMIN_URL}
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ services:
|
|||||||
dockerfile: deploy/caddy/Dockerfile
|
dockerfile: deploy/caddy/Dockerfile
|
||||||
args:
|
args:
|
||||||
VITE_ADMIN_BASE_PATH: ${VITE_ADMIN_BASE_PATH:-/manage-7x9k2p}
|
VITE_ADMIN_BASE_PATH: ${VITE_ADMIN_BASE_PATH:-/manage-7x9k2p}
|
||||||
|
# 留空时前端按 distribution.<CADDY_DOMAIN> 推导,与 Caddyfile 的站点规则保持一致。
|
||||||
|
VITE_DISTRIBUTION_ADMIN_URL: ${VITE_DISTRIBUTION_ADMIN_URL:-}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
CADDY_DOMAIN: ${CADDY_DOMAIN:?请先导出 CADDY_DOMAIN,或使用 scripts/deploy-prod.sh 部署}
|
CADDY_DOMAIN: ${CADDY_DOMAIN:?请先导出 CADDY_DOMAIN,或使用 scripts/deploy-prod.sh 部署}
|
||||||
CADDY_EMAIL: ${CADDY_EMAIL:?请先导出 CADDY_EMAIL,或使用 scripts/deploy-prod.sh 部署}
|
CADDY_EMAIL: ${CADDY_EMAIL:?请先导出 CADDY_EMAIL,或使用 scripts/deploy-prod.sh 部署}
|
||||||
|
# Caddyfile 用它放行主站后台隐藏入口,并与前端构建期的 VITE_ADMIN_BASE_PATH 保持一致。
|
||||||
|
ADMIN_BASE_PATH: ${VITE_ADMIN_BASE_PATH:-/manage-7x9k2p}
|
||||||
# 选号网域名可选;未配置时 conf.d 为空,不影响主站
|
# 选号网域名可选;未配置时 conf.d 为空,不影响主站
|
||||||
CADDY_SHOW_DOMAIN: ${CADDY_SHOW_DOMAIN:-}
|
CADDY_SHOW_DOMAIN: ${CADDY_SHOW_DOMAIN:-}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean
|
|||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const text = `分销站点: ${successResult.site.site_name}\n后台地址: http://${window.location.host}/login\n用户名: ${successResult.member.username}`
|
const text = `分销站点: ${successResult.site.site_name}\n后台地址: ${window.location.origin}/login\n用户名: ${successResult.member.username}`
|
||||||
navigator.clipboard?.writeText(text)
|
navigator.clipboard?.writeText(text)
|
||||||
message.success('已复制分销商开通信息到剪贴板')
|
message.success('已复制分销商开通信息到剪贴板')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import { uploadAdminFile } from '@/shared/api/files'
|
|||||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
import { readError } from '@/shared/utils/error'
|
import { readError } from '@/shared/utils/error'
|
||||||
|
import {
|
||||||
|
distributionAdminBaseUrl,
|
||||||
|
distributionAdminLoginUrl,
|
||||||
|
} from '@/shared/utils/distributionAdmin'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const sites = ref<DistributionSite[]>([])
|
const sites = ref<DistributionSite[]>([])
|
||||||
@@ -423,13 +427,13 @@ async function handleOnboardSubmit() {
|
|||||||
|
|
||||||
function handleCopyInfo() {
|
function handleCopyInfo() {
|
||||||
if (!onboardResult.value) return
|
if (!onboardResult.value) return
|
||||||
const text = `分销站点:${onboardResult.value.site.site_name} (${onboardResult.value.site.site_code})\n分销域名:${onboardResult.value.site.domain}\n后台登录地址:http://${window.location.hostname}:5190/login\n管理员账号:${onboardResult.value.member.username}`
|
const text = `分销站点:${onboardResult.value.site.site_name} (${onboardResult.value.site.site_code})\n分销域名:${onboardResult.value.site.domain}\n后台登录地址:${distributionAdminLoginUrl()}\n管理员账号:${onboardResult.value.member.username}`
|
||||||
navigator.clipboard?.writeText(text)
|
navigator.clipboard?.writeText(text)
|
||||||
ElMessage.success('已复制分销商开通凭证到剪贴板')
|
ElMessage.success('已复制分销商开通凭证到剪贴板')
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDistributionAdmin() {
|
function openDistributionAdmin() {
|
||||||
window.open(`http://${window.location.hostname}:5190`, '_blank')
|
window.open(distributionAdminBaseUrl(), '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncingSiteId = ref<number | null>(null)
|
const syncingSiteId = ref<number | null>(null)
|
||||||
@@ -699,7 +703,7 @@ onMounted(() => {
|
|||||||
<el-tag type="success">{{ onboardResult.member.username }}</el-tag>
|
<el-tag type="success">{{ onboardResult.member.username }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="分销后台地址">
|
<el-descriptions-item label="分销后台地址">
|
||||||
<code>http://{{ windowHost }}:5190/login</code>
|
<code>{{ distributionAdminLoginUrl() }}</code>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="初始分成规则">
|
<el-descriptions-item label="初始分成规则">
|
||||||
四种模式均已按默认生效:收号 35% / 卖出 40% / 服务费 25%(本站自卖 75% +
|
四种模式均已按默认生效:收号 35% / 卖出 40% / 服务费 25%(本站自卖 75% +
|
||||||
@@ -925,16 +929,6 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
windowHost: typeof window !== 'undefined' ? window.location.hostname : 'localhost',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.distribution-sites-view {
|
.distribution-sites-view {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { distributionAdminBaseUrl, distributionAdminLoginUrl } from '../distributionAdmin'
|
||||||
|
|
||||||
|
function stubLocation(hostname: string, protocol = 'https:') {
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: { hostname, protocol } as Location,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('distributionAdmin 分销后台地址', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('优先使用 VITE_DISTRIBUTION_ADMIN_URL 并去掉结尾斜杠', () => {
|
||||||
|
vi.stubEnv('VITE_DISTRIBUTION_ADMIN_URL', ' https://dist.example.com/ ')
|
||||||
|
stubLocation('hao858.com')
|
||||||
|
expect(distributionAdminBaseUrl()).toBe('https://dist.example.com')
|
||||||
|
expect(distributionAdminLoginUrl()).toBe('https://dist.example.com/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('本地开发回退到 5190 独立端口', () => {
|
||||||
|
vi.stubEnv('VITE_DISTRIBUTION_ADMIN_URL', '')
|
||||||
|
stubLocation('localhost', 'http:')
|
||||||
|
expect(distributionAdminBaseUrl()).toBe('http://localhost:5190')
|
||||||
|
expect(distributionAdminLoginUrl()).toBe('http://localhost:5190/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('生产按主站域名推导 distribution 子域', () => {
|
||||||
|
vi.stubEnv('VITE_DISTRIBUTION_ADMIN_URL', '')
|
||||||
|
stubLocation('hao858.com')
|
||||||
|
expect(distributionAdminBaseUrl()).toBe('https://distribution.hao858.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('h5 过渡域名会去掉前缀后再推导', () => {
|
||||||
|
vi.stubEnv('VITE_DISTRIBUTION_ADMIN_URL', '')
|
||||||
|
stubLocation('h5.hao858.com')
|
||||||
|
expect(distributionAdminBaseUrl()).toBe('https://distribution.hao858.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('多级域名与 Caddyfile 的 distribution.<CADDY_DOMAIN> 保持一致', () => {
|
||||||
|
vi.stubEnv('VITE_DISTRIBUTION_ADMIN_URL', '')
|
||||||
|
stubLocation('hfb.221329.cc.cd')
|
||||||
|
expect(distributionAdminBaseUrl()).toBe('https://distribution.hfb.221329.cc.cd')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 分销(分站)管理后台入口地址。
|
||||||
|
*
|
||||||
|
* 解析优先级:
|
||||||
|
* 1. 构建期注入的 VITE_DISTRIBUTION_ADMIN_URL
|
||||||
|
* (deploy/docker-compose.prod.yml 会把它透传给 Caddy 镜像构建,推荐生产显式配置);
|
||||||
|
* 2. 本地开发保留独立端口(与 scripts/dev.sh 的 DEV_DISTRIBUTION_PORT 默认值一致);
|
||||||
|
* 3. 其余情况按 deploy/caddy/Caddyfile 的 `distribution.{$CADDY_DOMAIN}` 规则从当前主站域名推导,
|
||||||
|
* 主站可能同时挂 h5.{$CADDY_DOMAIN} 过渡域名,推导时先去掉 h5. 前缀。
|
||||||
|
*/
|
||||||
|
const DEV_DISTRIBUTION_ADMIN_PORT = 5190
|
||||||
|
|
||||||
|
function isLocalHostname(hostname: string): boolean {
|
||||||
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.localhost')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function distributionAdminBaseUrl(): string {
|
||||||
|
const configured = (import.meta.env.VITE_DISTRIBUTION_ADMIN_URL as string | undefined)?.trim()
|
||||||
|
if (configured) return configured.replace(/\/+$/, '')
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') return ''
|
||||||
|
|
||||||
|
const { hostname, protocol } = window.location
|
||||||
|
if (isLocalHostname(hostname)) {
|
||||||
|
return `http://${hostname}:${DEV_DISTRIBUTION_ADMIN_PORT}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootHostname = hostname.startsWith('h5.') ? hostname.slice('h5.'.length) : hostname
|
||||||
|
return `${protocol}//distribution.${rootHostname}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function distributionAdminLoginUrl(): string {
|
||||||
|
const base = distributionAdminBaseUrl()
|
||||||
|
return base ? `${base}/login` : ''
|
||||||
|
}
|
||||||
@@ -264,6 +264,14 @@ export_caddy_env() {
|
|||||||
CADDY_EMAIL="$(require_env CADDY_EMAIL)"
|
CADDY_EMAIL="$(require_env CADDY_EMAIL)"
|
||||||
CADDY_SHOW_DOMAIN="$(env_value CADDY_SHOW_DOMAIN || true)"
|
CADDY_SHOW_DOMAIN="$(env_value CADDY_SHOW_DOMAIN || true)"
|
||||||
export CADDY_SHOW_DOMAIN
|
export CADDY_SHOW_DOMAIN
|
||||||
|
|
||||||
|
# 分销后台地址:未在 backend/.env 显式配置时,按 Caddyfile 的 distribution.<CADDY_DOMAIN>
|
||||||
|
# 推导,供 Caddy 镜像构建期注入主站前端的「打开独立分销后台」入口与开通凭证。
|
||||||
|
VITE_DISTRIBUTION_ADMIN_URL="$(env_value VITE_DISTRIBUTION_ADMIN_URL || true)"
|
||||||
|
if [[ -z "${VITE_DISTRIBUTION_ADMIN_URL}" ]]; then
|
||||||
|
VITE_DISTRIBUTION_ADMIN_URL="https://distribution.${CADDY_DOMAIN}"
|
||||||
|
fi
|
||||||
|
export VITE_DISTRIBUTION_ADMIN_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
restore_show_dist_placeholders() {
|
restore_show_dist_placeholders() {
|
||||||
|
|||||||
Reference in New Issue
Block a user