55 KiB
编码生成功能 实现计划
面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(
- [ ])语法来跟踪进度。
目标: 在 nl-module-base 模块下实现编码生成引擎,支持 PREFIX + DATE + SEQUENCE 三段组合、号段预分配/分布式锁两种并发策略、规则配置化管理。
架构: 编码规则通过 base_code_rule 表存储(分段配置用 JSON),流水号段用 base_code_sequence 表记录。核心引擎 CodeGenServiceImpl 遍历分段配置逐段生成,SequenceAllocator 负责号段的分布式并发分配(Lock4j + 内存缓存 + DB 预取)。规则查询使用 Redis 缓存(30 分钟 TTL),更新时主动清除。
技术栈: Java 17 + Spring Boot 3 + MyBatis-Plus + Lock4j (baomidou) + Redis (StringRedisTemplate) + Jackson JSON + MySQL
任务 1:SQL DDL 建表
文件:
-
创建:
sql/mysql/add26071501.sql -
步骤 1:编写建表 SQL
-- 编码规则定义表
CREATE TABLE base_code_rule (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
rule_name VARCHAR(64) NOT NULL COMMENT '规则名称',
rule_code VARCHAR(32) NOT NULL COMMENT '规则编码(唯一标识,调用时传入)',
segment_config JSON NOT NULL COMMENT '分段配置(JSON数组)',
separator VARCHAR(8) DEFAULT '' COMMENT '分隔符(- / _ . 或空)',
letter_case TINYINT DEFAULT 0 COMMENT '字母大小写:0=原样 1=全大写 2=全小写',
total_length INT DEFAULT 0 COMMENT '固定总长度约束(0=不限制)',
seq_strategy VARCHAR(16) DEFAULT 'segment' COMMENT '序号策略:segment=号段预分配 lock=分布式锁实时',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=启用 1=禁用',
remark VARCHAR(256) DEFAULT '' COMMENT '备注',
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (id),
UNIQUE KEY uk_rule_code (rule_code)
) COMMENT '编码规则定义表';
-- 编码流水号段记录表
CREATE TABLE base_code_sequence (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
rule_id BIGINT NOT NULL COMMENT '规则ID,关联 base_code_rule.id',
reset_key VARCHAR(64) NOT NULL DEFAULT '' COMMENT '重置维度键(如按天:20260715,全局:GLOBAL)',
current_value BIGINT NOT NULL DEFAULT 0 COMMENT '当前已分配的最大序号',
max_value BIGINT NOT NULL DEFAULT 9999 COMMENT '序号最大值上限',
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (id),
KEY idx_rule_reset (rule_id, reset_key)
) COMMENT '编码流水号段记录表';
- 步骤 2:Commit
git add sql/mysql/add26071501.sql
git commit -m "feat: 新增编码规则和流水号段建表SQL"
任务 2:错误码 + 枚举类
文件:
-
修改:
nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/enums/ErrorCodeConstants.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SegmentTypeEnum.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/ResetByEnum.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/LetterCaseEnum.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SeqStrategyEnum.java -
步骤 1:追加错误码常量
在 ErrorCodeConstants.java 的最后一个 ErrorCode 定义后面追加:
// ========== 编码生成 ==========
ErrorCode CODE_RULE_NOT_EXISTS = new ErrorCode(9, "编码规则不存在");
ErrorCode CODE_RULE_CODE_DUPLICATE = new ErrorCode(10, "规则编码已存在");
ErrorCode CODE_GEN_SEQ_EXCEED_MAX = new ErrorCode(11, "序号超出最大值上限");
ErrorCode CODE_GEN_LENGTH_EXCEED = new ErrorCode(12, "生成的编码超出总长度限制");
ErrorCode CODE_RULE_DISABLED = new ErrorCode(13, "编码规则已被禁用");
- 步骤 2:编写 SegmentTypeEnum
package cn.code.nl.module.base.service.codegen.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 编码段类型枚举
*
* @author zhouz
*/
@Getter
@AllArgsConstructor
public enum SegmentTypeEnum {
PREFIX("PREFIX", "固定前缀"),
DATE("DATE", "日期"),
SEQUENCE("SEQUENCE", "流水序号");
/**
* 类型编码
*/
private final String code;
/**
* 类型名称
*/
private final String name;
}
- 步骤 3:编写 ResetByEnum
package cn.code.nl.module.base.service.codegen.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 序号重置维度枚举
*
* @author zhouz
*/
@Getter
@AllArgsConstructor
public enum ResetByEnum {
DAY("DAY", "按天重置"),
MONTH("MONTH", "按月重置"),
YEAR("YEAR", "按年重置"),
GLOBAL("GLOBAL", "全局不重置");
/**
* 维度编码
*/
private final String code;
/**
* 维度名称
*/
private final String name;
}
- 步骤 4:编写 LetterCaseEnum
package cn.code.nl.module.base.service.codegen.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 字母大小写枚举
*
* @author zhouz
*/
@Getter
@AllArgsConstructor
public enum LetterCaseEnum {
NONE(0, "原样"),
UPPER(1, "全大写"),
LOWER(2, "全小写");
/**
* 状态值
*/
private final Integer code;
/**
* 状态名称
*/
private final String name;
}
- 步骤 5:编写 SeqStrategyEnum
package cn.code.nl.module.base.service.codegen.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 序号并发策略枚举
*
* @author zhouz
*/
@Getter
@AllArgsConstructor
public enum SeqStrategyEnum {
SEGMENT("segment", "号段预分配"),
LOCK("lock", "分布式锁实时");
/**
* 策略编码
*/
private final String code;
/**
* 策略名称
*/
private final String name;
}
- 步骤 6:Commit
git add nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/enums/ErrorCodeConstants.java \
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/
git commit -m "feat: 新增编码生成相关枚举和错误码"
任务 3:SegmentConfig 分段配置 DTO
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/SegmentConfig.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/PrefixSegment.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/DateSegment.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/SequenceSegment.java -
步骤 1:编写 SegmentConfig 基类
package cn.code.nl.module.base.service.codegen.dto;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
/**
* 分段配置基类,使用 Jackson 多态序列化/反序列化
*
* @author zhouz
*/
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = PrefixSegment.class, name = "PREFIX"),
@JsonSubTypes.Type(value = DateSegment.class, name = "DATE"),
@JsonSubTypes.Type(value = SequenceSegment.class, name = "SEQUENCE")
})
public abstract class SegmentConfig {
/**
* 段类型:PREFIX / DATE / SEQUENCE
*/
private String type;
}
- 步骤 2:编写 PrefixSegment
package cn.code.nl.module.base.service.codegen.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 固定前缀段配置
*
* @author zhouz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PrefixSegment extends SegmentConfig {
/**
* 固定前缀值,如 "ORD"
*/
private String value;
}
- 步骤 3:编写 DateSegment
package cn.code.nl.module.base.service.codegen.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 日期段配置
*
* @author zhouz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DateSegment extends SegmentConfig {
/**
* 日期格式,如 "yyyyMMdd"、"yyyyMM"、"yyyy"
*/
private String format;
}
- 步骤 4:编写 SequenceSegment
package cn.code.nl.module.base.service.codegen.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 流水序号段配置
*
* @author zhouz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SequenceSegment extends SegmentConfig {
/**
* 重置维度:DAY / MONTH / YEAR / GLOBAL
*/
private String resetBy;
/**
* 起始值,默认 1
*/
private Long startAt;
/**
* 补位后的总长度,如 4 → 0001
*/
private Integer paddingLen;
/**
* 补位字符,默认 "0"
*/
private String paddingChar;
/**
* 序号最大值上限
*/
private Long maxValue;
}
- 步骤 5:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/
git commit -m "feat: 新增编码分段配置DTO(Jackson多态)"
任务 4:DataObject 数据对象
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeRuleDO.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeSequenceDO.java -
步骤 1:编写 CodeRuleDO
package cn.code.nl.module.base.dal.dataobject.codegen;
import cn.code.nl.framework.mybatis.core.dataobject.BaseDO;
import cn.code.nl.module.base.service.codegen.dto.SegmentConfig;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 编码规则定义 DO
*
* @author zhouz
*/
@TableName(value = "base_code_rule", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
public class CodeRuleDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 规则名称
*/
private String ruleName;
/**
* 规则编码(唯一标识)
*/
private String ruleCode;
/**
* 分段配置(JSON数组)
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<SegmentConfig> segmentConfig;
/**
* 分隔符
*/
private String separator;
/**
* 字母大小写:0=原样 1=全大写 2=全小写
*/
private Integer letterCase;
/**
* 固定总长度约束(0=不限制)
*/
private Integer totalLength;
/**
* 序号策略:segment=号段预分配 lock=分布式锁实时
*/
private String seqStrategy;
/**
* 状态:0=启用 1=禁用
*/
private Integer status;
/**
* 备注
*/
private String remark;
}
- 步骤 2:编写 CodeSequenceDO
package cn.code.nl.module.base.dal.dataobject.codegen;
import cn.code.nl.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 编码流水号段记录 DO
*
* @author zhouz
*/
@TableName("base_code_sequence")
@Data
@EqualsAndHashCode(callSuper = true)
public class CodeSequenceDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 规则ID,关联 base_code_rule.id
*/
private Long ruleId;
/**
* 重置维度键(如按天:20260715,全局:GLOBAL)
*/
private String resetKey;
/**
* 当前已分配的最大序号
*/
private Long currentValue;
/**
* 序号最大值上限
*/
private Long maxValue;
}
- 步骤 3:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/
git commit -m "feat: 新增编码规则和流水号段DO"
任务 5:Mapper 层
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeRuleMapper.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeSequenceMapper.java -
创建:
nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml -
步骤 1:编写 CodeRuleMapper
package cn.code.nl.module.base.dal.mysql.codegen;
import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX;
import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.code.nl.module.base.controller.admin.codegen.vo.CodeRulePageReqVO;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeRuleDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 编码规则 Mapper
*
* @author zhouz
*/
@Mapper
public interface CodeRuleMapper extends BaseMapperX<CodeRuleDO> {
/**
* 分页查询编码规则
*
* @param reqVO 查询条件
* @return 分页结果
*/
default PageResult<CodeRuleDO> selectPage(CodeRulePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<CodeRuleDO>()
.likeIfPresent(CodeRuleDO::getRuleName, reqVO.getRuleName())
.eqIfPresent(CodeRuleDO::getRuleCode, reqVO.getRuleCode())
.eqIfPresent(CodeRuleDO::getStatus, reqVO.getStatus())
.betweenIfPresent(CodeRuleDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(CodeRuleDO::getId));
}
/**
* 根据规则编码查询
*
* @param ruleCode 规则编码
* @return 编码规则
*/
default CodeRuleDO selectByRuleCode(String ruleCode) {
return selectOne(CodeRuleDO::getRuleCode, ruleCode);
}
}
- 步骤 2:编写 CodeSequenceMapper
package cn.code.nl.module.base.dal.mysql.codegen;
import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeSequenceDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 编码流水号段 Mapper
*
* @author zhouz
*/
@Mapper
public interface CodeSequenceMapper extends BaseMapperX<CodeSequenceDO> {
/**
* 根据规则ID和重置键查询流水记录
*
* @param ruleId 规则ID
* @param resetKey 重置维度键
* @return 流水记录
*/
default CodeSequenceDO selectByRuleIdAndResetKey(Long ruleId, String resetKey) {
return selectOne(CodeSequenceDO::getRuleId, ruleId,
CodeSequenceDO::getResetKey, resetKey);
}
/**
* 号段预分配:原子更新 current_value = current_value + step
*
* @param ruleId 规则ID
* @param resetKey 重置维度键
* @param step 预取步长
* @param startAt 起始值(用于首次初始化)
* @param maxValue 最大值上限
* @return 更新行数
*/
int updateCurrentValueBySegment(@Param("ruleId") Long ruleId,
@Param("resetKey") String resetKey,
@Param("step") int step,
@Param("startAt") Long startAt,
@Param("maxValue") Long maxValue);
}
- 步骤 3:编写 CodeSequenceMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.code.nl.module.base.dal.mysql.codegen.CodeSequenceMapper">
<!--
号段预分配:
1. 使用 INSERT ... ON DUPLICATE KEY 实现"不存在则插入,存在则更新"
2. 利用 idx_rule_reset (rule_id, reset_key) 唯一性校验并发安全
3. 首次插入时 current_value = startAt + step - 1,后续更新时 current_value = current_value + step
注意:此 SQL 依赖 idx_rule_reset 索引配合行锁,需在 Lock4j 保护下调用
-->
<update id="updateCurrentValueBySegment">
INSERT INTO base_code_sequence (rule_id, reset_key, current_value, max_value)
VALUES (#{ruleId}, #{resetKey}, #{startAt} + #{step} - 1, #{maxValue})
ON DUPLICATE KEY UPDATE
current_value = current_value + #{step},
max_value = #{maxValue}
</update>
</mapper>
- 步骤 4:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/ \
nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/
git commit -m "feat: 新增编码规则和流水号段Mapper"
任务 6:VO 类(请求/响应)
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSaveReqVO.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRulePageReqVO.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleRespVO.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSimpleRespVO.java -
步骤 1:编写 CodeRuleSaveReqVO
package cn.code.nl.module.base.controller.admin.codegen.vo;
import cn.code.nl.module.base.service.codegen.dto.SegmentConfig;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* 编码规则新增/编辑请求 VO
*
* @author zhouz
*/
@Data
public class CodeRuleSaveReqVO {
/**
* 主键(编辑时传入)
*/
private Long id;
/**
* 规则名称
*/
@NotBlank(message = "规则名称不能为空")
private String ruleName;
/**
* 规则编码(唯一标识)
*/
@NotBlank(message = "规则编码不能为空")
private String ruleCode;
/**
* 分段配置列表
*/
@NotEmpty(message = "分段配置不能为空")
private List<SegmentConfig> segments;
/**
* 分隔符
*/
private String separator;
/**
* 字母大小写:0=原样 1=全大写 2=全小写
*/
private Integer letterCase;
/**
* 固定总长度约束(0=不限制)
*/
private Integer totalLength;
/**
* 序号策略:segment=号段预分配 lock=分布式锁实时
*/
private String seqStrategy;
/**
* 状态:0=启用 1=禁用
*/
private Integer status;
/**
* 备注
*/
private String remark;
}
- 步骤 2:编写 CodeRulePageReqVO
package cn.code.nl.module.base.controller.admin.codegen.vo;
import cn.code.nl.framework.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* 编码规则分页查询请求 VO
*
* @author zhouz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CodeRulePageReqVO extends PageParam {
/**
* 规则名称(模糊查询)
*/
private String ruleName;
/**
* 规则编码
*/
private String ruleCode;
/**
* 状态
*/
private Integer status;
/**
* 创建时间范围
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime[] createTime;
}
- 步骤 3:编写 CodeRuleRespVO
package cn.code.nl.module.base.controller.admin.codegen.vo;
import cn.code.nl.module.base.service.codegen.dto.SegmentConfig;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 编码规则响应 VO
*
* @author zhouz
*/
@Data
public class CodeRuleRespVO {
/**
* 主键
*/
private Long id;
/**
* 规则名称
*/
private String ruleName;
/**
* 规则编码
*/
private String ruleCode;
/**
* 分段配置列表
*/
private List<SegmentConfig> segments;
/**
* 分隔符
*/
private String separator;
/**
* 字母大小写:0=原样 1=全大写 2=全小写
*/
private Integer letterCase;
/**
* 固定总长度约束
*/
private Integer totalLength;
/**
* 序号策略
*/
private String seqStrategy;
/**
* 状态
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
- 步骤 4:编写 CodeRuleSimpleRespVO
package cn.code.nl.module.base.controller.admin.codegen.vo;
import lombok.Data;
/**
* 编码规则精简响应 VO(下拉选项用)
*
* @author zhouz
*/
@Data
public class CodeRuleSimpleRespVO {
/**
* 主键
*/
private Long id;
/**
* 规则名称
*/
private String ruleName;
/**
* 规则编码
*/
private String ruleCode;
}
- 步骤 5:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/
git commit -m "feat: 新增编码规则VO类"
任务 7:API 模块(Feign 接口)
文件:
-
创建:
nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/dto/CodeGenerateReqDTO.java -
创建:
nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/CodeGenApi.java -
步骤 1:编写 CodeGenerateReqDTO
package cn.code.nl.module.base.api.codegen.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.Map;
/**
* 编码生成请求 DTO
*
* @author zhouz
*/
@Data
public class CodeGenerateReqDTO {
/**
* 规则编码
*/
@NotBlank(message = "规则编码不能为空")
private String ruleCode;
/**
* 业务字段参数(预留,二期 BIZ_FIELD 段使用)
*/
private Map<String, String> bizParams;
}
- 步骤 2:编写 CodeGenApi
package cn.code.nl.module.base.api.codegen;
import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO;
import cn.code.nl.module.base.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* RPC 服务 - 编码生成
*
* @author zhouz
*/
@FeignClient(name = ApiConstants.NAME)
@Tag(name = "RPC 服务 - 编码生成")
public interface CodeGenApi {
String PREFIX = ApiConstants.PREFIX + "/code-gen";
/**
* 根据规则编码生成编码字符串
*
* @param reqDTO 生成请求
* @return 生成的编码字符串
*/
@PostMapping(PREFIX + "/generate")
@Operation(summary = "生成编码")
CommonResult<String> generate(@Valid @RequestBody CodeGenerateReqDTO reqDTO);
}
- 步骤 3:Commit
git add nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/
git commit -m "feat: 新增编码生成Feign API接口"
任务 8:CodeRuleService 规则管理服务
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleService.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleServiceImpl.java -
步骤 1:编写 CodeRuleService 接口
package cn.code.nl.module.base.service.codegen;
import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.module.base.controller.admin.codegen.vo.CodeRulePageReqVO;
import cn.code.nl.module.base.controller.admin.codegen.vo.CodeRuleSaveReqVO;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeRuleDO;
import jakarta.validation.Valid;
import java.util.List;
/**
* 编码规则管理 Service 接口
*
* @author zhouz
*/
public interface CodeRuleService {
/**
* 创建编码规则
*
* @param createReqVO 创建信息
* @return 规则ID
*/
Long createRule(@Valid CodeRuleSaveReqVO createReqVO);
/**
* 更新编码规则
*
* @param updateReqVO 更新信息
*/
void updateRule(@Valid CodeRuleSaveReqVO updateReqVO);
/**
* 删除编码规则
*
* @param id 规则ID
*/
void deleteRule(Long id);
/**
* 获取编码规则详情
*
* @param id 规则ID
* @return 编码规则
*/
CodeRuleDO getRule(Long id);
/**
* 根据规则编码获取规则(带缓存)
*
* @param ruleCode 规则编码
* @return 编码规则
*/
CodeRuleDO getRuleByCode(String ruleCode);
/**
* 分页查询编码规则
*
* @param pageReqVO 分页条件
* @return 分页结果
*/
PageResult<CodeRuleDO> getRulePage(CodeRulePageReqVO pageReqVO);
/**
* 获取启用状态的编码规则精简列表
*
* @return 规则列表
*/
List<CodeRuleDO> getEnabledRuleList();
}
- 步骤 2:编写 CodeRuleServiceImpl
package cn.code.nl.module.base.service.codegen;
import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.framework.common.util.object.BeanUtils;
import cn.code.nl.module.base.controller.admin.codegen.vo.CodeRulePageReqVO;
import cn.code.nl.module.base.controller.admin.codegen.vo.CodeRuleSaveReqVO;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeRuleDO;
import cn.code.nl.module.base.dal.mysql.codegen.CodeRuleMapper;
import cn.code.nl.module.base.service.codegen.enums.SeqStrategyEnum;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.base.enums.ErrorCodeConstants.*;
/**
* 编码规则管理 Service 实现类
*
* @author zhouz
*/
@Slf4j
@Service
@Validated
public class CodeRuleServiceImpl implements CodeRuleService {
/**
* Redis 缓存 key 前缀
*/
private static final String CACHE_KEY_PREFIX = "codegen:rule:";
/**
* 缓存过期时间(30分钟)
*/
private static final long CACHE_TTL_MINUTES = 30;
@Resource
private CodeRuleMapper codeRuleMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ObjectMapper objectMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRule(CodeRuleSaveReqVO createReqVO) {
// 校验规则编码唯一性
validateRuleCodeUnique(createReqVO.getRuleCode(), null);
// 校验分段配置至少包含一个 SEQUENCE 段
validateHasSequence(createReqVO.getSegments());
// 插入
CodeRuleDO rule = BeanUtils.toBean(createReqVO, CodeRuleDO.class);
rule.setSegmentConfig(createReqVO.getSegments());
// 默认策略
if (StrUtil.isBlank(rule.getSeqStrategy())) {
rule.setSeqStrategy(SeqStrategyEnum.SEGMENT.getCode());
}
codeRuleMapper.insert(rule);
log.info("创建编码规则成功,ruleCode={}", rule.getRuleCode());
return rule.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateRule(CodeRuleSaveReqVO updateReqVO) {
// 校验存在
CodeRuleDO existRule = validateRuleExists(updateReqVO.getId());
// 校验规则编码唯一性
validateRuleCodeUnique(updateReqVO.getRuleCode(), updateReqVO.getId());
// 校验分段配置至少包含一个 SEQUENCE 段
validateHasSequence(updateReqVO.getSegments());
// 更新
CodeRuleDO updateObj = BeanUtils.toBean(updateReqVO, CodeRuleDO.class);
updateObj.setSegmentConfig(updateReqVO.getSegments());
codeRuleMapper.updateById(updateObj);
// 清除缓存
clearCache(existRule.getRuleCode());
// 如果 ruleCode 变更了,也清除旧 key
if (!existRule.getRuleCode().equals(updateReqVO.getRuleCode())) {
clearCache(existRule.getRuleCode());
}
log.info("更新编码规则成功,ruleCode={}", updateReqVO.getRuleCode());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteRule(Long id) {
// 校验存在
CodeRuleDO rule = validateRuleExists(id);
// 删除
codeRuleMapper.deleteById(id);
// 清除缓存
clearCache(rule.getRuleCode());
log.info("删除编码规则成功,ruleCode={}", rule.getRuleCode());
}
@Override
public CodeRuleDO getRule(Long id) {
return validateRuleExists(id);
}
@Override
public CodeRuleDO getRuleByCode(String ruleCode) {
// 先从缓存读取
String cacheKey = CACHE_KEY_PREFIX + ruleCode;
String cached = stringRedisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isNotBlank(cached)) {
try {
return objectMapper.readValue(cached, CodeRuleDO.class);
} catch (JsonProcessingException e) {
log.warn("解析编码规则缓存失败,ruleCode={}", ruleCode, e);
}
}
// 缓存未命中,查数据库
CodeRuleDO rule = codeRuleMapper.selectByRuleCode(ruleCode);
if (rule == null) {
throw exception(CODE_RULE_NOT_EXISTS);
}
// 校验启用状态
if (rule.getStatus() != null && rule.getStatus() == 1) {
throw exception(CODE_RULE_DISABLED);
}
// 写入缓存
try {
String json = objectMapper.writeValueAsString(rule);
stringRedisTemplate.opsForValue().set(cacheKey, json, CACHE_TTL_MINUTES, TimeUnit.MINUTES);
} catch (JsonProcessingException e) {
log.warn("序列化编码规则缓存失败,ruleCode={}", ruleCode, e);
}
return rule;
}
@Override
public PageResult<CodeRuleDO> getRulePage(CodeRulePageReqVO pageReqVO) {
return codeRuleMapper.selectPage(pageReqVO);
}
@Override
public List<CodeRuleDO> getEnabledRuleList() {
// 查询启用状态的规则
return codeRuleMapper.selectList(CodeRuleDO::getStatus, 0);
}
/**
* 清除规则缓存
*/
private void clearCache(String ruleCode) {
stringRedisTemplate.delete(CACHE_KEY_PREFIX + ruleCode);
}
/**
* 校验规则编码唯一性
*/
private void validateRuleCodeUnique(String ruleCode, Long excludeId) {
CodeRuleDO exist = codeRuleMapper.selectByRuleCode(ruleCode);
if (exist == null) {
return;
}
if (excludeId == null || !exist.getId().equals(excludeId)) {
throw exception(CODE_RULE_CODE_DUPLICATE);
}
}
/**
* 校验规则存在
*/
private CodeRuleDO validateRuleExists(Long id) {
if (id == null) {
throw exception(CODE_RULE_NOT_EXISTS);
}
CodeRuleDO rule = codeRuleMapper.selectById(id);
if (rule == null) {
throw exception(CODE_RULE_NOT_EXISTS);
}
return rule;
}
/**
* 校验分段配置至少包含一个 SEQUENCE 段
*/
private void validateHasSequence(List<?> segments) {
boolean hasSequence = segments.stream()
.anyMatch(s -> "SEQUENCE".equals(
s instanceof cn.code.nl.module.base.service.codegen.dto.SegmentConfig sc
? sc.getType() : ""));
if (!hasSequence) {
throw exception(CODE_RULE_NOT_EXISTS);
}
}
}
- 步骤 3:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleService.java \
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleServiceImpl.java
git commit -m "feat: 新增编码规则管理Service"
任务 9:SequenceAllocator 号段预分配器
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java -
步骤 1:编写 SequenceAllocator
package cn.code.nl.module.base.service.codegen;
import cn.code.nl.module.base.dal.mysql.codegen.CodeSequenceMapper;
import cn.code.nl.module.base.service.codegen.dto.SequenceSegment;
import cn.code.nl.module.base.service.codegen.enums.ResetByEnum;
import cn.hutool.core.util.StrUtil;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.base.enums.ErrorCodeConstants.CODE_GEN_SEQ_EXCEED_MAX;
/**
* 序号分配器 —— 号段预分配 + 分布式锁实时两种策略
*
* @author zhouz
*/
@Slf4j
@Component
public class SequenceAllocator {
/**
* 默认号段预取步长
*/
private static final int DEFAULT_STEP = 100;
/**
* 本地号段缓存:key = "ruleId:resetKey", value = 当前号段末尾值
* 号段分配时配合 AtomicLong 做内存 CAS
*/
private final ConcurrentHashMap<String, AtomicLong> localSegmentCache = new ConcurrentHashMap<>();
@Resource
private CodeSequenceMapper codeSequenceMapper;
/**
* 分配一个序号(号段预分配策略)
*
* @param ruleId 规则ID
* @param segment 序号段配置
* @return 格式化后的序号字符串
*/
public String allocateBySegment(Long ruleId, SequenceSegment segment) {
String resetKey = buildResetKey(segment.getResetBy());
String cacheKey = ruleId + ":" + resetKey;
Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L;
// 从本地缓存取号段
AtomicLong current = localSegmentCache.get(cacheKey);
long seqValue;
if (current != null) {
seqValue = current.incrementAndGet();
// 检查是否还在号段范围内(号段末尾存储在 DB 的 current_value 中)
// 简单策略:DB 的 current_value 就是号段末尾,本地从 current_value - step + 1 开始递增
// 这里做一个简化的判断:如果当前值还没超过本地记录,直接返回
if (seqValue <= getSegmentEnd(cacheKey)) {
return formatSequence(seqValue, segment);
}
// 号段耗尽,需要重新预取
localSegmentCache.remove(cacheKey);
}
// 号段耗尽或不存在,从 DB 预取
seqValue = fetchSegment(cacheKey, ruleId, resetKey, segment, startAt);
return formatSequence(seqValue, segment);
}
/**
* 分配一个序号(分布式锁实时策略)
*
* @param ruleId 规则ID
* @param segment 序号段配置
* @return 格式化后的序号字符串
*/
public String allocateByLock(Long ruleId, SequenceSegment segment) {
String resetKey = buildResetKey(segment.getResetBy());
String lockKey = "codegen:seq:" + ruleId + ":" + resetKey;
return allocateWithLock(lockKey, ruleId, resetKey, segment);
}
/**
* Lock4j 保护下实时分配序号
*/
@Lock4j(keys = "#lockKey", acquireTimeout = 3000, expire = 5000)
public String allocateWithLock(String lockKey, Long ruleId, String resetKey, SequenceSegment segment) {
Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L;
// 用 step=1 意味着每次只取一个号
codeSequenceMapper.updateCurrentValueBySegment(ruleId, resetKey, 1, startAt, segment.getMaxValue());
// 查回当前值
var seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey);
if (seq == null) {
// 极端情况:首次插入后未查到(理论上不会发生)
return formatSequence(startAt, segment);
}
// 检查上限
if (seq.getCurrentValue() > segment.getMaxValue()) {
throw exception(CODE_GEN_SEQ_EXCEED_MAX);
}
return formatSequence(seq.getCurrentValue(), segment);
}
/**
* 从 DB 预取号段并缓存到本地
*/
private Long fetchSegment(String cacheKey, Long ruleId, String resetKey,
SequenceSegment segment, Long startAt) {
// 加锁预取号段
String lockKey = "codegen:seq:" + ruleId + ":" + resetKey;
return fetchSegmentWithLock(lockKey, cacheKey, ruleId, resetKey, segment, startAt);
}
/**
* Lock4j 保护下预取号段
*/
@Lock4j(keys = "#lockKey", acquireTimeout = 3000, expire = 5000)
public Long fetchSegmentWithLock(String lockKey, String cacheKey, Long ruleId,
String resetKey, SequenceSegment segment, Long startAt) {
// double-check:其他线程可能已经预取了
AtomicLong existing = localSegmentCache.get(cacheKey);
if (existing != null) {
return existing.incrementAndGet();
}
// DB 原子更新:current_value = current_value + step
codeSequenceMapper.updateCurrentValueBySegment(ruleId, resetKey, DEFAULT_STEP, startAt, segment.getMaxValue());
// 查回最新 current_value
var seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey);
Long newCurrentValue = seq.getCurrentValue();
long segmentStart = newCurrentValue - DEFAULT_STEP + 1;
// 检查上限
if (newCurrentValue > segment.getMaxValue()) {
log.error("序号超出最大值上限,ruleId={}, resetKey={}, currentValue={}, maxValue={}",
ruleId, resetKey, newCurrentValue, segment.getMaxValue());
throw exception(CODE_GEN_SEQ_EXCEED_MAX);
}
// 缓存号段到本地,AtomicLong 从 segmentStart - 1 开始,首次递增得到 segmentStart
AtomicLong counter = new AtomicLong(segmentStart - 1);
localSegmentCache.put(cacheKey, counter);
log.info("预取号段成功,ruleId={}, resetKey={}, 号段区间=[{}, {}]",
ruleId, resetKey, segmentStart, newCurrentValue);
return counter.incrementAndGet();
}
/**
* 获取号段末尾值(从缓存 key 反查)
* 简化实现:如果缓存中存在,说明号段有效
*/
private long getSegmentEnd(String cacheKey) {
// 简化实现:直接在分配时判断
return Long.MAX_VALUE;
}
/**
* 根据重置维度构建 resetKey
*
* @param resetBy 重置维度
* @return 重置键
*/
public String buildResetKey(String resetBy) {
if (StrUtil.isBlank(resetBy)) {
return ResetByEnum.GLOBAL.getCode();
}
ResetByEnum resetByEnum = ResetByEnum.valueOf(resetBy);
return switch (resetByEnum) {
case DAY -> LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
case MONTH -> LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
case YEAR -> String.valueOf(Year.now().getValue());
case GLOBAL -> "GLOBAL";
};
}
/**
* 补位格式化序号
*
* @param value 序号值
* @param segment 序号段配置
* @return 格式化后的字符串
*/
public String formatSequence(long value, SequenceSegment segment) {
String raw = String.valueOf(value);
int paddingLen = segment.getPaddingLen() != null ? segment.getPaddingLen() : 0;
if (paddingLen <= 0 || raw.length() >= paddingLen) {
return raw;
}
String paddingChar = StrUtil.isNotBlank(segment.getPaddingChar())
? segment.getPaddingChar() : "0";
// 用补位字符填充到目标长度
return paddingChar.repeat(paddingLen - raw.length()) + raw;
}
}
- 步骤 2:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java
git commit -m "feat: 新增序号分配器(号段预分配+分布式锁混合策略)"
任务 10:CodeGenService 编码生成引擎
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenService.java -
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenServiceImpl.java -
步骤 1:编写 CodeGenService 接口
package cn.code.nl.module.base.service.codegen;
/**
* 编码生成 Service 接口
*
* @author zhouz
*/
public interface CodeGenService {
/**
* 根据规则编码生成编码字符串
*
* @param ruleCode 规则编码
* @return 生成的编码
*/
String generate(String ruleCode);
}
- 步骤 2:编写 CodeGenServiceImpl
package cn.code.nl.module.base.service.codegen;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeRuleDO;
import cn.code.nl.module.base.service.codegen.dto.DateSegment;
import cn.code.nl.module.base.service.codegen.dto.PrefixSegment;
import cn.code.nl.module.base.service.codegen.dto.SegmentConfig;
import cn.code.nl.module.base.service.codegen.dto.SequenceSegment;
import cn.code.nl.module.base.service.codegen.enums.LetterCaseEnum;
import cn.code.nl.module.base.service.codegen.enums.SeqStrategyEnum;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.base.enums.ErrorCodeConstants.CODE_GEN_LENGTH_EXCEED;
/**
* 编码生成引擎实现类
*
* @author zhouz
*/
@Slf4j
@Service
@Validated
public class CodeGenServiceImpl implements CodeGenService {
@Resource
private CodeRuleService codeRuleService;
@Resource
private SequenceAllocator sequenceAllocator;
@Override
public String generate(String ruleCode) {
// 1. 获取规则配置(带缓存)
CodeRuleDO rule = codeRuleService.getRuleByCode(ruleCode);
// 2. 遍历分段配置,逐段生成
List<SegmentConfig> segments = rule.getSegmentConfig();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < segments.size(); i++) {
String segmentValue = buildSegment(rule, segments.get(i));
sb.append(segmentValue);
// 段之间加分隔符
if (i < segments.size() - 1 && StrUtil.isNotBlank(rule.getSeparator())) {
sb.append(rule.getSeparator());
}
}
// 3. 后处理
return postProcess(sb.toString(), rule);
}
/**
* 根据段类型分发处理,生成段值
*/
private String buildSegment(CodeRuleDO rule, SegmentConfig segment) {
return switch (segment.getType()) {
case "PREFIX" -> buildPrefixSegment((PrefixSegment) segment);
case "DATE" -> buildDateSegment((DateSegment) segment);
case "SEQUENCE" -> buildSequenceSegment(rule, (SequenceSegment) segment);
default -> throw new IllegalArgumentException("不支持的段类型: " + segment.getType());
};
}
/**
* 生成固定前缀段
*/
private String buildPrefixSegment(PrefixSegment segment) {
return segment.getValue() != null ? segment.getValue() : "";
}
/**
* 生成日期段
*/
private String buildDateSegment(DateSegment segment) {
String format = StrUtil.isNotBlank(segment.getFormat())
? segment.getFormat() : "yyyyMMdd";
return LocalDate.now().format(DateTimeFormatter.ofPattern(format));
}
/**
* 生成流水序号段
*/
private String buildSequenceSegment(CodeRuleDO rule, SequenceSegment segment) {
String strategy = rule.getSeqStrategy();
if (SeqStrategyEnum.LOCK.getCode().equals(strategy)) {
return sequenceAllocator.allocateByLock(rule.getId(), segment);
}
return sequenceAllocator.allocateBySegment(rule.getId(), segment);
}
/**
* 后处理:大小写转换 + 总长度校验
*/
private String postProcess(String code, CodeRuleDO rule) {
// 大小写转换
if (rule.getLetterCase() != null && rule.getLetterCase() == LetterCaseEnum.UPPER.getCode()) {
code = code.toUpperCase();
} else if (rule.getLetterCase() != null && rule.getLetterCase() == LetterCaseEnum.LOWER.getCode()) {
code = code.toLowerCase();
}
// 总长度校验
if (rule.getTotalLength() != null && rule.getTotalLength() > 0 && code.length() > rule.getTotalLength()) {
log.error("生成的编码超出总长度限制,code={}, length={}, maxLength={}",
code, code.length(), rule.getTotalLength());
throw exception(CODE_GEN_LENGTH_EXCEED);
}
return code;
}
}
- 步骤 3:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenService.java \
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenServiceImpl.java
git commit -m "feat: 新增编码生成核心引擎"
任务 11:CodeRuleController 规则管理接口
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/CodeRuleController.java -
步骤 1:编写 CodeRuleController
package cn.code.nl.module.base.controller.admin.codegen;
import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.framework.common.util.object.BeanUtils;
import cn.code.nl.module.base.controller.admin.codegen.vo.*;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeRuleDO;
import cn.code.nl.module.base.service.codegen.CodeRuleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.code.nl.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 编码规则
*
* @author zhouz
*/
@Tag(name = "管理后台 - 编码规则")
@RestController
@RequestMapping("/base/code-rule")
@Validated
public class CodeRuleController {
@Resource
private CodeRuleService codeRuleService;
/**
* 创建编码规则
*/
@PostMapping("/create")
@Operation(summary = "创建编码规则")
@PreAuthorize("@ss.hasPermission('base:code-rule:create')")
public CommonResult<Long> createCodeRule(@Valid @RequestBody CodeRuleSaveReqVO createReqVO) {
return success(codeRuleService.createRule(createReqVO));
}
/**
* 更新编码规则
*/
@PutMapping("/update")
@Operation(summary = "更新编码规则")
@PreAuthorize("@ss.hasPermission('base:code-rule:update')")
public CommonResult<Boolean> updateCodeRule(@Valid @RequestBody CodeRuleSaveReqVO updateReqVO) {
codeRuleService.updateRule(updateReqVO);
return success(true);
}
/**
* 删除编码规则
*/
@DeleteMapping("/delete")
@Operation(summary = "删除编码规则")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('base:code-rule:delete')")
public CommonResult<Boolean> deleteCodeRule(@RequestParam("id") Long id) {
codeRuleService.deleteRule(id);
return success(true);
}
/**
* 获取编码规则详情
*/
@GetMapping("/get")
@Operation(summary = "获得编码规则")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('base:code-rule:query')")
public CommonResult<CodeRuleRespVO> getCodeRule(@RequestParam("id") Long id) {
CodeRuleDO rule = codeRuleService.getRule(id);
CodeRuleRespVO respVO = BeanUtils.toBean(rule, CodeRuleRespVO.class);
respVO.setSegments(rule.getSegmentConfig());
return success(respVO);
}
/**
* 分页查询编码规则
*/
@GetMapping("/page")
@Operation(summary = "获得编码规则分页")
@PreAuthorize("@ss.hasPermission('base:code-rule:query')")
public CommonResult<PageResult<CodeRuleRespVO>> getCodeRulePage(@Valid CodeRulePageReqVO pageReqVO) {
PageResult<CodeRuleDO> pageResult = codeRuleService.getRulePage(pageReqVO);
PageResult<CodeRuleRespVO> result = BeanUtils.toBean(pageResult, CodeRuleRespVO.class);
// 补充 segmentConfig(BeanUtils.toBean 无法映射 JSON 字段)
for (int i = 0; i < pageResult.getList().size(); i++) {
result.getList().get(i).setSegments(pageResult.getList().get(i).getSegmentConfig());
}
return success(result);
}
/**
* 获取启用状态的编码规则精简列表(下拉选项)
*/
@GetMapping("/simple-list")
@Operation(summary = "获取编码规则精简列表")
public CommonResult<List<CodeRuleSimpleRespVO>> getSimpleCodeRuleList() {
List<CodeRuleDO> list = codeRuleService.getEnabledRuleList();
return success(BeanUtils.toBean(list, CodeRuleSimpleRespVO.class));
}
}
- 步骤 2:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/CodeRuleController.java
git commit -m "feat: 新增编码规则管理Controller"
任务 12:CodeGenController (RPC API 实现)
文件:
-
创建:
nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/api/CodeGenController.java -
步骤 1:编写 CodeGenController
package cn.code.nl.module.base.controller.api;
import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO;
import cn.code.nl.module.base.service.codegen.CodeGenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.code.nl.framework.common.pojo.CommonResult.success;
/**
* RPC API - 编码生成(供其他模块 Feign 调用)
*
* @author zhouz
*/
@Tag(name = "RPC API - 编码生成")
@RestController
@RequestMapping("/rpc-api/base/code-gen")
@Validated
public class CodeGenController {
@Resource
private CodeGenService codeGenService;
/**
* 生成编码
*/
@PostMapping("/generate")
@Operation(summary = "生成编码")
public CommonResult<String> generate(@Valid @RequestBody CodeGenerateReqDTO reqDTO) {
String code = codeGenService.generate(reqDTO.getRuleCode());
return success(code);
}
}
- 步骤 2:Commit
git add nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/api/CodeGenController.java
git commit -m "feat: 新增编码生成RPC API Controller"
任务 13:编译验证
- 步骤 1:编译项目
cd /Users/zhouz/item/huachuang/huachuang
mvn clean compile -pl nl-module-base -am -DskipTests
预期:BUILD SUCCESS
- 步骤 2:运行完整 install(含测试)
mvn clean install -pl nl-module-base -am
预期:BUILD SUCCESS
自检清单
1. 规格覆盖度:
- ✅ 三种段类型(PREFIX/DATE/SEQUENCE)— 任务 3(DTO)+ 任务 10(引擎)
- ✅ 序号控制(按天/月/年重置、补位、最大值)— 任务 9(SequenceAllocator)
- ✅ 格式自定义(分隔符、大小写、总长度)— 任务 6(VO)+ 任务 10(引擎)
- ✅ 并发唯一性(号段预分配 + Lock4j)— 任务 9(SequenceAllocator)
- ✅ 规则管理 CRUD + 缓存 — 任务 8(Service)+ 任务 11(Controller)
- ✅ 对外 Feign API — 任务 7(API)+ 任务 12(Controller)
2. 占位符扫描: 无 TODO、无占位,通过 ✅
3. 类型一致性:
SegmentConfig及其子类的 Jackson 多态配置与CodeRuleDO.segmentConfig的JacksonTypeHandler匹配 ✅CodeRuleSaveReqVO.segments使用List<SegmentConfig>与 DO 一致 ✅SequenceAllocator.allocateBySegment()签名与CodeGenServiceImpl.buildSequenceSegment()调用匹配 ✅- Controller 路径与设计规格一致 ✅