# 资源中心协作权限设计 ## 一、数据库变更 ```sql -- 扩展 app_resource 表,添加协作权限相关字段 ALTER TABLE app_resource ADD COLUMN owner_user_id BIGINT NULL COMMENT '资源创建者 userId(创建时自动设置)' AFTER user_id, ADD COLUMN access_level TINYINT DEFAULT 1 COMMENT '默认协作者访问级别: 1=基础查看 2=连接查看 3=完全权限' AFTER owner_user_id; -- 数据修复:将现有数据的 owner_user_id 设置为 user_id UPDATE app_resource SET owner_user_id = user_id WHERE owner_user_id IS NULL; -- 索引 CREATE INDEX idx_resource_owner ON app_resource(owner_user_id); ``` ## 二、权限级别定义 | 级别 | 名称 | 可见信息 | 适用角色 | |-----|------|---------|---------| | 0 | 无权限 | 无 | 非团队成员 | | 1 | 基础查看 | 名称、IP、端口、状态 | 所有团队成员 | | 2 | 连接查看 | + 用户名、Host、连接方式 | 技术负责人 | | 3 | 完全权限 | + 密码、私钥、编辑删除 | 资源创建者 (Owner) | ## 三、后端实现要点 ### 3.1 创建资源时自动设置 owner_user_id ```java // AppResourceServiceImpl.java @Override public void save(AppResource resource) { // 创建时自动设置所有者 Long currentUserId = StpUtil.getLoginIdAsLong(); if (resource.getOwnerUserId() == null) { resource.setOwnerUserId(currentUserId); } super.save(resource); } ``` ### 3.2 查询时计算 accessLevel 并过滤敏感字段 ```java // AppResourceController.java - 查询列表/分页时处理权限 private void enrichWithPermission(List resources) { Long currentUserId = StpUtil.getLoginIdAsLong(); for (AppResource resource : resources) { // 计算访问级别 ResourceAccessLevel level = checkAccessLevel(currentUserId, resource); resource.setAccessLevel(level.getValue()); resource.setOwner(currentUserId.equals(resource.getOwnerUserId())); // 过滤敏感字段 if (level.getValue() < 3) { resource.setSshPassword(hasContent(resource.getSshPassword()) ? "******" : null); resource.setAdminPassword(hasContent(resource.getAdminPassword()) ? "******" : null); resource.setDbPassword(hasContent(resource.getDbPassword()) ? "******" : null); resource.setPrivateKey(null); // 私钥完全不返回给非Owner } if (level.getValue() < 2) { resource.setSshUsername(null); resource.setDbUsername(null); } } } private ResourceAccessLevel checkAccessLevel(Long userId, AppResource resource) { // Owner 拥有完全权限 if (userId.equals(resource.getOwnerUserId())) { return ResourceAccessLevel.FULL; } // 检查应用团队权限(通过 app_invite 表) AppInvite invite = appInviteService.getByAppAndUser(resource.getAppId(), userId); if (invite == null) { return ResourceAccessLevel.NONE; } // 根据角色返回权限级别 String role = invite.getRole(); if ("technical_lead".equals(role) || "admin".equals(role)) { return ResourceAccessLevel.VIEW_CONNECTION; } return ResourceAccessLevel.VIEW_BASIC; } ``` ### 3.3 accessLevel 枚举 ```java public enum ResourceAccessLevel { NONE(0), VIEW_BASIC(1), VIEW_CONNECTION(2), FULL(3); private final int value; ResourceAccessLevel(int value) { this.value = value; } public int getValue() { return value; } } ``` ### 3.4 修改/删除时权限检查 ```java @PutMapping public ApiResult update(@RequestBody AppResource resource) { AppResource existing = service.getById(resource.getResourceId()); Long currentUserId = StpUtil.getLoginIdAsLong(); if (!currentUserId.equals(existing.getOwnerUserId())) { return ApiResult.fail("只有资源创建者才能修改"); } service.updateById(resource); return ApiResult.ok(); } ``` ## 四、前端已完成的改动 | 文件 | 改动说明 | |-----|---------| | `app/composables/useResourceAccess.ts` | 权限工具库:级别判断、enrichment、isOwner | | `app/api/app/appResource/model/index.ts` | 模型新增 `ownerUserId` / `accessLevel` / `isOwner` 字段 | | `app/pages/developer/resources/index.vue` | 总览页:协作说明banner、权限列标识 | | `app/pages/developer/resources/servers.vue` | 服务器:操作按权限显示,SSH/Panel 需连接权限 | | `app/pages/developer/resources/databases.vue` | 数据库:用户名列权限脱敏,操作按isOwner控制 | | `app/pages/developer/resources/ssl.vue` | SSL:私钥完全不显示给协作者,编辑/删除仅Owner | | `app/pages/developer/resources/storage.vue` | 云存储:操作按isOwner控制 | | `app/pages/developer/resources/domains.vue` | 域名:操作按isOwner控制,协作者标识 | ## 五、降级策略(后端尚未改造时) 前端的 `enrichResourcesWithPermission()` 提供了前端降级逻辑: - 若后端未返回 `accessLevel`,前端根据 `ownerUserId` vs localStorage `UserId` 判断 - `ownerUserId === userId` → accessLevel=3(完全权限) - 否则 → accessLevel=1(基础查看) > **注意**:前端降级逻辑仅用于开发阶段。生产环境必须由后端计算 accessLevel 并过滤敏感字段,前端无法保证数据安全。