| | |
| | | validateTimeslotNotDuplicated(createReqVO.getCheckType(), createReqVO.getTimeSlot(), null); |
| | | // 插入 |
| | | QueueSequenceDO queueSequence = BeanUtils.toBean(createReqVO, QueueSequenceDO.class); |
| | | if (null == queueSequence.getTimeSlot() || 0 != queueSequence.getTimeSlot()) { |
| | | // 【本需求】号段起点 / 预留起点由系统计算,不接受前端随意传值(否则号段会重叠) |
| | | queueSequence.setQueueSet(null); |
| | | queueSequence.setQueueVipSet(null); |
| | | } |
| | | queueSequenceMapper.insert(queueSequence); |
| | | // 【本需求】新增/编辑后统一重排该检查类型的号段(保证"段与段首尾相接") |
| | | if (null != createReqVO.getCheckType()) { |
| | | recalcSegmentBounds(createReqVO.getCheckType()); |
| | | } |
| | | // 返回 |
| | | return queueSequence.getId(); |
| | | } |
| | |
| | | updateReqVO.getId()); |
| | | // 更新 |
| | | QueueSequenceDO updateObj = BeanUtils.toBean(updateReqVO, QueueSequenceDO.class); |
| | | // 【本需求】起始序号由系统重排,忽略前端传值(前端已置为只读) |
| | | updateObj.setQueueSet(null); |
| | | updateObj.setQueueVipSet(null); |
| | | queueSequenceMapper.updateById(updateObj); |
| | | if (null != updateReqVO.getCheckType()) { |
| | | recalcSegmentBounds(updateReqVO.getCheckType()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 【本需求】按「时段升序」重排某检查类型的号段: |
| | | * <pre> |
| | | * 段 i:queue_set = 上一段的 queue_vip_full(首段 0) |
| | | * queue_full = queue_set + P |
| | | * queue_vip_set = queue_full |
| | | * queue_vip_full = queue_vip_set + V |
| | | * 其中 P / V = 该行当前的(queue_full - queue_set)、(queue_vip_full - queue_full), |
| | | * 即各段的"额度"保持不变,只把起点串起来。 |
| | | * </pre> |
| | | * 这样无论新增/删除/编辑时段,都不会出现号段重叠或空洞错位。 |
| | | * <p>总预留池(time_slot = 0)不参与重排,只把起点对齐到最后一段之后。 |
| | | */ |
| | | private void recalcSegmentBounds(Integer checkType) { |
| | | if (null == checkType) { |
| | | return; |
| | | } |
| | | List<QueueSequenceDO> all = queueSequenceMapper.selectTimeslotByCheckType(checkType); |
| | | if (null == all || all.isEmpty()) { |
| | | return; |
| | | } |
| | | all.sort(Comparator.comparingInt(r -> null == r.getTimeSlot() ? 0 : r.getTimeSlot())); |
| | | |
| | | CheckTypeDO ct = checkTypeMapper.getCheckTypeByValue(checkType); |
| | | |
| | | int cursor = 0; |
| | | int slotCount = 0; |
| | | for (QueueSequenceDO row : all) { |
| | | int slot = null == row.getTimeSlot() ? 0 : row.getTimeSlot(); |
| | | if (0 == slot) { |
| | | continue; // 总预留池最后单独处理 |
| | | } |
| | | slotCount++; |
| | | |
| | | // 额度(宽):优先沿用该行现有号段宽度;未知/非正时回退到检查类型配置 |
| | | int p = width(row.getQueueSet(), row.getQueueFull()); |
| | | if (p <= 0) { |
| | | p = (null == ct || null == ct.getTimeslotBookNum()) ? 0 : ct.getTimeslotBookNum(); |
| | | } |
| | | int v = width(row.getQueueFull(), row.getQueueVipFull()); |
| | | if (v < 0) { |
| | | v = (null == ct || null == ct.getTimeslotReservedNum()) ? 0 : ct.getTimeslotReservedNum(); |
| | | } |
| | | |
| | | QueueSequenceDO update = new QueueSequenceDO(); |
| | | update.setId(row.getId()); |
| | | update.setQueueSet(cursor); |
| | | update.setQueueFull(cursor + p); |
| | | update.setQueueVipSet(cursor + p); |
| | | update.setQueueVipFull(cursor + p + v); |
| | | // 游标只**上抬**到号段起点,绝不**下调**: |
| | | // 下调会让已发出的号被再次发出(重号)。若游标已超过上限,说明该段已超发, |
| | | // 取号时自然会落到「预留号 → 总预留池」。 |
| | | update.setQueueNo(raiseAtLeast(row.getQueueNo(), cursor)); |
| | | update.setQueueVipNo(raiseAtLeast(row.getQueueVipNo(), cursor + p)); |
| | | queueSequenceMapper.updateById(update); |
| | | |
| | | cursor = cursor + p + v; |
| | | } |
| | | |
| | | // 总预留池:起点对齐到最后一段之后 |
| | | QueueSequenceDO pool = queueSequenceMapper.selectTotalPool(checkType); |
| | | if (null != pool) { |
| | | int poolFull = Math.max(null == pool.getQueueVipFull() ? 0 : pool.getQueueVipFull(), cursor); |
| | | QueueSequenceDO update = new QueueSequenceDO(); |
| | | update.setId(pool.getId()); |
| | | update.setQueueVipSet(cursor); |
| | | update.setQueueVipFull(poolFull); |
| | | update.setQueueVipNo(raiseAtLeast(pool.getQueueVipNo(), cursor)); |
| | | queueSequenceMapper.updateById(update); |
| | | } |
| | | log.info("[recalcSegmentBounds] 已重排号段. checkType={} 段数={} 末尾最大号={}", |
| | | checkType, slotCount, cursor); |
| | | } |
| | | |
| | | /** 号段宽度(终止 - 起始),任一为 null 返回 -1 表示"未知" */ |
| | | private int width(Integer from, Integer to) { |
| | | if (null == from || null == to) { |
| | | return -1; |
| | | } |
| | | return to - from; |
| | | } |
| | | |
| | | /** |
| | | * 游标只上抬、不下调(下调会导致已发出的号被再次发出 ⇒ 重号)。 |
| | | */ |
| | | private Integer raiseAtLeast(Integer value, int min) { |
| | | int v = null == value ? min : value; |
| | | return Math.max(v, min); |
| | | } |
| | | |
| | | /** |
| | |
| | | @Override |
| | | public void deleteQueueSequence(Integer id) { |
| | | // 校验存在 |
| | | validateQueueSequenceExists(id); |
| | | QueueSequenceDO exist = queueSequenceMapper.selectById(id); |
| | | if (null == exist) { |
| | | throw exception(QUEUE_SEQUENCE_NOT_EXISTS); |
| | | } |
| | | // 删除 |
| | | queueSequenceMapper.deleteById(id); |
| | | // 【本需求】删除后重排号段(否则后续时段的起点会留空洞) |
| | | recalcSegmentBounds(exist.getCheckType()); |
| | | } |
| | | |
| | | private void validateQueueSequenceExists(Integer id) { |
| | |
| | | @Override |
| | | @Transactional |
| | | public Integer distributeSeqNo(Integer checkType, Integer timeslot) { |
| | | // 【本需求:按时段预约】普通患者三级取号,**不再跨段借号**: |
| | | // ① 本时段普通号(queue_no < queue_full) |
| | | // ② 本时段预留号(queue_vip_no < queue_vip_full) |
| | | // ③ 总预留池(time_slot=0,**不设上限**) |
| | | // 任何一级成功即返回;③ 永不失败 ⇒ 不存在"签不上"。 |
| | | // 无预约时段(timeslot 为 null/0/字典外的值)时 ①② 自然查不到行,直接落到 ③。 |
| | | |
| | | // ① 本段普通号 |
| | | Integer curSeqNo = queueSequenceMapper.selectQueueNoForUpdate(checkType, timeslot); |
| | | if (null == curSeqNo) { |
| | | // 【修复「时段不全/已满导致没法签到」】 |
| | | // 原逻辑:只取"字典里的紧邻下一个时段",且要求该时段在 queue_sequence 中存在。 |
| | | // 现实问题(实测): |
| | | // · 字典 ecg_book_timeslot 有 21 个时段,但 queue_sequence 中 checkType=400 |
| | | // 只有 13 个、checkType=100 只有 17 个 —— 部分时段根本没有号池行; |
| | | // · HIS 会下发 60 分钟粒度的 bookTimeslot(如 10001100),字典里没有该值, |
| | | // getNextTimeSlot 直接 return null; |
| | | // · 个别时段容量很小(平板运动 timeslot_book_num=3),很快销完。 |
| | | // 三者任一命中 → 抛 QUEUE_SEQUENCE_TIMESLOT_FULL → **患者没法签到**。 |
| | | // |
| | | // 现在改为:**向后最多找 N 个时段,取第一个仍有余量的**(下面 findAvailableTimeslot 内实现), |
| | | // 大幅降低"因个别时段缺失/已满就签到失败"的概率;确实都满了才抛错。 |
| | | Integer nextTimeslot = findAvailableTimeslot(checkType, timeslot, false); |
| | | if (null == nextTimeslot) { |
| | | log.error("[distributeSeqNo] 该检查类型向后 {} 个时段内均无可取号时段. checkType={} timeslot={}", |
| | | MAX_TIMESLOT_LOOKAHEAD, checkType, timeslot); |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_FULL); |
| | | if (null != curSeqNo) { |
| | | Integer updateNum = queueSequenceMapper.updateGivenCheckTypeTimeslotSeqNo(checkType, timeslot, curSeqNo); |
| | | if (null != updateNum && updateNum > 0) { |
| | | return curSeqNo + 1; |
| | | } |
| | | |
| | | curSeqNo = queueSequenceMapper.selectQueueNoForUpdate(checkType, nextTimeslot); |
| | | if (null == curSeqNo) { |
| | | // 探测与取号之间被并发抢空,保守报错(由前端提示重新签到) |
| | | log.warn("[distributeSeqNo] 探测到的时段随即被占满. checkType={} timeslot={}", |
| | | checkType, nextTimeslot); |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_FULL); |
| | | } |
| | | |
| | | Integer updateRowNum = queueSequenceMapper.updateGivenCheckTypeTimeslotSeqNo(checkType, nextTimeslot, curSeqNo); |
| | | if (null == updateRowNum || updateRowNum == 0) { |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_EXCEPTION); |
| | | } |
| | | |
| | | } else { |
| | | Integer updateRowNum = queueSequenceMapper.updateGivenCheckTypeTimeslotSeqNo(checkType, timeslot, curSeqNo); |
| | | if (null == updateRowNum || updateRowNum == 0) { |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_EXCEPTION); |
| | | } |
| | | // 并发被抢空 → 落到 ② |
| | | log.info("[distributeSeqNo] 本段普通号并发被抢空,降级到预留号. checkType={} timeslot={}", checkType, timeslot); |
| | | } |
| | | |
| | | return curSeqNo + 1; |
| | | } |
| | | // ② 本段预留号 |
| | | Integer curVipNo = queueSequenceMapper.selectQueueVipNoForUpdate(checkType, timeslot); |
| | | if (null != curVipNo) { |
| | | Integer updateNum = queueSequenceMapper.updateGivenCheckTypeTimeslotVipSeqNo(checkType, timeslot, curVipNo); |
| | | if (null != updateNum && updateNum > 0) { |
| | | log.info("[distributeSeqNo] 普通号已满,改发本段预留号. checkType={} timeslot={} seqNo={}", |
| | | checkType, timeslot, curVipNo + 1); |
| | | return curVipNo + 1; |
| | | } |
| | | log.info("[distributeSeqNo] 本段预留号并发被抢空,降级到总预留池. checkType={} timeslot={}", checkType, timeslot); |
| | | } |
| | | |
| | | // ③ 总预留池(不设上限,保证一定能签到) |
| | | return nextTotalPoolSeqNo(checkType, timeslot); |
| | | } |
| | | /** |
| | | * 【本需求】VIP / 临时加号患者取号:**直接取本段预留号**;本段预留也满 → 总预留池。 |
| | | * <p> |
| | | * 与普通通道的差别只在于"跳过本段普通号",其余(预留号、总池兜底)完全一致。 |
| | | */ |
| | | @Override |
| | | @Transactional |
| | | public Integer distributeVipSeqNo(Integer checkType, Integer timeslot) { |
| | | Integer curVipSeqNo = queueSequenceMapper.selectQueueVipNoForUpdate(checkType, timeslot); |
| | | if (null == curVipSeqNo) { |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_FULL); |
| | | if (null != curVipSeqNo) { |
| | | Integer updateRowNum = queueSequenceMapper.updateGivenCheckTypeTimeslotVipSeqNo(checkType, timeslot, curVipSeqNo); |
| | | if (null != updateRowNum && updateRowNum > 0) { |
| | | return curVipSeqNo + 1; |
| | | } |
| | | log.info("[distributeVipSeqNo] 本段预留号并发被抢空,降级到总预留池. checkType={} timeslot={}", checkType, timeslot); |
| | | } |
| | | return nextTotalPoolSeqNo(checkType, timeslot); |
| | | } |
| | | |
| | | /** |
| | | * 【本需求】总预留池取号({@code time_slot = 0} 那一行)—— **永不失败**。 |
| | | * |
| | | * <h3>为什么不会满</h3> |
| | | * 总池的 {@code queue_vip_full} 在发号时同步"顶高"(见 Mapper 的 |
| | | * {@code advanceTotalPoolVipNo}),因此 {@code queue_vip_no} 永远可以继续 +1。 |
| | | * 这是"绝不出现签不上"的最后一道兜底。 |
| | | * |
| | | * <h3>总池行缺失时自动补</h3> |
| | | * 号段放在当天所有时段的最大号之后({@code max(queue_vip_full)}), |
| | | * 保证总池号 **大于所有时段号** ⇒ 排序天然排在全天最后。 |
| | | */ |
| | | private Integer nextTotalPoolSeqNo(Integer checkType, Integer timeslot) { |
| | | Integer curVipNo = null; |
| | | try { |
| | | curVipNo = queueSequenceMapper.selectTotalPoolVipNo(checkType); |
| | | } catch (Exception e) { |
| | | log.warn("[nextTotalPoolSeqNo] 读取总预留池异常,按缺失处理. checkType={}", checkType, e); |
| | | } |
| | | |
| | | Integer updateRowNum = queueSequenceMapper.updateGivenCheckTypeTimeslotVipSeqNo(checkType, timeslot, curVipSeqNo); |
| | | if (null == updateRowNum || updateRowNum == 0) { |
| | | if (null == curVipNo) { |
| | | // 现场兜底:自动补一行总预留池(幂等:以 检查类型 + time_slot=0 唯一) |
| | | Integer base = queueSequenceMapper.selectMaxVipFullOfCheckType(checkType); |
| | | if (null == base) { |
| | | base = 0; |
| | | } |
| | | QueueSequenceDO pool = new QueueSequenceDO(); |
| | | pool.setCheckType(checkType); |
| | | pool.setTimeSlot(0); |
| | | pool.setQueueSet(0); |
| | | pool.setQueueNo(0); |
| | | pool.setQueueFull(0); |
| | | pool.setQueueVipSet(base); |
| | | pool.setQueueVipNo(base); |
| | | pool.setQueueVipFull(base); |
| | | try { |
| | | queueSequenceMapper.insert(pool); |
| | | } catch (Exception e) { |
| | | // 并发下可能已被别的线程插入,忽略后继续读 |
| | | log.warn("[nextTotalPoolSeqNo] 补总预留池行失败(可能并发已插入),继续取号. checkType={}", checkType, e); |
| | | } |
| | | if (null == queueSequenceMapper.selectTotalPoolVipNo(checkType)) { |
| | | // 插入后仍读不到(极端情况)→ 用当天最大号兜底,保证能签到 |
| | | Integer maxFull = queueSequenceMapper.selectMaxVipFullOfCheckType(checkType); |
| | | log.error("[nextTotalPoolSeqNo] 总预留池不可用,使用最大号兜底. checkType={} maxFull={}", checkType, maxFull); |
| | | return (null == maxFull ? 0 : maxFull) + 1; |
| | | } |
| | | } |
| | | |
| | | Integer updateNum = queueSequenceMapper.advanceTotalPoolVipNo(checkType); |
| | | if (null == updateNum || updateNum == 0) { |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_EXCEPTION); |
| | | } |
| | | |
| | | return curVipSeqNo + 1; |
| | | Integer after = queueSequenceMapper.selectTotalPoolVipNo(checkType); |
| | | if (null == after) { |
| | | throw exception(QUEUE_SEQUENCE_TIMESLOT_EXCEPTION); |
| | | } |
| | | log.info("[nextTotalPoolSeqNo] 已从总预留池取号(本段已满或没有时段). checkType={} timeslot={} seqNo={}", |
| | | checkType, timeslot, after); |
| | | return after; |
| | | } |
| | | |
| | | /** |
| | |
| | | @Transactional |
| | | public Integer distributeSeqNoByRule(Integer checkType, Integer timeslot, |
| | | Integer existingSeqNo, boolean urgent) { |
| | | // 兼容旧调用方:未传 VIP 标记时按"非 VIP"处理 |
| | | return distributeSeqNoByRule(checkType, timeslot, existingSeqNo, urgent, false); |
| | | } |
| | | |
| | | /** |
| | | * 【本需求】带 VIP/加号标记的取号入口。 |
| | | * <p> |
| | | * 与四参版本的唯一差别:按时段预约(TIMESLOT)下,VIP/加号患者**直接取本段预留号** |
| | | * (见 {@link #distributeVipSeqNo}),而不是先取普通号;签到序号(CHECKIN)下 |
| | | * VIP/加号同样可占用预留号(每 9 留 1 的预留位)。 |
| | | */ |
| | | @Override |
| | | @Transactional |
| | | public Integer distributeSeqNoByRule(Integer checkType, Integer timeslot, |
| | | Integer existingSeqNo, boolean urgent, boolean vip) { |
| | | SeqRuleEnum rule = resolveRule(checkType); |
| | | |
| | | switch (rule) { |
| | |
| | | } |
| | | log.warn("[distributeSeqNoByRule] 规则=预约号,但预约系统未下发号,回退到签到序号. checkType={}", |
| | | checkType); |
| | | return nextCheckInSeqNo(checkType, urgent); |
| | | return nextCheckInSeqNo(checkType, urgent || vip); |
| | | |
| | | case CHECKIN: |
| | | // ② 签到序号:当天同检查类型连续递增,每 9 个预留 1 个 |
| | | return nextCheckInSeqNo(checkType, urgent); |
| | | return nextCheckInSeqNo(checkType, urgent || vip); |
| | | |
| | | case TIMESLOT: |
| | | default: |
| | | // ③ 时段号:保持改造前的行为(含时段容量控制) |
| | | // ③ 时段号:VIP/加号走「本段预留号 → 总预留池」;普通走「普通 → 预留 → 总池」 |
| | | if (vip) { |
| | | return distributeVipSeqNo(checkType, timeslot); |
| | | } |
| | | return distributeSeqNo(checkType, timeslot); |
| | | } |
| | | } |