Files
tiantian-system/app/pages/developer/resources/README_RESOURCE_COLLAB.md
2026-04-08 17:10:58 +08:00

143 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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