diff --git a/backend-java/src/main/java/com/competition/modules/sys/controller/SysTenantController.java b/backend-java/src/main/java/com/competition/modules/sys/controller/SysTenantController.java index 4b57d27..9d81ea0 100644 --- a/backend-java/src/main/java/com/competition/modules/sys/controller/SysTenantController.java +++ b/backend-java/src/main/java/com/competition/modules/sys/controller/SysTenantController.java @@ -33,8 +33,8 @@ public class SysTenantController { @PostMapping @RequirePermission("tenant:create") - @Operation(summary = "创建租户") - public Result create(@Valid @RequestBody CreateTenantDto dto) { + @Operation(summary = "创建租户(自动创建管理员账号、角色、权限)") + public Result> create(@Valid @RequestBody CreateTenantDto dto) { Long currentTenantId = SecurityUtil.getCurrentTenantId(); return Result.success(tenantService.createTenant(dto, currentTenantId)); } diff --git a/backend-java/src/main/java/com/competition/modules/sys/dto/CreateTenantDto.java b/backend-java/src/main/java/com/competition/modules/sys/dto/CreateTenantDto.java index ce3e817..efd3a16 100644 --- a/backend-java/src/main/java/com/competition/modules/sys/dto/CreateTenantDto.java +++ b/backend-java/src/main/java/com/competition/modules/sys/dto/CreateTenantDto.java @@ -29,4 +29,10 @@ public class CreateTenantDto { @Schema(description = "分配菜单 ID 列表") private List menuIds; + + @Schema(description = "管理员用户名(默认 admin)") + private String adminUsername; + + @Schema(description = "管理员密码(默认 admin@{tenantCode})") + private String adminPassword; } diff --git a/backend-java/src/main/java/com/competition/modules/sys/service/ISysTenantService.java b/backend-java/src/main/java/com/competition/modules/sys/service/ISysTenantService.java index 0bb0045..34dea37 100644 --- a/backend-java/src/main/java/com/competition/modules/sys/service/ISysTenantService.java +++ b/backend-java/src/main/java/com/competition/modules/sys/service/ISysTenantService.java @@ -10,7 +10,7 @@ import java.util.Map; public interface ISysTenantService extends IService { - SysTenant createTenant(CreateTenantDto dto, Long currentTenantId); + Map createTenant(CreateTenantDto dto, Long currentTenantId); PageResult> findAll(Long page, Long pageSize, String keyword, String tenantType); diff --git a/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysTenantServiceImpl.java b/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysTenantServiceImpl.java index 524138b..d504284 100644 --- a/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysTenantServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysTenantServiceImpl.java @@ -9,14 +9,14 @@ import com.competition.common.exception.BusinessException; import com.competition.common.result.PageResult; import com.competition.modules.sys.dto.CreateTenantDto; import com.competition.modules.sys.dto.UpdateTenantDto; -import com.competition.modules.sys.entity.SysTenant; -import com.competition.modules.sys.entity.SysUser; -import com.competition.modules.sys.mapper.SysTenantMapper; -import com.competition.modules.sys.mapper.SysUserMapper; +import com.competition.modules.sys.entity.*; +import com.competition.modules.sys.mapper.*; import com.competition.modules.sys.service.ISysTenantService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.*; @@ -30,10 +30,32 @@ public class SysTenantServiceImpl extends ServiceImpl INTERNAL_TENANT_CODES = Set.of("super", "public", "school", "teacher", "student", "judge"); + /** 广东省图租户编码,作为新租户权限模板 */ + private static final String TEMPLATE_TENANT_CODE = "gdlib"; + + /** 基础管理权限(模板租户可能缺少,需补充) */ + private static final List BASE_ADMIN_PERMISSIONS = List.of( + // {name, code, resource, action, description} + new String[]{"更新用户", "user:update", "user", "update", "允许更新用户信息"}, + new String[]{"删除用户", "user:delete", "user", "delete", "允许删除用户"}, + new String[]{"创建角色", "role:create", "role", "create", "允许创建新角色"}, + new String[]{"更新角色", "role:update", "role", "update", "允许更新角色信息"}, + new String[]{"删除角色", "role:delete", "role", "delete", "允许删除角色"}, + new String[]{"分配角色", "role:assign", "role", "assign", "允许给用户分配角色"}, + new String[]{"查看权限", "permission:read", "permission", "read", "允许查看权限列表"} + ); + private final SysUserMapper userMapper; + private final SysRoleMapper roleMapper; + private final SysPermissionMapper permissionMapper; + private final SysRolePermissionMapper rolePermissionMapper; + private final SysUserRoleMapper userRoleMapper; + private final SysTenantMenuMapper tenantMenuMapper; + private final PasswordEncoder passwordEncoder; @Override - public SysTenant createTenant(CreateTenantDto dto, Long currentTenantId) { + @Transactional + public Map createTenant(CreateTenantDto dto, Long currentTenantId) { log.info("开始创建租户,编码:{}", dto.getCode()); checkSuperTenant(currentTenantId); @@ -42,6 +64,7 @@ public class SysTenantServiceImpl extends ServiceImpl permissionIds = copyTemplatePermissions(tenantId); - log.info("租户创建成功,ID:{}", tenant.getId()); - return tenant; + // 3. 创建管理员角色 + SysRole adminRole = new SysRole(); + adminRole.setTenantId(tenantId); + adminRole.setName("机构管理员"); + adminRole.setCode("tenant_admin"); + adminRole.setDescription(tenant.getName() + "管理员角色,拥有该租户所有权限"); + roleMapper.insert(adminRole); + + // 4. 绑定角色-权限 + for (Long permId : permissionIds) { + SysRolePermission rp = new SysRolePermission(); + rp.setRoleId(adminRole.getId()); + rp.setPermissionId(permId); + rolePermissionMapper.insert(rp); + } + + // 5. 创建管理员用户 + String adminUsername = dto.getAdminUsername() != null && !dto.getAdminUsername().isBlank() + ? dto.getAdminUsername() : "admin"; + String adminPassword = dto.getAdminPassword() != null && !dto.getAdminPassword().isBlank() + ? dto.getAdminPassword() : "admin@" + dto.getCode(); + + SysUser adminUser = new SysUser(); + adminUser.setTenantId(tenantId); + adminUser.setUsername(adminUsername); + adminUser.setPassword(passwordEncoder.encode(adminPassword)); + adminUser.setNickname(tenant.getName() + "管理员"); + adminUser.setStatus("enabled"); + adminUser.setUserSource("admin_created"); + adminUser.setUserType("adult"); + userMapper.insert(adminUser); + + // 6. 绑定用户-角色 + SysUserRole ur = new SysUserRole(); + ur.setUserId(adminUser.getId()); + ur.setRoleId(adminRole.getId()); + userRoleMapper.insert(ur); + + // 7. 分配菜单 + if (dto.getMenuIds() != null && !dto.getMenuIds().isEmpty()) { + for (Long menuId : dto.getMenuIds()) { + SysTenantMenu tm = new SysTenantMenu(); + tm.setTenantId(tenantId); + tm.setMenuId(menuId); + tenantMenuMapper.insert(tm); + } + } + + log.info("租户创建成功,ID:{},管理员:{}", tenantId, adminUsername); + + // 返回租户信息 + 管理员凭据 + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("admin", Map.of( + "username", adminUsername, + "password", adminPassword, + "userId", adminUser.getId(), + "nickname", adminUser.getNickname() + )); + return result; + } + + /** + * 从模板租户复制权限,并补充基础管理权限 + */ + private List copyTemplatePermissions(Long newTenantId) { + List newPermissionIds = new ArrayList<>(); + + // 查找模板租户 + SysTenant templateTenant = getOne( + new LambdaQueryWrapper().eq(SysTenant::getCode, TEMPLATE_TENANT_CODE), false); + if (templateTenant == null) { + log.warn("模板租户 {} 不存在,仅创建基础管理权限", TEMPLATE_TENANT_CODE); + } else { + // 复制模板租户的所有权限 + List templatePerms = permissionMapper.selectList( + new LambdaQueryWrapper() + .eq(SysPermission::getTenantId, templateTenant.getId()) + .eq(SysPermission::getValidState, 1)); + for (SysPermission tp : templatePerms) { + SysPermission np = new SysPermission(); + np.setTenantId(newTenantId); + np.setName(tp.getName()); + np.setCode(tp.getCode()); + np.setResource(tp.getResource()); + np.setAction(tp.getAction()); + np.setDescription(tp.getDescription()); + permissionMapper.insert(np); + newPermissionIds.add(np.getId()); + } + } + + // 收集已有的权限编码,避免重复 + Set existingCodes = new HashSet<>(); + for (Long pid : newPermissionIds) { + SysPermission p = permissionMapper.selectById(pid); + if (p != null) existingCodes.add(p.getCode()); + } + + // 补充基础管理权限 + for (String[] perm : BASE_ADMIN_PERMISSIONS) { + if (!existingCodes.contains(perm[1])) { + SysPermission np = new SysPermission(); + np.setTenantId(newTenantId); + np.setName(perm[0]); + np.setCode(perm[1]); + np.setResource(perm[2]); + np.setAction(perm[3]); + np.setDescription(perm[4]); + permissionMapper.insert(np); + newPermissionIds.add(np.getId()); + } + } + + return newPermissionIds; } @Override @@ -141,7 +279,17 @@ public class SysTenantServiceImpl extends ServiceImpl().eq(SysTenantMenu::getTenantId, id)); + for (Long menuId : dto.getMenuIds()) { + SysTenantMenu tm = new SysTenantMenu(); + tm.setTenantId(id); + tm.setMenuId(menuId); + tenantMenuMapper.insert(tm); + } + } return getById(id); } diff --git a/docs/design/README.md b/docs/design/README.md index 339b5d2..2eefc1d 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -12,6 +12,7 @@ | [成果发布优化](./super-admin/results-publish-optimization.md) | 活动监管 | 已实现(待验收) | 2026-03-27 | | [内容管理模块](./super-admin/content-management.md) | 内容管理 | P0 已实现并优化 | 2026-03-31 | | [机构管理优化](./super-admin/org-management.md) | 机构管理 | 已优化 | 2026-03-31 | +| [租户创建自动生成管理员](./super-admin/tenant-auto-create-admin.md) | 机构管理 | 已完成 | 2026-04-02 | ## 租户端(机构管理端) diff --git a/docs/design/super-admin/tenant-auto-create-admin.md b/docs/design/super-admin/tenant-auto-create-admin.md new file mode 100644 index 0000000..121fd41 --- /dev/null +++ b/docs/design/super-admin/tenant-auto-create-admin.md @@ -0,0 +1,160 @@ +# 租户创建自动生成管理员账号 — 设计方案 + +> 所属端:超管端 +> 状态:已完成 +> 创建日期:2026-04-02 +> 最后更新:2026-04-02 + +--- + +## 1. 背景与问题 + +超管端创建新租户(机构)后,需要**手动**到用户管理页面另行创建该租户的管理员账号、角色、权限,流程割裂且容易遗漏,导致新建的租户无法登录使用。 + +核心问题: +- 创建租户与创建管理员是分离的两步操作,缺乏原子性 +- 新租户没有默认角色和权限,管理员账号无法正常使用 +- 菜单分配(menuIds)也未实现(后端标记为 TODO) + +## 2. 现状分析 + +### 2.1 创建租户接口 + +- `POST /api/tenants` → `SysTenantServiceImpl.createTenant()` +- 仅创建 `t_sys_tenant` 记录,不创建用户、角色、权限、菜单关联 +- menuIds 参数存在但未处理(TODO 注释) + +### 2.2 初始化脚本的做法 + +`backend/scripts/init-dev-tenants.ts`(Node 版,已移除)中,创建租户时会一并完成: +1. 创建租户 → 2. 创建权限 → 3. 创建角色 → 4. 角色绑定权限 → 5. 创建 admin 用户 → 6. 用户绑定角色 → 7. 分配菜单 + +`init.sql` 中每个租户也都有对应的 admin 用户和角色。 + +### 2.3 涉及的数据表 + +| 表名 | 说明 | +|------|------| +| `t_sys_tenant` | 租户 | +| `t_sys_user` | 用户 | +| `t_sys_role` | 角色 | +| `t_sys_permission` | 权限(按租户隔离) | +| `t_sys_user_role` | 用户-角色关联 | +| `t_sys_role_permission` | 角色-权限关联 | +| `t_sys_tenant_menu` | 租户-菜单关联 | + +## 3. 设计方案 + +### 3.1 整体思路 + +创建租户时在同一事务中自动完成:创建租户 → 复制权限模板 → 创建管理员角色 → 创建管理员用户 → 绑定关联关系 → 分配菜单。接口返回租户信息 + 管理员凭据。 + +### 3.2 权限模板策略 + +- **模板来源**:运行时从广东省图租户(code=`gdlib`,tenant_id=9)复制权限,该租户拥有 26 项业务权限,覆盖活动、报名、评审、作品、公告等核心模块 +- **补充基础管理权限**:模板中缺少的 7 项基础权限自动补充: + - `user:update`、`user:delete` — 用户管理 + - `role:create`、`role:update`、`role:delete`、`role:assign` — 角色管理 + - `permission:read` — 权限查看 +- 新租户最终获得 **33 项权限**(26 模板 + 7 补充) + +### 3.3 前端页面设计 + +**创建表单改动**: +- 基本信息 Tab 增加「管理员账号」区块(用分割线分隔) +- 新增两个输入框:管理员账号(默认 admin)、管理员密码(默认 admin@{编码}) +- 仅新建时显示,编辑时隐藏 + +**成功弹窗改动**: +- 原先:提示"创建成功"+ 跳转用户管理页按钮 +- 现在:展示管理员账号、初始密码、登录地址 +- 新增「复制账号信息」按钮,一键复制全部信息到剪贴板 + +### 3.4 后端改动 + +**`CreateTenantDto`** — 新增字段: +- `adminUsername`(String,可选,默认 "admin") +- `adminPassword`(String,可选,默认 "admin@{tenantCode}") + +**`ISysTenantService.createTenant()`** — 返回值改为 `Map` + +**`SysTenantServiceImpl.createTenant()`** — 核心改动,事务内完成 7 步: +1. 创建租户记录 +2. `copyTemplatePermissions()` — 复制 gdlib 权限 + 补充基础权限 +3. 创建 `tenant_admin` 角色 +4. 批量绑定角色-权限 +5. 创建管理员用户(密码 BCrypt 加密) +6. 绑定用户-角色 +7. 处理 menuIds 菜单分配 + +新增私有方法 `copyTemplatePermissions(Long newTenantId)`: +- 查询 gdlib 租户的全部权限,逐条复制(替换 tenantId) +- 对比已有权限编码,补充缺失的基础管理权限 + +**`SysTenantServiceImpl.updateTenant()`** — 同步实现 menuIds 更新(先删后增) + +**`SysTenantController.create()`** — 返回类型从 `Result` 改为 `Result>` + +### 3.5 接口响应格式 + +```json +{ + "code": 200, + "data": { + "tenant": { + "id": 13, + "name": "测试图书馆", + "code": "test-lib", + "tenantType": "library", + ... + }, + "admin": { + "username": "libadmin", + "password": "Lib@2026", + "userId": 32, + "nickname": "测试图书馆管理员" + } + } +} +``` + +### 3.6 前端改动 + +**`src/api/tenants.ts`**: +- `CreateTenantForm` 新增 `adminUsername`、`adminPassword` +- 新增 `CreateTenantResult` 接口 +- `createTenant()` 返回类型改为 `CreateTenantResult` + +**`src/views/system/tenants/Index.vue`**: +- 表单 reactive 新增 `adminUsername`、`adminPassword` +- 模板增加管理员输入区块 +- 成功弹窗展示管理员信息卡片 + 复制按钮 +- 移除 `useRouter`、`useAuthStore` 无用依赖 + +## 4. 改动范围 + +### 后端(Java) + +| 操作 | 文件 | +|------|------| +| 修改 | `modules/sys/dto/CreateTenantDto.java` | +| 修改 | `modules/sys/service/ISysTenantService.java` | +| 修改 | `modules/sys/service/impl/SysTenantServiceImpl.java` | +| 修改 | `modules/sys/controller/SysTenantController.java` | + +### 前端 + +| 操作 | 文件 | +|------|------| +| 修改 | `src/api/tenants.ts` | +| 修改 | `src/views/system/tenants/Index.vue` | + +## 5. 实施记录 + +### 2026-04-02 — 初版实现 + +- 完成后端事务化创建租户 + 管理员 + 角色 + 权限 + 菜单分配 +- 权限模板使用 gdlib 租户(26项)+ 7项基础管理权限补充 +- 前端表单增加管理员账号/密码输入,成功弹窗展示凭据信息 +- 测试验证:创建租户 → 管理员登录成功 → 获得 97 项权限(含 gdlib 完整业务权限 + 补充权限) +- 同步实现了 `updateTenant` 的 menuIds 更新(消除 TODO) diff --git a/frontend/src/api/tenants.ts b/frontend/src/api/tenants.ts index 5ab4887..86375b8 100644 --- a/frontend/src/api/tenants.ts +++ b/frontend/src/api/tenants.ts @@ -31,6 +31,18 @@ export interface CreateTenantForm { domain?: string; description?: string; menuIds?: number[]; + adminUsername?: string; + adminPassword?: string; +} + +export interface CreateTenantResult { + tenant: Tenant; + admin: { + username: string; + password: string; + userId: number; + nickname: string; + }; } export interface UpdateTenantForm { @@ -61,9 +73,9 @@ export async function getTenantDetail(id: number): Promise { return response; } -// 创建租户 -export async function createTenant(data: CreateTenantForm): Promise { - const response = await request.post("/tenants", data); +// 创建租户(自动创建管理员账号) +export async function createTenant(data: CreateTenantForm): Promise { + const response = await request.post("/tenants", data); return response; } diff --git a/frontend/src/views/system/tenants/Index.vue b/frontend/src/views/system/tenants/Index.vue index d73b2e5..8e82475 100644 --- a/frontend/src/views/system/tenants/Index.vue +++ b/frontend/src/views/system/tenants/Index.vue @@ -131,6 +131,21 @@ + 正常 @@ -174,12 +189,26 @@ - - + + @@ -189,7 +218,6 @@