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:
52
AGENTS.md
52
AGENTS.md
@@ -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 后端
|
### 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 搜索所有引用 |
|
||||||
|
|
||||||
## 📈 成熟度追踪
|
## 📈 成熟度追踪
|
||||||
|
|
||||||
| 等级 | 特征 | 本项目 |
|
| 等级 | 特征 | 本项目 |
|
||||||
|
|||||||
@@ -207,6 +207,12 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
} else {
|
} else {
|
||||||
dto.setStatus("已取号");
|
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) {
|
} else if (status == SlotStatus.CANCELLED) {
|
||||||
dto.setStatus("已停诊");
|
dto.setStatus("已停诊");
|
||||||
} else if (status == SlotStatus.RETURNED) {
|
} else if (status == SlotStatus.RETURNED) {
|
||||||
@@ -388,6 +394,12 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
} else {
|
} else {
|
||||||
dto.setStatus("已取号");
|
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) {
|
} else if (status == SlotStatus.CANCELLED) {
|
||||||
dto.setStatus("已停诊");
|
dto.setStatus("已停诊");
|
||||||
} else if (status == SlotStatus.RETURNED) {
|
} else if (status == SlotStatus.RETURNED) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
|
|||||||
FROM adm_schedule_slot s
|
FROM adm_schedule_slot s
|
||||||
WHERE s.pool_id = p.id
|
WHERE s.pool_id = p.id
|
||||||
AND s.delete_flag = '0'
|
AND s.delete_flag = '0'
|
||||||
AND s.status = #{bookedStatus}
|
AND (s.status = #{bookedStatus} OR s.status = 3)
|
||||||
), 0),
|
), 0),
|
||||||
locked_num = COALESCE((
|
locked_num = COALESCE((
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
@@ -42,7 +42,7 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
|
|||||||
@Param("lockedStatus") Integer lockedStatus);
|
@Param("lockedStatus") Integer lockedStatus);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签到时更新号源池统计:锁定数-1,已预约数+1
|
* 签到时更新号源池统计:锁定数-1(签到后状态变为CHECKED_IN=3,由refreshPoolStats统一统计)
|
||||||
*
|
*
|
||||||
* @param poolId 号源池ID
|
* @param poolId 号源池ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
@@ -50,7 +50,6 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
|
|||||||
@Update("""
|
@Update("""
|
||||||
UPDATE adm_schedule_pool
|
UPDATE adm_schedule_pool
|
||||||
SET locked_num = locked_num - 1,
|
SET locked_num = locked_num - 1,
|
||||||
booked_num = booked_num + 1,
|
|
||||||
update_time = NOW()
|
update_time = NOW()
|
||||||
WHERE id = #{poolId}
|
WHERE id = #{poolId}
|
||||||
AND locked_num > 0
|
AND locked_num > 0
|
||||||
|
|||||||
@@ -329,16 +329,16 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
|
|||||||
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
|
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
|
||||||
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
|
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);
|
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
|
||||||
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
|
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
|
||||||
throw new RuntimeException("号源状态异常,无法签到");
|
throw new RuntimeException("号源状态异常,无法签到");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 更新号源槽位状态 2→1(LOCKED→BOOKED,已预约=已签到)
|
// 3. 更新号源槽位状态 2→3(LOCKED→CHECKED_IN,已签到)
|
||||||
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.BOOKED.getValue(), new Date(), SlotStatus.LOCKED.getValue());
|
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN.getValue(), new Date(), SlotStatus.LOCKED.getValue());
|
||||||
|
|
||||||
// 4. 更新号源池统计:锁定数-1,已预约数+1
|
// 4. 更新号源池统计:锁定数-1,已签到数+1
|
||||||
if (slot != null && slot.getPoolId() != null) {
|
if (slot != null && slot.getPoolId() != null) {
|
||||||
schedulePoolMapper.updatePoolStatsOnCheckIn(slot.getPoolId());
|
schedulePoolMapper.updatePoolStatsOnCheckIn(slot.getPoolId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,7 +279,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 7. 已预约患者信息 -->
|
<!-- 7. 已预约患者信息 -->
|
||||||
<div
|
<div
|
||||||
v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName"
|
v-if="(item.status === '已预约' || item.status === '已取号' || item.status === '已签到') && item.patientName"
|
||||||
class="ticket-patient"
|
class="ticket-patient"
|
||||||
>
|
>
|
||||||
{{ item.patientName }}({{ item.patientId }},{{ getGenderText(item.gender || item.patientGender) }})
|
{{ item.patientName }}({{ item.patientId }},{{ getGenderText(item.gender || item.patientGender) }})
|
||||||
@@ -472,6 +472,7 @@ const STATUS_CLASS_MAP = {
|
|||||||
'未预约': 'status-unbooked',
|
'未预约': 'status-unbooked',
|
||||||
'已预约': 'status-booked',
|
'已预约': 'status-booked',
|
||||||
'已取号': 'status-checked',
|
'已取号': 'status-checked',
|
||||||
|
'已签到': 'status-checked',
|
||||||
'已退号': 'status-returned',
|
'已退号': 'status-returned',
|
||||||
'已停诊': 'status-cancelled',
|
'已停诊': 'status-cancelled',
|
||||||
'已取消': 'status-cancelled'
|
'已取消': 'status-cancelled'
|
||||||
|
|||||||
Reference in New Issue
Block a user