添加递归查询全国行政区域功能,优化数据库查询性能

1. 新增`getAllChildren`和`getAllChildrenTree`接口,支持根据ID递归查询所有下级区域数据。
2. 实现SQL递归查询和Java递归查询的双重策略,提升查询效率。
3. 新增`DictRegionTreeVO`类,构建树形结构以便于展示层级关系。
4. 更新相关控制器和服务层,完善接口文档和注释。

影响范围:全国行政区域模块,提升数据查询的灵活性和性能。
This commit is contained in:
刘倡
2025-06-21 14:52:53 +08:00
parent 3bf1bda674
commit 71efb5cf0b
19 changed files with 988 additions and 104 deletions

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Java
*.class
*.jar
*.war
*.ear
# Maven
target/
!.mvn/wrapper/maven-wrapper.jar
# Gradle
.gradle/
build/
# IntelliJ IDEA
.idea/
*.iml
*.iws
out/
# VSCode
.vscode/
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
# Others
*.swp
*.swo
# FastRequest plugin
.fastRequest/
# Ignore generated sources
**/generated-sources/
**/generated-test-sources/
# Ignore test classes
**/test-classes/

View File

@ -13,12 +13,14 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DruidDataSourceAutoConfigure.class
// DataSourceAutoConfiguration.class,
// DruidDataSourceAutoConfigure.class
})
@EnableFeignClients(basePackages = {"com.coscoshipping.ebtp.*","com.chinaunicom.mall.ebtp.cloud.*","com.chinaunicom.mall.ebtp.*"})
@MapperScan({"com.coscoshipping.ebtp.project.**.dao","com.chinaunicom.mall.ebtp.**.dao"})
@ComponentScan(basePackages = {"com.coscoshipping.ebtp.*","com.chinaunicom.mall.ebtp.cloud.*","com.chinaunicom.mall.ebtp.*"})
@EnableFeignClients(basePackages = { "com.coscoshipping.ebtp.*", "com.chinaunicom.mall.ebtp.cloud.*",
"com.chinaunicom.mall.ebtp.*" })
@MapperScan({ "com.coscoshipping.ebtp.project.**.dao", "com.chinaunicom.mall.ebtp.**.dao" })
@ComponentScan(basePackages = { "com.coscoshipping.ebtp.*", "com.chinaunicom.mall.ebtp.cloud.*",
"com.chinaunicom.mall.ebtp.*" })
public class SysManagerEbtpProjectApplication {
public static void main(String[] args) {

View File

@ -0,0 +1,225 @@
# 全国行政区域递归查询功能说明
## 功能概述
本模块新增了递归查询功能可以根据传入的ID查询出包括自己在内的所有下级数据。支持两种查询方式
1. 平铺列表查询
2. 树形结构查询
## 性能优化
### 数据库查询优化
- **减少数据库调用次数**:树形结构构建时,先一次性获取所有下级数据,然后在内存中构建树形结构
- **使用Map提高查找效率**构建父ID到子节点的映射避免在列表中循环查找子节点
- **双重实现策略**优先使用SQL递归查询CTE失败时自动降级到Java递归查询
### 性能对比
- **优化前**构建树形结构时每个节点都会查询数据库N个节点需要N次数据库查询
- **优化后**构建树形结构时只需要1次数据库查询获取所有数据然后在内存中处理
## 新增接口
### 1. 递归查询所有下级数据(平铺列表)
**接口地址:** `GET /v1/dictRegion/getAllChildren/{id}`
**功能描述:** 根据传入的ID递归查询所有下级数据包括自己返回平铺的列表结构。
**参数说明:**
- `id` (路径参数): 当前节点ID必填
**返回示例:**
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": "110000",
"pId": "0",
"name": "北京市",
"level": "0",
"ab": "京"
},
{
"id": "110100",
"pId": "110000",
"name": "北京市",
"level": "1",
"ab": "京"
}
]
}
```
### 2. 构建树形结构(性能优化版)
**接口地址:** `GET /v1/dictRegion/getAllChildrenTree/{id}`
**功能描述:** 根据传入的ID构建树形结构包含层级关系和子节点信息。**已优化性能,减少数据库查询次数。**
**参数说明:**
- `id` (路径参数): 当前节点ID必填
**返回示例:**
```json
{
"code": 200,
"message": "success",
"data": {
"id": "110000",
"pId": "0",
"name": "北京市",
"level": "0",
"ab": "京",
"children": [
{
"id": "110100",
"pId": "110000",
"name": "北京市",
"level": "1",
"ab": "京",
"children": [],
"isLeaf": true,
"depth": 1
}
],
"isLeaf": false,
"depth": 0
}
}
```
## 技术实现
### 1. SQL递归查询
使用MySQL的CTECommon Table Expression递归查询
```sql
WITH RECURSIVE region_tree AS (
-- 基础查询:查询当前节点
SELECT id, p_id, name, level, ab
FROM dict_region
WHERE id = #{id}
UNION ALL
-- 递归查询:查询所有子节点
SELECT dr.id, dr.p_id, dr.name, dr.level, dr.ab
FROM dict_region dr
INNER JOIN region_tree rt ON dr.p_id = rt.id
)
SELECT * FROM region_tree
ORDER BY id
```
### 2. Java递归查询备选方案
如果数据库不支持CTE系统会自动降级使用Java递归查询
```java
private List<DictRegion> getAllChildrenRecursive(String parentId) {
List<DictRegion> children = new ArrayList<>();
// 查询直接子节点
QueryWrapper<DictRegion> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("p_id", parentId);
queryWrapper.orderByAsc("id");
List<DictRegion> directChildren = this.list(queryWrapper);
for (DictRegion child : directChildren) {
children.add(child);
// 递归查询子节点的子节点
children.addAll(getAllChildrenRecursive(child.getId()));
}
return children;
}
```
### 3. 优化的树形结构构建
**优化前的问题:**
```java
// 每次递归都会查询数据库
private DictRegionTreeVO buildTreeRecursive(DictRegion node, int depth) {
// 查询子节点 - 每次都会访问数据库
QueryWrapper<DictRegion> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("p_id", node.getId());
List<DictRegion> children = this.list(queryWrapper);
// 递归处理每个子节点
for (DictRegion child : children) {
childNodes.add(buildTreeRecursive(child, depth + 1)); // 再次查询数据库
}
}
```
**优化后的实现:**
```java
// 一次性获取所有数据,在内存中构建树形结构
public DictRegionTreeVO getAllChildrenTree(String id) {
// 1. 获取当前节点
DictRegion current = this.getById(id);
// 2. 一次性获取所有下级数据
List<DictRegion> allChildren = getAllChildrenIncludingSelfByJava(id);
// 3. 构建父ID到子节点的映射提高查找效率
Map<String, List<DictRegion>> parentToChildrenMap = new HashMap<>();
for (DictRegion node : allChildren) {
parentToChildrenMap.computeIfAbsent(node.getPId(), k -> new ArrayList<>()).add(node);
}
// 4. 在内存中构建树形结构
return buildTreeFromMap(current, parentToChildrenMap, 0);
}
```
### 4. 树形结构工具类
提供了`TreeUtil`工具类,支持:
- 将平铺列表转换为树形结构
- 将树形结构转换为平铺列表
- 递归构建树形结构
## 使用示例
### 查询北京市及其所有下级区域
```bash
# 平铺列表查询
GET /v1/dictRegion/getAllChildren/110000
# 树形结构查询(性能优化版)
GET /v1/dictRegion/getAllChildrenTree/110000
```
### 查询上海市及其所有下级区域
```bash
# 平铺列表查询
GET /v1/dictRegion/getAllChildren/310000
# 树形结构查询
GET /v1/dictRegion/getAllChildrenTree/310000
```
## 注意事项
1. **性能考虑:**
- 对于层级较深的数据建议使用SQL递归查询以获得更好的性能
- 树形结构构建已优化从N次数据库查询减少到1次查询
2. **数据完整性:** 确保数据库中的父子关系正确,避免循环引用
3. **内存使用:** 对于大量数据的递归查询,注意内存使用情况
4. **数据库兼容性:** 如果数据库不支持CTE系统会自动使用Java递归查询
## 扩展功能
可以根据需要扩展以下功能:
1. 添加缓存机制提高查询性能
2. 支持按层级深度限制查询
3. 支持按条件过滤子节点
4. 添加批量查询接口
5. 支持异步查询大数据量的树形结构

View File

@ -1,8 +1,9 @@
package com.coscoshipping.ebtp.project.dict.controller;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import com.coscoshipping.ebtp.project.dict.service.IDictRegionService;
import com.chinaunicom.mall.ebtp.common.base.entity.BaseResponse;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import com.coscoshipping.ebtp.project.dict.entity.DictRegionTreeVO;
import com.coscoshipping.ebtp.project.dict.service.IDictRegionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -54,6 +55,17 @@ public class DictRegionController {
return BaseResponse.success(idictRegionService.updateById(dictRegion));
}
/**
* 查询所有数据
*
* @return 返回结果
*/
@ApiOperation("查询所有数据")
@GetMapping("/all")
public BaseResponse<List<DictRegion>> listAll(){
List<DictRegion> list = idictRegionService.list();
return BaseResponse.success(list);
}
/**
* 查询数据
@ -87,5 +99,36 @@ public class DictRegionController {
return BaseResponse.success(dictRegion);
}
/**
* 递归查询所有下级数据(包括自己)
*
* @param id 当前节点id值
*
* @return 返回结果
*/
@ApiOperation("递归查询所有下级数据")
@GetMapping("/getAllChildren/{id}")
public BaseResponse<List<DictRegion>> getAllChildren(@ApiParam(value = "当前节点id", required = true) @PathVariable String id){
List<DictRegion> dictRegionList = idictRegionService.getAllChildren(id);
return BaseResponse.success(dictRegionList);
}
/**
* 构建树形结构
*
* @param id 当前节点id值
*
* @return 返回结果
*/
@ApiOperation("递归查询所有下级数据树形结构")
@GetMapping("/getAllChildrenTree/{id}")
public BaseResponse<DictRegionTreeVO> getAllChildrenTree(@ApiParam(value = "当前节点id", required = true) @PathVariable String id){
DictRegionTreeVO treeStructure = idictRegionService.getAllChildrenTree(id);
return BaseResponse.success(treeStructure);
}
}

View File

@ -1,5 +1,6 @@
package com.coscoshipping.ebtp.project.dict.controller;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import com.coscoshipping.ebtp.project.dict.entity.DictRegionInternational;
import com.coscoshipping.ebtp.project.dict.service.IDictRegionInternationalService;
import com.chinaunicom.mall.ebtp.common.base.entity.BaseResponse;
@ -14,6 +15,7 @@ import java.util.List;
/**
* 全球国家信息
*
* @author 自动生成
*/
@RestController
@ -33,7 +35,8 @@ public class DictRegionInternationalController {
*/
@ApiOperation("插入新数据")
@PostMapping("")
public BaseResponse<Boolean> insert(@ApiParam(value = "对象数据", required = true) @RequestBody @Valid DictRegionInternational dictRegionInternational){
public BaseResponse<Boolean> insert(
@ApiParam(value = "对象数据", required = true) @RequestBody @Valid DictRegionInternational dictRegionInternational) {
boolean save = iDictRegionInternationalService.save(dictRegionInternational);
return BaseResponse.success(save);
}
@ -47,10 +50,23 @@ public class DictRegionInternationalController {
*/
@ApiOperation("修改数据")
@PostMapping("/update")
public BaseResponse<Boolean> update(@ApiParam(value = "对象数据", required = true) @RequestBody DictRegionInternational dictRegionInternational){
public BaseResponse<Boolean> update(
@ApiParam(value = "对象数据", required = true) @RequestBody DictRegionInternational dictRegionInternational) {
return BaseResponse.success(iDictRegionInternationalService.updateById(dictRegionInternational));
}
/**
* 查询所有数据
*
* @return 返回结果
*/
@ApiOperation("查询所有数据")
@GetMapping("/all")
public BaseResponse<List<DictRegionInternational>> listAll(){
List<DictRegionInternational> list = iDictRegionInternationalService.list();
return BaseResponse.success(list);
}
/**
* 查询数据
*
@ -60,7 +76,8 @@ public class DictRegionInternationalController {
*/
@ApiOperation("查询数据")
@GetMapping("/{id}")
public BaseResponse<DictRegionInternational> get(@ApiParam(value = "主键id", required = true) @PathVariable String id){
public BaseResponse<DictRegionInternational> get(
@ApiParam(value = "主键id", required = true) @PathVariable String id) {
DictRegionInternational dictRegionInternational = iDictRegionInternationalService.getById(id);
return BaseResponse.success(dictRegionInternational);
}
@ -74,8 +91,9 @@ public class DictRegionInternationalController {
*/
@ApiOperation("查询子数据")
@GetMapping("/getChild")
public BaseResponse<List<DictRegionInternational>> getChild(@ApiParam(value = "主键id", required = true) @RequestParam(name = "pId") String pId){
public BaseResponse<List<DictRegionInternational>> getChild(
@ApiParam(value = "主键id", required = true) @RequestParam(name = "pId") String pId) {
List<DictRegionInternational> dictRegionInternational = iDictRegionInternationalService.getChild(pId);
return BaseResponse.success(dictRegionInternational);
}
}
}

View File

@ -3,8 +3,17 @@ package com.coscoshipping.ebtp.project.dict.dao;
import com.chinaunicom.mall.ebtp.common.base.dao.IBaseMapper;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface DictRegionMapper extends IBaseMapper<DictRegion> {
/**
* 递归查询所有下级数据(包括自己)
* @param id 当前节点ID
* @return 所有下级数据列表
*/
List<DictRegion> getAllChildrenIncludingSelf(@Param("id") String id);
}

View File

@ -8,6 +8,7 @@
<result column="p_id" jdbcType="VARCHAR" property="pId"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="level" jdbcType="VARCHAR" property="level"/>
<result column="ab" jdbcType="VARCHAR" property="ab"/>
</resultMap>
<!--逻辑删除方法 此方法为代码生成器生成 不允许修改 如有特殊需求 请自行新建SQL语句-->
@ -17,4 +18,23 @@
delete_flag="deleted"
where ID=#{id}
</update>
<!-- 递归查询所有下级数据(包括自己) -->
<select id="getAllChildrenIncludingSelf" resultMap="BaseResultMap">
WITH RECURSIVE region_tree AS (
-- 基础查询:查询当前节点
SELECT id, p_id, name, level, ab
FROM dict_region
WHERE id = #{id}
UNION ALL
-- 递归查询:查询所有子节点
SELECT dr.id, dr.p_id, dr.name, dr.level, dr.ab
FROM dict_region dr
INNER JOIN region_tree rt ON dr.p_id = rt.id
)
SELECT * FROM region_tree
ORDER BY id
</select>
</mapper>

View File

@ -0,0 +1,71 @@
package com.coscoshipping.ebtp.project.dict.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
/**
* 全国行政区域树形结构VO
*
* @author daixc
* @date 2020/10/29
*/
@Data
@Accessors(chain = true)
@ApiModel("全国行政区域树形结构VO")
public class DictRegionTreeVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@ApiModelProperty(value = "id")
private String id;
/**
* 上级id
*/
@ApiModelProperty(value = "上级id")
private String pId;
/**
* 名称
*/
@ApiModelProperty(value = "名称")
private String name;
/**
* 类型0-省1-市2-县
*/
@ApiModelProperty(value = "类型0-省1-市2-县")
private String level;
/**
* 缩写
*/
@ApiModelProperty(value = "缩写")
private String ab;
/**
* 子节点列表
*/
@ApiModelProperty(value = "子节点列表")
private List<DictRegionTreeVO> children;
/**
* 是否叶子节点
*/
@ApiModelProperty(value = "是否叶子节点")
private Boolean isLeaf;
/**
* 层级深度
*/
@ApiModelProperty(value = "层级深度")
private Integer depth;
}

View File

@ -3,6 +3,7 @@ package com.coscoshipping.ebtp.project.dict.service;
import com.chinaunicom.mall.ebtp.common.base.service.IBaseService;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import com.coscoshipping.ebtp.project.dict.entity.DictRegionTreeVO;
import java.util.List;
@ -20,4 +21,18 @@ public interface IDictRegionService extends IBaseService<DictRegion>{
* @return
*/
List<DictRegion> getChild(String pId);
/**
* 递归查询所有下级数据(包括自己)
* @param id 当前节点ID
* @return 所有下级数据列表
*/
List<DictRegion> getAllChildren(String id);
/**
* 递归查询所有下级数据并构建树形结构
* @param id 当前节点ID
* @return 树形结构数据
*/
DictRegionTreeVO getAllChildrenTree(String id);
}

View File

@ -5,11 +5,15 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.chinaunicom.mall.ebtp.common.base.service.impl.BaseServiceImpl;
import com.coscoshipping.ebtp.project.dict.dao.DictRegionMapper;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import com.coscoshipping.ebtp.project.dict.entity.DictRegionTreeVO;
import com.coscoshipping.ebtp.project.dict.service.IDictRegionService;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 对数据表 dict_region 操作的 serviceImpl
@ -32,4 +36,123 @@ public class DictRegionServiceImpl extends BaseServiceImpl<DictRegionMapper, Dic
queryWrapper.orderByAsc("id");
return this.list(queryWrapper);
}
@Override
public List<DictRegion> getAllChildren(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
try {
// 优先使用SQL递归查询
return this.baseMapper.getAllChildrenIncludingSelf(id);
} catch (Exception e) {
// 如果SQL递归查询失败使用Java递归查询作为备选方案
return getAllChildrenIncludingSelfByJava(id);
}
}
@Override
public DictRegionTreeVO getAllChildrenTree(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
// 获取当前节点
DictRegion current = this.getById(id);
if (current == null) {
return null;
}
// 获取所有下级数据(包括自己)
List<DictRegion> allChildren = getAllChildrenIncludingSelfByJava(id);
if (allChildren == null || allChildren.isEmpty()) {
return null;
}
// 构建父ID到子节点的映射提高查找效率
Map<String, List<DictRegion>> parentToChildrenMap = new HashMap<>();
for (DictRegion node : allChildren) {
parentToChildrenMap.computeIfAbsent(node.getPId(), k -> new ArrayList<>()).add(node);
}
// 在内存中构建树形结构
return buildTreeFromMap(current, parentToChildrenMap, 0);
}
/**
* Java版本递归查询所有下级数据包括自己
* @param id 当前节点ID
* @return 所有下级数据列表
*/
private List<DictRegion> getAllChildrenIncludingSelfByJava(String id) {
List<DictRegion> result = new ArrayList<>();
// 查询当前节点
DictRegion current = this.getById(id);
if (current != null) {
result.add(current);
// 递归查询子节点
result.addAll(getAllChildrenRecursive(id));
}
return result;
}
/**
* 递归查询所有子节点
* @param parentId 父节点ID
* @return 所有子节点列表
*/
private List<DictRegion> getAllChildrenRecursive(String parentId) {
List<DictRegion> children = new ArrayList<>();
// 查询直接子节点
QueryWrapper<DictRegion> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("p_id", parentId);
queryWrapper.orderByAsc("id");
List<DictRegion> directChildren = this.list(queryWrapper);
for (DictRegion child : directChildren) {
children.add(child);
// 递归查询子节点的子节点
children.addAll(getAllChildrenRecursive(child.getId()));
}
return children;
}
/**
* 从Map中构建树形结构内存中处理使用Map提高效率
* @param rootNode 根节点
* @param parentToChildrenMap 父ID到子节点的映射
* @param depth 当前深度
* @return 树形结构节点
*/
private DictRegionTreeVO buildTreeFromMap(DictRegion rootNode, Map<String, List<DictRegion>> parentToChildrenMap, int depth) {
DictRegionTreeVO treeNode = new DictRegionTreeVO()
.setId(rootNode.getId())
.setPId(rootNode.getPId())
.setName(rootNode.getName())
.setLevel(rootNode.getLevel())
.setAb(rootNode.getAb())
.setDepth(depth);
// 从Map中获取当前节点的直接子节点
List<DictRegion> directChildren = parentToChildrenMap.get(rootNode.getId());
if (directChildren == null || directChildren.isEmpty()) {
treeNode.setIsLeaf(true);
treeNode.setChildren(null);
} else {
treeNode.setIsLeaf(false);
List<DictRegionTreeVO> childNodes = new ArrayList<>();
for (DictRegion child : directChildren) {
childNodes.add(buildTreeFromMap(child, parentToChildrenMap, depth + 1));
}
treeNode.setChildren(childNodes);
}
return treeNode;
}
}

View File

@ -0,0 +1,108 @@
package com.coscoshipping.ebtp.project.dict.util;
import com.coscoshipping.ebtp.project.dict.entity.DictRegion;
import com.coscoshipping.ebtp.project.dict.entity.DictRegionTreeVO;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 树形结构工具类
*
* @author daixc
* @date 2020/10/29
*/
public class TreeUtil {
/**
* 将平铺的列表转换为树形结构
*
* @param list 平铺的列表
* @return 树形结构列表
*/
public static List<DictRegionTreeVO> buildTreeFromList(List<DictRegion> list) {
if (list == null || list.isEmpty()) {
return new ArrayList<>();
}
// 按父ID分组
Map<String, List<DictRegion>> parentMap = list.stream()
.collect(Collectors.groupingBy(DictRegion::getPId));
// 构建树形结构
return buildTreeRecursive(parentMap, "0", 0);
}
/**
* 递归构建树形结构
*
* @param parentMap 父ID映射
* @param parentId 父ID
* @param depth 当前深度
* @return 树形结构列表
*/
private static List<DictRegionTreeVO> buildTreeRecursive(Map<String, List<DictRegion>> parentMap, String parentId, int depth) {
List<DictRegion> children = parentMap.get(parentId);
if (children == null || children.isEmpty()) {
return new ArrayList<>();
}
List<DictRegionTreeVO> result = new ArrayList<>();
for (DictRegion child : children) {
DictRegionTreeVO treeNode = new DictRegionTreeVO()
.setId(child.getId())
.setPId(child.getPId())
.setName(child.getName())
.setLevel(child.getLevel())
.setAb(child.getAb())
.setDepth(depth);
// 递归构建子节点
List<DictRegionTreeVO> childNodes = buildTreeRecursive(parentMap, child.getId(), depth + 1);
if (childNodes.isEmpty()) {
treeNode.setIsLeaf(true);
treeNode.setChildren(null);
} else {
treeNode.setIsLeaf(false);
treeNode.setChildren(childNodes);
}
result.add(treeNode);
}
return result;
}
/**
* 将树形结构转换为平铺列表
*
* @param tree 树形结构
* @return 平铺列表
*/
public static List<DictRegionTreeVO> flattenTree(DictRegionTreeVO tree) {
List<DictRegionTreeVO> result = new ArrayList<>();
flattenTreeRecursive(tree, result);
return result;
}
/**
* 递归将树形结构转换为平铺列表
*
* @param node 当前节点
* @param result 结果列表
*/
private static void flattenTreeRecursive(DictRegionTreeVO node, List<DictRegionTreeVO> result) {
if (node == null) {
return;
}
result.add(node);
if (node.getChildren() != null) {
for (DictRegionTreeVO child : node.getChildren()) {
flattenTreeRecursive(child, result);
}
}
}
}

View File

@ -125,4 +125,17 @@ public class SysOrgController{
public BaseResponse<List<SysOrgVO>> queryAll(SysOrg sysOrg){
return BaseResponse.success(iSysOrgService.getTreeByNode(sysOrg));
}
/**
* 查询组织信息(当前组织及下级组织列表)
*
* @param sysOrg 查询条件
* @return 返回当前组织信息以及下级组织列表
*/
@ApiOperation("查询组织信息(当前组织及下级组织列表)")
@GetMapping("/queryOrgWithChildren")
public BaseResponse<List<SysOrg>> queryOrgWithChildren(@ApiParam(value = "查询条件", required = false) SysOrg sysOrg) {
List<SysOrg> result = iSysOrgService.queryOrgWithChildren(sysOrg);
return BaseResponse.success(result);
}
}

View File

@ -8,4 +8,12 @@ import com.coscoshipping.ebtp.project.org.entity.SysOrg;
@Repository
public interface SysOrgMapper extends IBaseMapper<SysOrg> {
/**
* 查询指定组织的所有下级组织(包括多级下级)
*
* @param parentOrgId 父组织ID
* @return 所有下级组织列表
*/
List<SysOrg> selectAllChildrenByOrgId(String parentOrgId);
}

View File

@ -38,4 +38,41 @@
delete_flag="1"
where ID=#{id }
</update>
</mapper>
<!-- 递归查询指定组织的所有下级组织 -->
<select id="selectAllChildrenByOrgId" resultMap="orgResultMap">
WITH RECURSIVE org_tree AS (
-- 基础查询:直接下级组织
SELECT
org_id, org_name, org_num, org_full_id, org_full_name,
up_org_id, org_category, leader_name, leader_oa_code, leader_user_id,
top_leader_name, top_leader_oa_code, top_leader_user_id, sort, status,
branch, site, cu_company_number, create_by, create_date, tenant_id,
tenant_name, update_by, update_date, delete_flag, last_update_time,
1 as level
FROM sys_org
WHERE up_org_id = #{parentOrgId}
UNION ALL
-- 递归查询:下级的下级组织
SELECT
o.org_id, o.org_name, o.org_num, o.org_full_id, o.org_full_name,
o.up_org_id, o.org_category, o.leader_name, o.leader_oa_code, o.leader_user_id,
o.top_leader_name, o.top_leader_oa_code, o.top_leader_user_id, o.sort, o.status,
o.branch, o.site, o.cu_company_number, o.create_by, o.create_date, o.tenant_id,
o.tenant_name, o.update_by, o.update_date, o.delete_flag, o.last_update_time,
ot.level + 1
FROM sys_org o
INNER JOIN org_tree ot ON o.up_org_id = ot.org_id
)
SELECT
org_id, org_name, org_num, org_full_id, org_full_name,
up_org_id, org_category, leader_name, leader_oa_code, leader_user_id,
top_leader_name, top_leader_oa_code, top_leader_user_id, sort, status,
branch, site, cu_company_number, create_by, create_date, tenant_id,
tenant_name, update_by, update_date, delete_flag, last_update_time
FROM org_tree
ORDER BY sort ASC, org_name ASC
</select>
</mapper>

View File

@ -41,4 +41,12 @@ public interface SysOrgService extends IBaseService<SysOrg>{
Boolean deleteById(String orgId);
SysOrg getCompanyByOrgId(String orgId);
/**
* 查询组织信息(当前组织及下级组织列表)
*
* @param sysOrg 查询条件
* @return 返回当前组织信息以及下级组织列表
*/
List<SysOrg> queryOrgWithChildren(SysOrg sysOrg);
}

View File

@ -1,29 +1,33 @@
package com.coscoshipping.ebtp.project.org.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chinaunicom.mall.ebtp.common.base.entity.BasePageRequest;
import com.chinaunicom.mall.ebtp.common.exception.common.CommonExceptionEnum;
import com.chinaunicom.mall.ebtp.common.util.JsonUtils;
import com.chinaunicom.mall.ebtp.common.util.PropertyUtils;
import com.coscoshipping.ebtp.project.org.entity.vo.SysOrgVO;
import com.coscoshipping.ebtp.project.org.util.CommonUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import com.chinaunicom.mall.ebtp.common.base.service.impl.BaseServiceImpl;
import com.chinaunicom.mall.ebtp.common.exception.common.CommonExceptionEnum;
import com.chinaunicom.mall.ebtp.common.util.PropertyUtils;
import com.coscoshipping.ebtp.project.org.dao.SysOrgMapper;
import com.coscoshipping.ebtp.project.org.entity.SysOrg;
import com.coscoshipping.ebtp.project.org.entity.vo.SysOrgVO;
import com.coscoshipping.ebtp.project.org.service.SysOrgService;
import org.springframework.util.CollectionUtils;
import com.coscoshipping.ebtp.project.org.util.CommonUtil;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import cn.hutool.core.bean.BeanUtil;
/**
* 对数据表 sys_org 操作的 serviceImpl
@ -48,53 +52,67 @@ public class SysOrgServiceImpl extends BaseServiceImpl<SysOrgMapper, SysOrg> imp
@Override
public List<SysOrgVO> getTreeByNode(SysOrg org) {
SysOrgVO sysOrgVO = new SysOrgVO();
List<SysOrg> sysOrgList = new ArrayList<>();
if (!StringUtils.isEmpty(org.getOrgName()) || !StringUtils.isEmpty(org.getOrgNum())) {
SysOrgVO vo = BeanUtil.toBean(org, SysOrgVO.class);
IPage<SysOrg> page = this.getPage(vo);
sysOrgList = page.getRecords();
return sysOrgList.stream().map(entity -> BeanUtil.toBean(entity, SysOrgVO.class)).collect(Collectors.toList());
} else {
sysOrgList = this.list();
List<SysOrgVO> voList = new ArrayList<>();
// 1. 先查询出所有的组织数据
List<SysOrg> allOrgList = this.list();
if (CollectionUtils.isEmpty(sysOrgList)) {
return voList;
}
// 预处理所有节点为SysOrgVO并构建子节点映射表
List<SysOrgVO> allOrgVOs = sysOrgList.stream()
.map(entity -> BeanUtil.toBean(entity, SysOrgVO.class))
.map(this::setInfoForTree) // 提前应用树形结构转换
.collect(Collectors.toList());
// 构建父节点到子节点的映射key: upOrgIdvalue: 子节点列表)
Map<String, List<SysOrgVO>> parentToChildrenMap = new HashMap<>();
String rootOrgCode = "10000000";
for (SysOrgVO orgVO : allOrgVOs) {
String parentId = StringUtils.isBlank(orgVO.getUpOrgId()) ? rootOrgCode : orgVO.getUpOrgId();
parentToChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(orgVO);
}
// 获取根节点ID
String rootNode = StringUtils.isNotBlank(org.getOrgId()) ? org.getOrgId() : rootOrgCode;
// 使用StringBuilder记录所有部门ID
StringBuilder allDepartmentIdBuilder = new StringBuilder(rootNode);
// 构建多级树结构(从根节点开始递归)
List<SysOrgVO> rootChildren = parentToChildrenMap.getOrDefault(rootNode, new ArrayList<>());
for (SysOrgVO rootChild : rootChildren) {
buildMultiLevelTree(rootChild, parentToChildrenMap, allDepartmentIdBuilder);
voList.add(rootChild);
}
// 设置全量部门ID
voList.forEach(vo -> vo.setAllDepartmentId(allDepartmentIdBuilder.toString()));
return voList;
// 如果没有数据,直接返回空列表
if (CollectionUtils.isEmpty(allOrgList)) {
return new ArrayList<>();
}
// 2. 构建完整的树形结构
List<SysOrgVO> fullTree = buildOrgTree(allOrgList, org);
// 3. 如果有查询条件,则根据条件过滤树形结构
if (!StringUtils.isEmpty(org.getOrgName()) || !StringUtils.isEmpty(org.getOrgNum())) {
return filterTreeByCondition(fullTree, org);
}
// 4. 没有查询条件,直接返回完整树形结构
return fullTree;
}
/**
* 构建组织机构树形结构
*
* @param sysOrgList 组织机构列表
* @param org 查询条件对象
* @return 树形结构的组织机构列表
*/
private List<SysOrgVO> buildOrgTree(List<SysOrg> sysOrgList, SysOrg org) {
List<SysOrgVO> voList = new ArrayList<>();
// 预处理所有节点为SysOrgVO并构建子节点映射表
List<SysOrgVO> allOrgVOs = sysOrgList.stream()
.map(entity -> BeanUtil.toBean(entity, SysOrgVO.class))
.map(this::setInfoForTree) // 提前应用树形结构转换
.collect(Collectors.toList());
// 构建父节点到子节点的映射key: upOrgIdvalue: 子节点列表)
Map<String, List<SysOrgVO>> parentToChildrenMap = new HashMap<>();
for (SysOrgVO orgVO : allOrgVOs) {
String parentId = StringUtils.isBlank(orgVO.getUpOrgId()) ? CommonUtil.ROOT_NODE : orgVO.getUpOrgId();
parentToChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(orgVO);
}
// 获取根节点ID
String rootNode = StringUtils.isNotBlank(org.getOrgId()) ? org.getOrgId() : CommonUtil.ROOT_NODE;
// 使用StringBuilder记录所有部门ID暂不知道这个参数有何作用先保留
// StringBuilder allDepartmentIdBuilder = new StringBuilder(rootNode);
StringBuilder allDepartmentIdBuilder = new StringBuilder("");
// 构建多级树结构(从根节点开始递归)
List<SysOrgVO> rootChildren = parentToChildrenMap.getOrDefault(rootNode, new ArrayList<>());
for (SysOrgVO rootChild : rootChildren) {
buildMultiLevelTree(rootChild, parentToChildrenMap, allDepartmentIdBuilder);
voList.add(rootChild);
}
// 设置全量部门ID
// voList.forEach(vo -> vo.setAllDepartmentId(allDepartmentIdBuilder.toString()));
return voList;
}
/**
@ -120,7 +138,7 @@ public class SysOrgServiceImpl extends BaseServiceImpl<SysOrgMapper, SysOrg> imp
currentNode.setChildren(sortedChildren);
// 记录当前节点ID,累加所有层级ID
allDepartmentIdBuilder.append(",").append(currentNode.getOrgId());
// allDepartmentIdBuilder.append(",").append(currentNode.getOrgId());
// 递归处理子节点
for (SysOrgVO child : sortedChildren) {
@ -244,4 +262,109 @@ public class SysOrgServiceImpl extends BaseServiceImpl<SysOrgMapper, SysOrg> imp
lqw.orderByDesc(SysOrg::getCreateDate);
return lqw;
}
/**
* 根据查询条件过滤树形结构
*
* @param fullTree 完整的树形结构
* @param org 查询条件对象
* @return 过滤后的树形结构
*/
private List<SysOrgVO> filterTreeByCondition(List<SysOrgVO> fullTree, SysOrg org) {
List<SysOrgVO> filteredTree = new ArrayList<>();
for (SysOrgVO node : fullTree) {
// 检查当前节点是否匹配条件
boolean isMatch = isNodeMatchCondition(node, org);
// 递归检查子节点
List<SysOrgVO> filteredChildren = filterTreeByCondition(node.getChildren(), org);
// 如果当前节点匹配条件或有匹配的子节点,则保留该节点
if (isMatch || !filteredChildren.isEmpty()) {
SysOrgVO filteredNode = BeanUtil.toBean(node, SysOrgVO.class);
filteredNode.setChildren(filteredChildren);
filteredTree.add(filteredNode);
}
}
return filteredTree;
}
/**
* 判断节点是否匹配查询条件
*
* @param node 待检查的节点
* @param org 查询条件对象
* @return 是否匹配
*/
private boolean isNodeMatchCondition(SysOrgVO node, SysOrg org) {
// 检查组织名称
if (StringUtils.isNotBlank(org.getOrgName()) &&
StringUtils.isNotBlank(node.getOrgName()) &&
node.getOrgName().contains(org.getOrgName())) {
return true;
}
// 检查组织编号
if (StringUtils.isNotBlank(org.getOrgNum()) &&
StringUtils.isNotBlank(node.getOrgNum()) &&
node.getOrgNum().contains(org.getOrgNum())) {
return true;
}
return false;
}
@Override
public List<SysOrg> queryOrgWithChildren(SysOrg sysOrg) {
List<SysOrg> result = new ArrayList<>();
// 如果没有指定组织ID则查询所有组织
if (StringUtils.isBlank(sysOrg.getOrgId())) {
LambdaQueryWrapper<SysOrg> queryWrapper = buildOrgQueryWrapper(sysOrg);
result = this.list(queryWrapper);
} else {
// 查询指定的组织
SysOrg currentOrg = this.getById(sysOrg.getOrgId());
if (currentOrg != null) {
result.add(currentOrg);
// 使用SQL递归查询所有下级组织
List<SysOrg> children = baseMapper.selectAllChildrenByOrgId(sysOrg.getOrgId());
result.addAll(children);
}
}
// 按排序字段排序
result.sort(Comparator.comparing(SysOrg::getSort, Comparator.nullsLast(String::compareTo)));
return result;
}
/**
* 构建组织查询条件
*
* @param sysOrg 查询条件对象
* @return 查询包装器
*/
private LambdaQueryWrapper<SysOrg> buildOrgQueryWrapper(SysOrg sysOrg) {
LambdaQueryWrapper<SysOrg> queryWrapper = Wrappers.lambdaQuery();
// 添加查询条件
queryWrapper.like(StringUtils.isNotBlank(sysOrg.getOrgName()), SysOrg::getOrgName, sysOrg.getOrgName());
queryWrapper.like(StringUtils.isNotBlank(sysOrg.getOrgNum()), SysOrg::getOrgNum, sysOrg.getOrgNum());
queryWrapper.eq(StringUtils.isNotBlank(sysOrg.getOrgCategory()), SysOrg::getOrgCategory, sysOrg.getOrgCategory());
queryWrapper.eq(StringUtils.isNotBlank(sysOrg.getStatus()), SysOrg::getStatus, sysOrg.getStatus());
queryWrapper.eq(StringUtils.isNotBlank(sysOrg.getUpOrgId()), SysOrg::getUpOrgId, sysOrg.getUpOrgId());
queryWrapper.like(StringUtils.isNotBlank(sysOrg.getLeaderName()), SysOrg::getLeaderName, sysOrg.getLeaderName());
queryWrapper.like(StringUtils.isNotBlank(sysOrg.getTopLeaderName()), SysOrg::getTopLeaderName, sysOrg.getTopLeaderName());
queryWrapper.eq(StringUtils.isNotBlank(sysOrg.getSite()), SysOrg::getSite, sysOrg.getSite());
queryWrapper.eq(StringUtils.isNotBlank(sysOrg.getCuCompanyNumber()), SysOrg::getCuCompanyNumber, sysOrg.getCuCompanyNumber());
// 按排序字段排序
queryWrapper.orderByAsc(SysOrg::getSort);
return queryWrapper;
}
}

View File

@ -29,7 +29,7 @@ public class CommonUtil {
/**
* 组织结构书根节点
*/
public static final String ROOT_NODE = "0";
public static final String ROOT_NODE = "10000000";
/**
* 系统编码

View File

@ -13,8 +13,8 @@ spring:
writeTimeout: 35000
nacos:
discovery:
server-addr: 127.0.0.1:18848
group: EBTP_GROUP # 例如EBTP_GROUP
server-addr: 10.60.161.59:8848
aop:
auto: true #开启spring的aop配置
proxy-target-class: true
@ -22,30 +22,29 @@ spring:
application:
name: sys-manager-ebtp-project
shardingsphere:
datasource:
names: ds0
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: Unicom@2024
jdbc-url: jdbc:mysql://59.110.10.99:53306/ebtp_sys_manager?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
url: jdbc:mysql://59.110.10.99:53306/ebtp_sys_manager?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
filters: stat,wall,log4j
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
connection-properties: druid.stat.merggSql=ture;druid.stat.slowSqlMillis=5000
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root@test
# username: root
password: Unicom@2024
# url: jdbc:mysql://59.110.10.99:53306/ebtp_sys_manager?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
url: jdbc:mysql://localhost:2881/ebtp_sys_manager?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
druid:
# filters: stat,wall,log4j
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
connection-properties: druid.stat.merggSql=ture;druid.stat.slowSqlMillis=5000
props:
sql:
show: true
@ -83,7 +82,8 @@ spring:
redis:
sentinel:
master: mymaster
nodes: 10.60.161.59:26379, 10.60.161.59:26380, 10.60.161.59:26381
# nodes: 10.60.161.59:26379, 10.60.161.59:26380, 10.60.161.59:26381
nodes: localhost:26379
password: pass
database:
sharding: 1
@ -91,6 +91,14 @@ spring:
idempotent: 3
userinfo: 4
thymeleaf:
prefix: classpath:/templates/
suffix: .html
cache: false
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
mybatis-plus:
@ -99,7 +107,7 @@ mybatis-plus:
map-underscore-to-camel-case: true
auto-mapping-behavior: full
# 这个配置会将执行的sql打印出来在开发或测试的时候可以用
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:com/coscoshipping/ebtp/**/mapper/*Mapper.xml,com/chinaunicom/mall/ebtp/**/mapper/*Mapper.xml
global-config:
# 逻辑删除配置

View File

@ -2,9 +2,9 @@
<configuration scan="true" scanPeriod="60 seconds" debug="true">
<property name="logback.logdir" value="/log" />
<property name="logback.appname" value="${APP_NAME}" />
<property name="logback.appname" value="sys-manager-ebtp-project" />
<contextName>${logback.appname}</contextName>
<!-- <contextName>${logback.appname}</contextName> -->
<!--输出到控制台 ConsoleAppender-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
@ -39,7 +39,7 @@
<pattern>%d ${MY_POD_IP} [%thread] %-5level %logger{64} %line - [ppTraceId: %X{PtxId}, ppSpanId: %X{PspanId}] - %msg%n</pattern>
</encoder>
</appender>
<!--指定最基础的日志输出级别-->
<root level="INFO">
<appender-ref ref="consoleLog"/>
@ -47,4 +47,14 @@
<appender-ref ref="fileInfoLog"/>
</root>
<!-- MyBatis SQL日志配置 -->
<logger name="com.coscoshipping.ebtp.project" level="DEBUG"/>
<logger name="com.chinaunicom.mall.ebtp" level="DEBUG"/>
<logger name="org.apache.ibatis" level="DEBUG"/>
<logger name="java.sql" level="DEBUG"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<logger name="java.sql.ResultSet" level="DEBUG"/>
</configuration>