【Java】ChineseCurrencyConverterV2(货币金额大写转换工具类Ver.2)

货币金额大写转换工具类 Ver.2

java 复制代码
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * 人民币金额大写转换工具.
 * <p>提供阿拉伯数字金额与中文大写金额之间的双向转换,采用「两级单调 + 分层校验」架构.
 *
 * <h3>支持两种运行模式,默认使用标准模式:</h3>
 * <ul>
 *   <li><b>标准模式 (默认)</b>:对齐通用财务规范,金额范围 0.01(分) ~ 999999999999.99(千亿级), 仅使用元、角、分、拾、佰、仟、万、亿标准单位</li>
 *   <li><b>拓展模式</b>:支持拓展大额单位(兆、京、垓), 金额范围扩展至仟垓级(1E+24), 用于超大额特殊场景</li>
 * </ul>
 * <h3>支持两种分段体系,共享同一套万进制内核:</h3>
 * <ul>
 *   <li><b>4位分段 (默认)</b>:万进位,标准段单位:元/万/亿,拓展段单位:兆/京/垓</li>
 *   <li><b>8位分段</b>:亿进位(段内复用万进制双子段结构),标准段单位:元/亿,拓展段单位:京</li>
 * </ul>
 * <h3>核心校验规则:</h3>
 * <ol>
 *   <li><b>全局级单调性</b>:大段单位正序严格递减,不可重复、不可逆序</li>
 *   <li><b>段内级单调性</b>:双子段结构,子段分隔符「万」唯一,子段内仟/佰/拾严格递减</li>
 *   <li><b>边界零规则</b>:非相邻数位必须补连接零;万位为0且千位非0时,可通过配置宽容省略</li>
 *   <li><b>邻接规则</b>:非零数字必须带单位修饰,禁止连续零,零后禁止直接接单位(零元除外)</li>
 * </ol>
 * <pre>设计说明:
 * 1. 本工具仅负责金额数字的合规转换,不包含业务前缀。
 *    - 由调用方自行拼接:"人民币" + toText(...)
 * 2. 反向解析(toAmount)仅支持标准语序:"人民币负XXX",不支持"负人民币XXX"。
 * 3. 核心工具类采用无状态纯函数设计,线程安全;上下文透传由上层业务框架自行封装。
 * 4. 所有异常采用统一结构化格式:[错误码] 描述文本; 实体: '值1', '值2',便于上层解析与转译。
 * </pre>
 * @since JDK 1.8
 */
public class ChineseCurrencyConverterV2 {

    // region ========== 配置类:不可变参数封装 ==========
    /**
     * 转换配置类
     * <p>使用不可变对象封装所有开关参数,通过预设实例 + with 派生方法构建,
     * 避免多参数重载的维护成本,所有配置项在正向生成、反向解析、校验逻辑中严格一致生效。
     *
     * <h3>配置项说明</h3>
     * <ul>
     *   <li>{@code stripLeadingZeroYuan}:纯小数场景是否省略「零元」前缀,仅正向生成生效</li>
     *   <li>{@code extendedMode}:是否启用兆/京/垓拓展大额单位,正反双向同步生效</li>
     *   <li>{@code wanZeroTolerant}:是否启用万位零宽容,正反双向同步生效;
     *       开启后万位为0且千位非0时,可省略连接零,严格对齐《支付结算办法》可选规则</li>
     *   <li>{@code eightSegmentMode}:是否启用8位分段(亿进位)体系,正反双向同步生效</li>
     * </ul>
     */
    public static final class Config {
        /**
         * 默认配置:省略零元前缀、标准模式、万位零宽容、4位分段
         */
        public static final Config DEFAULT = new Config(true, false, true, false);
        /**
         * 拓展模式配置:支持兆/京/垓大额单位
         */
        public static final Config EXTENDED = DEFAULT.withExtendedMode(true);
        /**
         * 严格模式配置:关闭万位零宽容,仅接受标准带零写法,适用于银行票据等强合规场景
         */
        public static final Config STRICT = DEFAULT.withWanZeroTolerant(false);
        /**
         * 8位分段配置:亿进位体系
         */
        public static final Config EIGHT_SEG = DEFAULT.withEightSegmentMode(true);

        private final boolean stripLeadingZeroYuan;
        private final boolean extendedMode;
        private final boolean wanZeroTolerant;
        private final boolean eightSegmentMode;

        /**
         * 私有构造,禁止外部直接实例化
         * @param stripLeadingZeroYuan 纯小数场景是否省略「零元」前缀
         * @param extendedMode 是否启用拓展大额单位
         * @param wanZeroTolerant 是否启用万位零宽容模式
         * @param eightSegmentMode 是否启用8位分段模式
         */
        private Config(boolean stripLeadingZeroYuan, boolean extendedMode,
                       boolean wanZeroTolerant, boolean eightSegmentMode) {
            this.stripLeadingZeroYuan = stripLeadingZeroYuan;
            this.extendedMode = extendedMode;
            this.wanZeroTolerant = wanZeroTolerant;
            this.eightSegmentMode = eightSegmentMode;
        }

        /**
         * @return 纯小数场景是否省略「零元」前缀
         */
        public boolean isStripLeadingZeroYuan() { return stripLeadingZeroYuan; }
        /**
         * @return 是否启用拓展大额单位模式
         */
        public boolean isExtendedMode() { return extendedMode; }
        /**
         * @return 是否启用万位零宽容模式
         */
        public boolean isWanZeroTolerant() { return wanZeroTolerant; }
        /**
         * @return 是否启用8位分段模式
         */
        public boolean isEightSegmentMode() { return eightSegmentMode; }

        /**
         * 派生新配置:设置是否省略零元前缀
         * @param value 是否省略
         * @return 新的配置实例
         */
        public Config withStripLeadingZeroYuan(boolean value) {
            return new Config(value, extendedMode, wanZeroTolerant, eightSegmentMode);
        }
        /**
         * 派生新配置:设置是否启用拓展模式
         * @param value 是否启用
         * @return 新的配置实例
         */
        public Config withExtendedMode(boolean value) {
            return new Config(stripLeadingZeroYuan, value, wanZeroTolerant, eightSegmentMode);
        }
        /**
         * 派生新配置:设置是否启用万位零宽容
         * @param value 是否启用
         * @return 新的配置实例
         */
        public Config withWanZeroTolerant(boolean value) {
            return new Config(stripLeadingZeroYuan, extendedMode, value, eightSegmentMode);
        }
        /**
         * 派生新配置:设置是否启用8位分段
         * @param value 是否启用
         * @return 新的配置实例
         */
        public Config withEightSegmentMode(boolean value) {
            return new Config(stripLeadingZeroYuan, extendedMode, wanZeroTolerant, value);
        }

        /**
         * 内部派生:大段单位的最低层级阈值
         * @return 判定为大段单位的最低层级
         */
        int getLargeSegmentThreshold() {
            return eightSegmentMode
                    ? UNIT_LEVEL_MAP.get(UNIT_YI)
                    : UNIT_LEVEL_MAP.get(UNIT_WAN);
        }
    }
    // endregion

    // region ========== 内部状态机:分层校验上下文 ==========
    /** 数字类型枚举,用于邻接状态追踪 */
    private enum DigitType { NONE, ZERO, NON_ZERO }

    /**
     * 字符邻接状态
     * <p>追踪上一字符的类型与位置,支撑数字连用、零后接单位等邻接校验
     */
    private static class CharAdjacencyState {
        char prevChar = Character.MIN_VALUE;
        DigitType prevDigitType = DigitType.NONE;
        int prevIndex = -1;

        /** 重置为初始状态 */
        void reset() {
            prevChar = Character.MIN_VALUE;
            prevDigitType = DigitType.NONE;
            prevIndex = -1;
        }

        /**
         * 更新扫描状态
         * @param ch 当前字符
         * @param isDigit 是否为数字
         * @param index 当前字符索引
         */
        void track(char ch, boolean isDigit, int index) {
            this.prevIndex = index;
            this.prevChar = ch;
            this.prevDigitType = isDigit
                    ? (ch == CHN_ZERO ? DigitType.ZERO : DigitType.NON_ZERO)
                    : DigitType.NONE;
        }

        /** @return 上一字符是否处于第1位 */
        boolean isIndexOne() { return prevIndex == 1; }
        /** @return 状态是否未初始化 */
        boolean isUninit() { return prevChar == Character.MIN_VALUE && prevIndex == -1; }
    }

    /**
     * 段内排序状态机
     * <p>原生支持双子段结构,保障段内单位严格单调:
     * 倒序视角下子段内单位层级严格递增,子段分隔符「万」仅允许出现一次
     */
    private static class SegmentOrderState {
        int currentLevel;
        int segmentIndex;
        final int subSegThreshold;

