feat(platform): 实现平台配置管理与权限优化

- 添加平台配置功能,支持动态设置项目名称、图标、Logo等
- 实现前端页面动态加载平台配置信息
- 完善权限控制,修复权限码格式问题
- 优化角色权限分配的安全校验机制
- 增加系统参数管理页面和相关API接口
- 更新数据库配置和MyBatis拦截器设置
- 调整安全配置,开放平台配置接口访问权限
- 重构权限查询逻辑,区分平台管理员与普通用户权限范围
- 补充实体类注解和字段定义,完善数据映射关系
- 优化登录页面样式,支持自定义背景图片和品牌信息显示
master
chenhao 2026-02-26 16:27:45 +08:00
parent 86009e2602
commit 9b721929c6
35 changed files with 578 additions and 460 deletions

View File

@ -58,10 +58,21 @@ com.xxx.project
## 四、角色与定位
你是一位**务实型后端开发者 Agent**,只修改后端文件,不修改前端,目标是:
你是一位**务实型后端开发者 Agent**,目标是:
> 以最清晰、最朴素、最可验证的方式交付可工作的 Java 服务。
> 基本原则
> 1. 生成内容必须完整、可运行、不可省略。
> 2. 不允许伪代码。
> 3. 不允许使用"示例代码"字样。
> 4. 不允许省略 import。
> 5. 不允许省略异常处理。
> 6. 所有写操作必须考虑事务控制。
> 7. 所有删除操作必须为逻辑删除is_deleted
> 8. 所有表必须包含:
> - created_at TIMESTAMP(6)
> - updated_at TIMESTAMP(6)
> - is_deleted SMALLINT DEFAULT 0
### 核心理念
* 清晰的意图胜于巧妙的代码
@ -88,7 +99,7 @@ com.xxx.project
2. 所有功能改动都必须更新设计文档
3. 遵循代码风格、目录结构和 Git 工作流规则
中大型需求必须先创建:
需求必须先创建:
`IMPLEMENTATION_PLAN.md`
@ -153,6 +164,22 @@ Status:
* 根本性反思
---
### 5.4. 变更同步规则
当数据库结构发生变更时,必须同步生成:
- Entity
- Mapper
- Service
- Controller
- DTO
- VO
- 前端类型定义
- API 封装
- 权限校验调整
同步修改backend/design/db_schema.md和backend/design/db_schema_pgsql.sql
禁止只修改数据库而不同步代码。
## 六、质量关卡DoD

View File

@ -1,108 +0,0 @@
# 项目设计文档imeetingNew
## 1. 项目概述
本项目为“智能会议语音识别与总结系统”的管理后台,提供用户、角色、权限、设备与任务等管理能力。
后端为 Java 服务,前端为后台管理 Web。 后续所有内容变更都必须同步更新该文档
## 2. 技术栈
### 后端
- Java 17
- Spring Boot 3.x
- Spring MVC
- Spring Security + JWT
- ORM: MyBatis / MyBatis-Plus
- Database: PostgreSQL
- Cache: Redis
- Build: Maven
### 前端
- React 18
- TypeScript
- Ant Design
- React Router
- Axios
- Vite
## 3. 系统架构
### 后端分层
- Controller接收请求、参数校验、返回响应
- Service业务编排与事务边界
- Mapper数据访问
- DTO/VO对外数据结构
### 前端分层
```
src
├─ api # 后端接口封装
├─ components # 通用组件
├─ layouts # 布局
├─ pages # 页面
├─ routes # 路由
├─ hooks # 自定义 hooks
├─ utils # 工具
└─ types # 类型定义
```
## 4. 数据库设计(核心)
详见 `design/db_schema.md``design/db_schema_pgsql.sql`
核心表:
- sys_user / sys_role / sys_user_role
- sys_permission / sys_role_permission
- sys_dict_type / sys_dict_item
- sys_param
- sys_log
## 5. 权限设计
### 权限模型
- 角色与权限多对多
- 用户与角色多对多
- 权限分为 menu / button
- 支持层级菜单1级/2级
### 超级管理员
- 约定 `user_id = 1` 为超级管理员
- 后端权限查询对其返回全量权限
### 接口
- `GET /api/permissions`:仅管理员可用(全量)
- `GET /api/permissions/me`:当前用户权限
- `GET /api/permissions/tree`:管理员权限树
- `GET /api/permissions/tree/me`:当前用户权限树
### 创建/更新校验
- level=1 时清空 parentId
- level=2 时 parentId 必须存在且为 level=1
- button 权限必须填写 code
- menu 权限 code 可选
## 6. 前端权限处理
- 登录后调用 `/api/users/me` 获取 `isAdmin`
- `isAdmin=true` 时前端不做权限限制
- 非管理员通过 `/api/permissions/me` 获取权限码用于按钮控制
- 菜单展示按权限树构建
## 7. 关键流程
### 登录
1. `/auth/captcha`
2. `/auth/login`
3. `/api/users/me`(获取用户信息与 isAdmin
### 验证码开关(系统参数)
- 系统参数 `security.captcha.enabled` 控制验证码是否启用true/false
- 系统启动时加载 `sys_param` 到 Redis Hash`sys:param:{paramKey}`字段value/type
- 前端登录页根据系统参数决定是否展示验证码
### 权限菜单渲染
1. `/api/permissions/me` 获取权限列表
2. 前端构建树形菜单
## 8. 约束与规范
- 后端禁用 JPA/Hibernate
- 统一响应 `ApiResponse<T>`
- Controller 不直接调用 Mapper
- 前端禁止在页面内直接调用 axios
## 9. 后续扩展建议
- 添加审计日志落库策略
- 任务管理模块完善
- 权限树缓存与增量刷新策略

View File

@ -23,6 +23,10 @@ public final class RedisKeys {
return "sys:dict:" + typeCode;
}
public static String platformConfigKey() {
return "sys:platform:config";
}
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
public static final String SYS_PARAM_FIELD_VALUE = "value";
public static final String SYS_PARAM_FIELD_TYPE = "type";

View File

@ -3,6 +3,7 @@ package com.imeeting.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.imeeting.security.LoginUser;
import net.sf.jsqlparser.expression.Expression;
@ -54,9 +55,10 @@ public class MybatisPlusConfig {
}
// 公共表始终忽略过滤
return List.of("sys_tenant", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
}
}));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}

View File

