diff --git a/springboot-backend/src/main/java/com/admin/AdminApplication.java b/springboot-backend/src/main/java/com/admin/AdminApplication.java index ac701e9..b0989df 100644 --- a/springboot-backend/src/main/java/com/admin/AdminApplication.java +++ b/springboot-backend/src/main/java/com/admin/AdminApplication.java @@ -13,6 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableAsync @EnableScheduling +@MapperScan("com.admin.mapper") public class AdminApplication { public static void main(String[] args) { diff --git a/springboot-backend/src/main/java/com/admin/common/dto/GroupCreateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/GroupCreateDto.java new file mode 100644 index 0000000..1516a90 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/GroupCreateDto.java @@ -0,0 +1,14 @@ +package com.admin.common.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class GroupCreateDto { + + @NotBlank(message = "分组名称不能为空") + private String name; + + private Integer status; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/GroupPermissionAssignDto.java b/springboot-backend/src/main/java/com/admin/common/dto/GroupPermissionAssignDto.java new file mode 100644 index 0000000..bb6784e --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/GroupPermissionAssignDto.java @@ -0,0 +1,15 @@ +package com.admin.common.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Data +public class GroupPermissionAssignDto { + + @NotNull(message = "用户分组ID不能为空") + private Long userGroupId; + + @NotNull(message = "隧道分组ID不能为空") + private Long tunnelGroupId; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/GroupPermissionDetailDto.java b/springboot-backend/src/main/java/com/admin/common/dto/GroupPermissionDetailDto.java new file mode 100644 index 0000000..a3ab62b --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/GroupPermissionDetailDto.java @@ -0,0 +1,13 @@ +package com.admin.common.dto; + +import lombok.Data; + +@Data +public class GroupPermissionDetailDto { + private Long id; + private Long userGroupId; + private String userGroupName; + private Long tunnelGroupId; + private String tunnelGroupName; + private Long createdTime; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/GroupUpdateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/GroupUpdateDto.java new file mode 100644 index 0000000..cec38d2 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/GroupUpdateDto.java @@ -0,0 +1,18 @@ +package com.admin.common.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +public class GroupUpdateDto { + + @NotNull(message = "分组ID不能为空") + private Long id; + + @NotBlank(message = "分组名称不能为空") + private String name; + + private Integer status; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/TunnelGroupAssignTunnelsDto.java b/springboot-backend/src/main/java/com/admin/common/dto/TunnelGroupAssignTunnelsDto.java new file mode 100644 index 0000000..2a99010 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/TunnelGroupAssignTunnelsDto.java @@ -0,0 +1,16 @@ +package com.admin.common.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Data +public class TunnelGroupAssignTunnelsDto { + + @NotNull(message = "隧道分组ID不能为空") + private Long groupId; + + @NotNull(message = "隧道列表不能为空") + private List tunnelIds; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/TunnelGroupDetailDto.java b/springboot-backend/src/main/java/com/admin/common/dto/TunnelGroupDetailDto.java new file mode 100644 index 0000000..e681692 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/TunnelGroupDetailDto.java @@ -0,0 +1,17 @@ +package com.admin.common.dto; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class TunnelGroupDetailDto { + private Long id; + private String name; + private Integer status; + private Long createdTime; + private Long updatedTime; + private List tunnelIds = new ArrayList<>(); + private List tunnelNames = new ArrayList<>(); +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/UserGroupAssignUsersDto.java b/springboot-backend/src/main/java/com/admin/common/dto/UserGroupAssignUsersDto.java new file mode 100644 index 0000000..a4f81a9 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/UserGroupAssignUsersDto.java @@ -0,0 +1,16 @@ +package com.admin.common.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Data +public class UserGroupAssignUsersDto { + + @NotNull(message = "用户分组ID不能为空") + private Long groupId; + + @NotNull(message = "用户列表不能为空") + private List userIds; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/UserGroupDetailDto.java b/springboot-backend/src/main/java/com/admin/common/dto/UserGroupDetailDto.java new file mode 100644 index 0000000..87a0eeb --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/UserGroupDetailDto.java @@ -0,0 +1,17 @@ +package com.admin.common.dto; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class UserGroupDetailDto { + private Long id; + private String name; + private Integer status; + private Long createdTime; + private Long updatedTime; + private List userIds = new ArrayList<>(); + private List userNames = new ArrayList<>(); +} diff --git a/springboot-backend/src/main/java/com/admin/common/migration/SqliteSchemaMigration.java b/springboot-backend/src/main/java/com/admin/common/migration/SqliteSchemaMigration.java index 16d3a05..63de544 100644 --- a/springboot-backend/src/main/java/com/admin/common/migration/SqliteSchemaMigration.java +++ b/springboot-backend/src/main/java/com/admin/common/migration/SqliteSchemaMigration.java @@ -32,6 +32,18 @@ public class SqliteSchemaMigration implements ApplicationRunner { public void run(ApplicationArguments args) { ensureColumn("node", "inx", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tunnel", "inx", "INTEGER NOT NULL DEFAULT 0"); + ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)"); + ensureTable("CREATE TABLE IF NOT EXISTS user_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)"); + ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (id INTEGER PRIMARY KEY AUTOINCREMENT, tunnel_group_id INTEGER NOT NULL, tunnel_id INTEGER NOT NULL, created_time INTEGER NOT NULL)"); + ensureTable("CREATE TABLE IF NOT EXISTS user_group_user (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_time INTEGER NOT NULL)"); + ensureTable("CREATE TABLE IF NOT EXISTS group_permission (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, created_time INTEGER NOT NULL)"); + ensureTable("CREATE TABLE IF NOT EXISTS group_permission_grant (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, user_tunnel_id INTEGER NOT NULL, created_time INTEGER NOT NULL)"); + ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name)"); + ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name)"); + ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id)"); + ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id)"); + ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id)"); + ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id)"); } private void ensureColumn(String table, String column, String columnDefinition) { @@ -51,4 +63,8 @@ public class SqliteSchemaMigration implements ApplicationRunner { "ALTER TABLE " + table + " ADD COLUMN " + column + " " + columnDefinition ); } + + private void ensureTable(String ddl) { + jdbcTemplate.execute(ddl); + } } diff --git a/springboot-backend/src/main/java/com/admin/controller/GroupController.java b/springboot-backend/src/main/java/com/admin/controller/GroupController.java new file mode 100644 index 0000000..63845d4 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/controller/GroupController.java @@ -0,0 +1,123 @@ +package com.admin.controller; + +import com.admin.common.aop.LogAnnotation; +import com.admin.common.annotation.RequireRole; +import com.admin.common.dto.GroupCreateDto; +import com.admin.common.dto.GroupPermissionAssignDto; +import com.admin.common.dto.GroupUpdateDto; +import com.admin.common.dto.TunnelGroupAssignTunnelsDto; +import com.admin.common.dto.UserGroupAssignUsersDto; +import com.admin.common.lang.R; +import com.admin.service.GroupService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Map; + +@RestController +@CrossOrigin +@RequestMapping("/api/v1/group") +public class GroupController { + + @Resource + private GroupService groupService; + + @LogAnnotation + @RequireRole + @PostMapping("/tunnel/list") + public R tunnelGroupList() { + return groupService.getTunnelGroups(); + } + + @LogAnnotation + @RequireRole + @PostMapping("/tunnel/create") + public R createTunnelGroup(@Validated @RequestBody GroupCreateDto dto) { + return groupService.createTunnelGroup(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/tunnel/update") + public R updateTunnelGroup(@Validated @RequestBody GroupUpdateDto dto) { + return groupService.updateTunnelGroup(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/tunnel/delete") + public R deleteTunnelGroup(@RequestBody Map params) { + Long id = Long.valueOf(params.get("id").toString()); + return groupService.deleteTunnelGroup(id); + } + + @LogAnnotation + @RequireRole + @PostMapping("/tunnel/assign") + public R assignTunnels(@Validated @RequestBody TunnelGroupAssignTunnelsDto dto) { + return groupService.assignTunnelsToGroup(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/user/list") + public R userGroupList() { + return groupService.getUserGroups(); + } + + @LogAnnotation + @RequireRole + @PostMapping("/user/create") + public R createUserGroup(@Validated @RequestBody GroupCreateDto dto) { + return groupService.createUserGroup(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/user/update") + public R updateUserGroup(@Validated @RequestBody GroupUpdateDto dto) { + return groupService.updateUserGroup(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/user/delete") + public R deleteUserGroup(@RequestBody Map params) { + Long id = Long.valueOf(params.get("id").toString()); + return groupService.deleteUserGroup(id); + } + + @LogAnnotation + @RequireRole + @PostMapping("/user/assign") + public R assignUsers(@Validated @RequestBody UserGroupAssignUsersDto dto) { + return groupService.assignUsersToGroup(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/permission/list") + public R listPermissions() { + return groupService.getGroupPermissions(); + } + + @LogAnnotation + @RequireRole + @PostMapping("/permission/assign") + public R assignPermission(@Validated @RequestBody GroupPermissionAssignDto dto) { + return groupService.assignGroupPermission(dto); + } + + @LogAnnotation + @RequireRole + @PostMapping("/permission/remove") + public R removePermission(@RequestBody Map params) { + Long id = Long.valueOf(params.get("id").toString()); + return groupService.removeGroupPermission(id); + } +} diff --git a/springboot-backend/src/main/java/com/admin/entity/GroupPermission.java b/springboot-backend/src/main/java/com/admin/entity/GroupPermission.java new file mode 100644 index 0000000..542bde4 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/GroupPermission.java @@ -0,0 +1,22 @@ +package com.admin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class GroupPermission implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userGroupId; + + private Long tunnelGroupId; + + private Long createdTime; +} diff --git a/springboot-backend/src/main/java/com/admin/entity/GroupPermissionGrant.java b/springboot-backend/src/main/java/com/admin/entity/GroupPermissionGrant.java new file mode 100644 index 0000000..a8ef9e6 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/GroupPermissionGrant.java @@ -0,0 +1,24 @@ +package com.admin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class GroupPermissionGrant implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userGroupId; + + private Long tunnelGroupId; + + private Long userTunnelId; + + private Long createdTime; +} diff --git a/springboot-backend/src/main/java/com/admin/entity/TunnelGroup.java b/springboot-backend/src/main/java/com/admin/entity/TunnelGroup.java new file mode 100644 index 0000000..d280af2 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/TunnelGroup.java @@ -0,0 +1,13 @@ +package com.admin.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TunnelGroup extends BaseEntity { + + private static final long serialVersionUID = 1L; + + private String name; +} diff --git a/springboot-backend/src/main/java/com/admin/entity/TunnelGroupTunnel.java b/springboot-backend/src/main/java/com/admin/entity/TunnelGroupTunnel.java new file mode 100644 index 0000000..530b7ed --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/TunnelGroupTunnel.java @@ -0,0 +1,22 @@ +package com.admin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class TunnelGroupTunnel implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long tunnelGroupId; + + private Long tunnelId; + + private Long createdTime; +} diff --git a/springboot-backend/src/main/java/com/admin/entity/UserGroup.java b/springboot-backend/src/main/java/com/admin/entity/UserGroup.java new file mode 100644 index 0000000..89cf5e6 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/UserGroup.java @@ -0,0 +1,13 @@ +package com.admin.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class UserGroup extends BaseEntity { + + private static final long serialVersionUID = 1L; + + private String name; +} diff --git a/springboot-backend/src/main/java/com/admin/entity/UserGroupUser.java b/springboot-backend/src/main/java/com/admin/entity/UserGroupUser.java new file mode 100644 index 0000000..3779fa8 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/UserGroupUser.java @@ -0,0 +1,22 @@ +package com.admin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class UserGroupUser implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userGroupId; + + private Long userId; + + private Long createdTime; +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/GroupPermissionGrantMapper.java b/springboot-backend/src/main/java/com/admin/mapper/GroupPermissionGrantMapper.java new file mode 100644 index 0000000..4bbb846 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/GroupPermissionGrantMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.GroupPermissionGrant; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface GroupPermissionGrantMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/GroupPermissionMapper.java b/springboot-backend/src/main/java/com/admin/mapper/GroupPermissionMapper.java new file mode 100644 index 0000000..a10ac69 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/GroupPermissionMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.GroupPermission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface GroupPermissionMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/TunnelGroupMapper.java b/springboot-backend/src/main/java/com/admin/mapper/TunnelGroupMapper.java new file mode 100644 index 0000000..fd8852e --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/TunnelGroupMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.TunnelGroup; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface TunnelGroupMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/TunnelGroupTunnelMapper.java b/springboot-backend/src/main/java/com/admin/mapper/TunnelGroupTunnelMapper.java new file mode 100644 index 0000000..c937ea0 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/TunnelGroupTunnelMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.TunnelGroupTunnel; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface TunnelGroupTunnelMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/UserGroupMapper.java b/springboot-backend/src/main/java/com/admin/mapper/UserGroupMapper.java new file mode 100644 index 0000000..2b3b1df --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/UserGroupMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.UserGroup; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface UserGroupMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/UserGroupUserMapper.java b/springboot-backend/src/main/java/com/admin/mapper/UserGroupUserMapper.java new file mode 100644 index 0000000..dfcf62f --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/UserGroupUserMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.UserGroupUser; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface UserGroupUserMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/service/GroupService.java b/springboot-backend/src/main/java/com/admin/service/GroupService.java new file mode 100644 index 0000000..d6f7918 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/GroupService.java @@ -0,0 +1,37 @@ +package com.admin.service; + +import com.admin.common.dto.GroupCreateDto; +import com.admin.common.dto.GroupPermissionAssignDto; +import com.admin.common.dto.GroupUpdateDto; +import com.admin.common.dto.TunnelGroupAssignTunnelsDto; +import com.admin.common.dto.UserGroupAssignUsersDto; +import com.admin.common.lang.R; + +public interface GroupService { + + R getTunnelGroups(); + + R createTunnelGroup(GroupCreateDto dto); + + R updateTunnelGroup(GroupUpdateDto dto); + + R deleteTunnelGroup(Long id); + + R assignTunnelsToGroup(TunnelGroupAssignTunnelsDto dto); + + R getUserGroups(); + + R createUserGroup(GroupCreateDto dto); + + R updateUserGroup(GroupUpdateDto dto); + + R deleteUserGroup(Long id); + + R assignUsersToGroup(UserGroupAssignUsersDto dto); + + R getGroupPermissions(); + + R assignGroupPermission(GroupPermissionAssignDto dto); + + R removeGroupPermission(Long id); +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/GroupServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/GroupServiceImpl.java new file mode 100644 index 0000000..81553bd --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/impl/GroupServiceImpl.java @@ -0,0 +1,587 @@ +package com.admin.service.impl; + +import com.admin.common.dto.GroupCreateDto; +import com.admin.common.dto.GroupPermissionAssignDto; +import com.admin.common.dto.GroupPermissionDetailDto; +import com.admin.common.dto.GroupUpdateDto; +import com.admin.common.dto.TunnelGroupAssignTunnelsDto; +import com.admin.common.dto.TunnelGroupDetailDto; +import com.admin.common.dto.UserGroupAssignUsersDto; +import com.admin.common.dto.UserGroupDetailDto; +import com.admin.common.lang.R; +import com.admin.entity.GroupPermission; +import com.admin.entity.GroupPermissionGrant; +import com.admin.entity.Tunnel; +import com.admin.entity.TunnelGroup; +import com.admin.entity.TunnelGroupTunnel; +import com.admin.entity.User; +import com.admin.entity.UserGroup; +import com.admin.entity.UserGroupUser; +import com.admin.entity.UserTunnel; +import com.admin.mapper.GroupPermissionGrantMapper; +import com.admin.mapper.GroupPermissionMapper; +import com.admin.mapper.TunnelGroupMapper; +import com.admin.mapper.TunnelGroupTunnelMapper; +import com.admin.mapper.UserGroupMapper; +import com.admin.mapper.UserGroupUserMapper; +import com.admin.service.GroupService; +import com.admin.service.TunnelService; +import com.admin.service.UserService; +import com.admin.service.UserTunnelService; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class GroupServiceImpl implements GroupService { + + @Resource + private TunnelGroupMapper tunnelGroupMapper; + + @Resource + private UserGroupMapper userGroupMapper; + + @Resource + private TunnelGroupTunnelMapper tunnelGroupTunnelMapper; + + @Resource + private UserGroupUserMapper userGroupUserMapper; + + @Resource + private GroupPermissionMapper groupPermissionMapper; + + @Resource + private GroupPermissionGrantMapper groupPermissionGrantMapper; + + @Resource + private TunnelService tunnelService; + + @Resource + private UserService userService; + + @Resource + private UserTunnelService userTunnelService; + + @Override + public R getTunnelGroups() { + List groups = tunnelGroupMapper.selectList(new QueryWrapper().orderByAsc("id")); + List mappings = tunnelGroupTunnelMapper.selectList(new QueryWrapper()); + + Map> mappingByGroupId = mappings.stream() + .collect(Collectors.groupingBy(TunnelGroupTunnel::getTunnelGroupId)); + + Set tunnelIds = mappings.stream().map(TunnelGroupTunnel::getTunnelId).collect(Collectors.toSet()); + Map tunnelNameMap = buildTunnelNameMap(tunnelIds); + + List result = new ArrayList<>(); + for (TunnelGroup group : groups) { + TunnelGroupDetailDto dto = new TunnelGroupDetailDto(); + dto.setId(group.getId()); + dto.setName(group.getName()); + dto.setStatus(group.getStatus()); + dto.setCreatedTime(group.getCreatedTime()); + dto.setUpdatedTime(group.getUpdatedTime()); + + List groupMappings = mappingByGroupId.getOrDefault(group.getId(), Collections.emptyList()); + List ids = groupMappings.stream().map(TunnelGroupTunnel::getTunnelId).collect(Collectors.toList()); + List names = ids.stream().map(tunnelNameMap::get).filter(name -> name != null && !name.isBlank()).collect(Collectors.toList()); + dto.setTunnelIds(ids); + dto.setTunnelNames(names); + result.add(dto); + } + return R.ok(result); + } + + @Override + public R createTunnelGroup(GroupCreateDto dto) { + String name = dto.getName().trim(); + int count = tunnelGroupMapper.selectCount(new QueryWrapper().eq("name", name)); + if (count > 0) { + return R.err("隧道分组名称已存在"); + } + + long now = System.currentTimeMillis(); + TunnelGroup group = new TunnelGroup(); + group.setName(name); + group.setStatus(normalizeStatus(dto.getStatus())); + group.setCreatedTime(now); + group.setUpdatedTime(now); + tunnelGroupMapper.insert(group); + return R.ok(); + } + + @Override + public R updateTunnelGroup(GroupUpdateDto dto) { + TunnelGroup group = tunnelGroupMapper.selectById(dto.getId()); + if (group == null) { + return R.err("隧道分组不存在"); + } + String name = dto.getName().trim(); + int count = tunnelGroupMapper.selectCount(new QueryWrapper().eq("name", name).ne("id", dto.getId())); + if (count > 0) { + return R.err("隧道分组名称已存在"); + } + + group.setName(name); + if (dto.getStatus() != null) { + group.setStatus(dto.getStatus()); + } + group.setUpdatedTime(System.currentTimeMillis()); + tunnelGroupMapper.updateById(group); + return R.ok(); + } + + @Override + public R deleteTunnelGroup(Long id) { + TunnelGroup group = tunnelGroupMapper.selectById(id); + if (group == null) { + return R.err("隧道分组不存在"); + } + + List grants = groupPermissionGrantMapper.selectList(new QueryWrapper().eq("tunnel_group_id", id)); + revokeGrantRecords(grants); + tunnelGroupTunnelMapper.delete(new QueryWrapper().eq("tunnel_group_id", id)); + groupPermissionMapper.delete(new QueryWrapper().eq("tunnel_group_id", id)); + tunnelGroupMapper.deleteById(id); + return R.ok(); + } + + @Override + public R assignTunnelsToGroup(TunnelGroupAssignTunnelsDto dto) { + TunnelGroup group = tunnelGroupMapper.selectById(dto.getGroupId()); + if (group == null) { + return R.err("隧道分组不存在"); + } + + Set tunnelIds = new LinkedHashSet<>(dto.getTunnelIds()); + if (!tunnelIds.isEmpty()) { + List tunnels = tunnelService.list(new QueryWrapper().in("id", tunnelIds)); + if (tunnels.size() != tunnelIds.size()) { + return R.err("隧道列表中存在无效ID"); + } + } + + tunnelGroupTunnelMapper.delete(new QueryWrapper().eq("tunnel_group_id", dto.getGroupId())); + + if (!tunnelIds.isEmpty()) { + long now = System.currentTimeMillis(); + for (Long tunnelId : tunnelIds) { + TunnelGroupTunnel relation = new TunnelGroupTunnel(); + relation.setTunnelGroupId(dto.getGroupId()); + relation.setTunnelId(tunnelId); + relation.setCreatedTime(now); + tunnelGroupTunnelMapper.insert(relation); + } + } + + syncByTunnelGroup(dto.getGroupId()); + return R.ok(); + } + + @Override + public R getUserGroups() { + List groups = userGroupMapper.selectList(new QueryWrapper().orderByAsc("id")); + List mappings = userGroupUserMapper.selectList(new QueryWrapper()); + + Map> mappingByGroupId = mappings.stream() + .collect(Collectors.groupingBy(UserGroupUser::getUserGroupId)); + + Set userIds = mappings.stream().map(UserGroupUser::getUserId).collect(Collectors.toSet()); + Map userNameMap = buildUserNameMap(userIds); + + List result = new ArrayList<>(); + for (UserGroup group : groups) { + UserGroupDetailDto dto = new UserGroupDetailDto(); + dto.setId(group.getId()); + dto.setName(group.getName()); + dto.setStatus(group.getStatus()); + dto.setCreatedTime(group.getCreatedTime()); + dto.setUpdatedTime(group.getUpdatedTime()); + + List groupMappings = mappingByGroupId.getOrDefault(group.getId(), Collections.emptyList()); + List ids = groupMappings.stream().map(UserGroupUser::getUserId).collect(Collectors.toList()); + List names = ids.stream().map(userNameMap::get).filter(name -> name != null && !name.isBlank()).collect(Collectors.toList()); + dto.setUserIds(ids); + dto.setUserNames(names); + result.add(dto); + } + return R.ok(result); + } + + @Override + public R createUserGroup(GroupCreateDto dto) { + String name = dto.getName().trim(); + int count = userGroupMapper.selectCount(new QueryWrapper().eq("name", name)); + if (count > 0) { + return R.err("用户分组名称已存在"); + } + + long now = System.currentTimeMillis(); + UserGroup group = new UserGroup(); + group.setName(name); + group.setStatus(normalizeStatus(dto.getStatus())); + group.setCreatedTime(now); + group.setUpdatedTime(now); + userGroupMapper.insert(group); + return R.ok(); + } + + @Override + public R updateUserGroup(GroupUpdateDto dto) { + UserGroup group = userGroupMapper.selectById(dto.getId()); + if (group == null) { + return R.err("用户分组不存在"); + } + String name = dto.getName().trim(); + int count = userGroupMapper.selectCount(new QueryWrapper().eq("name", name).ne("id", dto.getId())); + if (count > 0) { + return R.err("用户分组名称已存在"); + } + + group.setName(name); + if (dto.getStatus() != null) { + group.setStatus(dto.getStatus()); + } + group.setUpdatedTime(System.currentTimeMillis()); + userGroupMapper.updateById(group); + return R.ok(); + } + + @Override + public R deleteUserGroup(Long id) { + UserGroup group = userGroupMapper.selectById(id); + if (group == null) { + return R.err("用户分组不存在"); + } + + List grants = groupPermissionGrantMapper.selectList(new QueryWrapper().eq("user_group_id", id)); + revokeGrantRecords(grants); + userGroupUserMapper.delete(new QueryWrapper().eq("user_group_id", id)); + groupPermissionMapper.delete(new QueryWrapper().eq("user_group_id", id)); + userGroupMapper.deleteById(id); + return R.ok(); + } + + @Override + public R assignUsersToGroup(UserGroupAssignUsersDto dto) { + UserGroup group = userGroupMapper.selectById(dto.getGroupId()); + if (group == null) { + return R.err("用户分组不存在"); + } + + Set userIds = new LinkedHashSet<>(dto.getUserIds()); + if (!userIds.isEmpty()) { + List users = userService.list(new QueryWrapper().in("id", userIds).ne("role_id", 0)); + if (users.size() != userIds.size()) { + return R.err("用户列表中存在无效ID"); + } + } + + userGroupUserMapper.delete(new QueryWrapper().eq("user_group_id", dto.getGroupId())); + + if (!userIds.isEmpty()) { + long now = System.currentTimeMillis(); + for (Long userId : userIds) { + UserGroupUser relation = new UserGroupUser(); + relation.setUserGroupId(dto.getGroupId()); + relation.setUserId(userId); + relation.setCreatedTime(now); + userGroupUserMapper.insert(relation); + } + } + + syncByUserGroup(dto.getGroupId()); + return R.ok(); + } + + @Override + public R getGroupPermissions() { + List permissions = groupPermissionMapper.selectList(new QueryWrapper().orderByDesc("id")); + if (permissions.isEmpty()) { + return R.ok(new ArrayList()); + } + + Set userGroupIds = permissions.stream().map(GroupPermission::getUserGroupId).collect(Collectors.toSet()); + Set tunnelGroupIds = permissions.stream().map(GroupPermission::getTunnelGroupId).collect(Collectors.toSet()); + + Map userGroupNameMap = userGroupMapper.selectList(new QueryWrapper().in("id", userGroupIds)).stream() + .collect(Collectors.toMap(UserGroup::getId, UserGroup::getName)); + Map tunnelGroupNameMap = tunnelGroupMapper.selectList(new QueryWrapper().in("id", tunnelGroupIds)).stream() + .collect(Collectors.toMap(TunnelGroup::getId, TunnelGroup::getName)); + + List result = new ArrayList<>(); + for (GroupPermission permission : permissions) { + GroupPermissionDetailDto dto = new GroupPermissionDetailDto(); + dto.setId(permission.getId()); + dto.setUserGroupId(permission.getUserGroupId()); + dto.setTunnelGroupId(permission.getTunnelGroupId()); + dto.setCreatedTime(permission.getCreatedTime()); + dto.setUserGroupName(userGroupNameMap.get(permission.getUserGroupId())); + dto.setTunnelGroupName(tunnelGroupNameMap.get(permission.getTunnelGroupId())); + result.add(dto); + } + + return R.ok(result); + } + + @Override + public R assignGroupPermission(GroupPermissionAssignDto dto) { + UserGroup userGroup = userGroupMapper.selectById(dto.getUserGroupId()); + if (userGroup == null) { + return R.err("用户分组不存在"); + } + TunnelGroup tunnelGroup = tunnelGroupMapper.selectById(dto.getTunnelGroupId()); + if (tunnelGroup == null) { + return R.err("隧道分组不存在"); + } + + int existing = groupPermissionMapper.selectCount(new QueryWrapper() + .eq("user_group_id", dto.getUserGroupId()) + .eq("tunnel_group_id", dto.getTunnelGroupId())); + if (existing == 0) { + GroupPermission permission = new GroupPermission(); + permission.setUserGroupId(dto.getUserGroupId()); + permission.setTunnelGroupId(dto.getTunnelGroupId()); + permission.setCreatedTime(System.currentTimeMillis()); + groupPermissionMapper.insert(permission); + } + + reconcilePermission(dto.getUserGroupId(), dto.getTunnelGroupId()); + return existing > 0 ? R.ok("权限已存在,已完成同步") : R.ok(); + } + + @Override + public R removeGroupPermission(Long id) { + GroupPermission permission = groupPermissionMapper.selectById(id); + if (permission == null) { + return R.err("权限记录不存在"); + } + + revokeByPermissionPair(permission.getUserGroupId(), permission.getTunnelGroupId()); + groupPermissionMapper.deleteById(id); + return R.ok(); + } + + private void syncByUserGroup(Long userGroupId) { + List permissions = groupPermissionMapper.selectList(new QueryWrapper().eq("user_group_id", userGroupId)); + for (GroupPermission permission : permissions) { + reconcilePermission(permission.getUserGroupId(), permission.getTunnelGroupId()); + } + } + + private void syncByTunnelGroup(Long tunnelGroupId) { + List permissions = groupPermissionMapper.selectList(new QueryWrapper().eq("tunnel_group_id", tunnelGroupId)); + for (GroupPermission permission : permissions) { + reconcilePermission(permission.getUserGroupId(), permission.getTunnelGroupId()); + } + } + + private void reconcilePermission(Long userGroupId, Long tunnelGroupId) { + Set userIds = userGroupUserMapper.selectList(new QueryWrapper().eq("user_group_id", userGroupId)).stream() + .map(UserGroupUser::getUserId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Set tunnelIds = tunnelGroupTunnelMapper.selectList(new QueryWrapper().eq("tunnel_group_id", tunnelGroupId)).stream() + .map(TunnelGroupTunnel::getTunnelId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Set desiredKeys = new HashSet<>(); + for (Long userId : userIds) { + for (Long tunnelId : tunnelIds) { + desiredKeys.add(permissionKey(userId, tunnelId)); + } + } + + List currentGrants = groupPermissionGrantMapper.selectList( + new QueryWrapper() + .eq("user_group_id", userGroupId) + .eq("tunnel_group_id", tunnelGroupId) + ); + + Set grantUserTunnelIds = currentGrants.stream().map(GroupPermissionGrant::getUserTunnelId).collect(Collectors.toSet()); + Map grantUserTunnelMap = new HashMap<>(); + if (!grantUserTunnelIds.isEmpty()) { + List userTunnels = userTunnelService.list(new QueryWrapper().in("id", grantUserTunnelIds)); + grantUserTunnelMap = userTunnels.stream().collect(Collectors.toMap(ut -> ut.getId().longValue(), Function.identity())); + } + + Map pairUserTunnelMap = new HashMap<>(); + if (!userIds.isEmpty() && !tunnelIds.isEmpty()) { + List pairUserTunnels = userTunnelService.list(new QueryWrapper() + .in("user_id", userIds) + .in("tunnel_id", tunnelIds)); + for (UserTunnel userTunnel : pairUserTunnels) { + pairUserTunnelMap.putIfAbsent(permissionKey(userTunnel.getUserId().longValue(), userTunnel.getTunnelId().longValue()), userTunnel); + } + } + + Set pairUserTunnelIds = pairUserTunnelMap.values().stream() + .map(ut -> ut.getId().longValue()) + .collect(Collectors.toSet()); + Map totalGrantCountMap = buildGrantCountMap(pairUserTunnelIds); + + Set currentGrantUserTunnelIds = currentGrants.stream().map(GroupPermissionGrant::getUserTunnelId).collect(Collectors.toSet()); + if (!desiredKeys.isEmpty()) { + Map userMap = userService.list(new QueryWrapper().in("id", userIds)).stream() + .collect(Collectors.toMap(User::getId, Function.identity())); + + long now = System.currentTimeMillis(); + for (Long userId : userIds) { + User user = userMap.get(userId); + if (user == null) { + continue; + } + for (Long tunnelId : tunnelIds) { + String pairKey = permissionKey(userId, tunnelId); + UserTunnel userTunnel = pairUserTunnelMap.get(pairKey); + if (userTunnel == null) { + userTunnel = createGroupManagedUserTunnel(userId, tunnelId, user); + pairUserTunnelMap.put(pairKey, userTunnel); + createGrant(userGroupId, tunnelGroupId, userTunnel.getId().longValue(), now); + currentGrantUserTunnelIds.add(userTunnel.getId().longValue()); + totalGrantCountMap.put(userTunnel.getId().longValue(), 1L); + continue; + } + + Long userTunnelId = userTunnel.getId().longValue(); + if (currentGrantUserTunnelIds.contains(userTunnelId)) { + continue; + } + + long existingGrantCount = totalGrantCountMap.getOrDefault(userTunnelId, 0L); + if (existingGrantCount > 0L) { + createGrant(userGroupId, tunnelGroupId, userTunnelId, now); + currentGrantUserTunnelIds.add(userTunnelId); + totalGrantCountMap.put(userTunnelId, existingGrantCount + 1L); + } + } + } + } + + List staleGrants = new ArrayList<>(); + for (GroupPermissionGrant grant : currentGrants) { + UserTunnel userTunnel = grantUserTunnelMap.get(grant.getUserTunnelId()); + boolean keep = false; + if (userTunnel != null) { + String key = permissionKey(userTunnel.getUserId().longValue(), userTunnel.getTunnelId().longValue()); + keep = desiredKeys.contains(key); + } + + if (!keep) { + staleGrants.add(grant); + } + } + revokeGrantRecords(staleGrants); + } + + private UserTunnel createGroupManagedUserTunnel(Long userId, Long tunnelId, User user) { + UserTunnel userTunnel = new UserTunnel(); + userTunnel.setUserId(userId.intValue()); + userTunnel.setTunnelId(tunnelId.intValue()); + userTunnel.setStatus(1); + userTunnel.setInFlow(0L); + userTunnel.setOutFlow(0L); + userTunnel.setFlow(user.getFlow()); + userTunnel.setNum(user.getNum()); + userTunnel.setFlowResetTime(user.getFlowResetTime()); + userTunnel.setExpTime(user.getExpTime()); + userTunnelService.save(userTunnel); + return userTunnel; + } + + private void createGrant(Long userGroupId, Long tunnelGroupId, Long userTunnelId, long createdTime) { + int exists = groupPermissionGrantMapper.selectCount(new QueryWrapper() + .eq("user_group_id", userGroupId) + .eq("tunnel_group_id", tunnelGroupId) + .eq("user_tunnel_id", userTunnelId)); + if (exists > 0) { + return; + } + + GroupPermissionGrant grant = new GroupPermissionGrant(); + grant.setUserGroupId(userGroupId); + grant.setTunnelGroupId(tunnelGroupId); + grant.setUserTunnelId(userTunnelId); + grant.setCreatedTime(createdTime); + groupPermissionGrantMapper.insert(grant); + } + + private void revokeByPermissionPair(Long userGroupId, Long tunnelGroupId) { + List grants = groupPermissionGrantMapper.selectList(new QueryWrapper() + .eq("user_group_id", userGroupId) + .eq("tunnel_group_id", tunnelGroupId)); + revokeGrantRecords(grants); + } + + private void revokeGrantRecords(List grants) { + if (grants == null || grants.isEmpty()) { + return; + } + + Set candidateUserTunnelIds = new HashSet<>(); + for (GroupPermissionGrant grant : grants) { + candidateUserTunnelIds.add(grant.getUserTunnelId()); + groupPermissionGrantMapper.deleteById(grant.getId()); + } + + Set stillGrantedUserTunnelIds = groupPermissionGrantMapper.selectList( + new QueryWrapper().in("user_tunnel_id", candidateUserTunnelIds) + ).stream() + .map(GroupPermissionGrant::getUserTunnelId) + .collect(Collectors.toSet()); + + for (Long userTunnelId : candidateUserTunnelIds) { + if (!stillGrantedUserTunnelIds.contains(userTunnelId)) { + userTunnelService.removeUserTunnel(userTunnelId.intValue()); + } + } + } + + private Map buildGrantCountMap(Set userTunnelIds) { + if (userTunnelIds.isEmpty()) { + return Collections.emptyMap(); + } + + List grants = groupPermissionGrantMapper.selectList(new QueryWrapper().in("user_tunnel_id", userTunnelIds)); + Map countMap = new HashMap<>(); + for (GroupPermissionGrant grant : grants) { + countMap.merge(grant.getUserTunnelId(), 1L, Long::sum); + } + return countMap; + } + + private Map buildTunnelNameMap(Set tunnelIds) { + if (tunnelIds.isEmpty()) { + return Collections.emptyMap(); + } + return tunnelService.list(new QueryWrapper().in("id", tunnelIds)).stream() + .collect(Collectors.toMap(Tunnel::getId, Tunnel::getName)); + } + + private Map buildUserNameMap(Set userIds) { + if (userIds.isEmpty()) { + return Collections.emptyMap(); + } + return userService.list(new QueryWrapper().in("id", userIds)).stream() + .collect(Collectors.toMap(User::getId, User::getUser)); + } + + private String permissionKey(Long userId, Long tunnelId) { + return userId + "_" + tunnelId; + } + + private int normalizeStatus(Integer status) { + return status == null ? 1 : status; + } +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java index 022a252..982f8a8 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java @@ -8,6 +8,8 @@ import com.admin.common.utils.JwtUtil; import com.admin.common.utils.WebSocketServer; import com.admin.entity.*; import com.admin.mapper.TunnelMapper; +import com.admin.mapper.GroupPermissionGrantMapper; +import com.admin.mapper.TunnelGroupTunnelMapper; import com.admin.mapper.UserTunnelMapper; import com.admin.service.*; import com.alibaba.fastjson.JSONArray; @@ -52,6 +54,12 @@ public class TunnelServiceImpl extends ServiceImpl impleme @Resource ForwardPortService forwardPortService; + @Resource + TunnelGroupTunnelMapper tunnelGroupTunnelMapper; + + @Resource + GroupPermissionGrantMapper groupPermissionGrantMapper; + @Override public R createTunnel(TunnelDto tunnelDto) { @@ -578,8 +586,16 @@ public class TunnelServiceImpl extends ServiceImpl impleme for (Forward forward : forwardList) { forwardService.deleteForward(forward.getId()); } + + List userTunnels = userTunnelService.list(new QueryWrapper().eq("tunnel_id", id)); + if (!userTunnels.isEmpty()) { + List userTunnelIds = userTunnels.stream().map(UserTunnel::getId).toList(); + groupPermissionGrantMapper.delete(new QueryWrapper().in("user_tunnel_id", userTunnelIds)); + } + forwardService.remove(new QueryWrapper().eq("tunnel_id", id)); userTunnelService.remove(new QueryWrapper().eq("tunnel_id", id)); + tunnelGroupTunnelMapper.delete(new QueryWrapper().eq("tunnel_id", id)); this.removeById(id); List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", id)); diff --git a/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java index 17b183d..d291faa 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java @@ -11,6 +11,8 @@ import com.admin.common.utils.JwtUtil; import com.admin.common.utils.Md5Util; import com.admin.entity.*; import com.admin.mapper.UserMapper; +import com.admin.mapper.GroupPermissionGrantMapper; +import com.admin.mapper.UserGroupUserMapper; import com.admin.service.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -56,6 +58,12 @@ public class UserServiceImpl extends ServiceImpl implements Us @Resource StatisticsFlowService statisticsFlowService; + @Resource + GroupPermissionGrantMapper groupPermissionGrantMapper; + + @Resource + UserGroupUserMapper userGroupUserMapper; + @Resource @Lazy ForwardPortService forwardPortService; @@ -140,8 +148,16 @@ public class UserServiceImpl extends ServiceImpl implements Us for (Forward forward : forwardList) { forwardService.deleteForward(forward.getId()); } + + List userTunnels = userTunnelService.list(new QueryWrapper().eq("user_id", id)); + if (!userTunnels.isEmpty()) { + List userTunnelIds = userTunnels.stream().map(UserTunnel::getId).toList(); + groupPermissionGrantMapper.delete(new QueryWrapper().in("user_tunnel_id", userTunnelIds)); + } + forwardService.remove(new QueryWrapper().eq("user_id", id)); userTunnelService.remove(new QueryWrapper().eq("user_id", id)); + userGroupUserMapper.delete(new QueryWrapper().eq("user_id", id)); statisticsFlowService.remove(new QueryWrapper().eq("user_id", id)); this.removeById(id); return R.ok(); diff --git a/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java index 6f8c29e..d130870 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java @@ -4,6 +4,7 @@ import com.admin.common.dto.*; import com.admin.common.lang.R; import com.admin.entity.User; import com.admin.entity.UserTunnel; +import com.admin.mapper.GroupPermissionGrantMapper; import com.admin.mapper.UserTunnelMapper; import com.admin.service.UserService; import com.admin.service.UserTunnelService; @@ -34,6 +35,9 @@ public class UserTunnelServiceImpl extends ServiceImpl() @@ -129,6 +133,7 @@ public class UserTunnelServiceImpl extends ServiceImpl().eq("user_tunnel_id", id)); this.removeById(id); return R.ok(); } diff --git a/springboot-backend/src/main/resources/schema.sql b/springboot-backend/src/main/resources/schema.sql index ef324e8..0c963e2 100644 --- a/springboot-backend/src/main/resources/schema.sql +++ b/springboot-backend/src/main/resources/schema.sql @@ -121,6 +121,58 @@ CREATE TABLE IF NOT EXISTS user_tunnel ( status INTEGER NOT NULL ); +CREATE TABLE IF NOT EXISTS tunnel_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS tunnel_group_tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tunnel_group_id INTEGER NOT NULL, + tunnel_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_group_user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS group_permission ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group_id INTEGER NOT NULL, + tunnel_group_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS group_permission_grant ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group_id INTEGER NOT NULL, + tunnel_group_id INTEGER NOT NULL, + user_tunnel_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id); + CREATE TABLE IF NOT EXISTS vite_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(200) NOT NULL UNIQUE, diff --git a/vite-frontend/src/App.tsx b/vite-frontend/src/App.tsx index bc642d1..ab3c8a8 100644 --- a/vite-frontend/src/App.tsx +++ b/vite-frontend/src/App.tsx @@ -8,6 +8,7 @@ import ForwardPage from "@/pages/forward"; import TunnelPage from "@/pages/tunnel"; import NodePage from "@/pages/node"; import UserPage from "@/pages/user"; +import GroupPage from "@/pages/group"; import ProfilePage from "@/pages/profile"; import LimitPage from "@/pages/limit"; import ConfigPage from "@/pages/config"; @@ -208,6 +209,14 @@ function App() { } path="/user" /> + + + + } + path="/group" + /> diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index 5b3164f..4d972de 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -147,3 +147,43 @@ export const batchChangeTunnel = (data: { forwardIds: number[]; targetTunnelId: number; }) => Network.post("/forward/batch-change-tunnel", data); + +// 分组与权限分配接口 +export const getTunnelGroupList = () => Network.post("/group/tunnel/list"); +export const createTunnelGroup = (data: { name: string; status?: number }) => + Network.post("/group/tunnel/create", data); +export const updateTunnelGroup = (data: { + id: number; + name: string; + status?: number; +}) => Network.post("/group/tunnel/update", data); +export const deleteTunnelGroup = (id: number) => + Network.post("/group/tunnel/delete", { id }); +export const assignTunnelsToGroup = (data: { + groupId: number; + tunnelIds: number[]; +}) => Network.post("/group/tunnel/assign", data); + +export const getUserGroupList = () => Network.post("/group/user/list"); +export const createUserGroup = (data: { name: string; status?: number }) => + Network.post("/group/user/create", data); +export const updateUserGroup = (data: { + id: number; + name: string; + status?: number; +}) => Network.post("/group/user/update", data); +export const deleteUserGroup = (id: number) => + Network.post("/group/user/delete", { id }); +export const assignUsersToGroup = (data: { + groupId: number; + userIds: number[]; +}) => Network.post("/group/user/assign", data); + +export const getGroupPermissionList = () => + Network.post("/group/permission/list"); +export const assignGroupPermission = (data: { + userGroupId: number; + tunnelGroupId: number; +}) => Network.post("/group/permission/assign", data); +export const removeGroupPermission = (id: number) => + Network.post("/group/permission/remove", { id }); diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index 23c8f9d..8880ad1 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -134,6 +134,16 @@ export default function AdminLayout({ ), adminOnly: true, }, + { + path: "/group", + label: "分组", + icon: ( + + + + ), + adminOnly: true, + }, { path: "/config", label: "设置", diff --git a/vite-frontend/src/pages/group.tsx b/vite-frontend/src/pages/group.tsx new file mode 100644 index 0000000..d30051b --- /dev/null +++ b/vite-frontend/src/pages/group.tsx @@ -0,0 +1,858 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Card, CardBody, CardHeader } from "@heroui/card"; +import { Button } from "@heroui/button"; +import { Input } from "@heroui/input"; +import { + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + useDisclosure, +} from "@heroui/modal"; +import { Select, SelectItem } from "@heroui/select"; +import { + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from "@heroui/table"; +import { Chip } from "@heroui/chip"; +import { Spinner } from "@heroui/spinner"; +import toast from "react-hot-toast"; + +import { + assignGroupPermission, + assignTunnelsToGroup, + assignUsersToGroup, + createTunnelGroup, + createUserGroup, + deleteTunnelGroup, + deleteUserGroup, + getAllUsers, + getGroupPermissionList, + getTunnelGroupList, + getTunnelList, + getUserGroupList, + removeGroupPermission, + updateTunnelGroup, + updateUserGroup, +} from "@/api"; + +interface TunnelItem { + id: number; + name: string; +} + +interface UserItem { + id: number; + user: string; +} + +interface TunnelGroup { + id: number; + name: string; + status: number; + tunnelIds: number[]; + tunnelNames: string[]; + createdTime: number; +} + +interface UserGroup { + id: number; + name: string; + status: number; + userIds: number[]; + userNames: string[]; + createdTime: number; +} + +interface GroupPermission { + id: number; + userGroupId: number; + userGroupName: string; + tunnelGroupId: number; + tunnelGroupName: string; + createdTime: number; +} + +const formatDate = (timestamp?: number): string => { + if (!timestamp) { + return "-"; + } + + return new Date(timestamp).toLocaleString(); +}; + +const isAdminUser = () => { + let adminFlag = localStorage.getItem("admin") === "true"; + + if (localStorage.getItem("admin") === null) { + const roleId = parseInt(localStorage.getItem("role_id") || "1", 10); + + adminFlag = roleId === 0; + localStorage.setItem("admin", adminFlag.toString()); + } + + return adminFlag; +}; + +export default function GroupPage() { + const [loading, setLoading] = useState(true); + const [isAdmin] = useState(isAdminUser()); + + const [tunnelGroups, setTunnelGroups] = useState([]); + const [userGroups, setUserGroups] = useState([]); + const [permissions, setPermissions] = useState([]); + const [tunnels, setTunnels] = useState([]); + const [users, setUsers] = useState([]); + + const [selectedUserGroupId, setSelectedUserGroupId] = useState( + null, + ); + const [selectedTunnelGroupId, setSelectedTunnelGroupId] = useState< + number | null + >(null); + + const [savingPermission, setSavingPermission] = useState(false); + + const { + isOpen: tunnelGroupModalOpen, + onOpen: onTunnelGroupModalOpen, + onClose: onTunnelGroupModalClose, + onOpenChange: onTunnelGroupModalChange, + } = useDisclosure(); + const { + isOpen: userGroupModalOpen, + onOpen: onUserGroupModalOpen, + onClose: onUserGroupModalClose, + onOpenChange: onUserGroupModalChange, + } = useDisclosure(); + const { + isOpen: tunnelAssignModalOpen, + onOpen: onTunnelAssignModalOpen, + onClose: onTunnelAssignModalClose, + onOpenChange: onTunnelAssignModalChange, + } = useDisclosure(); + const { + isOpen: userAssignModalOpen, + onOpen: onUserAssignModalOpen, + onClose: onUserAssignModalClose, + onOpenChange: onUserAssignModalChange, + } = useDisclosure(); + + const [editingTunnelGroup, setEditingTunnelGroup] = + useState(null); + const [editingUserGroup, setEditingUserGroup] = useState( + null, + ); + const [groupName, setGroupName] = useState(""); + const [groupStatus, setGroupStatus] = useState("1"); + const [savingGroup, setSavingGroup] = useState(false); + + const [assignTunnelGroup, setAssignTunnelGroup] = + useState(null); + const [assignUserGroup, setAssignUserGroup] = useState( + null, + ); + const [selectedTunnelKeys, setSelectedTunnelKeys] = useState>( + new Set(), + ); + const [selectedUserKeys, setSelectedUserKeys] = useState>( + new Set(), + ); + const [savingAssign, setSavingAssign] = useState(false); + + const tunnelNameMap = useMemo(() => { + const map = new Map(); + + tunnels.forEach((item) => { + map.set(item.id, item.name); + }); + + return map; + }, [tunnels]); + + const userNameMap = useMemo(() => { + const map = new Map(); + + users.forEach((item) => { + map.set(item.id, item.user); + }); + + return map; + }, [users]); + + const loadData = useCallback(async () => { + setLoading(true); + try { + const [tunnelGroupRes, userGroupRes, permissionRes, tunnelRes, userRes] = + await Promise.all([ + getTunnelGroupList(), + getUserGroupList(), + getGroupPermissionList(), + getTunnelList(), + getAllUsers(), + ]); + + if (tunnelGroupRes.code === 0) { + setTunnelGroups(tunnelGroupRes.data || []); + } + if (userGroupRes.code === 0) { + setUserGroups(userGroupRes.data || []); + } + if (permissionRes.code === 0) { + setPermissions(permissionRes.data || []); + } + if (tunnelRes.code === 0) { + setTunnels(tunnelRes.data || []); + } + if (userRes.code === 0) { + setUsers(userRes.data || []); + } + + if ( + tunnelGroupRes.code !== 0 || + userGroupRes.code !== 0 || + permissionRes.code !== 0 + ) { + toast.error("部分分组数据加载失败"); + } + } catch { + toast.error("分组数据加载失败"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadData(); + }, [loadData]); + + const openCreateTunnelGroup = () => { + setEditingTunnelGroup(null); + setGroupName(""); + setGroupStatus("1"); + onTunnelGroupModalOpen(); + }; + + const openEditTunnelGroup = (group: TunnelGroup) => { + setEditingTunnelGroup(group); + setGroupName(group.name); + setGroupStatus(String(group.status)); + onTunnelGroupModalOpen(); + }; + + const openCreateUserGroup = () => { + setEditingUserGroup(null); + setGroupName(""); + setGroupStatus("1"); + onUserGroupModalOpen(); + }; + + const openEditUserGroup = (group: UserGroup) => { + setEditingUserGroup(group); + setGroupName(group.name); + setGroupStatus(String(group.status)); + onUserGroupModalOpen(); + }; + + const saveTunnelGroup = async () => { + if (!groupName.trim()) { + toast.error("请输入分组名称"); + + return; + } + setSavingGroup(true); + try { + const payload = { name: groupName.trim(), status: Number(groupStatus) }; + const res = editingTunnelGroup + ? await updateTunnelGroup({ id: editingTunnelGroup.id, ...payload }) + : await createTunnelGroup(payload); + + if (res.code === 0) { + toast.success(editingTunnelGroup ? "更新成功" : "创建成功"); + onTunnelGroupModalClose(); + loadData(); + } else { + toast.error(res.msg || "保存失败"); + } + } catch { + toast.error("保存失败"); + } finally { + setSavingGroup(false); + } + }; + + const saveUserGroup = async () => { + if (!groupName.trim()) { + toast.error("请输入分组名称"); + + return; + } + setSavingGroup(true); + try { + const payload = { name: groupName.trim(), status: Number(groupStatus) }; + const res = editingUserGroup + ? await updateUserGroup({ id: editingUserGroup.id, ...payload }) + : await createUserGroup(payload); + + if (res.code === 0) { + toast.success(editingUserGroup ? "更新成功" : "创建成功"); + onUserGroupModalClose(); + loadData(); + } else { + toast.error(res.msg || "保存失败"); + } + } catch { + toast.error("保存失败"); + } finally { + setSavingGroup(false); + } + }; + + const handleDeleteTunnelGroup = async (id: number) => { + try { + const res = await deleteTunnelGroup(id); + + if (res.code === 0) { + toast.success("删除成功"); + loadData(); + } else { + toast.error(res.msg || "删除失败"); + } + } catch { + toast.error("删除失败"); + } + }; + + const handleDeleteUserGroup = async (id: number) => { + try { + const res = await deleteUserGroup(id); + + if (res.code === 0) { + toast.success("删除成功"); + loadData(); + } else { + toast.error(res.msg || "删除失败"); + } + } catch { + toast.error("删除失败"); + } + }; + + const openAssignTunnels = (group: TunnelGroup) => { + setAssignTunnelGroup(group); + setSelectedTunnelKeys(new Set(group.tunnelIds.map((id) => String(id)))); + onTunnelAssignModalOpen(); + }; + + const openAssignUsers = (group: UserGroup) => { + setAssignUserGroup(group); + setSelectedUserKeys(new Set(group.userIds.map((id) => String(id)))); + onUserAssignModalOpen(); + }; + + const saveAssignTunnels = async () => { + if (!assignTunnelGroup) return; + setSavingAssign(true); + try { + const tunnelIds = Array.from(selectedTunnelKeys).map((id) => Number(id)); + const res = await assignTunnelsToGroup({ + groupId: assignTunnelGroup.id, + tunnelIds, + }); + + if (res.code === 0) { + toast.success("分配成功"); + onTunnelAssignModalClose(); + loadData(); + } else { + toast.error(res.msg || "分配失败"); + } + } catch { + toast.error("分配失败"); + } finally { + setSavingAssign(false); + } + }; + + const saveAssignUsers = async () => { + if (!assignUserGroup) return; + setSavingAssign(true); + try { + const userIds = Array.from(selectedUserKeys).map((id) => Number(id)); + const res = await assignUsersToGroup({ + groupId: assignUserGroup.id, + userIds, + }); + + if (res.code === 0) { + toast.success("分配成功"); + onUserAssignModalClose(); + loadData(); + } else { + toast.error(res.msg || "分配失败"); + } + } catch { + toast.error("分配失败"); + } finally { + setSavingAssign(false); + } + }; + + const handleAssignPermission = async () => { + if (!selectedUserGroupId || !selectedTunnelGroupId) { + toast.error("请选择用户分组和隧道分组"); + + return; + } + setSavingPermission(true); + try { + const res = await assignGroupPermission({ + userGroupId: selectedUserGroupId, + tunnelGroupId: selectedTunnelGroupId, + }); + + if (res.code === 0) { + toast.success(res.msg || "权限分配成功"); + loadData(); + } else { + toast.error(res.msg || "权限分配失败"); + } + } catch { + toast.error("权限分配失败"); + } finally { + setSavingPermission(false); + } + }; + + const handleRemovePermission = async (id: number) => { + try { + const res = await removeGroupPermission(id); + + if (res.code === 0) { + toast.success("权限回收成功"); + loadData(); + } else { + toast.error(res.msg || "权限回收失败"); + } + } catch { + toast.error("权限回收失败"); + } + }; + + if (!isAdmin) { + return ( +
+ + +

+ 权限不足,只有管理员可以访问分组管理页面。 +

+
+
+
+ ); + } + + return ( +
+ {loading && ( +
+ +
+ )} + + + +

隧道分组

+ +
+ + + + 名称 + 隧道 + 状态 + 创建时间 + 操作 + + + {(item) => ( + + {item.name} + + {item.tunnelNames.length > 0 + ? item.tunnelNames.join("、") + : "-"} + + + + {item.status === 1 ? "启用" : "停用"} + + + {formatDate(item.createdTime)} + +
+ + + +
+
+
+ )} +
+
+
+
+ + + +

用户分组

+ +
+ + + + 名称 + 用户 + 状态 + 创建时间 + 操作 + + + {(item) => ( + + {item.name} + + {item.userNames.length > 0 + ? item.userNames.join("、") + : "-"} + + + + {item.status === 1 ? "启用" : "停用"} + + + {formatDate(item.createdTime)} + +
+ + + +
+
+
+ )} +
+
+
+
+ + + +

权限分配

+
+ +
+ + + +
+ + + + ID + 用户分组 + 隧道分组 + 创建时间 + 操作 + + + {(item) => ( + + {item.id} + + {item.userGroupName || item.userGroupId} + + + {item.tunnelGroupName || item.tunnelGroupId} + + {formatDate(item.createdTime)} + + + + + )} + +
+
+
+ + + + + {editingTunnelGroup ? "编辑隧道分组" : "新建隧道分组"} + + + setGroupName(e.target.value)} + /> + + + + + + + + + + + + + {editingUserGroup ? "编辑用户分组" : "新建用户分组"} + + + setGroupName(e.target.value)} + /> + + + + + + + + + + + + 分配隧道 - {assignTunnelGroup?.name} + + +

+ 当前已选: + {Array.from(selectedTunnelKeys) + .map((id) => tunnelNameMap.get(Number(id)) || id) + .join("、") || "无"} +

+

+ 不选择任何隧道并保存将清空该分组成员。 +

+
+ + + + +
+
+ + + + 分配用户 - {assignUserGroup?.name} + + +

+ 当前已选: + {Array.from(selectedUserKeys) + .map((id) => userNameMap.get(Number(id)) || id) + .join("、") || "无"} +

+

+ 不选择任何用户并保存将清空该分组成员。 +

+
+ + + + +
+
+
+ ); +}