diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/dto/SysAreaSortDTO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/dto/SysAreaSortDTO.java new file mode 100644 index 000000000..6eb5c497c --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/dto/SysAreaSortDTO.java @@ -0,0 +1,34 @@ +package com.pig4cloud.pigx.admin.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * 行政区划排序 DTO + * + * @author lengleng + * @date 2026/7/18 + */ +@Data +@Schema(description = "行政区划排序") +public class SysAreaSortDTO { + + /** + * 父级行政编码 + */ + @NotNull(message = "父级行政编码不能为空") + @Schema(description = "父级行政编码") + private Long pid; + + /** + * 同级行政区划数据库主键,按目标顺序排列 + */ + @NotEmpty(message = "行政区划ID列表不能为空") + @Schema(description = "行政区划ID列表") + private List areaIds; + +} diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/dto/SysAreaUpdateDTO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/dto/SysAreaUpdateDTO.java new file mode 100644 index 000000000..89f02b882 --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/dto/SysAreaUpdateDTO.java @@ -0,0 +1,33 @@ +package com.pig4cloud.pigx.admin.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 行政区划可编辑字段 + * + * @author lengleng + * @date 2026/7/18 + */ +@Data +@Schema(description = "行政区划可编辑字段") +public class SysAreaUpdateDTO { + + @NotNull(message = "行政区划ID不能为空") + private Long id; + + @NotBlank(message = "行政区划名称不能为空") + @Size(max = 255, message = "行政区划名称长度不能超过255个字符") + private String name; + + @Pattern(regexp = "[01]", message = "行政区划状态不正确") + private String areaStatus; + + @Pattern(regexp = "[01]", message = "热门状态不正确") + private String hot; + +} diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/SysAreaChildCountVO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/SysAreaChildCountVO.java new file mode 100644 index 000000000..061aef694 --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/SysAreaChildCountVO.java @@ -0,0 +1,18 @@ +package com.pig4cloud.pigx.admin.api.vo; + +import lombok.Data; + +/** + * 行政区划直属子级数量 + * + * @author lengleng + * @date 2026/7/18 + */ +@Data +public class SysAreaChildCountVO { + + private Long pid; + + private Long childCount; + +} diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/SysAreaManageVO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/SysAreaManageVO.java new file mode 100644 index 000000000..e000adff7 --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/SysAreaManageVO.java @@ -0,0 +1,54 @@ +package com.pig4cloud.pigx.admin.api.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 行政区划管理视图 + * + * @author lengleng + * @date 2026/7/18 + */ +@Data +@Schema(description = "行政区划管理视图") +public class SysAreaManageVO { + + @Schema(description = "数据库主键") + private Long id; + + @Schema(description = "父级行政编码") + private Long pid; + + @Schema(description = "行政编码") + private Long adcode; + + @Schema(description = "名称") + private String name; + + @Schema(description = "行政区划类型") + private String areaType; + + @Schema(description = "热门标记") + private String hot; + + @Schema(description = "状态") + private String areaStatus; + + @Schema(description = "排序值") + private Long areaSort; + + @Schema(description = "是否存在下级") + private Boolean hasChildren; + + @Schema(description = "直属子级数量") + private Long childCount; + + @Schema(description = "完整路径行政编码") + private List pathCodes; + + @Schema(description = "完整路径名称") + private String pathName; + +} diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/SysAreaController.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/SysAreaController.java index 285161091..baebf04a5 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/SysAreaController.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/SysAreaController.java @@ -4,6 +4,8 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.pig4cloud.pigx.admin.api.dto.SysAreaSortDTO; +import com.pig4cloud.pigx.admin.api.dto.SysAreaUpdateDTO; import com.pig4cloud.pigx.admin.api.entity.SysAreaEntity; import com.pig4cloud.pigx.admin.service.SysAreaService; import com.pig4cloud.pigx.common.core.util.R; @@ -13,10 +15,13 @@ import com.pig4cloud.pigx.common.security.annotation.HasPermission; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; import java.util.List; @@ -27,6 +32,7 @@ import java.util.List; * @date 2024-02-16 22:40:06 */ @RestController +@Validated @RequiredArgsConstructor @RequestMapping("/sysArea") @Tag(description = "sysArea", name = "行政区划管理") @@ -39,7 +45,7 @@ public class SysAreaController { * 分页查询 * * @param page 分页对象 - * @param sysArea 行政区划 + * @param updateDTO 可编辑字段 * @return */ @Operation(summary = "分页查询", description = "分页查询") @@ -62,17 +68,56 @@ public class SysAreaController { return R.ok(sysAreaService.selectTree(sysArea)); } + /** + * 查询管理端直属子级 + * + * @param pid 父级行政编码 + * @return 直属子级 + */ + @Operation(summary = "查询管理端直属子级", description = "包含启用和停用的直属子级") + @GetMapping("/manage/children") + @HasPermission("sys_sysArea_view") + public R getManageChildren(@RequestParam(required = false, defaultValue = "0") Long pid) { + return R.ok(sysAreaService.listManageChildren(pid)); + } + + /** + * 搜索管理端行政区划 + * + * @param keyword 名称或行政编码 + * @return 搜索结果 + */ + @Operation(summary = "搜索管理端行政区划", description = "按名称或行政编码搜索") + @GetMapping("/manage/search") + @HasPermission("sys_sysArea_view") + public R searchManage(@RequestParam @Size(max = 64, message = "搜索关键词长度不能超过64个字符") String keyword) { + return R.ok(sysAreaService.searchManage(keyword)); + } + + /** + * 判断行政编码是否存在 + * + * @param adcode 行政编码 + * @return 是否存在 + */ + @Operation(summary = "判断行政编码是否存在", description = "新增行政区划时校验行政编码") + @GetMapping("/exists/adcode") + @HasPermission("sys_sysArea_add") + public R existsByAdcode(@RequestParam Long adcode) { + return R.ok(sysAreaService.existsByAdcode(adcode)); + } + /** * 获取详细信息 * - * @param sysArea 查询条件 + * @param id 行政区划ID * @return {@link R } */ @Operation(summary = "获取详细信息", description = "获取详细信息") @GetMapping("/details") @HasPermission("sys_sysArea_view") - public R getDetails(@ParameterObject SysAreaEntity sysArea) { - return R.ok(sysAreaService.getOne(Wrappers.query(sysArea))); + public R getDetails(@RequestParam Long id) { + return R.ok(sysAreaService.getById(id)); } /** @@ -86,7 +131,7 @@ public class SysAreaController { @PostMapping @HasPermission("sys_sysArea_add") public R save(@RequestBody SysAreaEntity sysArea) { - return R.ok(sysAreaService.save(sysArea)); + return sysAreaService.saveArea(sysArea); } /** @@ -99,10 +144,24 @@ public class SysAreaController { @SysLog("修改行政区划") @PutMapping @HasPermission("sys_sysArea_edit") - public R updateById(@RequestBody SysAreaEntity sysArea) { - return R.ok(sysAreaService.updateById(sysArea)); + public R updateById(@Valid @RequestBody SysAreaUpdateDTO updateDTO) { + return sysAreaService.updateArea(updateDTO); } + /** + * 更新同级行政区划排序 + * + * @param sortDTO 排序参数 + * @return R + */ + @Operation(summary = "更新行政区划排序", description = "仅支持同一父级的完整直属子级排序") + @SysLog("更新行政区划排序") + @PutMapping("/sort") + @HasPermission("sys_sysArea_edit") + public R sort(@Valid @RequestBody SysAreaSortDTO sortDTO) { + return sysAreaService.updateAreaSort(sortDTO); + } + /** * 通过id删除行政区划 * @@ -114,7 +173,7 @@ public class SysAreaController { @DeleteMapping @HasPermission("sys_sysArea_del") public R removeById(@RequestBody Long[] ids) { - return R.ok(sysAreaService.removeBatchByIds(CollUtil.toList(ids))); + return sysAreaService.removeAreas(CollUtil.toList(ids)); } /** diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/mapper/SysAreaMapper.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/mapper/SysAreaMapper.java index ce9d78450..e8055f063 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/mapper/SysAreaMapper.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/mapper/SysAreaMapper.java @@ -1,10 +1,23 @@ package com.pig4cloud.pigx.admin.mapper; import com.pig4cloud.pigx.admin.api.entity.SysAreaEntity; +import com.pig4cloud.pigx.admin.api.vo.SysAreaChildCountVO; import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; @Mapper public interface SysAreaMapper extends PigxBaseMapper { + /** + * 按父级行政编码统计直属子级数量 + * + * @param parentCodes 父级行政编码 + * @return 子级数量 + */ + List selectChildCounts(@Param("parentCodes") Collection parentCodes); + } diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/SysAreaService.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/SysAreaService.java index 1d1b8a8bc..21c7a7d48 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/SysAreaService.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/SysAreaService.java @@ -3,7 +3,11 @@ package com.pig4cloud.pigx.admin.service; import cn.hutool.core.lang.tree.Tree; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; +import com.pig4cloud.pigx.admin.api.dto.SysAreaSortDTO; +import com.pig4cloud.pigx.admin.api.dto.SysAreaUpdateDTO; import com.pig4cloud.pigx.admin.api.entity.SysAreaEntity; +import com.pig4cloud.pigx.admin.api.vo.SysAreaManageVO; +import com.pig4cloud.pigx.common.core.util.R; import java.util.List; @@ -26,4 +30,60 @@ public interface SysAreaService extends IService { */ Page selectPage(Page page, SysAreaEntity sysArea); + /** + * 查询管理端直属子级 + * + * @param pid 父级行政编码 + * @return 直属子级 + */ + List listManageChildren(Long pid); + + /** + * 搜索管理端行政区划 + * + * @param keyword 名称或行政编码 + * @return 搜索结果 + */ + List searchManage(String keyword); + + /** + * 判断行政编码是否存在 + * + * @param adcode 行政编码 + * @return 是否存在 + */ + boolean existsByAdcode(Long adcode); + + /** + * 更新同级行政区划排序 + * + * @param sortDTO 排序参数 + * @return 更新结果 + */ + R updateAreaSort(SysAreaSortDTO sortDTO); + + /** + * 新增行政区划并追加到同级末尾 + * + * @param sysArea 行政区划 + * @return 新增结果 + */ + R saveArea(SysAreaEntity sysArea); + + /** + * 更新行政区划可编辑字段 + * + * @param updateDTO 可编辑字段 + * @return 更新结果 + */ + R updateArea(SysAreaUpdateDTO updateDTO); + + /** + * 删除叶子行政区划 + * + * @param ids 主键列表 + * @return 删除结果 + */ + R removeAreas(List ids); + } diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImpl.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImpl.java index 01fa00a3f..06207b978 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImpl.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImpl.java @@ -10,16 +10,29 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.pig4cloud.pigx.admin.api.dto.SysAreaSortDTO; +import com.pig4cloud.pigx.admin.api.dto.SysAreaUpdateDTO; import com.pig4cloud.pigx.admin.api.entity.SysAreaEntity; +import com.pig4cloud.pigx.admin.api.vo.SysAreaChildCountVO; +import com.pig4cloud.pigx.admin.api.vo.SysAreaManageVO; import com.pig4cloud.pigx.admin.mapper.SysAreaMapper; import com.pig4cloud.pigx.admin.service.SysAreaService; import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum; +import com.pig4cloud.pigx.common.core.util.R; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 行政区划 @@ -30,6 +43,10 @@ import java.util.Optional; @Service public class SysAreaServiceImpl extends ServiceImpl implements SysAreaService { + private static final Long AREA_ROOT_ADCODE = 100000L; + + private static final int SEARCH_RESULT_LIMIT = 50; + /** * 查询行政区划树 * @@ -46,7 +63,8 @@ public class SysAreaServiceImpl extends ServiceImpl entityList = baseMapper.selectList(wrapper); @@ -79,7 +97,243 @@ public class SysAreaServiceImpl extends ServiceImpl listManageChildren(Long pid) { + Long parentCode = Optional.ofNullable(pid).orElse(0L); + List children = list(Wrappers.lambdaQuery() + .eq(SysAreaEntity::getPid, parentCode) + .orderByDesc(SysAreaEntity::getAreaSort) + .orderByAsc(SysAreaEntity::getAdcode)); + return toManageList(children); + } + + @Override + public List searchManage(String keyword) { + String normalizedKeyword = StrUtil.trim(keyword); + if (StrUtil.isBlank(normalizedKeyword)) { + return List.of(); + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + Long adcode = parseAdcode(normalizedKeyword); + if (Objects.nonNull(adcode)) { + wrapper.and(query -> query.like(SysAreaEntity::getName, normalizedKeyword) + .or() + .eq(SysAreaEntity::getAdcode, adcode)); + } + else { + wrapper.like(SysAreaEntity::getName, normalizedKeyword); + } + wrapper.orderByAsc(SysAreaEntity::getAreaType) + .orderByDesc(SysAreaEntity::getAreaSort) + .orderByAsc(SysAreaEntity::getAdcode); + + Page page = new Page<>(1, SEARCH_RESULT_LIMIT, false); + return toManageList(baseMapper.selectPage(page, wrapper).getRecords()); + } + + @Override + public boolean existsByAdcode(Long adcode) { + return Objects.nonNull(adcode) && count(Wrappers.lambdaQuery() + .eq(SysAreaEntity::getAdcode, adcode)) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R updateAreaSort(SysAreaSortDTO sortDTO) { + if (Objects.equals(sortDTO.getPid(), 0L)) { + return R.failed("全国根节点不支持排序"); + } + + List areaIds = sortDTO.getAreaIds(); + Set uniqueIds = new HashSet<>(areaIds); + if (uniqueIds.size() != areaIds.size()) { + return R.failed("行政区划排序ID不能重复"); + } + + List siblings = list(Wrappers.lambdaQuery() + .eq(SysAreaEntity::getPid, sortDTO.getPid())); + Set siblingIds = siblings.stream().map(SysAreaEntity::getId).collect(Collectors.toSet()); + if (siblings.size() != areaIds.size() || !siblingIds.equals(uniqueIds)) { + return R.failed("行政区划列表已变化,请刷新后重试"); + } + + Map siblingMap = siblings.stream() + .collect(Collectors.toMap(SysAreaEntity::getId, Function.identity())); + if (!updateBatchById(buildSortUpdates(areaIds, siblingMap))) { + throw new IllegalStateException("行政区划排序保存失败"); + } + return R.ok(true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R saveArea(SysAreaEntity sysArea) { + Long pid = Optional.ofNullable(sysArea.getPid()).orElse(AREA_ROOT_ADCODE); + sysArea.setPid(pid); + + List siblings = list(Wrappers.lambdaQuery() + .eq(SysAreaEntity::getPid, pid) + .orderByDesc(SysAreaEntity::getAreaSort) + .orderByAsc(SysAreaEntity::getAdcode)); + sysArea.setAreaSort(0L); + if (!save(sysArea)) { + return R.ok(false); + } + + List orderedIds = siblings.stream().map(SysAreaEntity::getId).collect(Collectors.toCollection(ArrayList::new)); + orderedIds.add(sysArea.getId()); + Map areaMap = siblings.stream() + .collect(Collectors.toMap(SysAreaEntity::getId, Function.identity())); + areaMap.put(sysArea.getId(), sysArea); + if (!updateBatchById(buildSortUpdates(orderedIds, areaMap))) { + throw new IllegalStateException("行政区划新增排序初始化失败"); + } + return R.ok(true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R updateArea(SysAreaUpdateDTO updateDTO) { + SysAreaEntity current = getById(updateDTO.getId()); + if (Objects.isNull(current)) { + return R.failed("行政区划不存在或已删除"); + } + if (Objects.equals(current.getAdcode(), AREA_ROOT_ADCODE)) { + return R.failed("全国根节点不允许修改"); + } + + SysAreaEntity update = new SysAreaEntity(); + update.setId(updateDTO.getId()); + update.setName(StrUtil.trim(updateDTO.getName())); + update.setAreaStatus(updateDTO.getAreaStatus()); + update.setHot(updateDTO.getHot()); + return updateById(update) ? R.ok(true) : R.failed("行政区划更新失败"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R removeAreas(List ids) { + if (CollUtil.isEmpty(ids)) { + return R.failed("行政区划ID不能为空"); + } + + Set uniqueIds = new HashSet<>(ids); + if (uniqueIds.size() != ids.size()) { + return R.failed("行政区划ID不能重复"); + } + + List areas = listByIds(uniqueIds); + if (areas.size() != uniqueIds.size()) { + return R.failed("行政区划不存在或已删除"); + } + if (areas.stream().anyMatch(area -> Objects.equals(area.getAdcode(), AREA_ROOT_ADCODE))) { + return R.failed("全国根节点不允许删除"); + } + + Set adcodes = areas.stream().map(SysAreaEntity::getAdcode).collect(Collectors.toSet()); + long childCount = count(Wrappers.lambdaQuery().in(SysAreaEntity::getPid, adcodes)); + if (childCount > 0) { + return R.failed("所选行政区划包含下级节点,请先处理下级行政区划"); + } + if (!removeByIds(uniqueIds)) { + throw new IllegalStateException("行政区划删除失败"); + } + return R.ok(true); + } + + private List toManageList(List areas) { + if (CollUtil.isEmpty(areas)) { + return List.of(); + } + + Set parentCodes = areas.stream().map(SysAreaEntity::getAdcode).collect(Collectors.toSet()); + Map childCounts = baseMapper.selectChildCounts(parentCodes).stream() + .collect(Collectors.toMap(SysAreaChildCountVO::getPid, SysAreaChildCountVO::getChildCount)); + Map areaMap = loadAncestorMap(areas); + + return areas.stream() + .map(area -> toManageVO(area, childCounts.getOrDefault(area.getAdcode(), 0L), areaMap)) + .toList(); + } + + private Map loadAncestorMap(List areas) { + Map areaMap = areas.stream() + .collect(Collectors.toMap(SysAreaEntity::getAdcode, Function.identity(), (left, right) -> left)); + Set pendingCodes = areas.stream() + .map(SysAreaEntity::getPid) + .filter(code -> Objects.nonNull(code) && code > 0 && !areaMap.containsKey(code)) + .collect(Collectors.toSet()); + + while (CollUtil.isNotEmpty(pendingCodes)) { + List parents = list(Wrappers.lambdaQuery() + .in(SysAreaEntity::getAdcode, pendingCodes)); + if (CollUtil.isEmpty(parents)) { + break; + } + parents.forEach(parent -> areaMap.put(parent.getAdcode(), parent)); + pendingCodes = parents.stream() + .map(SysAreaEntity::getPid) + .filter(code -> Objects.nonNull(code) && code > 0 && !areaMap.containsKey(code)) + .collect(Collectors.toSet()); + } + return areaMap; + } + + private SysAreaManageVO toManageVO(SysAreaEntity area, long childCount, + Map areaMap) { + SysAreaManageVO vo = new SysAreaManageVO(); + vo.setId(area.getId()); + vo.setPid(area.getPid()); + vo.setAdcode(area.getAdcode()); + vo.setName(area.getName()); + vo.setAreaType(area.getAreaType()); + vo.setHot(area.getHot()); + vo.setAreaStatus(area.getAreaStatus()); + vo.setAreaSort(area.getAreaSort()); + vo.setHasChildren(childCount > 0); + vo.setChildCount(childCount); + + List path = buildPath(area, areaMap); + vo.setPathCodes(path.stream().map(SysAreaEntity::getAdcode).toList()); + vo.setPathName(path.stream().map(SysAreaEntity::getName).collect(Collectors.joining(StrUtil.SPACE + "/" + StrUtil.SPACE))); + return vo; + } + + private List buildPath(SysAreaEntity area, Map areaMap) { + LinkedList path = new LinkedList<>(); + Set visitedCodes = new HashSet<>(); + SysAreaEntity current = area; + while (Objects.nonNull(current) && visitedCodes.add(current.getAdcode())) { + path.addFirst(current); + current = areaMap.get(current.getPid()); + } + return path; + } + + private List buildSortUpdates(List orderedIds, Map areaMap) { + List updates = new ArrayList<>(orderedIds.size()); + for (int index = 0; index < orderedIds.size(); index++) { + SysAreaEntity original = areaMap.get(orderedIds.get(index)); + SysAreaEntity update = new SysAreaEntity(); + update.setId(original.getId()); + update.setAreaSort((long) orderedIds.size() - index); + updates.add(update); + } + return updates; + } + + private Long parseAdcode(String keyword) { + try { + return Long.valueOf(keyword); + } + catch (NumberFormatException ignored) { + return null; + } + } + } diff --git a/pigx-upms/pigx-upms-biz/src/main/resources/mapper/SysAreaMapper.xml b/pigx-upms/pigx-upms-biz/src/main/resources/mapper/SysAreaMapper.xml index 3a39091bb..bd075c565 100644 --- a/pigx-upms/pigx-upms-biz/src/main/resources/mapper/SysAreaMapper.xml +++ b/pigx-upms/pigx-upms-biz/src/main/resources/mapper/SysAreaMapper.xml @@ -21,4 +21,20 @@ + + + + + + + diff --git a/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImplTests.java b/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImplTests.java new file mode 100644 index 000000000..27179313c --- /dev/null +++ b/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysAreaServiceImplTests.java @@ -0,0 +1,180 @@ +package com.pig4cloud.pigx.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.pig4cloud.pigx.admin.api.dto.SysAreaSortDTO; +import com.pig4cloud.pigx.admin.api.dto.SysAreaUpdateDTO; +import com.pig4cloud.pigx.admin.api.entity.SysAreaEntity; +import com.pig4cloud.pigx.admin.api.vo.SysAreaChildCountVO; +import com.pig4cloud.pigx.admin.api.vo.SysAreaManageVO; +import com.pig4cloud.pigx.admin.mapper.SysAreaMapper; +import com.pig4cloud.pigx.common.core.util.R; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SysAreaServiceImplTests { + + @Test + void saveAreaUsesSubmittedEntityWithoutExtraValidation() { + SysAreaServiceImpl service = spy(new SysAreaServiceImpl()); + SysAreaEntity area = area(3L, null, null); + doReturn(List.of()).when(service).list(any(Wrapper.class)); + doReturn(true).when(service).save(area); + doReturn(true).when(service).updateBatchById(any(Collection.class)); + + R result = service.saveArea(area); + + assertThat(result.isOk()).isTrue(); + assertThat(area.getPid()).isEqualTo(100000L); + assertThat(area.getAreaSort()).isZero(); + verify(service).save(area); + } + + @Test + void listManageChildrenLoadsChildCountsFromMapper() { + SysAreaServiceImpl service = new SysAreaServiceImpl(); + SysAreaMapper mapper = mock(SysAreaMapper.class); + ReflectionTestUtils.setField(service, "baseMapper", mapper); + SysAreaEntity root = area(1L, 100000L, 0L); + root.setName("全国"); + SysAreaChildCountVO childCount = new SysAreaChildCountVO(); + childCount.setPid(100000L); + childCount.setChildCount(32L); + when(mapper.selectList(any(Wrapper.class))).thenReturn(List.of(root)); + when(mapper.selectChildCounts(Set.of(100000L))).thenReturn(List.of(childCount)); + + List result = service.listManageChildren(0L); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getChildCount()).isEqualTo(32L); + assertThat(result.get(0).getHasChildren()).isTrue(); + verify(mapper).selectChildCounts(Set.of(100000L)); + } + + @Test + void removeAreasRejectsEmptyIds() { + SysAreaServiceImpl service = new SysAreaServiceImpl(); + + R result = service.removeAreas(List.of()); + + assertThat(result.isOk()).isFalse(); + assertThat(result.getMsg()).contains("不能为空"); + } + + @Test + void updateAreaRejectsNationwideRoot() { + SysAreaServiceImpl service = new SysAreaServiceImpl(); + SysAreaMapper mapper = mock(SysAreaMapper.class); + ReflectionTestUtils.setField(service, "baseMapper", mapper); + when(mapper.selectById(1L)).thenReturn(area(1L, 100000L, 0L)); + SysAreaUpdateDTO update = new SysAreaUpdateDTO(); + update.setId(1L); + update.setName("全国"); + update.setAreaStatus("1"); + update.setHot("0"); + + R result = service.updateArea(update); + + assertThat(result.isOk()).isFalse(); + assertThat(result.getMsg()).contains("根节点"); + } + + @Test + void updateAreaWritesOnlyEditableFields() { + SysAreaServiceImpl service = new SysAreaServiceImpl(); + SysAreaMapper mapper = mock(SysAreaMapper.class); + ReflectionTestUtils.setField(service, "baseMapper", mapper); + when(mapper.selectById(2L)).thenReturn(area(2L, 110000L, 100000L)); + when(mapper.updateById(any(SysAreaEntity.class))).thenReturn(1); + SysAreaUpdateDTO update = new SysAreaUpdateDTO(); + update.setId(2L); + update.setName(" 北京市 "); + update.setAreaStatus("1"); + update.setHot("1"); + + R result = service.updateArea(update); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SysAreaEntity.class); + verify(mapper).updateById(captor.capture()); + SysAreaEntity saved = captor.getValue(); + assertThat(result.isOk()).isTrue(); + assertThat(saved.getName()).isEqualTo("北京市"); + assertThat(saved.getAreaStatus()).isEqualTo("1"); + assertThat(saved.getHot()).isEqualTo("1"); + assertThat(saved.getPid()).isNull(); + assertThat(saved.getAdcode()).isNull(); + assertThat(saved.getAreaType()).isNull(); + assertThat(saved.getAreaSort()).isNull(); + } + + @Test + void updateAreaSortRejectsDuplicateIds() { + SysAreaServiceImpl service = new SysAreaServiceImpl(); + SysAreaSortDTO sortDTO = new SysAreaSortDTO(); + sortDTO.setPid(100000L); + sortDTO.setAreaIds(List.of(1L, 1L)); + + R result = service.updateAreaSort(sortDTO); + + assertThat(result.isOk()).isFalse(); + assertThat(result.getMsg()).contains("不能重复"); + } + + @Test + @SuppressWarnings("unchecked") + void updateAreaSortWritesDescendingValuesInVisualOrder() { + SysAreaServiceImpl service = spy(new SysAreaServiceImpl()); + SysAreaEntity first = area(1L, 110000L, 100000L); + SysAreaEntity second = area(2L, 120000L, 100000L); + doReturn(List.of(first, second)).when(service).list(any(Wrapper.class)); + doReturn(true).when(service).updateBatchById(any(Collection.class)); + + SysAreaSortDTO sortDTO = new SysAreaSortDTO(); + sortDTO.setPid(100000L); + sortDTO.setAreaIds(List.of(2L, 1L)); + + R result = service.updateAreaSort(sortDTO); + ArgumentCaptor> updatesCaptor = ArgumentCaptor.forClass(Collection.class); + verify(service).updateBatchById(updatesCaptor.capture()); + List updates = updatesCaptor.getValue().stream().toList(); + + assertThat(result.isOk()).isTrue(); + assertThat(result.getData()).isEqualTo(true); + assertThat(updates).extracting(SysAreaEntity::getId).containsExactly(2L, 1L); + assertThat(updates).extracting(SysAreaEntity::getAreaSort).containsExactly(2L, 1L); + } + + @Test + @SuppressWarnings("unchecked") + void removeAreasRejectsNationwideRoot() { + SysAreaServiceImpl service = spy(new SysAreaServiceImpl()); + SysAreaEntity root = area(1L, 100000L, 0L); + doReturn(List.of(root)).when(service).listByIds(any(Collection.class)); + + R result = service.removeAreas(List.of(1L)); + + assertThat(result.isOk()).isFalse(); + assertThat(result.getMsg()).contains("根节点"); + } + + private SysAreaEntity area(Long id, Long adcode, Long pid) { + SysAreaEntity area = new SysAreaEntity(); + area.setId(id); + area.setAdcode(adcode); + area.setPid(pid); + return area; + } + +}