初始化2

This commit is contained in:
2026-04-08 17:10:58 +08:00
commit 4986d90eb9
532 changed files with 112617 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
# 资源中心协作权限设计
## 一、数据库变更
```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<AppResource> 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<Void> 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 并过滤敏感字段,前端无法保证数据安全。