package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.bean.dto.AccountSubjectDto;
import com.ruoyi.account.bean.dto.AccountSubjectImportDto;
import com.ruoyi.account.bean.vo.AccountSubjectVo;
import com.ruoyi.account.mapper.AccountSubjectMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.service.AccountSubjectService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
*
* 总账科目表 服务实现类
*
*
* @author 芯导软件(江苏)有限公司
* @since 2026-05-07 04:45:30
*/
@Service
@RequiredArgsConstructor
public class AccountSubjectServiceImpl extends ServiceImpl implements AccountSubjectService {
private final AccountSubjectMapper accountSubjectMapper;
@Override
public IPage baseList(Page page, AccountSubjectDto accountSubjectDto) {
Page requestPage = page == null ? new Page<>(1, 10) : page;
List allSubjects = list(loadBaseQueryWrapper(accountSubjectDto));
List filteredSubjects = applyTreeFilter(allSubjects, accountSubjectDto);
List fullTree = buildTree(filteredSubjects);
long current = requestPage.getCurrent() <= 0 ? 1 : requestPage.getCurrent();
long size = requestPage.getSize() <= 0 ? 10 : requestPage.getSize();
int fromIndex = (int) Math.min((current - 1) * size, fullTree.size());
int toIndex = (int) Math.min(fromIndex + size, fullTree.size());
List pagedRoots = fromIndex >= toIndex
? Collections.emptyList()
: fullTree.subList(fromIndex, toIndex);
Page resultPage = new Page<>(current, size, fullTree.size());
resultPage.setRecords(pagedRoots);
return resultPage;
}
@Override
public Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto) {
validateSubjectRequiredFields(accountSubjectDto);
validateSubjectCodeUnique(accountSubjectDto, false);
validateParent(accountSubjectDto.getParentId(), null);
if (accountSubjectDto.getStatus() == null) {
accountSubjectDto.setStatus(0);
}
return save(accountSubjectDto);
}
@Override
public Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto) {
if (accountSubjectDto == null || accountSubjectDto.getId() == null) {
throw new ServiceException("修改失败,科目ID不能为空");
}
if (getById(accountSubjectDto.getId()) == null) {
throw new ServiceException("修改失败,未找到对应科目");
}
validateParent(accountSubjectDto.getParentId(), accountSubjectDto.getId());
validateSubjectRequiredFields(accountSubjectDto);
validateSubjectCodeUnique(accountSubjectDto, true);
return updateById(accountSubjectDto);
}
@Override
public Boolean removeAccountSubjectByIds(List ids) {
if (ids == null || ids.isEmpty()) {
return true;
}
List allSubjects = list();
if (allSubjects == null || allSubjects.isEmpty()) {
return true;
}
Map> childrenIdMap = buildChildrenIdMap(allSubjects);
Set removeIds = new LinkedHashSet<>();
for (Long id : ids) {
collectDescendantIds(id, childrenIdMap, removeIds);
}
if (removeIds.isEmpty()) {
return true;
}
List subjectCodes = allSubjects.stream()
.filter(subject -> removeIds.contains(subject.getId()))
.map(AccountSubject::getSubjectCode)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
if (!subjectCodes.isEmpty()) {
Long referencedCount = accountSubjectMapper.countReferencedBySubjectCodes(subjectCodes);
if (referencedCount != null && referencedCount > 0) {
throw new ServiceException("删除失败,科目已被凭证分录引用");
}
}
return removeByIds(removeIds);
}
@Override
public void exportAccountSubject(HttpServletResponse response) {
List list = accountSubjectMapper.selectList(null);
List importDtos = list.stream().map(accountSubject -> {
AccountSubjectImportDto accountSubjectImportDto = new AccountSubjectImportDto();
BeanUtils.copyProperties(accountSubject, accountSubjectImportDto);
return accountSubjectImportDto;
}).collect(Collectors.toList());
ExcelUtil util = new ExcelUtil<>(AccountSubjectImportDto.class);
util.exportExcel(response, importDtos , "总账科目");
}
/**
* 校验科目必填字段,避免脏数据写入。
*/
private void validateSubjectRequiredFields(AccountSubjectDto accountSubjectDto) {
if (accountSubjectDto == null) {
throw new ServiceException("总账科目数据不能为空");
}
if (StringUtils.isEmpty(accountSubjectDto.getSubjectCode())) {
throw new ServiceException("科目编码不能为空");
}
if (StringUtils.isEmpty(accountSubjectDto.getSubjectName())) {
throw new ServiceException("科目名称不能为空");
}
if (StringUtils.isEmpty(accountSubjectDto.getSubjectType())) {
throw new ServiceException("科目类型不能为空");
}
}
/**
* 校验科目编码唯一,新增和修改都要执行。
*/
private void validateSubjectCodeUnique(AccountSubjectDto accountSubjectDto, boolean isUpdate) {
LambdaQueryWrapper codeQueryWrapper = new LambdaQueryWrapper<>();
codeQueryWrapper.eq(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
if (isUpdate) {
codeQueryWrapper.ne(AccountSubject::getId, accountSubjectDto.getId());
}
AccountSubject exists = getOne(codeQueryWrapper, false);
if (Objects.nonNull(exists)) {
throw new ServiceException("科目编码已存在,请勿重复提交");
}
}
/**
* 仅按通用过滤条件查询基础数据(树形过滤后续再做)。
*/
private LambdaQueryWrapper loadBaseQueryWrapper(AccountSubjectDto accountSubjectDto) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
if (accountSubjectDto != null && accountSubjectDto.getStatus() != null) {
queryWrapper.eq(AccountSubject::getStatus, accountSubjectDto.getStatus());
}
queryWrapper.orderByAsc(AccountSubject::getSubjectCode).orderByAsc(AccountSubject::getId);
return queryWrapper;
}
/**
* 树形过滤:命中节点后保留其父链与子树,保证递归结构完整。
*/
private List applyTreeFilter(List allSubjects, AccountSubjectDto queryDto) {
if (allSubjects == null || allSubjects.isEmpty()) {
return Collections.emptyList();
}
boolean hasFilter = queryDto != null && (
StringUtils.isNotEmpty(queryDto.getSubjectCode())
|| StringUtils.isNotEmpty(queryDto.getSubjectName())
|| StringUtils.isNotEmpty(queryDto.getSubjectType())
);
if (!hasFilter) {
return allSubjects;
}
Map subjectMap = allSubjects.stream()
.filter(item -> item.getId() != null)
.collect(Collectors.toMap(AccountSubject::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
Map> childrenMap = buildChildrenMap(allSubjects);
Set matchedIds = new LinkedHashSet<>();
for (AccountSubject subject : allSubjects) {
if (subject.getId() == null) {
continue;
}
if (matchesFilter(subject, queryDto)) {
matchedIds.add(subject.getId());
}
}
if (matchedIds.isEmpty()) {
return Collections.emptyList();
}
Set resultIds = new LinkedHashSet<>(matchedIds);
for (Long matchedId : matchedIds) {
addAncestors(matchedId, subjectMap, resultIds);
addDescendants(matchedId, childrenMap, resultIds);
}
return allSubjects.stream()
.filter(item -> item.getId() != null && resultIds.contains(item.getId()))
.collect(Collectors.toList());
}
private boolean matchesFilter(AccountSubject subject, AccountSubjectDto queryDto) {
if (queryDto == null) {
return true;
}
if (StringUtils.isNotEmpty(queryDto.getSubjectCode())
&& (subject.getSubjectCode() == null || !subject.getSubjectCode().contains(queryDto.getSubjectCode()))) {
return false;
}
if (StringUtils.isNotEmpty(queryDto.getSubjectName())
&& (subject.getSubjectName() == null || !subject.getSubjectName().contains(queryDto.getSubjectName()))) {
return false;
}
if (StringUtils.isNotEmpty(queryDto.getSubjectType())
&& !queryDto.getSubjectType().equals(subject.getSubjectType())) {
return false;
}
return true;
}
private void addAncestors(Long subjectId, Map subjectMap, Set resultIds) {
AccountSubject current = subjectMap.get(subjectId);
if (current == null) {
return;
}
Long parentId = current.getParentId();
while (parentId != null && parentId > 0) {
AccountSubject parent = subjectMap.get(parentId);
if (parent == null) {
break;
}
if (!resultIds.add(parent.getId())) {
break;
}
parentId = parent.getParentId();
}
}
private void addDescendants(Long subjectId, Map> childrenMap, Set resultIds) {
List children = childrenMap.getOrDefault(subjectId, Collections.emptyList());
for (AccountSubject child : children) {
if (child.getId() == null) {
continue;
}
if (resultIds.add(child.getId())) {
addDescendants(child.getId(), childrenMap, resultIds);
}
}
}
private Map> buildChildrenMap(List subjects) {
Map> childrenMap = new HashMap<>();
for (AccountSubject subject : subjects) {
if (subject.getId() == null) {
continue;
}
Long parentId = subject.getParentId();
if (parentId == null || parentId <= 0) {
continue;
}
childrenMap.computeIfAbsent(parentId, key -> new ArrayList<>()).add(subject);
}
return childrenMap;
}
/**
* 基于 parentId 递归构建科目树。
*/
private List buildTree(List subjects) {
if (subjects == null || subjects.isEmpty()) {
return Collections.emptyList();
}
List sortedSubjects = new ArrayList<>(subjects);
sortedSubjects.sort(Comparator
.comparing(AccountSubject::getSubjectCode, Comparator.nullsLast(String::compareTo))
.thenComparing(AccountSubject::getId, Comparator.nullsLast(Long::compareTo)));
Map subjectVoMap = new LinkedHashMap<>();
for (AccountSubject subject : sortedSubjects) {
if (subject.getId() == null) {
continue;
}
AccountSubjectVo vo = new AccountSubjectVo();
BeanUtils.copyProperties(subject, vo);
subjectVoMap.put(subject.getId(), vo);
}
List roots = new ArrayList<>();
for (AccountSubject subject : sortedSubjects) {
if (subject.getId() == null) {
continue;
}
AccountSubjectVo current = subjectVoMap.get(subject.getId());
Long parentId = subject.getParentId();
if (parentId != null && parentId > 0 && subjectVoMap.containsKey(parentId)) {
subjectVoMap.get(parentId).getChildren().add(current);
} else {
roots.add(current);
}
}
markLeafRecursively(roots);
return roots;
}
private void markLeafRecursively(List nodes) {
for (AccountSubjectVo node : nodes) {
List children = node.getChildren();
node.setLeaf(children == null || children.isEmpty());
if (children != null && !children.isEmpty()) {
markLeafRecursively(children);
}
}
}
/**
* 校验父子关系:父节点必须存在,且不能形成循环引用。
*/
private void validateParent(Long parentId, Long currentId) {
if (parentId == null || parentId <= 0) {
return;
}
if (currentId != null && parentId.equals(currentId)) {
throw new ServiceException("父科目不能选择自身");
}
AccountSubject parent = getById(parentId);
if (parent == null) {
throw new ServiceException("父科目不存在,请重新选择");
}
// 防止形成环:更新时,父节点不能是当前节点的任意子孙节点。
if (currentId != null) {
Set visited = new HashSet<>();
Long traceParentId = parentId;
while (traceParentId != null && traceParentId > 0) {
if (!visited.add(traceParentId)) {
throw new ServiceException("科目层级存在循环引用,请检查父科目设置");
}
if (traceParentId.equals(currentId)) {
throw new ServiceException("父科目不能是当前科目或其子科目");
}
AccountSubject traceNode = getById(traceParentId);
if (traceNode == null) {
break;
}
traceParentId = traceNode.getParentId();
}
}
}
private Map> buildChildrenIdMap(List subjects) {
Map> map = new HashMap<>();
for (AccountSubject subject : subjects) {
if (subject.getId() == null || subject.getParentId() == null || subject.getParentId() <= 0) {
continue;
}
map.computeIfAbsent(subject.getParentId(), key -> new ArrayList<>()).add(subject.getId());
}
return map;
}
/**
* 收集待删除节点及其所有子孙节点。
*/
private void collectDescendantIds(Long id, Map> childrenIdMap, Set result) {
if (id == null || !result.add(id)) {
return;
}
List children = childrenIdMap.getOrDefault(id, Collections.emptyList());
for (Long childId : children) {
collectDescendantIds(childId, childrenIdMap, result);
}
}
}