Files
huachuang/docs/superpowers/plans/2026-07-15-codegen-plan.md
2026-07-15 16:45:01 +08:00

1913 lines
55 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 编码生成功能 实现计划
> **面向 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
---
### 任务 1SQL 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 '编码流水号段记录表';
```
- [ ] **步骤 2Commit**
```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;
}
```
- [ ] **步骤 6Commit**
```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: 新增编码生成相关枚举和错误码"
```
---
### 任务 3SegmentConfig 分段配置 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;
}
```
- [ ] **步骤 5Commit**
```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: 新增编码分段配置DTOJackson多态"
```
---
### 任务 4DataObject 数据对象
**文件:**
- 创建:`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> 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;
}
```
- [ ] **步骤 3Commit**
```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"
```
---
### 任务 5Mapper 层
**文件:**
- 创建:`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<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**
```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<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
<?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>
```
- [ ] **步骤 4Commit**
```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"
```
---
### 任务 6VO 类(请求/响应)
**文件:**
- 创建:`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<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**
```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<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**
```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;
}
```
- [ ] **步骤 5Commit**
```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类"
```
---
### 任务 7API 模块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<String, String> 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<String> generate(@Valid @RequestBody CodeGenerateReqDTO reqDTO);
}
```
- [ ] **步骤 3Commit**
```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接口"
```
---
### 任务 8CodeRuleService 规则管理服务
**文件:**
- 创建:`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<CodeRuleDO> getRulePage(CodeRulePageReqVO pageReqVO);
/**
* 获取启用状态的编码规则精简列表
*
* @return 规则列表
*/
List<CodeRuleDO> 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<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);
}
}
}
```
- [ ] **步骤 3Commit**
```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"
```
---
### 任务 9SequenceAllocator 号段预分配器
**文件:**
- 创建:`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<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;
}
}
```
- [ ] **步骤 2Commit**
```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: 新增序号分配器(号段预分配+分布式锁混合策略)"
```
---
### 任务 10CodeGenService 编码生成引擎
**文件:**
- 创建:`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<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;
}
}
```
- [ ] **步骤 3Commit**
```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: 新增编码生成核心引擎"
```
---
### 任务 11CodeRuleController 规则管理接口
**文件:**
- 创建:`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<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);
// 补充 segmentConfigBeanUtils.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));
}
}
```
- [ ] **步骤 2Commit**
```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"
```
---
### 任务 12CodeGenController 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<String> generate(@Valid @RequestBody CodeGenerateReqDTO reqDTO) {
String code = codeGenService.generate(reqDTO.getRuleCode());
return success(code);
}
}
```
- [ ] **步骤 2Commit**
```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— 任务 3DTO+ 任务 10引擎
- ✅ 序号控制(按天/月/年重置、补位、最大值)— 任务 9SequenceAllocator
- ✅ 格式自定义(分隔符、大小写、总长度)— 任务 6VO+ 任务 10引擎
- ✅ 并发唯一性(号段预分配 + Lock4j— 任务 9SequenceAllocator
- ✅ 规则管理 CRUD + 缓存 — 任务 8Service+ 任务 11Controller
- ✅ 对外 Feign API — 任务 7API+ 任务 12Controller
**2. 占位符扫描:** 无 TODO、无占位通过 ✅
**3. 类型一致性:**
- `SegmentConfig` 及其子类的 Jackson 多态配置与 `CodeRuleDO.segmentConfig``JacksonTypeHandler` 匹配 ✅
- `CodeRuleSaveReqVO.segments` 使用 `List<SegmentConfig>` 与 DO 一致 ✅
- `SequenceAllocator.allocateBySegment()` 签名与 `CodeGenServiceImpl.buildSequenceSegment()` 调用匹配 ✅
- Controller 路径与设计规格一致 ✅