        /**
         * 构造分段排序状态机
         * @param subSegThreshold 子段分隔单位的层级(万的层级)
         */
        SegmentOrderState(int subSegThreshold) {
            this.subSegThreshold = subSegThreshold;
            reset();
        }

        /** 重置为初始状态,复用于下一个大段 */
        void reset() {
            this.currentLevel = 0;
            this.segmentIndex = 0;
        }

        /**
         * 接收单位并校验单调性,非法则直接抛出异常
         * @param unitLevel 当前单位层级
         * @param segmentUnit 所属大段单位,用于异常上下文
         * @param position 当前字符位置,用于异常快照
         */
        void accept(int unitLevel, char segmentUnit, int position) {
            // 子段分隔符仅允许切换一次
            if (isSubSegmentSeparator(unitLevel)) {
                if (segmentIndex != 0) {
                    Map<String, String> entities = new HashMap<>(2);
                    entities.put("unit", String.valueOf(VALID_UNIT_CHARS.charAt(subSegThreshold - 1)));
                    fail(ERR_UNIT, "段内单位重复,子段分隔符仅允许出现一次:{unit}", position, entities);
                }
                segmentIndex++;
                currentLevel = 0;
                return;
            }
            // 段内禁止出现拾以下的低位单位
            if (unitLevel < UNIT_LEVEL_MAP.get(UNIT_SHI)) {
                Map<String, String> entities = new HashMap<>(2);
                entities.put("unit", String.valueOf(VALID_UNIT_CHARS.charAt(unitLevel - 1)));
                fail(ERR_UNIT, "段内禁止出现低位单位:{unit}", position, entities);
            }
            // 子段内严格递增校验(对应正序严格递减)
            if (unitLevel <= currentLevel) {
                Map<String, String> entities = new HashMap<>(2);
                entities.put("unit", String.valueOf(VALID_UNIT_CHARS.charAt(unitLevel - 1)));
                fail(ERR_UNIT, "段内单位语序非法(逆序或重复):{unit}", position, entities);
            }
            currentLevel = unitLevel;
        }

        /**
         * 判断是否为子段分隔单位
         * @param unitLevel 单位层级
         * @return 是否为分隔符
         */
        boolean isSubSegmentSeparator(int unitLevel) {
            return unitLevel == subSegThreshold;
        }
    }

    /**
     * 段边界零校验状态
     * <p>基于单位层级差 + 间隔非零标记,判断是否缺少连接零
     */
    private static class SegmentBoundaryState {
        int prevUnitLevel;
        boolean intervalHasNonZero;
        int currentSegmentIndex;
        final int baseLevel;

        /**
         * 构造边界零校验状态
         * @param baseLevel 大段基准层级
         */
        SegmentBoundaryState(int baseLevel) {
            this.baseLevel = baseLevel;
            reset();
        }

        /** 重置为初始状态 */
        void reset() {
            prevUnitLevel = 0;
            intervalHasNonZero = false;
            currentSegmentIndex = 0;
        }

        /**
         * 遇到单位时更新状态
         * @param level 当前单位层级
         */
        void trackUnit(int level) {
            this.prevUnitLevel = level;
            this.intervalHasNonZero = false;
            int yuanLevel = UNIT_LEVEL_MAP.get(UNIT_YUAN);
            if (level == yuanLevel) {
                this.currentSegmentIndex = 0;
            } else if (level >= baseLevel) {
                this.currentSegmentIndex = level - baseLevel + 1;
            }
        }

        /**
         * 遇到数字时更新间隔标记
         * @param ch 当前数字字符
         */
        void trackDigit(char ch) {
            this.intervalHasNonZero = (ch != CHN_ZERO);
        }
    }

    /**
     * 单段解析内核
     * <p>与外层大段调度完全解耦,仅负责单段内的语法校验与数值累加,可复用原子组件
     */
    private static class SegmentParser {
        private final CharAdjacencyState chAdjState = new CharAdjacencyState();
        private final SegmentOrderState orderState;
        private final SegmentBoundaryState boundState;
        private final boolean tolerant;
        private boolean shi, bai, qian, wan;
        private long segmentValue;
        private char segmentChar;
        private int maxPower;
        private int currentIndex;

        /**
         * 构造单段解析器
         * @param subSegBase 子段基准层级
         * @param tolerant 是否启用万位零宽容
         */
        SegmentParser(int subSegBase, boolean tolerant) {
            this.orderState = new SegmentOrderState(subSegBase);
            this.boundState = new SegmentBoundaryState(subSegBase);
            this.tolerant = tolerant;
        }

        /**
         * 重置解析器状态,复用于下一个大段
         * @param segChar 当前大段单位字符,用于异常上下文
         */
        void reset(char segChar) {
            chAdjState.reset();
            orderState.reset();
            boundState.reset();
            shi = bai = qian = wan = false;
            segmentValue = 0;
            segmentChar = segChar;
            maxPower = 0;
            currentIndex = -1;
        }

        /**
         * 处理单个字符(倒序输入)
         * @param ch 当前字符
         * @param leftChar 正序左侧字符,用于邻接校验
         * @param index 当前字符索引
         */
        void processChar(char ch, char leftChar, int index) {
            this.currentIndex = index;
            boolean isDigit = DIGIT_VALUE_MAP.containsKey(ch);
            boolean isUnit = !isDigit && UNIT_LEVEL_MAP.containsKey(ch);

            // @formatter:off
            if (isUnit) {
                if (!chAdjState.isUninit()) {
                    checkAdjacentUnitPair(ch, leftChar, index);
                }
                orderState.accept(UNIT_LEVEL_MAP.get(ch), segmentChar, index);
                checkBoundaryZero(ch, boundState, tolerant, index);
                boundState.trackUnit(UNIT_LEVEL_MAP.get(ch));

                switch (ch) {
                    case UNIT_SHI  : shi  = true; break;
                    case UNIT_BAI  : bai  = true; break;
                    case UNIT_QIAN : qian = true; break;
                    case UNIT_WAN  : wan  = true; break;
                    default: break;
                }
            } else if (isDigit) {
                checkDigitAdjacency(ch, chAdjState, index);
                checkZeroFollowByUnit(ch, chAdjState, index);
                boundState.trackDigit(ch);

                int digit = DIGIT_VALUE_MAP.get(ch);
                if (digit > 0) {
                    long val = digit;
                    int power = 0;
                    if (shi)  { val *= 10;    power += 1; }
                    if (bai)  { val *= 100;   power += 2; }
                    if (qian) { val *= 1000;  power += 3; }
                    if (wan)  { val *= 10000; power += 4; }
                    segmentValue += val;
                    if (power > maxPower) maxPower = power;
                }
                shi = bai = qian = false;
            } else {
                fail(ERR_FORMAT, "包含非法字符:{char}", "char", String.valueOf(ch));
            }
            // @formatter:on
            chAdjState.track(ch, isDigit, index);
        }

        /** @return 当前段的数值 */
        long getValue() { return segmentValue; }
        /** @return 当前段是否包含非零数字 */
        boolean hasNonZero() { return segmentValue > 0; }
        /** @return 当前段最高有效位的相对幂次(相对于段基准) */
        int getMaxPower() { return hasNonZero() ? maxPower : -1; }
    }

    /**
     * 预处理结果封装
     */
    private static class PreprocessResult {
        /** 整数部分字符串(已移除元单位) */
        final String intPart;
        /** 小数部分数值 */
        final BigDecimal decimalValue;
        /** 是否为负数 */
        final boolean negative;

        /**
         * @param intPart 整数部分字符串
         * @param decimalValue 小数部分数值
         * @param negative 是否为负数
         */
        PreprocessResult(String intPart, BigDecimal decimalValue, boolean negative) {
            this.intPart = intPart;
            this.decimalValue = decimalValue;
            this.negative = negative;
        }
    }
    // endregion

    // region ========== 常量定义 ==========
    // 错误码分类:严格对应异常类型
    // @formatter:off
    private static final String ERR_PARAM  = "ERR_PARAM";
    private static final String ERR_FORMAT = "ERR_FORMAT";
    private static final String ERR_UNIT   = "ERR_UNIT";
    private static final String ERR_RANGE  = "ERR_RANGE";
    // @formatter:on

    // 数字字符:0~9 对应中文大写
    // @formatter:off
    private static final char CHN_ZERO   = '零';
    private static final char CHN_ONE    = '壹';
    private static final char CHN_TWO    = '贰';
    private static final char CHN_THREE  = '叁';
    private static final char CHN_FOUR   = '肆';
    private static final char CHN_FIVE   = '伍';
    private static final char CHN_SIX    = '陆';
    private static final char CHN_SEVEN  = '柒';
    private static final char CHN_EIGHT  = '捌';
    private static final char CHN_NINE   = '玖';
    // @formatter:on

