yxh
7 天以前 68ff33beff11310e830b0615b5a81f83fad0f683
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package cn.lihu.jh.module.ecg.enums;
 
import org.junit.Test;
 
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
 
import static org.junit.Assert.*;
 
/**
 * 回归测试:排队序号规则枚举 {@link SeqRuleEnum}。
 *
 * <h3>背景</h3>
 * 「签到时如何给患者排队序号」按检查项目配置,三种规则:
 * <ol>
 *   <li>预约号 —— 用预约系统下发的号</li>
 *   <li>签到序号 —— 当天同检查类型内连续递增,<b>每 9 个号预留 1 个给加急</b></li>
 *   <li>时段号 —— 按预约时间段给号(改造前的原有行为,也是未配置时的默认)</li>
 * </ol>
 *
 * <h3>本测试锁定的关键行为</h3>
 * <ul>
 *   <li>{@code parse(null)} 回退到 {@code TIMESLOT} —— 存量数据 {@code seq_rule} 为 NULL 时
 *       升级后**行为与改造前一致**;</li>
 *   <li>普通号序列跳过 10 的倍数(预留号),且**不跳号、不重号**;</li>
 *   <li>预留号序列 = 10 的倍数;</li>
 *   <li>两类号集合**不相交**(这是"两个独立计数器"设计的目的)。</li>
 * </ul>
 *
 * <h3>曾捕获的真实缺陷(保留记录)</h3>
 * 初版把「分配号」写成 {@code nextNormalSeq(++counter)} 并期望它在 10 时跳到 11:
 * <pre>
 *   counter: 9 → 分配 9
 *   counter: 10 → nextNormalSeq(10) 期望 11?(错)
 *   counter: 11 → 分配 11            ← 与上一行撞车
 * </pre>
 * 正确语义是:**分配号 = {@code nextNormalSeq(counter)},且计数器在"下一个号是预留号"时额外 +1**,
 * 即 {@code normal_next = normal_next + 1 + (normal_next + 1 是 10 的倍数 ? 1 : 0)}。
 */
public class SeqRuleEnumTest {
 
    // ---------------------------------------------------------------- 解析
 
    @Test
    public void testParse_null_shouldFallbackToTimeslot() {
        assertEquals(SeqRuleEnum.TIMESLOT, SeqRuleEnum.parse(null));
    }
 
    @Test
    public void testParse_unknownValue_shouldFallbackToTimeslot() {
        assertEquals(SeqRuleEnum.TIMESLOT, SeqRuleEnum.parse(99));
        assertEquals(SeqRuleEnum.TIMESLOT, SeqRuleEnum.parse(-1));
    }
 
    @Test
    public void testParse_knownValues() {
        assertEquals(SeqRuleEnum.APPOINT, SeqRuleEnum.parse(0));
        assertEquals(SeqRuleEnum.CHECKIN, SeqRuleEnum.parse(1));
        assertEquals(SeqRuleEnum.TIMESLOT, SeqRuleEnum.parse(2));
    }
 
    // ------------------------------------------------------- 容量语义差异
 
    @Test
    public void testIsCapacityLimited_onlyTimeslot() {
        assertFalse(SeqRuleEnum.APPOINT.isCapacityLimited());
        assertFalse(SeqRuleEnum.CHECKIN.isCapacityLimited());
        assertTrue(SeqRuleEnum.TIMESLOT.isCapacityLimited());
    }
 
    // ---------------------------------------------------------- 预留号判定
 
    @Test
    public void testIsReservedSeq() {
        assertTrue(SeqRuleEnum.isReservedSeq(10));
        assertTrue(SeqRuleEnum.isReservedSeq(20));
        assertTrue(SeqRuleEnum.isReservedSeq(100));
        assertFalse(SeqRuleEnum.isReservedSeq(1));
        assertFalse(SeqRuleEnum.isReservedSeq(9));
        assertFalse(SeqRuleEnum.isReservedSeq(11));
        assertFalse(SeqRuleEnum.isReservedSeq(0));
        assertFalse(SeqRuleEnum.isReservedSeq(-10));
    }
 
    @Test
    public void testRoundSize_matchesReservedRule() {
        assertEquals(10, SeqRuleEnum.ROUND_SIZE);
        assertEquals(1, SeqRuleEnum.RESERVED_PER_ROUND);
        // 「每 9 个号预留 1 个」= 9 普通 + 1 预留 = 一轮 10 个
        assertEquals(SeqRuleEnum.ROUND_SIZE, 9 + SeqRuleEnum.RESERVED_PER_ROUND);
    }
 
    // -------------------------------------------------------- 普通号序列
 
    @Test
    public void testNextNormalSeq_shouldReturnNonReservedValue() {
        // 直接语义:返回 from 起第一个非预留号(10 本身是预留号 → 返回 11)
        assertEquals(1, SeqRuleEnum.nextNormalSeq(1));
        assertEquals(9, SeqRuleEnum.nextNormalSeq(9));
        assertEquals("10 是预留号,应跳到 11", 11, SeqRuleEnum.nextNormalSeq(10));
        assertEquals(11, SeqRuleEnum.nextNormalSeq(11));
        assertEquals(19, SeqRuleEnum.nextNormalSeq(19));
        assertEquals(21, SeqRuleEnum.nextNormalSeq(20));
        assertEquals("99 本身不是预留号,原样返回", 99, SeqRuleEnum.nextNormalSeq(99));
        assertEquals("100 是预留号,跳到 101", 101, SeqRuleEnum.nextNormalSeq(100));
    }
 
