Compare commits

...

9 Commits

19 changed files with 614 additions and 0 deletions

View File

@ -0,0 +1,29 @@
package net.rzdata.demo.trait;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class GetReq<T> {
private Integer pageSize = 15;
private Integer pageNum = 1;
private String orderBy;
private Boolean asc = true;
public IPage<T> into() {
Page<T> page = new Page<>();
page.setSize(pageSize);
page.setCurrent(pageNum);
if (orderBy != null) {
page.addOrder(this.asc ? OrderItem.asc(orderBy) : OrderItem.desc(orderBy));
}
return page;
}
}

View File

@ -0,0 +1,18 @@
package net.rzdata.demo.trait;
import java.util.List;
public interface TreeNode<T extends TreeNode<T, ID>, ID> {
ID getId();
ID getParentId();
List<T> getChildren();
void setChildren(List<T> children);
default boolean isLeaf() {
return getChildren() == null;
}
}

View File

@ -0,0 +1,18 @@
package net.rzdata.exception;
/**
* 操作不存在的数据异常
*/
public class NotExistDataException extends ClientException {
public static final String ERROR_CODE = "A0429";
public static final String ERROR_MESSAGE = "数据不存在";
public NotExistDataException() {
super(ERROR_CODE, ERROR_MESSAGE);
}
public NotExistDataException(String message) {
super(ERROR_CODE, message);
}
}

View File

@ -0,0 +1,18 @@
package net.rzdata.exception;
/**
* 数据重复异常
*/
public class RepeatDataException extends ClientException {
public static final String ERROR_CODE = "A0428";
public static final String ERROR_MESSAGE = "数据已存在";
public RepeatDataException() {
super(ERROR_CODE, ERROR_MESSAGE);
}
public RepeatDataException(String message) {
super(ERROR_CODE, message);
}
}

View File

@ -18,4 +18,10 @@
<app.port>4567</app.port>
</properties>
<dependencies>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,100 @@
package net.rzdata.demo.dict;
import com.baomidou.mybatisplus.core.metadata.IPage;
import net.rzdata.demo.dict.domain.AddDictReq;
import net.rzdata.demo.dict.domain.Dict;
import net.rzdata.demo.dict.domain.GetDictTypeListReq;
import net.rzdata.demo.dict.domain.UpdateDictReq;
import net.rzdata.domain.Id;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
@RestController
@RequestMapping("/dict")
public class DictController {
private final DictService dictService;
public DictController(DictService dictService) {
this.dictService = dictService;
}
/**
* 新增字典
* @param req 新增字典请求
* @return 新增字典的ID
*/
@PostMapping()
public Id addDict(@RequestBody AddDictReq req) {
Dict dict = req.into();
if (dict.getParentId() == null) {
dict.setParentId(Dict.ROOT.getId());
dict.setType(Dict.ROOT.getType());
}
dictService.addDict(dict);
return Id.of(dict.getId());
}
/**
* 更新字典
* @param req 更新字典请求
* @return 字典的ID
*/
@PutMapping()
public Id updateDict(@RequestBody UpdateDictReq req) {
Dict dict = req.into();
dictService.updateDict(dict);
return Id.of(dict.getId());
}
/**
* 获取字典类型列表
* @param req 获取字典类型列表请求
* @return 字典类型列表分页
*/
@GetMapping("/type")
public IPage<Dict> getDictTypePage(GetDictTypeListReq req) {
return dictService.getDictTypePage(req);
}
/**
* 获取字典树
* @param parentId 父ID
* @param level 层级
* @return 字典树
*/
@GetMapping("/tree")
public List<Dict> getDictTree(
@RequestParam(required = false, defaultValue = "0") String parentId,
@RequestParam(required = false, defaultValue = "-1") Integer level
) {
return dictService.getDictTreeTopDown(Collections.singleton(parentId), level);
}
/**
* 获取字典数据
* @param type 字典类型
* @return 字典数据
*/
@GetMapping("/data/{type}")
public List<Dict> getDictData(@PathVariable String type) {
return dictService.getDictByType(type);
}
/**
* 获取字典数据
* @param type 字典类型
* @return 字典数据树形结构
*/
@GetMapping("/data/{type}/tree")
public List<Dict> getDictDataTree(@PathVariable String type) {
return dictService.getDictByTypeAsTree(type);
}
@DeleteMapping("{id}")
public void deleteDict(@PathVariable String id) {
dictService.deleteDict(Collections.singleton(id));
}
}

View File