    // 单位字符:按层级从低到高排列,层级 = 索引 + 1
    // @formatter:off
    private static final char UNIT_FEN   = '分';
    private static final char UNIT_JIAO  = '角';
    private static final char UNIT_YUAN  = '元';
    private static final char UNIT_SHI   = '拾';
    private static final char UNIT_BAI   = '佰';
    private static final char UNIT_QIAN  = '仟';
    private static final char UNIT_WAN   = '万';
    private static final char UNIT_YI    = '亿';
    private static final char UNIT_ZHAO  = '兆';
    private static final char UNIT_JING  = '京';
    private static final char UNIT_GAI   = '垓';
    // @formatter:on

    // 特殊字符
    // @formatter:off
    private static final char CHN_ZHENG      = '整';
    private static final char CHN_ZHENG_ALT  = '正';
    private static final char CHN_NEGATIVE   = '负';
    private static final String STR_ZHENG    = "整";
    private static final String STR_RMB_PREFIX = "人民币";
    private static final String STR_ZERO_YUAN_ZHENG = "零元整";
    // @formatter:off

    // 权值常量:与单位一一对应
    // @formatter:off
    private static final BigDecimal RATE_FEN   = new BigDecimal("0.01");
    private static final BigDecimal RATE_JIAO  = new BigDecimal("0.1");
    private static final BigDecimal RATE_SHI   = BigDecimal.TEN;
    private static final BigDecimal RATE_BAI   = new BigDecimal("100");
    private static final BigDecimal RATE_QIAN  = new BigDecimal("1000");
    private static final BigDecimal RATE_WAN   = new BigDecimal("10000");
    private static final BigDecimal RATE_YI    = new BigDecimal("100000000");
    private static final BigDecimal RATE_ZHAO  = new BigDecimal("1000000000000");
    private static final BigDecimal RATE_JING  = new BigDecimal("10000000000000000");
    private static final BigDecimal RATE_GAI   = new BigDecimal("1E+20");
    // @formatter:off

    // 范围常量
    private static final BigDecimal STANDARD_MAX = new BigDecimal("1000000000000.00");
    private static final BigDecimal EXTENDED_MAX = new BigDecimal("1E+24");

    // 映射表:运行时快速查询
    private static final char[] DIGITS = {
            CHN_ZERO, CHN_ONE, CHN_TWO, CHN_THREE, CHN_FOUR,
            CHN_FIVE, CHN_SIX, CHN_SEVEN, CHN_EIGHT, CHN_NINE
    };
    private static final String VALID_UNIT_CHARS = "分角元拾佰仟万亿兆京垓";
    private static final Map<Character, Integer> DIGIT_VALUE_MAP = new HashMap<Character, Integer>(16);
    private static final Map<Character, Integer> UNIT_LEVEL_MAP = new HashMap<Character, Integer>(16);

    static {
        for (int i = 0; i < DIGITS.length; i++) {
            DIGIT_VALUE_MAP.put(DIGITS[i], i);
        }
        for (int i = 0; i < VALID_UNIT_CHARS.length(); i++) {
            UNIT_LEVEL_MAP.put(VALID_UNIT_CHARS.charAt(i), i + 1);
        }
    }
    // endregion

    // region ========== 结构化异常 + 极简fail工具 ==========
    /**
     * 金额解析异常
     * <p>继承 {@link IllegalArgumentException},完全兼容原有捕获逻辑;
     * 携带结构化上下文字段,便于包装器生成友好提示与问题定位。
     */
    public static class AmountParseException extends IllegalArgumentException {
        private final String errorCode;
        private final int position;
        private final Map<String, String> entities;

        /**
         * 构造结构化解析异常
         * @param message 人类可读异常消息
         * @param errorCode 错误码
         * @param position 出错字符正序位置,从0开始;无位置信息为-1
         * @param entities 命名实体集合
         */
        AmountParseException(String message, String errorCode, int position, Map<String, String> entities) {
            super(message);
            this.errorCode = errorCode;
            this.position = position;
            this.entities = entities != null
                    ? Collections.unmodifiableMap(entities)
                    : Collections.emptyMap();
        }

        /**
         * @return 错误码
         */
        public String getErrorCode() { return errorCode; }

        /**
         * @return 出错字符正序位置,从0开始;无位置信息返回-1
         */
        public int getPosition() { return position; }

        /**
         * 根据键获取实体值
         * @param key 实体键名
         * @return 实体值,不存在则返回空字符串
         */
        public String getEntity(String key) { return entities.getOrDefault(key, ""); }

        /**
         * @return 全部实体的不可变Map
         */
        public Map<String, String> getEntities() { return entities; }
    }

    /**
     * 抛出无实体无位置的异常
     * @param code 错误码
     * @param msg 错误描述
     */
    private static void fail(String code, String msg) {
        throw new AmountParseException(msg, code, -1, null);
    }

    /**
     * 抛出单命名实体、无位置的异常
     * @param code 错误码
     * @param msg 错误描述(含命名占位符)
     * @param key 实体键名
     * @param value 实体值
     */
    private static void fail(String code, String msg, String key, String value) {
        Map<String, String> entities = new HashMap<>(2);
        entities.put(key, value);
        String message = msg.replace("{" + key + "}", value);
        throw new AmountParseException(message, code, -1, entities);
    }

    /**
     * 抛出多命名实体、带位置的异常
     * @param code 错误码
     * @param msg 错误描述(含命名占位符)
     * @param position 出错字符位置
     * @param entities 命名实体集合
     */
    private static void fail(String code, String msg, int position, Map<String, String> entities) {
        StringBuilder sb = new StringBuilder(msg);
        for (Map.Entry<String, String> entry : entities.entrySet()) {
            String placeholder = "{" + entry.getKey() + "}";
            int idx = sb.indexOf(placeholder);
            if (idx >= 0) {
                sb.replace(idx, idx + placeholder.length(), entry.getValue());
            }
        }
        if (position >= 0) {
            sb.append("(位置:第").append(position + 1).append("个字符)");
        }
        throw new AmountParseException(sb.toString(), code, position, entities);
    }
    // endregion

    // region ========== 语法校验核心方法 ==========
    /**
     * 校验单位邻接合法性
     * <p>规则:单位左侧必须是数字,或允许的前置单位
     * @param cur 当前单位
     * @param left 正序左侧字符
     * @param position 当前字符位置
     */
    private static void checkAdjacentUnitPair(char cur, char left, int position) {
        if (DIGIT_VALUE_MAP.containsKey(left)) { return; }
        if (cur == UNIT_YUAN) {
            if (left == UNIT_FEN || left == UNIT_JIAO) {
                Map<String, String> entities = new HashMap<>(4);
                entities.put("left", String.valueOf(left));
                entities.put("curr", String.valueOf(cur));
                fail(ERR_UNIT, "元左侧禁止出现分/角单位:{left}", position, entities);
            }
            return;
        }
        if (UNIT_LEVEL_MAP.get(cur) >= UNIT_LEVEL_MAP.get(UNIT_WAN)) {
            if (left != UNIT_SHI && left != UNIT_BAI && left != UNIT_QIAN) {
                Map<String, String> entities = new HashMap<>(4);
                entities.put("left", String.valueOf(left));
                entities.put("curr", String.valueOf(cur));
                fail(ERR_UNIT, "量级单位左侧仅允许拾/佰/仟前置:{left}", position, entities);
            }
            return;
        }
        Map<String, String> entities = new HashMap<>(4);
        entities.put("left", String.valueOf(left));
        entities.put("curr", String.valueOf(cur));
        fail(ERR_UNIT, "段内单位禁止直接相邻:{left}、{curr}", position, entities);
    }

    /**
     * 校验段边界连接零合法性
     * @param unit 当前单位
     * @param state 边界零状态
     * @param tolerant 是否启用万位零宽容
     * @param position 当前字符位置
     */
    private static void checkBoundaryZero(char unit, SegmentBoundaryState state, boolean tolerant, int position) {
        int curLevel = UNIT_LEVEL_MAP.get(unit);
        int lastLevel = state.prevUnitLevel;
        if (lastLevel <= 0 || !state.intervalHasNonZero) { return; }

        if (curLevel < lastLevel) {
            if (curLevel > UNIT_LEVEL_MAP.get(UNIT_SHI)) {
                // 万位零宽容:严格对齐规范,仅万位为0且千位非0时豁免
                boolean wanTolerant = tolerant
                        && curLevel == UNIT_LEVEL_MAP.get(UNIT_WAN)
                        && lastLevel == UNIT_LEVEL_MAP.get(UNIT_QIAN);
                if (!wanTolerant) {
                    Map<String, String> entities = new HashMap<>(2);
                    entities.put("unit", String.valueOf(unit));
                    fail(ERR_FORMAT, "非相邻数位间缺少连接零:{unit}", position, entities);
                }
            }
        } else if (curLevel > lastLevel) {
            boolean adjacent = (curLevel - lastLevel == 1 && lastLevel < UNIT_LEVEL_MAP.get(UNIT_WAN))
                    || (lastLevel == UNIT_LEVEL_MAP.get(UNIT_QIAN)
                    && curLevel == state.baseLevel + state.currentSegmentIndex);
            if (!adjacent) {
                Map<String, String> entities = new HashMap<>(2);
                entities.put("unit", String.valueOf(unit));
                fail(ERR_FORMAT, "非相邻数位间缺少连接零:{unit}", position, entities);
            }
        }
    }

