✨ 接单后自动将接单节点置为已打卡

管理端打卡时间轴上的「接单」节点此前恒显示未打卡:接单动作只写运单字段,不落打卡流水,
而时间轴的节点来源含接单/回单,没有流水即判为未打卡。

改为按运单的接单事实派生:司机确认接单即为已打卡,时间取接单时间;项目「无需确认接单」时,
运单越过接单阶段(进行中/已完成/已取消)即为已打卡,但没有完成时刻——运单上没有任何字段
记着那一刻,时间栏留空,不拿创建时间或更新时间编一个。

不写打卡流水的原因:接单环节有 5 个产生点(司机确认、免确认落库、复制运单、导入进行中、
配载单开始),其中两处是绕过 Java 逻辑的批量 SQL;接单事实另有 4 个清除点。写流水要在每处
挂点,漏一处就出现「待接单却已打卡」或「进行中却未打卡」;派生只需在 listPunchRecords 一处判定,
且历史运单立即正确,无需回填。打卡表 (waybill_id, node_code) 也没有唯一约束,幂等只能靠代码自保。

配套:
- 接单节点不再允许补录或取消打卡:它由接单动作决定,人工订正只会造出一条与运单事实打架的
  流水(时间轴显示已打卡、运单却还没人接单),故后端拒绝、前端不展示编辑入口
- 接单节点判据提为 WaybillProcessSupport.isAcceptNode(nodeCode, nodeName) 公开方法,
  时间轴与补录校验共用同一份,不再各写一处硬编码
- 补 8 个时间轴用例(真接单带时间/待接单/已拒单/免确认进行中无时间/免确认草稿/免确认待执行/
  免确认已取消/普通节点不受影响)+ 1 个补录拒绝用例
This commit is contained in:
2026-10-06 12:36:17 +08:00
parent 4e5b6ce8f3
commit bf37c47856
4 changed files with 293 additions and 3 deletions
@@ -458,6 +458,29 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
continue;
}
// 接单节点不落打卡流水:它是否完成由运单的接单事实决定,不看打卡表。
// 司机确认接单即为已打卡;项目「无需确认接单」时,运单进入进行中即视为该环节已完成。
// 后者没有接单动作,也就没有完成时刻,时间栏留空——运单上没有任何字段记着那一刻。
if (WaybillProcessSupport.isAcceptNode(nodeCode, nodeName)) {
boolean accepted = WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus());
boolean noConfirm = !WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson);
if (accepted) {
item.setPunched(true);
item.setStatusName("已打卡");
item.setPunchTime(formatPunchTime(waybill.getDriverAcceptTime()));
} else if (noConfirm && passedAcceptStage(waybill.getBusinessStatus())) {
item.setPunched(true);
item.setStatusName("已打卡");
item.setPunchTime("");
} else {
item.setPunched(false);
item.setStatusName("未打卡");
item.setPunchTime("");
}
records.add(item);
continue;
}
WaybillNodePunch punched = findLatestNodePunch(latestNodePunch, nodeCode, nodeName);
if (punched != null) {
List<String> types = resolveNodeVoucherTypes(voucherTypesByNode, punched.getNodeCode(), nodeName);
@@ -524,6 +547,11 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
if (Func.isEmpty(nodeCode) && Func.isEmpty(nodeName)) {
throw new ServiceException("过程节点不能为空");
}
// 接单节点没有打卡流水:它是否完成由运单的接单事实决定,补录与取消都改不动它。
// 放行只会造出一条与运单事实打架的流水——时间轴显示已打卡,运单却还没人接单。
if (WaybillProcessSupport.isAcceptNode(nodeCode, nodeName)) {
throw new ServiceException("接单节点由接单动作决定,不支持补录或取消打卡");
}
List<WaybillNodePunch> rows = nodePunchRows(waybillId, nodeCode, nodeName);
if (!punched) {
if (!rows.isEmpty()) {
@@ -910,6 +938,18 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
return photo;
}
/**
* 运单是否已越过接单阶段。
* <p>
* 正向列举进行中/已完成/已取消,不能用「不是终态」取反:终态里还包含草稿、待调度、
* 调度中这些并未越过接单的阶段,取反会把它们判成已接单。
*/
private static boolean passedAcceptStage(String businessStatus) {
return "running".equals(businessStatus)
|| "completed".equals(businessStatus)
|| "cancelled".equals(businessStatus);
}
private String formatPunchTime(Date time) {
if (time == null) {
return "";
@@ -536,10 +536,18 @@ public final class WaybillProcessSupport {
return "return".equals(key) || "回单".equals(name);
}
/**
* 是否接单节点:key=accept 或 name=接单。
* <p>
* 管理端打卡时间轴与补录校验共用它——接单节点的完成由运单的接单事实决定,
* 不落打卡流水,两处的判据必须是同一个。
*/
public static boolean isAcceptNode(String nodeCode, String nodeName) {
return "accept".equals(nodeCode) || "接单".equals(nodeName);
}
private static boolean isAcceptNode(Map<String, Object> node) {
String key = stringVal(node.get("key"));
String name = stringVal(node.get("name"));
return "accept".equals(key) || "接单".equals(name);
return isAcceptNode(stringVal(node.get("key")), stringVal(node.get("name")));
}
private static boolean isTransitNode(Map<String, Object> node) {