✨ 末位打卡卡上完成运单,后台补「确认回单」入口
司机端打卡页的完成动作从底栏挪到末位卡上:节点下发时按与 submitNode 自动完成同一份判据 (末位可打卡节点 + 到货/卸货/签收 + 回单不要求业务员确认)给出「打这张卡即完成运单」, 客户端据此把按钮文案改成「完成运单」。两处判据同源,否则会出现「按钮写着完成运单、 打下去这张运单没完成」。 回单节点配成「需业务员确认」时,司机打完末位卡不置完成,此前全仓没有任何入口让业务员确认后置完成, 运单只能人工去后台点完成。新增 POST /waybill-manage/confirm-return:校验该运单的回单确实要求确认、 司机打卡节点(在途除外——它是频次打卡节点、不是必经一步)全部已打卡、无未了结异常,再走既有的 完成逻辑(改状态 + 生成应收应付 + 尝试完成配载单)。完成接口本身不要求节点打卡,不校验这两件事 就等于借这个入口绕过打卡门禁与异常门禁。本轮不做撤销。 新增 DriverPunchFinishFlagTest(4 项)与 WaybillConfirmReturnTest(5 项); blade-transport 全量 385 项用例通过。
This commit is contained in:
+7
@@ -373,6 +373,13 @@ public class WaybillController extends BladeController {
|
||||
return R.data(waybillService.batchComplete(ids));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm-return")
|
||||
@ApiOperationSupport(order = 24)
|
||||
@Operation(summary = "确认回单", description = "回单节点要求业务员确认时用:该运单的司机打卡节点全部已打卡且无未了结异常,才置为已完成")
|
||||
public R confirmReturn(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(waybillService.confirmReturnReceipt(id));
|
||||
}
|
||||
|
||||
@PostMapping("/road-loading")
|
||||
@ApiOperationSupport(order = 23)
|
||||
@Operation(summary = "公路配载", description = "传入ids")
|
||||
|
||||
+8
@@ -113,6 +113,14 @@ public interface IWaybillService extends BaseService<Waybill> {
|
||||
*/
|
||||
boolean completeWithoutDeptCheck(Long id);
|
||||
|
||||
/**
|
||||
* 确认回单:回单节点配成「需业务员确认」时,内部人员确认后把运单置为已完成。
|
||||
* <p>
|
||||
* 比 {@link #complete(Long)} 多两道校验:该运单的司机打卡节点(在途除外)必须全部已打卡,
|
||||
* 且没有未了结异常——不挡住就等于借这个入口绕过打卡门禁与异常门禁。
|
||||
*/
|
||||
boolean confirmReturnReceipt(Long id);
|
||||
|
||||
BusinessRemoveResultVO batchComplete(String ids);
|
||||
LoadingManageVO roadLoading(String ids);
|
||||
|
||||
|
||||
+6
@@ -1536,6 +1536,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
vo.setVisible(true);
|
||||
vo.setDone(done);
|
||||
vo.setActionable(!done);
|
||||
// 末位卡上打一下就完成这张运单:判据与下方 submitNode 的自动完成同一份
|
||||
// (末位可打卡节点 + 到货/卸货/签收 + 回单不要求业务员确认),客户端只按它把按钮文案改成「完成运单」。
|
||||
// 两处判据必须同源,否则会出现「按钮写着完成运单、打下去这张运单没完成」。
|
||||
vo.setFinishOnPunch(WaybillProcessSupport.isLastDriverPunchNode(processJson, vo.getKey())
|
||||
&& WaybillProcessSupport.isDeliveryNode(vo.getKey(), vo.getName())
|
||||
&& WaybillProcessSupport.returnNeedsNoConfirmation(processJson));
|
||||
if (done) {
|
||||
if (punched.getPunchTime() != null) {
|
||||
LocalDateTime ldt = punched.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
|
||||
+61
@@ -40,11 +40,13 @@ import org.springblade.thirdparty.lbs.feign.ILbsClient;
|
||||
import org.springblade.thirdparty.lbs.pojo.dto.LbsLocateRequest;
|
||||
import org.springblade.thirdparty.lbs.pojo.vo.LbsLocateResponse;
|
||||
import org.springblade.transport.excel.WaybillExcel;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillEnroutePunch;
|
||||
@@ -82,6 +84,7 @@ import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -125,6 +128,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@jakarta.annotation.Resource
|
||||
private WaybillEnroutePunchMapper waybillEnroutePunchMapper;
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private ExceptionDisposalMapper exceptionDisposalMapper;
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private ILbsClient lbsClient;
|
||||
|
||||
@@ -1495,6 +1501,61 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return doComplete(waybill);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean confirmReturnReceipt(Long id) {
|
||||
Waybill waybill = loadEditable(id, true);
|
||||
String processJson = LiveProcessConfigSupport.resolve(
|
||||
LiveProcessConfigSupport.byProject(processConfigService, waybill.getProjectId()),
|
||||
waybill.getProcessJson());
|
||||
if (WaybillProcessSupport.returnNeedsNoConfirmation(processJson)) {
|
||||
// 不要求确认时司机打完末位卡就已经完成了,运单不会停在进行中等确认;配置对不上要说清,
|
||||
// 否则用户会以为是确认没生效
|
||||
throw new ServiceException("该运单的回单不需要业务员确认");
|
||||
}
|
||||
assertPunchNodesFinished(waybill.getId(), processJson);
|
||||
assertNoUnresolvedException(waybill.getId());
|
||||
return doComplete(waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 司机打卡节点(在途除外)必须全部已打卡:完成接口本身不要求节点打卡,
|
||||
* 不挡住就能从「确认回单」绕过打卡门禁。
|
||||
*/
|
||||
private void assertPunchNodesFinished(Long waybillId, String processJson) {
|
||||
List<String> missing = WaybillProcessSupport.missingPunchNodeNames(processJson, punchedNodeKeys(waybillId));
|
||||
if (!missing.isEmpty()) {
|
||||
throw new ServiceException("该运单还有未打卡的节点:" + String.join("、", missing));
|
||||
}
|
||||
}
|
||||
|
||||
/** 该运单已打卡的节点编码与名称(小写):打卡记录里两者都写过,对节点时任一命中即可 */
|
||||
private Set<String> punchedNodeKeys(Long waybillId) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
List<WaybillNodePunch> punches = waybillNodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
|
||||
.eq(WaybillNodePunch::getWaybillId, waybillId));
|
||||
for (WaybillNodePunch punch : punches) {
|
||||
if (Func.isNotEmpty(punch.getNodeCode())) {
|
||||
keys.add(punch.getNodeCode().trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
if (Func.isNotEmpty(punch.getNodeName())) {
|
||||
keys.add(punch.getNodeName().trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** 存在未了结异常时不允许置完成:判据与司机端完成同一道门(处置单达终态才算解除) */
|
||||
private void assertNoUnresolvedException(Long waybillId) {
|
||||
Long unresolved = exceptionDisposalMapper.selectCount(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.eq(ExceptionDisposal::getWaybillId, waybillId)
|
||||
.eq(ExceptionDisposal::getIsDeleted, 0)
|
||||
.notIn(ExceptionDisposal::getDisposalStatus, WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES));
|
||||
if (unresolved != null && unresolved > 0) {
|
||||
throw new ServiceException("该运单存在未处理异常,处理完成后才能确认回单");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成运单核心逻辑:状态改为 completed,并检查生成应收应付明细。
|
||||
*/
|
||||
|
||||
+29
@@ -38,6 +38,7 @@ import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -637,6 +638,34 @@ public final class WaybillProcessSupport {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 还没打卡的司机打卡节点名称(在途除外):回单由业务员确认前,这些节点必须都打过卡,否则
|
||||
* 「确认回单」就成了绕过打卡门禁的通道——完成接口本身不要求节点打卡。
|
||||
* <p>
|
||||
* 在途是频次打卡节点、不是必经的一步,故不参与;空列表表示该打卡的都打完了。
|
||||
*
|
||||
* @param punchedKeys 已打卡的节点编码与名称,**小写**(库里的 node_code / node_name 都可能被用来对节点)
|
||||
*/
|
||||
public static List<String> missingPunchNodeNames(String processJson, Collection<String> punchedKeys) {
|
||||
List<Map<String, Object>> punchConfigs = listDriverPunchNodes(processJson);
|
||||
if (punchConfigs.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Collection<String> punched = punchedKeys == null ? Collections.emptySet() : punchedKeys;
|
||||
List<String> missing = new ArrayList<>();
|
||||
for (Map<String, Object> node : punchConfigs) {
|
||||
if (isTransitNode(node)) {
|
||||
continue;
|
||||
}
|
||||
String code = nodeKey(node).toLowerCase(Locale.ROOT);
|
||||
String name = nodeName(node).toLowerCase(Locale.ROOT);
|
||||
if (!punched.contains(code) && !punched.contains(name)) {
|
||||
missing.add(nodeName(node));
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
private static boolean isTransitNode(Map<String, Object> node) {
|
||||
String key = stringVal(node.get("key"));
|
||||
String name = stringVal(node.get("name"));
|
||||
|
||||
Reference in New Issue
Block a user