    /**
     * 校验数字邻接规则
     * <p>规则:非零数字必须带单位修饰;禁止连续出现多个零字
     * @param ch 当前数字
     * @param state 邻接状态
     * @param position 当前字符位置
     */
    private static void checkDigitAdjacency(char ch, CharAdjacencyState state, int position) {
        if (state.isUninit()) { return; }
        if (ch != CHN_ZERO) {
            if (state.prevDigitType == DigitType.NON_ZERO || state.prevDigitType == DigitType.ZERO) {
                Map<String, String> entities = new HashMap<>(4);
                entities.put("curr", String.valueOf(ch));
                entities.put("prev", String.valueOf(state.prevChar));
                fail(ERR_UNIT, "非零数字缺少单位修饰:{curr}", position, entities);
            }
            return;
        }
        if (state.prevDigitType == DigitType.ZERO) {
            Map<String, String> entities = new HashMap<>(2);
            entities.put("chars", String.valueOf(ch) + state.prevChar);
            fail(ERR_FORMAT, "禁止连续多零结构:{chars}", position, entities);
        }
    }

    /**
     * 校验零后接单位规则
     * <p>规则:零不能直接跟单位,「零元」除外
     * @param ch 当前字符
     * @param state 邻接状态
     * @param position 当前字符位置
     */
    private static void checkZeroFollowByUnit(char ch, CharAdjacencyState state, int position) {
        if (state.isUninit()) { return; }
        if (ch == CHN_ZERO && state.prevDigitType == DigitType.NONE) {
            if (state.isIndexOne()) {
                if (state.prevChar == UNIT_YUAN) return;
                Map<String, String> entities = new HashMap<>(2);
                entities.put("chars", String.valueOf(ch) + state.prevChar);
                fail(ERR_FORMAT, "零字开头仅允许零元:{chars}", position, entities);
            }
            Map<String, String> entities = new HashMap<>(4);
            entities.put("zero", String.valueOf(ch));
            entities.put("unit", String.valueOf(state.prevChar));
            fail(ERR_UNIT, "禁止零后直接接单位:{zero}、{unit}", position, entities);
        }
    }
    // endregion

    // region ========== 通用工具方法 ==========
    /**
     * 判断是否为拓展单位(兆及以上)
     * @param unit 单位字符
     * @return 是否为拓展单位
     */
    private static boolean isExtendedUnit(char unit) {
        return UNIT_LEVEL_MAP.get(unit) >= UNIT_LEVEL_MAP.get(UNIT_ZHAO);
    }

    /**
     * 判断是否为大段单位(达到分段阈值)
     * @param unit 单位字符
     * @param threshold 分段阈值
     * @return 是否为大段单位
     */
    private static boolean isLargeSegmentUnit(char unit, int threshold) {
        return UNIT_LEVEL_MAP.getOrDefault(unit, Integer.MIN_VALUE) >= threshold;
    }

    /**
     * 获取大段单位对应的权值
     * @param unit 单位字符
     * @return 权值
     */
    private static BigDecimal getLargeSegmentRate(char unit) {
        // @formatter:off
        switch (unit) {
            case UNIT_YUAN: return BigDecimal.ONE;
            case UNIT_WAN:  return RATE_WAN;
            case UNIT_YI:   return RATE_YI;
            case UNIT_ZHAO: return RATE_ZHAO;
            case UNIT_JING: return RATE_JING;
            case UNIT_GAI:  return RATE_GAI;
            default:        return BigDecimal.ONE;
        }
        // @formatter:on
    }

    /**
     * 获取大段单位的基准幂次(以元为0位)
     * @param unit 单位字符
     * @return 10的幂次
     */
    private static int getBasePower(char unit) {
        // @formatter:off
        switch (unit) {
            case UNIT_YUAN: return 0;
            case UNIT_WAN:  return 4;
            case UNIT_YI:   return 8;
            case UNIT_ZHAO: return 12;
            case UNIT_JING: return 16;
            case UNIT_GAI:  return 20;
            default:        return 0;
        }
        // @formatter:on
    }

    /**
     * 计算高位段最低非零数位的全局幂次
     * <p>依据:大写金额段内末尾零省略规则,大段单位左侧紧邻字符直接对应最低有效位
     * @param segUnit 大段单位字符
     * @param index 大段单位的倒序索引
     * @param text 整数部分文本
     * @return 最低非零位的全局幂次
     */
    private static int calcHighSegMinNonZeroPower(char segUnit, int index, String text) {
        int basePower = getBasePower(segUnit);
        if (index <= 0) {
            return basePower;
        }
        char leftChar = text.charAt(index - 1);
        if (DIGIT_VALUE_MAP.containsKey(leftChar)) {
            return basePower;
        }
        // @formatter:off
        switch (leftChar) {
            case UNIT_SHI:  return basePower + 1;
            case UNIT_BAI:  return basePower + 2;
            case UNIT_QIAN: return basePower + 3;
            default:        return basePower;
        }
        // @formatter:on
    }
    // endregion

    // region ========== 预处理逻辑 ==========
    /**
     * 输入预处理:清洗字符串、提取角分、基础格式校验
     * <p>执行顺序:零值判定 → 长度粗筛 → 去前缀 → 负号识别 → 结尾校验 → 去整字 → 提取角分 → 移除元单位
     * @param raw 原始输入
     * @param config 转换配置
     * @return 预处理结果
     */
    private static PreprocessResult preprocess(String raw, Config config) {
        String input = null;
        if (raw == null || (input = raw.trim()).isEmpty()) {
            fail(ERR_PARAM, "金额字符串不能为空");
        }

        // 合法零值快速通道:仅零元整为财务标准写法
        if (STR_ZERO_YUAN_ZHENG.equals(input) || "零元正".equals(input)) {
            return new PreprocessResult("", BigDecimal.ZERO, false);
        }
        // 冗余零值拦截:零元零角零分属于不规范写法
        if ("零元零角零分".equals(input)) {
            fail(ERR_FORMAT, "零金额标准写法为「零元整」,禁止零角零分冗余表述");
        }

        // 长度粗筛
        int maxPureLen = config.isExtendedMode()
                ? (config.isEightSegmentMode() ? 38 : 32) * 2
                : (config.isEightSegmentMode() ? 22 : 14) * 2;
        if (input.length() > maxPureLen + 5) {
            String mode = config.isExtendedMode() ? "拓展模式" : "标准模式";
            fail(ERR_FORMAT, mode + "下输入长度超过最大允许值");
        }

        // 移除人民币前缀
        if (input.startsWith(STR_RMB_PREFIX)) {
            input = input.substring(3);
        }

        // 识别负号
        boolean negative = false;
        if (!input.isEmpty() && input.charAt(0) == CHN_NEGATIVE) {
            negative = true;
            input = input.substring(1);
        }
        if (input.isEmpty()) {
            fail(ERR_FORMAT, "未包含有效金额内容");
        }

        // 结尾合法性校验
        char last = input.charAt(input.length() - 1);
        if (last != UNIT_JIAO && last != UNIT_FEN && last != CHN_ZHENG && last != CHN_ZHENG_ALT) {
            fail(ERR_FORMAT, "金额结尾必须为分/角/整/正:{last}", "last", String.valueOf(last));
        }

        // 移除整/正后缀
        if (last == CHN_ZHENG || last == CHN_ZHENG_ALT) {
            if (input.length() < 2) {
                fail(ERR_FORMAT, "金额格式非法,缺少有效主体:{last}", "last", String.valueOf(last));
            }
            char secondLast = input.charAt(input.length() - 2);
            if (secondLast != UNIT_YUAN && secondLast != UNIT_JIAO) {
                fail(ERR_FORMAT, "整字前必须为元或角:{chars}", "chars", String.valueOf(secondLast) + last);
            }
            input = input.substring(0, input.length() - 1);
        }

        // 提取分位:分位数字必须非零(零分无需写出)
        BigDecimal decimal = BigDecimal.ZERO;
        boolean hasFen = false;
        if (input.length() >= 2 && input.charAt(input.length() - 1) == UNIT_FEN) {
            char digitChar = input.charAt(input.length() - 2);
            if (!DIGIT_VALUE_MAP.containsKey(digitChar) || digitChar == CHN_ZERO) {
                fail(ERR_FORMAT, "分位缺少有效数字:{digit}", "digit", String.valueOf(digitChar));
            }
            int digit = DIGIT_VALUE_MAP.get(digitChar);
            decimal = decimal.add(BigDecimal.valueOf(digit).multiply(RATE_FEN));
            input = input.substring(0, input.length() - 2);
            hasFen = true;
        }

        // 提取角位:角位数字必须非零(零角需用连接零替代)
        boolean hasJiao = false;
        if (input.length() >= 2 && input.charAt(input.length() - 1) == UNIT_JIAO) {
            char digitChar = input.charAt(input.length() - 2);
            if (!DIGIT_VALUE_MAP.containsKey(digitChar) || digitChar == CHN_ZERO) {
                fail(ERR_FORMAT, "角位缺少有效数字:{digit}", "digit", String.valueOf(digitChar));
            }
            int digit = DIGIT_VALUE_MAP.get(digitChar);
            decimal = decimal.add(BigDecimal.valueOf(digit).multiply(RATE_JIAO));
            input = input.substring(0, input.length() - 2);
            hasJiao = true;
        }

        // 有分无角且有整数部分时,分前必须有连接零
        if (hasFen && !hasJiao && !input.isEmpty()) {
            if (input.charAt(input.length() - 1) != CHN_ZERO) {
                fail(ERR_FORMAT, "非相邻数位间缺少连接零:{unit}", "unit", "分");
            }
            input = input.substring(0, input.length() - 1);
        }

        // 移除末尾元单位,剩余为纯整数部分
        if (input.endsWith(String.valueOf(UNIT_YUAN))) {
            input = input.substring(0, input.length() - 1);
        }
        return new PreprocessResult(input, decimal, negative);
    }
    // endregion

