feat: 新增支出类型管理模块

- 新增 ExpenseType 实体类
- 新增 ExpenseTypeMapper 和 ExpenseTypeDataService
- 新增 ExpenseTypeDTO 和 ExpenseTypeVO
- 新增 ExpenseTypeService 业务服务层(支持树形结构)
- 新增 ExpenseTypeController REST API
- 数据库创建 expense_type 表并插入初始数据(5个一级类型+12个二级类型)
- API测试通过:树形查询、子类型查询、创建类型
This commit is contained in:
zhangjf 2026-02-17 17:12:02 +08:00
parent 0c87462b68
commit 33d7cc2145
9 changed files with 702 additions and 0 deletions

View File

@ -0,0 +1,80 @@
package com.fundplatform.exp.controller;
import com.fundplatform.common.core.Result;
import com.fundplatform.exp.dto.ExpenseTypeDTO;
import com.fundplatform.exp.service.ExpenseTypeService;
import com.fundplatform.exp.vo.ExpenseTypeVO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 支出类型管理Controller
*/
@RestController
@RequestMapping("/api/v1/exp/expense-type")
public class ExpenseTypeController {
private final ExpenseTypeService typeService;
public ExpenseTypeController(ExpenseTypeService typeService) {
this.typeService = typeService;
}
/**
* 创建支出类型
*/
@PostMapping
public Result<Long> create(@Valid @RequestBody ExpenseTypeDTO dto) {
return Result.success(typeService.createType(dto));
}
/**
* 更新支出类型
*/
@PutMapping
public Result<Boolean> update(@Valid @RequestBody ExpenseTypeDTO dto) {
return Result.success(typeService.updateType(dto));
}
/**
* 根据ID查询支出类型
*/
@GetMapping("/{id}")
public Result<ExpenseTypeVO> getById(@PathVariable Long id) {
return Result.success(typeService.getTypeById(id));
}
/**
* 查询支出类型树形结构
*/
@GetMapping("/tree")
public Result<List<ExpenseTypeVO>> getTree() {
return Result.success(typeService.getTypeTree());
}
/**
* 查询指定父级的子类型列表
*/
@GetMapping("/children")
public Result<List<ExpenseTypeVO>> getChildren(@RequestParam(required = false) Long parentId) {
return Result.success(typeService.getChildrenByParentId(parentId));
}
/**
* 删除支出类型
*/
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(typeService.deleteType(id));
}
/**
* 更新状态
*/
@PutMapping("/{id}/status")
public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
return Result.success(typeService.updateStatus(id, status));
}
}

View File

