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 路径与设计规格一致 ✅ diff --git a/docs/superpowers/specs/2026-07-15-codegen-design.md b/docs/superpowers/specs/2026-07-15-codegen-design.md new file mode 100644 index 00000000..74aac249 --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-codegen-design.md @@ -0,0 +1,359 @@ +# 编码生成功能设计规格 + +> 日期:2026-07-15 +> 分支:feature/20260713/base-module +> 模块:nl-module-base(基础模块) + +## 一、需求概述 + +在 base 模块下实现通用编码生成引擎,支持固定前缀、日期、流水序号三段组合,通过规则配置化管理,满足订单编码(`ORD202607150001`)、入库单编码(`CK202607150001`)等业务场景。 + +### 1.1 范围 + +**第一期实现:** +- 三种编码段类型:固定前缀(PREFIX)、日期(DATE)、流水序号(SEQUENCE) +- 序号控制:按天/按月/按年/全局重置、补位字符可自定义、最大值上限 +- 格式控制:分隔符配置、字母大小写转换、固定总长度约束 +- 并发唯一性:号段预分配 + 分布式锁实时两种策略混合 +- 规则管理:CRUD + 缓存 + +**本期不实现(预留扩展点):** +- 固定后缀段(SUFFIX)→ 与 PREFIX 逻辑相同,二期加 type 即可 +- 业务字段映射段(BIZ_FIELD) +- 随机段(RANDOM) +- 可视化拖拽配置 +- 多租户/数据隔离 +- 溢出告警(LOG 打点即可,不做通知) + +### 1.2 决策记录 + +| 决策项 | 选择 | 理由 | +|--------|------|------| +| 前端范围 | 仅后端 API | 聚焦核心引擎质量,前端可后续对接 | +| 并发方案 | 混合方案(号段预分配 + Lock4j 实时) | 高性能 + 灵活切换 | +| 段类型 | 核心三种(PREFIX/DATE/SEQUENCE) | 覆盖 80% 场景,其他预留扩展 | +| 存储模型 | 主表字段 + JSON 分段 | 基本属性可查询,分段配置灵活 | +| 重置维度 | 全部(天/月/年/全局) | 实现成本差异不大,一次到位 | + +--- + +## 二、数据库设计 + +### 2.1 编码规则表 `base_code_rule` + +```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 '编码规则定义表'; +``` + +### 2.2 流水号段表 `base_code_sequence` + +```sql +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.3 `segment_config` JSON 结构 + +```json +[ + { "type": "PREFIX", "value": "ORD" }, + { "type": "DATE", "format": "yyyyMMdd" }, + { "type": "SEQUENCE", "resetBy": "DAY", "startAt": 1, "paddingLen": 4, "paddingChar": "0", "maxValue": 9999 } +] +``` + +### 2.4 段类型字段定义 + +| 段类型 | 字段 | 说明 | +|--------|------|------| +| PREFIX | `value` | 固定前缀字符串 | +| DATE | `format` | Java 日期格式,如 `yyyyMMdd`、`yyyyMM`、`yyyy` | +| SEQUENCE | `resetBy` | 重置维度:`DAY`/`MONTH`/`YEAR`/`GLOBAL` | +| SEQUENCE | `startAt` | 起始值,默认 1 | +| SEQUENCE | `paddingLen` | 补位后总长度,如 4 → 0001 | +| SEQUENCE | `paddingChar` | 补位字符,默认 `"0"`,可任意字符串 | +| SEQUENCE | `maxValue` | 序号最大值上限 | + +--- + +## 三、包结构与模块划分 + +### 3.1 API 模块(`nl-module-base-api`) + +``` +cn.code.nl.module.base +├── enums/ +│ └── ErrorCodeConstants.java # 追加 CODE_RULE_NOT_EXISTS 等 +└── api/codegen/ + ├── CodeGenApi.java # Feign 接口,供其他模块调用 + └── dto/ + └── CodeGenerateReqDTO.java # 编码生成请求(ruleCode + bizParams) +``` + +### 3.2 Server 模块(`nl-module-base-server`) + +``` +cn.code.nl.module.base +├── controller/admin/codegen/ +│ ├── CodeRuleController.java # 规则 CRUD 接口 +│ └── vo/ +│ ├── CodeRuleSaveReqVO.java # 新增/编辑请求 +│ ├── CodeRulePageReqVO.java # 分页查询请求 +│ ├── CodeRuleRespVO.java # 规则响应 +│ └── CodeRuleSimpleRespVO.java # 精简响应(下拉选项) +├── service/codegen/ +│ ├── CodeRuleService.java # 规则管理接口 +│ ├── CodeRuleServiceImpl.java # 规则管理实现 +│ ├── CodeGenService.java # 编码生成接口 +│ ├── CodeGenServiceImpl.java # 编码生成核心引擎 +│ └── SequenceAllocator.java # 号段预分配器 +├── dal/dataobject/codegen/ +│ ├── CodeRuleDO.java # 编码规则 DO +│ └── CodeSequenceDO.java # 流水号段 DO +└── dal/mysql/codegen/ + ├── CodeRuleMapper.java + ├── CodeSequenceMapper.java + └── CodeSequenceMapper.xml # 流水号段的 UPDATE/DELETE SQL +``` + +--- + +## 四、核心流程设计 + +### 4.1 编码生成流程 + +``` +外部调用 generate(ruleCode) + │ + ▼ + ┌─ 查询规则缓存(Redis)──► 未命中时从 DB 加载并写缓存 + │ + ▼ + ┌─ 遍历分段配置,逐段生成: + │ PREFIX 段 ──► 直接返回配置的固定值 + │ DATE 段 ──► 按 format 格式化当前日期 + │ SEQUENCE 段──► 调用 SequenceAllocator.allocate() + │ + ▼ + ┌─ 拼装:段值之间插入 separator + │ + ▼ + ┌─ 后处理:letterCase 转换 / totalLength 校验 + │ + ▼ + 返回编码字符串 +``` + +### 4.2 SequenceAllocator 号段分配流程 + +``` +allocate(ruleId, resetKey, segment, strategy) + │ + ├── strategy = "lock" ──► Lock4j 加锁 → DB SELECT + UPDATE current_value+1 → 解锁 → 返回 + │ + └── strategy = "segment" ──► 先从内存 ConcurrentHashMap 取号段 + │ + ├── 命中且未用完 ──► 从当前号段取一个 → 返回 + │ + └── 未命中或耗尽 ──► + ├── Lock4j 加锁 + ├── DB UPDATE current_value = current_value + step(step=100) + ├── 解锁 + ├── 号段 [current_value-step+1, current_value] 放入本地缓存 + └── 从缓存取一个返回 +``` + +### 4.3 重置维度键计算 + +```java +public String buildResetKey(ResetByEnum resetBy) { + return switch (resetBy) { + case DAY -> LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); // "20260715" + case MONTH -> LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")); // "202607" + case YEAR -> String.valueOf(Year.now().getValue()); // "2026" + case GLOBAL -> "GLOBAL"; + }; +} +``` + +不同 `resetKey` 在 `base_code_sequence` 表中为不同行,各周期流水自然隔离。 + +### 4.4 并发保障总结 + +| 策略 | 实现 | 性能 | 连续性 | 适用场景 | +|------|------|------|--------|----------| +| `segment`(默认) | Lock4j + DB UPDATE 预取 + 内存 CAS | 高 | 允许断号 | 高并发订单编码 | +| `lock` | Lock4j + DB SELECT FOR UPDATE 实时 | 低 | 严格连续 | 低频关键编码 | + +--- + +## 五、API 设计 + +### 5.1 规则管理(Controller) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/base/code-rule/create` | 创建规则 | +| PUT | `/base/code-rule/update` | 更新规则 | +| DELETE | `/base/code-rule/delete?id=` | 删除规则 | +| GET | `/base/code-rule/get?id=` | 获取规则详情 | +| GET | `/base/code-rule/page` | 分页查询规则列表 | +| GET | `/base/code-rule/simple-list` | 精简列表(下拉选项) | + +### 5.2 编码生成(Feign API) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/base/code-gen/generate` | 生成编码 | + +请求体: +```json +{ "ruleCode": "ORDER_CODE" } +``` + +响应:纯字符串,如 `"ORD202607150001"` + +### 5.3 规则 Save 请求体示例 + +```json +{ + "ruleName": "订单编码", + "ruleCode": "ORDER_CODE", + "segments": [ + { "type": "PREFIX", "value": "ORD" }, + { "type": "DATE", "format": "yyyyMMdd" }, + { "type": "SEQUENCE", "resetBy": "DAY", "startAt": 1, "paddingLen": 4, "paddingChar": "0", "maxValue": 9999 } + ], + "separator": "", + "letterCase": 1, + "totalLength": 0, + "seqStrategy": "segment", + "status": 0, + "remark": "订单编号生成规则" +} +``` + +--- + +## 六、核心类与方法清单 + +### 6.1 CodeRuleServiceImpl + +| 方法 | 职责 | +|------|------| +| `createRule(CodeRuleSaveReqVO)` | 创建规则,校验 ruleCode 唯一性 | +| `updateRule(CodeRuleSaveReqVO)` | 更新规则,清除 Redis 缓存 | +| `deleteRule(Long id)` | 删除规则 | +| `getRule(Long id)` | 获取规则详情 | +| `getRuleByCode(String ruleCode)` | 根据 ruleCode 查规则(带 Redis 缓存) | +| `getRulePage(CodeRulePageReqVO)` | 分页查询规则列表 | + +### 6.2 CodeGenServiceImpl + +| 方法 | 职责 | +|------|------| +| `generate(String ruleCode)` | 编码生成入口 | +| `buildSegment(CodeRuleDO, SegmentConfig)` | 分发到具体段类型处理 | +| `buildPrefixSegment(PrefixSegment)` | 返回固定前缀值 | +| `buildDateSegment(DateSegment)` | 按 format 格式化当前日期 | +| `buildSequenceSegment(CodeRuleDO, SequenceSegment)` | 委托 SequenceAllocator 分配序号 | +| `postProcess(String code, CodeRuleDO)` | 大小写转换 + 总长度校验 | + +### 6.3 SequenceAllocator + +| 方法 | 职责 | +|------|------| +| `allocate(Long ruleId, SequenceSegment, String strategy)` | 序号分配入口 | +| `allocateByLock(Long ruleId, String resetKey, SequenceSegment)` | Lock4j 锁 + DB 实时分配 | +| `allocateBySegment(Long ruleId, String resetKey, SequenceSegment)` | 号段预分配(内存 CAS + Lock4j + DB) | +| `buildResetKey(String resetBy)` | 根据重置维度计算 resetKey | +| `formatSequence(long value, SequenceSegment)` | 补位格式化 | + +### 6.4 数据对象 + +| 类 | 映射表 | 说明 | +|----|--------|------| +| `CodeRuleDO` | `base_code_rule` | segmentConfig 用 JacksonTypeHandler | +| `CodeSequenceDO` | `base_code_sequence` | ruleId + resetKey 联合定位 | + +### 6.5 枚举 + +| 枚举 | 值 | 说明 | +|------|-----|------| +| `SegmentTypeEnum` | `PREFIX` / `DATE` / `SEQUENCE` | 段类型 | +| `ResetByEnum` | `DAY` / `MONTH` / `YEAR` / `GLOBAL` | 重置维度 | +| `LetterCaseEnum` | `NONE(0)` / `UPPER(1)` / `LOWER(2)` | 大小写 | +| `SeqStrategyEnum` | `SEGMENT` / `LOCK` | 序号策略 | + +### 6.6 错误码(追加到 ErrorCodeConstants,code 从 9 开始) + +| 常量名 | code | 说明 | +|--------|------|------| +| `CODE_RULE_NOT_EXISTS` | 9 | 编码规则不存在 | +| `CODE_RULE_CODE_DUPLICATE` | 10 | 规则编码已存在 | +| `CODE_GEN_SEQ_EXCEED_MAX` | 11 | 序号超出最大值上限 | +| `CODE_GEN_LENGTH_EXCEED` | 12 | 生成的编码超出总长度限制 | + +--- + +## 七、缓存设计 + +| 缓存 Key | 内容 | TTL | 失效时机 | +|----------|------|-----|----------| +| `codegen:rule:{ruleCode}` | 规则 DO(含分段配置) | 30 分钟 | 规则更新/删除时主动清除 | +| 内存号段 `{ruleId}:{resetKey}` | 预分配号段区间 | 用完即弃 | 号段耗尽后自动刷新 | + +--- + +## 八、边界与异常处理 + +| 场景 | 处理方式 | +|------|----------| +| 规则不存在 | 抛出 `CODE_RULE_NOT_EXISTS` 业务异常 | +| 规则被禁用 | 抛出业务异常 | +| 序号达上限 | Lock4j 锁内检测,达上限抛 `CODE_GEN_SEQ_EXCEED_MAX`,日志记录 | +| 生成后总长度超限 | 校验后抛 `CODE_GEN_LENGTH_EXCEED` | +| 号段预分配时并发冲突 | Lock4j 保证串行,不冲突 | +| 服务重启 | 内存号段丢失,从 DB 重新预取新的 range(少量断号可接受) | +| segment 策略下 DB 不可用 | 如已缓存号段则继续分配,耗尽后失败;全局不可用则抛异常 | + +--- + +## 九、二期扩展预留 + +| 扩展项 | 实现方式 | +|--------|----------| +| SUFFIX 段 | 在 `SegmentTypeEnum` 加 `SUFFIX`,逻辑同 PREFIX | +| BIz_FIELD 段 | 在 `CodeGenerateReqDTO.bizParams` 中传入字段值,`buildBizFieldSegment` 读取 | +| RANDOM 段 | 加 `RANDOM` 类型,配置位数和字符集(数字/字母/混合),用 `SecureRandom` 生成 | +| 溢出告警 | 加 `AlertService` 发钉钉通知,配置项控制 | diff --git a/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/CodeGenApi.java b/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/CodeGenApi.java new file mode 100644 index 00000000..6ac04ef9 --- /dev/null +++ b/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/CodeGenApi.java @@ -0,0 +1,40 @@ +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; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 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); + + @GetMapping(PREFIX + "/preview") + @Operation(summary = "预览下一个编码(不消耗序号)") + CommonResult preview(@RequestParam("ruleCode") String ruleCode); + +} diff --git a/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/dto/CodeGenerateReqDTO.java b/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/dto/CodeGenerateReqDTO.java new file mode 100644 index 00000000..a449edbd --- /dev/null +++ b/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/api/codegen/dto/CodeGenerateReqDTO.java @@ -0,0 +1,27 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/enums/ErrorCodeConstants.java b/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/enums/ErrorCodeConstants.java index cf79db85..158d08b5 100644 --- a/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/enums/ErrorCodeConstants.java +++ b/nl-module-base/nl-module-base-api/src/main/java/cn/code/nl/module/base/enums/ErrorCodeConstants.java @@ -14,4 +14,14 @@ public interface ErrorCodeConstants { ErrorCode CLASSSTANDARD_PARENT_ERROR = new ErrorCode(5, "不能设置自己为父分类"); ErrorCode CLASSSTANDARD_PARENT_IS_CHILD = new ErrorCode(6, "不能设置自己的子分类为父分类"); ErrorCode CLASSSTANDARD_EXISTS_CHILDREN = new ErrorCode(7, "存在子分类,无法删除"); + + // ========== 计量单位 ========== + ErrorCode MEASURE_UNIT_NOT_EXISTS = new ErrorCode(8, "计量单位不存在"); + + // ========== 编码生成 ========== + 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, "编码规则已被禁用"); } diff --git a/nl-module-base/nl-module-base-server/pom.xml b/nl-module-base/nl-module-base-server/pom.xml index ef5da7d2..639c9046 100644 --- a/nl-module-base/nl-module-base-server/pom.xml +++ b/nl-module-base/nl-module-base-server/pom.xml @@ -46,7 +46,15 @@ cn.nl.cloud nl-spring-boot-starter-biz-ip - + + cn.nl.cloud + nl-spring-boot-starter-protection + + + + com.baomidou + lock4j-redisson-spring-boot-starter + diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/CodeRuleController.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/CodeRuleController.java new file mode 100644 index 00000000..840634ee --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/CodeRuleController.java @@ -0,0 +1,120 @@ +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.CodeGenService; +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; + + @Resource + private CodeGenService codeGenService; + + /** + * 创建编码规则 + */ + @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)); + } + + @GetMapping("/preview") + @Operation(summary = "预览下一个编码(不消耗序号)") + @PreAuthorize("@ss.hasPermission('base:code-rule:query')") + public CommonResult previewCode(@RequestParam("ruleCode") String ruleCode) { + return success(codeGenService.preview(ruleCode)); + } + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRulePageReqVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRulePageReqVO.java new file mode 100644 index 00000000..17ceeaac --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRulePageReqVO.java @@ -0,0 +1,40 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleRespVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleRespVO.java new file mode 100644 index 00000000..d980396a --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleRespVO.java @@ -0,0 +1,72 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSaveReqVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSaveReqVO.java new file mode 100644 index 00000000..f28cb379 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSaveReqVO.java @@ -0,0 +1,71 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSimpleRespVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSimpleRespVO.java new file mode 100644 index 00000000..1bf9e59d --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/codegen/vo/CodeRuleSimpleRespVO.java @@ -0,0 +1,28 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/MeasureUnitController.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/MeasureUnitController.java new file mode 100644 index 00000000..5e646462 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/MeasureUnitController.java @@ -0,0 +1,111 @@ +package cn.code.nl.module.base.controller.admin.measureunit; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.code.nl.framework.common.pojo.PageParam; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.framework.common.util.object.BeanUtils; +import static cn.code.nl.framework.common.pojo.CommonResult.success; + +import cn.code.nl.framework.excel.core.util.ExcelUtils; + +import cn.code.nl.framework.apilog.core.annotation.ApiAccessLog; +import static cn.code.nl.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.code.nl.module.base.controller.admin.measureunit.vo.*; +import cn.code.nl.module.base.dal.dataobject.measureunit.MeasureUnitDO; +import cn.code.nl.module.base.service.measureunit.MeasureUnitService; + +@Tag(name = "管理后台 - 计量单位") +@RestController +@RequestMapping("/base/measure-unit") +@Validated +public class MeasureUnitController { + + @Resource + private MeasureUnitService measureUnitService; + + @PostMapping("/create") + @Operation(summary = "创建计量单位") + @PreAuthorize("@ss.hasPermission('base:measure-unit:create')") + public CommonResult createMeasureUnit(@Valid @RequestBody MeasureUnitSaveReqVO createReqVO) { + return success(measureUnitService.createMeasureUnit(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新计量单位") + @PreAuthorize("@ss.hasPermission('base:measure-unit:update')") + public CommonResult updateMeasureUnit(@Valid @RequestBody MeasureUnitSaveReqVO updateReqVO) { + measureUnitService.updateMeasureUnit(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除计量单位") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('base:measure-unit:delete')") + public CommonResult deleteMeasureUnit(@RequestParam("id") Long id) { + measureUnitService.deleteMeasureUnit(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除计量单位") + @PreAuthorize("@ss.hasPermission('base:measure-unit:delete')") + public CommonResult deleteMeasureUnitList(@RequestParam("ids") List ids) { + measureUnitService.deleteMeasureUnitListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得计量单位") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('base:measure-unit:query')") + public CommonResult getMeasureUnit(@RequestParam("id") Long id) { + MeasureUnitDO measureUnit = measureUnitService.getMeasureUnit(id); + return success(BeanUtils.toBean(measureUnit, MeasureUnitRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得计量单位分页") + @PreAuthorize("@ss.hasPermission('base:measure-unit:query')") + public CommonResult> getMeasureUnitPage(@Valid MeasureUnitPageReqVO pageReqVO) { + PageResult pageResult = measureUnitService.getMeasureUnitPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, MeasureUnitRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出计量单位 Excel") + @PreAuthorize("@ss.hasPermission('base:measure-unit:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportMeasureUnitExcel(@Valid MeasureUnitPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = measureUnitService.getMeasureUnitPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "计量单位.xls", "数据", MeasureUnitRespVO.class, + BeanUtils.toBean(list, MeasureUnitRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取计量单位精简列表", description = "只包含被开启的单位,用于前端的下拉选项") + public CommonResult> getSimpleMeasureUnitList() { + List list = measureUnitService.getMeasureUnitList(); + return success(BeanUtils.toBean(list, MeasureUnitSimpleRespVO.class)); + } + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitPageReqVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitPageReqVO.java new file mode 100644 index 00000000..3c09b5e7 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitPageReqVO.java @@ -0,0 +1,35 @@ +package cn.code.nl.module.base.controller.admin.measureunit.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.code.nl.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 计量单位分页 Request VO") +@Data +public class MeasureUnitPageReqVO extends PageParam { + + @Schema(description = "编码") + private String unitCode; + + @Schema(description = "名称", example = "芋艿") + private String unitName; + + @Schema(description = "数据精度") + private Integer qtyPrecision; + + @Schema(description = "是否启用") + private String isUsed; + + @Schema(description = "外部标识", example = "20158") + private String extId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitRespVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitRespVO.java new file mode 100644 index 00000000..a257b3e2 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitRespVO.java @@ -0,0 +1,43 @@ +package cn.code.nl.module.base.controller.admin.measureunit.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; + +@Schema(description = "管理后台 - 计量单位 Response VO") +@Data +@ExcelIgnoreUnannotated +public class MeasureUnitRespVO { + + @Schema(description = "计量单位标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("计量单位标识") + private Long measureUnitId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("编码") + private String unitCode; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("名称") + private String unitName; + + @Schema(description = "数据精度") + @ExcelProperty("数据精度") + private Integer qtyPrecision; + + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("是否启用") + private String isUsed; + + @Schema(description = "外部标识", example = "20158") + @ExcelProperty("外部标识") + private String extId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitSaveReqVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitSaveReqVO.java new file mode 100644 index 00000000..92ab5f53 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitSaveReqVO.java @@ -0,0 +1,33 @@ +package cn.code.nl.module.base.controller.admin.measureunit.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; + +@Schema(description = "管理后台 - 计量单位新增/修改 Request VO") +@Data +public class MeasureUnitSaveReqVO { + + @Schema(description = "计量单位标识", requiredMode = Schema.RequiredMode.REQUIRED) + private Long measureUnitId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "编码不能为空") + private String unitCode; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "名称不能为空") + private String unitName; + + @Schema(description = "数据精度") + private Integer qtyPrecision; + + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "是否启用不能为空") + private String isUsed; + + @Schema(description = "外部标识", example = "20158") + private String extId; + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitSimpleRespVO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitSimpleRespVO.java new file mode 100644 index 00000000..29b04c2b --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/admin/measureunit/vo/MeasureUnitSimpleRespVO.java @@ -0,0 +1,24 @@ +package cn.code.nl.module.base.controller.admin.measureunit.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 计量单位精简 Response VO(用于下拉框) + * + * @author zhouz + */ +@Schema(description = "管理后台 - 计量单位精简 Response VO") +@Data +public class MeasureUnitSimpleRespVO { + + @Schema(description = "计量单位标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long measureUnitId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String unitCode; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个") + private String unitName; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/api/CodeGenController.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/api/CodeGenController.java new file mode 100644 index 00000000..8cb27cb1 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/controller/api/CodeGenController.java @@ -0,0 +1,51 @@ +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.GetMapping; +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.RequestParam; +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); + } + + @GetMapping("/preview") + @Operation(summary = "预览下一个编码(不消耗序号)") + public CommonResult preview(@RequestParam("ruleCode") String ruleCode) { + String code = codeGenService.preview(ruleCode); + return success(code); + } + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeRuleDO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeRuleDO.java new file mode 100644 index 00000000..d367f472 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeRuleDO.java @@ -0,0 +1,77 @@ +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; + + /** + * 分隔符 + */ + @TableField("code_separator") + 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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeSequenceDO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeSequenceDO.java new file mode 100644 index 00000000..e9ef6c0d --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/codegen/CodeSequenceDO.java @@ -0,0 +1,45 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/measureunit/MeasureUnitDO.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/measureunit/MeasureUnitDO.java new file mode 100644 index 00000000..5d752697 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/dataobject/measureunit/MeasureUnitDO.java @@ -0,0 +1,52 @@ +package cn.code.nl.module.base.dal.dataobject.measureunit; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.code.nl.framework.mybatis.core.dataobject.BaseDO; + +/** + * 计量单位 DO + * + * @author 芋道源码 + */ +@TableName("base_measureunit") +@KeySequence("base_measureunit_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MeasureUnitDO extends BaseDO { + + /** + * 计量单位标识 + */ + @TableId + private Long measureUnitId; + /** + * 编码 + */ + private String unitCode; + /** + * 名称 + */ + private String unitName; + /** + * 数据精度 + */ + private Integer qtyPrecision; + /** + * 是否启用 + */ + private String isUsed; + /** + * 外部标识 + */ + private String extId; + + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeRuleMapper.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeRuleMapper.java new file mode 100644 index 00000000..0a0f0003 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeRuleMapper.java @@ -0,0 +1,43 @@ +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); + } + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeSequenceMapper.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeSequenceMapper.java new file mode 100644 index 00000000..9a8a82b3 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/codegen/CodeSequenceMapper.java @@ -0,0 +1,44 @@ +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); + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/measureunit/MeasureUnitMapper.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/measureunit/MeasureUnitMapper.java new file mode 100644 index 00000000..bf823b1f --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/dal/mysql/measureunit/MeasureUnitMapper.java @@ -0,0 +1,37 @@ +package cn.code.nl.module.base.dal.mysql.measureunit; + +import java.util.*; + +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; +import cn.code.nl.module.base.dal.dataobject.measureunit.MeasureUnitDO; +import org.apache.ibatis.annotations.Mapper; +import cn.code.nl.module.base.controller.admin.measureunit.vo.*; + +/** + * 计量单位 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MeasureUnitMapper extends BaseMapperX { + + default PageResult selectPage(MeasureUnitPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MeasureUnitDO::getUnitCode, reqVO.getUnitCode()) + .likeIfPresent(MeasureUnitDO::getUnitName, reqVO.getUnitName()) + .eqIfPresent(MeasureUnitDO::getQtyPrecision, reqVO.getQtyPrecision()) + .eqIfPresent(MeasureUnitDO::getIsUsed, reqVO.getIsUsed()) + .eqIfPresent(MeasureUnitDO::getExtId, reqVO.getExtId()) + .betweenIfPresent(MeasureUnitDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MeasureUnitDO::getMeasureUnitId)); + } + + default List selectList() { + return selectList(new LambdaQueryWrapperX() + .eq(MeasureUnitDO::getIsUsed, "1") + .orderByAsc(MeasureUnitDO::getUnitCode)); + } + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenService.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenService.java new file mode 100644 index 00000000..57fe3c1a --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenService.java @@ -0,0 +1,26 @@ +package cn.code.nl.module.base.service.codegen; + +/** + * 编码生成 Service 接口 + * + * @author zhouz + */ +public interface CodeGenService { + + /** + * 根据规则编码生成编码字符串 + * + * @param ruleCode 规则编码 + * @return 生成的编码 + */ + String generate(String ruleCode); + + /** + * 预览下一个编码(不消耗序号) + * + * @param ruleCode 规则编码 + * @return 预览的编码 + */ + String preview(String ruleCode); + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenServiceImpl.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenServiceImpl.java new file mode 100644 index 00000000..95be27d0 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeGenServiceImpl.java @@ -0,0 +1,149 @@ +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.SegmentConfig; +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); + } + + @Override + public String preview(String ruleCode) { + // 1. 获取规则配置 + CodeRuleDO rule = codeRuleService.getRuleByCode(ruleCode); + + // 2. 遍历分段,SEQUENCE 段用 preview(不消耗序号) + List segments = rule.getSegmentConfig(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.size(); i++) { + String segmentValue = buildPreviewSegment(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(segment); + case "DATE" -> buildDateSegment(segment); + case "SEQUENCE" -> buildSequenceSegment(rule, segment); + default -> throw new IllegalArgumentException("不支持的段类型: " + segment.getType()); + }; + } + + /** + * 预览模式分发(SEQUENCE 段不消耗序号) + */ + private String buildPreviewSegment(CodeRuleDO rule, SegmentConfig segment) { + return switch (segment.getType()) { + case "PREFIX" -> buildPrefixSegment(segment); + case "DATE" -> buildDateSegment(segment); + case "SEQUENCE" -> sequenceAllocator.preview(rule.getId(), segment); + default -> throw new IllegalArgumentException("不支持的段类型: " + segment.getType()); + }; + } + + /** + * 生成固定前缀段 + */ + private String buildPrefixSegment(SegmentConfig segment) { + return segment.getValue() != null ? segment.getValue() : ""; + } + + /** + * 生成日期段 + */ + private String buildDateSegment(SegmentConfig segment) { + String format = StrUtil.isNotBlank(segment.getFormat()) + ? segment.getFormat() : "yyyyMMdd"; + return LocalDate.now().format(DateTimeFormatter.ofPattern(format)); + } + + /** + * 生成流水序号段 + */ + private String buildSequenceSegment(CodeRuleDO rule, SegmentConfig 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; + } + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleService.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleService.java new file mode 100644 index 00000000..f178f5c9 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleService.java @@ -0,0 +1,71 @@ +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(); + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleServiceImpl.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleServiceImpl.java new file mode 100644 index 00000000..5232691e --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/CodeRuleServiceImpl.java @@ -0,0 +1,190 @@ +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); + + // 插入 + 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()); + + // 更新 + 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; + } + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java new file mode 100644 index 00000000..bed9ad55 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java @@ -0,0 +1,207 @@ +package cn.code.nl.module.base.service.codegen; + +import cn.code.nl.module.base.dal.dataobject.codegen.CodeSequenceDO; +import cn.code.nl.module.base.dal.mysql.codegen.CodeSequenceMapper; +import cn.code.nl.module.base.service.codegen.dto.SegmentConfig; +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.context.annotation.Lazy; +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; + + /** + * 自注入,用于 Spring AOP 代理调用带 @Lock4j 的方法 + */ + @Resource + @Lazy + private SequenceAllocator self; + + /** + * 分配一个序号(号段预分配策略) + * + * @param ruleId 规则ID + * @param segment 序号段配置 + * @return 格式化后的序号字符串 + */ + public String allocateBySegment(Long ruleId, SegmentConfig segment) { + String resetKey = buildResetKey(segment.getResetBy()); + String cacheKey = ruleId + ":" + resetKey; + + // 从本地缓存取号段 + AtomicLong current = localSegmentCache.get(cacheKey); + if (current != null) { + long seqValue = current.incrementAndGet(); + // 检查号段是否还有效 - 从 DB 查当前号段末尾 + CodeSequenceDO seqRecord = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey); + if (seqRecord != null && seqValue <= seqRecord.getCurrentValue()) { + return formatSequence(seqValue, segment); + } + // 号段耗尽,清理缓存 + localSegmentCache.remove(cacheKey); + } + + // 号段耗尽或不存在,通过代理调用带 @Lock4j 的方法从 DB 预取 + String lockKey = "codegen:seq:segment:" + ruleId + ":" + resetKey; + long seqValue = self.fetchSegmentWithLock(lockKey, cacheKey, ruleId, resetKey, segment); + return formatSequence(seqValue, segment); + } + + /** + * 分配一个序号(分布式锁实时策略,每次取一个号) + * + * @param ruleId 规则ID + * @param segment 序号段配置 + * @return 格式化后的序号字符串 + */ + public String allocateByLock(Long ruleId, SegmentConfig segment) { + String resetKey = buildResetKey(segment.getResetBy()); + String lockKey = "codegen:seq:lock:" + ruleId + ":" + resetKey; + return self.allocateWithLock(lockKey, ruleId, resetKey, segment); + } + + /** + * Lock4j 保护下从 DB 预取号段 + */ + @Lock4j(keys = "#lockKey", acquireTimeout = 3000, expire = 5000) + public long fetchSegmentWithLock(String lockKey, String cacheKey, Long ruleId, + String resetKey, SegmentConfig segment) { + // double-check:其他线程可能已经预取了 + AtomicLong existing = localSegmentCache.get(cacheKey); + if (existing != null) { + return existing.incrementAndGet(); + } + + Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L; + Long maxValue = segment.getMaxValue() != null ? segment.getMaxValue() : 9999L; + + // DB 原子更新:current_value = current_value + step + codeSequenceMapper.updateCurrentValueBySegment(ruleId, resetKey, DEFAULT_STEP, startAt, maxValue); + + // 查回最新 current_value + CodeSequenceDO seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey); + Long newCurrentValue = (seq != null) ? seq.getCurrentValue() : (startAt + DEFAULT_STEP - 1); + long segmentStart = newCurrentValue - DEFAULT_STEP + 1; + + // 检查上限 + if (newCurrentValue > maxValue) { + log.error("序号超出最大值上限,ruleId={}, resetKey={}, currentValue={}, maxValue={}", + ruleId, resetKey, newCurrentValue, maxValue); + 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(); + } + + /** + * Lock4j 保护下实时分配序号 + */ + @Lock4j(keys = "#lockKey", acquireTimeout = 3000, expire = 5000) + public String allocateWithLock(String lockKey, Long ruleId, String resetKey, SegmentConfig segment) { + Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L; + // step=1 每次只取一个号 + codeSequenceMapper.updateCurrentValueBySegment(ruleId, resetKey, 1, startAt, segment.getMaxValue()); + // 查回当前值 + CodeSequenceDO seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey); + Long value = (seq != null) ? seq.getCurrentValue() : startAt; + // 检查上限 + if (segment.getMaxValue() != null && value > segment.getMaxValue()) { + log.error("序号超出最大值上限,ruleId={}, resetKey={}, currentValue={}, maxValue={}", + ruleId, resetKey, value, segment.getMaxValue()); + throw exception(CODE_GEN_SEQ_EXCEED_MAX); + } + return formatSequence(value, segment); + } + + /** + * 根据重置维度构建 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, SegmentConfig 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; + } + + /** + * 预览下一个序号(不实际分配,不更新数据库) + * + * @param ruleId 规则ID + * @param segment 序号段配置 + * @return 格式化后的序号字符串 + */ + public String preview(Long ruleId, SegmentConfig segment) { + String resetKey = buildResetKey(segment.getResetBy()); + CodeSequenceDO seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey); + Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L; + long nextValue = (seq != null) ? seq.getCurrentValue() + 1 : startAt; + return formatSequence(nextValue, segment); + } + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/SegmentConfig.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/SegmentConfig.java new file mode 100644 index 00000000..75cf785c --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/dto/SegmentConfig.java @@ -0,0 +1,60 @@ +package cn.code.nl.module.base.service.codegen.dto; + +import lombok.Data; + +/** + * 分段配置 —— 通用 DTO,不做 Jackson 多态。 + * type 决定了哪些字段有效:PREFIX→value, DATE→format, SEQUENCE→resetBy/startAt/paddingLen/paddingChar/maxValue + * + * @author zhouz + */ +@Data +public class SegmentConfig { + + /** + * 段类型:PREFIX / DATE / SEQUENCE + */ + private String type; + + // ========== PREFIX 段 ========== + + /** + * 固定值,如 "ORD" + */ + private String value; + + // ========== DATE 段 ========== + + /** + * 日期格式,如 "yyyyMMdd" + */ + private String format; + + // ========== SEQUENCE 段 ========== + + /** + * 重置维度:DAY / MONTH / YEAR / GLOBAL + */ + private String resetBy; + + /** + * 起始值,默认 1 + */ + private Long startAt; + + /** + * 补位后的总长度,如 4 → 0001 + */ + private Integer paddingLen; + + /** + * 补位字符,默认 "0" + */ + private String paddingChar; + + /** + * 序号最大值上限 + */ + private Long maxValue; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/LetterCaseEnum.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/LetterCaseEnum.java new file mode 100644 index 00000000..f6d7119e --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/LetterCaseEnum.java @@ -0,0 +1,28 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/ResetByEnum.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/ResetByEnum.java new file mode 100644 index 00000000..736746be --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/ResetByEnum.java @@ -0,0 +1,29 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SegmentTypeEnum.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SegmentTypeEnum.java new file mode 100644 index 00000000..c9f1b588 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SegmentTypeEnum.java @@ -0,0 +1,28 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SeqStrategyEnum.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SeqStrategyEnum.java new file mode 100644 index 00000000..513279c6 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/enums/SeqStrategyEnum.java @@ -0,0 +1,27 @@ +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; + +} diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/measureunit/MeasureUnitService.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/measureunit/MeasureUnitService.java new file mode 100644 index 00000000..d7efb134 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/measureunit/MeasureUnitService.java @@ -0,0 +1,69 @@ +package cn.code.nl.module.base.service.measureunit; + +import java.util.*; +import jakarta.validation.*; +import cn.code.nl.module.base.controller.admin.measureunit.vo.*; +import cn.code.nl.module.base.dal.dataobject.measureunit.MeasureUnitDO; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.PageParam; + +/** + * 计量单位 Service 接口 + * + * @author 芋道源码 + */ +public interface MeasureUnitService { + + /** + * 创建计量单位 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createMeasureUnit(@Valid MeasureUnitSaveReqVO createReqVO); + + /** + * 更新计量单位 + * + * @param updateReqVO 更新信息 + */ + void updateMeasureUnit(@Valid MeasureUnitSaveReqVO updateReqVO); + + /** + * 删除计量单位 + * + * @param id 编号 + */ + void deleteMeasureUnit(Long id); + + /** + * 批量删除计量单位 + * + * @param ids 编号 + */ + void deleteMeasureUnitListByIds(List ids); + + /** + * 获得计量单位 + * + * @param id 编号 + * @return 计量单位 + */ + MeasureUnitDO getMeasureUnit(Long id); + + /** + * 获得计量单位分页 + * + * @param pageReqVO 分页查询 + * @return 计量单位分页 + */ + PageResult getMeasureUnitPage(MeasureUnitPageReqVO pageReqVO); + + /** + * 获得计量单位列表(精简,仅启用的) + * + * @return 计量单位列表 + */ + List getMeasureUnitList(); + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/measureunit/MeasureUnitServiceImpl.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/measureunit/MeasureUnitServiceImpl.java new file mode 100644 index 00000000..23a580af --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/measureunit/MeasureUnitServiceImpl.java @@ -0,0 +1,90 @@ +package cn.code.nl.module.base.service.measureunit; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.code.nl.module.base.controller.admin.measureunit.vo.*; +import cn.code.nl.module.base.dal.dataobject.measureunit.MeasureUnitDO; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.PageParam; +import cn.code.nl.framework.common.util.object.BeanUtils; + +import cn.code.nl.module.base.dal.mysql.measureunit.MeasureUnitMapper; + +import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.code.nl.framework.common.util.collection.CollectionUtils.convertList; +import static cn.code.nl.framework.common.util.collection.CollectionUtils.diffList; +import static cn.code.nl.module.base.enums.ErrorCodeConstants.*; + +/** + * 计量单位 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MeasureUnitServiceImpl implements MeasureUnitService { + + @Resource + private MeasureUnitMapper measureUnitMapper; + + @Override + public Long createMeasureUnit(MeasureUnitSaveReqVO createReqVO) { + // 插入 + MeasureUnitDO measureUnit = BeanUtils.toBean(createReqVO, MeasureUnitDO.class); + measureUnitMapper.insert(measureUnit); + + // 返回 + return measureUnit.getMeasureUnitId(); + } + + @Override + public void updateMeasureUnit(MeasureUnitSaveReqVO updateReqVO) { + // 校验存在 + validateMeasureUnitExists(updateReqVO.getMeasureUnitId()); + // 更新 + MeasureUnitDO updateObj = BeanUtils.toBean(updateReqVO, MeasureUnitDO.class); + measureUnitMapper.updateById(updateObj); + } + + @Override + public void deleteMeasureUnit(Long id) { + // 校验存在 + validateMeasureUnitExists(id); + // 删除 + measureUnitMapper.deleteById(id); + } + + @Override + public void deleteMeasureUnitListByIds(List ids) { + // 删除 + measureUnitMapper.deleteByIds(ids); + } + + + private void validateMeasureUnitExists(Long id) { + if (measureUnitMapper.selectById(id) == null) { + throw exception(MEASURE_UNIT_NOT_EXISTS); + } + } + + @Override + public MeasureUnitDO getMeasureUnit(Long id) { + return measureUnitMapper.selectById(id); + } + + @Override + public PageResult getMeasureUnitPage(MeasureUnitPageReqVO pageReqVO) { + return measureUnitMapper.selectPage(pageReqVO); + } + + @Override + public List getMeasureUnitList() { + return measureUnitMapper.selectList(); + } + +} \ No newline at end of file diff --git a/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml b/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml new file mode 100644 index 00000000..74909a0f --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml @@ -0,0 +1,20 @@ + + + + + + + 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} + + + diff --git a/nl-module-base/nl-module-base-server/src/main/resources/mapper/measureunit/MeasureUnitMapper.xml b/nl-module-base/nl-module-base-server/src/main/resources/mapper/measureunit/MeasureUnitMapper.xml new file mode 100644 index 00000000..8c25fd7a --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/resources/mapper/measureunit/MeasureUnitMapper.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml b/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml index 831d09c5..d8b15523 100644 --- a/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml +++ b/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml @@ -134,4 +134,4 @@ spring: # 芋道配置项,设置当前项目所有自定义的配置 nl: - demo: true # 开启演示模式 + demo: false # 开启演示模式 diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/codegen/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/codegen/index.ts new file mode 100644 index 00000000..5129d55a --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/codegen/index.ts @@ -0,0 +1,89 @@ +import { requestClient } from '#/api/request'; + +export namespace BaseCodeRuleApi { + /** 分段配置 */ + export interface SegmentConfig { + type: 'PREFIX' | 'DATE' | 'SEQUENCE'; + /** PREFIX 段:固定值 */ + value?: string; + /** DATE 段:日期格式 */ + format?: string; + /** SEQUENCE 段:重置维度 */ + resetBy?: 'DAY' | 'MONTH' | 'YEAR' | 'GLOBAL'; + /** SEQUENCE 段:起始值 */ + startAt?: number; + /** SEQUENCE 段:补位长度 */ + paddingLen?: number; + /** SEQUENCE 段:补位字符 */ + paddingChar?: string; + /** SEQUENCE 段:最大值上限 */ + maxValue?: number; + } + + /** 编码规则 */ + export interface CodeRule { + id?: number; + ruleName: string; + ruleCode: string; + segments: SegmentConfig[]; + separator?: string; + letterCase?: number; + totalLength?: number; + seqStrategy?: string; + status?: number; + remark?: string; + createTime?: string; + } +} + +/** 查询编码规则分页 */ +export async function getCodeRulePage(params: { + pageNo: number; + pageSize: number; + ruleName?: string; + ruleCode?: string; + status?: number; + createTime?: string[]; +}) { + return requestClient.get<{ + list: BaseCodeRuleApi.CodeRule[]; + total: number; + }>('/base/code-rule/page', { params }); +} + +/** 查询编码规则详情 */ +export async function getCodeRule(id: number) { + return requestClient.get( + `/base/code-rule/get?id=${id}`, + ); +} + +/** 查询编码规则精简列表(下拉选项) */ +export async function getSimpleCodeRuleList() { + return requestClient.get( + '/base/code-rule/simple-list', + ); +} + +/** 新增编码规则 */ +export async function createCodeRule(data: BaseCodeRuleApi.CodeRule) { + return requestClient.post('/base/code-rule/create', data); +} + +/** 修改编码规则 */ +export async function updateCodeRule(data: BaseCodeRuleApi.CodeRule) { + return requestClient.put('/base/code-rule/update', data); +} + +/** 删除编码规则 */ +export async function deleteCodeRule(id: number) { + return requestClient.delete(`/base/code-rule/delete?id=${id}`); +} + +/** 预览下一个编码(不消耗序号) */ +export async function previewCode(ruleCode: string) { + return requestClient.get( + '/base/code-rule/preview', + { params: { ruleCode } }, + ); +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/measureunit/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/measureunit/index.ts new file mode 100644 index 00000000..3f398f9a --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/measureunit/index.ts @@ -0,0 +1,64 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace BaseMeasureUnitApi { + /** 计量单位信息 */ + export interface MeasureUnit { + measureUnitId?: number; + unitCode?: string; // 编码 + unitName?: string; // 名称 + qtyPrecision: number; // 数据精度 + isUsed?: string; // 是否启用 + extId: string; // 外部标识 + } +} + +/** 查询计量单位精简列表(仅启用的,用于下拉框) */ +export async function getSimpleMeasureUnitList() { + return requestClient.get( + '/base/measure-unit/simple-list', + ); +} + +/** 查询计量单位分页 */ +export function getMeasureUnitPage(params: PageParam) { + return requestClient.get>( + '/base/measure-unit/page', + { params }, + ); +} + +/** 查询计量单位详情 */ +export function getMeasureUnit(id: number) { + return requestClient.get( + `/base/measure-unit/get?id=${id}`, + ); +} + +/** 新增计量单位 */ +export function createMeasureUnit(data: BaseMeasureUnitApi.MeasureUnit) { + return requestClient.post('/base/measure-unit/create', data); +} + +/** 修改计量单位 */ +export function updateMeasureUnit(data: BaseMeasureUnitApi.MeasureUnit) { + return requestClient.put('/base/measure-unit/update', data); +} + +/** 删除计量单位 */ +export function deleteMeasureUnit(id: number) { + return requestClient.delete(`/base/measure-unit/delete?id=${id}`); +} + +/** 批量删除计量单位 */ +export function deleteMeasureUnitList(ids: number[]) { + return requestClient.delete( + `/base/measure-unit/delete-list?ids=${ids.join(',')}`, + ); +} + +/** 导出计量单位 */ +export function exportMeasureUnit(params: any) { + return requestClient.download('/base/measure-unit/export-excel', { params }); +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/classstandard/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/classstandard/data.ts index 25099420..07867458 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/classstandard/data.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/classstandard/data.ts @@ -28,10 +28,7 @@ export function useFormSchema(): VbenFormSchema[] { allowClear: true, api: async () => { const data = await getClassStandardList(); - data.unshift({ - classId: 0, - className: '顶级分类', - }); + data.unshift({sort: 0, status: 0, classId: 0, className: '顶级分类'}); return handleTree(data, 'classId', 'parentClassId'); }, labelField: 'className', diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/components/SegmentEditor.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/components/SegmentEditor.vue new file mode 100644 index 00000000..71eded9b --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/components/SegmentEditor.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/data.ts new file mode 100644 index 00000000..3dfd9382 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/data.ts @@ -0,0 +1,228 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { BaseCodeRuleApi } from '#/api/base/codegen'; + +import { getRangePickerDefaultProps } from '#/utils'; + +/** 字母大小写选项 */ +const LETTER_CASE_OPTIONS = [ + { label: '原样', value: 0 }, + { label: '全大写', value: 1 }, + { label: '全小写', value: 2 }, +]; + +/** 序号策略选项 */ +const SEQ_STRATEGY_OPTIONS = [ + { label: '号段预分配', value: 'segment' }, + { label: '分布式锁实时', value: 'lock' }, +]; + +/** 状态选项 */ +const STATUS_OPTIONS = [ + { label: '启用', value: 0 }, + { label: '禁用', value: 1 }, +]; + +/** 新增/修改的表单(不含 segments,segments 使用自定义组件) */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'ruleName', + label: '规则名称', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入规则名称', + }, + }, + { + fieldName: 'ruleCode', + label: '规则编码', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入规则编码(唯一标识)', + }, + }, + { + fieldName: 'separator', + label: '分隔符', + component: 'Input', + componentProps: { + placeholder: '如 -、/、_,留空则不使用分隔符', + }, + }, + { + fieldName: 'letterCase', + label: '字母大小写', + component: 'RadioGroup', + componentProps: { + options: LETTER_CASE_OPTIONS, + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + fieldName: 'totalLength', + label: '总长度约束', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 0, + placeholder: '0 表示不限制', + }, + }, + { + fieldName: 'seqStrategy', + label: '序号策略', + component: 'RadioGroup', + componentProps: { + options: SEQ_STRATEGY_OPTIONS, + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'RadioGroup', + componentProps: { + options: STATUS_OPTIONS, + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Input', + componentProps: { + type: 'textarea', + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'ruleName', + label: '规则名称', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入规则名称', + }, + }, + { + fieldName: 'ruleCode', + label: '规则编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入规则编码', + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + allowClear: true, + placeholder: '请选择', + options: STATUS_OPTIONS, + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'checkbox', width: 40 }, + { + field: 'ruleName', + title: '规则名称', + minWidth: 160, + }, + { + field: 'ruleCode', + title: '规则编码', + minWidth: 160, + }, + { + field: 'segmentConfig', + title: '分段配置', + minWidth: 240, + formatter: ({ cellValue }: { cellValue: BaseCodeRuleApi.SegmentConfig[] }) => { + if (!cellValue || cellValue.length === 0) return '-'; + return cellValue + .map((s) => { + switch (s.type) { + case 'PREFIX': + return `前缀:${s.value || ''}`; + case 'DATE': + return `日期(${s.format || 'yyyyMMdd'})`; + case 'SEQUENCE': + return `序号(${s.resetBy || 'DAY'}/${s.paddingLen || 4}位)`; + default: + return s.type; + } + }) + .join(' + '); + }, + }, + { + field: 'separator', + title: '分隔符', + minWidth: 80, + formatter: ({ cellValue }: { cellValue: string }) => cellValue || '-', + }, + { + field: 'seqStrategy', + title: '序号策略', + minWidth: 120, + formatter: ({ cellValue }: { cellValue: string }) => + cellValue === 'lock' ? '分布式锁实时' : '号段预分配', + }, + { + field: 'status', + title: '状态', + minWidth: 80, + formatter: ({ cellValue }: { cellValue: number }) => + cellValue === 0 ? '启用' : '禁用', + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/index.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/index.vue new file mode 100644 index 00000000..3bf6899e --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/index.vue @@ -0,0 +1,195 @@ + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/modules/form.vue new file mode 100644 index 00000000..ac5d92bd --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/codeGen/modules/form.vue @@ -0,0 +1,113 @@ + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/data.ts index 3868d9f2..0937d1bd 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/data.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/data.ts @@ -1,371 +1,147 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { BaseMaterialBaseApi } from '#/api/base/materialbase'; -import type { BaseClassStandardApi } from '#/api/base/classstandard'; import { handleTree } from '@vben/utils'; -import { z } from '#/adapter/form'; import { getClassStandardList, getClassStandardListByCode } from '#/api/base/classstandard'; +import { getSimpleMeasureUnitList } from '#/api/base/measureunit'; import { getRangePickerDefaultProps } from '#/utils'; -/** 分类名称映射表:classId → className */ -let categoryNameMap: Record = {}; -getClassStandardList().then((data) => { - if (data) { - data.forEach((item) => { - if (item.classId) { - categoryNameMap[item.classId] = item.className; - } - }); - } -}); +// ========== 分类映射:classId → className ========== +const classNameMap: Record = {}; +const unitCodeMap: Record = {}; +const unitNameMap: Record = {}; -/** 获取物料分类的树形下拉数据 */ +/** 预加载所有映射数据,确保表格渲染前映射表已填充 */ +export async function loadAllLookupData() { + const [classData, unitData] = await Promise.all([ + getClassStandardList(), + getSimpleMeasureUnitList(), + ]); + classData?.forEach((item) => { + if (item.classId) classNameMap[item.classId] = item.className; + }); + unitData?.forEach((item) => { + if (item.measureUnitId) { + unitCodeMap[item.measureUnitId] = item.unitCode; + unitNameMap[item.measureUnitId] = item.unitName; + } + }); +} + +// ========== 物料分类树形下拉 ========== async function loadMaterialTypeTree() { const data = await getClassStandardListByCode('0001'); - if (!data || data.length === 0) { - return []; - } + if (!data || data.length === 0) return []; return handleTree(data, 'classId', 'parentClassId'); } -/** 新增/修改的表单 */ +// ========== 计量单位下拉 ========== +async function loadMeasureUnitList() { + const data = await getSimpleMeasureUnitList(); + return data || []; +} + +// ========== 新增/修改的表单 ========== export function useFormSchema(): VbenFormSchema[] { return [ + { fieldName: 'materialId', component: 'Input', dependencies: { triggerFields: [''], show: () => false } }, + { fieldName: 'materialCode', label: '物料编码', rules: 'required', component: 'Input', componentProps: { placeholder: '请输入物料编码' } }, + { fieldName: 'materialName', label: '物料名称', rules: 'required', component: 'Input', componentProps: { placeholder: '请输入物料名称' } }, + { fieldName: 'materialSpec', label: '规格', component: 'Input', componentProps: { placeholder: '请输入规格' } }, + { fieldName: 'materialModel', label: '型号', component: 'Input', componentProps: { placeholder: '请输入型号' } }, + { fieldName: 'englishName', label: '外文名称', component: 'Input', componentProps: { placeholder: '请输入外文名称' } }, { - fieldName: 'materialId', - component: 'Input', - dependencies: { - triggerFields: [''], - show: () => false, - }, - }, - { - fieldName: 'materialCode', - label: '物料编码', - rules: 'required', - component: 'Input', - componentProps: { - placeholder: '请输入物料编码', - }, - }, - { - fieldName: 'materialName', - label: '物料名称', - rules: 'required', - component: 'Input', - componentProps: { - placeholder: '请输入物料名称', - }, - }, - { - fieldName: 'materialSpec', - label: '规格', - component: 'Input', - componentProps: { - placeholder: '请输入规格', - }, - }, - { - fieldName: 'materialModel', - label: '型号', - component: 'Input', - componentProps: { - placeholder: '请输入型号', - }, - }, - { - fieldName: 'englishName', - label: '外文名称', - component: 'Input', - componentProps: { - placeholder: '请输入外文名称', - }, - }, - { - fieldName: 'materialTypeId', - label: '物料分类', + fieldName: 'materialTypeId', label: '物料分类', component: 'ApiTreeSelect', - componentProps: { - api: loadMaterialTypeTree, - labelField: 'className', - valueField: 'classId', - childrenField: 'children', - placeholder: '请选择物料分类', - allowClear: true, - treeDefaultExpandAll: true, - }, + componentProps: { api: loadMaterialTypeTree, labelField: 'className', valueField: 'classId', childrenField: 'children', placeholder: '请选择物料分类', allowClear: true, treeDefaultExpandAll: true }, }, { - fieldName: 'baseUnitId', - label: '基本计量单位', - rules: 'required', - component: 'Input', - componentProps: { - placeholder: '请输入基本计量单位', - }, + fieldName: 'baseUnitId', label: '基本计量单位', rules: 'required', + component: 'ApiSelect', + componentProps: { api: loadMeasureUnitList, labelField: 'unitCode', valueField: 'measureUnitId', placeholder: '请选择基本计量单位', allowClear: true }, }, { - fieldName: 'assUnitId', - label: '辅助计量单位', - component: 'Input', - componentProps: { - placeholder: '请输入辅助计量单位', - }, + fieldName: 'assUnitId', label: '辅助计量单位', + component: 'ApiSelect', + componentProps: { api: loadMeasureUnitList, labelField: 'unitCode', valueField: 'measureUnitId', placeholder: '请选择辅助计量单位', allowClear: true }, }, { - fieldName: 'lenUnitId', - label: '长度单位', - component: 'Input', - componentProps: { - placeholder: '请输入长度单位', - }, + fieldName: 'lenUnitId', label: '长度单位', + component: 'ApiSelect', + componentProps: { api: loadMeasureUnitList, labelField: 'unitCode', valueField: 'measureUnitId', placeholder: '请选择长度单位', allowClear: true }, }, { - fieldName: 'weightUnitId', - label: '重量单位', - component: 'Input', - componentProps: { - placeholder: '请输入重量单位', - }, + fieldName: 'weightUnitId', label: '重量单位', + component: 'ApiSelect', + componentProps: { api: loadMeasureUnitList, labelField: 'unitCode', valueField: 'measureUnitId', placeholder: '请选择重量单位', allowClear: true }, }, { - fieldName: 'isUsed', - label: '是否启用', - rules: 'required', + fieldName: 'cubageUnitId', label: '体积单位', + component: 'ApiSelect', + componentProps: { api: loadMeasureUnitList, labelField: 'unitCode', valueField: 'measureUnitId', placeholder: '请选择体积单位', allowClear: true }, + }, + { + fieldName: 'isUsed', label: '是否启用', rules: 'required', component: 'RadioGroup', - componentProps: { - options: [ - { label: '是', value: '1' }, - { label: '否', value: '0' }, - ], - buttonStyle: 'solid', - optionType: 'button', - }, - }, - { - fieldName: 'extId', - label: '外部标识', - component: 'Input', - componentProps: { - placeholder: '请输入外部标识', - }, + componentProps: { options: [{ label: '是', value: '1' }, { label: '否', value: '0' }], buttonStyle: 'solid', optionType: 'button' }, }, + { fieldName: 'extId', label: '外部标识', component: 'Input', componentProps: { placeholder: '请输入外部标识' } }, ]; } -/** 列表的搜索表单 */ +// ========== 列表的搜索表单 ========== export function useGridFormSchema(): VbenFormSchema[] { return [ + { fieldName: 'materialCode', label: '物料编码', component: 'Input', componentProps: { allowClear: true, placeholder: '请输入物料编码' } }, + { fieldName: 'materialName', label: '物料名称', component: 'Input', componentProps: { allowClear: true, placeholder: '请输入物料名称' } }, { - fieldName: 'materialCode', - label: '物料编码', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入物料编码', - }, - }, - { - fieldName: 'materialName', - label: '物料名称', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入物料名称', - }, - }, - { - fieldName: 'materialSpec', - label: '规格', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入规格', - }, - }, - { - fieldName: 'materialModel', - label: '型号', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入型号', - }, - }, - { - fieldName: 'englishName', - label: '外文名称', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入外文名称', - }, - }, - { - fieldName: 'materialTypeId', - label: '物料分类', + fieldName: 'materialTypeId', label: '物料分类', component: 'ApiTreeSelect', - componentProps: { - api: loadMaterialTypeTree, - labelField: 'className', - valueField: 'classId', - childrenField: 'children', - placeholder: '请选择物料分类', - allowClear: true, - treeDefaultExpandAll: true, - }, - }, - { - fieldName: 'baseUnitId', - label: '基本计量单位', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入基本计量单位', - }, - }, - { - fieldName: 'assUnitId', - label: '辅助计量单位', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入辅助计量单位', - }, - }, - { - fieldName: 'lenUnitId', - label: '长度单位', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入长度单位', - }, - }, - { - fieldName: 'weightUnitId', - label: '重量单位', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入重量单位', - }, - }, - { - fieldName: 'createTime', - label: '创建时间', - component: 'RangePicker', - componentProps: { - ...getRangePickerDefaultProps(), - allowClear: true, - }, - }, - { - fieldName: 'isUsed', - label: '是否启用', - component: 'Select', - componentProps: { - allowClear: true, - placeholder: '请选择', - options: [ - { label: '是', value: '1' }, - { label: '否', value: '0' }, - ], - }, - }, - { - fieldName: 'extId', - label: '外部标识', - component: 'Input', - componentProps: { - allowClear: true, - placeholder: '请输入外部标识', - }, + componentProps: { api: loadMaterialTypeTree, labelField: 'className', valueField: 'classId', childrenField: 'children', placeholder: '请选择物料分类', allowClear: true }, }, + { fieldName: 'createTime', label: '创建时间', component: 'RangePicker', componentProps: { ...getRangePickerDefaultProps(), allowClear: true } }, + { fieldName: 'isUsed', label: '是否启用', component: 'Select', componentProps: { allowClear: true, placeholder: '请选择', options: [{ label: '是', value: '1' }, { label: '否', value: '0' }] } }, ]; } -/** 列表的字段 */ +// ========== 列表的字段 ========== export function useGridColumns(): VxeTableGridOptions['columns'] { return [ { type: 'checkbox', width: 40 }, + { field: 'materialCode', title: '物料编码', minWidth: 120 }, + { field: 'materialName', title: '物料名称', minWidth: 200 }, + { field: 'materialSpec', title: '规格', minWidth: 120 }, + { field: 'materialModel', title: '型号', minWidth: 120 }, + { field: 'englishName', title: '外文名称', minWidth: 120 }, { - field: 'materialCode', - title: '物料编码', - minWidth: 120, + field: 'materialTypeId', title: '物料分类', minWidth: 150, + formatter: ({ cellValue }: { cellValue: number }) => classNameMap[cellValue] || cellValue || '-', }, { - field: 'materialName', - title: '物料名称', - minWidth: 200, + field: 'baseUnitId', title: '基本计量单位', minWidth: 120, + formatter: ({ cellValue }: { cellValue: number }) => unitCodeMap[cellValue] || cellValue || '-', }, { - field: 'materialSpec', - title: '规格', - minWidth: 120, + field: 'assUnitId', title: '辅助计量单位', minWidth: 120, + formatter: ({ cellValue }: { cellValue: number }) => unitCodeMap[cellValue] || cellValue || '-', }, { - field: 'materialModel', - title: '型号', - minWidth: 120, + field: 'lenUnitId', title: '长度单位', minWidth: 120, + formatter: ({ cellValue }: { cellValue: number }) => unitCodeMap[cellValue] || cellValue || '-', }, { - field: 'englishName', - title: '外文名称', - minWidth: 120, + field: 'weightUnitId', title: '重量单位', minWidth: 120, + formatter: ({ cellValue }: { cellValue: number }) => unitCodeMap[cellValue] || cellValue || '-', }, { - field: 'baseUnitId', - title: '基本计量单位', - minWidth: 120, - }, - { - field: 'assUnitId', - title: '辅助计量单位', - minWidth: 120, - }, - { - field: 'materialTypeId', - title: '物料分类', - minWidth: 150, - formatter: ({ cellValue }: { cellValue: number }) => - categoryNameMap[cellValue] || cellValue || '-', - }, - { - field: 'lenUnitId', - title: '长度单位', - minWidth: 120, - }, - { - field: 'weightUnitId', - title: '重量单位', - minWidth: 120, - }, - { - field: 'createTime', - title: '创建时间', - minWidth: 180, - formatter: 'formatDateTime', - }, - { - field: 'isUsed', - title: '是否启用', - minWidth: 100, - formatter: ({ cellValue }: { cellValue: string }) => - cellValue === '1' ? '是' : '否', - }, - { - field: 'extId', - title: '外部标识', - minWidth: 120, - }, - { - title: '操作', - width: 200, - fixed: 'right', - slots: { default: 'actions' }, + field: 'cubageUnitId', title: '体积单位', minWidth: 120, + formatter: ({ cellValue }: { cellValue: number }) => unitCodeMap[cellValue] || cellValue || '-', }, + { field: 'createTime', title: '创建时间', minWidth: 180, formatter: 'formatDateTime' }, + { field: 'isUsed', title: '是否启用', minWidth: 100, formatter: ({ cellValue }: { cellValue: string }) => (cellValue === '1' ? '是' : '否') }, + { field: 'extId', title: '外部标识', minWidth: 120 }, + { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } }, ]; } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/index.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/index.vue index d66cb71d..a44b5bfe 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/index.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/base/materialbase/index.vue @@ -2,7 +2,7 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { BaseMaterialBaseApi } from '#/api/base/materialbase'; -import { ref } from 'vue'; +import { onMounted, ref } from 'vue'; import { confirm, Page, useVbenModal } from '@vben/common-ui'; import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; @@ -18,9 +18,15 @@ import { } from '#/api/base/materialbase'; import { $t } from '#/locales'; -import { useGridColumns, useGridFormSchema } from './data'; +import { loadAllLookupData, useGridColumns, useGridFormSchema } from './data'; import Form from './modules/form.vue'; +const ready = ref(false); +onMounted(async () => { + await loadAllLookupData(); + ready.value = true; +}); + const [FormModal, formModalApi] = useVbenModal({ connectedComponent: Form, destroyOnClose: true, @@ -125,7 +131,7 @@ const [Grid, gridApi] = useVbenVxeGrid({