主数据管理平台 主数据管理平台
产品介绍
产品安装指南
快速入门手册
用户操作手册
接口文档
  • 字段编码——码段类型扩展SPI
  • 有状态编码器扩展办法
  • 定义编码器配置对象
  • 定义状态对象(可选)
  • 编写码段编码器实现
  • 国际化配置(可选)
  • 无状态编码器扩展办法
  • 定义编码器配置对象
  • 编写码段编码器实现
  • 国际化配置(可选)

# 字段编码——码段类型扩展SPI

码段分为有状态编码器和无状态编码器,比如序号(流水码)就是有状态编码器 -- 状态对象:计数器counter。

  • 有状态编码器SPI: com.primeton.mdm.management.spi.code.StatelessEncoder
  • 无状态编码器SPI: com.primeton.mdm.management.spi.code.StatefulEncoder

# 有状态编码器扩展办法

# 定义编码器配置对象

即在WEB端数据模型配置向导——字段编码配置向导页中,定义给字段配置码段编码规则时能够配置的那些参数。(由于WEB端尚未完全支持扩展码段编码器扩展动态表单,目前WEB端对于扩展的码段编码配置都是以一个JSON编辑器来展现的)

@Getter
@Setter
public class SequenceNumber extends Config {

    public static final String TYPE = "SEQUENCE";

    private long start; // 起始值
    private int step; // 步长(每次增长值)
    private int length; // 码段长度限制
    /**
     * 其他字段值(一般情况下是枚举值不多,所选字段也不多1-2个)每组引用值独享一个计数器(隔离)。
     */
    private String[] refFields;
    /**
     * 输出结果转成其他进制(一般是往大的转,16/32/36)高于十进制有助于缩短码段长度,减少存储空间和便于用户记忆
     */
    private int radix = 10;

    @Override
    public final String getType() {
        return TYPE;
    }

}

# 定义状态对象(可选)

  基类状态对象`State`只有一个序列号数值(计数器),如果不满足则编写子类继承它并添加需要存储的状态信息字段(存储采用JSON序列化状态信息)。

# 编写码段编码器实现

  对于有状态字段编码器来说,分为单一状态对象(单一计数器)和多状态对象(多个计数器);后者需要实现`#key(...)`方法 —— 根码段配置/业务数据/系统时间等信息生成一个键值(`identity`|用于映射一个状态对象实例)
@Component("MDMSequenceNumberEncoder")
public class SequenceNumberEncoder implements StatefulEncoder<SequenceNumber, State>, Formatter<SequenceNumber> {

    @Override
    public final String name() {
        return SequenceNumber.TYPE;
    }

    @Override
    public Class<SequenceNumber> type() {
        return SequenceNumber.class;
    }

    @Override
    public Supplier<State> state() {
        return State::new;
    }

    @Override
    public String generate(String model, SequenceNumber config, State state, Map<String, Object> data, StateManager stateManager) {
        //  最大长度 19 应该足够用了吧
        final int length = (config.getLength() > 0 && config.getLength() < 20) ? config.getLength() : (config.getLength() > 19 ? 19 : 5);
        long value = (state.getValue() > 0 ? state.getValue() : config.getStart())
                + (config.getStep() > 0 ? config.getStep() : 1);
        StringBuilder sb = format(config, value, length);
        state.setValue(value); // save-state
        if (null != stateManager) {
            stateManager.save(state);
        }
        return format(config, sb.toString());
    }

    private static StringBuilder format(SequenceNumber config, long value, int length) {
        StringBuilder sb = new StringBuilder();
        if (config.getRadix() >= 2 && config.getRadix() <= 36 && config.getRadix() != 10) {
            sb.append(Long.toString(value, config.getRadix()));
        } else {
            sb.append(value);
        }
        if (sb.length() > length) {
            throw new RuntimeException("Generated code snippet exceeding the length limit. " + value + " (limit length: " + length + ")");
        }
        while (sb.length() < length) {
            sb.insert(0, 0);
        }
        return sb;
    }