    /**
     * 端到端模拟 Mapper 的推进公式,验证**连续 30 次普通取号不重号、不跳号**。
     *
     * <p>公式与 {@code QueueSequenceMapper#advanceNormalSeq} 完全一致:
     * <pre>
     *   INSERT 时 normal_next = 1
     *   UPDATE 时 normal_next = normal_next + 1 + IF((normal_next + 1) % 10 = 0, 1, 0)
     *   分配号 = nextNormalSeq(normal_next - 上次推进量)? —— 不对,
     *   实际是:分配号由「读回后的 normal_next」经 nextNormalSeq 得到
     * </pre>
     * 这里用最直白的方式还原:维护 {@code lastAssigned},每次
     * {@code next = lastAssigned + 1},若是预留号则 {@code next++}(跳过),
     * 然后 {@code lastAssigned = next}。
     */
    @Test
    public void testNormalSequence_isContiguousAndSkipsReserved() {
        List<Integer> assigned = new ArrayList<>();
        int lastAssigned = 0;
        for (int i = 0; i < 30; i++) {
            int next = lastAssigned + 1;
            next = SeqRuleEnum.nextNormalSeq(next);   // 跳过预留号
            assigned.add(next);
            lastAssigned = next;
        }
 
        // 期望:1..9, 11..19, 21..29, 31..39 的前 30 个
        int[] expected = {
                1, 2, 3, 4, 5, 6, 7, 8, 9,
                11, 12, 13, 14, 15, 16, 17, 18, 19,
                21, 22, 23, 24, 25, 26, 27, 28, 29,
                31, 32, 33
        };
        for (int i = 0; i < expected.length; i++) {
            assertEquals("第 " + (i + 1) + " 个普通号", expected[i], (int) assigned.get(i));
        }
 
        // 无重号
        Set<Integer> uniq = new HashSet<>(assigned);
        assertEquals("普通号不得重复", assigned.size(), uniq.size());
 
        // 无 10 的倍数
        for (Integer v : assigned) {
            assertFalse("普通号不应落在预留号上: " + v, SeqRuleEnum.isReservedSeq(v));
        }
    }
 
    @Test
    public void testNextNormalSeq_withZeroOrNegative_shouldStartFromOne() {
        assertEquals(1, SeqRuleEnum.nextNormalSeq(0));
        assertEquals(1, SeqRuleEnum.nextNormalSeq(-5));
    }
 
    // -------------------------------------------------------- 预留号序列
 
    @Test
    public void testNextReservedSeq_shouldLandOnMultiplesOfTen() {
        assertEquals(10, SeqRuleEnum.nextReservedSeq(1));
        assertEquals(10, SeqRuleEnum.nextReservedSeq(9));
        assertEquals("已是预留号则原样返回", 10, SeqRuleEnum.nextReservedSeq(10));
        assertEquals(20, SeqRuleEnum.nextReservedSeq(11));
        assertEquals(20, SeqRuleEnum.nextReservedSeq(19));
        assertEquals(20, SeqRuleEnum.nextReservedSeq(20));
        assertEquals(30, SeqRuleEnum.nextReservedSeq(21));
    }
 
    @Test
    public void testNextReservedSeq_withZeroOrNegative() {
        assertEquals(10, SeqRuleEnum.nextReservedSeq(0));
        assertEquals(10, SeqRuleEnum.nextReservedSeq(-3));
    }
 
    // ------------------------------------------------- 两类号不相交(核心)
 
    @Test
    public void testNormalAndReservedSequences_shouldNotIntersect() {
        // 模拟:100 次普通 + 10 次加急
        Set<Integer> normalSet = new HashSet<>();
        int last = 0;
        for (int i = 0; i < 100; i++) {
            last = SeqRuleEnum.nextNormalSeq(last + 1);
            normalSet.add(last);
        }
 
        Set<Integer> reservedSet = new HashSet<>();
        int reservedNext = 10;
        for (int i = 0; i < 10; i++) {
            reservedSet.add(SeqRuleEnum.nextReservedSeq(reservedNext));
            reservedNext += SeqRuleEnum.ROUND_SIZE;
        }
 
        assertEquals("普通号不得重复", 100, normalSet.size());
        assertEquals("预留号不得重复", 10, reservedSet.size());
 
        Set<Integer> intersection = new HashSet<>(normalSet);
        intersection.retainAll(reservedSet);
        assertTrue("普通号与预留号集合不得有交集,实际交集=" + intersection, intersection.isEmpty());
    }
 
    /**
     * 反证:为什么必须"两个独立计数器"。
     *
     * <p>若只用一个递增计数器、并把"跳过预留号"后的值直接当号码用,
     * 会在 counter=10 与 counter=11 两次分配中得到同一个号 11。
     */
    @Test
    public void testSingleCounterNaiveApproach_wouldCollide_documentingWhyTwoCounters() {
        assertEquals("counter=10 时跳过预留号得到 11", 11, SeqRuleEnum.nextNormalSeq(10));
        assertEquals("counter=11 时直接得到 11", 11, SeqRuleEnum.nextNormalSeq(11));
        assertEquals("两者相同 → 朴素做法必然重号",
                SeqRuleEnum.nextNormalSeq(10), SeqRuleEnum.nextNormalSeq(11));
    }
 
    // ------------------------------------------------------------- 数组
 
    @Test
    public void testArrays_containsAllRules() {
        int[] arrays = SeqRuleEnum.ARRAYS;
        assertEquals(3, arrays.length);
        assertEquals(0, arrays[0]);
        assertEquals(1, arrays[1]);
        assertEquals(2, arrays[2]);
    }
}