@ -0,0 +1,115 @@
package net.rzdata.demo.dict;
import com.baomidou.mybatisplus.core.metadata.IPage;
import net.rzdata.demo.dict.domain.Dict;
import net.rzdata.demo.dict.domain.GetDictTypeListReq;
import net.rzdata.demo.dict.mapper.DictRepository;
import net.rzdata.exception.NotExistDataException;
import net.rzdata.exception.RepeatDataException;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class DictService {
private final DictRepository dictRepository;
public DictService(DictRepository dictRepository) {
this.dictRepository = dictRepository;
}
protected void addDict(Dict dict) {
check(dict);
dictRepository.get().insert(dict);
}
protected List<Dict> getDictByType(String type) {
return dictRepository.findByTypeOrderBySortAsc(type);
}
protected List<Dict> getDictByTypeAsTree(String type) {
List<Dict> dictList = dictRepository.findByTypeOrderBySortAsc(type);
Set<String> idSet = dictList.parallelStream()
.map(Dict::getId)
.collect(Collectors.toSet());
List<Dict> root = dictList.parallelStream()
.filter(dict -> !idSet.contains(dict.getParentId()))
.toList();
List<Dict> increment = root;
while (!increment.isEmpty()) {
for (Dict dict : increment) {
List<Dict> children = dictList.parallelStream()
.filter(d -> Objects.equals(d.getParentId(), dict.getId()))
.toList();
dict.setChildren(children);
}
increment = increment.parallelStream()
.map(Dict::getChildren)
.flatMap(List::stream)
.toList();
}
return root;
}
protected void updateDict(Dict dict) {
Dict exist = dictRepository.get().selectById(dict.getId());
if (exist == null) {
throw new NotExistDataException("字典已被删除,无法更新");
}
if (!Objects.equals(dict.getValue(), exist.getValue())) {
check(dict);
}
dictRepository.get().updateById(dict);
// 如果是字典类 则需要更新该类下所有值的type字段
if (Objects.equals(exist.getParentId(), Dict.ROOT.getId())) {
dictRepository.updateType(exist.getValue(), dict.getValue());
}
}
protected IPage<Dict> getDictTypePage(GetDictTypeListReq req) {
return dictRepository.getDictTypePage(req);
}
protected List<Dict> getDictTreeTopDown(Collection<String> parentId, int level) {
if (level == 0 || level < Byte.MIN_VALUE) {
return Collections.emptyList();
} else if (parentId.isEmpty()) {
return Collections.emptyList();
}
List<Dict> children = dictRepository.findByParentIdIn(parentId);
Set<String> childIds = children.parallelStream()
.map(Dict::getId)
.collect(Collectors.toSet());
List<Dict> grandchildren = getDictTreeTopDown(childIds, level - 1);
for (Dict child : children) {
List<Dict> grandchild = grandchildren.parallelStream()
.filter(g -> Objects.equals(g.getParentId(), child.getId()))
.toList();
child.setChildren(grandchild);
}
return children;
}
private Optional<Dict> getDictByTypeAndValue(String type, String value) {
List<Dict> dictList = this.getDictByType(type);
return dictList.parallelStream()
.filter(d -> Objects.equals(d.getValue(), value))
.findAny();
}
private void check(Dict dict) {
Optional<Dict> exist = this.getDictByTypeAndValue(dict.getType(), dict.getValue());
if (exist.isPresent()) {
throw new RepeatDataException(String.format("字典类型:%s, 值:%s 已存在", dict.getType(), dict.getValue()));
}
}
public void deleteDict(Collection<String> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
dictRepository.get().deleteBatchIds(ids);
}
}

View File

@ -0,0 +1,67 @@
package net.rzdata.demo.dict.domain;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.rzdata.trait.IConverter;
import net.rzdata.trait.IQuery;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Getter
@Setter
@ToString
public class AddDictReq implements IQuery<Dict> {
/**
* 字典标签
*/
@Max(255)
private String label;
/**
* 字典值
*/
@Max(255)
@NotNull(message = "字典值不能为空")
@Pattern(regexp = "[\\w-]+", message = "字典值只允许英文字母、下划线、短横线")
private String value;
/**
* 字典类型
*/
@NotNull(message = "字典类型不能为空")
private String type;
/**
* 父级ID
* 如果是顶级字典 则为0
*/
private String parentId;
/**
* 排序
*/
@NotNull(message = "排序值不能为空")
private Integer sort;
/**
* 是否禁用
*/
private Boolean disabled;
@Override
public Dict into() {
return AddDictReqConverter.INSTANCE.convert(this);
}
}
@Mapper
interface AddDictReqConverter extends IConverter<AddDictReq, Dict> {
AddDictReqConverter INSTANCE = Mappers.getMapper(AddDictReqConverter.class);
@Mapping(target = "id", ignore = true)
@Mapping(target = "children", ignore = true)
@Override
Dict convert(AddDictReq req);
}

View File

@ -0,0 +1,58 @@
package net.rzdata.demo.dict.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.*;
import net.rzdata.demo.trait.TreeNode;
import java.util.Collections;
import java.util.List;
/**
* 字典
*/
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Dict implements TreeNode<Dict, String> {
public static final Dict ROOT = new Dict("0", "字典", "dict", "dict", 0, false, "0", Collections.emptyList());
/**
* 字典主键
*/
@TableId(type = IdType.ASSIGN_ID)
String id;
/**
* 字典标签
*/
String label;
/**
* 字典键值
*/
String value;
/**
* 字典类型
*/
String type;
/**
* 字典排序
*/
Integer sort;
/**
* 是否停用
*/
Boolean disabled;
/**
* 父节点ID
*/
String parentId;
/**
* 子节点
*/
@TableField(exist = false)
List<Dict> children;
}

View File

@ -0,0 +1,6 @@
package net.rzdata.demo.dict.domain;
import net.rzdata.demo.trait.GetReq;
public class GetDictTypeListReq extends GetReq<Dict> {
}

View File

@ -0,0 +1,64 @@
package net.rzdata.demo.dict.domain;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.rzdata.trait.IConverter;
import net.rzdata.trait.IQuery;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Getter
@Setter
@ToString
public class UpdateDictReq implements IQuery<Dict> {
/**
* 字典ID
*/
private String id;
/**
* 字典标签
*/
@Max(255)
private String label;
/**
* 字典值
*/
@Max(255)
@NotNull(message = "字典值不能为空")
@Pattern(regexp = "[\\w-]+", message = "字典值只允许英文字母、下划线、短横线")
private String value;
/**
* 排序
*/
@NotNull(message = "排序值不能为空")
private Integer sort;
/**
* 父级ID
*/
private String parentId;
/**
* 是否禁用
*/
private Boolean disabled;
public Dict into() {
return UpdateDictReqConverter.INSTANCE.convert(this);
}
}
@Mapper
interface UpdateDictReqConverter extends IConverter<UpdateDictReq, Dict> {
UpdateDictReqConverter INSTANCE = Mappers.getMapper(UpdateDictReqConverter.class);
@Mapping(target = "type", ignore = true)
@Mapping(target = "children", ignore = true)
@Override
Dict convert(UpdateDictReq req);
}

View File

@ -0,0 +1,9 @@
package net.rzdata.demo.dict.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import net.rzdata.demo.dict.domain.Dict;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DictMapper extends BaseMapper<Dict> {
}

View File

@ -0,0 +1,48 @@
package net.rzdata.demo.dict.mapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import net.rzdata.demo.dict.domain.Dict;
import net.rzdata.demo.dict.domain.GetDictTypeListReq;
import net.rzdata.demo.trait.BaseRepository;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
@Repository
public class DictRepository extends BaseRepository<Dict, DictMapper> {
public DictRepository(DictMapper mapper) {
super(mapper);
}
public List<Dict> findByTypeOrderBySortAsc(String type) {
return this.get().selectList(new LambdaQueryWrapper<Dict>()
.eq(Dict::getType, type)
.orderByAsc(Dict::getSort)
);
}
public int updateType(String oldValue, String newValue) {
return this.get().update(null, new LambdaUpdateWrapper<Dict>()
.set(Dict::getType, newValue)
.eq(Dict::getType, oldValue)
);
}
public List<Dict> findByParentIdIn(Collection<String> parentId) {
return this.get().selectList(new LambdaQueryWrapper<Dict>()
.in(Dict::getParentId, parentId)
.orderByAsc(Dict::getSort)
);
}
public IPage<Dict> getDictTypePage(GetDictTypeListReq req) {
return this.get().selectPage(req.into(), new LambdaQueryWrapper<Dict>()
.eq(Dict::getParentId, Dict.ROOT.getId())
.orderByAsc(Dict::getSort)
);
}
}

View File

@ -19,3 +19,13 @@ spring:
maximum-pool-size: 20
idle-timeout: 30000
max-lifetime: 1800000
liquibase:
change-log: classpath:/db/changelog/db.changelog-master.yaml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
logging:
level:
root: info
net.rzdata: debug
com.baomidou.example.mapper: debug

View File

@ -0,0 +1,3 @@
databaseChangeLog:
- includeAll:
path: db/changelog/sql

View File

@ -0,0 +1,13 @@
create table dict
(
id varchar(64) not null comment '字典主键',
label varchar(255) not null comment '字典标签',
value varchar(255) not null comment '字典键值',
type varchar(255) not null comment '字典类型',
sort int default 1 not null comment '字典排序',
disabled bool default false not null comment '是否停用',
parent_id varchar(64) default 0 not null comment '父级字典主键',
constraint table_name_pk primary key (id),
constraint table_name_uk_type_value unique (type, value)
) comment '字典';

View File

@ -0,0 +1,12 @@
package net.rzdata.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoTestApplication {
public static void main(String[] args) {
SpringApplication.run(DemoTestApplication.class, args);
}
}

View File

@ -0,0 +1,20 @@
package net.rzdata.demo.dict;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DictServiceTest {
@Resource
private transient DictService dictService;
@Test
void addDict() {
}
@Test
void getDictByType() {
}
}