--- todos: - id: "db-tenant-code" content: "新增 tenant_code 字段、唯一索引,更新 saimulti.sql 与存量回填脚本" status: pending - id: "server-generate-resolve" content: "SystemOrganizationLogic 实现码生成、add 自动写入、appInfo code 分支;LoginController 接收 tenant_code" status: pending - id: "tenant-vue-code-mode" content: "tenant-vue siteStore/App.vue/http/.env/types 从 app_id 模式切换为 tenant_code 模式" status: pending - id: "admin-vue-display" content: "admin-vue 机构列表与编辑弹窗展示 tenant_code(只读)及可选登录链接" status: pending - id: "docs-verify" content: "更新 README;跑 verify 脚本与手动登录联调" status: pending isProject: false --- # tenant_code 租户登录改造方案 ## 现状 当前租户端通过 [`tenant-vue/.env`](tenant-vue/.env) 的 `VITE_APP_MODE` 区分两种模式: | 模式 | 环境值 | 识别方式 | 入口示例 | |------|--------|----------|----------| | app_id | `appid` | URL `?app_id=1` | 可猜测自增 id | | domain | `domain` | `location.host` | 管理端配置域名 | 核心链路: ```mermaid sequenceDiagram participant Browser participant TenantVue participant Server participant DB Browser->>TenantVue: ?app_id=1 或域名访问 TenantVue->>Server: GET /saimulti/appInfo Server->>DB: 按 id 或 domain 查 sm_system_organization Server-->>TenantVue: 返回 id/title/logo... TenantVue->>Server: 后续请求 Header App-Id=机构id ``` 关键文件: - 前端站点识别:[`tenant-vue/src/store/modules/site.ts`](tenant-vue/src/store/modules/site.ts)、[`tenant-vue/src/App.vue`](tenant-vue/src/App.vue)、[`tenant-vue/src/utils/http/index.ts`](tenant-vue/src/utils/http/index.ts) - 后端入口:[`server/plugin/saimulti/app/controller/LoginController.php`](server/plugin/saimulti/app/controller/LoginController.php) → [`SystemOrganizationLogic::appInfo()`](server/plugin/saimulti/app/logic/system/SystemOrganizationLogic.php) - 租户隔离:全站仍依赖 `App-Id` 请求头([`TenantController`](server/plugin/saimulti/basic/TenantController.php)、[`TenantModel`](server/plugin/saimulti/basic/TenantModel.php)),**内部继续使用数字机构 id,不改为 tenant_code** ## 目标架构 将 `appid` 模式替换为 `code` 模式(domain 模式不动): ```mermaid sequenceDiagram participant Browser participant TenantVue participant Server participant DB Browser->>TenantVue: /auth/login?tenant_code=A3K9M2 TenantVue->>Server: GET /saimulti/appInfo?tenant_code=A3K9M2&mode=code Server->>DB: WHERE tenant_code=A3K9M2 Server-->>TenantVue: id, title, logo, tenant_code... Note over TenantVue: 持久化 tenant_code + 解析后的 org id TenantVue->>Server: POST /tenant/login, Header App-Id=1 ``` **安全收益**:对外 URL 只暴露 6 位随机码(大写+数字,排除 0/O、1/I/L),不再暴露可枚举的自增 id。 **tenant_code 获取方式**:与现有 app_id 一致,从浏览器 URL 查询参数自动读取([`getUrlQueryValue`](tenant-vue/src/store/modules/site.ts) 已支持 hash 路由),**登录页无需新增输入框**(避免与验证码字段 `code` 混淆)。 --- ## 1. 数据库变更 在 [`sm_system_organization`](db/saimulti.sql) 增加字段: ```sql ALTER TABLE `sm_system_organization` ADD COLUMN `tenant_code` varchar(6) NOT NULL COMMENT '租户访问码(6位随机)' AFTER `domain`, ADD UNIQUE INDEX `uk_tenant_code` (`tenant_code`); ``` 同步更新 [`db/saimulti.sql`](db/saimulti.sql) 建表语句与 seed 数据。 **存量机构回填**:新增独立迁移脚本(如 `db/migrate_tenant_code.sql` 或由一次性 PHP 命令生成),为已有记录生成不重复 6 位码;规则与新建一致。 --- ## 2. Server 后端 ### 2.1 Model [`SystemOrganization.php`](server/plugin/saimulti/app/model/system/SystemOrganization.php) 补充类 PHPDoc(表说明 + `@property string $tenant_code 租户访问码`)。 可选:新增 `findEnabledByTenantCode(string $tenantCode)` 查询方法。 ### 2.2 Logic — 核心改动 [`SystemOrganizationLogic.php`](server/plugin/saimulti/app/logic/system/SystemOrganizationLogic.php): - **`generateUniqueTenantCode()`**(private) - 字符集:`ABCDEFGHJKLMNPQRSTUVWXYZ23456789`(32 字符) - 循环生成 6 位 + 查重,直到唯一 - **`add($data)`** 重写/扩展:保存前若未传 `tenant_code`,自动调用生成器写入 - **`appInfo($identifier, $mode)`** 扩展分支: - `mode === 'domain'` → 现有逻辑不变 - `mode === 'code'` → `where('tenant_code', $identifier)` 查询 - 移除或废弃 `mode !== 'domain'` 时按自增 id 查询的路径(原 appid 模式) - **`appInfo` 返回字段**增加 `tenant_code`(供前端展示/持久化) ### 2.3 Controller [`LoginController::appInfo()`](server/plugin/saimulti/app/controller/LoginController.php): - 读取 `tenant_code` 查询参数(code 模式) - `mode=code` 时传入 `appInfo($tenantCode, 'code')` - domain 模式保持 `appid=host` + `mode=domain` **不改动** `App-Id` 头解析逻辑 — 租户 API 仍接收机构数字 id。 --- ## 3. tenant-vue 租户端 ### 3.1 环境配置 [`tenant-vue/.env`](tenant-vue/.env): ```env # 模式1: code 通过 URL ?tenant_code= 区分租户 # 模式2: domain 通过域名区分 VITE_APP_MODE = code ``` ### 3.2 siteStore 重构 [`tenant-vue/src/store/modules/site.ts`](tenant-vue/src/store/modules/site.ts): - 常量 `TENANT_CODE_QUERY_KEY = 'tenant_code'` - `SiteInfoParams` 改为 `{ tenantCode: string; mode: string }`(替代 `appid`) - `loadSiteInfo` 向 `/saimulti/appInfo` 传 `tenant_code` + `mode` - 解析成功后:`info.id` 用于 `App-Id` 头;`tenant_code` 持久化到 localStorage(刷新后无需 URL 参数也能识别租户) - domain 模式分支逻辑保持不变 ### 3.3 路由监听 [`tenant-vue/src/App.vue`](tenant-vue/src/App.vue): - `route.query.app_id` → `route.query.tenant_code` - 相应函数重命名(如 `getRouteTenantCode`) ### 3.4 HTTP 拦截器 [`tenant-vue/src/utils/http/index.ts`](tenant-vue/src/utils/http/index.ts): - `appMode === 'code'`:优先 `siteStore.info.id` 作为 `App-Id`;未加载时用已持久化的 org id - 移除 `appid` 模式分支 ### 3.5 类型定义 [`tenant-vue/src/types/api/api.d.ts`](tenant-vue/src/types/api/api.d.ts):`siteInfoResponse` 增加 `tenant_code?: string`。 --- ## 4. admin-vue 总后台 ### 4.1 机构列表 [`admin-vue/src/views/admin/panel/organization/index.vue`](admin-vue/src/views/admin/panel/organization/index.vue) 增加列: - `tenant_code` — 标签「租户码」 - 可选:展示租户登录链接模板 `租户端地址?tenant_code=xxx`(方便复制发给客户) ### 4.2 编辑弹窗 [`admin-vue/src/views/admin/panel/organization/modules/edit-dialog.vue`](admin-vue/src/views/admin/panel/organization/modules/edit-dialog.vue): - **新增**时:不展示 tenant_code(后端自动生成) - **编辑**时:只读展示 `tenant_code`(不可修改,保证链接稳定) - 不在表单提交中包含 tenant_code(防篡改) --- ## 5. 文档 更新 [`README.md`](README.md) 租户前端配置章节: - 模式 1:`code` + `?tenant_code=A3K9M2` - 模式 2:`domain`(不变) - 删除 app_id 相关说明 --- ## 6. 兼容与测试要点 | 场景 | 预期 | |------|------| | 新机构创建 | 自动生成唯一 6 位 tenant_code | | `?tenant_code=xxx` 访问 | 加载站点信息,登录成功 | | 刷新页面(无 URL 参数) | localStorage 中 tenant_code + org id 仍可识别 | | 错误/不存在 tenant_code | appInfo 报错「未找到该应用」 | | domain 模式部署 | 行为与改造前一致 | | 旧 `?app_id=1` 链接 | **不再支持**(按你的替换需求) | 手动验证路径: 1. 总后台新建机构 → 列表出现 tenant_code 2. 租户端 `http://localhost:16888/?tenant_code=XXXXXX` → Logo/标题正确 3. admin/admin 登录 → 租户 API 正常(菜单、用户信息) 4. domain 模式 `.env` 切换后仍可用 --- ## 改动范围摘要 | 层 | 文件 | 动作 | |----|------|------| | DB | `db/saimulti.sql` + 迁移脚本 | 加字段、唯一索引、存量回填 | | Server | `SystemOrganizationLogic.php` | 生成码、appInfo code 分支、add 钩子 | | Server | `LoginController.php` | 接收 tenant_code 参数 | | Server | `SystemOrganization.php` | PHPDoc | | tenant-vue | `site.ts`, `App.vue`, `http/index.ts`, `.env`, `api.d.ts` | appid → code | | admin-vue | `organization/index.vue`, `edit-dialog.vue` | 展示 tenant_code | | Docs | `README.md` | 更新模式说明 | **刻意不改**:`App-Id` 请求头机制、TenantModel 全局 scope、登录接口参数(验证码 `code` 保持不变)。