From 370d572bf9b234c559492e160416fdf0e4c2e3da Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Wed, 22 Apr 2026 14:28:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=BF=E9=97=B4=E8=BF=9B=E5=85=A5=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=AE=A1=E7=90=86=EF=BC=8C=E4=B8=89=E7=BA=A7=E6=9D=83?= =?UTF-8?q?=E9=99=90=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/MissionScenarioController.java | 2 - .../controller/ObjectOperationLogController.java | 1 - .../ruoyi/web/controller/RoomAccessController.java | 231 +++++++++++++++ .../com/ruoyi/web/controller/RoomsController.java | 1 - .../web/controller/RouteWaypointsController.java | 34 ++- .../com/ruoyi/web/controller/RoutesController.java | 39 ++- .../ruoyi/web/service/RoomAccessRedisService.java | 165 +++++++++++ .../web/service/RoomRoutePermissionHelper.java | 116 ++++++++ .../controller/RoomWebSocketController.java | 100 ++++++- .../com/ruoyi/framework/config/RedisConfig.java | 16 + .../main/java/com/ruoyi/system/domain/Routes.java | 12 + .../com/ruoyi/system/mapper/SysUserMapper.java | 7 + .../com/ruoyi/system/service/ISysUserService.java | 5 + .../system/service/impl/SysUserServiceImpl.java | 10 + .../main/resources/mapper/system/RoutesMapper.xml | 5 +- .../main/resources/mapper/system/SysUserMapper.xml | 21 ++ ruoyi-ui/src/api/system/rooms.js | 76 ++++- ruoyi-ui/src/utils/websocket.js | 27 +- ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue | 29 +- ruoyi-ui/src/views/childRoom/RightPanel.vue | 78 +++++ ruoyi-ui/src/views/childRoom/TopHeader.vue | 19 ++ ruoyi-ui/src/views/childRoom/index.vue | 326 ++++++++++++++++++--- .../src/views/dialogs/RoomAccessManageDialog.vue | 309 +++++++++++++++++++ ruoyi-ui/src/views/selectRoom/index.vue | 22 +- sql/room_platform_icon_add_scale_xy.sql | 11 + sql/routes_add_creator_id.sql | 9 + 26 files changed, 1596 insertions(+), 75 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomAccessController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomAccessRedisService.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomRoutePermissionHelper.java create mode 100644 ruoyi-ui/src/views/dialogs/RoomAccessManageDialog.vue create mode 100644 sql/room_platform_icon_add_scale_xy.sql create mode 100644 sql/routes_add_creator_id.sql diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/MissionScenarioController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/MissionScenarioController.java index c627339..ca7e4c7 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/MissionScenarioController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/MissionScenarioController.java @@ -37,7 +37,6 @@ public class MissionScenarioController extends BaseController /** * 查询任务方案/沙箱列表 */ - @PreAuthorize("@ss.hasPermi('system:scenario:list')") @GetMapping("/list") public TableDataInfo list(MissionScenario missionScenario) { @@ -62,7 +61,6 @@ public class MissionScenarioController extends BaseController /** * 获取任务方案/沙箱详细信息 */ - @PreAuthorize("@ss.hasPermi('system:scenario:query')") @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Long id) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java index e6f7d6d..d158c12 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java @@ -29,7 +29,6 @@ public class ObjectOperationLogController extends BaseController { /** * 分页查询对象级操作日志(按房间) */ - @PreAuthorize("@ss.hasPermi('system:routes:list')") @GetMapping("/list") public TableDataInfo list( @RequestParam(required = false) Long roomId, diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomAccessController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomAccessController.java new file mode 100644 index 0000000..7518590 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomAccessController.java @@ -0,0 +1,231 @@ +package com.ruoyi.web.controller; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.system.domain.Rooms; +import com.ruoyi.system.service.IRoomsService; +import com.ruoyi.system.service.ISysUserService; +import com.ruoyi.web.service.RoomAccessRedisService; + +/** + * Room access management (Redis-backed). + * + * Role hierarchy: + * Admin (userLevel=1 or system admin userId=1) - can enter any room, manage hosts per room + * Host (userLevel=2, assigned to specific rooms via Redis) - can enter assigned rooms, manage normal users for assigned rooms + * Normal (userLevel=3) - can only enter rooms they are granted access to + */ +@RestController +@RequestMapping("/system/room-access") +public class RoomAccessController extends BaseController { + + @Autowired + private RoomAccessRedisService roomAccessRedisService; + + @Autowired + private ISysUserService userService; + + @Autowired + private IRoomsService roomsService; + + private boolean isAdminMode(LoginUser loginUser) { + return roomAccessRedisService.isAdmin(loginUser); + } + + private boolean isHostForRoom(LoginUser loginUser, Long roomId) { + Long uid = loginUser.getUserId(); + return roomAccessRedisService.isHost(uid, roomId); + } + + private List parseIdList(Object raw) { + List out = new ArrayList<>(); + if (!(raw instanceof List)) { + return out; + } + for (Object o : (List) raw) { + if (o == null) continue; + try { + out.add(Long.parseLong(String.valueOf(o))); + } catch (NumberFormatException ignored) {} + } + return out; + } + + /** Check if current user can enter a room. */ + @GetMapping("/check") + public AjaxResult check(@RequestParam Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long userId = loginUser.getUserId(); + boolean ok = roomAccessRedisService.canEnterRoom(userId, roomId, loginUser); + Map data = new HashMap<>(); + data.put("allowed", ok); + return success(data); + } + + /** Check if current user is host for a specific room. */ + @GetMapping("/isHost") + public AjaxResult isHost(@RequestParam Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + boolean host = isHostForRoom(loginUser, roomId); + Map data = new HashMap<>(); + data.put("isHost", host); + return success(data); + } + + /** Context for the permission management dialog. */ + @GetMapping("/manageContext") + public AjaxResult manageContext(@RequestParam Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + boolean adminMode = isAdminMode(loginUser); + boolean hostMode = !adminMode && isHostForRoom(loginUser, roomId); + if (!adminMode && !hostMode) { + throw new ServiceException("No permission to manage room access"); + } + Map data = new HashMap<>(); + data.put("adminMode", adminMode); + data.put("hostMode", hostMode); + data.put("userIds", roomAccessRedisService.listAllowedUserIds(roomId)); + data.put("hostUserIds", roomAccessRedisService.listHostUserIds(roomId)); + return success(data); + } + + /** All rooms list (for admin to pick target rooms when assigning hosts). */ + @GetMapping("/allRooms") + public AjaxResult allRooms() { + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (!isAdminMode(loginUser)) { + throw new ServiceException("Only admins can list all rooms"); + } + List list = roomsService.selectRoomsList(new Rooms()); + List> result = new ArrayList<>(); + for (Rooms r : list) { + if (r == null || r.getId() == null) continue; + Map m = new HashMap<>(); + m.put("id", r.getId()); + m.put("name", r.getName()); + m.put("parentId", r.getParentId()); + result.add(m); + } + return success(result); + } + + /** User candidates (paginated): profile=host -> userLevel=2, profile=normal -> userLevel=3/empty */ + @GetMapping("/userCandidates") + public TableDataInfo userCandidates( + @RequestParam Long roomId, + @RequestParam String profile, + @RequestParam(required = false) String userName) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + if ("host".equals(profile) && !isAdminMode(loginUser)) { + throw new ServiceException("Only admins can query host list"); + } + if ("normal".equals(profile) && !isAdminMode(loginUser) && !isHostForRoom(loginUser, roomId)) { + throw new ServiceException("No permission to query user list"); + } + SysUser q = new SysUser(); + if (userName != null && !userName.isEmpty()) { + q.setUserName(userName.trim()); + } + startPage(); + List list = userService.selectUsersForRoomAccess(q, profile); + return getDataTable(list); + } + + /** + * Admin: assign hosts to specific rooms. + * Body: { userIds: [...], roomIds: [...] } + * Each user in userIds gets host status + room access for each room in roomIds. + */ + @PostMapping("/setHostBatch") + public AjaxResult setHostBatch(@RequestBody Map body) { + LoginUser op = SecurityUtils.getLoginUser(); + if (!isAdminMode(op)) { + return error("Only admins can assign hosts"); + } + List userIds = parseIdList(body.get("userIds")); + List roomIds = parseIdList(body.get("roomIds")); + if (userIds.isEmpty()) { + return error("userIds is required"); + } + // If roomIds not provided, try single roomId for backward compatibility + if (roomIds.isEmpty()) { + Object rid = body.get("roomId"); + if (rid != null) { + roomIds.add(Long.parseLong(String.valueOf(rid))); + } + } + if (roomIds.isEmpty()) { + return error("roomIds is required"); + } + int ok = 0; + for (Long uid : userIds) { + for (Long rid : roomIds) { + try { + roomAccessRedisService.setHost(rid, uid, op); + ok++; + } catch (Exception e) { + logger.warn("setHostBatch skip userId={} roomId={} : {}", uid, rid, e.getMessage()); + } + } + } + return success("Done: " + ok + " / " + (userIds.size() * roomIds.size())); + } + + /** + * Admin or Host: grant normal users access to specific rooms. + * Body: { userIds: [...], roomId: N } + * Host can only grant for rooms they are host of. + */ + @PostMapping("/grantBatch") + public AjaxResult grantBatch(@RequestBody Map body) { + Long roomId = body.get("roomId") == null ? null : Long.parseLong(String.valueOf(body.get("roomId"))); + List userIds = parseIdList(body.get("userIds")); + if (roomId == null || userIds.isEmpty()) { + return error("roomId and userIds are required"); + } + LoginUser op = SecurityUtils.getLoginUser(); + if (!isAdminMode(op) && !isHostForRoom(op, roomId)) { + return error("No permission to grant access for this room"); + } + int ok = 0; + for (Long uid : userIds) { + try { + roomAccessRedisService.grantUser(roomId, uid, op); + ok++; + } catch (Exception e) { + logger.warn("grantBatch skip userId={} : {}", uid, e.getMessage()); + } + } + return success("Done: " + ok + " / " + userIds.size()); + } + + @PostMapping("/revoke") + public AjaxResult revoke(@RequestBody Map body) { + Long roomId = body.get("roomId"); + Long userId = body.get("userId"); + try { + roomAccessRedisService.revokeUser(roomId, userId, SecurityUtils.getLoginUser()); + return success(); + } catch (SecurityException e) { + return error(e.getMessage()); + } catch (Exception e) { + return error(e.getMessage()); + } + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java index 8108a8e..4c12ce5 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java @@ -62,7 +62,6 @@ public class RoomsController extends BaseController /** * 获取项目与任务房间详细信息 */ - @PreAuthorize("@ss.hasPermi('system:rooms:query')") @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Long id) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java index f6d4d8f..41b2b94 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java @@ -21,6 +21,9 @@ import com.ruoyi.system.domain.ObjectOperationLog; import com.ruoyi.system.domain.RouteWaypoints; import com.ruoyi.system.service.IObjectOperationLogService; import com.ruoyi.system.service.IRouteWaypointsService; +import com.ruoyi.web.service.RoomRoutePermissionHelper; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; import org.springframework.web.bind.annotation.RequestParam; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; @@ -41,10 +44,12 @@ public class RouteWaypointsController extends BaseController @Autowired private IObjectOperationLogService objectOperationLogService; + @Autowired + private RoomRoutePermissionHelper roomRoutePermissionHelper; + /** * 查询航线具体航点明细列表 */ - @PreAuthorize("@ss.hasPermi('system:routes:list')") @GetMapping("/list") public TableDataInfo list(RouteWaypoints routeWaypoints) { @@ -69,7 +74,6 @@ public class RouteWaypointsController extends BaseController /** * 获取航线具体航点明细详细信息 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Long id) { @@ -85,6 +89,14 @@ public class RouteWaypointsController extends BaseController @PostMapping public AjaxResult add(@RequestBody RouteWaypoints routeWaypoints, @RequestParam(required = false) Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long rid = roomId; + if (rid == null && routeWaypoints.getRouteId() != null) { + rid = roomRoutePermissionHelper.resolveRoomIdByRouteId(routeWaypoints.getRouteId()); + } + if (rid != null && !roomRoutePermissionHelper.canEditRoutesInRoom(rid, loginUser)) { + return AjaxResult.error("暂无权限修改航点"); + } int rows = routeWaypointsService.insertRouteWaypoints(routeWaypoints); if (rows > 0) { try { @@ -115,6 +127,14 @@ public class RouteWaypointsController extends BaseController @PutMapping public AjaxResult edit(@RequestBody RouteWaypoints routeWaypoints, @RequestParam(required = false) Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long rid = roomId; + if (rid == null && routeWaypoints.getRouteId() != null) { + rid = roomRoutePermissionHelper.resolveRoomIdByRouteId(routeWaypoints.getRouteId()); + } + if (rid != null && !roomRoutePermissionHelper.canEditRoutesInRoom(rid, loginUser)) { + return AjaxResult.error("暂无权限修改航点"); + } RouteWaypoints before = routeWaypoints.getId() != null ? routeWaypointsService.selectRouteWaypointsById(routeWaypoints.getId()) : null; int rows = routeWaypointsService.updateRouteWaypoints(routeWaypoints); if (rows > 0 && before != null) { @@ -147,6 +167,16 @@ public class RouteWaypointsController extends BaseController @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable Long[] ids, @RequestParam(required = false) Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + for (Long id : ids) { + RouteWaypoints wp = routeWaypointsService.selectRouteWaypointsById(id); + if (wp != null && wp.getRouteId() != null) { + Long rid = roomId != null ? roomId : roomRoutePermissionHelper.resolveRoomIdByRouteId(wp.getRouteId()); + if (rid != null && !roomRoutePermissionHelper.canEditRoutesInRoom(rid, loginUser)) { + return AjaxResult.error("暂无权限删除航点"); + } + } + } for (Long id : ids) { RouteWaypoints before = routeWaypointsService.selectRouteWaypointsById(id); if (before != null) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java index 061fbd0..290a144 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java @@ -23,6 +23,9 @@ import com.ruoyi.system.domain.Routes; import com.ruoyi.system.service.IObjectOperationLogService; import com.ruoyi.system.service.IRoomPlatformIconService; import com.ruoyi.system.service.IRoutesService; +import com.ruoyi.web.service.RoomRoutePermissionHelper; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.system.domain.dto.PlatformStyleDTO; @@ -58,6 +61,9 @@ public class RoutesController extends BaseController private RedisTemplate redisTemplate; @Autowired + private RoomRoutePermissionHelper roomRoutePermissionHelper; + + @Autowired @Qualifier("fourTRedisTemplate") private RedisTemplate fourTRedisTemplate; @@ -148,7 +154,6 @@ public class RoutesController extends BaseController * 从 Redis 获取平台样式 * 独立平台(routeId=0):优先用 platformIconInstanceId 取该实例样式;未传时用 platformId(兼容旧数据)。 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping("/getPlatformStyle") public AjaxResult getPlatformStyle(PlatformStyleDTO dto) { @@ -242,7 +247,6 @@ public class RoutesController extends BaseController /** * 从 Redis 获取4T数据 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping("/get4TData") public AjaxResult get4TData(Long roomId) { @@ -265,7 +269,6 @@ public class RoutesController extends BaseController * 从 Redis 获取「截图展示」数据(GET + roomId 查询参数) * 与保存共用路径前缀、方法不同,避免与其它 GET 字面路径、网关规则混淆 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping("/roomScreenshotGallery") public AjaxResult getScreenshotGalleryData(@RequestParam Long roomId) { @@ -321,7 +324,6 @@ public class RoutesController extends BaseController /** * 从 Redis 获取六步法任务页数据 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping("/getTaskPageData") public AjaxResult getTaskPageData(Long roomId) { @@ -360,7 +362,6 @@ public class RoutesController extends BaseController /** * 从 Redis 获取六步法全部数据 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping("/getSixStepsData") public AjaxResult getSixStepsData(Long roomId) { @@ -382,7 +383,6 @@ public class RoutesController extends BaseController /** * 获取导弹发射参数列表(Redis,房间+航线+平台为 key,值为数组,每项含 angle/distance/launchTimeMinutesFromK/startLng/startLat/platformHeadingDeg) */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping("/missile-params") public AjaxResult getMissileParams(Long roomId, Long routeId, Long platformId) { @@ -481,7 +481,6 @@ public class RoutesController extends BaseController /** * 查询实体部署与航线列表 */ - @PreAuthorize("@ss.hasPermi('system:routes:list')") @GetMapping("/list") public TableDataInfo list(Routes routes) { @@ -507,7 +506,6 @@ public class RoutesController extends BaseController * 获取实体部署与航线详细信息 * 路径仅匹配数字 id,避免与 /get4TData、/getScreenshotGalleryData 等字面路径冲突 */ - @PreAuthorize("@ss.hasPermi('system:routes:query')") @GetMapping(value = "/{id:\\d+}") public AjaxResult getInfo(@PathVariable("id") Long id) { @@ -523,7 +521,15 @@ public class RoutesController extends BaseController @PostMapping public AjaxResult add(@RequestBody Routes routes, @RequestParam(required = false) Long roomId) { - // 1. 执行插入,MyBatis 会通过 useGeneratedKeys="true" 自动将新 ID 注入 routes 对象 + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long rid = roomId; + if (rid == null && routes.getScenarioId() != null) { + rid = roomRoutePermissionHelper.resolveRoomIdByScenarioId(routes.getScenarioId()); + } + if (rid != null && !roomRoutePermissionHelper.canEditRoutesInRoom(rid, loginUser)) { + return AjaxResult.error("暂无权限在该房间创建航线"); + } + routes.setCreatorId(loginUser.getUserId()); int rows = routesService.insertRoutes(routes); if (rows > 0) { try { @@ -554,6 +560,14 @@ public class RoutesController extends BaseController @PutMapping public AjaxResult edit(@RequestBody Routes routes, @RequestParam(required = false) Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long rid = roomId; + if (rid == null && routes.getId() != null) { + rid = roomRoutePermissionHelper.resolveRoomIdByRouteId(routes.getId()); + } + if (!roomRoutePermissionHelper.canEditRoute(routes.getId(), rid, loginUser)) { + return AjaxResult.error("暂无权限修改该航线(仅管理员或航线创建者可修改)"); + } Routes before = null; if (routes.getId() != null) { before = routesService.selectRoutesById(routes.getId()); @@ -591,6 +605,13 @@ public class RoutesController extends BaseController @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable Long[] ids, @RequestParam(required = false) Long roomId) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + for (Long id : ids) { + Long rid = roomId != null ? roomId : roomRoutePermissionHelper.resolveRoomIdByRouteId(id); + if (!roomRoutePermissionHelper.canEditRoute(id, rid, loginUser)) { + return AjaxResult.error("暂无权限删除该航线(仅管理员或航线创建者可删除)"); + } + } for (Long id : ids) { Routes before = routesService.selectRoutesById(id); if (before != null) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomAccessRedisService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomAccessRedisService.java new file mode 100644 index 0000000..cab3fd6 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomAccessRedisService.java @@ -0,0 +1,165 @@ +package com.ruoyi.web.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import javax.annotation.Resource; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * Room access control via Redis (pure string serialization). + */ +@Service +public class RoomAccessRedisService { + + private static final String KEY_HOSTS = "room:hosts:"; + private static final String KEY_ROOM_USERS = "room:allowedUsers:"; + private static final String KEY_USER_ROOMS = "user:"; + private static final String KEY_USER_ROOMS_SUFFIX = ":allowedRooms"; + + @Resource(name = "roomAccessRedisTemplate") + private RedisTemplate redisTemplate; + + private String hostsKey(Long roomId) { + return KEY_HOSTS + roomId; + } + + private String roomUsersKey(Long roomId) { + return KEY_ROOM_USERS + roomId; + } + + private String userRoomsKey(Long userId) { + return KEY_USER_ROOMS + userId + KEY_USER_ROOMS_SUFFIX; + } + + public boolean isRoomAccessConfigured(Long roomId) { + if (roomId == null) { + return false; + } + Long n = redisTemplate.opsForSet().size(roomUsersKey(roomId)); + return n != null && n.longValue() > 0; + } + + /** + * Admin = userId==1 OR userLevel=='1' OR has 'admin' role. + */ + public boolean isAdmin(com.ruoyi.common.core.domain.model.LoginUser user) { + if (user == null) return false; + if (user.getUserId() != null && user.getUserId() == 1L) return true; + if (user.getUser() != null && "1".equals(user.getUser().getUserLevel())) return true; + try { + if (SecurityUtils.hasRole("admin")) return true; + } catch (Exception ignored) {} + return false; + } + + /** + * Admin always allowed. Others must be in the Redis allowed-user set. + */ + public boolean canEnterRoom(Long userId, Long roomId, com.ruoyi.common.core.domain.model.LoginUser loginUser) { + if (roomId == null || userId == null) { + return false; + } + if (isAdmin(loginUser)) { + return true; + } + Boolean in = redisTemplate.opsForSet().isMember(roomUsersKey(roomId), userId.toString()); + return Boolean.TRUE.equals(in); + } + + public boolean isHost(Long userId, Long roomId) { + if (userId == null || roomId == null) { + return false; + } + Boolean h = redisTemplate.opsForSet().isMember(hostsKey(roomId), userId.toString()); + return Boolean.TRUE.equals(h); + } + + /** Admin sets a user as host for a room (also adds to allowed set). */ + public void setHost(Long roomId, Long hostUserId, com.ruoyi.common.core.domain.model.LoginUser operator) { + if (roomId == null || hostUserId == null || operator == null) { + throw new IllegalArgumentException("Missing required parameters"); + } + if (!isAdmin(operator)) { + throw new SecurityException("Only admins can assign hosts"); + } + String uid = hostUserId.toString(); + String rid = roomId.toString(); + redisTemplate.opsForSet().add(hostsKey(roomId), uid); + redisTemplate.opsForSet().add(roomUsersKey(roomId), uid); + redisTemplate.opsForSet().add(userRoomsKey(hostUserId), rid); + } + + /** Admin or host grants a user access to a room. */ + public void grantUser(Long roomId, Long targetUserId, com.ruoyi.common.core.domain.model.LoginUser operator) { + if (roomId == null || targetUserId == null || operator == null) { + throw new IllegalArgumentException("Missing required parameters"); + } + Long opId = operator.getUserId(); + boolean ok = isAdmin(operator) || isHost(opId, roomId); + if (!ok) { + throw new SecurityException("No permission to grant access"); + } + String uid = targetUserId.toString(); + String rid = roomId.toString(); + redisTemplate.opsForSet().add(roomUsersKey(roomId), uid); + redisTemplate.opsForSet().add(userRoomsKey(targetUserId), rid); + } + + public void revokeUser(Long roomId, Long targetUserId, com.ruoyi.common.core.domain.model.LoginUser operator) { + if (roomId == null || targetUserId == null || operator == null) { + throw new IllegalArgumentException("Missing required parameters"); + } + Long opId = operator.getUserId(); + boolean ok = isAdmin(operator) || isHost(opId, roomId); + if (!ok) { + throw new SecurityException("No permission to revoke access"); + } + String uid = targetUserId.toString(); + String rid = roomId.toString(); + redisTemplate.opsForSet().remove(roomUsersKey(roomId), uid); + redisTemplate.opsForSet().remove(userRoomsKey(targetUserId), rid); + redisTemplate.opsForSet().remove(hostsKey(roomId), uid); + } + + public List listAllowedUserIds(Long roomId) { + if (roomId == null) { + return Collections.emptyList(); + } + Set members = redisTemplate.opsForSet().members(roomUsersKey(roomId)); + if (members == null || members.isEmpty()) { + return Collections.emptyList(); + } + List out = new ArrayList<>(); + for (String s : members) { + try { + out.add(Long.parseLong(s)); + } catch (NumberFormatException ignored) { + // skip + } + } + return out; + } + + public List listHostUserIds(Long roomId) { + if (roomId == null) { + return Collections.emptyList(); + } + Set members = redisTemplate.opsForSet().members(hostsKey(roomId)); + if (members == null || members.isEmpty()) { + return Collections.emptyList(); + } + List out = new ArrayList<>(); + for (String s : members) { + try { + out.add(Long.parseLong(s)); + } catch (NumberFormatException ignored) { + // skip + } + } + return out; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomRoutePermissionHelper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomRoutePermissionHelper.java new file mode 100644 index 0000000..f63e07b --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/service/RoomRoutePermissionHelper.java @@ -0,0 +1,116 @@ +package com.ruoyi.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.system.domain.MissionScenario; +import com.ruoyi.system.domain.Rooms; +import com.ruoyi.system.domain.Routes; +import com.ruoyi.system.service.IMissionScenarioService; +import com.ruoyi.system.service.IRoomsService; +import com.ruoyi.system.service.IRoutesService; + +/** + * Route edit permission: + * Admin (userLevel=1 / system admin) -> always allowed + * Host of the room (Redis room:hosts:{roomId}) -> allowed + * Route creator (routes.creator_id) -> allowed + * Others -> denied + * + * In parent rooms (parentId == null), only admin can edit. + */ +@Component +public class RoomRoutePermissionHelper { + + @Autowired + private IRoomsService roomsService; + + @Autowired + private IRoutesService routesService; + + @Autowired + private IMissionScenarioService missionScenarioService; + + @Autowired + private RoomAccessRedisService roomAccessService; + + public Long resolveRoomIdByScenarioId(Long scenarioId) { + if (scenarioId == null) { + return null; + } + MissionScenario sc = missionScenarioService.selectMissionScenarioById(scenarioId); + return sc != null ? sc.getRoomId() : null; + } + + public Long resolveRoomIdByRouteId(Long routeId) { + if (routeId == null) { + return null; + } + Routes r = routesService.selectRoutesById(routeId); + if (r == null || r.getScenarioId() == null) { + return null; + } + MissionScenario sc = missionScenarioService.selectMissionScenarioById(r.getScenarioId()); + return sc != null ? sc.getRoomId() : null; + } + + private boolean isAdminUser(LoginUser loginUser) { + return roomAccessService.isAdmin(loginUser); + } + + /** + * Can the user create routes in this room? + * Admin -> yes; Host of this room -> yes; Parent room -> only admin; Others in child room -> yes (they create, they own) + */ + public boolean canEditRoutesInRoom(Long roomId, LoginUser loginUser) { + if (roomId == null || loginUser == null) { + return false; + } + if (isAdminUser(loginUser)) { + return true; + } + Rooms room = roomsService.selectRoomsById(roomId); + if (room == null) { + return false; + } + if (room.getParentId() == null) { + return false; + } + return true; + } + + /** + * Can the user edit/delete a specific route? + * Admin -> always yes + * Parent room -> only admin + * Host of this room -> yes + * Route creator -> yes + * Others -> no + */ + public boolean canEditRoute(Long routeId, Long roomId, LoginUser loginUser) { + if (loginUser == null) { + return false; + } + Long uid = loginUser.getUserId(); + if (isAdminUser(loginUser)) { + return true; + } + Rooms room = null; + if (roomId != null) { + room = roomsService.selectRoomsById(roomId); + } + if (room != null && room.getParentId() == null) { + return false; + } + if (roomId != null && roomAccessService.isHost(uid, roomId)) { + return true; + } + if (routeId != null) { + Routes route = routesService.selectRoutesById(routeId); + if (route != null && route.getCreatorId() != null && route.getCreatorId().equals(uid)) { + return true; + } + } + return false; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java index 76dcd4a..42711a6 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java @@ -2,8 +2,10 @@ package com.ruoyi.websocket.controller; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -78,6 +80,8 @@ public class RoomWebSocketController { private static final String TYPE_OBJECT_EDIT_UNLOCK = "OBJECT_EDIT_UNLOCK"; /** 地图选中状态同步(供「当前操作」展示) */ private static final String TYPE_USER_SELECTION = "USER_SELECTION"; + /** 六步法开关:大房间下发时同步到所有子房间 topic */ + private static final String TYPE_SYNC_SIX_STEPS = "SYNC_SIX_STEPS"; /** * 处理房间消息:JOIN、LEAVE、PING、CHAT、PRIVATE_CHAT、SYNC_* @@ -113,6 +117,8 @@ public class RoomWebSocketController { handlePrivateChatHistoryRequest(roomId, loginUser, body); } else if (TYPE_SYNC_ROUTE_VISIBILITY.equals(type)) { handleSyncRouteVisibility(roomId, body, sessionId); + } else if (TYPE_SYNC_SIX_STEPS.equals(type)) { + handleSyncSixSteps(roomId, body, sessionId); } else if (TYPE_SYNC_WAYPOINTS.equals(type)) { handleSyncWaypoints(roomId, body, sessionId); } else if (TYPE_SYNC_PLATFORM_ICONS.equals(type)) { @@ -362,9 +368,99 @@ public class RoomWebSocketController { messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp); } - /** 航线显隐变更:小房间内每人展示各自内容,不再广播和持久化 */ + /** + * 航线显隐:在同一父房间下的所有子房间 + 父房间 topic 之间广播(仅展示同步,不涉及库表)。 + */ private void handleSyncRouteVisibility(Long roomId, Map body, String sessionId) { - // 小房间内每人展示各自内容,航线显隐不再同步给他人、不再持久化 + if (body == null || !body.containsKey("routeId")) { + return; + } + Rooms room = roomsService.selectRoomsById(roomId); + if (room == null) { + return; + } + Map msg = new HashMap<>(); + msg.put("type", TYPE_SYNC_ROUTE_VISIBILITY); + msg.put("routeId", body.get("routeId")); + msg.put("visible", body.get("visible") != null && Boolean.TRUE.equals(body.get("visible"))); + if (body.containsKey("styleRoomId")) { + msg.put("styleRoomId", body.get("styleRoomId")); + } + msg.put("senderSessionId", sessionId); + + Set targets = new HashSet<>(); + targets.add(roomId); + if (room.getParentId() != null) { + targets.add(room.getParentId()); + Rooms query = new Rooms(); + query.setParentId(room.getParentId()); + List siblings = roomsService.selectRoomsList(query); + if (siblings != null) { + for (Rooms s : siblings) { + if (s != null && s.getId() != null) { + targets.add(s.getId()); + } + } + } + } else { + Rooms query = new Rooms(); + query.setParentId(roomId); + List children = roomsService.selectRoomsList(query); + if (children != null) { + for (Rooms c : children) { + if (c != null && c.getId() != null) { + targets.add(c.getId()); + } + } + } + } + for (Long tid : targets) { + messagingTemplate.convertAndSend("/topic/room/" + tid, msg); + } + } + + /** 解析前端 JSON 中的 boolean(兼容 true/false、1/0、字符串) */ + private static boolean parseBooleanOpen(Map body) { + if (body == null || !body.containsKey("open")) { + return false; + } + Object o = body.get("open"); + if (o instanceof Boolean) { + return (Boolean) o; + } + if (o instanceof Number) { + return ((Number) o).intValue() != 0; + } + String s = String.valueOf(o); + return "true".equalsIgnoreCase(s) || "1".equals(s); + } + + /** + * 六步法:仅当发送端在大房间(parent_id 为空)时,除本房间外再广播到全部子房间。 + */ + private void handleSyncSixSteps(Long roomId, Map body, String sessionId) { + Rooms room = roomsService.selectRoomsById(roomId); + if (room == null) { + return; + } + boolean open = parseBooleanOpen(body); + Map msg = new HashMap<>(); + msg.put("type", TYPE_SYNC_SIX_STEPS); + msg.put("open", open); + msg.put("senderSessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + if (room.getParentId() == null) { + Rooms query = new Rooms(); + query.setParentId(roomId); + List children = roomsService.selectRoomsList(query); + if (children != null) { + for (Rooms c : children) { + if (c != null && c.getId() != null) { + messagingTemplate.convertAndSend("/topic/room/" + c.getId(), msg); + } + } + } + } } /** 广播航点变更,供其他设备实时同步 */ diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java index 8a372c3..74bbfe7 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java @@ -67,6 +67,22 @@ public class RedisConfig extends CachingConfigurerSupport * 纯字符串 RedisTemplate,用于存储 4T 等 JSON 字符串,避免 FastJson 序列化问题 * 命名为 fourTRedisTemplate 避免与 Spring Boot 自带的 stringRedisTemplate 冲突 */ + /** + * Room access RedisTemplate: pure string key+value, no FastJson wrapping. + */ + @Bean("roomAccessRedisTemplate") + public RedisTemplate roomAccessRedisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new StringRedisSerializer()); + template.afterPropertiesSet(); + return template; + } + @Bean("fourTRedisTemplate") public RedisTemplate fourTRedisTemplate(RedisConnectionFactory connectionFactory) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java index e8188ef..abb13a9 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java @@ -45,6 +45,9 @@ public class Routes extends BaseEntity { @Excel(name = "实例属性JSON: 覆盖库里的默认值 (如当前初始油量, 挂载配置)") private String attributes; + /** 航线创建者用户ID */ + private Long creatorId; + private List waypoints; /** 方案ID列表(查询用,非持久化):传入时查询 scenario_id IN (...) */ @@ -96,6 +99,14 @@ public class Routes extends BaseEntity { return attributes; } + public Long getCreatorId() { + return creatorId; + } + + public void setCreatorId(Long creatorId) { + this.creatorId = creatorId; + } + public List getWaypoints() { return waypoints; } @@ -148,6 +159,7 @@ public class Routes extends BaseEntity { .append("platformId", getPlatformId()) .append("callSign", getCallSign()) .append("attributes", getAttributes()) + .append("creatorId", getCreatorId()) .append("waypoints", getWaypoints()) .toString(); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java index 543c59c..7bf8f76 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -144,4 +144,11 @@ public interface SysUserMapper * @return 结果 */ public SysUser checkEmailUnique(String email); + + /** + * 房间准入管理:按账号等级筛选用户(不走数据权限切面,仅由房间准入接口鉴权后调用) + * + * @param user 查询条件(userName 模糊;params.roomAccessProfile = host | normal) + */ + public List selectUsersForRoomAccess(SysUser user); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java index 067245b..b569858 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -20,6 +20,11 @@ public interface ISysUserService public List selectUserList(SysUser user); /** + * 房间准入管理:筛选主持人(2)或普通用户(3/空),无数据权限过滤 + */ + public List selectUsersForRoomAccess(SysUser user, String profile); + + /** * 根据条件分页查询已分配用户角色列表 * * @param user 用户信息 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java index 3f7c189..f96c31b 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -77,6 +77,16 @@ public class SysUserServiceImpl implements ISysUserService } /** + * 房间准入:仅按等级与关键字筛选,不应用数据权限(由房间准入 Controller 鉴权) + */ + @Override + public List selectUsersForRoomAccess(SysUser user, String profile) + { + user.getParams().put("roomAccessProfile", profile); + return userMapper.selectUsersForRoomAccess(user); + } + + /** * 根据条件分页查询已分配用户角色列表 * * @param user 用户信息 diff --git a/ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml index 8cc066c..280f2fc 100644 --- a/ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml @@ -10,11 +10,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + - select id, scenario_id, platform_id, call_sign, attributes from routes + select id, scenario_id, platform_id, call_sign, attributes, creator_id from routes + +