域名驱动多租户入驻:后台配置 + 前端解析
摘要 :线上租户以访问域名为准,机构与域名的绑定在后台维护;前端在启动阶段用当前域名请求 getByDomain 解析机构信息等,写入会话并在后续请求携带 机构信息。src/tenant 与构建变量承担可选白标定制,与域名租户上下文分层,不宜混写成「一租户一构建产物」。
一、先分清两件事
多租户常被说成「一套代码服务很多家机构」。在本项目里,建议拆成两层理解:
| 层次 | 回答的问题 | 谁配置 | 前端落点 |
|---|---|---|---|
| 业务租户(域名) | 当前访问的是哪家机构 | 后台维护域名与机构关系 | getByDomain → 机构信息 → 请求头 |
| 前端白标包(可选) | 页面品牌、路由、权限、主题是否与标准版不同 | 工程侧 src/tenant/*、构建变量 |
loadTenantConfig、@tenant 资源 |
入驻的主线是:后台开通机构并绑定域名 → 用户用该域名访问 → 前端解析机构上下文 → 业务接口按租户隔离。
createOrg.js、多租户构建脚本属于「新机构要深定制页面时」的工程补充,不能替代后台域名配置。
build/tenant-domain-map.js 租户身份在构建期由 VITE_TENANT_ID 参与白标构建;运行时「你是哪家机构」仍由域名 + 后台决定。
二、入驻在业务上包含什么
从「机构能对外服务」倒推,通常至少包括:
- 后台 :创建机构、分配
机构关键字段/appId、绑定访问域名(可含测试域、正式域)。 - 前端运行时 :用当前域名换机构信息,并把
机构关键字段带入后续 API。 - (可选)工程侧 :从
tenant-standard脚手架出新租户目录,改 Logo、协议页、路由覆盖、权限开关;独立租户写入independentTenants等。
课程、订单、钱包等数据的租户隔离在后端;前端职责是稳定带上租户上下文,避免串租户。
三、运行时主链路:域名 → 机构 → 请求头
3.1 用域名换机构
店铺/机构信息通过半登录接口按域名查询,开发环境可用 VITE_APP_DOMAIN 模拟线上域名:
php
export function getShopInfo(params) {
return request({
url: "/getByDomain",
method: "get",
params: {
hostname: window.location.hostname,
},
})
}
要点:
- 生产以
window.location.hostname为准。 - 域名与机构的映射在后台维护,前端不写死映射表。
- 接口路径带
semiLogin,与未登录也可访问的站点场景一致。
3.2 路由守卫里初始化店铺与 orgCode
应用启动后 initRouter 注册全局 beforeEach,在进页面前 await ensureShopInfo()。成功后将 机构关键字段 写入 sessionStorage(键与 configId 组合),并更新 Pinia:
864:876:d:/project/pro-a/src/router/index.js
try {
shopStore.setLoading(true);
const shopRes = await getShopInfo();
if (shopRes && shopRes.data) {
sessionStorage.setItem('机构关键字段_'+sessionStorage.getItem('configId'), shopRes.data['机构关键字段信息']);
let shopInfo = {
...shopRes.data
};
shopStore.setShopInfo(shopInfo);
}
ensureShopInfo 有缓存:Pinia 已有店铺信息则不再请求;失败时可回退 localStorage 中的 shopId。新机构验收时要覆盖「域名已配、接口 200、首屏守卫执行」与「域名未配、接口失败」两类路径。
3.3 请求拦截器:把租户上下文带给后端
request 拦截器从 Cookie、sessionStorage 取 token 与 机构关键字段,写入 Authorization、机构关键字段;并从租户配置取 x-app-id:
csharp
let token = Cookies.get('t_'+sessionStorage.getItem('configId'))
// ...
if (token) {
config.headers['Authorization'] = ''
}
let 机构关键字 = sessionStorage.getItem('机构关键字段_'+sessionStorage.getItem('configId')) || ''
if (机构关键字段) {
config.headers['机构关键字段'] = 机构关键字段值
}
// ...
const tenantStore = useTenantStore();
if (tenantStore.config && tenantStore.config.appInfo) {
config.headers['x-app-id'] = tenantStore.config.appInfo.appId;
}
configId 在 main.js 里由租户配置的 appInfo.appId 写入 sessionStorage,用于隔离不同应用下的 token、机构关键字段 键名。同一前端包服务多域名时,域名解析出的 机构关键字段 与 配置里的 appId 会共同构成请求上下文。
四、应用启动顺序(与白标配置并行)
main.js 中:创建应用 → initRouter(含上述守卫)→ initTenantConfig(站点 meta、主题、configId)→ initPermissions → 挂载。
ini
const router = await initRouter();
app.use(router);
const tenantConfig = await initTenantConfig(app);
app.config.globalProperties.tenantConfig = tenantConfig;
setGlobalConfig(tenantConfig);
sessionStorage.setItem('configId', tenantConfig?.appInfo?.appId || '');
await initPermissions();
域名租户 与白标配置 在启动阶段并行就绪:前者靠守卫拉机构,后者靠 loadTenantConfig 加载 src/tenant/<id>/config/index.js(构建期 VITE_TENANT_ID 决定加载哪一份)。缺配置文件会直接报错,与「域名是否已在后台绑定」是两类问题。
五、工程侧入驻:createOrg 与白标目录
需要新机构独立品牌资源、协议、路由 时,可用根目录 createOrg.js:创建 src/tenant/<tenantId>/,从 tenant-standard 复制图片、permissions.js、assets/public,生成 config/index.js;若选独立租户,写入 build/tenant-domain-map.js 的 independentTenants。
脚本不会 自动改 package.json 或后台域名,README 要求手动增加 dev:tenant-xxx / build:tenant-xxx,并在后台完成域名绑定。
租户路由通过 name 覆盖平台路由,并合并 beforeEnter,避免换组件时丢掉平台守卫(详见 router/index.js 中 initRouter 合并逻辑)。权限来自各租户 assets/js/permissions.js,由 hasPermission 做点路径判断。
六、开发 / 生产差异
| 场景 | 域名来源 | 注意 |
|---|---|---|
| 生产 | 用户访问的 hostname |
后台域名与机构一致 |
| 本地 | VITE_APP_DOMAIN 或本机 host |
模拟线上域名,否则 getByDomain 可能对不上 |
| 白标构建 | VITE_TENANT_ID |
决定打进产物的 config、静态资源、@tenant 别名 |
线上「多域名、一套前端」依赖后台域名表 + 前端 getByDomain ;深定制可再出多份带不同 VITE_TENANT_ID 的构建产物,二者可组合,但不是同一概念。
七、端到端流程(入驻视角)
八、验收清单(建议)
- 后台:机构、
机构关键字段、域名(含测试域)已保存且生效。 - 用该域名访问:
getByDomain返回预期机构名与机构关键字段。 - 任意业务请求:请求头含
机构关键字段(及约定的x-app-id)。 - 若走白标脚手架:
config/index.js、协议静态页、权限与构建脚本可本地/预发验证。 - 异常:未绑定域名、接口失败时,页面与接口错误可感知,避免静默串到默认机构。
九、小结
多租户入驻的主线是域名在后台配置、前端按域名解析机构并贯穿请求链 ;createOrg 与 src/tenant 解决的是白标与工程交付。写方案或专栏时应用「域名租户」与「白标包」两层表述,避免与「单包单租户构建」混为一谈。
支付、分账、宝付监管、独立商户结算属订单与资金域,可在后续篇章单独写;本篇只界定前端在「租户识别」上的边界。