    // region ========== 正向转换:数字转大写 ==========
    /**
     * 数字转人民币大写(默认配置)
     * @param amount 金额数值
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount) {
        return toText(amount, Config.DEFAULT);
    }

    /**
     * 数字转人民币大写(可配置零元前缀)
     * @param amount 金额数值
     * @param stripZeroYuan 纯小数是否省略零元前缀
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, boolean stripZeroYuan) {
        Config config = new Config(stripZeroYuan, false, true, false);
        return toText(amount, config);
    }

    /**
     * 数字转人民币大写(可配置零元前缀+拓展模式)
     * @param amount 金额数值
     * @param stripZeroYuan 纯小数是否省略零元前缀
     * @param extended 是否启用拓展大额单位
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, boolean stripZeroYuan, boolean extended) {
        Config config = new Config(stripZeroYuan, extended, true, false);
        return toText(amount, config);
    }

    /**
     * 数字转人民币大写(全参数配置)
     * @param amount 金额数值
     * @param stripZeroYuan 纯小数是否省略零元前缀
     * @param extended 是否启用拓展大额单位
     * @param wanTolerant 是否启用万位零宽容
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, boolean stripZeroYuan,
                                boolean extended, boolean wanTolerant) {
        Config config = new Config(stripZeroYuan, extended, wanTolerant, false);
        return toText(amount, config);
    }

    /**
     * 数字转人民币大写(配置驱动核心入口)
     * <p>所有配置项严格生效:拓展模式控制范围、分段模式控制进位体系、
     * 万位零宽容控制万位零的省略、零元前缀控制纯小数写法
     * @param amount 金额数值
     * @param config 转换配置
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, Config config) {
        if (amount == null) { fail(ERR_PARAM, "金额不能为空"); }
        if (BigDecimal.ZERO.compareTo(amount) == 0)  { return STR_ZERO_YUAN_ZHENG; }

        boolean negative = amount.signum() < 0;
        BigDecimal abs = amount.abs().setScale(2, RoundingMode.HALF_UP);
        BigDecimal max = config.isExtendedMode() ? EXTENDED_MAX : STANDARD_MAX;
        if (abs.compareTo(max) >= 0) {
            fail(ERR_RANGE, "金额超出上限:{max}", "max", max.stripTrailingZeros().toPlainString());
        }

        String result = config.isEightSegmentMode()
                ? buildText8Seg(abs, config)
                : buildText4Seg(abs, config);
        return negative ? CHN_NEGATIVE + result : result;
    }

    /**
     * 4位分段模式大写生成
     * @param abs 绝对值金额
     * @param config 转换配置
     * @return 整数部分大写字符串
     */
    private static String buildText4Seg(BigDecimal abs, Config config) {
        String numStr = abs.toPlainString().replace(".", "");
        int totalLen = numStr.length();
        int intLen = totalLen - 2;
        StringBuilder sb = new StringBuilder(totalLen * 2);

        int padLen = (4 - intLen % 4) % 4;
        char[] intChars = new char[intLen + padLen];
        for (int i = 0; i < padLen; i++) intChars[i] = '0';
        numStr.getChars(0, intLen, intChars, padLen);
        int segCount = intChars.length / 4;

        char[] segUnits = {' ', '万', '亿', '兆', '京', '垓'};
        char[] posUnits = {'仟', '佰', '拾', ' '};
        boolean zeroPending = false;
        boolean intHasValue = false;
        boolean prevSegEndWithUnit = false;

        for (int s = 0; s < segCount; s++) {
            boolean segHasValue = false;
            for (int p = 0; p < 4; p++) {
                int digit = intChars[s * 4 + p] - '0';
                if (digit != 0) {
                    segHasValue = true;
                    intHasValue = true;
                    if (zeroPending) {
                        if (!(p == 0 && prevSegEndWithUnit)) sb.append(CHN_ZERO);
                        zeroPending = false;
                    }
                    sb.append(DIGITS[digit]);
                    if (p < 3) sb.append(posUnits[p]);
                } else {
                    if (intHasValue) zeroPending = true;
                }
            }
            if (segHasValue && s < segCount - 1) {
                sb.append(segUnits[segCount - 1 - s]);
            }
            // 万位零宽容:万段且开启宽容时,万位为0也视为段尾有单位,抵消连接零
            boolean isWanSeg = (segCount - 1 - s) == 1;
            prevSegEndWithUnit = segHasValue
                    && (intChars[s * 4 + 3] != '0' || (isWanSeg && config.isWanZeroTolerant()));
        }
        return appendDecimal(sb, numStr, intLen, intHasValue, config.isStripLeadingZeroYuan());
    }

    /**
     * 8位分段模式大写生成
     * <p>段内采用双子段结构:高4位万子段 + 万分隔符 + 低4位原子段;
     * 万位零宽容规则与4位分段完全一致,受配置统一控制
     * @param abs    绝对值金额
     * @param config 转换配置
     * @return 整数部分大写字符串
     */
    private static String buildText8Seg(BigDecimal abs, Config config) {
        String numStr = abs.toPlainString().replace(".", "");
        int totalLen = numStr.length();
        int intLen = totalLen - 2;
        StringBuilder sb = new StringBuilder(totalLen * 2);

        int padLen = (8 - intLen % 8) % 8;
        char[] intChars = new char[intLen + padLen];
        for (int i = 0; i < padLen; i++) intChars[i] = '0';
        numStr.getChars(0, intLen, intChars, padLen);
        int segCount = intChars.length / 8;

        char[] largeUnits = {'京', '亿', '元'};
        int unitOffset = largeUnits.length - segCount;
        boolean zeroPending = false;
        boolean intHasValue = false;
        boolean prevSegEndWithUnit = false;

        for (int s = 0; s < segCount; s++) {
            boolean segHasValue = false;
            boolean highPartHasValue = false;
            for (int p = 0; p < 8; p++) {
                int digit = intChars[s * 8 + p] - '0';
                if (digit != 0) {
                    segHasValue = true;
                    if (p < 4) highPartHasValue = true;
                    intHasValue = true;
                    if (zeroPending) {
                        if (!(p == 0 && prevSegEndWithUnit)) sb.append(CHN_ZERO);
                        zeroPending = false;
                    }
                    sb.append(DIGITS[digit]);
                    if (p != 3 && p < 7) {
                        int idx = p < 3 ? p : p - 4;
                        sb.append("仟佰拾".charAt(idx));
                    }
                } else {
                    if (intHasValue) zeroPending = true;
                }

                // 万子段分隔符处理:高子段有值则加万单位
                if (p == 3 && highPartHasValue) {
                    sb.append(UNIT_WAN);
                    // 万位零宽容:万位为0且开启宽容时,抵消末尾零
                    boolean wanIsZero = (intChars[s * 8 + 3] == '0');
                    if (wanIsZero && config.isWanZeroTolerant()) {
                        zeroPending = false;
                    }
                }
            }
            if (s < segCount - 1 && segHasValue) {
                sb.append(largeUnits[unitOffset + s]);
            }
            prevSegEndWithUnit = segHasValue && (intChars[s * 8 + 7] != '0');
        }
        return appendDecimal(sb, numStr, intLen, intHasValue, config.isStripLeadingZeroYuan());
    }

