diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/config/RoleBootstrapRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/config/RoleBootstrapRunner.java new file mode 100644 index 0000000..d024833 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/config/RoleBootstrapRunner.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.config; + +import io.github.malonetalk.entity.SysRole; +import io.github.malonetalk.mapper.SysRoleMapper; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * 启动引导:sys_role 为空时创建初始角色。 + * + *

创建管理员(id=1)和普通用户(id=2)两个角色, + * 与 {@code @AdminOnly} 注解配合实现管理员权限判定。 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class RoleBootstrapRunner implements CommandLineRunner { + + private final SysRoleMapper sysRoleMapper; + + @Override + public void run(String... args) { + if (sysRoleMapper.countAll() > 0) { + return; + } + LocalDateTime now = LocalDateTime.now(); + + SysRole admin = new SysRole(); + admin.setId(1); + admin.setName("管理员"); + admin.setDescription("系统管理员,拥有所有权限"); + admin.setCreateTime(now); + admin.setUpdateTime(now); + sysRoleMapper.insert(admin); + + SysRole user = new SysRole(); + user.setId(2); + user.setName("普通用户"); + user.setDescription("普通用户,受限权限"); + user.setCreateTime(now); + user.setUpdateTime(now); + sysRoleMapper.insert(user); + + log.info("Bootstrapped initial roles: 管理员 (id=1), 普通用户 (id=2)."); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java index da9b162..32629aa 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java @@ -24,12 +24,18 @@ import io.github.malonetalk.convertor.DatasourceConverter; import io.github.malonetalk.dto.DatasourceRequest; import io.github.malonetalk.dto.DatasourceResponse; +import io.github.malonetalk.entity.ColumnInfo; import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.entity.TableInfo; import io.github.malonetalk.enums.Status; import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.mapper.ColumnSemanticInfoMapper; +import io.github.malonetalk.mapper.TableInfoMapper; import io.github.malonetalk.service.DatasourceService; import jakarta.validation.Valid; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -48,6 +54,18 @@ public class DatasourceController { private final DatasourceService dataSourceService; private final DatasourceConverter datasourceConverter; + /** + * TableInfoMapper 和 ColumnSemanticInfoMapper 直接注入 Controller 而非通过 Service: + * + *

{@code GET /{id}/tables} 和 {@code GET /{id}/columns} 仅做"查 mapper → 返回"的纯透传, + * 零业务逻辑(无 join、无事务、无缓存、无校验)。引入中间 Service 只会产生一层无意义的委托代码。 + * + *

如果后续需要按角色过滤返回结果(权限控制),届时再将两个查询收拢到 TableMetadataService。 + */ + private final TableInfoMapper tableInfoMapper; + + private final ColumnSemanticInfoMapper columnSemanticInfoMapper; + @GetMapping public Result> findAll() { List list = @@ -60,6 +78,33 @@ public Result findById(@PathVariable Integer id) { return Result.success(datasourceConverter.toResponse(requireDatasource(id))); } + /** 返回某数据源下的所有物理表名,供权限配置页使用。 */ + @AdminOnly + @GetMapping("/{id}/tables") + public Result> listTableNames(@PathVariable Integer id) { + requireDatasource(id); + List tables = + tableInfoMapper.selectByDatasourceId(id).stream() + .map(TableInfo::getTableName) + .toList(); + return Result.success(tables); + } + + /** 返回某数据源下所有表的列名,一次查询,供列级权限配置使用。 */ + @AdminOnly + @GetMapping("/{id}/columns") + public Result>> listAllColumns(@PathVariable Integer id) { + requireDatasource(id); + Map> map = + columnSemanticInfoMapper.selectByDatasourceId(id).stream() + .collect( + Collectors.groupingBy( + ColumnInfo::getTableName, + Collectors.mapping( + ColumnInfo::getColumnName, Collectors.toList()))); + return Result.success(map); + } + @AdminOnly @PostMapping public Result save(@Valid @RequestBody DatasourceRequest request) { diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java index a6fbd77..f1f3ccb 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java @@ -39,7 +39,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@AdminOnly @Slf4j @RestController @AllArgsConstructor @@ -50,12 +49,14 @@ public class MetricController { private final MetricService metricService; private final MetricConverter metricConverter; + @AdminOnly @PostMapping public Result create(@Valid @RequestBody MetricRequest request) { MetricInfo entity = metricConverter.toEntity(request); return Result.success(metricConverter.toResponse(metricService.create(entity))); } + @AdminOnly @PutMapping("/{id}") public Result update( @PathVariable @Positive(message = "id 必须为正数") Integer id, @@ -64,6 +65,7 @@ public Result update( return Result.success(metricConverter.toResponse(metricService.update(id, entity))); } + @AdminOnly @DeleteMapping("/{id}") public Result delete(@PathVariable @Positive(message = "id 必须为正数") Integer id) { metricService.delete(id); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/SysRoleController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/SysRoleController.java new file mode 100644 index 0000000..b284357 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/SysRoleController.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.controller; + +import io.github.malonetalk.annotation.AdminOnly; +import io.github.malonetalk.common.Result; +import io.github.malonetalk.dto.ColumnPermissionResponse; +import io.github.malonetalk.dto.RoleRequest; +import io.github.malonetalk.dto.RoleResponse; +import io.github.malonetalk.dto.SaveColumnPermissionRequest; +import io.github.malonetalk.dto.SaveTablePermissionRequest; +import io.github.malonetalk.dto.TablePermissionResponse; +import io.github.malonetalk.service.SysRoleService; +import jakarta.validation.Valid; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +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; + +@AdminOnly +@RestController +@AllArgsConstructor +@RequestMapping("/api/sys/role") +@Validated +public class SysRoleController { + + private final SysRoleService sysRoleService; + + @GetMapping + public Result> listAll() { + return Result.success(sysRoleService.listAll()); + } + + @PostMapping + public Result create(@Valid @RequestBody RoleRequest request) { + return Result.success(sysRoleService.create(request)); + } + + @PutMapping("/{id}") + public Result update( + @PathVariable Integer id, @Valid @RequestBody RoleRequest request) { + return Result.success(sysRoleService.update(id, request)); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + sysRoleService.delete(id); + return Result.success(); + } + + @GetMapping("/{roleId}/permissions") + public Result> getPermissions(@PathVariable Integer roleId) { + return Result.success(sysRoleService.getPermissions(roleId)); + } + + @PutMapping("/{roleId}/permissions") + public Result savePermissions( + @PathVariable Integer roleId, @Valid @RequestBody SaveTablePermissionRequest request) { + sysRoleService.savePermissions(roleId, request); + return Result.success(); + } + + @GetMapping("/{roleId}/columns") + public Result> getColumnPermissions( + @PathVariable Integer roleId, @RequestParam Integer datasourceId) { + return Result.success(sysRoleService.getColumnPermissions(roleId, datasourceId)); + } + + @PutMapping("/{roleId}/columns") + public Result saveColumnPermissions( + @PathVariable Integer roleId, @Valid @RequestBody SaveColumnPermissionRequest request) { + sysRoleService.saveColumnPermissions(roleId, request); + return Result.success(); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/RoleConverter.java b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/RoleConverter.java new file mode 100644 index 0000000..afd97e3 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/RoleConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.convertor; + +import io.github.malonetalk.dto.RoleResponse; +import io.github.malonetalk.entity.SysRole; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface RoleConverter { + + RoleResponse toResponse(SysRole role); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ColumnPermissionResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ColumnPermissionResponse.java new file mode 100644 index 0000000..bb5e5f4 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ColumnPermissionResponse.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import java.util.List; + +public record ColumnPermissionResponse(String tableName, List columnNames) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/RoleRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/RoleRequest.java new file mode 100644 index 0000000..2995bee --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/RoleRequest.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; + +public record RoleRequest(@NotBlank(message = "name 不能为空") String name, String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/RoleResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/RoleResponse.java new file mode 100644 index 0000000..1f08dee --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/RoleResponse.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import java.time.LocalDateTime; + +public record RoleResponse(Integer id, String name, String description, LocalDateTime createTime) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/SaveColumnPermissionRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SaveColumnPermissionRequest.java new file mode 100644 index 0000000..d926934 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SaveColumnPermissionRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record SaveColumnPermissionRequest( + @NotNull(message = "datasourceId 不能为空") Integer datasourceId, + @NotBlank(message = "tableName 不能为空") String tableName, + List columnNames) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/SaveTablePermissionRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SaveTablePermissionRequest.java new file mode 100644 index 0000000..b1a4956 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SaveTablePermissionRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record SaveTablePermissionRequest( + @NotNull(message = "datasourceId 不能为空") Integer datasourceId, List tableNames) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/TablePermissionResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/TablePermissionResponse.java new file mode 100644 index 0000000..31ebde2 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/TablePermissionResponse.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import java.util.List; + +public record TablePermissionResponse(Integer datasourceId, List tableNames) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/RoleHiddenColumn.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/RoleHiddenColumn.java new file mode 100644 index 0000000..9ba8b0b --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/RoleHiddenColumn.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class RoleHiddenColumn { + private Integer id; + private Integer roleId; + private Integer datasourceId; + private String tableName; + private String columnName; + private LocalDateTime createTime; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/RoleTablePermission.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/RoleTablePermission.java new file mode 100644 index 0000000..ae79233 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/RoleTablePermission.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class RoleTablePermission { + private Integer id; + private Integer roleId; + private Integer datasourceId; + private String tableName; + private LocalDateTime createTime; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/SysRole.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/SysRole.java new file mode 100644 index 0000000..6f4e6f3 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/SysRole.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class SysRole { + private Integer id; + private String name; + private String description; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/RoleHiddenColumnMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/RoleHiddenColumnMapper.java new file mode 100644 index 0000000..40897eb --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/RoleHiddenColumnMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.entity.RoleHiddenColumn; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface RoleHiddenColumnMapper { + + List selectByRoleAndDatasource( + @Param("roleId") Integer roleId, @Param("datasourceId") Integer datasourceId); + + int insert(RoleHiddenColumn perm); + + int deleteByRoleDatasourceAndTable( + @Param("roleId") Integer roleId, + @Param("datasourceId") Integer datasourceId, + @Param("tableName") String tableName); + + int deleteByRoleId(@Param("roleId") Integer roleId); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/RoleTablePermissionMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/RoleTablePermissionMapper.java new file mode 100644 index 0000000..da92fcf --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/RoleTablePermissionMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.entity.RoleTablePermission; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface RoleTablePermissionMapper { + + List selectByRoleId(@Param("roleId") Integer roleId); + + List selectByRoleAndDatasource( + @Param("roleId") Integer roleId, @Param("datasourceId") Integer datasourceId); + + int insert(RoleTablePermission perm); + + int deleteByRoleAndDatasource( + @Param("roleId") Integer roleId, @Param("datasourceId") Integer datasourceId); + + int deleteByRoleId(@Param("roleId") Integer roleId); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysRoleMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysRoleMapper.java new file mode 100644 index 0000000..b04c593 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysRoleMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.entity.SysRole; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface SysRoleMapper { + + List selectAll(); + + SysRole selectById(@Param("id") Integer id); + + int insert(SysRole role); + + int update(SysRole role); + + int deleteById(@Param("id") Integer id); + + int countAll(); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/SysRoleService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysRoleService.java new file mode 100644 index 0000000..1d08980 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysRoleService.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.dto.ColumnPermissionResponse; +import io.github.malonetalk.dto.RoleRequest; +import io.github.malonetalk.dto.RoleResponse; +import io.github.malonetalk.dto.SaveColumnPermissionRequest; +import io.github.malonetalk.dto.SaveTablePermissionRequest; +import io.github.malonetalk.dto.TablePermissionResponse; +import java.util.List; + +public interface SysRoleService { + + List listAll(); + + RoleResponse create(RoleRequest request); + + RoleResponse update(Integer id, RoleRequest request); + + void delete(Integer id); + + List getPermissions(Integer roleId); + + void savePermissions(Integer roleId, SaveTablePermissionRequest request); + + List getColumnPermissions(Integer roleId, Integer datasourceId); + + void saveColumnPermissions(Integer roleId, SaveColumnPermissionRequest request); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/SysRoleServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysRoleServiceImpl.java new file mode 100644 index 0000000..dea1ba7 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysRoleServiceImpl.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.convertor.RoleConverter; +import io.github.malonetalk.dto.ColumnPermissionResponse; +import io.github.malonetalk.dto.RoleRequest; +import io.github.malonetalk.dto.RoleResponse; +import io.github.malonetalk.dto.SaveColumnPermissionRequest; +import io.github.malonetalk.dto.SaveTablePermissionRequest; +import io.github.malonetalk.dto.TablePermissionResponse; +import io.github.malonetalk.entity.RoleHiddenColumn; +import io.github.malonetalk.entity.RoleTablePermission; +import io.github.malonetalk.entity.SysRole; +import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.mapper.RoleHiddenColumnMapper; +import io.github.malonetalk.mapper.RoleTablePermissionMapper; +import io.github.malonetalk.mapper.SysRoleMapper; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@AllArgsConstructor +public class SysRoleServiceImpl implements SysRoleService { + + private final SysRoleMapper sysRoleMapper; + private final RoleTablePermissionMapper roleTablePermissionMapper; + private final RoleHiddenColumnMapper roleHiddenColumnMapper; + private final RoleConverter roleConverter; + + /** 管理员角色 id:AuthInterceptor 靠 role_id==1 判 @AdminOnly,删掉即永久锁死管理员。 */ + private static final int ADMIN_ROLE_ID = 1; + + @Override + public List listAll() { + return sysRoleMapper.selectAll().stream().map(roleConverter::toResponse).toList(); + } + + @Override + public RoleResponse create(RoleRequest request) { + checkNameConflict(request.name(), null); + LocalDateTime now = LocalDateTime.now(); + SysRole role = new SysRole(); + role.setName(request.name()); + role.setDescription(request.description()); + role.setCreateTime(now); + role.setUpdateTime(now); + sysRoleMapper.insert(role); + return roleConverter.toResponse(role); + } + + @Override + public RoleResponse update(Integer id, RoleRequest request) { + SysRole role = requireRole(id); + checkNameConflict(request.name(), id); + role.setName(request.name()); + role.setDescription(request.description()); + sysRoleMapper.update(role); + return roleConverter.toResponse(role); + } + + @Override + @Transactional + public void delete(Integer id) { + requireRole(id); + if (id == ADMIN_ROLE_ID) { + throw BusinessException.of(ErrorCode.FORBIDDEN, "不能删除管理员角色"); + } + roleTablePermissionMapper.deleteByRoleId(id); + roleHiddenColumnMapper.deleteByRoleId(id); + sysRoleMapper.deleteById(id); + } + + @Override + public List getPermissions(Integer roleId) { + List perms = roleTablePermissionMapper.selectByRoleId(roleId); + return perms.stream() + .collect( + Collectors.groupingBy( + RoleTablePermission::getDatasourceId, + Collectors.mapping( + RoleTablePermission::getTableName, Collectors.toList()))) + .entrySet() + .stream() + .map(e -> new TablePermissionResponse(e.getKey(), e.getValue())) + .toList(); + } + + @Override + @Transactional + public void savePermissions(Integer roleId, SaveTablePermissionRequest request) { + requireRole(roleId); + roleTablePermissionMapper.deleteByRoleAndDatasource(roleId, request.datasourceId()); + if (request.tableNames() != null) { + LocalDateTime now = LocalDateTime.now(); + for (String tableName : request.tableNames()) { + RoleTablePermission perm = new RoleTablePermission(); + perm.setRoleId(roleId); + perm.setDatasourceId(request.datasourceId()); + perm.setTableName(tableName); + perm.setCreateTime(now); + roleTablePermissionMapper.insert(perm); + } + } + } + + @Override + public List getColumnPermissions( + Integer roleId, Integer datasourceId) { + List perms = + roleHiddenColumnMapper.selectByRoleAndDatasource(roleId, datasourceId); + return perms.stream() + .collect( + Collectors.groupingBy( + RoleHiddenColumn::getTableName, + Collectors.mapping( + RoleHiddenColumn::getColumnName, Collectors.toList()))) + .entrySet() + .stream() + .map(e -> new ColumnPermissionResponse(e.getKey(), e.getValue())) + .toList(); + } + + @Override + @Transactional + public void saveColumnPermissions(Integer roleId, SaveColumnPermissionRequest request) { + requireRole(roleId); + roleHiddenColumnMapper.deleteByRoleDatasourceAndTable( + roleId, request.datasourceId(), request.tableName()); + if (request.columnNames() != null && !request.columnNames().isEmpty()) { + LocalDateTime now = LocalDateTime.now(); + for (String columnName : request.columnNames()) { + RoleHiddenColumn perm = new RoleHiddenColumn(); + perm.setRoleId(roleId); + perm.setDatasourceId(request.datasourceId()); + perm.setTableName(request.tableName()); + perm.setColumnName(columnName); + perm.setCreateTime(now); + roleHiddenColumnMapper.insert(perm); + } + } + } + + private void checkNameConflict(String name, Integer excludeId) { + List all = sysRoleMapper.selectAll(); + for (SysRole role : all) { + if (role.getName().equals(name) && !role.getId().equals(excludeId)) { + throw BusinessException.of(ErrorCode.DATA_CONFLICT, "角色名 '" + name + "' 已存在"); + } + } + } + + private SysRole requireRole(Integer id) { + SysRole role = sysRoleMapper.selectById(id); + if (role == null) { + throw BusinessException.of(ErrorCode.RESOURCE_NOT_FOUND, "角色不存在"); + } + return role; + } +} diff --git a/data-agent-backend/src/main/resources/mapper/RoleHiddenColumnMapper.xml b/data-agent-backend/src/main/resources/mapper/RoleHiddenColumnMapper.xml new file mode 100644 index 0000000..de3fa37 --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/RoleHiddenColumnMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + INSERT INTO role_hidden_column (role_id, datasource_id, table_name, column_name, create_time) + VALUES (#{roleId}, #{datasourceId}, #{tableName}, #{columnName}, #{createTime}) + + + + DELETE FROM role_hidden_column + WHERE role_id = #{roleId} + AND datasource_id = #{datasourceId} + AND table_name = #{tableName} + + + + DELETE FROM role_hidden_column WHERE role_id = #{roleId} + + + diff --git a/data-agent-backend/src/main/resources/mapper/RoleTablePermissionMapper.xml b/data-agent-backend/src/main/resources/mapper/RoleTablePermissionMapper.xml new file mode 100644 index 0000000..57f574c --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/RoleTablePermissionMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + INSERT INTO role_table_permission (role_id, datasource_id, table_name, create_time) + VALUES (#{roleId}, #{datasourceId}, #{tableName}, #{createTime}) + + + + DELETE FROM role_table_permission WHERE role_id = #{roleId} AND datasource_id = #{datasourceId} + + + + DELETE FROM role_table_permission WHERE role_id = #{roleId} + + + diff --git a/data-agent-backend/src/main/resources/mapper/SysRoleMapper.xml b/data-agent-backend/src/main/resources/mapper/SysRoleMapper.xml new file mode 100644 index 0000000..6cc3893 --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/SysRoleMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + INSERT INTO sys_role (name, description, create_time, update_time) + VALUES (#{name}, #{description}, #{createTime}, #{updateTime}) + + + + UPDATE sys_role + + name = #{name}, + description = #{description}, + + WHERE id = #{id} + + + + DELETE FROM sys_role WHERE id = #{id} + + + + + diff --git a/data-agent-frontend/src/api/sysRole.ts b/data-agent-frontend/src/api/sysRole.ts new file mode 100644 index 0000000..a1d4d5a --- /dev/null +++ b/data-agent-frontend/src/api/sysRole.ts @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ + +import request from './request'; + +export interface RoleResponse { + id: number; + name: string; + description: string; + createTime: string; +} + +export interface RoleRequest { + name: string; + description: string; +} + +export interface TablePermissionResponse { + datasourceId: number; + tableNames: string[]; +} + +export interface SaveTablePermissionRequest { + datasourceId: number; + tableNames: string[]; +} + +export interface ColumnPermissionResponse { + tableName: string; + columnNames: string[]; +} + +export interface SaveColumnPermissionRequest { + datasourceId: number; + tableName: string; + columnNames: string[]; +} + +type ApiResult = { code: number; message: string; data: T }; + +export function listRoles() { + return request.get>('/sys/role').then(res => res.data.data); +} + +export function createRole(payload: RoleRequest) { + return request.post>('/sys/role', payload).then(res => res.data.data); +} + +export function updateRole(id: number, payload: RoleRequest) { + return request + .put>(`/sys/role/${id}`, payload) + .then(res => res.data.data); +} + +export function deleteRole(id: number) { + return request.delete>(`/sys/role/${id}`).then(res => res.data.data); +} + +export function getPermissions(roleId: number) { + return request + .get>(`/sys/role/${roleId}/permissions`) + .then(res => res.data.data); +} + +export function savePermissions(roleId: number, payload: SaveTablePermissionRequest) { + return request + .put>(`/sys/role/${roleId}/permissions`, payload) + .then(res => res.data.data); +} + +export function getColumnPermissions(roleId: number, datasourceId: number) { + return request + .get>(`/sys/role/${roleId}/columns`, { + params: { datasourceId }, + }) + .then(res => res.data.data); +} + +export function saveColumnPermissions(roleId: number, payload: SaveColumnPermissionRequest) { + return request + .put>(`/sys/role/${roleId}/columns`, payload) + .then(res => res.data.data); +} + +/** 一次性获取数据源下所有表的列名,避免 N 次请求。 */ +export function getAllTableColumns(datasourceId: number) { + return request + .get>>(`/datasource/${datasourceId}/columns`) + .then(res => res.data.data); +} diff --git a/data-agent-frontend/src/components/layout/AppSidebar.vue b/data-agent-frontend/src/components/layout/AppSidebar.vue index 8f5d6b2..7b10d20 100644 --- a/data-agent-frontend/src/components/layout/AppSidebar.vue +++ b/data-agent-frontend/src/components/layout/AppSidebar.vue @@ -31,6 +31,7 @@ { path: '/report', title: '报告管理', icon: 'Document' }, { path: '/metric', title: '指标口径管理', icon: 'DataLine' }, { path: '/sys-user', title: '用户管理', icon: 'User' }, + { path: '/sys-role', title: '角色管理', icon: 'Avatar' }, ]; const activeMenu = computed(() => route.path); diff --git a/data-agent-frontend/src/router/index.ts b/data-agent-frontend/src/router/index.ts index 7264f69..acf89ee 100644 --- a/data-agent-frontend/src/router/index.ts +++ b/data-agent-frontend/src/router/index.ts @@ -73,6 +73,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/sys-user/UserManage.vue'), meta: { title: '用户管理' }, }, + { + path: '/sys-role', + name: 'RoleManage', + component: () => import('@/views/sys-role/RoleManage.vue'), + meta: { title: '角色管理' }, + }, ]; const router = createRouter({ diff --git a/data-agent-frontend/src/views/sys-role/RoleManage.vue b/data-agent-frontend/src/views/sys-role/RoleManage.vue new file mode 100644 index 0000000..8badda9 --- /dev/null +++ b/data-agent-frontend/src/views/sys-role/RoleManage.vue @@ -0,0 +1,371 @@ + + + + + + + diff --git a/data-agent-frontend/src/views/sys-user/UserManage.vue b/data-agent-frontend/src/views/sys-user/UserManage.vue index f44979b..5b5804b 100644 --- a/data-agent-frontend/src/views/sys-user/UserManage.vue +++ b/data-agent-frontend/src/views/sys-user/UserManage.vue @@ -20,10 +20,15 @@ import { ref, reactive, onMounted } from 'vue'; import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'; import * as sysUserApi from '@/api/sysUser'; + import { listRoles, type RoleResponse } from '@/api/sysRole'; import type { UserResponse } from '@/api/sysUser'; const loading = ref(false); const users = ref([]); + const roleOptions = ref([]); + function roleName(roleId: number) { + return roleOptions.value.find(r => r.id === roleId)?.name || String(roleId); + } async function reload() { loading.value = true; @@ -148,7 +153,14 @@ } } - onMounted(reload); + onMounted(() => { + reload(); + listRoles() + .then(list => { + roleOptions.value = list; + }) + .catch(() => {}); + });