fix(#574): 签到状态 BOOKED(1)→CHECKED_IN(3) + 全链路映射修复

根因:checkInTicket() 将签到后状态设为 BOOKED(1) 而非 CHECKED_IN(3)
导致:前端无法识别已签到状态,池统计漏计已签到人数

修复:
- TicketServiceImpl: 签到状态改为 SlotStatus.CHECKED_IN(3)
- TicketAppServiceImpl: 新增 CHECKED_IN→已签到 映射分支
- SchedulePoolMapper: 池统计兼容 BOOKED 和 CHECKED_IN
- outpatientAppointment/index.vue: STATUS_CLASS_MAP + 患者信息条件加上已签到
- AGENTS.md: 写入状态值一致性/禁止删文件/全链路验证铁律
This commit is contained in:
2026-06-02 09:48:51 +08:00
parent 710a215597
commit b8463f4659
5 changed files with 72 additions and 8 deletions

View File

@@ -155,6 +155,50 @@ Harness: .harness/ (init.sh, PROGRESS.md, feature_list.json, ...)
---
## 🚨 铁律(不可违反 — 来自实际 Bug 教训)
### 状态值一致性
涉及状态流转的 Bug修改前**必须**列出完整链路并逐项检查
1. 枚举定义 `SlotStatus``OrderStatus`的数值
2. Service 层设置的状态值是否与枚举一致
3. 查询/列表接口的状态映射是否覆盖所有枚举值
4. 前端 `STATUS_CLASS_MAP` 是否包含新状态
5. 前端过滤条件`v-if``v-for`是否兼容新状态
6. /统计表的聚合 SQL 是否包含新状态值
**禁止**只改一端不检查其他端必须全链路对齐
### 禁止删除源文件
- **绝对禁止**删除项目中已有的 Java/Vue/SQL 源文件
- 编译错误 修复错误不删除文件
- 重复文件 重构合并不删除文件
- AI 幻觉文件 检查 `git ls-tree baseline -- <file>` 确认后再删除
- **唯一例外**人类明确确认删除
### 全链路验证(状态流转 Bug 必做)
修复后按以下顺序验证**编译通过不等于修复完成**
```
① 数据库SELECT status FROM table WHERE id = ? → 确认写入正确
② 后端接口:检查所有 if/switch 分支 → 确认映射正确
③ 前端显示:检查 STATUS_CLASS_MAP → 确认文本正确
④ 前端交互:检查 v-if/v-for/disabled → 确认按钮状态正确
⑤ 统计数据:检查聚合 SQL → 确认统计包含新状态
```
### 禁止修改已有公开方法签名
- 不能删除或重命名已有的 public 方法
- 不能修改已有方法的参数列表
- 需要新功能 添加重载方法
- 需要改行为 修改方法内部实现
### 搜索所有相关代码路径
修复前必须用 `rg` 搜索
```
rg "状态枚举名\|相关方法名\|相关字段名" --type java --type vue
```
确保不遗漏任何引用该状态的代码路径
## 📐 代码风格规范
### Java 后端
@@ -206,6 +250,14 @@ Harness: .harness/ (init.sh, PROGRESS.md, feature_list.json, ...)
---
## 📈 过往 Bug 教训
| Bug | 教训 |
|---|---|
| #574 | `checkInTicket()` 状态值写错BOOKED应为CHECKED_IN前端映射缺失池统计漏计根因没走完整状态链路 |
| #574 | AI 智能体看到编译错误直接删文件没检查 git baseline根因没验证文件来源 |
| #574 | 多次 fallback 修复改错文件OrderServiceImpl没触及真正问题TicketServiceImpl)。根因没用 rg 搜索所有引用 |
## 📈 成熟度追踪
| 等级 | 特征 | 本项目 |

View File

@@ -207,6 +207,12 @@ public class TicketAppServiceImpl implements ITicketAppService {
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CHECKED_IN) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已签到");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (status == SlotStatus.RETURNED) {
@@ -388,6 +394,12 @@ public class TicketAppServiceImpl implements ITicketAppService {
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CHECKED_IN) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已签到");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (status == SlotStatus.RETURNED) {

View File

@@ -24,7 +24,7 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
FROM adm_schedule_slot s
WHERE s.pool_id = p.id
AND s.delete_flag = '0'
AND s.status = #{bookedStatus}
AND (s.status = #{bookedStatus} OR s.status = 3)
), 0),
locked_num = COALESCE((
SELECT COUNT(1)
@@ -42,7 +42,7 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
@Param("lockedStatus") Integer lockedStatus);
/**
* 签到时更新号源池统计:锁定数-1,已预约数+1
* 签到时更新号源池统计:锁定数-1签到后状态变为CHECKED_IN=3由refreshPoolStats统一统计
*
* @param poolId 号源池ID
* @return 结果
@@ -50,7 +50,6 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
@Update("""
UPDATE adm_schedule_pool
SET locked_num = locked_num - 1,
booked_num = booked_num + 1,
update_time = NOW()
WHERE id = #{poolId}
AND locked_num > 0

View File

@@ -329,16 +329,16 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 只有锁定态(2)的号源才能签到,签到时 2→1(LOCKED→BOOKED)
// 2. 只有锁定态(2)的号源才能签到,签到时 2→3(LOCKED→CHECKED_IN)
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
throw new RuntimeException("号源状态异常,无法签到");
}
// 3. 更新号源槽位状态 2→1LOCKED→BOOKED已预约=已签到)
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.BOOKED.getValue(), new Date(), SlotStatus.LOCKED.getValue());
// 3. 更新号源槽位状态 2→3LOCKED→CHECKED_IN已签到)
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN.getValue(), new Date(), SlotStatus.LOCKED.getValue());
// 4. 更新号源池统计:锁定数-1预约数+1
// 4. 更新号源池统计:锁定数-1签到数+1
if (slot != null && slot.getPoolId() != null) {
schedulePoolMapper.updatePoolStatsOnCheckIn(slot.getPoolId());
}

View File

@@ -279,7 +279,7 @@
</div>
<!-- 7. 已预约患者信息 -->
<div
v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName"
v-if="(item.status === '已预约' || item.status === '已取号' || item.status === '已签到') && item.patientName"
class="ticket-patient"
>
{{ item.patientName }}({{ item.patientId }},{{ getGenderText(item.gender || item.patientGender) }})
@@ -472,6 +472,7 @@ const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已签到': 'status-checked',
'已退号': 'status-returned',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled'