diff --git a/common/src/main/java/net/rzdata/demo/trait/GetReq.java b/common/src/main/java/net/rzdata/demo/trait/GetReq.java new file mode 100644 index 0000000..0f14655 --- /dev/null +++ b/common/src/main/java/net/rzdata/demo/trait/GetReq.java @@ -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 { + + private Integer pageSize = 15; + private Integer pageNum = 1; + private String orderBy; + private Boolean asc = true; + + public IPage into() { + Page page = new Page<>(); + page.setSize(pageSize); + page.setCurrent(pageNum); + if (orderBy != null) { + page.addOrder(this.asc ? OrderItem.asc(orderBy) : OrderItem.desc(orderBy)); + } + return page; + } +} diff --git a/common/src/main/java/net/rzdata/demo/trait/TreeNode.java b/common/src/main/java/net/rzdata/demo/trait/TreeNode.java new file mode 100644 index 0000000..5ab7fd1 --- /dev/null +++ b/common/src/main/java/net/rzdata/demo/trait/TreeNode.java @@ -0,0 +1,18 @@ +package net.rzdata.demo.trait; + +import java.util.List; + +public interface TreeNode, ID> { + + ID getId(); + + ID getParentId(); + + List getChildren(); + + void setChildren(List children); + + default boolean isLeaf() { + return getChildren() == null; + } +} diff --git a/core/src/main/java/net/rzdata/exception/NotExistDataException.java b/core/src/main/java/net/rzdata/exception/NotExistDataException.java new file mode 100644 index 0000000..ab32ae1 --- /dev/null +++ b/core/src/main/java/net/rzdata/exception/NotExistDataException.java @@ -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); + } +} diff --git a/core/src/main/java/net/rzdata/exception/RepeatDataException.java b/core/src/main/java/net/rzdata/exception/RepeatDataException.java new file mode 100644 index 0000000..502ed07 --- /dev/null +++ b/core/src/main/java/net/rzdata/exception/RepeatDataException.java @@ -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); + } +} diff --git a/service/system/pom.xml b/service/system/pom.xml index 5843d8a..0b490c7 100644 --- a/service/system/pom.xml +++ b/service/system/pom.xml @@ -18,4 +18,10 @@ 4567 + + + org.liquibase + liquibase-core + + diff --git a/service/system/src/main/java/net/rzdata/demo/dict/DictController.java b/service/system/src/main/java/net/rzdata/demo/dict/DictController.java new file mode 100644 index 0000000..18cb671 --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/DictController.java @@ -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 getDictTypePage(GetDictTypeListReq req) { + return dictService.getDictTypePage(req); + } + + /** + * 获取字典树 + * @param parentId 父ID + * @param level 层级 + * @return 字典树 + */ + @GetMapping("/tree") + public List 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 getDictData(@PathVariable String type) { + return dictService.getDictByType(type); + } + + /** + * 获取字典数据 + * @param type 字典类型 + * @return 字典数据(树形结构) + */ + @GetMapping("/data/{type}/tree") + public List getDictDataTree(@PathVariable String type) { + return dictService.getDictByTypeAsTree(type); + } + + @DeleteMapping("{id}") + public void deleteDict(@PathVariable String id) { + dictService.deleteDict(Collections.singleton(id)); + } +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/DictService.java b/service/system/src/main/java/net/rzdata/demo/dict/DictService.java new file mode 100644 index 0000000..c492dcc --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/DictService.java @@ -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 getDictByType(String type) { + return dictRepository.findByTypeOrderBySortAsc(type); + } + + protected List getDictByTypeAsTree(String type) { + List dictList = dictRepository.findByTypeOrderBySortAsc(type); + Set idSet = dictList.parallelStream() + .map(Dict::getId) + .collect(Collectors.toSet()); + List root = dictList.parallelStream() + .filter(dict -> !idSet.contains(dict.getParentId())) + .toList(); + List increment = root; + while (!increment.isEmpty()) { + for (Dict dict : increment) { + List 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 getDictTypePage(GetDictTypeListReq req) { + return dictRepository.getDictTypePage(req); + } + + protected List getDictTreeTopDown(Collection parentId, int level) { + if (level == 0 || level < Byte.MIN_VALUE) { + return Collections.emptyList(); + } else if (parentId.isEmpty()) { + return Collections.emptyList(); + } + List children = dictRepository.findByParentIdIn(parentId); + Set childIds = children.parallelStream() + .map(Dict::getId) + .collect(Collectors.toSet()); + List grandchildren = getDictTreeTopDown(childIds, level - 1); + for (Dict child : children) { + List grandchild = grandchildren.parallelStream() + .filter(g -> Objects.equals(g.getParentId(), child.getId())) + .toList(); + child.setChildren(grandchild); + } + return children; + } + + private Optional getDictByTypeAndValue(String type, String value) { + List dictList = this.getDictByType(type); + return dictList.parallelStream() + .filter(d -> Objects.equals(d.getValue(), value)) + .findAny(); + } + + private void check(Dict dict) { + Optional 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 ids) { + if (ids == null || ids.isEmpty()) { + return; + } + dictRepository.get().deleteBatchIds(ids); + } +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/domain/AddDictReq.java b/service/system/src/main/java/net/rzdata/demo/dict/domain/AddDictReq.java new file mode 100644 index 0000000..c33338e --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/domain/AddDictReq.java @@ -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 { + + /** + * 字典标签 + */ + @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 { + + AddDictReqConverter INSTANCE = Mappers.getMapper(AddDictReqConverter.class); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "children", ignore = true) + @Override + Dict convert(AddDictReq req); +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/domain/Dict.java b/service/system/src/main/java/net/rzdata/demo/dict/domain/Dict.java new file mode 100644 index 0000000..dee7e75 --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/domain/Dict.java @@ -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 { + + 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 children; +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/domain/GetDictTypeListReq.java b/service/system/src/main/java/net/rzdata/demo/dict/domain/GetDictTypeListReq.java new file mode 100644 index 0000000..48a7814 --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/domain/GetDictTypeListReq.java @@ -0,0 +1,6 @@ +package net.rzdata.demo.dict.domain; + +import net.rzdata.demo.trait.GetReq; + +public class GetDictTypeListReq extends GetReq { +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/domain/UpdateDictReq.java b/service/system/src/main/java/net/rzdata/demo/dict/domain/UpdateDictReq.java new file mode 100644 index 0000000..9dc1b1f --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/domain/UpdateDictReq.java @@ -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 { + + /** + * 字典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 { + + UpdateDictReqConverter INSTANCE = Mappers.getMapper(UpdateDictReqConverter.class); + + @Mapping(target = "type", ignore = true) + @Mapping(target = "children", ignore = true) + @Override + Dict convert(UpdateDictReq req); +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/mapper/DictMapper.java b/service/system/src/main/java/net/rzdata/demo/dict/mapper/DictMapper.java new file mode 100644 index 0000000..d307a19 --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/mapper/DictMapper.java @@ -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 { +} diff --git a/service/system/src/main/java/net/rzdata/demo/dict/mapper/DictRepository.java b/service/system/src/main/java/net/rzdata/demo/dict/mapper/DictRepository.java new file mode 100644 index 0000000..f5fc8f3 --- /dev/null +++ b/service/system/src/main/java/net/rzdata/demo/dict/mapper/DictRepository.java @@ -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 { + + public DictRepository(DictMapper mapper) { + super(mapper); + } + + public List findByTypeOrderBySortAsc(String type) { + return this.get().selectList(new LambdaQueryWrapper() + .eq(Dict::getType, type) + .orderByAsc(Dict::getSort) + ); + } + + public int updateType(String oldValue, String newValue) { + return this.get().update(null, new LambdaUpdateWrapper() + .set(Dict::getType, newValue) + .eq(Dict::getType, oldValue) + ); + } + + public List findByParentIdIn(Collection parentId) { + return this.get().selectList(new LambdaQueryWrapper() + .in(Dict::getParentId, parentId) + .orderByAsc(Dict::getSort) + ); + } + + public IPage getDictTypePage(GetDictTypeListReq req) { + return this.get().selectPage(req.into(), new LambdaQueryWrapper() + .eq(Dict::getParentId, Dict.ROOT.getId()) + .orderByAsc(Dict::getSort) + ); + } +} diff --git a/service/system/src/main/resources/config/application.yml b/service/system/src/main/resources/config/application.yml index 8e7d599..89a3e1c 100644 --- a/service/system/src/main/resources/config/application.yml +++ b/service/system/src/main/resources/config/application.yml @@ -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 diff --git a/service/system/src/main/resources/db/changelog/db.changelog-master.yaml b/service/system/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..062d441 --- /dev/null +++ b/service/system/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,3 @@ +databaseChangeLog: + - includeAll: + path: db/changelog/sql diff --git a/service/system/src/main/resources/db/changelog/sql/dict.sql b/service/system/src/main/resources/db/changelog/sql/dict.sql new file mode 100644 index 0000000..fa331fc --- /dev/null +++ b/service/system/src/main/resources/db/changelog/sql/dict.sql @@ -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 '字典'; + diff --git a/service/system/src/test/java/net/rzdata/demo/DemoTestApplication.java b/service/system/src/test/java/net/rzdata/demo/DemoTestApplication.java new file mode 100644 index 0000000..41bee7f --- /dev/null +++ b/service/system/src/test/java/net/rzdata/demo/DemoTestApplication.java @@ -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); + } +} diff --git a/service/system/src/test/java/net/rzdata/demo/dict/DictServiceTest.java b/service/system/src/test/java/net/rzdata/demo/dict/DictServiceTest.java new file mode 100644 index 0000000..27f03f6 --- /dev/null +++ b/service/system/src/test/java/net/rzdata/demo/dict/DictServiceTest.java @@ -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() { + } +} diff --git a/service/system/src/test/resources/application-test.yml b/service/system/src/test/resources/application-test.yml new file mode 100644 index 0000000..e69de29