@ -28,6 +28,8 @@ public class SecurityConfig {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/api/open/**").permitAll()
.requestMatchers("/api/static/**").permitAll()
.requestMatchers("/api/params/value").permitAll()
.anyRequest().authenticated()
)

View File

@ -27,9 +27,15 @@ public class PermissionController {
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_permission:list')")
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
public ApiResponse<List<SysPermission>> list() {
return ApiResponse.ok(sysPermissionService.list());
Long tenantId = getCurrentTenantId();
// 平台管理员查询所有
if (Long.valueOf(0).equals(tenantId)) {
return ApiResponse.ok(sysPermissionService.list());
}
// 非平台管理员只能查询自己拥有的权限
return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), tenantId));
}
@GetMapping("/me")
@ -38,9 +44,16 @@ public class PermissionController {
}
@GetMapping("/tree")
@PreAuthorize("@ss.hasPermi('sys_permission:list')")
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
public ApiResponse<List<PermissionNode>> tree() {
return ApiResponse.ok(buildTree(sysPermissionService.list()));
Long tenantId = getCurrentTenantId();
List<SysPermission> list;
if (Long.valueOf(0).equals(tenantId)) {
list = sysPermissionService.list();
} else {
list = sysPermissionService.listByUserId(getCurrentUserId(), tenantId);
}
return ApiResponse.ok(buildTree(list));
}
@GetMapping("/tree/me")
@ -49,13 +62,13 @@ public class PermissionController {
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_permission:query')")
@PreAuthorize("@ss.hasPermi('sys:permission:query')")
public ApiResponse<SysPermission> get(@PathVariable Long id) {
return ApiResponse.ok(sysPermissionService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_permission:create')")
@PreAuthorize("@ss.hasPermi('sys:permission:create')")
public ApiResponse<Boolean> create(@RequestBody SysPermission perm) {
String error = validateParent(perm);
if (error != null) {
@ -65,7 +78,7 @@ public class PermissionController {
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_permission:update')")
@PreAuthorize("@ss.hasPermi('sys:permission:update')")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysPermission perm) {
perm.setPermId(id);
String error = validateParent(perm);
@ -83,7 +96,7 @@ public class PermissionController {
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_permission:delete')")
@PreAuthorize("@ss.hasPermi('sys:permission:delete')")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysPermissionService.removeById(id));
}

View File

@ -11,11 +11,14 @@ import com.imeeting.mapper.SysRolePermissionMapper;
import com.imeeting.mapper.SysUserRoleMapper;
import com.imeeting.service.SysRoleService;
import com.imeeting.service.SysUserService;
import com.imeeting.service.SysPermissionService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Set;
@RestController
@RequestMapping("/api/roles")
@ -24,56 +27,60 @@ public class RoleController {
private final SysUserService sysUserService;
private final SysRolePermissionMapper sysRolePermissionMapper;
private final SysUserRoleMapper sysUserRoleMapper;
private final SysPermissionService sysPermissionService;
public RoleController(SysRoleService sysRoleService, SysUserService sysUserService, SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper) {
public RoleController(SysRoleService sysRoleService, SysUserService sysUserService,
SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper,
SysPermissionService sysPermissionService) {
this.sysRoleService = sysRoleService;
this.sysUserService = sysUserService;
this.sysRolePermissionMapper = sysRolePermissionMapper;
this.sysUserRoleMapper = sysUserRoleMapper;
this.sysPermissionService = sysPermissionService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_role:list')")
@PreAuthorize("@ss.hasPermi('sys:role:list')")
public ApiResponse<List<SysRole>> list() {
return ApiResponse.ok(sysRoleService.list());
}
@GetMapping("/{id}/users")
@PreAuthorize("@ss.hasPermi('sys_role:query')")
@PreAuthorize("@ss.hasPermi('sys:role:query')")
public ApiResponse<List<SysUser>> listUsers(@PathVariable Long id) {
return ApiResponse.ok(sysUserService.listUsersByRoleId(id));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_role:query')")
@PreAuthorize("@ss.hasPermi('sys:role:query')")
public ApiResponse<SysRole> get(@PathVariable Long id) {
return ApiResponse.ok(sysRoleService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_role:create')")
@com.imeeting.common.annotation.Log(value = "新增角色", type = "角色管理")
@PreAuthorize("@ss.hasPermi('sys:role:create')")
@Log(value = "新增角色", type = "角色管理")
public ApiResponse<Boolean> create(@RequestBody SysRole role) {
return ApiResponse.ok(sysRoleService.save(role));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_role:update')")
@com.imeeting.common.annotation.Log(value = "修改角色", type = "角色管理")
@PreAuthorize("@ss.hasPermi('sys:role:update')")
@Log(value = "修改角色", type = "角色管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysRole role) {
role.setRoleId(id);
return ApiResponse.ok(sysRoleService.updateById(role));
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_role:delete')")
@PreAuthorize("@ss.hasPermi('sys:role:delete')")
@Log(value = "删除角色", type = "角色管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysRoleService.removeById(id));
}
@GetMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermi('sys_role:permission:list')")
@PreAuthorize("@ss.hasPermi('sys:role:permission:list')")
public ApiResponse<List<Long>> listRolePermissions(@PathVariable Long id) {
List<SysRolePermission> rows = sysRolePermissionMapper.selectList(
new QueryWrapper<SysRolePermission>().eq("role_id", id)
@ -88,9 +95,28 @@ public class RoleController {
}
@PostMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermi('sys_role:permission:save')")
@PreAuthorize("@ss.hasPermi('sys:role:permission:save')")
public ApiResponse<Boolean> saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) {
List<Long> permIds = payload == null ? null : payload.getPermIds();
// 权限越权校验
Long currentTenantId = getCurrentTenantId();
if (!Long.valueOf(0).equals(currentTenantId)) {
List<com.imeeting.entity.SysPermission> myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId);
Set<Long> myPermIds = myPerms.stream()
.map(com.imeeting.entity.SysPermission::getPermId)
.collect(Collectors.toSet());
if (permIds != null) {
for (Long pId : permIds) {
if (!myPermIds.contains(pId)) {
return ApiResponse.error("越权分配权限:" + pId);
}
}
}
}
sysRolePermissionMapper.delete(new QueryWrapper<SysRolePermission>().eq("role_id", id));
if (permIds == null || permIds.isEmpty()) {
return ApiResponse.ok(true);
@ -108,7 +134,7 @@ public class RoleController {
}
@PostMapping("/{id}/users")
@PreAuthorize("@ss.hasPermi('sys_role:update')")
@PreAuthorize("@ss.hasPermi('sys:role:update')")
@Log(value = "角色关联用户", type = "角色管理")
public ApiResponse<Boolean> bindUsers(@PathVariable Long id, @RequestBody UserBindingPayload payload) {
if (payload == null || payload.getUserIds() == null) {
@ -128,7 +154,7 @@ public class RoleController {
}
@DeleteMapping("/{id}/users/{userId}")
@PreAuthorize("@ss.hasPermi('sys_role:update')")
@PreAuthorize("@ss.hasPermi('sys:role:update')")
@Log(value = "角色取消关联用户", type = "角色管理")
public ApiResponse<Boolean> unbindUser(@PathVariable Long id, @PathVariable Long userId) {
QueryWrapper<SysUserRole> qw = new QueryWrapper<>();
@ -137,6 +163,22 @@ public class RoleController {
return ApiResponse.ok(true);
}
private Long getCurrentUserId() {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId();
}
return null;
}
private Long getCurrentTenantId() {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId();
}
return null;
}
public static class UserBindingPayload {
private List<Long> userIds;
public List<Long> getUserIds() { return userIds; }

View File

@ -20,26 +20,26 @@ public class SysOrgController {
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_org:list')")
@PreAuthorize("@ss.hasPermi('sys:org:list')")
public ApiResponse<List<SysOrg>> list(@RequestParam(required = false) Long tenantId) {
return ApiResponse.ok(sysOrgService.listTree(tenantId));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_org:query')")
@PreAuthorize("@ss.hasPermi('sys:org:query')")
public ApiResponse<SysOrg> get(@PathVariable Long id) {
return ApiResponse.ok(sysOrgService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_org:create')")
@PreAuthorize("@ss.hasPermi('sys:org:create')")
@Log(value = "新增组织", type = "组织管理")
public ApiResponse<Boolean> create(@RequestBody SysOrg org) {
return ApiResponse.ok(sysOrgService.save(org));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_org:update')")
@PreAuthorize("@ss.hasPermi('sys:org:update')")
@Log(value = "修改组织", type = "组织管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysOrg org) {
org.setId(id);
@ -47,7 +47,7 @@ public class SysOrgController {
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_org:delete')")
@PreAuthorize("@ss.hasPermi('sys:org:delete')")
@Log(value = "删除组织", type = "组织管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
// Check if has children

View File

@ -1,12 +1,16 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.common.PageResult;
import com.imeeting.dto.SysParamQueryDTO;
import com.imeeting.dto.SysParamVO;
import com.imeeting.entity.SysParam;
import com.imeeting.service.SysParamService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/params")
@ -17,16 +21,22 @@ public class SysParamController {
this.sysParamService = sysParamService;
}
@GetMapping("/page")
@PreAuthorize("@ss.hasPermi('sys_param:list')")
public ApiResponse<PageResult<List<SysParamVO>>> page(SysParamQueryDTO query) {
return ApiResponse.ok(sysParamService.page(query));
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_param:list')")
public ApiResponse<List<SysParam>> list() {
return ApiResponse.ok(sysParamService.list());
public ApiResponse<List<SysParamVO>> list() {
return ApiResponse.ok(sysParamService.list().stream().map(this::toVO).collect(Collectors.toList()));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_param:query')")
public ApiResponse<SysParam> get(@PathVariable Long id) {
return ApiResponse.ok(sysParamService.getById(id));
public ApiResponse<SysParamVO> get(@PathVariable Long id) {
return ApiResponse.ok(toVO(sysParamService.getById(id)));
}
@PostMapping
@ -66,4 +76,19 @@ public class SysParamController {
@RequestParam(value = "defaultValue", required = false) String defaultValue) {
return ApiResponse.ok(sysParamService.getCachedParamValue(key, defaultValue));
}
private SysParamVO toVO(SysParam entity) {
if (entity == null) return null;
SysParamVO vo = new SysParamVO();
vo.setParamId(entity.getParamId());
vo.setParamKey(entity.getParamKey());
vo.setParamValue(entity.getParamValue());
vo.setParamType(entity.getParamType());
vo.setIsSystem(entity.getIsSystem());
vo.setDescription(entity.getDescription());
vo.setStatus(entity.getStatus());
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
}

View File

@ -29,34 +29,54 @@ public class UserController {
private final JwtTokenProvider jwtTokenProvider;
private final SysUserRoleMapper sysUserRoleMapper;
private final com.imeeting.service.SysTenantUserService sysTenantUserService;
private final com.imeeting.service.SysRoleService sysRoleService;
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider, SysUserRoleMapper sysUserRoleMapper, com.imeeting.service.SysTenantUserService sysTenantUserService) {
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider, SysUserRoleMapper sysUserRoleMapper,
com.imeeting.service.SysTenantUserService sysTenantUserService,
com.imeeting.service.SysRoleService sysRoleService) {
this.sysUserService = sysUserService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.sysUserRoleMapper = sysUserRoleMapper;
this.sysTenantUserService = sysTenantUserService;
this.sysRoleService = sysRoleService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_user:list')")
@PreAuthorize("@ss.hasPermi('sys:user:list')")
public ApiResponse<List<SysUser>> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) {
Long currentTenantId = getCurrentTenantId();
List<SysUser> users;
if (Long.valueOf(0).equals(currentTenantId) && tenantId == null) {
List<SysUser> allUsers = sysUserService.list();
// 为每个用户加载其租户关系
if (allUsers != null && !allUsers.isEmpty()) {
for (SysUser user : allUsers) {
user.setMemberships(sysTenantUserService.listByUserId(user.getUserId()));
users = sysUserService.list();
} else {
Long targetTenantId = tenantId != null ? tenantId : currentTenantId;
if (targetTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
users = sysUserService.listUsersByTenant(targetTenantId, orgId);
}
if (users != null && !users.isEmpty()) {
for (SysUser user : users) {
// 加载租户关系
user.setMemberships(sysTenantUserService.listByUserId(user.getUserId()));
// 加载角色信息
List<SysUserRole> userRoles = sysUserRoleMapper.selectList(
new QueryWrapper<SysUserRole>().eq("user_id", user.getUserId())
);
if (userRoles != null && !userRoles.isEmpty()) {
List<Long> roleIds = userRoles.stream()
.map(SysUserRole::getRoleId)
.collect(java.util.stream.Collectors.toList());
user.setRoles(sysRoleService.listByIds(roleIds));
}
}
return ApiResponse.ok(allUsers);
}
Long targetTenantId = tenantId != null ? tenantId : currentTenantId;
if (targetTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
return ApiResponse.ok(sysUserService.listUsersByTenant(targetTenantId, orgId));
return ApiResponse.ok(users);
}
@GetMapping("/me")
@ -85,7 +105,7 @@ public class UserController {
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_user:query')")
@PreAuthorize("@ss.hasPermi('sys:user:query')")
public ApiResponse<SysUser> get(@PathVariable Long id) {
SysUser user = sysUserService.getByIdIgnoreTenant(id);
if (user != null) {
@ -103,9 +123,24 @@ public class UserController {
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_user:create')")
@PreAuthorize("@ss.hasPermi('sys:user:create')")
@Log(value = "新增用户", type = "用户管理")
public ApiResponse<Boolean> create(@RequestBody SysUser user) {
Long currentTenantId = getCurrentTenantId();
// 非平台管理员强制设置为当前租户
if (!Long.valueOf(0).equals(currentTenantId)) {
if (user.getMemberships() != null && !user.getMemberships().isEmpty()) {
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
} else {
// 如果没传身份,补齐当前租户身份
List<com.imeeting.entity.SysTenantUser> memberships = new java.util.ArrayList<>();
com.imeeting.entity.SysTenantUser m = new com.imeeting.entity.SysTenantUser();
m.setTenantId(currentTenantId);
memberships.add(m);
user.setMemberships(memberships);
}
}
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
}
@ -117,10 +152,19 @@ public class UserController {
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_user:update')")
@PreAuthorize("@ss.hasPermi('sys:user:update')")
@Log(value = "修改用户", type = "用户管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysUser user) {
Long currentTenantId = getCurrentTenantId();
user.setUserId(id);
// 非平台管理员强制约束租户身份
if (!Long.valueOf(0).equals(currentTenantId)) {
if (user.getMemberships() != null) {
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
}
}
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
}
@ -132,14 +176,14 @@ public class UserController {
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_user:delete')")
@PreAuthorize("@ss.hasPermi('sys:user:delete')")
@Log(value = "删除用户", type = "用户管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysUserService.removeById(id));
}
@GetMapping("/{id}/roles")
@PreAuthorize("@ss.hasPermi('sys_user:role:list')")
@PreAuthorize("@ss.hasPermi('sys:user:role:list')")
public ApiResponse<List<Long>> listUserRoles(@PathVariable Long id) {
List<SysUserRole> rows = sysUserRoleMapper.selectList(
new QueryWrapper<SysUserRole>().eq("user_id", id)
@ -154,7 +198,7 @@ public class UserController {
}
@PostMapping("/{id}/roles")
@PreAuthorize("@ss.hasPermi('sys_user:role:save')")
@PreAuthorize("@ss.hasPermi('sys:user:role:save')")
public ApiResponse<Boolean> saveUserRoles(@PathVariable Long id, @RequestBody RoleBindingPayload payload) {
List<Long> roleIds = payload == null ? null : payload.getRoleIds();
sysUserRoleMapper.delete(new QueryWrapper<SysUserRole>().eq("user_id", id));

View File

@ -21,7 +21,4 @@ public class SysDictItem extends BaseEntity {
@TableField(exist = false)
private Long tenantId;
@TableField(exist = false)
private Integer isDeleted;
}

View File

@ -19,7 +19,4 @@ public class SysDictType extends BaseEntity {
@TableField(exist = false)
private Long tenantId;
@TableField(exist = false)
private Integer isDeleted;
}

View File

@ -5,7 +5,15 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_param")
public class SysParam extends BaseEntity {
@TableId(value = "param_id", type = IdType.AUTO)
@ -15,4 +23,7 @@ public class SysParam extends BaseEntity {
private String paramType;
private Integer isSystem;
private String description;
@TableField(exist = false)
private Long tenantId;
}

View File

@ -26,4 +26,7 @@ public class SysUser extends BaseEntity {
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.List<SysTenantUser> memberships;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.List<SysRole> roles;
}

View File

@ -14,7 +14,7 @@ public interface SysPermissionMapper extends BaseMapper<SysPermission> {
@Select("""
SELECT DISTINCT p.*
FROM sys_permission p
JOIN sys_role_permission rp ON rp.perm_id = p.perm_id
JOIN sys_role_permission rp ON rp.perm_id = p.perm_id and p.is_deleted=0
JOIN sys_role r ON r.role_id = rp.role_id
JOIN sys_user_role ur ON ur.role_id = r.role_id
WHERE ur.user_id = #{userId} AND r.tenant_id = #{tenantId}

View File

@ -1,9 +1,16 @@
package com.imeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.imeeting.common.PageResult;
import com.imeeting.dto.SysParamQueryDTO;
import com.imeeting.dto.SysParamVO;
import com.imeeting.entity.SysParam;
import java.util.List;
public interface SysParamService extends IService<SysParam> {
PageResult<List<SysParamVO>> page(SysParamQueryDTO query);
String getParamValue(String key, String defaultValue);
String getCachedParamValue(String key, String defaultValue);

View File

@ -1,8 +1,12 @@
package com.imeeting.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.imeeting.common.PageResult;
import com.imeeting.common.RedisKeys;
import com.imeeting.dto.SysParamQueryDTO;
import com.imeeting.dto.SysParamVO;
import com.imeeting.entity.SysParam;
import com.imeeting.mapper.SysParamMapper;
import com.imeeting.service.SysParamService;
@ -14,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@ -24,6 +29,44 @@ public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> i
this.redisTemplate = redisTemplate;
}
@Override
public PageResult<List<SysParamVO>> page(SysParamQueryDTO query) {
Page<SysParam> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<SysParam> wrapper = new LambdaQueryWrapper<>();
if (query.getParamKey() != null && !query.getParamKey().isEmpty()) {
wrapper.like(SysParam::getParamKey, query.getParamKey());
}
if (query.getParamType() != null && !query.getParamType().isEmpty()) {
wrapper.eq(SysParam::getParamType, query.getParamType());
}
if (query.getDescription() != null && !query.getDescription().isEmpty()) {
wrapper.like(SysParam::getDescription, query.getDescription());
}
wrapper.orderByDesc(SysParam::getCreatedAt);
Page<SysParam> result = this.baseMapper.selectPage(page, wrapper);
PageResult<List<SysParamVO>> pageResult = new PageResult<>();
pageResult.setTotal(result.getTotal());
pageResult.setRecords(result.getRecords().stream().map(this::toVO).collect(Collectors.toList()));
return pageResult;
}
private SysParamVO toVO(SysParam entity) {
if (entity == null) return null;
SysParamVO vo = new SysParamVO();
vo.setParamId(entity.getParamId());
vo.setParamKey(entity.getParamKey());
vo.setParamValue(entity.getParamValue());
vo.setParamType(entity.getParamType());
vo.setIsSystem(entity.getIsSystem());
vo.setDescription(entity.getDescription());
vo.setStatus(entity.getStatus());
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
@Override
public String getParamValue(String key, String defaultValue) {
if (key == null || key.isEmpty()) {

View File

@ -30,6 +30,8 @@ security:
secret: change-me-please-change-me-32bytes
app:
upload-path: D:/data/imeeting/uploads/
resource-prefix: /api/static/
captcha:
ttl-seconds: 120
max-attempts: 5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

View File

@ -47,7 +47,7 @@ src
你是一位**务实型前端开发者 Agent**,目标是:
> 以清晰的数据流和稳定的交互,构建易维护的管理后台。
> 阅读对应的后端controller了解接口
### 核心原则
* 清晰的意图胜于技巧性的实现

View File

@ -1,167 +0,0 @@
# Nex Design 前端设计规范
面向 **React + Ant Design + Tailwind CSS** 的前端设计语言系统,目标:
- **一致性**:保证视觉与交互统一
- **高效性**:提供可复用组件与布局模式
- **可维护性**:清晰规范,便于迭代
- **用户体验**:直观易用
## 技术栈
- 框架React 18+
- 组件库Ant Design 5.x
- 样式Tailwind CSS 3.x
- 包管理Yarn
- 运行时Node.js 16+
---
## 设计原则
- **清晰明确**:界面直观,操作易理解
- **一致性优先**:视觉、交互、用词统一
- **效率至上**:减少操作步骤,提高效率
- **反馈及时**:操作有明确状态反馈
- **容错友好**:预防错误,提示明确
**使用规范**
- 主色:关键按钮、重要信息、链接
- 功能色:按语义使用,不混淆
- 中性色:文本、背景、边框
- 对比度 ≥ 4.5:1
---
## 排版规范
### 字体
- 默认:`-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto …`
- 等宽:`'SF Mono', 'Monaco', 'Fira Code', …`
### 字号
| 用途 | 大小 | Tailwind 类 | 场景 |
|------|------|-------------|------|
| 特大标题 | 32px | `text-4xl` | 页面主标题 |
| 大标题 | 24px | `text-2xl` | 区块标题 |
| 中标题 | 20px | `text-xl` | 卡片标题 |
| 小标题 | 16px | `text-base` | 表单标签 |
| 正文 | 14px | `text-sm` | 正文 |
| 辅助文字 | 12px | `text-xs` | 说明 |
### 字重
| 用途 | 字重 |
|------|-----|
| 正文 | Regular 400 |
| 表单标签、列表 | Medium 500 |
| 小标题、强调 | Semibold 600 |
| 标题、重要信息 | Bold 700 |
---
## 间距系统
- 基于 **8px 网格**,间距均为 8 的倍数
- Tailwind 对应类:
| 尺寸 | Tailwind 类 | 间距 |
|------|------------|------|
| xs | p-1 / m-1 | 4px |
| sm | p-2 / m-2 | 8px |
| md | p-4 / m-4 | 16px |
| lg | p-6 / m-6 | 24px |
| xl | p-8 / m-8 | 32px |
| 2xl | p-12 / m-12 | 48px |
---
## 组件规范
### 按钮 (Button)
- 类型Primary / Default / Text / Link / Danger
- 尺寸Large 40px / Middle 32px / Small 24px
- 使用:
- 单区域最多一个主按钮
- 文字 ≤ 4 个字
- 危险操作二次确认
### 表单 (Form)
- 布局vertical
- 必填:红色星号
- 字段宽度合理,间距 24px
- 错误提示显示在字段下方
### 表格 (Table)
- 分页默认 10 条
- 行高 54px (middle)
- 操作列固定右侧
- 加载状态使用 `loading`
### 卡片 (Card)
- 内边距 24px
- 圆角 8px
- 阴影 `shadow-sm`
- 卡片间距 16px
---
## 布局规范
- 页面Header 64px, Sider 200px, Content 区域
- 栅格24 栏,使用 Ant Design Grid
- 页面内边距 24px内容最大宽 1200px
- 响应式Flexbox/Grid + Tailwind 前缀
---
## 交互规范
- **全局提示**`message.success/error/warning/loading`
- **通知提醒**`notification.open`
- **模态框**`Modal.confirm`
- **加载状态**`Spin / Skeleton / Table loading`
- **动画**300ms, ease-in-out
---
## 页面模板
### 通用结构
- 面包屑导航
- 页面标题区
- 主要内容区
### 已实现
- 主框架页面:侧边栏、顶部导航、内容滚动
- Dashboard统计卡片、图表
### 待完善
- 列表页
- 详情页
- 表单页
- 设置页
---
## 开发规范
- **新增操作**:默认使用**右侧抽屉**打开表单进行创建(除非需求明确允许其他方式)
- **组件命名**PascalCase
- **文件命名**:与组件同名
- **样式类命名**kebab-case
- **常量命名**UPPER_SNAKE_CASE
- **样式**Tailwind 优先Ant Design 主题定制,自定义样式放组件目录

View File

@ -1,5 +1,36 @@
import AppRoutes from "./routes";
import { useEffect, useState } from "react";
import AppRoutes from "./routes";
import { getOpenPlatformConfig } from "./api";
import type { SysPlatformConfig } from "./types";
export default function App() {
const [config, setConfig] = useState<SysPlatformConfig | null>(null);
useEffect(() => {
const fetchConfig = async () => {
try {
const data = await getOpenPlatformConfig();
setConfig(data);
if (data.projectName) {
document.title = data.projectName;
}
if (data.iconUrl) {
let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.getElementsByTagName('head')[0].appendChild(link);
}
link.href = data.iconUrl;
}
// Save to sessionStorage for other components
sessionStorage.setItem("platformConfig", JSON.stringify(data));
} catch (e) {
console.error("Failed to load platform config", e);
}
};
fetchConfig();
}, []);
return <AppRoutes />;
}

View File

@ -1,5 +1,28 @@
import http from "./http";
import { DeviceInfo, SysPermission, SysRole, SysUser, UserProfile } from "../types";
import {
DeviceInfo, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
PermissionNode
} from "../types";
export async function pageParams(params: SysParamQuery) {
const resp = await http.get("/api/params/page", { params });
return resp.data.data as PageResult<SysParamVO[]>;
}
export async function createParam(payload: Partial<SysParamVO>) {
const resp = await http.post("/api/params", payload);
return resp.data.data as boolean;
}
export async function updateParam(id: number, payload: Partial<SysParamVO>) {
const resp = await http.put(`/api/params/${id}`, payload);
return resp.data.data as boolean;
}
export async function deleteParam(id: number) {
const resp = await http.delete(`/api/params/${id}`);
return resp.data.data as boolean;
}
export async function listUsers(params?: { tenantId?: number; orgId?: number }) {
const resp = await http.get("/api/users", { params });
@ -150,4 +173,5 @@ export async function fetchLogs(params: any) {
export * from "./dict";
export * from "./tenant";
export * from "./org";
export * from "./platform";

View File

@ -21,7 +21,7 @@ import { useAuth } from "../hooks/useAuth";
import { usePermission } from "../hooks/usePermission";
import { listMyPermissions, getCurrentUser } from "../api";
import { switchTenant, type TenantInfo } from "../api/auth";
import { SysPermission } from "../types";
import { SysPermission, SysPlatformConfig } from "../types";
const { Header, Sider, Content } = Layout;
@ -40,6 +40,11 @@ export default function AppLayout() {
const [menus, setMenus] = useState<SysPermission[]>([]);
const [availableTenants, setAvailableTenants] = useState<TenantInfo[]>([]);
const [currentTenantId, setCurrentTenantId] = useState<number | null>(null);
const platformConfig = useMemo<SysPlatformConfig | null>(() => {
const configStr = sessionStorage.getItem("platformConfig");
return configStr ? JSON.parse(configStr) : null;
}, []);
const location = useLocation();
const navigate = useNavigate();
@ -189,15 +194,18 @@ export default function AppLayout() {
gap: '12px',
borderBottom: '1px solid #f0f0f0'
}}>
<img src="/logo.svg" alt="logo" style={{ width: 32, height: 32 }} />
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 32, height: 32, objectFit: 'contain' }} />
{!collapsed && (
<span style={{
fontSize: '18px',
fontWeight: 700,
color: '#1677ff',
letterSpacing: '0.5px'
letterSpacing: '0.5px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
MeetingAI
{platformConfig?.projectName || "MeetingAI"}
</span>
)}
</div>

View File

@ -2,8 +2,9 @@ import { Button, Checkbox, Form, Input, message, Typography, Space } from "antd"
import { useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
import { getCurrentUser, getSystemParamValue } from "../api";
import { getCurrentUser, getSystemParamValue, getOpenPlatformConfig } from "../api";
import { UserOutlined, LockOutlined, SafetyOutlined, ReloadOutlined, ShopOutlined } from "@ant-design/icons";
import type { SysPlatformConfig } from "../types";
import "./Login.css";
const { Title, Text, Link } = Typography;
@ -13,6 +14,7 @@ export default function Login() {
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
const [captchaEnabled, setCaptchaEnabled] = useState(true);
const [loading, setLoading] = useState(false);
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(null);
const [form] = Form.useForm();
const loadCaptcha = useCallback(async () => {
@ -30,8 +32,13 @@ export default function Login() {
useEffect(() => {
const init = async () => {
try {
const value = await getSystemParamValue("security.captcha.enabled", "true");
const enabled = value !== "false";
const [captchaVal, pConfig] = await Promise.all([
getSystemParamValue("security.captcha.enabled", "true"),
getOpenPlatformConfig()
]);
setPlatformConfig(pConfig);
const enabled = captchaVal !== "false";
setCaptchaEnabled(enabled);
if (enabled) {
loadCaptcha();
@ -93,12 +100,31 @@ export default function Login() {
}
};
const loginStyle = platformConfig?.loginBgUrl ? {
backgroundImage: `url(${platformConfig.loginBgUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
position: 'relative' as const
} : {};
// 如果设置了背景图,左侧和右侧的背景应该透明或者半透明
const leftStyle = platformConfig?.loginBgUrl ? {
...loginStyle,
background: 'rgba(255, 255, 255, 0.2)',
backdropFilter: 'blur(10px)',
} : {};
const rightStyle = platformConfig?.loginBgUrl ? {
background: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(20px)',
} : {};
return (
<div className="login-page">
<div className="login-left">
<div className="login-page" style={loginStyle}>
<div className="login-left" style={leftStyle}>
<div className="login-brand">
<img src="/logo.svg" alt="MeetingAI Logo" className="brand-logo-img" />
<span className="brand-name">MeetingAI</span>
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="Logo" className="brand-logo-img" />
<span className="brand-name">{platformConfig?.projectName || "MeetingAI"}</span>
</div>
<div className="login-hero">
@ -108,7 +134,7 @@ export default function Login() {
{t('login.heroTitle3')}
</h1>
<p className="hero-desc">
{t('login.heroDesc')}
{platformConfig?.systemDescription || t('login.heroDesc')}
</p>
</div>
@ -116,10 +142,16 @@ export default function Login() {
<div className="footer-item">{t('login.enterpriseSecurity')}</div>
<div className="footer-divider" aria-hidden="true" />
<div className="footer-item">{t('login.multiLang')}</div>
{platformConfig?.icpInfo && (
<>
<div className="footer-divider" aria-hidden="true" />
<div className="footer-item">{platformConfig.icpInfo}</div>
</>
)}
</div>
</div>
<div className="login-right">
<div className="login-right" style={rightStyle}>
<div className="login-container">
<div className="login-header">
<Title level={2}>{t('login.welcome')}</Title>

View File

@ -204,13 +204,13 @@ export default function Orgs() {
width: 180,
render: (_: any, record: SysOrg) => (
<Space>
{can("sys_org:create") && (
{can("sys:org:create") && (
<Button type="link" size="small" onClick={() => openCreate(record.id)}>{t('orgs.addSub')}</Button>
)}
{can("sys_org:update") && (
{can("sys:org:update") && (
<Button type="text" size="small" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label={t('common.edit')} />
)}
{can("sys_org:delete") && (
{can("sys:org:delete") && (
<Popconfirm title={`确定删除 "${record.orgName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
<Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
</Popconfirm>
@ -227,7 +227,7 @@ export default function Orgs() {
<Title level={4} className="mb-1">{t('orgs.title')}</Title>
<Text type="secondary">{t('orgs.subtitle')}</Text>
</div>
{can("sys_org:create") && (
{can("sys:org:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
{t('orgs.createRoot')}
</Button>

View File

@ -247,7 +247,7 @@ export default function Permissions() {
fixed: "right" as const,
render: (_: any, record: SysPermission) => (
<Space>
{can("sys_permission:create") && record.permType === 'menu' && (
{can("sys:permission:create") && record.permType === 'menu' && (
<Tooltip title="添加子项">
<Button
type="text"
@ -257,7 +257,7 @@ export default function Permissions() {
/>
</Tooltip>
)}
{can("sys_permission:update") && (
{can("sys:permission:update") && (
<Button
type="text"
size="small"
@ -266,7 +266,7 @@ export default function Permissions() {
aria-label={t('common.edit')}
/>
)}
{can("sys_permission:delete") && (
{can("sys:permission:delete") && (
<Popconfirm title={`确定删除权限 "${record.name}" 吗?`} onConfirm={() => remove(record.permId)}>
<Button
type="text"
@ -289,7 +289,7 @@ export default function Permissions() {
<Title level={4} className="mb-1">{t('permissions.title')}</Title>
<Text type="secondary">{t('permissions.subtitle')}</Text>
</div>
{can("sys_permission:create") && (
{can("sys:permission:create") && (
<Button
type="primary"
icon={<PlusOutlined aria-hidden="true" />}
@ -452,7 +452,7 @@ export default function Permissions() {
})
]}
>
<Input placeholder="例如sys_user:export…" className="tabular-nums" />
<Input placeholder="例如sys:user:export…" className="tabular-nums" />
</Form.Item>
<Row gutter={16}>

View File

@ -34,7 +34,7 @@ import {
unbindUserFromRole,
listUsers
} from "../api";
import type { SysPermission, SysRole, SysUser } from "../types";
import type {SysPermission, SysRole, SysTenant, SysUser} from "../types";
import { usePermission } from "../hooks/usePermission";
import {
EditOutlined,
@ -370,7 +370,7 @@ export default function Roles() {
<Card
title={t('roles.title')}
className="full-height-card shadow-sm"
extra={can("sys_role:create") && (
extra={can("sys:role:create") && (
<Button
type="primary"
size="small"
@ -423,7 +423,7 @@ export default function Roles() {
<div className="role-item-code text-xs text-gray-400 truncate">{record.roleCode}</div>
</div>
<div className="role-item-actions flex gap-1">
{can("sys_role:update") && (
{can("sys:role:update") && (
<Button
type="text"
size="small"
@ -431,7 +431,7 @@ export default function Roles() {
onClick={e => openEditBasic(e, record)}
/>
)}
{can("sys_role:delete") && record.roleCode !== 'ADMIN' && (
{can("sys:role:delete") && record.roleCode !== 'ADMIN' && (
<Popconfirm
title={`确定删除角色 "${record.roleName}" 吗?`}
onConfirm={e => handleRemove(e!, record.roleId)}
@ -473,7 +473,7 @@ export default function Roles() {
icon={<SaveOutlined aria-hidden="true" />}
loading={saving}
onClick={savePermissions}
disabled={!can("sys_role:permission:save")}
disabled={!can("sys:role:permission:save")}
>
{t('roles.savePerms')}
</Button>
@ -510,7 +510,7 @@ export default function Roles() {
type="primary"
icon={<UserAddOutlined aria-hidden="true" />}
onClick={openUserModal}
disabled={!can("sys_role:update")}
disabled={!can("sys:role:update")}
>
{t('common.create')}
</Button>
@ -550,14 +550,14 @@ export default function Roles() {
<Popconfirm
title={t('common.delete') + "?"}
onConfirm={() => handleUnbindUser(record.userId)}
disabled={!can("sys_role:update")}
disabled={!can("sys:role:update")}
>
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined aria-hidden="true" />}
disabled={!can("sys_role:update")}
disabled={!can("sys:role:update")}
/>
</Popconfirm>
)

View File

@ -134,12 +134,16 @@ export default function Users() {
const loadBaseData = async () => {
try {
const [rolesList, tenantsResp] = await Promise.all([
listRoles(),
listTenants({ current: 1, size: 1000 })
]);
const promises: Promise<any>[] = [listRoles()];
if (isPlatformMode) {
promises.push(listTenants({ current: 1, size: 1000 }));
}
const [rolesList, tenantsResp] = await Promise.all(promises);
setRoles(rolesList || []);
setTenants(tenantsResp.records || []);
if (isPlatformMode && tenantsResp) {
setTenants(tenantsResp.records || []);
}
} catch (e) {
message.error(t('common.error'));
}
@ -165,15 +169,16 @@ export default function Users() {
useEffect(() => {
const fetchOrgs = async () => {
if (selectedTenantId) {
const list = await listOrgs(selectedTenantId);
const targetId = isPlatformMode ? selectedTenantId : activeTenantId;
if (targetId) {
const list = await listOrgs(targetId);
setOrgs(list || []);
} else {
setOrgs([]);
}
};
fetchOrgs();
}, [selectedTenantId]);
}, [selectedTenantId, isPlatformMode, activeTenantId]);
const tenantMap = useMemo(() => {
const map: Record<number, string> = {};
@ -251,12 +256,13 @@ export default function Users() {
phone: values.phone,
status: values.status,
isPlatformAdmin: values.isPlatformAdmin,
memberships: values.memberships || []
};
// If single tenant view, ensure current active tenant is included
if (!isPlatformMode && userPayload.memberships!.length === 0) {
// If single tenant view (not platform admin mode)
if (!isPlatformMode) {
userPayload.memberships = [{ tenantId: activeTenantId, orgId: values.orgId } as any];
} else {
userPayload.memberships = values.memberships || [];
}
if (values.password) {
@ -306,43 +312,62 @@ export default function Users() {
</Space>
),
},
{
title: t('users.org'),
key: "org",
...(isPlatformMode ? [{
title: t('users.tenant'),
key: "tenant",
render: (_: any, record: SysUser) => {
// Platform mode: show all tenants as tags
if (isPlatformMode && record.memberships && record.memberships.length > 0) {
if (record.memberships && record.memberships.length > 0) {
return (
<div className="flex flex-col gap-1">
{record.memberships.slice(0, 3).map(m => (
{record.memberships.slice(0, 2).map(m => (
<Tag key={m.tenantId} color="blue" style={{ margin: 0, padding: '0 4px', fontSize: 11 }}>
{tenantMap[m.tenantId] || `租户${m.tenantId}`}
</Tag>
))}
{record.memberships.length > 3 && <Text type="secondary" style={{ fontSize: 11 }}> {record.memberships.length} </Text>}
{record.memberships.length > 2 && <Text type="secondary" style={{ fontSize: 11 }}> {record.memberships.length} </Text>}
</div>
);
}
// Single tenant mode or fallback: show specific tenant and org
const tid = record.tenantId || record.memberships?.[0]?.tenantId;
const oid = record.orgId || record.memberships?.[0]?.orgId;
return (
<div className="flex flex-col gap-1">
<Space size={4} style={{ fontSize: 13 }}>
<ShopOutlined style={{ color: '#8c8c8c' }} />
<span>{tenantMap[tid || 0] || "未知租户"}</span>
</Space>
{oid && (
<Space size={4} style={{ fontSize: 12, color: '#8c8c8c' }}>
<ApartmentOutlined />
<span></span>
</Space>
)}
</div>
);
return <Text type="secondary"></Text>;
}
}] : []),
{
title: t('users.orgNode'),
key: "org",
render: (_: any, record: SysUser) => {
if (record.memberships && record.memberships.length > 0) {
const orgNames = record.memberships
.map(m => m.orgName)
.filter(Boolean);
if (orgNames.length > 0) {
return (
<div className="flex flex-col gap-1">
{orgNames.map((name, idx) => (
<Space key={idx} size={4} style={{ fontSize: 13, color: '#555' }}>
<ApartmentOutlined />
<span>{name}</span>
</Space>
))}
</div>
);
}
}
return <Text type="secondary">-</Text>;
}
},
{
title: t('users.roles'),
key: "roles",
render: (_: any, record: SysUser) => (
<Space wrap size={[0, 4]}>
{record.roles && record.roles.length > 0 ? (
record.roles.map(r => (
<Tag key={r.roleId} color="cyan">{r.roleName}</Tag>
))
) : <Text type="secondary"></Text>}
</Space>
)
},
{
title: t('common.status'),
@ -361,7 +386,7 @@ export default function Users() {
fixed: "right" as const,
render: (_: any, record: SysUser) => (
<Space>
{can("sys_user:update") && (
{can("sys:user:update") && (
<Button
type="text"
icon={<EditOutlined aria-hidden="true" />}
@ -369,7 +394,7 @@ export default function Users() {
aria-label={t('common.edit')}
/>
)}
{can("sys_user:delete") && record.userId !== 1 && (
{can("sys:user:delete") && record.userId !== 1 && (
<Popconfirm title="确定注销该用户吗?" onConfirm={() => handleDelete(record.userId)}>
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
</Popconfirm>
@ -386,7 +411,7 @@ export default function Users() {
<Title level={4} className="mb-1">{t('users.title')}</Title>
<Text type="secondary">{t('users.subtitle')}</Text>
</div>
{can("sys_user:create") && (
{can("sys:user:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
{t('users.drawerTitleCreate')}
</Button>
@ -498,68 +523,84 @@ export default function Users() {
/>
</Form.Item>
{!isPlatformMode && (
<Form.Item label={t('users.orgNode')} name="orgId">
<TreeSelect
placeholder="选择所属组织/部门"
allowClear
treeData={orgTreeData}
/>
</Form.Item>
)}
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t('users.platformAdmin')} name="isPlatformAdmin" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
{isPlatformMode && (
<Col span={12}>
<Form.Item label={t('users.platformAdmin')} name="isPlatformAdmin" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
)}
</Row>
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}></Title>
<Form.List name="memberships">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Card
key={key}
size="small"
className="mb-3"
styles={{ body: { padding: '12px' } }}
title={isPlatformMode ? `身份 #${name + 1}` : undefined}
extra={isPlatformMode && fields.length > 1 && (
<Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />
{isPlatformMode && (
<>
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}></Title>
<Form.List name="memberships">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Card
key={key}
size="small"
className="mb-3"
styles={{ body: { padding: '12px' } }}
title={isPlatformMode ? `身份 #${name + 1}` : undefined}
extra={isPlatformMode && fields.length > 1 && (
<Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />
)}
>
<Row gutter={12}>
<Col span={12}>
<Form.Item
{...restField}
label={t('users.tenant')}
name={[name, 'tenantId']}
rules={[{ required: true, message: '必填' }]}
>
<Select
options={tenants.map(t => ({ label: t.tenantName, value: t.id }))}
disabled={!isPlatformMode}
placeholder="选择租户"
/>
</Form.Item>
</Col>
<Col span={12}>
<MembershipOrgSelect
fieldProps={{...restField}}
name={name}
tenantId={form.getFieldValue(['memberships', name, 'tenantId'])}
/>
</Col>
</Row>
</Card>
))}
{isPlatformMode && (
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button>
)}
>
<Row gutter={12}>
<Col span={12}>
<Form.Item
{...restField}
label={t('users.tenant')}
name={[name, 'tenantId']}
rules={[{ required: true, message: '必填' }]}
>
<Select
options={tenants.map(t => ({ label: t.tenantName, value: t.id }))}
disabled={!isPlatformMode}
placeholder="选择租户"
/>
</Form.Item>
</Col>
<Col span={12}>
<MembershipOrgSelect
fieldProps={{...restField}}
name={name}
tenantId={form.getFieldValue(['memberships', name, 'tenantId'])}
/>
</Col>
</Row>
</Card>
))}
{isPlatformMode && (
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button>
</>
)}
</>
)}
</Form.List>
</Form.List>
</>
)}
</Form>
</Drawer>
</div>

View File

@ -9,6 +9,8 @@ import Tenants from "../pages/Tenants";
import Orgs from "../pages/Orgs";
import UserRoleBinding from "../pages/UserRoleBinding";
import RolePermissionBinding from "../pages/RolePermissionBinding";
import SysParams from "../pages/SysParams";
import PlatformSettings from "../pages/PlatformSettings";
import type { MenuRoute } from "../types";
@ -19,6 +21,8 @@ export const menuRoutes: MenuRoute[] = [
{ path: "/users", label: "用户管理", element: <Users />, perm: "menu:users" },
{ path: "/roles", label: "角色管理", element: <Roles />, perm: "menu:roles" },
{ path: "/permissions", label: "权限管理", element: <Permissions />, perm: "menu:permissions" },
{ path: "/params", label: "系统参数", element: <SysParams />, perm: "menu:params" },
{ path: "/platform-settings", label: "平台设置", element: <PlatformSettings />, perm: "menu:platform" },
{ path: "/dictionaries", label: "字典管理", element: <Dictionaries />, perm: "menu:dict" },
{ path: "/logs", label: "日志管理", element: <Logs />, perm: "menu:logs" },
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" },

View File

@ -16,6 +16,8 @@ export interface SysUser extends BaseEntity {
tenantId: number;
orgId?: number;
isPlatformAdmin?: boolean;
memberships?: any[];
roles?: SysRole[];
}
export interface UserProfile {
@ -120,6 +122,38 @@ export interface OrgNode extends SysOrg {
children: OrgNode[];
}
export interface SysParamVO extends BaseEntity {
paramId: number;
paramKey: string;
paramValue: string;
paramType: string;
isSystem: number;
description?: string;
}
export interface SysPlatformConfig {
projectName: string;
logoUrl?: string;
iconUrl?: string;
loginBgUrl?: string;
icpInfo?: string;
copyrightInfo?: string;
systemDescription?: string;
}
export interface SysParamQuery {
paramKey?: string;
paramType?: string;
description?: string;
pageNum?: number;
pageSize?: number;
}
export interface PageResult<T> {
total: number;
records: T;
}
import type { ReactNode } from "react";
export interface MenuRoute {