diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigDto.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigDto.java index 491ba0c5..91caf151 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigDto.java @@ -25,6 +25,9 @@ public class SandboxConfigDto { @Schema(description = "用户ID(scope为user时必填)") private Long userId; + @Schema(description = "用户组/单位ID") + private Long groupId; + @Schema(description = "配置名称") private String name; @@ -81,4 +84,4 @@ public class SandboxConfigDto { @Schema(description = "更新时间") private Date modified; -} +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigQueryDto.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigQueryDto.java index 3461b1fe..06baf013 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigQueryDto.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/dto/SandboxConfigQueryDto.java @@ -4,6 +4,8 @@ import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.List; + /** * 沙盒配置查询 DTO */ @@ -17,6 +19,12 @@ public class SandboxConfigQueryDto { @Schema(description = "用户ID") private Long userId; + @Schema(description = "用户组/单位ID") + private Long groupId; + + @Schema(description = "用户组/单位ID列表") + private List groupIds; + @Schema(description = "配置名称(模糊查询)") private String name; diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/service/impl/SandboxConfigApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/service/impl/SandboxConfigApplicationServiceImpl.java index ce4c29e5..5eb0aa1a 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/service/impl/SandboxConfigApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-application/src/main/java/com/xspaceagi/sandbox/application/service/impl/SandboxConfigApplicationServiceImpl.java @@ -20,6 +20,8 @@ import com.xspaceagi.sandbox.infra.dao.vo.SandboxServerInfo; import com.xspaceagi.sandbox.infra.network.ReverseServerContainer; import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; import com.xspaceagi.system.application.dto.TenantConfigDto; +import com.xspaceagi.system.application.service.SysGroupApplicationService; +import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.enums.ErrorCodeEnum; import com.xspaceagi.system.spec.exception.BizException; @@ -27,6 +29,7 @@ import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.utils.RedisUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -41,6 +44,7 @@ import java.net.http.HttpResponse; import java.time.Duration; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -65,6 +69,9 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica @Resource private IAgentRpcService iAgentRpcService; + @Resource + private SysGroupApplicationService sysGroupApplicationService; + static { // disable keep alive,暂不使用连接池 System.setProperty("jdk.httpclient.keepalive.timeout", "0"); @@ -201,6 +208,7 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica if (entity.getScope() == SandboxScopeEnum.USER && entity.getUserId() == null) { entity.setUserId(RequestContext.get().getUserId()); } + bindAuthorizedGroupForUserConfig(entity); sandboxConfigService.save(entity); // 创建用户沙盒代理 @@ -240,6 +248,14 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica SandboxConfig entity = convertToEntity(dto); entity.setId(dto.getId()); entity.setModified(new Date()); + entity.setScope(existingEntity.getScope()); + entity.setUserId(existingEntity.getUserId()); + entity.setGroupId(existingEntity.getGroupId()); + if (existingEntity.getGroupId() == null) { + bindAuthorizedGroupForUserConfig(entity); + } else { + validateExistingGroupForUserConfig(entity); + } sandboxConfigService.updateById(entity); @@ -337,6 +353,14 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica queryWrapper.eq(SandboxConfig::getUserId, queryDto.getUserId()); } + if (queryDto.getGroupId() != null) { + queryWrapper.eq(SandboxConfig::getGroupId, queryDto.getGroupId()); + } + + if (CollectionUtils.isNotEmpty(queryDto.getGroupIds())) { + queryWrapper.in(SandboxConfig::getGroupId, queryDto.getGroupIds()); + } + if (StringUtils.isNotBlank(queryDto.getName())) { queryWrapper.like(SandboxConfig::getName, queryDto.getName()); } @@ -350,6 +374,44 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica return queryWrapper; } + private void bindAuthorizedGroupForUserConfig(SandboxConfig entity) { + if (entity.getScope() != SandboxScopeEnum.USER || entity.getUserId() == null) { + return; + } + List groups = sysGroupApplicationService.getEffectiveGroupListByUserId(entity.getUserId()); + if (groups.isEmpty()) { + return; + } + Map clientCountByGroupId = groups.stream() + .filter(group -> group.getId() != null) + .collect(Collectors.toMap( + SysGroup::getId, + group -> countUserClientsByGroupId(group.getId()), + (a, b) -> a)); + SysGroup group = sysGroupApplicationService.selectAuthorizedGroupForClient(entity.getUserId(), clientCountByGroupId); + if (group == null) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemGroupClientLimitExceeded); + } + entity.setGroupId(group.getId()); + } + + private long countUserClientsByGroupId(Long groupId) { + return sandboxConfigService.count(new LambdaQueryWrapper() + .eq(SandboxConfig::getScope, SandboxScopeEnum.USER) + .eq(SandboxConfig::getGroupId, groupId)); + } + + private void validateExistingGroupForUserConfig(SandboxConfig entity) { + if (entity.getScope() != SandboxScopeEnum.USER || entity.getUserId() == null || entity.getGroupId() == null) { + return; + } + boolean authorized = sysGroupApplicationService.getAuthorizedGroupListForLogin(entity.getUserId()).stream() + .anyMatch(group -> entity.getGroupId().equals(group.getId())); + if (!authorized) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemGroupAuthorizationUnavailable); + } + } + /** * 验证配置键唯一性 */ diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-infra/src/main/java/com/xspaceagi/sandbox/infra/dao/entity/SandboxConfig.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-infra/src/main/java/com/xspaceagi/sandbox/infra/dao/entity/SandboxConfig.java index 260bd7ca..1c2709b3 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-infra/src/main/java/com/xspaceagi/sandbox/infra/dao/entity/SandboxConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-infra/src/main/java/com/xspaceagi/sandbox/infra/dao/entity/SandboxConfig.java @@ -37,6 +37,9 @@ public class SandboxConfig implements Serializable { @TableField(value = "user_id") private Long userId; + @TableField(value = "group_id") + private Long groupId; + @TableField(value = "name") private String name; @@ -75,4 +78,4 @@ public class SandboxConfig implements Serializable { @TableField(value = "modified") private Date modified; -} +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigController.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigController.java index c992ebbe..2604b1ba 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigController.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigController.java @@ -199,15 +199,16 @@ public class SandboxConfigController { RequestContext.get().setLangMap(user.getLangMap()); } SandboxConfigDto dto = new SandboxConfigDto(); + String clientName = StringUtils.trimToNull(sandboxRegDto.getName()); dto.setUserId(user.getId()); dto.setScope(SandboxScopeEnum.USER); dto.setConfigKey(UUID.randomUUID().toString().replace("-", "")); - dto.setName(I18nUtil.systemMessage("Backend.Sandbox.MyComputer")); + dto.setName(clientName != null ? clientName : I18nUtil.systemMessage("Backend.Sandbox.MyComputer")); dto.setConfigValue(sandboxRegDto.getSandboxConfigValue()); dto.setDescription(""); dto.setIsActive(true); dto.setOnline(false); - sandboxConfigApplicationService.create(dto, false); + sandboxConfigApplicationService.create(dto, clientName != null); String token = authService.createToken(user, StringUtils.isNotBlank(sandboxRegDto.getDeviceId()) ? sandboxRegDto.getDeviceId() : UUID.randomUUID().toString().replace("-", "")); dto.setToken(token); return ReqResult.success(sandboxConfigApplicationService.getByKey(dto.getConfigKey())); diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigManageController.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigManageController.java index d15bd112..ab4d4a90 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigManageController.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/controller/SandboxConfigManageController.java @@ -5,9 +5,16 @@ import com.xspaceagi.sandbox.application.dto.SandboxConfigDto; import com.xspaceagi.sandbox.application.dto.SandboxConfigQueryDto; import com.xspaceagi.sandbox.application.dto.SandboxGlobalConfigDto; import com.xspaceagi.sandbox.application.service.SandboxConfigApplicationService; +import com.xspaceagi.system.application.dto.UserDto; +import com.xspaceagi.system.application.service.SysGroupApplicationService; +import com.xspaceagi.system.infra.dao.entity.SysGroup; +import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,6 +22,8 @@ import jakarta.annotation.Resource; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static com.xspaceagi.system.spec.enums.ResourceEnum.*; @@ -29,10 +38,15 @@ public class SandboxConfigManageController { @Resource private SandboxConfigApplicationService sandboxConfigApplicationService; + @Resource + private SysGroupApplicationService sysGroupApplicationService; + @RequireResource(SANDBOX_CONFIG_QUERY) @Operation(summary = "分页查询配置列表") @PostMapping("/page") public ReqResult> pageQuery(@RequestBody SandboxConfigQueryDto queryDto) { + queryDto = queryDto == null ? new SandboxConfigQueryDto() : queryDto; + applySandboxConfigManageScope(queryDto); return ReqResult.success(sandboxConfigApplicationService.pageQuery(queryDto)); } @@ -41,13 +55,16 @@ public class SandboxConfigManageController { @GetMapping("/get/{id}") public ReqResult getById( @Parameter(description = "配置ID") @PathVariable Long id) { - return ReqResult.success(sandboxConfigApplicationService.getById(id)); + SandboxConfigDto dto = sandboxConfigApplicationService.getById(id); + checkSandboxConfigManageScope(dto); + return ReqResult.success(dto); } @Operation(summary = "沙箱连通性测试") @GetMapping("/test/{id}") public ReqResult testConnection(@Parameter(description = "配置ID") @PathVariable Long id) { try { + checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(id)); sandboxConfigApplicationService.testConnection(id); } catch (Exception e) { return ReqResult.error("0007", e.getMessage()); @@ -60,7 +77,14 @@ public class SandboxConfigManageController { @GetMapping("/user/list") public ReqResult> listUserConfigsByType( @Parameter(description = "用户ID(为空时查询当前用户)") @RequestParam(required = false) Long userId) { - return ReqResult.success(sandboxConfigApplicationService.listUserConfigsByType(userId)); + List configs = sandboxConfigApplicationService.listUserConfigsByType(userId); + if (!isPlatformAdmin()) { + Set allowedGroupIds = currentUserGroupIds(); + configs = configs.stream() + .filter(config -> config.getGroupId() != null && allowedGroupIds.contains(config.getGroupId())) + .collect(Collectors.toList()); + } + return ReqResult.success(configs); } @RequireResource(SANDBOX_CONFIG_QUERY) @@ -74,6 +98,7 @@ public class SandboxConfigManageController { @Operation(summary = "创建配置") @PostMapping("/create") public ReqResult create(@RequestBody SandboxConfigDto dto) { + checkSandboxConfigManageScope(dto); sandboxConfigApplicationService.create(dto, false); return ReqResult.success(); } @@ -82,6 +107,7 @@ public class SandboxConfigManageController { @Operation(summary = "更新配置") @PostMapping("/update") public ReqResult update(@RequestBody SandboxConfigDto dto) { + checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(dto.getId())); sandboxConfigApplicationService.update(dto); return ReqResult.success(); } @@ -91,6 +117,7 @@ public class SandboxConfigManageController { @PostMapping("/delete/{id}") public ReqResult delete( @Parameter(description = "配置ID") @PathVariable Long id) { + checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(id)); sandboxConfigApplicationService.delete(id); return ReqResult.success(); } @@ -100,6 +127,7 @@ public class SandboxConfigManageController { @PostMapping("/toggle/{id}") public ReqResult toggle( @Parameter(description = "配置ID") @PathVariable Long id) { + checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(id)); sandboxConfigApplicationService.toggle(id); return ReqResult.success(); } @@ -119,4 +147,45 @@ public class SandboxConfigManageController { SandboxGlobalConfigDto globalConfig = sandboxConfigApplicationService.getGlobalConfig(RequestContext.get().getTenantId()); return ReqResult.success(globalConfig); } + + private void applySandboxConfigManageScope(SandboxConfigQueryDto queryDto) { + if (queryDto == null || isPlatformAdmin()) { + return; + } + Set allowedGroupIds = currentUserGroupIds(); + if (queryDto.getGroupId() != null) { + if (!allowedGroupIds.contains(queryDto.getGroupId())) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + return; + } + queryDto.setGroupIds(allowedGroupIds.isEmpty() ? List.of(-1L) : allowedGroupIds.stream().toList()); + } + + private void checkSandboxConfigManageScope(SandboxConfigDto dto) { + if (dto == null || isPlatformAdmin()) { + return; + } + if (dto.getGroupId() == null || !currentUserGroupIds().contains(dto.getGroupId())) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + } + + private Set currentUserGroupIds() { + return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream() + .map(SysGroup::getId) + .collect(Collectors.toSet()); + } + + private boolean isPlatformAdmin() { + return getCurrentUserDto().getRole() == User.Role.Admin; + } + + private UserDto getCurrentUserDto() { + UserDto userDto = (UserDto) RequestContext.get().getUser(); + if (userDto == null) { + throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb); + } + return userDto; + } } diff --git a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/dto/SandboxRegDto.java b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/dto/SandboxRegDto.java index d36b8fac..a0076fd0 100644 --- a/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/dto/SandboxRegDto.java +++ b/qiming-backend/app-platform-modules/app-platform-sandbox/app-platform-sandbox-ui/src/main/java/com/xspaceagi/sandbox/ui/web/dto/SandboxRegDto.java @@ -19,6 +19,9 @@ public class SandboxRegDto { @Schema(description = "设备ID") private String deviceId; + @Schema(description = "客户端注册名称") + private String name; + @Schema(description = "终端配置信息") private SandboxConfigValue sandboxConfigValue; } diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupAddDto.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupAddDto.java index 3af06b12..b833e423 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupAddDto.java +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupAddDto.java @@ -1,6 +1,7 @@ package com.xspaceagi.system.application.dto.permission; import java.io.Serializable; +import java.util.Date; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -20,6 +21,15 @@ public class SysGroupAddDto implements Serializable { @Schema(description = "最大用户数") private Integer maxUserCount; + @Schema(description = "最大客户端数,-1表示不限制") + private Integer maxClientCount; + + @Schema(description = "授权开关:1-已授权,0-未授权") + private Integer authEnabled; + + @Schema(description = "授权到期时间,空表示不过期") + private Date expireTime; + @Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true) private Integer source; diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupDto.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupDto.java index 16b67ff4..140cc586 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupDto.java +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupDto.java @@ -31,6 +31,15 @@ public class SysGroupDto implements Serializable { @Schema(description = "最大用户数") private Integer maxUserCount; + @Schema(description = "最大客户端数,-1表示不限制") + private Integer maxClientCount; + + @Schema(description = "授权开关:1-已授权,0-未授权") + private Integer authEnabled; + + @Schema(description = "授权到期时间,空表示不过期") + private Date expireTime; + @Schema(description = "来源,1:系统内置 2:用户自定义") private Integer source; diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupUpdateDto.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupUpdateDto.java index 227639b0..96a388fd 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupUpdateDto.java +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/dto/permission/SysGroupUpdateDto.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; +import java.util.Date; @Data public class SysGroupUpdateDto implements Serializable { @@ -23,6 +24,15 @@ public class SysGroupUpdateDto implements Serializable { @Schema(description = "最大用户数") private Integer maxUserCount; + @Schema(description = "最大客户端数,-1表示不限制") + private Integer maxClientCount; + + @Schema(description = "授权开关:1-已授权,0-未授权") + private Integer authEnabled; + + @Schema(description = "授权到期时间,空表示不过期") + private Date expireTime; + @Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true) private Integer source; diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/SysGroupApplicationService.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/SysGroupApplicationService.java index 6cf56da7..f48fae87 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/SysGroupApplicationService.java +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/SysGroupApplicationService.java @@ -76,6 +76,16 @@ public interface SysGroupApplicationService { */ List getEffectiveGroupListByUserId(Long userId); + /** + * 查询用户可用于系统登录的已授权用户组(启用、授权开启、未过期) + */ + List getAuthorizedGroupListForLogin(Long userId); + + /** + * 为用户选择一个可注册客户端的已授权用户组(同时满足客户端数量限制) + */ + SysGroup selectAuthorizedGroupForClient(Long userId, java.util.Map clientCountByGroupId); + /** * 用户组绑定用户(全量覆盖) */ diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/AuthServiceImpl.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/AuthServiceImpl.java index 6bcb92f2..52145244 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/AuthServiceImpl.java +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/AuthServiceImpl.java @@ -3,13 +3,16 @@ package com.xspaceagi.system.application.service.impl; import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.AuthService; +import com.xspaceagi.system.application.service.SysGroupApplicationService; import com.xspaceagi.system.application.service.UserApplicationService; import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.infra.rpc.WeChatMpService; import com.xspaceagi.system.infra.verify.VerifyCodeSendAndCheckService; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.enums.CodeTypeEnum; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.utils.I18nUtil; import com.xspaceagi.system.spec.utils.JwtUtils; import com.xspaceagi.system.spec.utils.RedisUtil; @@ -28,6 +31,9 @@ public class AuthServiceImpl implements AuthService { @Resource private UserApplicationService userApplicationService; + @Resource + private SysGroupApplicationService sysGroupApplicationService; + @Resource private VerifyCodeSendAndCheckService verifyCodeSendAndCheckService; @@ -66,9 +72,7 @@ public class AuthServiceImpl implements AuthService { userDto.setEmail(email); userApplicationService.add(userDto); } - if (userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) { - throw new BizException(I18nUtil.systemMessage("Backend.Auth.Login.AccountDisabled")); - } + validateLoginUser(userDto); updateLastLoginTime(userDto); return createToken(userDto, UUID.randomUUID().toString().replace("-", "")); } @@ -87,13 +91,23 @@ public class AuthServiceImpl implements AuthService { userDto.setPhone(phone); userApplicationService.add(userDto); } - if (userDto.getStatus() == User.Status.Disabled) { - throw new BizException(I18nUtil.systemMessage("Backend.Auth.Login.AccountDisabled")); - } + validateLoginUser(userDto); updateLastLoginTime(userDto); return createToken(userDto, UUID.randomUUID().toString().replace("-", "")); } + private void validateLoginUser(UserDto userDto) { + if (userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) { + throw new BizException(I18nUtil.systemMessage("Backend.Auth.Login.AccountDisabled")); + } + if (userDto.getRole() == User.Role.Admin) { + return; + } + if (sysGroupApplicationService.getAuthorizedGroupListForLogin(userDto.getId()).isEmpty()) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemGroupAuthorizationUnavailable); + } + } + private void updateLastLoginTime(UserDto userDto) { UserDto update = new UserDto(); update.setId(userDto.getId()); @@ -105,6 +119,7 @@ public class AuthServiceImpl implements AuthService { } public String createToken(UserDto userDto, String clientId) { + validateLoginUser(userDto); //过期token检查 Map map = redisUtil.hashGetAll("user-token:" + userDto.getId()); for (Map.Entry entry : map.entrySet()) { @@ -127,6 +142,7 @@ public class AuthServiceImpl implements AuthService { Assert.notNull(emailOrPhone, "emailOrPhone must be non-null"); UserDto userDto = userApplicationService.queryUserByPhoneOrEmailWithPassword(emailOrPhone, password); if (userDto != null) { + validateLoginUser(userDto); updateLastLoginTime(userDto); return createToken(userDto, UUID.randomUUID().toString().replace("-", "")); } diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImpl.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImpl.java index a9ce0076..79df40a0 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/main/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImpl.java @@ -2,9 +2,11 @@ package com.xspaceagi.system.application.service.impl; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -23,6 +25,7 @@ import com.xspaceagi.system.domain.model.GroupBindMenuModel; import com.xspaceagi.system.domain.model.MenuNode; import com.xspaceagi.system.domain.model.SortIndex; import com.xspaceagi.system.domain.service.SysGroupDomainService; +import com.xspaceagi.system.domain.service.impl.GroupAuthorizationPolicy; import com.xspaceagi.system.infra.dao.entity.SysDataPermission; import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.infra.dao.entity.User; @@ -141,6 +144,39 @@ public class SysGroupApplicationServiceImpl implements SysGroupApplicationServic : groupList.stream().filter(g -> StatusEnum.isEnabled(g.getStatus())).toList(); } + @Override + public List getAuthorizedGroupListForLogin(Long userId) { + return filterAuthorizedGroupsForLogin(getEffectiveGroupListByUserId(userId)); + } + + @Override + public SysGroup selectAuthorizedGroupForClient(Long userId, Map clientCountByGroupId) { + return selectAuthorizedGroupForClient(getEffectiveGroupListByUserId(userId), clientCountByGroupId); + } + + List filterAuthorizedGroupsForLogin(List groups) { + if (CollectionUtils.isEmpty(groups)) { + return List.of(); + } + Date now = new Date(); + return groups.stream() + .filter(group -> GroupAuthorizationPolicy.evaluate(group, -1, now).allowed()) + .toList(); + } + + SysGroup selectAuthorizedGroupForClient(List groups, Map clientCountByGroupId) { + if (CollectionUtils.isEmpty(groups)) { + return null; + } + Date now = new Date(); + Map counts = clientCountByGroupId == null ? Map.of() : clientCountByGroupId; + return groups.stream() + .filter(group -> group.getId() != null) + .filter(group -> GroupAuthorizationPolicy.evaluate(group, counts.getOrDefault(group.getId(), 0L), now).allowed()) + .findFirst() + .orElse(null); + } + private List getSubscriptionPlanGroupIds(Long userId) { try { UserSubscriptionDTO subscription = subscriptionRpcService.getUserCurrentSystemSubscription(userId); diff --git a/qiming-backend/app-platform-modules/platform-system/system-application/src/test/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImplAuthorizationTest.java b/qiming-backend/app-platform-modules/platform-system/system-application/src/test/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImplAuthorizationTest.java new file mode 100644 index 00000000..82404ba3 --- /dev/null +++ b/qiming-backend/app-platform-modules/platform-system/system-application/src/test/java/com/xspaceagi/system/application/service/impl/SysGroupApplicationServiceImplAuthorizationTest.java @@ -0,0 +1,58 @@ +package com.xspaceagi.system.application.service.impl; + +import com.xspaceagi.system.infra.dao.entity.SysGroup; +import com.xspaceagi.system.spec.enums.StatusEnum; +import org.junit.jupiter.api.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class SysGroupApplicationServiceImplAuthorizationTest { + + @Test + void filtersGroupsWithoutActiveAuthorizationForLogin() { + SysGroupApplicationServiceImpl service = new SysGroupApplicationServiceImpl(); + + List result = service.filterAuthorizedGroupsForLogin(List.of( + group(1L, 0, future(), -1), + group(2L, 1, past(), -1), + group(3L, 1, future(), -1) + )); + + assertThat(result).extracting(SysGroup::getId).containsExactly(3L); + } + + @Test + void selectsAuthorizedGroupWhoseClientQuotaIsAvailable() { + SysGroupApplicationServiceImpl service = new SysGroupApplicationServiceImpl(); + + SysGroup result = service.selectAuthorizedGroupForClient(List.of( + group(1L, 1, future(), 1), + group(2L, 1, future(), 2) + ), Map.of(1L, 1L, 2L, 1L)); + + assertThat(result).isNotNull(); + assertThat(result.getId()).isEqualTo(2L); + } + + private SysGroup group(Long id, Integer authEnabled, Date expireTime, Integer maxClientCount) { + SysGroup group = new SysGroup(); + group.setId(id); + group.setStatus(StatusEnum.ENABLED.getCode()); + group.setAuthEnabled(authEnabled); + group.setExpireTime(expireTime); + group.setMaxClientCount(maxClientCount); + return group; + } + + private Date future() { + return new Date(System.currentTimeMillis() + 86_400_000L); + } + + private Date past() { + return new Date(System.currentTimeMillis() - 86_400_000L); + } +} diff --git a/qiming-backend/app-platform-modules/platform-system/system-domain/src/main/java/com/xspaceagi/system/domain/service/impl/GroupAuthorizationPolicy.java b/qiming-backend/app-platform-modules/platform-system/system-domain/src/main/java/com/xspaceagi/system/domain/service/impl/GroupAuthorizationPolicy.java new file mode 100644 index 00000000..38e3559a --- /dev/null +++ b/qiming-backend/app-platform-modules/platform-system/system-domain/src/main/java/com/xspaceagi/system/domain/service/impl/GroupAuthorizationPolicy.java @@ -0,0 +1,55 @@ +package com.xspaceagi.system.domain.service.impl; + +import com.xspaceagi.system.infra.dao.entity.SysGroup; +import com.xspaceagi.system.spec.enums.StatusEnum; + +import java.util.Date; + +/** + * Centralized department authorization rule for login and client registration. + */ +public final class GroupAuthorizationPolicy { + + private GroupAuthorizationPolicy() { + } + + public static Result evaluate(SysGroup group, long currentClientCount, Date now) { + if (group == null) { + return Result.deny(Reason.NO_GROUP); + } + if (!StatusEnum.isEnabled(group.getStatus())) { + return Result.deny(Reason.GROUP_DISABLED); + } + if (!Integer.valueOf(1).equals(group.getAuthEnabled())) { + return Result.deny(Reason.AUTH_DISABLED); + } + Date expireTime = group.getExpireTime(); + if (expireTime != null && !expireTime.after(now)) { + return Result.deny(Reason.EXPIRED); + } + Integer maxClientCount = group.getMaxClientCount(); + if (maxClientCount != null && maxClientCount > -1 && currentClientCount >= maxClientCount) { + return Result.deny(Reason.CLIENT_QUOTA_REACHED); + } + return Result.allow(); + } + + public enum Reason { + ALLOWED, + NO_GROUP, + GROUP_DISABLED, + AUTH_DISABLED, + EXPIRED, + CLIENT_QUOTA_REACHED + } + + public record Result(boolean allowed, Reason reason) { + private static Result allow() { + return new Result(true, Reason.ALLOWED); + } + + private static Result deny(Reason reason) { + return new Result(false, reason); + } + } +} diff --git a/qiming-backend/app-platform-modules/platform-system/system-domain/src/test/java/com/xspaceagi/system/domain/service/impl/GroupAuthorizationPolicyTest.java b/qiming-backend/app-platform-modules/platform-system/system-domain/src/test/java/com/xspaceagi/system/domain/service/impl/GroupAuthorizationPolicyTest.java new file mode 100644 index 00000000..a4dbaf8d --- /dev/null +++ b/qiming-backend/app-platform-modules/platform-system/system-domain/src/test/java/com/xspaceagi/system/domain/service/impl/GroupAuthorizationPolicyTest.java @@ -0,0 +1,90 @@ +package com.xspaceagi.system.domain.service.impl; + +import com.xspaceagi.system.infra.dao.entity.SysGroup; +import com.xspaceagi.system.spec.enums.StatusEnum; +import org.junit.jupiter.api.Test; + +import java.util.Calendar; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +class GroupAuthorizationPolicyTest { + + @Test + void allowsEnabledAuthorizedGroupBeforeExpirationWhenClientQuotaAvailable() { + SysGroup group = group(StatusEnum.ENABLED.getCode(), 1, future(), 2); + + GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 1, now()); + + assertThat(result.allowed()).isTrue(); + assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.ALLOWED); + } + + @Test + void deniesDisabledGroup() { + SysGroup group = group(StatusEnum.DISABLED.getCode(), 1, future(), 2); + + GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 0, now()); + + assertThat(result.allowed()).isFalse(); + assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.GROUP_DISABLED); + } + + @Test + void deniesGroupWithoutAuthorization() { + SysGroup group = group(StatusEnum.ENABLED.getCode(), 0, future(), 2); + + GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 0, now()); + + assertThat(result.allowed()).isFalse(); + assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.AUTH_DISABLED); + } + + @Test + void deniesExpiredGroup() { + SysGroup group = group(StatusEnum.ENABLED.getCode(), 1, past(), 2); + + GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 0, now()); + + assertThat(result.allowed()).isFalse(); + assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.EXPIRED); + } + + @Test + void deniesWhenClientQuotaReached() { + SysGroup group = group(StatusEnum.ENABLED.getCode(), 1, future(), 2); + + GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 2, now()); + + assertThat(result.allowed()).isFalse(); + assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.CLIENT_QUOTA_REACHED); + } + + private SysGroup group(Integer status, Integer authEnabled, Date expireTime, Integer maxClientCount) { + SysGroup group = new SysGroup(); + group.setStatus(status); + group.setAuthEnabled(authEnabled); + group.setExpireTime(expireTime); + group.setMaxClientCount(maxClientCount); + return group; + } + + private Date now() { + return new Date(1_700_000_000_000L); + } + + private Date future() { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now()); + calendar.add(Calendar.DATE, 1); + return calendar.getTime(); + } + + private Date past() { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now()); + calendar.add(Calendar.DATE, -1); + return calendar.getTime(); + } +} diff --git a/qiming-backend/app-platform-modules/platform-system/system-infra/src/main/java/com/xspaceagi/system/infra/dao/entity/SysGroup.java b/qiming-backend/app-platform-modules/platform-system/system-infra/src/main/java/com/xspaceagi/system/infra/dao/entity/SysGroup.java index 92d907c6..a95ec602 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-infra/src/main/java/com/xspaceagi/system/infra/dao/entity/SysGroup.java +++ b/qiming-backend/app-platform-modules/platform-system/system-infra/src/main/java/com/xspaceagi/system/infra/dao/entity/SysGroup.java @@ -43,6 +43,21 @@ public class SysGroup { */ private Integer maxUserCount; + /** + * 最大客户端数,-1 表示不限制 + */ + private Integer maxClientCount; + + /** + * 授权开关:1-已授权,0-未授权 + */ + private Integer authEnabled; + + /** + * 授权到期时间,空表示不过期 + */ + private Date expireTime; + /** * 来源:1-系统内置,2-用户自定义 * @see SourceEnum diff --git a/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/exception/BizExceptionCodeEnum.java b/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/exception/BizExceptionCodeEnum.java index f5652e1a..10af8dbf 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/exception/BizExceptionCodeEnum.java +++ b/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/exception/BizExceptionCodeEnum.java @@ -120,6 +120,8 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum { systemRoleNotFoundWithRoleId("3122", "角色不存在,id=%s", "Role does not exist, id=%s", "RBAC"), systemGroupMaxUsersBelowBound("3123", "最大用户数不能小于已绑定用户数,当前已绑定%s人", "Max users cannot be below the number already bound; currently %s user(s) bound.", "RBAC"), systemGroupUserLimitExceeded("3124", "组用户数量超出限制", "Group user count exceeds the limit.", "RBAC"), + systemGroupAuthorizationUnavailable("3186", "当前用户组未授权或授权已过期", "The current user group is not authorized or the authorization has expired.", "RBAC"), + systemGroupClientLimitExceeded("3187", "用户组客户端数量超出限制", "Group client count exceeds the limit.", "RBAC"), systemGroupNotFoundWithRowId("3125", "用户组不存在: id=%s", "User group does not exist: id=%s", "RBAC"), systemGroupNotFoundWithGroupId("3126", "组不存在,id=%s", "Group does not exist, id=%s", "RBAC"), systemGroupNotFoundWithGroupIdAlt("3127", "用户组不存在,id=%s", "User group does not exist, id=%s", "RBAC"), diff --git a/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysGroupController.java b/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysGroupController.java index 52887094..ab1c843b 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysGroupController.java +++ b/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysGroupController.java @@ -9,6 +9,7 @@ import com.xspaceagi.system.application.dto.permission.*; import com.xspaceagi.system.application.service.SysDataPermissionApplicationService; import com.xspaceagi.system.application.service.SysGroupApplicationService; import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationService; +import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.domain.model.GroupBindMenuModel; import com.xspaceagi.system.domain.model.MenuNode; import com.xspaceagi.system.domain.model.SortIndex; @@ -16,6 +17,7 @@ import com.xspaceagi.system.infra.dao.entity.SysDataPermission; import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.spec.annotation.RequireResource; +import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.PageQueryVo; import com.xspaceagi.system.spec.dto.ReqResult; import com.xspaceagi.system.spec.enums.*; @@ -36,6 +38,7 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import static com.xspaceagi.system.spec.enums.ResourceEnum.*; @@ -89,6 +92,7 @@ public class SysGroupController extends BaseController { SysGroup group = new SysGroup(); BeanUtils.copyProperties(dto, group); + checkGroupManageScope(group.getId()); group.setCode(null); // code不允许更新 sysGroupApplicationService.updateGroup(group, getUser()); return ReqResult.success(); @@ -101,6 +105,7 @@ public class SysGroupController extends BaseController { if (groupId == null) { return ReqResult.error("参数不能为空"); } + checkGroupManageScope(groupId); sysGroupApplicationService.deleteGroup(groupId, getUser()); return ReqResult.success(); } @@ -112,6 +117,7 @@ public class SysGroupController extends BaseController { if (groupId == null) { return ReqResult.error("参数不能为空"); } + checkGroupManageScope(groupId); SysGroup group = sysGroupApplicationService.getGroupById(groupId); if (group == null) { @@ -141,6 +147,7 @@ public class SysGroupController extends BaseController { if (group == null) { return ReqResult.error("用户组不存在"); } + checkGroupManageScope(group.getId()); SysGroupDto groupDto = new SysGroupDto(); BeanUtils.copyProperties(group, groupDto); @@ -162,6 +169,7 @@ public class SysGroupController extends BaseController { } List sortIndexList = dto.getItems().stream() .map(item -> { + checkGroupManageScope(item.getId()); SortIndex model = new SortIndex(); model.setId(item.getId()); model.setParentId(item.getParentId()); @@ -183,6 +191,7 @@ public class SysGroupController extends BaseController { } List groupList = sysGroupApplicationService.getGroupList(sysGroup); + groupList = filterGroupManageScope(groupList); if (CollectionUtils.isEmpty(groupList)) { return ReqResult.success(); } @@ -214,6 +223,7 @@ public class SysGroupController extends BaseController { } SysGroupUserQueryDto queryDto = pageQueryVo.getQueryFilter(); Long groupId = queryDto.getGroupId(); + checkGroupManageScope(groupId); String userName = queryDto.getUserName(); long pageNo = pageQueryVo.getPageNo() != null ? pageQueryVo.getPageNo() : 1L; long pageSize = pageQueryVo.getPageSize() != null ? pageQueryVo.getPageSize() : 10L; @@ -238,6 +248,7 @@ public class SysGroupController extends BaseController { if (groupId == null) { return ReqResult.error("参数不能为空"); } + checkGroupManageScope(groupId); SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.GROUP, groupId); SysDataPermissionBindDto result = SysDataPermissionConverter.toDto(dataPermission); result = result == null ? new SysDataPermissionBindDto() : result; @@ -282,6 +293,7 @@ public class SysGroupController extends BaseController { if (dto == null || dto.getGroupId() == null) { return ReqResult.error("用户组ID不能为空"); } + checkGroupManageScope(dto.getGroupId()); SysDataPermissionBindDto bindDto = dto.getDataPermission(); SysDataPermission dataPermission = SysDataPermissionConverter.toEntity(bindDto); sysGroupApplicationService.bindDataPermission(dto.getGroupId(), dataPermission, getUser()); @@ -295,6 +307,7 @@ public class SysGroupController extends BaseController { if (dto == null) { return ReqResult.error("参数不能为空"); } + checkGroupManageScope(dto.getGroupId()); sysGroupApplicationService.groupBindUser(dto.getGroupId(), dto.getUserIds(), getUser()); return ReqResult.success(); } @@ -306,6 +319,7 @@ public class SysGroupController extends BaseController { if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) { return ReqResult.error("用户组ID和用户ID不能为空"); } + checkGroupManageScope(dto.getGroupId()); sysGroupApplicationService.groupAddUser(dto.getGroupId(), dto.getUserId(), getUser()); return ReqResult.success(); } @@ -317,6 +331,7 @@ public class SysGroupController extends BaseController { if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) { return ReqResult.error("用户组ID和用户ID不能为空"); } + checkGroupManageScope(dto.getGroupId()); sysGroupApplicationService.groupRemoveUser(dto.getGroupId(), dto.getUserId(), getUser()); return ReqResult.success(); } @@ -328,6 +343,7 @@ public class SysGroupController extends BaseController { if (dto == null) { return ReqResult.error("参数不能为空"); } + checkGroupManageScope(dto.getGroupId()); // 用户组不允许绑定 生态市场/系统管理 checkForbiddenMenuCodes(dto.getMenuTree()); @@ -343,6 +359,7 @@ public class SysGroupController extends BaseController { if (groupId == null) { return ReqResult.error("参数不能为空"); } + checkGroupManageScope(groupId); // 获取处理好的菜单树(已包含资源详细信息) List menuNodeList = sysGroupApplicationService.getMenuTreeByGroupId(groupId); @@ -373,4 +390,41 @@ public class SysGroupController extends BaseController { } } } -} \ No newline at end of file + + private List filterGroupManageScope(List groupList) { + if (isPlatformAdmin() || CollectionUtils.isEmpty(groupList)) { + return groupList; + } + Set allowedGroupIds = currentUserGroupIds(); + return groupList.stream() + .filter(group -> group.getId() != null && allowedGroupIds.contains(group.getId())) + .collect(Collectors.toList()); + } + + private void checkGroupManageScope(Long groupId) { + if (isPlatformAdmin()) { + return; + } + if (groupId == null || !currentUserGroupIds().contains(groupId)) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + } + + private Set currentUserGroupIds() { + return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream() + .map(SysGroup::getId) + .collect(Collectors.toSet()); + } + + private boolean isPlatformAdmin() { + return getCurrentUserDto().getRole() == User.Role.Admin; + } + + private UserDto getCurrentUserDto() { + UserDto userDto = (UserDto) RequestContext.get().getUser(); + if (userDto == null) { + throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb); + } + return userDto; + } +} diff --git a/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysUserController.java b/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysUserController.java index 94a17014..1b0971b9 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysUserController.java +++ b/qiming-backend/app-platform-modules/platform-system/system-web/src/main/java/com/xspaceagi/system/web/controller/permission/SysUserController.java @@ -1,5 +1,6 @@ package com.xspaceagi.system.web.controller.permission; +import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.dto.permission.*; import com.xspaceagi.system.application.service.SysDataPermissionApplicationService; import com.xspaceagi.system.application.service.SysGroupApplicationService; @@ -7,10 +8,15 @@ import com.xspaceagi.system.application.service.SysRoleApplicationService; import com.xspaceagi.system.application.service.impl.SysUserPermissionCacheServiceImpl; import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.infra.dao.entity.SysRole; +import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.annotation.SaasAdmin; +import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.utils.I18nUtil; import com.xspaceagi.system.web.controller.base.BaseController; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +28,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static com.xspaceagi.system.spec.enums.ResourceEnum.*; @@ -48,6 +55,7 @@ public class SysUserController extends BaseController { if (userId == null) { return ReqResult.error("参数不能为空"); } + checkUserManageScope(userId); List roleList = sysRoleApplicationService.getRoleListByUserId(userId); if (CollectionUtils.isEmpty(roleList)) { @@ -70,6 +78,8 @@ public class SysUserController extends BaseController { if (dto == null) { return ReqResult.error("参数不能为空"); } + checkUserManageScope(dto.getUserId()); + checkRoleBindScope(dto.getRoleIds()); sysRoleApplicationService.userBindRole(dto.getUserId(), dto.getRoleIds(), getUser()); return ReqResult.success(); } @@ -81,6 +91,7 @@ public class SysUserController extends BaseController { if (userId == null) { return ReqResult.error("参数不能为空"); } + checkUserManageScope(userId); List groupList = sysGroupApplicationService.getGroupListByUserId(userId); if (CollectionUtils.isEmpty(groupList)) { return ReqResult.success(); @@ -102,6 +113,8 @@ public class SysUserController extends BaseController { if (dto == null) { return ReqResult.error("参数不能为空"); } + checkUserManageScope(dto.getUserId()); + checkGroupBindScope(dto.getGroupIds()); sysGroupApplicationService.userBindGroup(dto.getUserId(), dto.getGroupIds(), getUser()); return ReqResult.success(); } @@ -113,6 +126,7 @@ public class SysUserController extends BaseController { if (userId == null) { return ReqResult.error("参数不能为空"); } + checkUserManageScope(userId); List menuTree = sysUserPermissionCacheService.getUserMenuTree(userId); I18nUtil.replaceSystemMessage(menuTree); @@ -126,10 +140,66 @@ public class SysUserController extends BaseController { if (userId == null) { return ReqResult.error("参数不能为空"); } + checkUserManageScope(userId); UserDataPermissionDto dataPermission = sysDataPermissionApplicationService.getUserDataPermission(userId); return ReqResult.success(dataPermission); } + private void checkUserManageScope(Long userId) { + if (isPlatformAdmin()) { + return; + } + Set allowedGroupIds = currentUserGroupIds(); + boolean inScope = sysGroupApplicationService.getEffectiveGroupListByUserId(userId).stream() + .map(SysGroup::getId) + .anyMatch(allowedGroupIds::contains); + if (!inScope) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + } + + private void checkGroupBindScope(List groupIds) { + if (isPlatformAdmin() || CollectionUtils.isEmpty(groupIds)) { + return; + } + Set allowedGroupIds = currentUserGroupIds(); + boolean allInScope = groupIds.stream().allMatch(allowedGroupIds::contains); + if (!allInScope) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + } + + private void checkRoleBindScope(List roleIds) { + if (isPlatformAdmin() || CollectionUtils.isEmpty(roleIds)) { + return; + } + Set allowedRoleIds = sysRoleApplicationService.getRoleListByUserId(getCurrentUserDto().getId()).stream() + .map(SysRole::getId) + .collect(Collectors.toSet()); + boolean allInScope = roleIds.stream().allMatch(allowedRoleIds::contains); + if (!allInScope) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + } + + private Set currentUserGroupIds() { + return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream() + .map(SysGroup::getId) + .collect(Collectors.toSet()); + } + + private boolean isPlatformAdmin() { + return getCurrentUserDto().getRole() == User.Role.Admin; + } + + private UserDto getCurrentUserDto() { + UserDto userDto = (UserDto) RequestContext.get().getUser(); + if (userDto == null) { + throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb); + } + return userDto; + } + @SaasAdmin @Operation(summary = "清除指定用户权限缓存") @GetMapping("/cache/clear-user") @@ -148,4 +218,4 @@ public class SysUserController extends BaseController { sysUserPermissionCacheService.clearCacheAllByTenant(tenantId); return ReqResult.success(); } -} \ No newline at end of file +} diff --git a/qiming-backend/sql/init.sql b/qiming-backend/sql/init.sql index 937cfb8e..9bca6446 100644 --- a/qiming-backend/sql/init.sql +++ b/qiming-backend/sql/init.sql @@ -929,6 +929,7 @@ CREATE TABLE `sandbox_config` `_tenant_id` bigint(20) NOT NULL COMMENT '租户id', `scope` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置范围:global-全局配置 user-个人配置', `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID(scope为user时必填)', + `group_id` bigint(20) DEFAULT NULL COMMENT '用户组/单位ID', `agent_id` bigint(20) DEFAULT NULL COMMENT '智能体电脑绑定的agentId', `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置名称(用于界面显示)', `config_key` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '唯一标识', @@ -1120,6 +1121,9 @@ CREATE TABLE `sys_group` `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称', `description` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述', `max_user_count` int(11) DEFAULT NULL COMMENT '最大用户数', + `max_client_count` int(11) DEFAULT -1 COMMENT '最大客户端数,-1表示不限制', + `auth_enabled` tinyint(4) DEFAULT 0 COMMENT '授权开关:1-已授权,0-未授权', + `expire_time` datetime DEFAULT NULL COMMENT '授权到期时间,空表示不过期', `source` tinyint(4) NOT NULL COMMENT '来源:1-系统内置,2-用户自定义,对应 SourceEnum', `status` tinyint(4) NOT NULL COMMENT '状态:1-启用,0-禁用', `sort_index` int(11) DEFAULT NULL COMMENT '排序', @@ -1906,6 +1910,7 @@ ALTER TABLE `sandbox_config` ADD UNIQUE KEY `uk_scope_user_key` (`scope`,`user_id`,`config_key`), ADD KEY `idx_scope` (`scope`), ADD KEY `idx_user_id` (`user_id`), + ADD KEY `idx_group_id` (`group_id`), ADD KEY `idx_config_key` (`config_key`), ADD KEY `idx_is_active` (`is_active`); @@ -3180,4 +3185,4 @@ ALTER TABLE `pay_order` ALTER TABLE `pay_order` ADD KEY `idx_reconcile_scan` (`gateway_sync_status`, `gateway_order_status`, `modified`, `id`); ALTER TABLE `pay_order` - ADD UNIQUE KEY `uk_tenant_biz_order` (`_tenant_id`, `biz_order_no`); \ No newline at end of file + ADD UNIQUE KEY `uk_tenant_biz_order` (`_tenant_id`, `biz_order_no`); diff --git a/qiming-backend/sql/update-20260530.sql b/qiming-backend/sql/update-20260530.sql new file mode 100644 index 00000000..9df3a5e2 --- /dev/null +++ b/qiming-backend/sql/update-20260530.sql @@ -0,0 +1,8 @@ +-- 修改表: sys_group +ALTER TABLE `sys_group` ADD COLUMN `max_client_count` int(11) DEFAULT -1 COMMENT '最大客户端数,-1表示不限制' AFTER `max_user_count`; +ALTER TABLE `sys_group` ADD COLUMN `auth_enabled` tinyint(4) DEFAULT 0 COMMENT '授权开关:1-已授权,0-未授权' AFTER `max_client_count`; +ALTER TABLE `sys_group` ADD COLUMN `expire_time` datetime DEFAULT NULL COMMENT '授权到期时间,空表示不过期' AFTER `auth_enabled`; + +-- 修改表: sandbox_config +ALTER TABLE `sandbox_config` ADD COLUMN `group_id` bigint(20) DEFAULT NULL COMMENT '用户组/单位ID' AFTER `user_id`; +ALTER TABLE `sandbox_config` ADD KEY `idx_group_id` (`group_id`); diff --git a/qiming/src/locales/i18n/zh-CN.ts b/qiming/src/locales/i18n/zh-CN.ts index d8976a81..0416cb09 100644 --- a/qiming/src/locales/i18n/zh-CN.ts +++ b/qiming/src/locales/i18n/zh-CN.ts @@ -1088,7 +1088,7 @@ export const ZH_CN: SystemLangMap = { "PC.Constants.System.systemSetting": "系统设置", "PC.Constants.System.taskManage": "任务管理", "PC.Constants.System.themeConfig": "主题配置", - "PC.Constants.System.userGroupManage": "用户组管理", + "PC.Constants.System.userGroupManage": "单位(部门)管理", "PC.Constants.System.userManage": "用户管理", "PC.Constants.Theme.bgCloudyDay": "云朵白天", "PC.Constants.Theme.bgCloudyDayDesc": "浅色背景,适合浅色布局风格", @@ -4530,8 +4530,8 @@ export const ZH_CN: SystemLangMap = { "PC.Pages.SystemTargetAuthModal.roleManagement": "角色管理", "PC.Pages.SystemTargetAuthModal.roleTab": "角色", "PC.Pages.SystemTargetAuthModal.selectAll": "全选", - "PC.Pages.SystemTargetAuthModal.userGroupManagement": "用户组管理", - "PC.Pages.SystemTargetAuthModal.userGroupTab": "用户组", + "PC.Pages.SystemTargetAuthModal.userGroupManagement": "单位(部门)管理", + "PC.Pages.SystemTargetAuthModal.userGroupTab": "单位(部门)", "PC.Pages.SystemTaskCenterProTable.actions": "操作", "PC.Pages.SystemTaskCenterProTable.agent": "智能体", "PC.Pages.SystemTaskCenterProTable.confirmDeleteTask": "确认删除该任务?", @@ -4604,16 +4604,16 @@ export const ZH_CN: SystemLangMap = { "PC.Pages.SystemThemeConfig.saveConfig": "保存配置", "PC.Pages.SystemThemeConfig.saveSuccess": "主题配置保存成功", "PC.Pages.SystemUserGroupFormModal.create": "创建", - "PC.Pages.SystemUserGroupFormModal.createTitle": "新增用户组", + "PC.Pages.SystemUserGroupFormModal.createTitle": "新增单位(部门)", "PC.Pages.SystemUserGroupFormModal.description": "描述", - "PC.Pages.SystemUserGroupFormModal.descriptionPlaceholder": "请输入用户组描述", + "PC.Pages.SystemUserGroupFormModal.descriptionPlaceholder": "请输入单位(部门)描述", "PC.Pages.SystemUserGroupFormModal.disabled": "禁用", - "PC.Pages.SystemUserGroupFormModal.editTitle": "编辑用户组", + "PC.Pages.SystemUserGroupFormModal.editTitle": "编辑单位(部门)", "PC.Pages.SystemUserGroupFormModal.enabled": "启用", "PC.Pages.SystemUserGroupFormModal.formValidateFailed": "表单验证失败", - "PC.Pages.SystemUserGroupFormModal.name": "用户组名称", - "PC.Pages.SystemUserGroupFormModal.namePlaceholder": "请输入用户组名称", - "PC.Pages.SystemUserGroupFormModal.nameRequired": "请输入用户组名称", + "PC.Pages.SystemUserGroupFormModal.name": "单位(部门)名称", + "PC.Pages.SystemUserGroupFormModal.namePlaceholder": "请输入单位(部门)名称", + "PC.Pages.SystemUserGroupFormModal.nameRequired": "请输入单位(部门)名称", "PC.Pages.SystemUserGroupFormModal.save": "保存", "PC.Pages.SystemUserGroupFormModal.sort": "排序", "PC.Pages.SystemUserGroupFormModal.sortPlaceholder": "请输入排序", @@ -4622,32 +4622,52 @@ export const ZH_CN: SystemLangMap = { "PC.Pages.SystemUserGroupFormModal.sourceSystemBuiltIn": "系统内置", "PC.Pages.SystemUserGroupFormModal.sourceUserDefined": "用户自定义", "PC.Pages.SystemUserGroupFormModal.status": "状态", - "PC.Pages.SystemUserGroupFormModal.statusTip": "启用或禁用此用户组", - "PC.Pages.SystemUserGroupManage.add": "新增用户组", - "PC.Pages.SystemUserGroupManage.bindUser": "绑定用户", - "PC.Pages.SystemUserGroupManage.builtInCannotDelete": "系统内置的用户组不能删除", - "PC.Pages.SystemUserGroupManage.builtInCannotDisable": "系统内置的用户组不能禁用", - "PC.Pages.SystemUserGroupManage.builtInCannotEdit": "系统内置的用户组不能编辑", + "PC.Pages.SystemUserGroupFormModal.statusTip": "启用或禁用此单位(部门)", + "PC.Pages.SystemUserGroupFormModal.maxUserCount": "最大用户数", + "PC.Pages.SystemUserGroupFormModal.maxUserCountPlaceholder": "不填表示不限制", + "PC.Pages.SystemUserGroupFormModal.maxClientCount": "最大客户端数", + "PC.Pages.SystemUserGroupFormModal.maxClientCountPlaceholder": "-1 表示不限制", + "PC.Pages.SystemUserGroupFormModal.authEnabled": "单位授权", + "PC.Pages.SystemUserGroupFormModal.authEnabledTip": "未授权或已过期时,单位成员不能登录系统或注册客户端", + "PC.Pages.SystemUserGroupFormModal.authorized": "已授权", + "PC.Pages.SystemUserGroupFormModal.unauthorized": "未授权", + "PC.Pages.SystemUserGroupFormModal.expireTime": "到期时间", + "PC.Pages.SystemUserGroupFormModal.expireTimePlaceholder": "不填表示不过期", + "PC.Pages.SystemUserGroupManage.add": "新增单位(部门)", + "PC.Pages.SystemUserGroupManage.bindUser": "绑定成员", + "PC.Pages.SystemUserGroupManage.bindMember": "绑定成员", + "PC.Pages.SystemUserGroupManage.builtInCannotDelete": "系统内置的单位(部门)不能删除", + "PC.Pages.SystemUserGroupManage.builtInCannotDisable": "系统内置的单位(部门)不能禁用", + "PC.Pages.SystemUserGroupManage.builtInCannotEdit": "系统内置的单位(部门)不能编辑", "PC.Pages.SystemUserGroupManage.columnAction": "操作", "PC.Pages.SystemUserGroupManage.columnCode": "编码", "PC.Pages.SystemUserGroupManage.columnDescription": "描述", - "PC.Pages.SystemUserGroupManage.columnName": "用户组名称", + "PC.Pages.SystemUserGroupManage.columnName": "单位(部门)名称", + "PC.Pages.SystemUserGroupManage.columnAuthorization": "授权状态", + "PC.Pages.SystemUserGroupManage.columnMaxUsers": "最大用户数", + "PC.Pages.SystemUserGroupManage.columnMaxClients": "最大客户端数", + "PC.Pages.SystemUserGroupManage.columnExpireTime": "到期时间", "PC.Pages.SystemUserGroupManage.columnSort": "排序", "PC.Pages.SystemUserGroupManage.columnStatus": "状态", "PC.Pages.SystemUserGroupManage.dataPermission": "数据权限", "PC.Pages.SystemUserGroupManage.delete": "删除", - "PC.Pages.SystemUserGroupManage.deleteConfirm": "确认删除用户组 \"{0}\" 吗?", + "PC.Pages.SystemUserGroupManage.deleteConfirm": "确认删除单位(部门) \"{0}\" 吗?", "PC.Pages.SystemUserGroupManage.deleteSuccess": "删除成功", - "PC.Pages.SystemUserGroupManage.deleteTitle": "删除用户组", + "PC.Pages.SystemUserGroupManage.deleteTitle": "删除单位(部门)", "PC.Pages.SystemUserGroupManage.disabled": "禁用", "PC.Pages.SystemUserGroupManage.edit": "编辑", - "PC.Pages.SystemUserGroupManage.empty": "暂无用户组数据", + "PC.Pages.SystemUserGroupManage.empty": "暂无单位(部门)数据", "PC.Pages.SystemUserGroupManage.enabled": "启用", - "PC.Pages.SystemUserGroupManage.menuPermission": "菜单权限", + "PC.Pages.SystemUserGroupManage.menuPermission": "单位权限", + "PC.Pages.SystemUserGroupManage.unitPermission": "单位权限", "PC.Pages.SystemUserGroupManage.noPermission": "无此资源权限", - "PC.Pages.SystemUserGroupManage.pageTitle": "用户组管理", + "PC.Pages.SystemUserGroupManage.pageTitle": "单位(部门)管理", "PC.Pages.SystemUserGroupManage.sortUpdated": "排序更新成功", - "PC.Pages.SystemUserGroupManage.statusTip": "启用或禁用此用户组", + "PC.Pages.SystemUserGroupManage.statusTip": "启用或禁用此单位(部门)", + "PC.Pages.SystemUserGroupManage.authorized": "已授权", + "PC.Pages.SystemUserGroupManage.unauthorized": "未授权", + "PC.Pages.SystemUserGroupManage.unlimited": "不限制", + "PC.Pages.SystemUserGroupManage.neverExpire": "不过期", "PC.Pages.TeamSetting.AddMember.addNewMember": "添加新成员", "PC.Pages.TeamSetting.AddMember.addSuccess": "添加成功", "PC.Pages.TeamSetting.AddMember.all": "全部", @@ -4765,7 +4785,7 @@ export const ZH_CN: SystemLangMap = { "PC.Pages.UserManage.UserAuthModal.deselectAll": "取消全选", "PC.Pages.UserManage.UserAuthModal.role": "角色", "PC.Pages.UserManage.UserAuthModal.selectAll": "全选", - "PC.Pages.UserManage.UserAuthModal.userGroup": "用户组", + "PC.Pages.UserManage.UserAuthModal.userGroup": "单位(部门)", "PC.Pages.UserManage.UserFormModal.addUser": "新增用户", "PC.Pages.UserManage.UserFormModal.admin": "管理员", "PC.Pages.UserManage.UserFormModal.editUser": "修改用户信息", @@ -4825,7 +4845,7 @@ export const ZH_CN: SystemLangMap = { "PC.Routes.systemSetting": "系统设置", "PC.Routes.taskManagement": "任务管理", "PC.Routes.themeConfig": "主题配置", - "PC.Routes.userGroupManage": "用户组管理", + "PC.Routes.userGroupManage": "单位(部门)管理", "PC.Routes.userManagement": "用户管理", "PC.Routes.devPaymentEarnings": "支付与收益(开发者)", "PC.Routes.adminSubscriptionCredits": "订阅与积分(管理员)", @@ -4866,8 +4886,8 @@ export const ZH_CN: SystemLangMap = { "PC.Toast.SystemRoleFormModal.createSuccess": "角色创建成功", "PC.Toast.SystemRoleFormModal.editSuccess": "角色编辑成功", "PC.Toast.SystemTargetAuthModal.authorized": "授权成功", - "PC.Toast.SystemUserGroupFormModal.createSuccess": "新增用户组成功", - "PC.Toast.SystemUserGroupFormModal.updateSuccess": "更新用户组成功", + "PC.Toast.SystemUserGroupFormModal.createSuccess": "新增单位(部门)成功", + "PC.Toast.SystemUserGroupFormModal.updateSuccess": "更新单位(部门)成功", "PC.Toast.UploadAvatar.invalidSize": "图片大小不能超过 2MB", "PC.Toast.UploadAvatar.invalidType": "请上传 JPG、JPEG 或 PNG 图片文件", "PC.Utils.AntCustom.cancelText": "取消", diff --git a/qiming/src/pages/SystemManagement/MenuPermission/UserGroupManage/UserGroupFormModal/index.tsx b/qiming/src/pages/SystemManagement/MenuPermission/UserGroupManage/UserGroupFormModal/index.tsx index 1c0e769c..561182d3 100644 --- a/qiming/src/pages/SystemManagement/MenuPermission/UserGroupManage/UserGroupFormModal/index.tsx +++ b/qiming/src/pages/SystemManagement/MenuPermission/UserGroupManage/UserGroupFormModal/index.tsx @@ -4,6 +4,7 @@ import { customizeRequiredMark } from '@/utils/form'; import { InfoCircleOutlined } from '@ant-design/icons'; import { Col, + DatePicker, Form, Input, InputNumber, @@ -15,6 +16,7 @@ import { import classNames from 'classnames'; import React, { useEffect } from 'react'; import { useRequest } from 'umi'; +import dayjs from 'dayjs'; import { apiAddUserGroup, apiGetUserGroupById, @@ -105,6 +107,10 @@ const UserGroupFormModal: React.FC = ({ description: data.description, sortIndex: data.sortIndex || 1, status: data.status === UserGroupStatusEnum.Enabled, + maxUserCount: data.maxUserCount, + maxClientCount: data.maxClientCount ?? -1, + authEnabled: data.authEnabled === 1, + expireTime: data.expireTime ? dayjs(data.expireTime) : undefined, source: data.source || UserGroupSourceEnum.UserDefined, }); }, @@ -125,6 +131,8 @@ const UserGroupFormModal: React.FC = ({ source: UserGroupSourceEnum.UserDefined, sortIndex: defaultSortIndex || 1, status: true, + authEnabled: false, + maxClientCount: -1, }); } } @@ -140,6 +148,10 @@ const UserGroupFormModal: React.FC = ({ status: values.status ? UserGroupStatusEnum.Enabled : UserGroupStatusEnum.Disabled, + authEnabled: values.authEnabled ? 1 : 0, + expireTime: values.expireTime + ? values.expireTime.format('YYYY-MM-DD HH:mm:ss') + : undefined, }; if (isEdit && userGroupInfo) { @@ -261,6 +273,80 @@ const UserGroupFormModal: React.FC = ({ + + + + + + + + + + + + + + + , + }} + > + + + + + + + + + + + { hideInSearch: true, ellipsis: true, }, + { + title: t('PC.Pages.SystemUserGroupManage.columnAuthorization'), + dataIndex: 'authEnabled', + key: 'authEnabled', + width: 110, + align: 'center', + hideInSearch: true, + render: (_: ReactNode, record: UserGroupInfo & { key: number }) => { + const authorized = record.authEnabled === 1; + return ( + + {authorized + ? t('PC.Pages.SystemUserGroupManage.authorized') + : t('PC.Pages.SystemUserGroupManage.unauthorized')} + + ); + }, + }, + { + title: t('PC.Pages.SystemUserGroupManage.columnMaxUsers'), + dataIndex: 'maxUserCount', + key: 'maxUserCount', + width: 120, + align: 'center', + hideInSearch: true, + render: (_: ReactNode, record: UserGroupInfo & { key: number }) => + record.maxUserCount ?? t('PC.Pages.SystemUserGroupManage.unlimited'), + }, + { + title: t('PC.Pages.SystemUserGroupManage.columnMaxClients'), + dataIndex: 'maxClientCount', + key: 'maxClientCount', + width: 120, + align: 'center', + hideInSearch: true, + render: (_: ReactNode, record: UserGroupInfo & { key: number }) => + record.maxClientCount == null || record.maxClientCount < 0 + ? t('PC.Pages.SystemUserGroupManage.unlimited') + : record.maxClientCount, + }, + { + title: t('PC.Pages.SystemUserGroupManage.columnExpireTime'), + dataIndex: 'expireTime', + key: 'expireTime', + width: 170, + hideInSearch: true, + render: (_: ReactNode, record: UserGroupInfo & { key: number }) => + record.expireTime + ? dayjs(record.expireTime).format('YYYY-MM-DD HH:mm') + : t('PC.Pages.SystemUserGroupManage.neverExpire'), + }, { title: ( @@ -416,7 +468,7 @@ const UserGroupManage: React.FC = () => { const actions: ActionItem[] = [ { key: 'bindUser', - label: t('PC.Pages.SystemUserGroupManage.bindUser'), + label: t('PC.Pages.SystemUserGroupManage.bindMember'), onClick: () => handleBindUser(record), visible: hasPermissionByMenuCode( 'user_group_manage', @@ -425,7 +477,7 @@ const UserGroupManage: React.FC = () => { }, { key: 'menuPermission', - label: t('PC.Pages.SystemUserGroupManage.menuPermission'), + label: t('PC.Pages.SystemUserGroupManage.unitPermission'), onClick: () => handleMenuPermission(record), visible: hasPermissionByMenuCode( 'user_group_manage', @@ -517,7 +569,7 @@ const UserGroupManage: React.FC = () => { request={request} dataSource={draggableData} pagination={false} - scroll={{ x: 1090 }} + scroll={{ x: 1610 }} showQueryButtons={hasPermissionByMenuCode( 'user_group_manage', 'user_group_manage_query', diff --git a/qiming/src/pages/SystemManagement/MenuPermission/types/user-group-manage.ts b/qiming/src/pages/SystemManagement/MenuPermission/types/user-group-manage.ts index e34b6c93..d1ef3945 100644 --- a/qiming/src/pages/SystemManagement/MenuPermission/types/user-group-manage.ts +++ b/qiming/src/pages/SystemManagement/MenuPermission/types/user-group-manage.ts @@ -38,6 +38,14 @@ export interface UserGroupInfo { code: string; /** 用户组描述 */ description: string; + /** 最大用户数,空表示不限制 */ + maxUserCount?: number; + /** 最大客户端数,-1表示不限制 */ + maxClientCount?: number; + /** 授权开关:1-已授权,0-未授权 */ + authEnabled?: number; + /** 授权到期时间,空表示不过期 */ + expireTime?: string; /** 来源 1:系统内置 2:用户自定义 */ source: UserGroupSourceEnum; /** 状态,1:启用 0:禁用 */ @@ -75,6 +83,14 @@ export interface AddUserGroupParams { name?: string; /** 描述 */ description?: string; + /** 最大用户数,空表示不限制 */ + maxUserCount?: number; + /** 最大客户端数,-1表示不限制 */ + maxClientCount?: number; + /** 授权开关:1-已授权,0-未授权 */ + authEnabled?: number; + /** 授权到期时间,空表示不过期 */ + expireTime?: string; /* 状态,1:启用 0:禁用 */ status?: UserGroupStatusEnum; /** 数据模型ID列表,全部模型传[0],未选中任何模型不传值 */ diff --git a/qimingclaw/crates/agent-electron-client/src/main/ipc/appHandlers.ts b/qimingclaw/crates/agent-electron-client/src/main/ipc/appHandlers.ts index ec54cb2c..cfe05fee 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/ipc/appHandlers.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/ipc/appHandlers.ts @@ -20,7 +20,7 @@ import { getUpdateState, openReleasesPage, } from "../services/autoUpdater"; -import { getDeviceId } from "../services/system/deviceId"; +import { getDeviceId, getPrimaryLocalIp } from "../services/system/deviceId"; import { getTrayManager } from "../window/trayManager"; import { getAutoLaunchManager } from "../window/autoLaunchManager"; import { APP_DISPLAY_NAME } from "@shared/constants"; @@ -284,6 +284,10 @@ export function registerAppHandlers(ctx: HandlerContext): void { return getDeviceId(); }); + ipcMain.handle("app:getPrimaryLocalIp", () => { + return getPrimaryLocalIp(); + }); + ipcMain.handle("app:checkUpdate", async () => { try { return await checkForUpdates(); diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts b/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts index add82515..9ebae2c4 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts @@ -314,7 +314,7 @@ describe("AcpEngine.prompt", () => { { messageID: "rid-timeout-001" }, ); - await vi.advanceTimersByTimeAsync(60_001); + await vi.advanceTimersByTimeAsync(600_001); await expect(promptPromise).resolves.toBeDefined(); expect(onPromptEnd).toHaveBeenCalledTimes(1); diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/system/deviceId.ts b/qimingclaw/crates/agent-electron-client/src/main/services/system/deviceId.ts index 9d8da215..1f092e3d 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/system/deviceId.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/system/deviceId.ts @@ -28,6 +28,18 @@ export function getDeviceId(): string { return cachedDeviceId; } +export function getPrimaryLocalIp(): string | null { + const interfaces = os.networkInterfaces(); + for (const entries of Object.values(interfaces)) { + for (const entry of entries ?? []) { + if (entry.family === "IPv4" && !entry.internal) { + return entry.address; + } + } + } + return null; +} + export function logSystemInfo(): void { // 固定字段尽量保持稳定输出,便于跨平台日志分析与检索。 const baseInfo = { diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/system/index.ts b/qimingclaw/crates/agent-electron-client/src/main/services/system/index.ts index 5c10ebb1..8bb6e373 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/system/index.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/system/index.ts @@ -1,5 +1,5 @@ // Main process system services -export { getDeviceId } from './deviceId'; +export { getDeviceId, getPrimaryLocalIp } from './deviceId'; export { getAppEnv, setMirrorConfig } from './dependencies'; export { getAppDataDir, diff --git a/qimingclaw/crates/agent-electron-client/src/preload/index.ts b/qimingclaw/crates/agent-electron-client/src/preload/index.ts index a11e6319..963bbb99 100644 --- a/qimingclaw/crates/agent-electron-client/src/preload/index.ts +++ b/qimingclaw/crates/agent-electron-client/src/preload/index.ts @@ -503,6 +503,7 @@ contextBridge.exposeInMainWorld("electronAPI", { openReleasesPage: () => ipcRenderer.invoke("app:openReleasesPage"), getUpdateDebugInfo: () => ipcRenderer.invoke("app:getUpdateDebugInfo"), getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"), + getPrimaryLocalIp: () => ipcRenderer.invoke("app:getPrimaryLocalIp"), }, // Permissions (macOS) diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/api.ts b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/api.ts index 211b2de2..34a07f03 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/api.ts +++ b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/api.ts @@ -192,6 +192,7 @@ export interface ClientRegisterParams { password: string; savedKey?: string; deviceId?: string; + name?: string; sandboxConfigValue: SandboxValue; } diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.test.ts b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.test.ts index 5a989998..cbc9725d 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.test.ts +++ b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.test.ts @@ -28,6 +28,7 @@ vi.stubGlobal("window", { electronAPI: { app: { getDeviceId: vi.fn(async () => "mock-device-id"), + getPrimaryLocalIp: vi.fn(async () => "192.168.1.23"), }, settings: { get: mockSettingsGet, @@ -166,6 +167,7 @@ describe("auth - savedKey 认证 (快捷登录)", () => { expect(params.username).toBe(""); expect(params.password).toBe(""); expect(params.savedKey).toBe(SAVED_KEY); + expect(params.name).toBe("ip-192.168.1.23"); }); it("username/password 为 null + 有 savedKey → 应正常同步", async () => { @@ -313,6 +315,9 @@ describe("auth - savedKey 认证 (快捷登录)", () => { expect(store["auth.password"]).toBeUndefined(); // savedKey 应保存 expect(store["auth.saved_key"]).toBe(CONFIG_KEY_FROM_SERVER); + + const [params] = mockRegisterClient.mock.calls[0]; + expect(params.name).toBe("ip-192.168.1.23"); }); }); diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.ts b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.ts index 98999979..cd65a16c 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.ts +++ b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/auth.ts @@ -203,6 +203,17 @@ async function getLocalSandboxValue(): Promise { }; } +async function getDefaultSandboxName(): Promise { + try { + const ip = await window.electronAPI?.app.getPrimaryLocalIp?.(); + const normalized = typeof ip === "string" ? ip.trim() : ""; + return normalized ? `ip-${normalized}` : undefined; + } catch (error) { + logger.warn("Failed to resolve default sandbox name", "Auth", error); + return undefined; + } +} + // ========== 错误处理 === /** * 获取友好的错误信息 @@ -288,11 +299,13 @@ export async function loginAndRegister( // 构建注册参数 const deviceId = await window.electronAPI?.app.getDeviceId(); + const defaultSandboxName = await getDefaultSandboxName(); const params: ClientRegisterParams = { username, password, savedKey: savedKey || undefined, deviceId: deviceId || undefined, + name: defaultSandboxName, sandboxConfigValue: await getLocalSandboxValue(), }; @@ -471,11 +484,13 @@ export async function reRegisterClient(): Promise logger.info("Re-registering client (using savedKey)...", "Auth"); const deviceId = await window.electronAPI?.app.getDeviceId(); + const defaultSandboxName = await getDefaultSandboxName(); const params: ClientRegisterParams = { username: username || "", password: "", // 密码不持久化,使用 savedKey 认证 savedKey, deviceId: deviceId || undefined, + name: defaultSandboxName, sandboxConfigValue: await getLocalSandboxValue(), }; @@ -575,11 +590,13 @@ export async function syncConfigToServer(options?: { } const deviceId = await window.electronAPI?.app.getDeviceId(); + const defaultSandboxName = await getDefaultSandboxName(); const params: ClientRegisterParams = { username: username || "", password: "", // 密码不持久化,使用 savedKey 认证 savedKey, deviceId: deviceId || undefined, + name: defaultSandboxName, sandboxConfigValue: await getLocalSandboxValue(), }; diff --git a/qimingclaw/crates/agent-electron-client/src/shared/constants.ts b/qimingclaw/crates/agent-electron-client/src/shared/constants.ts index 36a8a124..a51097ed 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/constants.ts +++ b/qimingclaw/crates/agent-electron-client/src/shared/constants.ts @@ -152,7 +152,7 @@ export const PROCESS_KILL_ESCALATION_TIMEOUT = 5000; export const ACP_ABORT_TIMEOUT = 15_000; /** ACP prompt 等待响应超时 (ms) */ -export const ACP_PROMPT_TIMEOUT = 60_000; +export const ACP_PROMPT_TIMEOUT = 600_000; /** * 用户主动取消会话时,挂在 `Error` 上的 `code`(与 `message` 语言无关)。 diff --git a/qimingclaw/crates/agent-electron-client/src/shared/types/electron.d.ts b/qimingclaw/crates/agent-electron-client/src/shared/types/electron.d.ts index 922375d0..ebc46ec8 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/types/electron.d.ts +++ b/qimingclaw/crates/agent-electron-client/src/shared/types/electron.d.ts @@ -515,6 +515,7 @@ export interface AppAPI { error?: string; }>; getDeviceId: () => Promise; + getPrimaryLocalIp: () => Promise; } export type PermissionStatus = "granted" | "denied" | "unknown";