    /**
     * 追加角分部分与整字
     * @param sb 整数部分构建器
     * @param numStr 全量数字字符串
     * @param intLen 整数部分长度
     * @param intHasValue 整数部分是否有非零值
     * @param stripZeroYuan 是否省略零元前缀
     * @return 完整大写字符串
     */
    private static String appendDecimal(StringBuilder sb, String numStr,
                                        int intLen, boolean intHasValue, boolean stripZeroYuan) {
        int jiao = numStr.charAt(intLen) - '0';
        int fen = numStr.charAt(intLen + 1) - '0';
        if (intHasValue) {
            sb.append(UNIT_YUAN);
            if (jiao == 0 && fen == 0) {
                sb.append(STR_ZHENG);
            } else if (jiao == 0) {
                sb.append(CHN_ZERO).append(DIGITS[fen]).append(UNIT_FEN);
            } else if (fen == 0) {
                sb.append(DIGITS[jiao]).append(UNIT_JIAO);
            } else {
                sb.append(DIGITS[jiao]).append(UNIT_JIAO)
                        .append(DIGITS[fen]).append(UNIT_FEN);
            }
        } else {
            if (jiao == 0 && fen == 0) {
                sb.append(STR_ZERO_YUAN_ZHENG);
            } else if (jiao == 0) {
                if (!stripZeroYuan) sb.append("零元").append(CHN_ZERO);
                sb.append(DIGITS[fen]).append(UNIT_FEN);
            } else if (fen == 0) {
                if (!stripZeroYuan) sb.append("零元");
                sb.append(DIGITS[jiao]).append(UNIT_JIAO);
            } else {
                if (!stripZeroYuan) sb.append("零元");
                sb.append(DIGITS[jiao]).append(UNIT_JIAO)
                        .append(DIGITS[fen]).append(UNIT_FEN);
            }
        }
        return sb.toString();
    }
    // endregion

    // region ========== 反向转换:大写转数字 ==========
    /**
     * 人民币大写转数值(默认配置)
     * @param chinese 大写金额字符串
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
     */
    public static BigDecimal toAmount(String chinese) {
        return toAmount(chinese, Config.DEFAULT);
    }

    /**
     * 人民币大写转数值(可配置拓展模式)
     * @param chinese 大写金额字符串
     * @param extendedMode 是否启用拓展大额单位
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
     */
    public static BigDecimal toAmount(String chinese, boolean extendedMode) {
        Config config = new Config(true, extendedMode, true, false);
        return toAmount(chinese, config);
    }

    /**
     * 人民币大写转数值(配置驱动核心入口)
     * <p>采用倒序扫描 + 分层校验架构:外层调度大段,内核负责段内语法解析;
     * 所有校验规则严格跟随配置:拓展单位、分段体系、万位零宽容均同步生效
     * @param chinese 大写金额字符串
     * @param config 转换配置
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
     */
    public static BigDecimal toAmount(String chinese, Config config) {
        PreprocessResult pre = preprocess(chinese, config);
        String intPart = pre.intPart;
        BigDecimal total = pre.decimalValue;
        boolean negative = pre.negative;

        if (!intPart.isEmpty()) {
            int threshold = config.getLargeSegmentThreshold();
            int subSegBase = UNIT_LEVEL_MAP.get(UNIT_WAN);
            SegmentParser parser = new SegmentParser(subSegBase, config.isWanZeroTolerant());

            parser.reset(UNIT_YUAN);
            char lastLargeUnit = UNIT_YUAN;
            int lastLargeUnitLevel = UNIT_LEVEL_MAP.get(UNIT_YUAN);
            final int len = intPart.length();

            for (int i = len - 1; i >= 0; i--) {
                char ch = intPart.charAt(i);
                char leftChar = i > 0 ? intPart.charAt(i - 1) : Character.MIN_VALUE;
                boolean isUnit = UNIT_LEVEL_MAP.containsKey(ch);

                if (isUnit && isLargeSegmentUnit(ch, threshold)) {
                    // 模式兼容性校验
                    if (config.isEightSegmentMode() && (ch == UNIT_ZHAO || ch == UNIT_GAI)) {
                        fail(ERR_UNIT, "8位模式下不支持该单位:{unit}", "unit", String.valueOf(ch));
                    }
                    if (!config.isExtendedMode() && isExtendedUnit(ch)) {
                        fail(ERR_UNIT, "标准模式不支持拓展大额单位:{unit}", "unit", String.valueOf(ch));
                    }

                    // 全局大段单调性校验(严格递增 = 正序严格递减)
                    int curLargeUnitLevel = UNIT_LEVEL_MAP.get(ch);
                    if (curLargeUnitLevel <= lastLargeUnitLevel) {
                        Map<String, String> entities = new HashMap<String, String>(4);
                        entities.put("curr", String.valueOf(ch));
                        entities.put("prev", String.valueOf(lastLargeUnit));
                        fail(ERR_UNIT, "大段单位语序非法,{curr} 不能出现在 {prev} 前面", i, entities);
                    }
                    lastLargeUnitLevel = curLargeUnitLevel;

                    // 大段单位左侧合法性校验
                    boolean leftIllegal = (i == 0)
                            || leftChar == CHN_ZERO
                            || isLargeSegmentUnit(leftChar, threshold);
                    if (leftIllegal) {
                        Map<String, String> entities = new HashMap<String, String>(2);
                        entities.put("unit", String.valueOf(ch));
                        fail(ERR_UNIT, "大段单位左侧语法错误:{unit}", i, entities);
                    }

                    // 大段间连接零校验(基于幂次差判断数位相邻性)
                    if (parser.hasNonZero()) {
                        int lowMaxPower = getBasePower(lastLargeUnit) + parser.getMaxPower();
                        int highMinPower = calcHighSegMinNonZeroPower(ch, i, intPart);
                        boolean adjacent = (highMinPower - lowMaxPower == 1);
                        boolean hasBetweenZero = (i + 1 < len) && (intPart.charAt(i + 1) == CHN_ZERO);

                        if (!adjacent && !hasBetweenZero) {
                            // 万位零宽容:严格对齐《支付结算办法》
                            // 仅万位为0、千位非0、且开启宽容时,方可豁免连接零
                            boolean wanTolerant = config.isWanZeroTolerant()
                                    && ch == UNIT_WAN
                                    && lastLargeUnit == UNIT_YUAN
                                    && highMinPower - lowMaxPower == 2
                                    && parser.getMaxPower() == 3;

                            if (!wanTolerant) {
                                Map<String, String> entities = new HashMap<>(2);
                                entities.put("unit", String.valueOf(ch));
                                fail(ERR_FORMAT, "非相邻大段间缺少连接零:{unit}", i, entities);
                            }
                        }
                    }

                    // 结算当前低位段
                    BigDecimal segVal = BigDecimal.valueOf(parser.getValue());
                    total = total.add(segVal.multiply(getLargeSegmentRate(lastLargeUnit)));
                    // 状态重置,进入高位段解析
                    lastLargeUnit = ch;
                    parser.reset(ch);
                    continue;
                }

                // 兜底低位单位校验:整数段禁止出现元/角/分
                if (ch == UNIT_YUAN || ch == UNIT_JIAO || ch == UNIT_FEN) {
                    String msg = ch == UNIT_YUAN
                            ? "元单位仅可出现在金额末尾,禁止在整数段中间重复出现"
                            : "整数部分禁止包含角/分小数单位,角分必须位于金额末尾";
                    Map<String, String> entities = new HashMap<>(2);
                    entities.put("unit", String.valueOf(ch));
                    fail(ERR_UNIT, msg, i, entities);
                }

                // 普通字符交由段内解析器处理
                parser.processChar(ch, leftChar, i);
            }

            // 结算最高位大段
            BigDecimal topSegVal = BigDecimal.valueOf(parser.getValue());
            total = total.add(topSegVal.multiply(getLargeSegmentRate(lastLargeUnit)));
        }

        // 范围校验
        BigDecimal max = config.isExtendedMode() ? EXTENDED_MAX : STANDARD_MAX;
        if (total.compareTo(max) >= 0) {
            fail(ERR_RANGE, "解析金额超出上限:{max}", "max", max.stripTrailingZeros().toPlainString());
        }

        if (negative) total = total.negate();
        return total.setScale(2, RoundingMode.HALF_UP);
    }
    // endregion

