支持单位授权与客户端限制

This commit is contained in:
Codex
2026-06-01 11:58:14 +08:00
parent 28adc8143a
commit c136cc2cc8
36 changed files with 861 additions and 49 deletions

View File

@@ -25,6 +25,9 @@ public class SandboxConfigDto {
@Schema(description = "用户IDscope为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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}
/**
* 验证配置键唯一性
*/

View File

@@ -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;
}
}

View File

@@ -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()));

View File

@@ -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;
}
}

View File

@@ -19,6 +19,9 @@ public class SandboxRegDto {
@Schema(description = "设备ID")
private String deviceId;
@Schema(description = "客户端注册名称")
private String name;
@Schema(description = "终端配置信息")
private SandboxConfigValue sandboxConfigValue;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
/**
* 用户组绑定用户(全量覆盖)
*/

View File

@@ -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("-", ""));
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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"),

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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 '用户IDscope为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`);

View 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`);