You've already forked DataMate
- 数据库层面: - 创建 RBAC 核心表(角色、菜单权限) - 扩展现有表支持数据共享 - 初始化基础数据 - 后端层面: - 实现 UserContext 用户上下文管理 - 实现数据集访问权限服务 - 实现菜单权限服务 - 添加数据集共享功能 - 修复前端命名不匹配问题(snake_case vs camelCase) - 修复请求头不匹配问题(X-User-Roles vs X-Role-Codes) - 修复 Mapper 方法未实现问题 - 修复共享设置持久化缺失问题 - 前端层面: - 创建菜单权限工具 - 更新 Redux Store 支持菜单过滤 - 创建数据集共享设置组件 - 添加用户信息到请求头 - 实现 Token 刷新逻辑 - 数据隔离: - 实现 MyBatis 查询权限检查 - 实现数据文件访问控制 参考: - Codex 生成的实施方案 - kimI-cli 实施结果 - Codex Review 审核报告 修复的问题: 1. 前端命名不匹配(is_shared -> isShared, shared_with -> sharedWith) 2. 请求头不匹配(X-User-Roles -> X-Role-Codes) 3. Mapper 方法未实现(添加 findFilesWithAccessCheck 等方法声明) 4. 共享设置持久化缺失(添加 isShared 和 sharedWith 字段到 UpdateDatasetRequest) 5. 用户上下文加载问题(实现 Token 刷新逻辑)
25 KiB
25 KiB
DataMate 用户权限体系 - 具体实现方案
用户选择
- 多租户架构:否,只用 owner_id(不使用 tenant_id)
- 权限粒度:菜单级(粗粒度,控制页面访问)
- 资源共享:允许共享,但由创建人控制是否允许共享
1. 数据库层面的具体实现
1.1 RBAC 核心表
菜单权限表
CREATE TABLE t_sys_menu_permissions (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID',
menu_code VARCHAR(50) NOT NULL UNIQUE COMMENT '菜单编码,如 DATASET_MANAGEMENT',
menu_name VARCHAR(100) NOT NULL COMMENT '菜单名称',
menu_path VARCHAR(200) COMMENT '菜单路径,如 /data/management',
parent_code VARCHAR(50) COMMENT '父菜单编码',
icon VARCHAR(50) COMMENT '菜单图标',
sort_order INT DEFAULT 0 COMMENT '排序',
description VARCHAR(500) COMMENT '描述',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='菜单权限表';
-- 初始化菜单数据
INSERT INTO t_sys_menu_permissions (id, menu_code, menu_name, menu_path, parent_code, icon, sort_order) VALUES
('1', 'HOME', '首页', '/', NULL, 'Home', 1),
('2', 'DATASET_MANAGEMENT', '数据管理', '/data/management', NULL, 'Database', 2),
('3', 'DATASET_CREATE', '数据管理-创建', '/data/management/create', 'DATASET_MANAGEMENT', 'Plus', 3),
('4', 'DATASET_VIEW', '数据管理-查看', '/data/management/view', 'DATASET_MANAGEMENT', 'Eye', 4),
('5', 'DATA_ANNOTATION', '数据标注', '/annotation', NULL, 'PenTool', 5),
('6', 'ANNOTATION_CREATE', '数据标注-创建', '/annotation/create', 'DATA_ANNOTATION', 'Plus', 6),
('7', 'OPERATOR_MARKET', '操作符市场', '/operator/market', NULL, 'ShoppingCart', 7);
角色菜单权限关联表
CREATE TABLE t_sys_role_menu_permissions (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID',
role_id VARCHAR(36) NOT NULL COMMENT '角色ID',
menu_code VARCHAR(50) NOT NULL COMMENT '菜单编码',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_menu (role_id, menu_code),
FOREIGN KEY fk_role (role_id) REFERENCES t_sys_roles(id) ON DELETE CASCADE,
FOREIGN KEY fk_menu (menu_code) REFERENCES t_sys_menu_permissions(menu_code) ON DELETE CASCADE
) COMMENT='角色菜单权限关联表';
角色表(更新)
CREATE TABLE IF NOT EXISTS t_sys_roles (
id VARCHAR(36) PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description VARCHAR(500),
is_system BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='角色表';
-- 初始化角色
INSERT INTO t_sys_roles (id, code, name, description, is_system) VALUES
('R1', 'ADMIN', '系统管理员', '拥有所有权限', TRUE),
('R2', 'USER', '普通用户', '基础权限', FALSE);
1.2 数据集表扩展(支持共享)
-- 添加共享相关字段
ALTER TABLE t_dm_datasets ADD COLUMN owner_id VARCHAR(36) COMMENT '数据集所有者(用户ID)';
ALTER TABLE t_dm_datasets ADD COLUMN is_shared BOOLEAN DEFAULT FALSE COMMENT '是否允许共享';
ALTER TABLE t_dm_datasets ADD COLUMN shared_with JSON COMMENT '可共享的用户ID列表,JSON数组格式';
-- 迁移现有数据(设置 owner_id)
UPDATE t_dm_datasets SET owner_id = created_by WHERE owner_id IS NULL;
-- 创建索引
CREATE INDEX idx_owner_id ON t_dm_datasets(owner_id);
CREATE INDEX idx_is_shared ON t_dm_datasets(is_shared);
1.3 数据迁移脚本
-- 20260204_add_user_permissions.sql
USE datamate;
-- Step 1: 创建权限表
-- (上面的表定义)
-- Step 2: 初始化管理员权限
INSERT INTO t_sys_role_menu_permissions (id, role_id, menu_code)
SELECT
REPLACE(UUID(), '-', '-'),
r.id,
m.menu_code
FROM t_sys_roles r
CROSS JOIN t_sys_menu_permissions m
WHERE r.code = 'ADMIN';
-- Step 3: 初始化普通用户权限
INSERT INTO t_sys_role_menu_permissions (id, role_id, menu_code)
SELECT
REPLACE(UUID(), '-', '-'),
r.id,
m.menu_code
FROM t_sys_roles r
CROSS JOIN t_sys_menu_permissions m
WHERE r.code = 'USER' AND m.menu_code IN ('HOME', 'DATASET_VIEW', 'DATA_ANNOTATION');
2. 后端层面的具体实现
2.1 UserContext 拦截器
// backend/shared/domain-common/src/main/java/com/datamate/common/security/UserContext.java
package com.datamate.common.security;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 用户上下文信息
*/
@Data
@AllArgsConstructor
public class UserContext {
private String userId;
private String username;
private String[] roleCodes;
}
// backend/shared/domain-common/src/main/java/com/datamate/common/security/UserContextHolder.java
package com.datamate.common.security;
/**
* 用户上下文持有者(ThreadLocal)
*/
public class UserContextHolder {
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
public static void setContext(UserContext context) {
CONTEXT.set(context);
}
public static UserContext getContext() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
// backend/shared/domain-common/src/main/java/com/datamate/common/infrastructure/web/UserContextInterceptor.java
package com.datamate.common.infrastructure.web;
import com.datamate.common.security.UserContext;
import com.datamate.common.security.UserContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userId = request.getHeader("X-User-Id");
String username = request.getHeader("X-User-Name");
String roles = request.getHeader("X-User-Roles");
if (userId != null) {
UserContext context = new UserContext(userId, username, roles.split(","));
UserContextHolder.setContext(context);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContextHolder.clear();
}
}
// backend/shared/domain-common/src/main/java/com/datamate/common/infrastructure/web/WebMvcConfig.java
package com.datamate.common.infrastructure.web;
import com.datamate.common.infrastructure.web.UserContextInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserContextInterceptor userContextInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userContextInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/**");
}
}
2.2 数据共享服务
// backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetAccessService.java
package com.datamate.datamanagement.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.datamate.datamanagement.domain.model.dataset.Dataset;
import com.datamate.datamanagement.infrastructure.persistence.repository.DatasetRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class DatasetAccessService {
private final DatasetRepository datasetRepository;
/**
* 检查用户是否可以访问数据集
*/
public boolean canAccessDataset(String userId, String datasetId) {
Dataset dataset = datasetRepository.selectById(datasetId);
if (dataset == null) {
return false;
}
// 自己创建的,可以直接访问
if (userId.equals(dataset.getOwnerId())) {
return true;
}
// 共享的,检查是否在共享列表中
if (Boolean.TRUE.equals(dataset.getIsShared())) {
return isUserInSharedList(userId, dataset.getSharedWith());
}
return false;
}
/**
* 检查用户是否在共享列表中
*/
private boolean isUserInSharedList(String userId, String sharedWith) {
if (sharedWith == null || sharedWith.isEmpty()) {
return false;
}
// 解析 JSON 数组(sharedWith 格式为 ["user1", "user2", ...])
return sharedWith.contains(userId);
}
/**
* 获取用户可访问的数据集列表
*/
public List<Dataset> getAccessibleDatasets(String userId) {
return datasetRepository.selectList(new LambdaQueryWrapper<Dataset>()
.eq(Dataset::getOwnerId, userId)
.or()
.eq(Dataset::getIsShared, false)
.and(wrapper -> wrapper
.eq(Dataset::getIsShared, true)
.apply("JSON_CONTAINS(shared_with, {0})", userId))
)
.orderByAsc(Dataset::getCreatedAt));
}
}
// backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/ShareDatasetRequest.java
package com.datamate.datamanagement.interfaces.dto;
import lombok.Data;
import java.util.List;
@Data
public class ShareDatasetRequest {
private Boolean isShared;
private List<String> sharedWith;
}
2.3 菜单权限服务
// backend/services/main-application/src/main/java/com/datamate/main/application/MenuPermissionService.java
package com.datamate.main.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.datamate.common.domain.model.role.Role;
import com.datamate.common.infrastructure.persistence.repository.RoleRepository;
import com.datamate.common.infrastructure.persistence.repository.RoleMenuPermissionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class MenuPermissionService {
private final RoleRepository roleRepository;
private final RoleMenuPermissionRepository roleMenuPermissionRepository;
/**
* 获取用户可访问的菜单列表
*/
public Set<String> getAccessibleMenus(String userId) {
// 获取用户角色
Set<String> roleCodes = roleRepository.findByUserId(userId)
.stream()
.map(Role::getCode)
.collect(Collectors.toSet());
// 获取角色对应的菜单权限
return roleMenuPermissionRepository.findMenuCodesByRoleCodes(roleCodes);
}
/**
* 检查用户是否有菜单访问权限
*/
public boolean hasMenuAccess(String userId, String menuCode) {
Set<String> accessibleMenus = getAccessibleMenus(userId);
return accessibleMenus.contains(menuCode);
}
}
// backend/services/main-application/src/main/java/com/datamate/main/interfaces/rest/MenuPermissionController.java
package com.datamate.main.interfaces.rest;
import com.datamate.main.application.MenuPermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/api/menu-permissions")
@RequiredArgsConstructor
public class MenuPermissionController {
private final MenuPermissionService menuPermissionService;
@GetMapping("/accessible-menus")
public ResponseEntity<Set<String>> getAccessibleMenus(
@RequestHeader("X-User-Id") String userId) {
Set<String> menus = menuPermissionService.getAccessibleMenus(userId);
return ResponseEntity.ok(menus);
}
}
2.4 DatasetController 更新
// backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/DatasetController.java
// 添加共享接口
@PostMapping("/{id}/share")
public ResponseEntity<Void> shareDataset(
@PathVariable String id,
@RequestBody ShareDatasetRequest request,
@RequestHeader("X-User-Id") String userId) {
Dataset dataset = datasetService.findById(id);
if (dataset == null) {
return ResponseEntity.notFound().build();
}
// 验证权限:只有所有者可以修改共享设置
if (!userId.equals(dataset.getOwnerId())) {
return ResponseEntity.status(403).build();
}
dataset.setIsShared(request.getIsShared());
dataset.setSharedWith(request.getSharedWith() != null ? JsonUtil.toJson(request.getSharedWith()) : null);
datasetService.updateById(dataset);
return ResponseEntity.ok().build();
}
// 修改查询接口,添加权限检查
@GetMapping
public ResponseEntity<List<Dataset>> getDatasets(
@RequestHeader("X-User-Id") String userId) {
// 使用 DatasetAccessService 获取可访问的数据集
List<Dataset> datasets = datasetAccessService.getAccessibleDatasets(userId);
return ResponseEntity.ok(datasets);
}
3. 前端层面的具体实现
3.1 menu.ts 工具
// frontend/src/utils/menu.ts
export interface MenuItem {
menuCode: string;
menuName: string;
menuPath: string;
icon?: string;
parentCode?: string;
}
// 定义菜单项
export const menuItems: MenuItem[] = [
{
menuCode: 'HOME',
menuName: '首页',
menuPath: '/',
icon: 'Home',
},
{
menuCode: 'DATASET_MANAGEMENT',
menuName: '数据管理',
menuPath: '/data/management',
icon: 'Database',
},
{
menuCode: 'DATASET_CREATE',
menuName: '数据管理-创建',
menuPath: '/data/management/create',
parentCode: 'DATASET_MANAGEMENT',
icon: 'Plus',
},
{
menuCode: 'DATASET_VIEW',
menuName: '数据管理-查看',
menuPath: '/data/management/view',
parentCode: 'DATASET_MANAGEMENT',
icon: 'Eye',
},
{
menuCode: 'DATA_ANNOTATION',
menuName: '数据标注',
menuPath: '/annotation',
icon: 'PenTool',
},
{
menuCode: 'ANNOTATION_CREATE',
menuName: '数据标注-创建',
menuPath: '/annotation/create',
parentCode: 'DATA_ANNOTATION',
icon: 'Plus',
},
{
menuCode: 'OPERATOR_MARKET',
menuName: '操作符市场',
menuPath: '/operator/market',
icon: 'ShoppingCart',
},
];
/**
* 过滤菜单(基于权限)
*/
export const getFilteredMenus = (accessibleMenus: string[]) => {
return menuItems.filter(item => accessibleMenus.includes(item.menuCode));
};
/**
* 查找菜单
*/
export const findMenuItem = (menuCode: string) => {
return menuItems.find(item => item.menuCode === menuCode);
};
3.2 menu.tsx 菜单组件
// frontend/src/pages/Layout/menu.tsx
import { menuItems, getFilteredMenus } from '@/utils/menu';
import { useSelector } from 'react-redux';
export default function AppMenu() {
const accessibleMenus = useSelector((state: RootState) => state.auth.accessibleMenus);
const filteredMenus = getFilteredMenus(accessibleMenus);
return (
<Menu theme="dark" mode="inline">
{filteredMenus.map(menu => (
<Menu.Item key={menu.menuCode} icon={<Icon icon={menu.icon} />}>
{menu.parentCode ? (
<SubMenu title={menu.menuName}>
{getFilteredMenus(accessibleMenus)
.filter(m => m.parentCode === menu.menuCode)
.map(subMenu => (
<Menu.Item key={subMenu.menuCode}>{subMenu.menuName}</Menu.Item>
))}
</SubMenu>
) : (
<a href={menu.menuPath}>{menu.menuName}</a>
)}
</Menu.Item>
))}
</Menu>
);
}
3.3 ShareSettings.tsx 共享设置组件
// frontend/src/pages/DataManagement/Detail/components/ShareSettings.tsx
import { useState } from 'react';
import { Modal, Form, Switch, Select, Button, message } from 'antd';
import { shareDataset } from '@/pages/DataManagement/dataset.api';
interface ShareSettingsProps {
datasetId: string;
ownerId: string;
currentUserId: string;
isShared: boolean;
sharedWith: string[];
onSuccess: () => void;
}
export default function ShareSettings({
datasetId,
ownerId,
currentUserId,
isShared: isSharedProp,
sharedWith: sharedWithProp,
onSuccess,
}: ShareSettingsProps) {
const [isOpen, setIsOpen] = useState(false);
const [isShared, setIsShared] = useState(isSharedProp);
const [sharedWith, setSharedWith] = useState<string[]>(sharedWithProp || []);
const handleSave = async () => {
try {
await shareDataset(datasetId, isShared, sharedWith);
message.success('共享设置已更新');
setIsOpen(false);
onSuccess();
} catch (error) {
message.error('更新共享设置失败');
}
};
// 只有所有者可以修改共享设置
const canEdit = currentUserId === ownerId;
return (
<>
{canEdit && (
<Button onClick={() => setIsOpen(true)}>共享设置</Button>
)}
<Modal title="共享设置" open={isOpen} onCancel={() => setIsOpen(false)} footer={
<Button type="primary" onClick={handleSave}>保存</Button>
}>
<Form>
<Form.Item label="允许共享">
<Switch checked={isShared} onChange={setIsShared} />
</Form.Item>
{isShared && (
<Form.Item label="共享给">
<Select
mode="multiple"
value={sharedWith}
onChange={setSharedWith}
placeholder="选择用户"
options={[]} // 从用户列表获取
/>
</Form.Item>
)}
</Form>
</Modal>
</>
);
}
3.4 dataset.api.ts 更新
// frontend/src/pages/DataManagement/dataset.api.ts
export const shareDataset = async (
datasetId: string,
isShared: boolean,
sharedWith: string[]
) => {
return request.post(`/api/datasets/${datasetId}/share`, {
is_shared: isShared,
shared_with: sharedWith,
});
};
// 新增:获取可访问的菜单
export const getAccessibleMenus = async () => {
return request.get('/api/menu-permissions/accessible-menus');
};
3.5 Redux Store 更新
// frontend/src/store/slices/authSlice.ts
export interface AuthState {
isAuthenticated: boolean;
token: string | null;
user: User | null;
accessibleMenus: string[]; // 新增
}
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
loginSuccess: (state, action) => {
state.token = action.payload.token;
state.user = action.payload.user;
state.accessibleMenus = action.payload.accessibleMenus; // 新增
state.isAuthenticated = true;
},
logout: (state) => {
state.token = null;
state.user = null;
state.accessibleMenus = []; // 新增
state.isAuthenticated = false;
},
},
});
3.6 request.ts 更新
// frontend/src/utils/request.ts
import { UserContextHolder } from '@/utils/userContext'; // 需要实现
// 修改 request 拦截器,添加用户信息到 headers
instance.interceptors.request.use(config => {
const state = store.getState();
const { user } = state.auth;
if (user) {
config.headers = config.headers || {};
config.headers['X-User-Id'] = user.id;
config.headers['X-User-Name'] = user.username;
config.headers['X-User-Roles'] = user.roles?.join(',') || '';
}
return config;
});
3.7 Sidebar.tsx 更新
// frontend/src/pages/Layout/Sidebar.tsx
import { menuItems } from '@/utils/menu';
import { useSelector } from 'react-redux';
import { getFilteredMenus } from '@/utils/menu';
export default function Sidebar() {
const accessibleMenus = useSelector((state: RootState) => state.auth.accessibleMenus);
const filteredMenus = getFilteredMenus(accessibleMenus);
return (
<Sider width={256} theme="dark">
<Menu theme="dark" mode="inline">
{filteredMenus.map(menu => (
<Menu.Item key={menu.menuCode} icon={<Icon icon={menu.icon} />}>
{menu.parentCode ? (
<SubMenu title={menu.menuName}>
{getFilteredMenus(accessibleMenus)
.filter(m => m.parentCode === menu.menuCode)
.map(subMenu => (
<Menu.Item key={subMenu.menuCode}>{subMenu.menuName}</Menu.Item>
))}
</SubMenu>
) : (
<a href={menu.menuPath}>{menu.menuName}</a>
)}
</Menu.Item>
))}
</Menu>
</Sider>
);
}
4. 数据隔离的具体实现
4.1 MyBatis XML 更新
<!-- backend/services/data-management-service/src/main/resources/mappers/DatasetMapper.xml -->
<!-- 添加查询方法 -->
<select id="findAccessibleDatasets" resultType="Dataset">
SELECT
d.id,
d.parent_dataset_id,
d.name,
d.description,
d.dataset_type,
d.category,
d.path,
d.format,
d.schema_info,
d.size_bytes,
d.file_count,
d.record_count,
d.retention_days,
d.tags,
d.metadata,
d.status,
d.is_public,
d.is_featured,
d.version,
d.created_at,
d.updated_at,
d.created_by,
d.updated_by,
d.owner_id,
d.is_shared,
d.shared_with,
CASE WHEN d.owner_id = #{userId} THEN 1 ELSE 0 END AS is_owner
FROM t_dm_datasets d
WHERE
d.owner_id = #{userId}
OR (
d.is_shared = 1
AND JSON_CONTAINS(d.shared_with, JSON_QUOTE(#{userId}))
)
ORDER BY d.created_at DESC
</select>
<select id="findByIdWithAccessCheck" resultType="Dataset">
SELECT d.*
FROM t_dm_datasets d
WHERE d.id = #{datasetId}
AND (
d.owner_id = #{userId}
OR (d.is_shared = 1 AND JSON_CONTAINS(d.shared_with, JSON_QUOTE(#{userId})))
)
</select>
<!-- backend/services/data-management-service/src/main/resources/mappers/DatasetFileMapper.xml -->
<select id="findFilesWithAccessCheck" resultType="DatasetFile">
SELECT f.*
FROM t_dm_dataset_files f
JOIN t_dm_datasets d ON d.id = f.dataset_id
WHERE f.dataset_id = #{datasetId}
AND (
d.owner_id = #{userId}
OR (d.is_shared = 1 AND JSON_CONTAINS(d.shared_with, JSON_QUOTE(#{userId})))
)
</select>
5. 实施顺序建议
Phase 1:数据库迁移(1-2 天)
- 执行数据库迁移脚本
20260204_add_user_permissions.sql - 验证表结构是否正确创建
- 验证数据是否正确迁移
Phase 2:后端基础(3-5 天)
- 创建 UserContext 相关类
- 创建 UserContextInterceptor 拦截器
- 配置 WebMvcConfig
- 测试拦截器是否正常工作
Phase 3:后端服务(3-5 天)
- 创建 DatasetAccessService
- 创建 MenuPermissionService
- 更新 DatasetController 添加共享接口
- 创建 MenuPermissionController
- 单元测试和集成测试
Phase 4:前端基础(2-3 天)
- 创建 menu.ts 工具
- 更新 Redux Store(添加 accessibleMenus)
- 更新 request.ts(添加用户 headers)
- 测试菜单过滤逻辑
Phase 5:前端 UI(2-3 天)
- 创建 ShareSettings 组件
- 更新 Sidebar 组件
- 更新 dataset.api.ts
- 集成到数据集详情页
- UI 测试
Phase 6:数据隔离实现(2-3 天)
- 更新 MyBatis XML 映射文件
- 更新 Repository 查询方法
- 测试数据隔离是否正确
- 测试共享功能
总结
需要创建/修改的文件(共 20+ 个)
后端(11 个):
- backend/shared/domain-common/src/main/java/com/datamate/common/security/UserContext.java
- backend/shared/domain-common/src/main/java/com/datamate/common/security/UserContextHolder.java
- backend/shared/domain-common/src/main/java/com/datamate/common/infrastructure/web/UserContextInterceptor.java
- backend/shared/domain-common/src/main/java/com/datamate/common/infrastructure/web/WebMvcConfig.java
- backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetAccessService.java
- backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/ShareDatasetRequest.java
- backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/DatasetController.java (更新)
- backend/services/main-application/src/main/java/com/datamate/main/application/MenuPermissionService.java
- backend/services/main-application/src/main/java/com/datamate/main/interfaces/rest/MenuPermissionController.java
- backend/services/main-application/src/main/resources/mappers/MenuPermissionMapper.xml
- scripts/db/20260204_add_user_permissions.sql (新建)
前端(9 个):
- frontend/src/utils/menu.ts (新建)
- frontend/src/pages/Layout/menu.tsx (新建/更新)
- frontend/src/pages/Layout/Sidebar.tsx (更新)
- frontend/src/pages/DataManagement/Detail/components/ShareSettings.tsx (新建)
- frontend/src/pages/DataManagement/dataset.api.ts (更新)
- frontend/src/pages/DataManagement/dataset.model.ts (更新)
- frontend/src/store/slices/authSlice.ts (更新)
- frontend/src/utils/request.ts (更新)
- frontend/src/pages/DataManagement/Detail/components/DatasetDetail.tsx (集成)