支持单位授权与客户端限制
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Long> groupIds;
|
||||
|
||||
@Schema(description = "配置名称(模糊查询)")
|
||||
private String name;
|
||||
|
||||
|
||||
@@ -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<SysGroup> groups = sysGroupApplicationService.getEffectiveGroupListByUserId(entity.getUserId());
|
||||
if (groups.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, Long> 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<SandboxConfig>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置键唯一性
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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<Page<SandboxConfigDto>> 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<SandboxConfigDto> 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<Void> 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<List<SandboxConfigDto>> listUserConfigsByType(
|
||||
@Parameter(description = "用户ID(为空时查询当前用户)") @RequestParam(required = false) Long userId) {
|
||||
return ReqResult.success(sandboxConfigApplicationService.listUserConfigsByType(userId));
|
||||
List<SandboxConfigDto> configs = sandboxConfigApplicationService.listUserConfigsByType(userId);
|
||||
if (!isPlatformAdmin()) {
|
||||
Set<Long> 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<Void> 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<Void> 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<Void> 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<Void> 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<Long> 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<Long> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ public class SandboxRegDto {
|
||||
@Schema(description = "设备ID")
|
||||
private String deviceId;
|
||||
|
||||
@Schema(description = "客户端注册名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "终端配置信息")
|
||||
private SandboxConfigValue sandboxConfigValue;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -76,6 +76,16 @@ public interface SysGroupApplicationService {
|
||||
*/
|
||||
List<SysGroup> getEffectiveGroupListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 查询用户可用于系统登录的已授权用户组(启用、授权开启、未过期)
|
||||
*/
|
||||
List<SysGroup> getAuthorizedGroupListForLogin(Long userId);
|
||||
|
||||
/**
|
||||
* 为用户选择一个可注册客户端的已授权用户组(同时满足客户端数量限制)
|
||||
*/
|
||||
SysGroup selectAuthorizedGroupForClient(Long userId, java.util.Map<Long, Long> clientCountByGroupId);
|
||||
|
||||
/**
|
||||
* 用户组绑定用户(全量覆盖)
|
||||
*/
|
||||
|
||||
@@ -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<String, Object> map = redisUtil.hashGetAll("user-token:" + userDto.getId());
|
||||
for (Map.Entry<String, Object> 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("-", ""));
|
||||
}
|
||||
|
||||
@@ -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<SysGroup> getAuthorizedGroupListForLogin(Long userId) {
|
||||
return filterAuthorizedGroupsForLogin(getEffectiveGroupListByUserId(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysGroup selectAuthorizedGroupForClient(Long userId, Map<Long, Long> clientCountByGroupId) {
|
||||
return selectAuthorizedGroupForClient(getEffectiveGroupListByUserId(userId), clientCountByGroupId);
|
||||
}
|
||||
|
||||
List<SysGroup> filterAuthorizedGroupsForLogin(List<SysGroup> 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<SysGroup> groups, Map<Long, Long> clientCountByGroupId) {
|
||||
if (CollectionUtils.isEmpty(groups)) {
|
||||
return null;
|
||||
}
|
||||
Date now = new Date();
|
||||
Map<Long, Long> 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<Long> getSubscriptionPlanGroupIds(Long userId) {
|
||||
try {
|
||||
UserSubscriptionDTO subscription = subscriptionRpcService.getUserCurrentSystemSubscription(userId);
|
||||
|
||||
@@ -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<SysGroup> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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<SortIndex> 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<SysGroup> 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<MenuNode> menuNodeList = sysGroupApplicationService.getMenuTreeByGroupId(groupId);
|
||||
|
||||
@@ -373,4 +390,41 @@ public class SysGroupController extends BaseController {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SysGroup> filterGroupManageScope(List<SysGroup> groupList) {
|
||||
if (isPlatformAdmin() || CollectionUtils.isEmpty(groupList)) {
|
||||
return groupList;
|
||||
}
|
||||
Set<Long> 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<Long> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SysRole> 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<SysGroup> 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<MenuNodeDto> 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<Long> 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<Long> groupIds) {
|
||||
if (isPlatformAdmin() || CollectionUtils.isEmpty(groupIds)) {
|
||||
return;
|
||||
}
|
||||
Set<Long> allowedGroupIds = currentUserGroupIds();
|
||||
boolean allInScope = groupIds.stream().allMatch(allowedGroupIds::contains);
|
||||
if (!allInScope) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRoleBindScope(List<Long> roleIds) {
|
||||
if (isPlatformAdmin() || CollectionUtils.isEmpty(roleIds)) {
|
||||
return;
|
||||
}
|
||||
Set<Long> 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<Long> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
ADD UNIQUE KEY `uk_tenant_biz_order` (`_tenant_id`, `biz_order_no`);
|
||||
|
||||
8
qiming-backend/sql/update-20260530.sql
Normal file
8
qiming-backend/sql/update-20260530.sql
Normal file
@@ -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`);
|
||||
@@ -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": "取消",
|
||||
|
||||
@@ -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<UserGroupFormModalProps> = ({
|
||||
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<UserGroupFormModalProps> = ({
|
||||
source: UserGroupSourceEnum.UserDefined,
|
||||
sortIndex: defaultSortIndex || 1,
|
||||
status: true,
|
||||
authEnabled: false,
|
||||
maxClientCount: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -140,6 +148,10 @@ const UserGroupFormModal: React.FC<UserGroupFormModalProps> = ({
|
||||
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<UserGroupFormModalProps> = ({
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('PC.Pages.SystemUserGroupFormModal.maxUserCount')}
|
||||
name="maxUserCount"
|
||||
className={cx(styles.fieldItem)}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t(
|
||||
'PC.Pages.SystemUserGroupFormModal.maxUserCountPlaceholder',
|
||||
)}
|
||||
className={cx('w-full')}
|
||||
min={1}
|
||||
max={999999}
|
||||
precision={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('PC.Pages.SystemUserGroupFormModal.maxClientCount')}
|
||||
name="maxClientCount"
|
||||
className={cx(styles.fieldItem)}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t(
|
||||
'PC.Pages.SystemUserGroupFormModal.maxClientCountPlaceholder',
|
||||
)}
|
||||
className={cx('w-full')}
|
||||
min={-1}
|
||||
max={999999}
|
||||
precision={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('PC.Pages.SystemUserGroupFormModal.authEnabled')}
|
||||
name="authEnabled"
|
||||
valuePropName="checked"
|
||||
tooltip={{
|
||||
title: t('PC.Pages.SystemUserGroupFormModal.authEnabledTip'),
|
||||
icon: <InfoCircleOutlined />,
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checkedChildren={t(
|
||||
'PC.Pages.SystemUserGroupFormModal.authorized',
|
||||
)}
|
||||
unCheckedChildren={t(
|
||||
'PC.Pages.SystemUserGroupFormModal.unauthorized',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('PC.Pages.SystemUserGroupFormModal.expireTime')}
|
||||
name="expireTime"
|
||||
>
|
||||
<DatePicker
|
||||
showTime
|
||||
className={cx('w-full')}
|
||||
placeholder={t(
|
||||
'PC.Pages.SystemUserGroupFormModal.expireTimePlaceholder',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label={t('PC.Pages.SystemUserGroupFormModal.description')}
|
||||
name="description"
|
||||
|
||||
@@ -22,8 +22,9 @@ import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Button, Empty, message, Switch, Tooltip } from 'antd';
|
||||
import { Button, Empty, message, Switch, Tag, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useModel, useRequest } from 'umi';
|
||||
@@ -366,6 +367,57 @@ const UserGroupManage: 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 (
|
||||
<Tag color={authorized ? 'success' : 'default'}>
|
||||
{authorized
|
||||
? t('PC.Pages.SystemUserGroupManage.authorized')
|
||||
: t('PC.Pages.SystemUserGroupManage.unauthorized')}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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: (
|
||||
<span>
|
||||
@@ -416,7 +468,7 @@ const UserGroupManage: React.FC = () => {
|
||||
const actions: ActionItem<UserGroupInfo & { key: number }>[] = [
|
||||
{
|
||||
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',
|
||||
|
||||
@@ -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],未选中任何模型不传值 */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Main process system services
|
||||
export { getDeviceId } from './deviceId';
|
||||
export { getDeviceId, getPrimaryLocalIp } from './deviceId';
|
||||
export { getAppEnv, setMirrorConfig } from './dependencies';
|
||||
export {
|
||||
getAppDataDir,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -192,6 +192,7 @@ export interface ClientRegisterParams {
|
||||
password: string;
|
||||
savedKey?: string;
|
||||
deviceId?: string;
|
||||
name?: string;
|
||||
sandboxConfigValue: SandboxValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -203,6 +203,17 @@ async function getLocalSandboxValue(): Promise<SandboxValue> {
|
||||
};
|
||||
}
|
||||
|
||||
async function getDefaultSandboxName(): Promise<string | undefined> {
|
||||
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<ClientRegisterResponse | null>
|
||||
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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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` 语言无关)。
|
||||
|
||||
@@ -515,6 +515,7 @@ export interface AppAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
getDeviceId: () => Promise<string>;
|
||||
getPrimaryLocalIp: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
export type PermissionStatus = "granted" | "denied" | "unknown";
|
||||
|
||||
Reference in New Issue
Block a user