🐛 运单打卡收紧为仅运输中,修复同日重复在途打卡
This commit is contained in:
+59
-8
@@ -405,18 +405,21 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
// 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明)
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), true);
|
||||
// 加行锁:重复打卡是先查后插,并发请求读不到对方未提交的流水,不锁会各插一条
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), true, true);
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
assertPunchNotEnded(waybill.getBusinessStatus());
|
||||
assertPunchStarted(waybill.getBusinessStatus());
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
Date lastPunchAt = findLastPunchTime(waybill.getId());
|
||||
Date lastPunchAt = findLastPunchTime(waybill.getId(), true);
|
||||
WaybillProcessSupport.TransitCheckinDecision decision = WaybillProcessSupport.evaluateTransitCheckin(
|
||||
processJson, waybill.getBusinessStatus(), lastPunchAt, LocalDateTime.now());
|
||||
if (!decision.punchEnabled()) {
|
||||
throw new ServiceException("该运单未启用在途打卡");
|
||||
}
|
||||
if (decision.doneToday()) {
|
||||
// 重复校验按「自然日是否打过」独立判:decision.doneToday() 只在进行中分支计算
|
||||
if (WaybillProcessSupport.isPunchedOn(lastPunchAt, null)) {
|
||||
throw new ServiceException("今日已完成在途打卡,不可重复打卡");
|
||||
}
|
||||
// 在途也是过程节点之一,占过程配置顺序里的位置,同样受异常门禁约束
|
||||
@@ -458,6 +461,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId());
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
assertPunchNotEnded(waybill.getBusinessStatus());
|
||||
assertPunchStarted(waybill.getBusinessStatus());
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
Map<String, Object> nodeCfg = findPunchNodeConfig(processJson, nodeCode);
|
||||
@@ -781,6 +785,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
/** @param includeRejected 仅 submitEnroute 传 true,见 scopedQuery 的重载说明 */
|
||||
private Waybill loadDriverWaybill(Long id, boolean includeRejected) {
|
||||
return loadDriverWaybill(id, includeRejected, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forUpdate 加行锁,仅 submitEnroute 使用:「同一天是否打过」是查完再插,
|
||||
* 不加锁时并发的两个请求都读不到对方未提交的流水,会各插一条。
|
||||
* 锁的是运单行,同一运单的打卡提交因此串行。
|
||||
*/
|
||||
private Waybill loadDriverWaybill(Long id, boolean includeRejected, boolean forUpdate) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
@@ -788,8 +801,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (Func.isEmpty(phone)) {
|
||||
throw new ServiceException("当前用户手机号为空,无法查找运单");
|
||||
}
|
||||
// MySQL 的 FOR UPDATE 必须跟在 LIMIT 之后
|
||||
String tail = forUpdate ? "LIMIT 1 FOR UPDATE" : "LIMIT 1";
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(phone, includeRejected)
|
||||
.eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
.eq(Waybill::getId, id).last(tail));
|
||||
if (waybill == null) {
|
||||
throw new ServiceException("运单不存在或无权操作");
|
||||
}
|
||||
@@ -1333,6 +1348,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言运单已开始运输。
|
||||
* <p>
|
||||
* 只有进行中的运单能打:草稿、待执行(待接单)、待调度、调度中都还没上路。
|
||||
* 与「运单已结束」分开判:结束没有解除途径,未开始会随接单/调度自行变为进行中。
|
||||
*/
|
||||
private void assertPunchStarted(String businessStatus) {
|
||||
String reason = WaybillProcessSupport.punchNotStartedReason(businessStatus);
|
||||
if (reason != null) {
|
||||
throw new ServiceException(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言指定节点未被异常门禁阻断。
|
||||
* <p>
|
||||
@@ -1527,6 +1555,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
latestEnroute = enroutes.get(enroutes.size() - 1);
|
||||
}
|
||||
Map<String, WaybillNodePunch> latestNodePunchByCode = latestNodePunchMap(waybill.getId());
|
||||
// 未开始运输的运单在途卡仍下发,但不可提交:司机要看得到这张卡,才知道为什么不能打
|
||||
String notStartedReason = WaybillProcessSupport.punchNotStartedReason(waybill.getBusinessStatus());
|
||||
|
||||
for (Map<String, Object> cfg : punchConfigs) {
|
||||
boolean isTransit = WaybillProcessSupport.isTransitNodePublic(cfg);
|
||||
@@ -1554,7 +1584,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
vo.setCargoTypes(Collections.emptyList());
|
||||
vo.setVisible(true);
|
||||
vo.setDone(done);
|
||||
vo.setActionable(!done);
|
||||
vo.setActionable(!done && notStartedReason == null);
|
||||
if (notStartedReason != null) {
|
||||
vo.setLockReason(notStartedReason);
|
||||
}
|
||||
if (done && latestEnroute != null) {
|
||||
vo.setCheckinTime(latestEnroute.getTime());
|
||||
vo.setCheckinPlace(latestEnroute.getAddress());
|
||||
@@ -1575,7 +1608,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
boolean done = punched != null;
|
||||
vo.setVisible(true);
|
||||
vo.setDone(done);
|
||||
vo.setActionable(!done);
|
||||
vo.setActionable(!done && notStartedReason == null);
|
||||
if (notStartedReason != null && !done) {
|
||||
vo.setLockReason(notStartedReason);
|
||||
}
|
||||
// 末位卡上打一下就完成这张运单:判据与下方 submitNode 的自动完成同一份
|
||||
// (末位可打卡节点 + 到货/卸货/签收 + 回单不要求业务员确认),客户端只按它把按钮文案改成「完成运单」。
|
||||
// 两处判据必须同源,否则会出现「按钮写着完成运单、打下去这张运单没完成」。
|
||||
@@ -1610,6 +1646,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (Boolean.TRUE.equals(vo.getDone()) || !blockedKeys.contains(vo.getKey())) {
|
||||
continue;
|
||||
}
|
||||
// 未开始运输是更根本的拒绝:运单状态不变,处理完异常也打不了。
|
||||
// 这里不能再改写成异常文案,否则会暗示「处理完就能打」。
|
||||
if (notStartedReason != null) {
|
||||
continue;
|
||||
}
|
||||
vo.setBlocked(true);
|
||||
vo.setActionable(false);
|
||||
if (blocker != null) {
|
||||
@@ -1665,7 +1706,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
boolean ended = Boolean.TRUE.equals(vo.getEnded());
|
||||
boolean done = Boolean.TRUE.equals(vo.getDone());
|
||||
boolean transitCard = Boolean.TRUE.equals(vo.getTransit());
|
||||
vo.setExpandable(!blockedByOrder && (!ended || (done && !transitCard)));
|
||||
// 未开始运输的未打卡卡片:打不了也展不开——展开是空表单,状态由锁文案说明
|
||||
boolean notStartedLocked = !done && notStartedReason != null;
|
||||
vo.setExpandable(!blockedByOrder && !notStartedLocked && (!ended || (done && !transitCard)));
|
||||
}
|
||||
|
||||
// 「异常」标签:只挂**存在未处理完成异常**的节点(与异常门禁同一口径,结案即消失)。
|
||||
@@ -1899,13 +1942,21 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
private Date findLastPunchTime(Long waybillId) {
|
||||
return findLastPunchTime(waybillId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forUpdate 仅 submitEnroute:与运单行锁配对。不加锁读走的是事务快照,
|
||||
* 并发提交的第一笔已提交后,第二笔可能仍读到旧快照而看不到它。
|
||||
*/
|
||||
private Date findLastPunchTime(Long waybillId, boolean forUpdate) {
|
||||
if (waybillId == null) {
|
||||
return null;
|
||||
}
|
||||
WaybillEnroutePunch latest = enroutePunchMapper.selectOne(Wrappers.<WaybillEnroutePunch>lambdaQuery()
|
||||
.eq(WaybillEnroutePunch::getWaybillId, waybillId)
|
||||
.orderByDesc(WaybillEnroutePunch::getPunchTime)
|
||||
.last("LIMIT 1"));
|
||||
.last(forUpdate ? "LIMIT 1 FOR UPDATE" : "LIMIT 1"));
|
||||
return latest == null ? null : latest.getPunchTime();
|
||||
}
|
||||
|
||||
|
||||
+38
-4
@@ -158,6 +158,39 @@ public final class WaybillProcessSupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运单已开始运输:只有进行中可以打卡(在途与普通节点同一判据)。
|
||||
* <p>
|
||||
* 草稿、待执行(待接单)、待调度、调度中都还没上路,任何打卡都没有承载物。
|
||||
* 判据是校正后落库的状态:无需接单的待执行运单在 {@link #normalizeBusinessStatus}
|
||||
* 里已被提升为进行中,未提升的待执行就是真的还没接单。
|
||||
*/
|
||||
public static boolean isTransitPunchStarted(String businessStatus) {
|
||||
return STATUS_RUNNING.equalsIgnoreCase(stringVal(businessStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* 未开始运输时的打卡拒绝文案;进行中与已结束返回 null(结束文案由 {@link #punchEndedReason} 给)。
|
||||
*/
|
||||
public static String punchNotStartedReason(String businessStatus) {
|
||||
if (isTransitPunchStarted(businessStatus) || isPunchEnded(businessStatus)) {
|
||||
return null;
|
||||
}
|
||||
return "运单尚未开始运输,暂不可打卡";
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定自然日是否已打过在途卡。
|
||||
* <p>
|
||||
* 与运单状态无关,只回答「这一天打过没有」。{@link TransitCheckinDecision#doneToday()}
|
||||
* 只在进行中分支计算,状态一非进行中就回到 false,两者不可互相替代。
|
||||
*/
|
||||
public static boolean isPunchedOn(Date lastPunchAt, LocalDateTime now) {
|
||||
LocalDate day = (now == null ? LocalDateTime.now() : now).toLocalDate();
|
||||
LocalDate last = toLocalDate(lastPunchAt);
|
||||
return last != null && last.equals(day);
|
||||
}
|
||||
|
||||
public static boolean isTerminalBusinessStatus(String businessStatus) {
|
||||
return "draft".equals(businessStatus)
|
||||
|| "completed".equals(businessStatus)
|
||||
@@ -227,15 +260,16 @@ public final class WaybillProcessSupport {
|
||||
if (isPunchEnded(businessStatus)) {
|
||||
return new TransitCheckinDecision(false, true, false, false, frequencyDays, timeStart, timeEnd);
|
||||
}
|
||||
// punch=是即展示卡片;非进行中仅不可作为「今日待打」
|
||||
if (!STATUS_RUNNING.equals(businessStatus)) {
|
||||
return new TransitCheckinDecision(true, true, false, false, frequencyDays, timeStart, timeEnd);
|
||||
// punch=是即展示卡片;未开始运输(草稿/待执行/待调度/调度中)的卡片仍下发,
|
||||
// 但不可提交——打卡的前提是运单已上路(判据同时用于节点打卡)。
|
||||
if (!isTransitPunchStarted(businessStatus)) {
|
||||
return new TransitCheckinDecision(false, true, false, false, frequencyDays, timeStart, timeEnd);
|
||||
}
|
||||
|
||||
LocalDateTime current = now == null ? LocalDateTime.now() : now;
|
||||
LocalDate today = current.toLocalDate();
|
||||
LocalDate lastDate = toLocalDate(lastPunchAt);
|
||||
boolean doneToday = lastDate != null && lastDate.equals(today);
|
||||
boolean doneToday = isPunchedOn(lastPunchAt, current);
|
||||
boolean dueByFrequency;
|
||||
if (lastDate == null) {
|
||||
dueByFrequency = true;
|
||||
|
||||
Reference in New Issue
Block a user