@ -0,0 +1,88 @@
package com.fundplatform.exp.data.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fundplatform.common.core.BaseEntity;
/**
* 支出类型实体
*/
@TableName("expense_type")
public class ExpenseType extends BaseEntity {
/** 支出类型编码 */
private String typeCode;
/** 支出类型名称 */
private String typeName;
/** 父类型ID0表示一级类型 */
private Long parentId;
/** 类型层级 */
private Integer typeLevel;
/** 排序顺序 */
private Integer sortOrder;
/** 类型描述 */
private String description;
/** 状态0-禁用1-启用) */
private Integer status;
public String getTypeCode() {
return typeCode;
}
public void setTypeCode(String typeCode) {
this.typeCode = typeCode;
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public Integer getTypeLevel() {
return typeLevel;
}
public void setTypeLevel(Integer typeLevel) {
this.typeLevel = typeLevel;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}

View File

@ -0,0 +1,9 @@
package com.fundplatform.exp.data.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fundplatform.exp.data.entity.ExpenseType;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ExpenseTypeMapper extends BaseMapper<ExpenseType> {
}

View File

@ -0,0 +1,11 @@
package com.fundplatform.exp.data.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fundplatform.exp.data.entity.ExpenseType;
import com.fundplatform.exp.data.mapper.ExpenseTypeMapper;
import org.springframework.stereotype.Service;
@Service
public class ExpenseTypeDataService extends ServiceImpl<ExpenseTypeMapper, ExpenseType> implements IService<ExpenseType> {
}

View File

@ -0,0 +1,102 @@
package com.fundplatform.exp.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class ExpenseTypeDTO {
private Long id;
@Size(max = 50, message = "类型编码不能超过50个字符")
private String typeCode;
@NotBlank(message = "类型名称不能为空")
@Size(max = 100, message = "类型名称不能超过100个字符")
private String typeName;
private Long parentId = 0L;
private Integer typeLevel = 1;
private Integer sortOrder = 0;
@Size(max = 200, message = "类型描述不能超过200个字符")
private String description;
private Integer status = 1;
private String remark;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTypeCode() {
return typeCode;
}
public void setTypeCode(String typeCode) {
this.typeCode = typeCode;
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public Integer getTypeLevel() {
return typeLevel;
}
public void setTypeLevel(Integer typeLevel) {
this.typeLevel = typeLevel;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,44 @@
package com.fundplatform.exp.service;
import com.fundplatform.exp.dto.ExpenseTypeDTO;
import com.fundplatform.exp.vo.ExpenseTypeVO;
import java.util.List;
public interface ExpenseTypeService {
/**
* 创建支出类型
*/
Long createType(ExpenseTypeDTO dto);
/**
* 更新支出类型
*/
boolean updateType(ExpenseTypeDTO dto);
/**
* 根据ID查询支出类型
*/
ExpenseTypeVO getTypeById(Long id);
/**
* 查询支出类型树形结构
*/
List<ExpenseTypeVO> getTypeTree();
/**
* 查询指定父级的子类型列表
*/
List<ExpenseTypeVO> getChildrenByParentId(Long parentId);
/**
* 删除支出类型
*/
boolean deleteType(Long id);
/**
* 更新状态
*/
boolean updateStatus(Long id, Integer status);
}

View File

@ -0,0 +1,181 @@
package com.fundplatform.exp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fundplatform.common.context.TenantContextHolder;
import com.fundplatform.common.context.UserContextHolder;
import com.fundplatform.exp.data.entity.ExpenseType;
import com.fundplatform.exp.data.service.ExpenseTypeDataService;
import com.fundplatform.exp.dto.ExpenseTypeDTO;
import com.fundplatform.exp.service.ExpenseTypeService;
import com.fundplatform.exp.vo.ExpenseTypeVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class ExpenseTypeServiceImpl implements ExpenseTypeService {
private static final Logger log = LoggerFactory.getLogger(ExpenseTypeServiceImpl.class);
private final ExpenseTypeDataService typeDataService;
public ExpenseTypeServiceImpl(ExpenseTypeDataService typeDataService) {
this.typeDataService = typeDataService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createType(ExpenseTypeDTO dto) {
ExpenseType type = new ExpenseType();
type.setTypeCode(dto.getTypeCode());
type.setTypeName(dto.getTypeName());
type.setParentId(dto.getParentId() != null ? dto.getParentId() : 0L);
type.setTypeLevel(dto.getTypeLevel() != null ? dto.getTypeLevel() : 1);
type.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0);
type.setDescription(dto.getDescription());
type.setStatus(dto.getStatus() != null ? dto.getStatus() : 1);
type.setDeleted(0);
type.setCreatedTime(LocalDateTime.now());
type.setTenantId(TenantContextHolder.getTenantId());
type.setCreatedBy(UserContextHolder.getUserId());
typeDataService.save(type);
log.info("创建支出类型成功: id={}, typeName={}", type.getId(), type.getTypeName());
return type.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateType(ExpenseTypeDTO dto) {
if (dto.getId() == null) {
throw new RuntimeException("类型ID不能为空");
}
ExpenseType existing = typeDataService.getById(dto.getId());
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出类型不存在");
}
ExpenseType type = new ExpenseType();
type.setId(dto.getId());
type.setTypeCode(dto.getTypeCode());
type.setTypeName(dto.getTypeName());
type.setSortOrder(dto.getSortOrder());
type.setDescription(dto.getDescription());
type.setUpdatedTime(LocalDateTime.now());
type.setUpdatedBy(UserContextHolder.getUserId());
return typeDataService.updateById(type);
}
@Override
public ExpenseTypeVO getTypeById(Long id) {
ExpenseType type = typeDataService.getById(id);
if (type == null || type.getDeleted() == 1) {
throw new RuntimeException("支出类型不存在");
}
return convertToVO(type);
}
@Override
public List<ExpenseTypeVO> getTypeTree() {
LambdaQueryWrapper<ExpenseType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ExpenseType::getDeleted, 0)
.eq(ExpenseType::getStatus, 1)
.orderByAsc(ExpenseType::getTypeLevel)
.orderByAsc(ExpenseType::getSortOrder);
List<ExpenseType> types = typeDataService.list(wrapper);
return buildTree(types, 0L);
}
@Override
public List<ExpenseTypeVO> getChildrenByParentId(Long parentId) {
LambdaQueryWrapper<ExpenseType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ExpenseType::getDeleted, 0)
.eq(ExpenseType::getParentId, parentId != null ? parentId : 0L)
.orderByAsc(ExpenseType::getSortOrder);
List<ExpenseType> types = typeDataService.list(wrapper);
return types.stream().map(this::convertToVO).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteType(Long id) {
// 检查是否有子类型
LambdaQueryWrapper<ExpenseType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ExpenseType::getParentId, id).eq(ExpenseType::getDeleted, 0);
long childCount = typeDataService.count(wrapper);
if (childCount > 0) {
throw new RuntimeException("该类型下存在子类型,无法删除");
}
LambdaUpdateWrapper<ExpenseType> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ExpenseType::getId, id)
.set(ExpenseType::getDeleted, 1)
.set(ExpenseType::getUpdatedTime, LocalDateTime.now());
return typeDataService.update(updateWrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStatus(Long id, Integer status) {
LambdaUpdateWrapper<ExpenseType> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(ExpenseType::getId, id)
.set(ExpenseType::getStatus, status)
.set(ExpenseType::getUpdatedTime, LocalDateTime.now());
return typeDataService.update(wrapper);
}
/**
* 构建树形结构
*/
private List<ExpenseTypeVO> buildTree(List<ExpenseType> types, Long parentId) {
List<ExpenseTypeVO> tree = new ArrayList<>();
Map<Long, List<ExpenseType>> groupedByParent = types.stream()
.collect(Collectors.groupingBy(ExpenseType::getParentId));
List<ExpenseType> rootTypes = groupedByParent.getOrDefault(parentId, new ArrayList<>());
for (ExpenseType type : rootTypes) {
ExpenseTypeVO vo = convertToVO(type);
vo.setChildren(buildChildren(groupedByParent, type.getId()));
tree.add(vo);
}
return tree;
}
private List<ExpenseTypeVO> buildChildren(Map<Long, List<ExpenseType>> groupedByParent, Long parentId) {
List<ExpenseType> children = groupedByParent.getOrDefault(parentId, new ArrayList<>());
return children.stream().map(type -> {
ExpenseTypeVO vo = convertToVO(type);
vo.setChildren(buildChildren(groupedByParent, type.getId()));
return vo;
}).collect(Collectors.toList());
}
private ExpenseTypeVO convertToVO(ExpenseType type) {
ExpenseTypeVO vo = new ExpenseTypeVO();
vo.setId(type.getId());
vo.setTypeCode(type.getTypeCode());
vo.setTypeName(type.getTypeName());
vo.setParentId(type.getParentId());
vo.setTypeLevel(type.getTypeLevel());
vo.setSortOrder(type.getSortOrder());
vo.setDescription(type.getDescription());
vo.setStatus(type.getStatus());
vo.setTenantId(type.getTenantId());
vo.setCreatedBy(type.getCreatedBy());
vo.setCreatedTime(type.getCreatedTime());
vo.setUpdatedTime(type.getUpdatedTime());
vo.setRemark(type.getRemark());
return vo;
}
}

View File

@ -0,0 +1,145 @@
package com.fundplatform.exp.vo;
import java.time.LocalDateTime;
import java.util.List;
public class ExpenseTypeVO {
private Long id;
private String typeCode;
private String typeName;
private Long parentId;
private String parentName;
private Integer typeLevel;
private Integer sortOrder;
private String description;
private Integer status;
private Long tenantId;
private Long createdBy;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
private String remark;
/** 子节点列表(用于树形结构) */
private List<ExpenseTypeVO> children;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTypeCode() {
return typeCode;
}
public void setTypeCode(String typeCode) {
this.typeCode = typeCode;
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public String getParentName() {
return parentName;
}
public void setParentName(String parentName) {
this.parentName = parentName;
}
public Integer getTypeLevel() {
return typeLevel;
}
public void setTypeLevel(Integer typeLevel) {
this.typeLevel = typeLevel;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getCreatedTime() {
return createdTime;
}
public void setCreatedTime(LocalDateTime createdTime) {
this.createdTime = createdTime;
}
public LocalDateTime getUpdatedTime() {
return updatedTime;
}
public void setUpdatedTime(LocalDateTime updatedTime) {
this.updatedTime = updatedTime;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public List<ExpenseTypeVO> getChildren() {
return children;
}
public void setChildren(List<ExpenseTypeVO> children) {
this.children = children;
}
}

View File

@ -422,3 +422,45 @@ Error starting ApplicationContext. To display the condition evaluation report re
2026-02-17 16:39:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 16:39:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 16:39:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 活跃连接数: 0
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 空闲连接数: 5
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 16:44:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5
2026-02-17 16:49:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-17 16:49:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-17 16:49:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 活跃连接数: 0
2026-02-17 16:49:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 空闲连接数: 5
2026-02-17 16:49:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 16:49:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 16:49:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5
2026-02-17 16:54:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-17 16:54:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-17 16:54:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 活跃连接数: 0
2026-02-17 16:54:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 空闲连接数: 5
2026-02-17 16:54:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 16:54:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 16:54:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5
2026-02-17 16:59:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-17 16:59:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-17 16:59:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 活跃连接数: 0
2026-02-17 16:59:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 空闲连接数: 5
2026-02-17 16:59:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 16:59:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 16:59:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5
2026-02-17 17:04:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-17 17:04:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-17 17:04:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 活跃连接数: 0
2026-02-17 17:04:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 空闲连接数: 5
2026-02-17 17:04:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 17:04:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 17:04:42.665 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 活跃连接数: 0
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 空闲连接数: 5
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 等待获取连接的线程数: 0
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最大连接数: 20
2026-02-17 17:09:42.664 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 最小空闲连接数: 5