diff --git a/backend/design/AGENTS.md b/backend/design/AGENTS.md index cc8847e..cbc02ff 100644 --- a/backend/design/AGENTS.md +++ b/backend/design/AGENTS.md @@ -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) diff --git a/backend/design/project_design.md b/backend/design/project_design.md deleted file mode 100644 index eed34e0..0000000 --- a/backend/design/project_design.md +++ /dev/null @@ -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` -- Controller 不直接调用 Mapper -- 前端禁止在页面内直接调用 axios - -## 9. 后续扩展建议 -- 添加审计日志落库策略 -- 任务管理模块完善 -- 权限树缓存与增量刷新策略 diff --git a/backend/src/main/java/com/imeeting/common/RedisKeys.java b/backend/src/main/java/com/imeeting/common/RedisKeys.java index a9b1614..1695069 100644 --- a/backend/src/main/java/com/imeeting/common/RedisKeys.java +++ b/backend/src/main/java/com/imeeting/common/RedisKeys.java @@ -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"; diff --git a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java index 2ec99da..c007bbc 100644 --- a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java +++ b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java @@ -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; } diff --git a/backend/src/main/java/com/imeeting/config/SecurityConfig.java b/backend/src/main/java/com/imeeting/config/SecurityConfig.java index ea145c9..c6f621d 100644 --- a/backend/src/main/java/com/imeeting/config/SecurityConfig.java +++ b/backend/src/main/java/com/imeeting/config/SecurityConfig.java @@ -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() ) diff --git a/backend/src/main/java/com/imeeting/controller/PermissionController.java b/backend/src/main/java/com/imeeting/controller/PermissionController.java index d53d6a6..82e911b 100644 --- a/backend/src/main/java/com/imeeting/controller/PermissionController.java +++ b/backend/src/main/java/com/imeeting/controller/PermissionController.java @@ -27,9 +27,15 @@ public class PermissionController { } @GetMapping - @PreAuthorize("@ss.hasPermi('sys_permission:list')") + @PreAuthorize("@ss.hasPermi('sys:permission:list')") public ApiResponse> 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> tree() { - return ApiResponse.ok(buildTree(sysPermissionService.list())); + Long tenantId = getCurrentTenantId(); + List 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 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 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 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 delete(@PathVariable Long id) { return ApiResponse.ok(sysPermissionService.removeById(id)); } diff --git a/backend/src/main/java/com/imeeting/controller/RoleController.java b/backend/src/main/java/com/imeeting/controller/RoleController.java index 04cdc95..5a6e57a 100644 --- a/backend/src/main/java/com/imeeting/controller/RoleController.java +++ b/backend/src/main/java/com/imeeting/controller/RoleController.java @@ -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() { return ApiResponse.ok(sysRoleService.list()); } @GetMapping("/{id}/users") - @PreAuthorize("@ss.hasPermi('sys_role:query')") + @PreAuthorize("@ss.hasPermi('sys:role:query')") public ApiResponse> 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 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 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 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 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> listRolePermissions(@PathVariable Long id) { List rows = sysRolePermissionMapper.selectList( new QueryWrapper().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 saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) { List permIds = payload == null ? null : payload.getPermIds(); + + // 权限越权校验 + Long currentTenantId = getCurrentTenantId(); + if (!Long.valueOf(0).equals(currentTenantId)) { + List myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId); + + Set 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().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 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 unbindUser(@PathVariable Long id, @PathVariable Long userId) { QueryWrapper 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 userIds; public List getUserIds() { return userIds; } diff --git a/backend/src/main/java/com/imeeting/controller/SysOrgController.java b/backend/src/main/java/com/imeeting/controller/SysOrgController.java index 3ad8d7a..a63efc1 100644 --- a/backend/src/main/java/com/imeeting/controller/SysOrgController.java +++ b/backend/src/main/java/com/imeeting/controller/SysOrgController.java @@ -20,26 +20,26 @@ public class SysOrgController { } @GetMapping - @PreAuthorize("@ss.hasPermi('sys_org:list')") + @PreAuthorize("@ss.hasPermi('sys:org:list')") public ApiResponse> 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 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 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 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 delete(@PathVariable Long id) { // Check if has children diff --git a/backend/src/main/java/com/imeeting/controller/SysParamController.java b/backend/src/main/java/com/imeeting/controller/SysParamController.java index e5e6841..733137c 100644 --- a/backend/src/main/java/com/imeeting/controller/SysParamController.java +++ b/backend/src/main/java/com/imeeting/controller/SysParamController.java @@ -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>> page(SysParamQueryDTO query) { + return ApiResponse.ok(sysParamService.page(query)); + } + @GetMapping @PreAuthorize("@ss.hasPermi('sys_param:list')") - public ApiResponse> list() { - return ApiResponse.ok(sysParamService.list()); + public ApiResponse> list() { + return ApiResponse.ok(sysParamService.list().stream().map(this::toVO).collect(Collectors.toList())); } @GetMapping("/{id}") @PreAuthorize("@ss.hasPermi('sys_param:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(sysParamService.getById(id)); + public ApiResponse 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; + } } diff --git a/backend/src/main/java/com/imeeting/controller/UserController.java b/backend/src/main/java/com/imeeting/controller/UserController.java index da3795d..bdf01c6 100644 --- a/backend/src/main/java/com/imeeting/controller/UserController.java +++ b/backend/src/main/java/com/imeeting/controller/UserController.java @@ -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(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) { Long currentTenantId = getCurrentTenantId(); + List users; + if (Long.valueOf(0).equals(currentTenantId) && tenantId == null) { - List 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 userRoles = sysUserRoleMapper.selectList( + new QueryWrapper().eq("user_id", user.getUserId()) + ); + if (userRoles != null && !userRoles.isEmpty()) { + List 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 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 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 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 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 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> listUserRoles(@PathVariable Long id) { List rows = sysUserRoleMapper.selectList( new QueryWrapper().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 saveUserRoles(@PathVariable Long id, @RequestBody RoleBindingPayload payload) { List roleIds = payload == null ? null : payload.getRoleIds(); sysUserRoleMapper.delete(new QueryWrapper().eq("user_id", id)); diff --git a/backend/src/main/java/com/imeeting/entity/SysDictItem.java b/backend/src/main/java/com/imeeting/entity/SysDictItem.java index 47da67b..49191c9 100644 --- a/backend/src/main/java/com/imeeting/entity/SysDictItem.java +++ b/backend/src/main/java/com/imeeting/entity/SysDictItem.java @@ -21,7 +21,4 @@ public class SysDictItem extends BaseEntity { @TableField(exist = false) private Long tenantId; - - @TableField(exist = false) - private Integer isDeleted; } diff --git a/backend/src/main/java/com/imeeting/entity/SysDictType.java b/backend/src/main/java/com/imeeting/entity/SysDictType.java index 9600d2e..905ae82 100644 --- a/backend/src/main/java/com/imeeting/entity/SysDictType.java +++ b/backend/src/main/java/com/imeeting/entity/SysDictType.java @@ -19,7 +19,4 @@ public class SysDictType extends BaseEntity { @TableField(exist = false) private Long tenantId; - - @TableField(exist = false) - private Integer isDeleted; } diff --git a/backend/src/main/java/com/imeeting/entity/SysParam.java b/backend/src/main/java/com/imeeting/entity/SysParam.java index 904ef6e..b353014 100644 --- a/backend/src/main/java/com/imeeting/entity/SysParam.java +++ b/backend/src/main/java/com/imeeting/entity/SysParam.java @@ -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; } diff --git a/backend/src/main/java/com/imeeting/entity/SysUser.java b/backend/src/main/java/com/imeeting/entity/SysUser.java index d70443e..13ae135 100644 --- a/backend/src/main/java/com/imeeting/entity/SysUser.java +++ b/backend/src/main/java/com/imeeting/entity/SysUser.java @@ -26,4 +26,7 @@ public class SysUser extends BaseEntity { @com.baomidou.mybatisplus.annotation.TableField(exist = false) private java.util.List memberships; + + @com.baomidou.mybatisplus.annotation.TableField(exist = false) + private java.util.List roles; } diff --git a/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java b/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java index 0b69567..737d2a4 100644 --- a/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java @@ -14,7 +14,7 @@ public interface SysPermissionMapper extends BaseMapper { @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} diff --git a/backend/src/main/java/com/imeeting/service/SysParamService.java b/backend/src/main/java/com/imeeting/service/SysParamService.java index 7428f91..8c04695 100644 --- a/backend/src/main/java/com/imeeting/service/SysParamService.java +++ b/backend/src/main/java/com/imeeting/service/SysParamService.java @@ -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 { + PageResult> page(SysParamQueryDTO query); + String getParamValue(String key, String defaultValue); String getCachedParamValue(String key, String defaultValue); diff --git a/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java index 7db63c3..f7ff1f2 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java @@ -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 i this.redisTemplate = redisTemplate; } + @Override + public PageResult> page(SysParamQueryDTO query) { + Page page = new Page<>(query.getPageNum(), query.getPageSize()); + LambdaQueryWrapper 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 result = this.baseMapper.selectPage(page, wrapper); + + PageResult> 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()) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index aaab273..47da648 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -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 diff --git a/design/用户管理.png b/design/用户管理.png deleted file mode 100644 index b446962..0000000 Binary files a/design/用户管理.png and /dev/null differ diff --git a/design/登录页.PNG b/design/登录页.PNG deleted file mode 100644 index 9989776..0000000 Binary files a/design/登录页.PNG and /dev/null differ diff --git a/design/角色新增.png b/design/角色新增.png deleted file mode 100644 index b96719a..0000000 Binary files a/design/角色新增.png and /dev/null differ diff --git a/design/角色管理.png b/design/角色管理.png deleted file mode 100644 index 37c51bc..0000000 Binary files a/design/角色管理.png and /dev/null differ diff --git a/design/首页.PNG b/design/首页.PNG deleted file mode 100644 index 39069fa..0000000 Binary files a/design/首页.PNG and /dev/null differ diff --git a/frontend/design/AGENTS.md b/frontend/design/AGENTS.md index ad417a4..ccfca13 100644 --- a/frontend/design/AGENTS.md +++ b/frontend/design/AGENTS.md @@ -47,7 +47,7 @@ src 你是一位**务实型前端开发者 Agent**,目标是: > 以清晰的数据流和稳定的交互,构建易维护的管理后台。 - +> 阅读对应的后端controller了解接口 ### 核心原则 * 清晰的意图胜于技巧性的实现 diff --git a/frontend/design/开发规范.md b/frontend/design/开发规范.md deleted file mode 100644 index e01bfcf..0000000 --- a/frontend/design/开发规范.md +++ /dev/null @@ -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 主题定制,自定义样式放组件目录 - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a7fa3c9..75d1b3e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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(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 ; } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 7e9f9e3..c7ee20b 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -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; +} + +export async function createParam(payload: Partial) { + const resp = await http.post("/api/params", payload); + return resp.data.data as boolean; +} + +export async function updateParam(id: number, payload: Partial) { + 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"; diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index e116479..fb035a0 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -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([]); const [availableTenants, setAvailableTenants] = useState([]); const [currentTenantId, setCurrentTenantId] = useState(null); + + const platformConfig = useMemo(() => { + 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' }}> - logo + logo {!collapsed && ( - MeetingAI + {platformConfig?.projectName || "MeetingAI"} )} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 5928763..6eaca23 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -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(null); const [captchaEnabled, setCaptchaEnabled] = useState(true); const [loading, setLoading] = useState(false); + const [platformConfig, setPlatformConfig] = useState(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 ( -
-
+
+
- MeetingAI Logo - MeetingAI + Logo + {platformConfig?.projectName || "MeetingAI"}
@@ -108,7 +134,7 @@ export default function Login() { {t('login.heroTitle3')}

- {t('login.heroDesc')} + {platformConfig?.systemDescription || t('login.heroDesc')}

@@ -116,10 +142,16 @@ export default function Login() {
{t('login.enterpriseSecurity')}
-
+
{t('login.welcome')} diff --git a/frontend/src/pages/Orgs.tsx b/frontend/src/pages/Orgs.tsx index fb58267..f77648f 100644 --- a/frontend/src/pages/Orgs.tsx +++ b/frontend/src/pages/Orgs.tsx @@ -204,13 +204,13 @@ export default function Orgs() { width: 180, render: (_: any, record: SysOrg) => ( - {can("sys_org:create") && ( + {can("sys:org:create") && ( )} - {can("sys_org:update") && ( + {can("sys:org:update") && (
- {can("sys_org:create") && ( + {can("sys:org:create") && ( diff --git a/frontend/src/pages/Permissions.tsx b/frontend/src/pages/Permissions.tsx index 0120fef..09fef02 100644 --- a/frontend/src/pages/Permissions.tsx +++ b/frontend/src/pages/Permissions.tsx @@ -247,7 +247,7 @@ export default function Permissions() { fixed: "right" as const, render: (_: any, record: SysPermission) => ( - {can("sys_permission:create") && record.permType === 'menu' && ( + {can("sys:permission:create") && record.permType === 'menu' && (
- {can("sys_permission:create") && ( + {can("sys:permission:create") && (
- {can("sys_role:update") && ( + {can("sys:role:update") && ( @@ -510,7 +510,7 @@ export default function Roles() { type="primary" icon={
- {can("sys_user:create") && ( + {can("sys:user:create") && ( @@ -498,68 +523,84 @@ export default function Users() { /> + {!isPlatformMode && ( + + + + )} + ({ label: t.tenantName, value: t.id }))} + disabled={!isPlatformMode} + placeholder="选择租户" + /> + + + + + + + + ))} + {isPlatformMode && ( + )} - > - - - -