diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index aa4f18f..4d11043 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '20.19.0' - name: Install dependencies run: npm install --legacy-peer-deps 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..dca5e99 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,19 @@ 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_by_group INTEGER NOT NULL DEFAULT 0, created_time INTEGER NOT NULL)"); + ensureColumn("group_permission_grant", "created_by_group", "INTEGER NOT NULL DEFAULT 0"); + 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 +64,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..123813c --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/GroupPermissionGrant.java @@ -0,0 +1,26 @@ +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 Integer createdByGroup; + + 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..c1d088b --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/impl/GroupServiceImpl.java @@ -0,0 +1,606 @@ +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 groupManagedUserTunnelIds = buildGroupManagedUserTunnelIds(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(), true, now); + currentGrantUserTunnelIds.add(userTunnel.getId().longValue()); + totalGrantCountMap.put(userTunnel.getId().longValue(), 1L); + groupManagedUserTunnelIds.add(userTunnel.getId().longValue()); + continue; + } + + Long userTunnelId = userTunnel.getId().longValue(); + if (currentGrantUserTunnelIds.contains(userTunnelId)) { + continue; + } + + long existingGrantCount = totalGrantCountMap.getOrDefault(userTunnelId, 0L); + boolean createdByGroup = groupManagedUserTunnelIds.contains(userTunnelId); + createGrant(userGroupId, tunnelGroupId, userTunnelId, createdByGroup, 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, boolean createdByGroup, 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.setCreatedByGroup(createdByGroup ? 1 : 0); + 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<>(); + Set groupManagedCandidates = new HashSet<>(); + for (GroupPermissionGrant grant : grants) { + candidateUserTunnelIds.add(grant.getUserTunnelId()); + if (grant.getCreatedByGroup() != null && grant.getCreatedByGroup() == 1) { + groupManagedCandidates.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) && groupManagedCandidates.contains(userTunnelId)) { + userTunnelService.removeUserTunnel(userTunnelId.intValue()); + } + } + } + + private Set buildGroupManagedUserTunnelIds(Set userTunnelIds) { + if (userTunnelIds.isEmpty()) { + return Collections.emptySet(); + } + + return groupPermissionGrantMapper.selectList(new QueryWrapper() + .in("user_tunnel_id", userTunnelIds) + .eq("created_by_group", 1)) + .stream() + .map(GroupPermissionGrant::getUserTunnelId) + .collect(Collectors.toSet()); + } + + private Map buildGrantCountMap(Set userTunnelIds) { + if (userTunnelIds.isEmpty()) { + return new HashMap<>(); + } + + 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..175933e 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 @@ -2,8 +2,10 @@ package com.admin.service.impl; import com.admin.common.dto.*; import com.admin.common.lang.R; +import com.admin.entity.GroupPermissionGrant; 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 +36,9 @@ public class UserTunnelServiceImpl extends ServiceImpl() @@ -123,6 +128,13 @@ public class UserTunnelServiceImpl extends ServiceImpl().eq("user_tunnel_id", id) + ); + if (grantCount > 0) { + return R.err("该隧道权限由分组授权,请先调整分组权限或分组成员"); + } + List forwardList = forwardService.list(new QueryWrapper() .eq("user_id", userTunnel.getUserId()) .eq("tunnel_id", userTunnel.getTunnelId())); diff --git a/springboot-backend/src/main/resources/schema.sql b/springboot-backend/src/main/resources/schema.sql index ef324e8..9330f04 100644 --- a/springboot-backend/src/main/resources/schema.sql +++ b/springboot-backend/src/main/resources/schema.sql @@ -121,6 +121,59 @@ 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_by_group INTEGER NOT NULL DEFAULT 0, + 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/package.json b/vite-frontend/package.json index c4016d1..2427832 100644 --- a/vite-frontend/package.json +++ b/vite-frontend/package.json @@ -45,7 +45,7 @@ "@react-aria/visually-hidden": "3.8.25", "@react-types/shared": "3.30.0", "@tailwindcss/postcss": "4.1.11", - "@tailwindcss/vite": "4.1.11", + "@tailwindcss/vite": "^4.1.18", "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.11.0", "clsx": "2.1.1", @@ -70,7 +70,7 @@ "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", - "@vitejs/plugin-react": "4.4.1", + "@vitejs/plugin-react": "^5.1.3", "eslint": "9.39.2", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.31.0", @@ -84,7 +84,7 @@ "postcss": "8.5.6", "prettier": "3.5.3", "typescript": "5.6.3", - "vite": "5.4.21", - "vite-tsconfig-paths": "4.3.2" + "vite": "npm:rolldown-vite@^7.3.1", + "vite-tsconfig-paths": "^6.0.5" } } diff --git a/vite-frontend/src/App.tsx b/vite-frontend/src/App.tsx index 4d5bbdd..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"; @@ -148,9 +149,7 @@ function App() { if (cachedAppName && cachedAppName !== document.title) { document.title = cachedAppName; } - } catch (error) { - console.warn("检查标题更新失败:", error); - } + } catch {} }; // 延迟检查,避免阻塞初始渲染 @@ -210,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/api/network.ts b/vite-frontend/src/api/network.ts index 0ea6734..53486e9 100644 --- a/vite-frontend/src/api/network.ts +++ b/vite-frontend/src/api/network.ts @@ -98,8 +98,6 @@ const Network = { resolve(response.data); }) .catch(function (error: any) { - console.error("GET请求错误:", error); - // 检查是否是401错误(token失效) if (error.response && error.response.status === 401) { handleTokenExpired(); @@ -146,8 +144,6 @@ const Network = { resolve(response.data); }) .catch(function (error: any) { - console.error("POST请求错误:", error); - // 检查是否是401错误(token失效) if (error.response && error.response.status === 401) { handleTokenExpired(); diff --git a/vite-frontend/src/components/navbar.tsx b/vite-frontend/src/components/navbar.tsx index 17ba3e3..b7b0c02 100644 --- a/vite-frontend/src/components/navbar.tsx +++ b/vite-frontend/src/components/navbar.tsx @@ -33,9 +33,7 @@ export const Navbar = () => { // 同步更新siteConfig siteConfig.name = cachedAppName; } - } catch (error) { - console.warn("检查配置更新失败:", error); - } + } catch {} }; // 延迟执行,避免阻塞初始渲染 @@ -50,9 +48,7 @@ export const Navbar = () => { setAppName(cachedAppName); siteConfig.name = cachedAppName; } - } catch (error) { - console.warn("更新配置失败:", error); - } + } catch {} }; window.addEventListener("configUpdated", handleConfigUpdate); diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index be054e3..bb89220 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -126,7 +126,7 @@ export const getCachedConfigs = async (): Promise> => { return configs; } - } catch (error) { + } catch { // API失败时返回缓存的数据 if (hasCachedData) { return cachedConfigs; diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index 650e5bb..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: "设置", @@ -260,9 +270,8 @@ export default function AdminLayout({ } else { toast.error(response.msg || "密码修改失败"); } - } catch (error) { + } catch { toast.error("修改密码时发生错误"); - console.error("修改密码错误:", error); } finally { setPasswordLoading(false); } @@ -289,8 +298,10 @@ export default function AdminLayout({ > {/* 移动端遮罩层 */} {isMobile && mobileMenuVisible && ( -
)} diff --git a/vite-frontend/src/layouts/h5-simple.tsx b/vite-frontend/src/layouts/h5-simple.tsx index e4d1b29..7f77735 100644 --- a/vite-frontend/src/layouts/h5-simple.tsx +++ b/vite-frontend/src/layouts/h5-simple.tsx @@ -17,7 +17,7 @@ export default function H5SimpleLayout({ React.useEffect(() => { try { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); - } catch (e) { + } catch { window.scrollTo(0, 0); } document.body.scrollTop = 0; diff --git a/vite-frontend/src/layouts/h5.tsx b/vite-frontend/src/layouts/h5.tsx index 533e1e5..db3f202 100644 --- a/vite-frontend/src/layouts/h5.tsx +++ b/vite-frontend/src/layouts/h5.tsx @@ -108,7 +108,7 @@ export default function H5Layout({ children }: { children: React.ReactNode }) { useEffect(() => { try { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); - } catch (e) { + } catch { window.scrollTo(0, 0); } document.body.scrollTop = 0; diff --git a/vite-frontend/src/pages/change-password.tsx b/vite-frontend/src/pages/change-password.tsx index 4a470f8..99d09cb 100644 --- a/vite-frontend/src/pages/change-password.tsx +++ b/vite-frontend/src/pages/change-password.tsx @@ -89,8 +89,7 @@ export default function ChangePasswordPage() { } else { toast.error(response.msg || "账号密码修改失败"); } - } catch (error) { - console.error("修改账号密码错误:", error); + } catch { toast.error("修改账号密码时发生错误"); } finally { setLoading(false); diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx index c46ad5f..9879cad 100644 --- a/vite-frontend/src/pages/config.tsx +++ b/vite-frontend/src/pages/config.tsx @@ -121,7 +121,7 @@ const getInitialConfigs = (): Record => { initialConfigs[key] = cachedValue; } }); - } catch (error) {} + } catch {} return initialConfigs; }; @@ -172,7 +172,7 @@ export default function ConfigPage() { setHasChanges(false); } else { } - } catch (error) { + } catch { // 只有在没有缓存数据时才显示错误 if (!hasInitialData) { toast.error("加载配置出错,请重试"); @@ -250,7 +250,7 @@ export default function ConfigPage() { } else { toast.error("保存配置失败: " + response.msg); } - } catch (error) { + } catch { toast.error("保存配置出错,请重试"); } finally { setSaving(false); diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index ca83a61..f66bab2 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -207,8 +207,7 @@ export default function DashboardPage() { } else { toast.error(res.msg || "获取套餐信息失败"); } - } catch (error) { - console.error("获取套餐信息失败:", error); + } catch { toast.error("获取套餐信息失败"); } finally { setLoading(false); @@ -648,7 +647,7 @@ export default function DashboardPage() { try { await navigator.clipboard.writeText(text); toast.success(`已复制`); - } catch (error) { + } catch { toast.error("复制失败"); } }; @@ -661,7 +660,7 @@ export default function DashboardPage() { ), ); await copyToClipboard(addressItem.address); - } catch (error) { + } catch { toast.error("复制失败"); } finally { setAddressList((prev) => @@ -1189,14 +1188,15 @@ export default function DashboardPage() { {forward.name}
- - hasMultipleIps(forward.inIp) && showAddressModal( forward.inIp, forward.inPort, @@ -1205,17 +1205,20 @@ export default function DashboardPage() { } > {formatInAddress(forward.inIp, forward.inPort)} - +
↓
- - hasMultipleRemoteAddresses( + disabled={ + !hasMultipleRemoteAddresses( forward.remoteAddr, - ) && + ) + } + title={formatRemoteAddress(forward.remoteAddr)} + type="button" + onClick={() => showRemoteAddressModal( forward.remoteAddr, "出口地址", @@ -1223,7 +1226,7 @@ export default function DashboardPage() { } > {formatRemoteAddress(forward.remoteAddr)} - +
diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index a6df3aa..4f3f846 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -224,8 +224,11 @@ export default function ForwardPage() { const [selectMode, setSelectMode] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false); - const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] = useState(false); - const [batchTargetTunnelId, setBatchTargetTunnelId] = useState(null); + const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] = + useState(false); + const [batchTargetTunnelId, setBatchTargetTunnelId] = useState( + null, + ); const [batchLoading, setBatchLoading] = useState(false); useEffect(() => { @@ -268,9 +271,7 @@ export default function ForwardPage() { // 同步到localStorage try { localStorage.setItem("forward-order", JSON.stringify(dbOrder)); - } catch (error) { - console.warn("无法保存排序到localStorage:", error); - } + } catch {} } else { // 使用本地存储的顺序 const savedOrder = localStorage.getItem("forward-order"); @@ -296,9 +297,7 @@ export default function ForwardPage() { } } } - } catch (error) { - console.warn("无法保存显示模式到localStorage:", error); - } + } catch {} }; // 加载所有数据 @@ -347,9 +346,7 @@ export default function ForwardPage() { // 同步到localStorage try { localStorage.setItem("forward-order", JSON.stringify(dbOrder)); - } catch (error) { - console.warn("无法保存排序到localStorage:", error); - } + } catch {} } else { // 使用本地存储的顺序 const savedOrder = localStorage.getItem("forward-order"); @@ -384,10 +381,8 @@ export default function ForwardPage() { if (tunnelsRes.code === 0) { setTunnels(tunnelsRes.data || []); } else { - console.warn("获取隧道列表失败:", tunnelsRes.msg); } - } catch (error) { - console.error("加载数据失败:", error); + } catch { toast.error("加载数据失败"); } finally { setLoading(false); @@ -566,8 +561,7 @@ export default function ForwardPage() { } } } - } catch (error) { - console.error("删除失败:", error); + } catch { toast.error("删除失败"); } finally { setDeleteLoading(false); @@ -628,8 +622,7 @@ export default function ForwardPage() { } else { toast.error(res.msg || "操作失败"); } - } catch (error) { - console.error("提交失败:", error); + } catch { toast.error("操作失败"); } finally { setSubmitLoading(false); @@ -679,14 +672,13 @@ export default function ForwardPage() { ); toast.error(res.msg || "操作失败"); } - } catch (error) { + } catch { // 操作失败,恢复UI状态 setForwards((prev) => prev.map((f) => f.id === forward.id ? { ...f, serviceRunning: !targetState } : f, ), ); - console.error("服务开关操作失败:", error); toast.error("网络错误,操作失败"); } }; @@ -720,8 +712,7 @@ export default function ForwardPage() { ], }); } - } catch (error) { - console.error("诊断失败:", error); + } catch { toast.error("网络错误,请重试"); setDiagnosisResult({ forwardName: forward.name, @@ -914,7 +905,7 @@ export default function ForwardPage() { try { await navigator.clipboard.writeText(text); toast.success(`已复制${label}`); - } catch (error) { + } catch { toast.error("复制失败"); } }; @@ -928,7 +919,7 @@ export default function ForwardPage() { ), ); await copyToClipboard(addressItem.address, "地址"); - } catch (error) { + } catch { toast.error("复制失败"); } finally { setAddressList((prev) => @@ -1001,8 +992,7 @@ export default function ForwardPage() { const exportText = exportLines.join("\n"); setExportData(exportText); - } catch (error) { - console.error("导出失败:", error); + } catch { toast.error("导出失败"); } finally { setExportLoading(false); @@ -1145,7 +1135,7 @@ export default function ForwardPage() { ...prev, ]); } - } catch (error) { + } catch { setImportResults((prev) => [ { line, @@ -1161,8 +1151,7 @@ export default function ForwardPage() { // 导入完成后刷新转发列表 await loadData(false); - } catch (error) { - console.error("导入失败:", error); + } catch { toast.error("导入过程中发生错误"); } finally { setImportLoading(false); @@ -1234,9 +1223,7 @@ export default function ForwardPage() { // 保存到localStorage try { localStorage.setItem("forward-order", JSON.stringify(newOrder)); - } catch (error) { - console.warn("无法保存排序到localStorage:", error); - } + } catch {} // 持久化到数据库 try { @@ -1267,8 +1254,7 @@ export default function ForwardPage() { } else { toast.error("保存排序失败:" + (response.msg || "未知错误")); } - } catch (error) { - console.error("保存排序到数据库失败:", error); + } catch { toast.error("保存排序失败,请重试"); } } @@ -1283,6 +1269,7 @@ export default function ForwardPage() { const toggleSelect = (id: number) => { const newSet = new Set(selectedIds); + if (newSet.has(id)) { newSet.delete(id); } else { @@ -1293,6 +1280,7 @@ export default function ForwardPage() { const selectAll = () => { const allIds = sortedForwards.map((f) => f.id); + setSelectedIds(new Set(allIds)); }; @@ -1305,12 +1293,16 @@ export default function ForwardPage() { setBatchLoading(true); try { const res = await batchDeleteForwards(Array.from(selectedIds)); + if (res.code === 0) { const result = res.data; + if (result.failCount === 0) { toast.success(`成功删除 ${result.successCount} 项`); } else { - toast.error(`成功 ${result.successCount} 项,失败 ${result.failCount} 项`); + toast.error( + `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, + ); } setSelectedIds(new Set()); setSelectMode(false); @@ -1334,8 +1326,10 @@ export default function ForwardPage() { const res = enable ? await batchResumeForwards(ids) : await batchPauseForwards(ids); + if (res.code === 0) { const result = res.data; + if (result.failCount === 0) { toast.success( enable @@ -1343,7 +1337,9 @@ export default function ForwardPage() { : `成功停用 ${result.successCount} 项`, ); } else { - toast.error(`成功 ${result.successCount} 项,失败 ${result.failCount} 项`); + toast.error( + `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, + ); } setSelectedIds(new Set()); setSelectMode(false); @@ -1363,12 +1359,16 @@ export default function ForwardPage() { setBatchLoading(true); try { const res = await batchRedeployForwards(Array.from(selectedIds)); + if (res.code === 0) { const result = res.data; + if (result.failCount === 0) { toast.success(`成功重新下发 ${result.successCount} 项`); } else { - toast.error(`成功 ${result.successCount} 项,失败 ${result.failCount} 项`); + toast.error( + `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, + ); } setSelectedIds(new Set()); setSelectMode(false); @@ -1391,12 +1391,16 @@ export default function ForwardPage() { forwardIds: Array.from(selectedIds), targetTunnelId: batchTargetTunnelId, }); + if (res.code === 0) { const result = res.data; + if (result.failCount === 0) { toast.success(`成功换隧道 ${result.successCount} 项`); } else { - toast.error(`成功 ${result.successCount} 项,失败 ${result.failCount} 项`); + toast.error( + `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, + ); } setSelectedIds(new Set()); setSelectMode(false); @@ -1501,11 +1505,6 @@ export default function ForwardPage() { // 可拖拽的转发卡片组件 const SortableForwardCard = ({ forward }: { forward: Forward }) => { - // 确保 forward 对象有效 - if (!forward || !forward.id) { - return null; - } - const { attributes, listeners, @@ -1543,9 +1542,9 @@ export default function ForwardPage() {
{selectMode && ( toggleSelect(forward.id)} - className="mr-2" /> )}
@@ -1560,12 +1559,12 @@ export default function ForwardPage() { {viewMode === "direct" && (
handleServiceToggle(forward)} /> @@ -1599,12 +1598,17 @@ export default function ForwardPage() {
{/* 地址信息 */}
-
showAddressModal(forward.inIp, forward.inPort, '入口端口')} + type="button" + onClick={() => + showAddressModal(forward.inIp, forward.inPort, "入口端口") + } >
@@ -1623,22 +1627,27 @@ export default function ForwardPage() { viewBox="0 0 24 24" > )}
-
+ -
showAddressModal(forward.remoteAddr, null, '目标地址')} + type="button" + onClick={() => + showAddressModal(forward.remoteAddr, null, "目标地址") + } >
@@ -1657,42 +1666,42 @@ export default function ForwardPage() { viewBox="0 0 24 24" > )}
-
+
{/* 统计信息 */}
{strategyDisplay.text}
↑{formatFlow(forward.inFlow || 0)}
↓{formatFlow(forward.outFlow || 0)} @@ -1705,7 +1714,11 @@ export default function ForwardPage() { color="primary" size="sm" startContent={ - + } @@ -1719,8 +1732,16 @@ export default function ForwardPage() { color="warning" size="sm" startContent={ - - + + } variant="flat" @@ -1733,9 +1754,21 @@ export default function ForwardPage() { color="danger" size="sm" startContent={ - - - + + + } variant="flat" @@ -1766,24 +1799,24 @@ export default function ForwardPage() {
{/* 页面头部 */}
-
+
{/* 显示模式切换按钮 */} {/* 导出按钮 */} -
@@ -1832,7 +1865,9 @@ export default function ForwardPage() { {selectMode && selectedIds.size > 0 && (
- 已选择 {selectedIds.size} 项 + + 已选择 {selectedIds.size} 项 + @@ -1840,43 +1875,43 @@ export default function ForwardPage() { 清空
@@ -1927,10 +1962,10 @@ export default function ForwardPage() {
用户 @@ -1943,6 +1978,7 @@ export default function ForwardPage() {
@@ -1954,10 +1990,10 @@ export default function ForwardPage() { viewBox="0 0 24 24" >
@@ -1969,9 +2005,9 @@ export default function ForwardPage() {
{ tunnelGroup.forwards.filter( @@ -1983,7 +2019,6 @@ export default function ForwardPage() {
} - className="shadow-none border border-divider" >
{tunnelGroup.forwards.map((forward) => @@ -2010,10 +2045,10 @@ export default function ForwardPage() { viewBox="0 0 24 24" >
@@ -2033,9 +2068,9 @@ export default function ForwardPage() { forwards.length > 0 ? ( {}} // 添加空的 onDragStart 处理器 + sensors={sensors} + onDragEnd={handleDragEnd} + onDragStart={() => {}} // 添加空的 onDragStart 处理器 >
@@ -2086,11 +2121,11 @@ export default function ForwardPage() { {/* 新增/编辑模态框 */} {(onClose) => ( @@ -2107,28 +2142,37 @@ export default function ForwardPage() {
setForm(prev => ({ ...prev, name: e.target.value }))} + isInvalid={!!errors.name} + label="转发名称" + placeholder="请输入转发名称" + value={form.name} + variant="bordered" + onChange={(e) => + setForm((prev) => ({ ...prev, name: e.target.value })) + } /> { - const value = e.target.value; - setForm(prev => ({ - ...prev, - inPort: value ? parseInt(value) : null - })); - }} + errorMessage={errors.inPort} + isInvalid={!!errors.inPort} + label="入口端口" + placeholder="留空则自动分配可用端口" + type="number" + value={form.inPort !== null ? form.inPort.toString() : ""} + variant="bordered" + onChange={(e) => { + const value = e.target.value; + + setForm((prev) => ({ + ...prev, + inPort: value ? parseInt(value) : null, + })); + }} />