diff --git a/docs/superpowers/plans/2026-07-15-codegen-plan.md b/docs/superpowers/plans/2026-07-15-codegen-plan.md new file mode 100644 index 00000000..f92e3ba0 --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-codegen-plan.md @@ -0,0 +1,1912 @@ +# 编码生成功能 实现计划 + +> **面向 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** + +```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** + +```bash +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 定义后面追加: + +```java + // ========== 编码生成 ========== + 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** + +```java +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** + +```java +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** + +```java +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** + +```java +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** + +```bash +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 基类** + +```java +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** + +```java +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** + +```java +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** + +```java +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** + +```bash +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** + +```java +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; + + /** + * 分隔符 + */ + 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** + +```java +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** + +```bash +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** + +```java +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 { + + /** + * 分页查询编码规则 + * + * @param reqVO 查询条件 + * @return 分页结果 + */ + default PageResult selectPage(CodeRulePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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** + +```java +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 { + + /** + * 根据规则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 + + + + + + + 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} + + + +``` + +- [ ] **步骤 4:Commit** + +```bash +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** + +```java +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 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** + +```java +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** + +```java +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 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** + +```java +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** + +```bash +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** + +```java +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 bizParams; + +} +``` + +- [ ] **步骤 2:编写 CodeGenApi** + +```java +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 generate(@Valid @RequestBody CodeGenerateReqDTO reqDTO); + +} +``` + +- [ ] **步骤 3:Commit** + +```bash +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 接口** + +```java +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 getRulePage(CodeRulePageReqVO pageReqVO); + + /** + * 获取启用状态的编码规则精简列表 + * + * @return 规则列表 + */ + List getEnabledRuleList(); + +} +``` + +- [ ] **步骤 2:编写 CodeRuleServiceImpl** + +```java +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 getRulePage(CodeRulePageReqVO pageReqVO) { + return codeRuleMapper.selectPage(pageReqVO); + } + + @Override + public List 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** + +```bash +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** + +```java +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 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** + +```bash +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 接口** + +```java +package cn.code.nl.module.base.service.codegen; + +/** + * 编码生成 Service 接口 + * + * @author zhouz + */ +public interface CodeGenService { + + /** + * 根据规则编码生成编码字符串 + * + * @param ruleCode 规则编码 + * @return 生成的编码 + */ + String generate(String ruleCode); + +} +``` + +- [ ] **步骤 2:编写 CodeGenServiceImpl** + +```java +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 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** + +```bash +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** + +```java +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 createCodeRule(@Valid @RequestBody CodeRuleSaveReqVO createReqVO) { + return success(codeRuleService.createRule(createReqVO)); + } + + /** + * 更新编码规则 + */ + @PutMapping("/update") + @Operation(summary = "更新编码规则") + @PreAuthorize("@ss.hasPermission('base:code-rule:update')") + public CommonResult 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 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 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> getCodeRulePage(@Valid CodeRulePageReqVO pageReqVO) { + PageResult pageResult = codeRuleService.getRulePage(pageReqVO); + PageResult 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> getSimpleCodeRuleList() { + List list = codeRuleService.getEnabledRuleList(); + return success(BeanUtils.toBean(list, CodeRuleSimpleRespVO.class)); + } + +} +``` + +- [ ] **步骤 2:Commit** + +```bash +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** + +```java +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 generate(@Valid @RequestBody CodeGenerateReqDTO reqDTO) { + String code = codeGenService.generate(reqDTO.getRuleCode()); + return success(code); + } + +} +``` + +- [ ] **步骤 2:Commit** + +```bash +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:编译项目** + +```bash +cd /Users/zhouz/item/huachuang/huachuang +mvn clean compile -pl nl-module-base -am -DskipTests +``` + +预期:BUILD SUCCESS + +- [ ] **步骤 2:运行完整 install(含测试)** + +```bash +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` 与 DO 一致 ✅ +- `SequenceAllocator.allocateBySegment()` 签名与 `CodeGenServiceImpl.buildSequenceSegment()` 调用匹配 ✅ +- Controller 路径与设计规格一致 ✅