    // region ========== 本地测试入口 ==========
    /**
     * 本地测试入口
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        System.out.println("===== 基础校验 =====");
        System.out.println("壹角壹分: " + toAmount("壹角壹分"));
        System.out.println("壹佰元整: " + toAmount("壹佰元整"));
        System.out.println("壹佰零壹元整: " + toAmount("壹佰零壹元整"));
        System.out.println("壹万贰仟元叁角肆分: " + toAmount("壹万贰仟元叁角肆分"));

        System.out.println("1_0000_2000.34: " + toText(new BigDecimal("100002000.34"), Config.EIGHT_SEG));
        System.out.println("100_1000_0100_2000.34: " + toText(new BigDecimal("100100001002000.34"), Config.EIGHT_SEG.withExtendedMode(true)));

        System.out.println("\n===== 角分零校验 =====");
        System.out.println("壹元零壹分(合法): " + toAmount("壹元零壹分"));
        testIllegal("壹元壹分", "壹元壹分(非法,缺零)");
        testIllegal("零元零角零分", "零元零角零分(非法,零角零分冗余)");

        System.out.println("\n===== 大段连接零校验 =====");
        System.out.println("壹亿壹仟万元整(合法,相邻): " + toAmount("壹亿壹仟万元整"));
        System.out.println("壹亿零壹万元整(合法,有零): " + toAmount("壹亿零壹万元整"));
        testIllegal("壹亿壹万元整", "壹亿壹万元整(非法,缺零)");

        System.out.println("\n===== 单位单调性校验 =====");
        testIllegal("壹角零壹万元整", "壹角零壹万元整(非法,低位单位混入整数段)");
        testIllegal("壹万壹万零壹亿零壹万元整", "壹万壹万零壹亿零壹万元整(非法,段内万重复)", Config.EIGHT_SEG);

        System.out.println("\n===== 万位零宽容校验 =====");
        System.out.println("壹拾万壹仟元整(宽容模式合法): " + toAmount("壹拾万壹仟元整", Config.DEFAULT));
        testIllegal("壹拾万零壹仟元整", "壹拾万零壹仟元整(严格模式非法)", Config.STRICT);

        System.out.println("\n===== 8位模式测试 =====");
        System.out.println("8位单段: " + toAmount("壹仟贰佰叁拾肆万伍仟陆佰柒拾捌元玖角", Config.EIGHT_SEG));
        System.out.println("8位跨段合法: " + toAmount("壹亿零壹元整", Config.EIGHT_SEG));

        System.out.println("===== 正向生成校验 =====");
        System.out.println("107000.53 默认宽容: " + toText(new BigDecimal("107000.53")));
        System.out.println("107000.53 严格模式: " + toText(new BigDecimal("107000.53"), Config.STRICT));
        System.out.println("10001.00 默认模式: " + toText(new BigDecimal("10001.00")));

        System.out.println("\n===== 反向解析校验 =====");
        System.out.println("壹拾万柒仟元伍角叁分(宽容合法): " + toAmount("壹拾万柒仟元伍角叁分"));
        testIllegal("壹拾万柒仟元伍角叁分", "严格模式-壹拾万柒仟(应拦截)", Config.STRICT);
        testIllegal("壹万壹元整", "宽容模式-壹万壹元(应拦截)", Config.DEFAULT);

        System.out.println("\n===== 8位分段万位零 =====");
        System.out.println("107000.53 8位宽容: " + toText(new BigDecimal("107000.53"), Config.EIGHT_SEG));
        System.out.println("107000.53 8位严格: " + toText(new BigDecimal("107000.53"), Config.EIGHT_SEG.withWanZeroTolerant(false)));
    }

    private static void testIllegal(String input, String label) {
        testIllegal(input, label, Config.DEFAULT);
    }
    private static void testIllegal(String input, String label, Config config) {
        try {
            toAmount(input, config);
            System.out.println(label + ": 未拦截,异常!");
        } catch (AmountParseException e) {
            System.out.println(label + ": 拦截成功 - " + e.getMessage());
        }
    }
    // endregion
}

增强包装器 Ver.2

java 复制代码
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;

/**
 * 人民币金额大写增强包装器
 * <p>采用 Wrapper 模式,严格遵循开闭原则,仅做兼容增强,不侵入核心转换逻辑。
 *
 * <h3>增强能力</h3>
 * <ul>
 *   <li>小写汉字归一化:支持一/二/三等小写数字、十/百/千等小写单位自动转为大写</li>
 *   <li>复合单位归一化:万亿/万万亿自动归一为兆/京(4位分段模式下)</li>
 *   <li>友好异常转换:将核心类结构化异常转为用户友好的提示文案</li>
 *   <li>往返校验:解析后重新生成大写,与原输入比对,保障语义一致</li>
 *   <li>合法性校验:一键判断输入是否为合规的大写金额</li>
 * </ul>
 *
 * <p>线程安全:全静态无状态设计,可并发调用。
 *
 * @since JDK 1.8
 */
public final class ChineseCurrencyEnhancerV2 {
    /**
     * 私有构造:禁止实例化工具类
     */
    private ChineseCurrencyEnhancerV2() {
        throw new UnsupportedOperationException("工具类禁止实例化");
    }

    // region ========== 兼容映射表 ==========
    /**
     * 单字符替换映射:小写数字、单位转大写
     */
    private static final char[][] CHAR_REPLACE_MAPPING = {
            {'圆', '元'},
            {'〇', '零'}, {'一', '壹'}, {'二', '贰'}, {'三', '叁'},
            {'四', '肆'}, {'五', '伍'}, {'六', '陆'}, {'七', '柒'},
            {'八', '捌'}, {'九', '玖'},
            {'十', '拾'}, {'百', '佰'}, {'千', '仟'}
    };

    /**
     * 正则替换规则:复杂句式归一化
     */
    private static final String[][] REGEX_REPLACE_RULES = {
            {"^拾(?=[万亿兆京兆垓元角分])", "壹拾"},
            {"两(?=[拾佰仟万亿元角分])", "贰"},
            {"(?<=[壹贰叁肆伍陆柒捌玖拾佰仟])万万亿", "京"},
            {"(?<=[壹贰叁肆伍陆柒捌玖拾佰仟])万亿", "兆"}
    };

    /**
     * 单位异常口语化映射:异常提示中大额单位转常用表述
     */
    private static final String[][] UNIT_EXCEPTION_MAPPING = {
            {"兆", "万亿"}, {"京", "万万亿"}, {"垓", "万京"}
    };

    /** 错误码:与核心类严格对应 */
    private static final String ERR_PARAM  = "ERR_PARAM";
    private static final String ERR_FORMAT = "ERR_FORMAT";
    private static final String ERR_UNIT   = "ERR_UNIT";
    private static final String ERR_RANGE  = "ERR_RANGE";

    /**
     * 友好异常模板:命名占位符与核心类实体键名严格对齐
     */
    private static final Map<String, String> FRIENDLY_TEMPLATES;
    static {
        Map<String, String> map = new HashMap<>();
        map.put(ERR_PARAM, "金额不能为空");
        map.put(ERR_FORMAT, "输入格式不符合规范");
        map.put(ERR_UNIT, "单位格式不符合规范");
        map.put(ERR_RANGE, "金额超出支持范围");
        FRIENDLY_TEMPLATES = Collections.unmodifiableMap(map);
    }
    // endregion

    // region ========== 预编译常量 ==========
    /**
     * 预编译正则规则列表
     */
    private static final List<Map.Entry<Pattern, String>> REGEX_RULES;
    static {
        List<Map.Entry<Pattern, String>> list = new ArrayList<>(REGEX_REPLACE_RULES.length);
        for (String[] rule : REGEX_REPLACE_RULES) {
            list.add(new AbstractMap.SimpleEntry<>(
                    Pattern.compile(rule[0]), rule[1]));
        }
        REGEX_RULES = Collections.unmodifiableList(list);
    }

    private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
    private static final Pattern CMP_PREFIX_PATTERN = Pattern.compile("^人民币");
    private static final Pattern CMP_SUFFIX_PATTERN = Pattern.compile("[整正]$");
    // endregion

