feat(security): 实现基于 Spring WebFlux 的用户认证和权限管理

- 新增 ReactiveUserDetailsService 实现类 FluxUserDetailServiceImpl- 添加 MenuFluxRepository、RoleFluxRepository、UserFluxRepository 和 UserRoleFluxRepository 接口
- 实现 SpringWebFluxSecurityConfig 配置类
- 在 V1ChatController、V1DatasetController、V1ServerController 和 V1WorkflowController 中添加权限控制注解
- 更新 DeptService接口,新增 listFluxTree 方法
This commit is contained in:
zhuangtianxiang 2025-04-28 00:09:31 +08:00
parent 9b72d5deb0
commit 54d4e14761
18 changed files with 360 additions and 37 deletions

View File

@ -119,6 +119,15 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<version>0.8.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>

View File

@ -6,10 +6,12 @@ import com.zsc.edu.dify.modules.system.entity.*;
import com.zsc.edu.dify.modules.system.repo.*;
import com.zsc.edu.dify.modules.system.service.DeptService;
import com.zsc.edu.dify.modules.system.service.RoleService;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

View File

@ -24,7 +24,6 @@ import javax.sql.DataSource;
/**
* @author harry_yao
*/
//@EnableWebFluxSecurity
@AllArgsConstructor
@EnableMethodSecurity
@Configuration

View File

@ -0,0 +1,83 @@
package com.zsc.edu.dify.framework.security.flux;
import com.zsc.edu.dify.common.util.TreeUtil;
import com.zsc.edu.dify.exception.StateException;
import com.zsc.edu.dify.framework.security.UserDetailsImpl;
import com.zsc.edu.dify.modules.system.entity.Dept;
import com.zsc.edu.dify.modules.system.entity.Menu;
import com.zsc.edu.dify.modules.system.entity.Role;
import com.zsc.edu.dify.modules.system.entity.UserRole;
import com.zsc.edu.dify.modules.system.repo.flux.*;
import com.zsc.edu.dify.modules.system.service.DeptService;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author zhuang
*/
@AllArgsConstructor
@Service
public class FluxUserDetailServiceImpl implements ReactiveUserDetailsService {
private final UserFluxRepository userRepo;
private final MenuFluxRepository menuRepository;
private final RoleFluxRepository roleFluxRepository;
private final UserRoleFluxRepository userRoleFluxRepository;
private final DeptService deptService;
@Override
public Mono<UserDetails> findByUsername(String username) {
return userRepo.findByUsername(username)
.flatMap(user -> {
if (!user.getEnableState()) {
return Mono.error(new StateException("用户 '" + username + "' 已被禁用!请联系管理员"));
}
// 先获取UserRole列表并从中提取roleIds
return userRoleFluxRepository.findByUserId(user.getId()).collectList()
.flatMap(userRoles -> Mono.zip(
roleFluxRepository.findByDeptId(user.getDeptId()),
deptService.listFluxTree(user.getDeptId()),
menuRepository.findByRoleId(user.getRoleId())
))
.flatMap(tuple -> {
Role role = (Role) tuple.getT1();
List<Dept> depts = (List<Dept>) tuple.getT2();
List<Menu> menus = tuple.getT3();
user.setRole(role);
Set<Long> dataScopeDeptIds = depts.stream()
.map(Dept::getId)
.collect(Collectors.toSet());
user.setDataScopeDeptIds(dataScopeDeptIds);
Set<String> permissions = menus.stream()
.map(Menu::getPermissions)
.collect(Collectors.toSet());
return Mono.just(UserDetailsImpl.from(user, permissions));
});
});
}
// @Override
// public Mono<UserDetails> findByUsername(String username) {
// return userRepo.findByUsername(username)
// .flatMap(user -> menuRepository.findByRoleId(user.getRoleId())
// .map(ids-> userRoleFluxRepository.findByUserId(user.getId()))
// .map(roles-> roleFluxRepository.findByIdIn(ids))
// .map(menus -> {
// Set<String> permissions = menus.stream().map(Menu::getPermissions).collect(Collectors.toSet());
// return UserDetailsImpl.from(user, permissions);
// })
// );
// }
}

View File