    /**
     * 1) 单状态模式:编码器无需实现这个方法;<br>
     * 2) 多状态模式:编码器必须实现这个方法;可以根据系统当前时间和目标数据生成一个key值作为计数器名称方便映射一个状态对象的存取操作<br>
     *
     * <br>
     * 有些编码常见需要多组状态值来记录序列。
     * e.g. 餐厅每条下单流水号(日期流水码)需要每天从头重新编号<blockquote><pre>{DAY}{SEQ}, key=>{DAY}</pre></blockquote>
     * e.g. 每个班级的学号独立流水编号<blockquote><pre>{届}{专业代号}{流水号}, key=>{届}{专业代号}</pre></blockquote>
     * 如果你所扩展的有状态编码器存在多组状态值(对于同一个模型的某字段编码的某个码段,不同码段直接完全隔离)你需要根据某条数据以及系统时间等参数生成一个key,
     * 不同行数据多次调用生成了相同的key则意味着他们使用了同一个状态对象(如流水码计数器),例如日期流水码按每天(或每月)到期自动重置(重新从头计数,
     * 则同一天需要生成码段的时候它们使用了同一个状态对象)。
     * <br>
     * 使用生成的KEY关联(映射)一个状态对象(计数器)。
     *
     * <blockquote><pre>
     * // e.g. 学生模型: 学号|届|专业代号|班级代号|身份证号|姓名|性别|出生日期|家庭住址|电话|邮箱|备注
     * // 学号编码规则: {届}{专业代号}{流水号}
     * // 码段1:{届} 选择日期码(系统时间|格式化年份)或引用码(`届`)
     * // 码段2:{专业代号} 选择引用码(`专业代号` -- 配置关联字典)
     * // 码段3:{流水号} 流水码(有状态码段)-- 计算key所需关联字段选择`届`和`专业代号` e.g. key1=2501 => 2025届数学专业 key2=2502=>2025届化学专业
     * // 编码示例:
     * // [key=2501] => 2025届|数学专业
     * // sno=250101 => 01|张无忌
     * // sno=250102 => 02|宋青书
     * // ...
     * // [key=2502] => 2025届|化学专业
     * // sno=250201 => 01|周芷若
     * // sno=250202 => 02|赵敏敏
     * // ...
     * </pre></blockquote>
     *
     * @param config 码段配置
     * @param data 目标数据(部分码段类型依赖数据部分内容)
     * @return 生成一个关键字(一般是用于多组状态值情况下)相关数据拼接一个字符串作为关键字,如日期流水码则关键字为日期值。这个值用于存储和读取码段状态以支持第2+次生成
     */
    @Override
    public String key(SequenceNumber config, Map<String, Object> data) {
        if (ArrayUtils.isEmpty(config.getRefFields())) {
            return null;
        }
        String snippets = Arrays.stream(config.getRefFields()).sorted().map(data::get).//filter(Objects::nonNull).
                map(Objects::toString).collect(Collectors.joining("-"));
        return StringUtils.isNotEmpty(snippets) ? snippets : null;
    }

}

# 国际化配置(可选)

/META-INF/resource/i18n/encoder/SEQUENCE.properties

en_US=sequence
zh_CN=流水码
about.en_US=TODO-INTRODUCTION
about.zh_CN=TODO-INTRODUCTION

无状态编码也适用。JAR:/META-INF/resource/i18n/encoder/{NAME}.properties

# 无状态编码器扩展办法

# 定义编码器配置对象

即在WEB端数据模型配置向导——字段编码配置向导页中,定义给字段配置码段编码规则时能够配置的那些参数。(由于WEB端尚未完全支持扩展码段编码器扩展动态表单,目前WEB端对于扩展的码段编码配置都是以一个JSON编辑器来展现的)

@Getter
@Setter
public class RandomULID extends Config {

    public static final String TYPE = "ULID";

    private boolean uppercase;

    @Override
    public final String getType() {
        return TYPE;
    }

}

# 编写码段编码器实现

@Component("MDMULIDEncoder")
public class ULIDEncoder implements StatelessEncoder<RandomULID>, Formatter<RandomULID> {

    private static final ThreadLocal<Ulid> ctx = ThreadLocal.withInitial(UlidCreator::getUlid);

    @Override
    public final String name() {
        return RandomULID.TYPE;
    }

    @Override
    public Class<RandomULID> type() {
        return RandomULID.class;
    }

    @Override
    public String generate(String model, RandomULID config, Map<String, Object> data) {
        Ulid ulid = ctx.get();
        String uuid = ulid.toString();
        ulid.increment();
        return format(config, config.isUppercase() ? uuid : uuid.toLowerCase());
    }

}

# 国际化配置(可选)

/META-INF/resource/i18n/encoder/ULID.properties

en_US=ULID
zh_CN=ULID
about.en_US=128 bit compatibility with UUID; 1.21e+24 unique ULIDs per millisecond; Sort in dictionary order (i.e. alphabetical order); Encode it into 26 strings in a standardized manner, instead of the 36 characters of UUID; Use Crockford's base32 for better efficiency and readability (5 bits per character); Not case sensitive; No special characters (URL safe); Monotonic sorting order (correctly detecting and processing the same milliseconds)
about.zh_CN=与UUID的128位兼容性;每毫秒1.21e + 24个唯一ULID;按字典顺序(也就是字母顺序)排序;规范地编码为26个字符串,而不是UUID的36个字符;使用Crockford的base32获得更好的效率和可读性(每个字符5位);不区分大小写;没有特殊字符(URL安全);单调排序顺序(正确检测并处理相同的毫秒)