    // region ========== 正向转换透传 ==========
    /**
     * 数字转人民币大写(默认配置)
     * @param amount 金额数值
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount) {
        return ChineseCurrencyConverterV2.toText(amount);
    }

    /**
     * 数字转人民币大写(可配置零元前缀)
     * @param amount 金额数值
     * @param stripZeroYuan 纯小数是否省略零元前缀
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, boolean stripZeroYuan) {
        return ChineseCurrencyConverterV2.toText(amount, stripZeroYuan);
    }

    /**
     * 数字转人民币大写(可配置零元前缀+拓展模式)
     * @param amount 金额数值
     * @param stripZeroYuan 纯小数是否省略零元前缀
     * @param extended 是否启用拓展大额单位
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, boolean stripZeroYuan, boolean extended) {
        return ChineseCurrencyConverterV2.toText(amount, stripZeroYuan, extended);
    }

    /**
     * 数字转人民币大写(全参数配置)
     * @param amount 金额数值
     * @param stripZeroYuan 纯小数是否省略零元前缀
     * @param extended 是否启用拓展大额单位
     * @param wanTolerant 是否启用万位零宽容
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, boolean stripZeroYuan,
                                boolean extended, boolean wanTolerant) {
        return ChineseCurrencyConverterV2.toText(amount, stripZeroYuan, extended, wanTolerant);
    }

    /**
     * 数字转人民币大写(配置驱动入口)
     * @param amount 金额数值
     * @param config 转换配置
     * @return 大写金额字符串
     * @throws IllegalArgumentException 参数为空或超出范围时抛出
     */
    public static String toText(BigDecimal amount, ChineseCurrencyConverterV2.Config config) {
        return ChineseCurrencyConverterV2.toText(amount, config);
    }
    // endregion

    // region ========== 反向增强转换 ==========
    /**
     * 大写金额转数值(默认配置,无往返校验)
     * @param chinese 大写金额字符串
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
     */
    public static BigDecimal toAmount(String chinese) {
        return toAmount(chinese, false, false);
    }

    /**
     * 大写金额转数值(可配置拓展模式,无往返校验)
     * @param chinese 大写金额字符串
     * @param extendedMode 是否启用拓展大额单位
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
     */
    public static BigDecimal toAmount(String chinese, boolean extendedMode) {
        return toAmount(chinese, extendedMode, false);
    }

    /**
     * 大写金额转数值(可配置拓展模式与往返校验)
     * @param chinese 大写金额字符串
     * @param extendedMode 是否启用拓展大额单位
     * @param roundTripVerify 是否开启往返语义校验
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、校验不通过时抛出
     */
    public static BigDecimal toAmount(String chinese, boolean extendedMode, boolean roundTripVerify) {
        return toAmount(chinese, ChineseCurrencyConverterV2.Config.DEFAULT
                .withExtendedMode(extendedMode), roundTripVerify);
    }

    /**
     * 大写金额转数值(配置驱动核心入口)
     * @param chinese 大写金额字符串
     * @param config 转换配置
     * @param roundTripVerify 是否开启往返语义校验
     * @return 金额数值(保留2位小数)
     * @throws IllegalArgumentException 格式错误、单位非法、校验不通过时抛出
     */
    public static BigDecimal toAmount(String chinese,
                                      ChineseCurrencyConverterV2.Config config,
                                      boolean roundTripVerify) {
        String normalized = normalizeInput(chinese, config);
        BigDecimal result;
        try {
            result = ChineseCurrencyConverterV2.toAmount(normalized, config);
        } catch (IllegalArgumentException e) {
            String friendlyMsg = translateException(e, config);
            throw new IllegalArgumentException(friendlyMsg, e);
        }
        if (roundTripVerify) {
            verifyRoundTrip(normalized, result, config);
        }
        return result;
    }

    /**
     * 校验大写金额是否合法(默认配置)
     * @param chinese 大写金额字符串
     * @return 合法返回true,非法返回false
     */
    public static boolean isValid(String chinese) {
        return isValid(chinese, false);
    }

    /**
     * 校验大写金额是否合法(可配置拓展模式)
     * @param chinese 大写金额字符串
     * @param extendedMode 是否启用拓展大额单位
     * @return 合法返回true,非法返回false
     */
    public static boolean isValid(String chinese, boolean extendedMode) {
        return isValid(chinese, ChineseCurrencyConverterV2.Config.DEFAULT
                .withExtendedMode(extendedMode));
    }

    /**
     * 校验大写金额是否合法(配置驱动)
     * @param chinese 大写金额字符串
     * @param config 转换配置
     * @return 合法返回true,非法返回false
     */
    public static boolean isValid(String chinese, ChineseCurrencyConverterV2.Config config) {
        try {
            toAmount(chinese, config, true);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    // endregion

    // region ========== 内部核心方法 ==========
    /**
     * 输入归一化:去空格、字符替换、复合单位归一
     * @param raw 原始输入
     * @param config 转换配置
     * @return 归一化后的标准大写字符串
     */
    private static String normalizeInput(String raw, ChineseCurrencyConverterV2.Config config) {
        if (raw == null) return null;
        String result = raw;
        result = WHITESPACE_PATTERN.matcher(result).replaceAll("");
        for (char[] mapping : CHAR_REPLACE_MAPPING) {
            result = result.replace(mapping[0], mapping[1]);
        }
        // 8位模式下禁用复合单位归一(万亿是合法段内写法)
        if (!config.isEightSegmentMode()) {
            for (Map.Entry<Pattern, String> rule : REGEX_RULES) {
                result = rule.getKey().matcher(result).replaceAll(rule.getValue());
            }
        }
        return result;
    }

    /**
     * 异常消息转换:结构化异常转友好提示
     * @param e         原始异常
     * @param config    转换配置项
     * @return 友好异常消息
     */
    private static String translateException(Exception e, ChineseCurrencyConverterV2.Config config) {
        // 优先读取结构化异常,零正则,高可靠
        if (e instanceof ChineseCurrencyConverterV2.AmountParseException) {
            ChineseCurrencyConverterV2.AmountParseException pe = (ChineseCurrencyConverterV2.AmountParseException) e;
            String code = pe.getErrorCode();
            Map<String, String> entities = new HashMap<>(pe.getEntities());

            // 单位名称口语化转换:8位模式下禁用复合单位归一(万亿是合法段内写法)
            if (!config.isEightSegmentMode() && ERR_UNIT.equals(code)) {
                for (Map.Entry<String, String> entry : entities.entrySet()) {
                    String value = entry.getValue();
                    for (String[] mapping : UNIT_EXCEPTION_MAPPING) {
                        value = value.replace(mapping[0], mapping[1]);
                    }
                    entry.setValue(value);
                }
            }

            // 匹配友好模板
            String template = FRIENDLY_TEMPLATES.get(code);
            String friendlyMsg = template != null ? template : pe.getMessage();

            // 填充命名实体
            for (Map.Entry<String, String> entry : entities.entrySet()) {
                friendlyMsg = friendlyMsg.replace("{" + entry.getKey() + "}", entry.getValue());
            }

            // 追加位置信息
            if (pe.getPosition() >= 0) {
                friendlyMsg += "(第" + (pe.getPosition() + 1) + "个字附近)";
            }
            return friendlyMsg;
        }

        // Fallback:非结构化异常直接返回原消息
        return e.getMessage();
    }

    /**
     * 往返语义校验:解析后重新生成大写,与原输入比对
     * @param original 原始归一化输入
     * @param value 解析出的数值
     * @param config 转换配置
     * @throws IllegalArgumentException 校验不通过时抛出
     */
    private static void verifyRoundTrip(String original, BigDecimal value,
                                        ChineseCurrencyConverterV2.Config config) {
        String roundTrip = ChineseCurrencyConverterV2.toText(value,
                config.withStripLeadingZeroYuan(false));
        String src = normalizeForCompare(original);
        String target = normalizeForCompare(roundTrip);
        if (!src.equals(target)) {
            throw new IllegalArgumentException("金额语义校验不通过,解析结果与原输入不一致");
        }
    }

    /**
     * 归一化对比字符串:移除前缀、后缀,统一对比基准
     * @param text 原始文本
     * @return 归一化后的对比文本
     */
    private static String normalizeForCompare(String text) {
        String result = text;
        result = CMP_PREFIX_PATTERN.matcher(result).replaceAll("");
        result = CMP_SUFFIX_PATTERN.matcher(result).replaceAll("");
        return result;
    }
    // endregion

    /**
     * 本地测试入口
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        System.out.println("===== 小写兼容测试 =====");
        System.out.println("一千二百三十四万五千六百七十八元五角: "
                + toAmount("一千二百三十四万五千六百七十八元五角",
                ChineseCurrencyConverterV2.Config.EIGHT_SEG, false));
        System.out.println("一千二百三十四万五千六百七十八元〇五分: "
                + toAmount("一千二百三十四万五千六百七十八元〇五分",
                ChineseCurrencyConverterV2.Config.EIGHT_SEG, false));
    }
}