@ -0,0 +1,102 @@
package com.zsc.edu.dify.framework.security.flux;
import com.zsc.edu.dify.framework.security.*;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import reactor.core.publisher.Mono;
import javax.sql.DataSource;
@EnableWebFluxSecurity
@AllArgsConstructor
@Configuration
public class SpringWebFluxSecurityConfig {
private final ReactiveUserDetailsService userDetailsService;
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final SecurityBeanConfig securityBeanConfig;
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authenticationManager.setPasswordEncoder(securityBeanConfig.passwordEncoder());
return authenticationManager;
}
@Bean
public ReactiveAuthorizationManager<AuthorizationContext> reactiveAuthorizationManager() {
return (authenticationMono, context) -> authenticationMono
.flatMap(authentication ->
userDetailsService.findByUsername(authentication.getName())
.map(userDetails -> {
SecurityUtil.setUserInfo((UserDetailsImpl) userDetails);
return new AuthorizationDecision(true);
})
)
.defaultIfEmpty(new AuthorizationDecision(false));
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// 配置认证过滤器
AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(reactiveAuthenticationManager());
authenticationFilter.setAuthenticationSuccessHandler((ServerAuthenticationSuccessHandler) customAuthenticationSuccessHandler);
authenticationFilter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler((ServerAuthenticationEntryPoint) customAuthenticationEntryPoint));
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/v1/flux/**").access(reactiveAuthorizationManager()) // 通过自定义权限管理器进行权限校验
.anyExchange().authenticated()
)
.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.exceptionHandling(handling -> handling
.authenticationEntryPoint((ServerAuthenticationEntryPoint) customAuthenticationEntryPoint)
.accessDeniedHandler((ServerAccessDeniedHandler) customAccessDeniedHandler)
)
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/v1/flux/**", config);
return source;
}
}

View File

@ -9,6 +9,7 @@ import io.github.guoshiqiufeng.dify.chat.dto.response.MessageConversationsRespon
import io.github.guoshiqiufeng.dify.core.pojo.DifyPageResult;
import jakarta.annotation.Resource;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@ -34,7 +35,8 @@ public class V1ChatController {
* apikey 建议在数据库进行存储前端调用时传智能体 id从数据库查询
*/
@PostMapping(value = "/completions/{appId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatMessageSendCompletionResponse> sendChatMessageStream(@RequestBody ChatMessageSendRequest sendRequest,@PathVariable String appId) {
@PreAuthorize("hasAuthority('difyChat:query')")
public Flux<ChatMessageSendCompletionResponse> sendChatMessageStream(@RequestBody ChatMessageSendRequest sendRequest, @PathVariable String appId) {
sendRequest.setApiKey(appEntityRepository.selectApiKey(appId));
return difyChat.sendChatMessageStream(sendRequest);
}
@ -46,6 +48,7 @@ public class V1ChatController {
* @return 会话列表
*/
@PostMapping("/conversations")
@PreAuthorize("hasAuthority('difyChat:query')")
public DifyPageResult<MessageConversationsResponse> conversations(@RequestBody MessageConversationsRequest request) {
request.setApiKey("app-mM2UGTE5QVPLCwGvwifnV0g7");
return difyChat.conversations(request);
@ -58,6 +61,7 @@ public class V1ChatController {
* @param userId 用户id
*/
@PatchMapping("/stopMessagesStream")
@PreAuthorize("hasAuthority('difyChat:update')")
public void stopMessagesStream( String taskId, String userId) {
difyChat.stopMessagesStream("app-mM2UGTE5QVPLCwGvwifnV0g7", taskId, userId);
}
@ -69,6 +73,7 @@ public class V1ChatController {
* @param userId 用户id
*/
@DeleteMapping("/messages/suggested")
@PreAuthorize("hasAuthority('difyChat:delete')")
public void deleteConversation(String conversationId, String userId) {
difyChat.deleteConversation(conversationId, "app-mM2UGTE5QVPLCwGvwifnV0g7", userId);
}

View File

@ -5,6 +5,7 @@ import io.github.guoshiqiufeng.dify.dataset.DifyDataset;
import io.github.guoshiqiufeng.dify.dataset.dto.request.*;
import io.github.guoshiqiufeng.dify.dataset.dto.response.*;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@ -20,6 +21,7 @@ public class V1DatasetController {
* @return
*/
@PostMapping("/page")
@PreAuthorize("hasAuthority('difyDataSet:query')")
public DifyPageResult<DatasetResponse> page(@RequestBody DatasetPageRequest request){
request.setApiKey("dataset-kN5WTJ8jR877YfN1A34JceVg");
return difyDataset.page(request);
@ -32,6 +34,7 @@ public class V1DatasetController {
* @return
*/
@PostMapping("/createDocumentByFile")
@PreAuthorize("hasAuthority('difyDataSet:create')")
public DocumentCreateResponse createDocumentByFile(DocumentCreateByFileRequest request){
return difyDataset.createDocumentByFile(request);
}
@ -43,6 +46,7 @@ public class V1DatasetController {
* @return
*/
@GetMapping("/pageDocument")
@PreAuthorize("hasAuthority('difyDataSet:query')")
public DifyPageResult<DocumentInfo> pageDocument(@RequestBody DatasetPageDocumentRequest request){
request.setApiKey("dataset-kN5WTJ8jR877YfN1A34JceVg");
return difyDataset.pageDocument(request);
@ -56,6 +60,7 @@ public class V1DatasetController {
* @return
*/
@PostMapping("/uploadFileInfo")
@PreAuthorize("hasAuthority('difyDataSet:query')")
public UploadFileInfoResponse uploadFileInfo(String datasetId, String documentId){
return difyDataset.uploadFileInfo(datasetId, documentId,"dataset-kN5WTJ8jR877YfN1A34JceVg");
}
@ -68,6 +73,7 @@ public class V1DatasetController {
* @return
*/
@DeleteMapping("/deleteDocument")
@PreAuthorize("hasAuthority('difyDataSet:delete')")
public DocumentDeleteResponse deleteDocument(String datasetId, String documentId){
return difyDataset.deleteDocument(datasetId, documentId, "app-mM2UGTE5QVPLCwGvwifnV0g7");
}
@ -79,6 +85,7 @@ public class V1DatasetController {
* @return
*/
@PostMapping("/createSegment")
@PreAuthorize("hasAuthority('difyDataSet:create')")
public SegmentResponse createSegment(@RequestBody SegmentCreateRequest request){
return difyDataset.createSegment(request);
}
@ -90,6 +97,7 @@ public class V1DatasetController {
* @return
*/
@PostMapping("/createSegmentChildChunk")
@PreAuthorize("hasAuthority('difyDataSet:create')")
public SegmentChildChunkCreateResponse createSegmentChildChunk(@RequestBody SegmentChildChunkCreateRequest request){
return difyDataset.createSegmentChildChunk(request);
}
@ -101,6 +109,7 @@ public class V1DatasetController {
* @return
*/
@GetMapping("/retrieve")
@PreAuthorize("hasAuthority('difyDataSet:query')")
public RetrieveResponse retrieve(@RequestBody RetrieveRequest request){
return difyDataset.retrieve(request);
}

View File

@ -10,6 +10,7 @@ import io.github.guoshiqiufeng.dify.server.dto.response.AppsResponseVO;
import io.github.guoshiqiufeng.dify.server.dto.response.DatasetApiKeyResponseVO;
import jakarta.annotation.Resource;
import org.apache.ibatis.annotations.Param;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@ -30,6 +31,7 @@ public class V1ServerController {
* @return
*/
@GetMapping("/apps")
@PreAuthorize("hasAuthority('difyServer:query')")
public List<AppsResponseVO> getApps(String mode, String name) {
return difyServerService.getApps(mode, name);
}
@ -40,6 +42,7 @@ public class V1ServerController {
* @return
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('difyServer:query')")
public AppsResponseVO getApp(@PathVariable("id") String id) {
return difyServer.app(id);
}
@ -50,6 +53,7 @@ public class V1ServerController {
* @return
*/
@GetMapping("/api-key/{id}")
@PreAuthorize("hasAuthority('difyServer:query')")
public List<ApiKeyResponseVO> getAppApiKey(@PathVariable("id") String id) {
return difyServer.getAppApiKey(id);
}
@ -60,6 +64,7 @@ public class V1ServerController {
* @return
*/
@PostMapping("/api-key/init/{id}")
@PreAuthorize("hasAuthority('difyServer:create')")
public List<ApiKeyResponseVO> initAppApiKey(@PathVariable("id") String id) {
return difyServer.initAppApiKey(id);
}
@ -69,6 +74,7 @@ public class V1ServerController {
* @return
*/
@GetMapping("/api-key/dataset")
@PreAuthorize("hasAuthority('difyServer:query')")
public List<DatasetApiKeyResponseVO> getDatasetApiKey() {
return difyServer.getDatasetApiKey();
}
@ -78,6 +84,7 @@ public class V1ServerController {
* @return
*/
@PostMapping("/api-key/dataset/init")
@PreAuthorize("hasAuthority('difyServer:create')")
public List<DatasetApiKeyResponseVO> initDatasetApiKey() {
return difyServer.initDatasetApiKey();
}
@ -89,6 +96,7 @@ public class V1ServerController {
* @return
*/
@PostMapping("/app/{id}")
@PreAuthorize("hasAuthority('difyServer:update')")
public boolean enabledApp(@PathVariable String id) {
return difyServerService.enabledApp(id);
}
@ -98,6 +106,7 @@ public class V1ServerController {
* @return
*/
@GetMapping("/apps/able")
@PreAuthorize("hasAuthority('difyServer:query')")
public List<AppEntity> getAbleApps() {
LambdaQueryWrapper<AppEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AppEntity::isEnabled, true);

View File

@ -6,6 +6,7 @@ import io.github.guoshiqiufeng.dify.workflow.dto.request.WorkflowLogsRequest;
import io.github.guoshiqiufeng.dify.workflow.dto.request.WorkflowRunRequest;
import io.github.guoshiqiufeng.dify.workflow.dto.response.*;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@ -23,6 +24,7 @@ public class V1WorkflowController {
* @return
*/
@PostMapping("/run")
@PreAuthorize("hasAuthority('difyWorkFlow:query')")
public WorkflowRunResponse runWorkflow(@RequestBody WorkflowRunRequest request) {
request.setApiKey("app-ZpkQM6yy767oUTfNSBYq65nB");
return difyWorkflow.runWorkflow(request);
@ -59,6 +61,7 @@ public class V1WorkflowController {
* @return
*/
@GetMapping("/info")
@PreAuthorize("hasAuthority('difyWorkFlow:query')")
public WorkflowInfoResponse info(String workflowRunId) {
return difyWorkflow.info(workflowRunId, "app-ZpkQM6yy767oUTfNSBYq65nB");
}
@ -70,6 +73,7 @@ public class V1WorkflowController {
* @return
*/
@PostMapping("/logs")
@PreAuthorize("hasAuthority('difyWorkFlow:query')")
public DifyPageResult<WorkflowLogs> logs(@RequestBody WorkflowLogsRequest request) {
request.setApiKey("app-ZpkQM6yy767oUTfNSBYq65nB");
return difyWorkflow.logs(request);

View File

@ -1,7 +1,7 @@
package com.zsc.edu.dify.modules.system.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.*;
import java.io.Serializable;
@ -10,6 +10,7 @@ import java.io.Serializable;
* @author zhuang
*/
@Data
@Getter
@TableName("sys_users_roles")
public class UserRole implements Serializable {
/**
@ -20,5 +21,7 @@ public class UserRole implements Serializable {
/**
* 角色ID
*/
@Getter
private Long roleId;
}

View File

@ -0,0 +1,16 @@
package com.zsc.edu.dify.modules.system.repo.flux;
import com.zsc.edu.dify.modules.system.entity.Menu;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@Repository
public interface MenuFluxRepository extends ReactiveCrudRepository<Menu,Long> {
@Query("select * from sys_menu where id in (select menu_id from sys_roles_menus where role_id = :roleId)")
Mono<List<Menu>> findByRoleId(Long roleId);
}

View File

@ -0,0 +1,15 @@
package com.zsc.edu.dify.modules.system.repo.flux;
import com.zsc.edu.dify.modules.system.entity.Role;
import org.mapstruct.Mapper;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
import java.util.List;
@Repository
public interface RoleFluxRepository extends ReactiveCrudRepository<Role,Long> {
Mono<?> findByDeptId(Long id);
}

View File

@ -0,0 +1,12 @@
package com.zsc.edu.dify.modules.system.repo.flux;
import com.zsc.edu.dify.modules.system.entity.User;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
@Repository
public interface UserFluxRepository extends ReactiveCrudRepository<User,Long> {
Mono<User> findByUsername(String username);
}

View File

@ -0,0 +1,16 @@
package com.zsc.edu.dify.modules.system.repo.flux;
import com.zsc.edu.dify.modules.system.entity.UserRole;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@Repository
public interface UserRoleFluxRepository extends ReactiveCrudRepository<UserRole,Long> {
Flux<List<Long>> findByUserId(Long userId);
}

View File

@ -3,6 +3,7 @@ package com.zsc.edu.dify.modules.system.service;
import com.zsc.edu.dify.modules.system.dto.DeptDto;
import com.zsc.edu.dify.modules.system.entity.Dept;
import com.baomidou.mybatisplus.extension.service.IService;
import reactor.core.publisher.Mono;
import java.util.List;
@ -31,8 +32,11 @@ public interface DeptService extends IService<Dept> {
Boolean toggle(Long id);
Mono<?> listFluxTree(Long deptId);
/**
* 生成部门树结构
*
* @param id
* @return
*/

View File

@ -11,6 +11,8 @@ import com.zsc.edu.dify.modules.system.service.DeptService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.ArrayList;
import java.util.List;
@ -57,6 +59,40 @@ public class DeptServiceImpl extends ServiceImpl<DeptRepository, Dept> implement
return updateById(dept);
}
@Override
public Mono<List<Dept>> listFluxTree(Long deptId) {
// 1. 将同步数据库查询改为响应式查询假设 baseMapper 支持响应式
return Mono.fromCallable(() -> baseMapper.selectDeptTree())
.subscribeOn(Schedulers.boundedElastic())
.flatMap(deptTrees -> {
// 2. 构建部门树TreeUtil.makeTree 如果是计算密集型需在弹性线程执行
List<Dept> deptTree = TreeUtil.makeTree(
deptTrees,
department -> department.getPid() == null || department.getPid() == -1L,
(parent, child) -> parent.getId().equals(child.getPid()),
Dept::setChildren
);
// 3. 根据 deptId 条件分支处理
if (deptId == null) {
return Mono.just(deptTree);
} else if (deptId == 0L) {
return Mono.just(deptTree);
} else {
// 4. 使用响应式方式过滤指定部门及其子部门
return Mono.fromCallable(() -> {
List<Dept> deptChildrenTree = new ArrayList<>();
TreeUtil.forLevelOrder(deptTree, node -> {
if (node.getId().equals(deptId)) {
deptChildrenTree.add(node);
}
}, Dept::getChildren);
return deptChildrenTree.isEmpty() ? deptTree : deptChildrenTree;
}).subscribeOn(Schedulers.boundedElastic());
}
});
}
@Override
public List<Dept> listTree(Long deptId) {
List<Dept> deptTrees = baseMapper.selectDeptTree();
@ -84,5 +120,4 @@ public class DeptServiceImpl extends ServiceImpl<DeptRepository, Dept> implement
}
return deptTree;
}
}

View File

@ -11,6 +11,12 @@ mybatis-plus:
map-underscore-to-camel-case: true
spring:
main:
allow-bean-definition-overriding: true
r2dbc:
url: r2dbc:postgresql://43.139.10.64:15432/dify?ssl=false&TimeZone=Asia/Shanghai
username: gitea
password: gitea
datasource:
url: jdbc:postgresql://43.139.10.64:15432/dify?ssl=false&TimeZone=Asia/Shanghai
username: gitea
@ -64,4 +70,5 @@ dify:
email: 2913129173@qq.com # 请替换为实际的 Dify 服务邮箱,若不需要调用 server相关接口可不填
password: tian14384, # 请替换为实际的 Dify 服务密码,若不需要调用 server相关接口可不填
dataset:
api-key: dataset-kN5WTJ8jR877YfN1A34JceVg # 请替换为实际的知识库api-key, 若不需要调用知识库可不填
api-key: dataset-kN5WTJ8jR877YfN1A34JceVg # 请替换为实际的知识库api-key, 若不需要调用知识库可不填

View File

@ -31,9 +31,9 @@ class MenuServiceImplTest {
Menu system = new Menu(null, Menu.Type.PAGE, "System", "/system", "menu.system", "icon-computer", true, false, 2, "system", "");
Menu user = new Menu(null, Menu.Type.PAGE, "User", "/user", "menu.user", "icon-user", true, false, 3, "user", "");
Menu message = new Menu(null, Menu.Type.PAGE, "Message", "/message", "消息管理", "icon-message", true, false, 4, "message", "");
Menu iot = new Menu(null, Menu.Type.PAGE, "Iot", "/iot", "物联网管理", "icon-iot", true, false, 5, "iot", "");
Menu operationLog = new Menu(null, Menu.Type.PAGE, "OperationLog", "/operationLog", "操作日志", "icon-log", true, false, 6, "operationLog", "");
menuService.saveBatch(List.of(dashboard, system, user, message, iot, operationLog));
Menu difyProxyBackend = new Menu(null, Menu.Type.PAGE, "DiFyProxyBackend", "/diFyProxyBackend", "dify", "icon-dify", true, false, 5, "dify", "");
menuService.saveBatch(List.of(dashboard, system, user, message, operationLog,difyProxyBackend));
Menu workplace = new Menu(dashboard.getId(), Menu.Type.PAGE, "Workplace", "workplace", "工作台", "icon-dashboard", true, false, 1, "dashboard:workplace", "");
Menu role = new Menu(system.getId(), Menu.Type.PAGE, "Role", "role", "角色管理", null, true, false, 1, "system:role", "");
Menu dept = new Menu(system.getId(), Menu.Type.PAGE, "Dept", "dept", "部门管理", null, true, false, 2, "system:dept", "");
@ -42,11 +42,9 @@ class MenuServiceImplTest {
Menu menu = new Menu(system.getId(), Menu.Type.PAGE, "Menu", "menu", "菜单管理", null, true, false, 5, "system:menu", "");
Menu notice = new Menu(message.getId(), Menu.Type.PAGE, "Notice", "notice", "通知管理", null, true, false, 1, "message:notice", "");
Menu bulletin = new Menu(message.getId(), Menu.Type.PAGE, "Bulletin", "bulletin", "公告管理", null, true, false, 2, "message:bulletin", "");
Menu device = new Menu(iot.getId(), Menu.Type.PAGE, "Device", "device", "设备管理", null, true, false, 1, "iot:device", "");
Menu product = new Menu(iot.getId(), Menu.Type.PAGE, "Product", "product", "产品管理", null, true, false, 2, "iot:product", "");
Menu tsl = new Menu(product.getId(), Menu.Type.PAGE, "Tsl", "tsl", "物模型管理", null, true, true, 3, "iot:tsl", "");
Menu log = new Menu(operationLog.getId(), Menu.Type.PAGE, "OperationLog", "operationLog", "操作日志", null, true, false, 1, "operationLog", "");
menuService.saveBatch(List.of(workplace, dept, users, authority, menu, notice, bulletin, device, product, tsl, role, log));
Menu dify = new Menu(difyProxyBackend.getId(), Menu.Type.PAGE, "Dify", "dify", "dify", null, true, false, 1, "dify", "");
menuService.saveBatch(List.of(workplace, dept, users, authority, menu, notice, bulletin,role, log, dify));
Menu roleCreate = new Menu(role.getId(), Menu.Type.OPERATION, "roleCreate", null, "角色新增", null, true, false, 1, "system:role:create", "");
Menu roleDelete = new Menu(role.getId(), Menu.Type.OPERATION, "roleDelete", null, "角色删除", null, true, false, 1, "system:role:delete", "");
Menu roleUpdate = new Menu(role.getId(), Menu.Type.OPERATION, "roleUpdate", null, "角色修改", null, true, false, 1, "system:role:update", "");
@ -71,40 +69,35 @@ class MenuServiceImplTest {
Menu bulletinUpdate = new Menu(bulletin.getId(), Menu.Type.OPERATION, "bulletinUpdate", null, "公告修改", null, true, false, 1, "message:bulletin:update", "");
Menu bulletinQuery = new Menu(bulletin.getId(), Menu.Type.OPERATION, "bulletinQuery", null, "公告查询", null, true, false, 1, "message:bulletin:query", "");
Menu bulletinDelete = new Menu(bulletin.getId(), Menu.Type.OPERATION, "bulletinDelete", null, "公告删除", null, true, false, 1, "message:bulletin:delete", "");
Menu deviceQuery = new Menu(device.getId(), Menu.Type.OPERATION, "deviceQuery", null, "设备查询", null, true, false, 1, "iot:device:query", "");
Menu deviceCreate = new Menu(device.getId(), Menu.Type.OPERATION, "deviceCreate", null, "设备新增", null, true, false, 1, "iot:device:create", "");
Menu deviceUpdate = new Menu(device.getId(), Menu.Type.OPERATION, "deviceUpdate", null, "设备修改", null, true, false, 1, "iot:device:update", "");
Menu deviceDelete = new Menu(device.getId(), Menu.Type.OPERATION, "deviceDelete", null, "设备删除", null, true, false, 1, "iot:device:delete", "");
Menu productQuery = new Menu(product.getId(), Menu.Type.OPERATION, "productQuery", null, "产品查询", null, true, false, 1, "iot:product:query", "");
Menu productCreate = new Menu(product.getId(), Menu.Type.OPERATION, "productCreate", null, "产品新增", null, true, false, 1, "iot:product:create", "");
Menu productUpdate = new Menu(product.getId(), Menu.Type.OPERATION, "productUpdate", null, "产品修改", null, true, false, 1, "iot:product:update", "");
Menu productDelete = new Menu(product.getId(), Menu.Type.OPERATION, "productDelete", null, "产品删除", null, true, false, 1, "iot:product:delete", "");
Menu eventQuery = new Menu(tsl.getId(), Menu.Type.OPERATION, "eventQuery", null, "事件查询", null, true, false, 1, "iot:event:query", "");
Menu eventCreate = new Menu(tsl.getId(), Menu.Type.OPERATION, "eventCreate", null, "事件新增", null, true, false, 1, "iot:event:create", "");
Menu eventUpdate = new Menu(tsl.getId(), Menu.Type.OPERATION, "eventUpdate", null, "事件修改", null, true, false, 1, "iot:event:update", "");
Menu eventDelete = new Menu(tsl.getId(), Menu.Type.OPERATION, "eventDelete", null, "事件删除", null, true, false, 1, "iot:event:delete", "");
Menu propertyQuery = new Menu(tsl.getId(), Menu.Type.OPERATION, "propertyQuery", null, "属性查询", null, true, false, 1, "iot:property:query", "");
Menu propertyCreate = new Menu(tsl.getId(), Menu.Type.OPERATION, "propertyCreate", null, "属性新增", null, true, false, 1, "iot:property:create", "");
Menu propertyUpdate = new Menu(tsl.getId(), Menu.Type.OPERATION, "propertyUpdate", null, "属性修改", null, true, false, 1, "iot:property:update", "");
Menu propertyDelete = new Menu(tsl.getId(), Menu.Type.OPERATION, "propertyDelete", null, "属性删除", null, true, false, 1, "iot:property:delete", "");
Menu serverQuery = new Menu(tsl.getId(), Menu.Type.OPERATION, "serverQuery", null, "服务查询", null, true, false, 1, "iot:server:query", "");
Menu serverCreate = new Menu(tsl.getId(), Menu.Type.OPERATION, "serverCreate", null, "服务新增", null, true, false, 1, "iot:server:create", "");
Menu serverUpdate = new Menu(tsl.getId(), Menu.Type.OPERATION, "serverUpdate", null, "服务修改", null, true, false, 1, "iot:server:update", "");
Menu serverDelete = new Menu(tsl.getId(), Menu.Type.OPERATION, "serverDelete", null, "服务删除", null, true, false, 1, "iot:server:delete", "");
Menu operationLogQuery = new Menu(operationLog.getId(), Menu.Type.OPERATION, "operationLogQuery", null, "操作日志查询", null, true, false, 1, "operationLog:query", "");
Menu operationLogDelete = new Menu(operationLog.getId(), Menu.Type.OPERATION, "operationLogDelete", null, "操作日志删除", null, true, false, 1, "operationLog:delete", "");
Menu difyChatQuery = new Menu(dify.getId(), Menu.Type.OPERATION, "difyChatQuery", null, "difyChat查询", null, true, false, 1, "difyChat:query", "");
Menu difyChatCreate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyChatCreate", null, "difyChat新增", null, true, false, 1, "difyChat:create", "");
Menu difyChatUpdate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyChatUpdate", null, "difyChat修改", null, true, false, 1, "difyChat:update", "");
Menu difyChatDelete = new Menu(dify.getId(), Menu.Type.OPERATION, "difyChatDelete", null, "difyChat删除", null, true, false, 1, "difyChat:delete", "");
Menu difyServerQuery = new Menu(dify.getId(), Menu.Type.OPERATION, "difyServerQuery", null, "difyServer查询", null, true, false, 1, "difyServer:query", "");
Menu difyServerCreate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyServerCreate", null, "difyServer新增", null, true, false, 1, "difyServer:create", "");
Menu difyServerUpdate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyServerUpdate", null, "difyServer修改", null, true, false, 1, "difyServer:update", "");
Menu difyServerDelete = new Menu(dify.getId(), Menu.Type.OPERATION, "difyServerDelete", null, "difyServer删除", null, true, false, 1, "difyServer:delete", "");
Menu difyDataSetQuery = new Menu(dify.getId(), Menu.Type.OPERATION, "difyDataSetQuery", null, "difyDataSet查询", null, true, false, 1, "difyDataSet:query", "");
Menu difyDataSetCreate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyDataSetCreate", null, "difyDataSet新增", null, true, false, 1, "difyDataSet:create", "");
Menu difyDataSetUpdate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyDataSetUpdate", null, "difyDataSet修改", null, true, false, 1, "difyDataSet:update", "");
Menu difyDataSetDelete = new Menu(dify.getId(), Menu.Type.OPERATION, "difyDataSetDelete", null, "difyDataSet删除", null, true, false, 1, "difyDataSet:delete", "");
Menu difyWorkFlowQuery = new Menu(dify.getId(), Menu.Type.OPERATION, "difyWorkFlowQuery", null, "difyWorkFlow查询", null, true, false, 1, "difyWorkFlow:query", "");
Menu difyWorkFlowCreate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyWorkFlowCreate", null, "difyWorkFlow新增", null, true, false, 1, "difyWorkFlow:create", "");
Menu difyWorkFlowUpdate = new Menu(dify.getId(), Menu.Type.OPERATION, "difyWorkFlowUpdate", null, "difyWorkFlow修改", null, true, false, 1, "difyWorkFlow:update", "");
Menu difyWorkFlowDelete = new Menu(dify.getId(), Menu.Type.OPERATION, "difyWorkFlowDelete", null, "difyWorkFlow删除", null, true, false, 1, "difyWorkFlow:delete", "");
menuService.saveBatch(List.of(roleCreate, roleDelete, roleUpdate, roleQuery,
deptSave, deptUpdate, deptQuery, deptDelete,
userSave, userUpdate, userQuery, userDelete,
menuSave, menuUpdate, menuQuery, menuDelete,
noticeCreate, noticeUpdate, noticeQuery, noticeDelete,
bulletinCreate, bulletinUpdate, bulletinQuery, bulletinDelete,
deviceQuery, deviceCreate, deviceUpdate, deviceDelete,
productQuery, productCreate, productUpdate, productDelete,
eventQuery, eventCreate, eventUpdate, eventDelete,
propertyQuery, propertyCreate, propertyUpdate, propertyDelete,
serverQuery, serverCreate, serverUpdate, serverDelete,
operationLogQuery, operationLogDelete
operationLogQuery, operationLogDelete,
difyChatQuery, difyChatCreate, difyChatUpdate, difyChatDelete,
difyServerQuery, difyServerCreate, difyServerUpdate, difyServerDelete,
difyDataSetQuery, difyDataSetCreate, difyDataSetUpdate, difyDataSetDelete,
difyWorkFlowQuery, difyWorkFlowCreate, difyWorkFlowUpdate, difyWorkFlowDelete
));
}