✨ 运单里程改为末位打卡采集,含里程计费时司机必填
This commit is contained in:
+9
@@ -54,6 +54,15 @@ public class NodeSubmitDTO implements Serializable {
|
||||
@Schema(description = "数量(件)")
|
||||
private String quantity;
|
||||
|
||||
/**
|
||||
* 里程(公里):只由末位打卡节点采集,运单计费规则含里程时必填。
|
||||
* <p>
|
||||
* 用字符串接收——旧版小程序不传、新版按输入框传字符串,两种都要能进;
|
||||
* 非末位节点收到该值时忽略,不报错。
|
||||
*/
|
||||
@Schema(description = "里程(公里),末位打卡节点采集")
|
||||
private String mileage;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
+8
@@ -27,6 +27,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -84,6 +85,13 @@ public class WaybillPunchRecordSaveRequest implements Serializable {
|
||||
@Schema(description = "数量(件)")
|
||||
private String quantity;
|
||||
|
||||
/**
|
||||
* 里程(公里):null 表示不改动运单里程;只在末位打卡节点上生效。
|
||||
* 管理端为选填,留空即保持原值。
|
||||
*/
|
||||
@Schema(description = "里程(公里),为空表示不改")
|
||||
private BigDecimal mileage;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
+12
@@ -11,6 +11,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -94,6 +95,17 @@ public class DriverPunchNodeVO implements Serializable {
|
||||
@Schema(description = "数量(件)")
|
||||
private String quantity;
|
||||
|
||||
/**
|
||||
* 本节点是否需要采集里程:末位可打卡节点、且运单计费规则含里程(应收或应付任一侧)。
|
||||
* <p>
|
||||
* 判据与 finishOnPunch、自动完成同源的「末位」定义;客户端只认这个标记,不自己推末位。
|
||||
*/
|
||||
@Schema(description = "本节点是否需要采集里程(末位可打卡节点且计费规则含里程)")
|
||||
private Boolean needMileage;
|
||||
|
||||
@Schema(description = "运单里程(公里),needMileage 为真时用于回显")
|
||||
private BigDecimal mileage;
|
||||
|
||||
@Schema(description = "已上传凭证图(已打卡回显)")
|
||||
private List<DriverPunchPhotoVO> photos = new ArrayList<>();
|
||||
|
||||
|
||||
+13
@@ -9,6 +9,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -55,6 +56,18 @@ public class WaybillPunchRecordItemVO implements Serializable {
|
||||
@Schema(description = "数量")
|
||||
private String quantity;
|
||||
|
||||
/**
|
||||
* 本条记录是否采集里程:末位打卡节点、且运单计费规则含里程。只有该条为真时管理端才展示里程项。
|
||||
*/
|
||||
@Schema(description = "本条是否需要采集里程(末位打卡节点且计费规则含里程)")
|
||||
private Boolean needMileage;
|
||||
|
||||
@Schema(description = "运单里程(公里),needMileage 为真时用于回显")
|
||||
private BigDecimal mileage;
|
||||
|
||||
@Schema(description = "里程是否已冻结(运单已生成结算单,只能看不能改)")
|
||||
private Boolean mileageLocked;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
+17
@@ -84,12 +84,29 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
|
||||
void generateForCompletedWaybills(List<Long> waybillIds);
|
||||
|
||||
/**
|
||||
* 同上,但按 {@code mileageOverrides} 覆盖运单里程后再算。
|
||||
* <p>
|
||||
* 生成走独立事务(REQUIRES_NEW),读不到调用方事务里尚未提交的里程更新,
|
||||
* 由调用方把本次刚写入的里程显式传进来,否则按里程计费会读到旧值或 null(算成 0 元)。
|
||||
*/
|
||||
void generateForCompletedWaybills(List<Long> waybillIds, Map<Long, BigDecimal> mileageOverrides);
|
||||
|
||||
/**
|
||||
* 运单维护里程后,同步未挂结算单的应收应付明细里程;
|
||||
* 对按里程、按吨·公里计费的费用行按新里程重新计算。
|
||||
*/
|
||||
void syncMileageFromWaybill(Long waybillId, BigDecimal mileage);
|
||||
|
||||
/**
|
||||
* 运单的计费规则是否包含里程:客户合同(应收)与承运商合同(应付)任一侧命中里程类计费要素即为真。
|
||||
* <p>
|
||||
* 判据与费用生成同源(同一份方案解析与规则命中),只读、不产生任何写操作;
|
||||
* 无合同、方案不匹配、规则不命中一律返回 false。
|
||||
* </p>
|
||||
*/
|
||||
boolean requiresMileage(Waybill waybill);
|
||||
|
||||
/**
|
||||
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
|
||||
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
|
||||
|
||||
+11
@@ -35,6 +35,7 @@ import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -97,6 +98,16 @@ public interface IWaybillService extends BaseService<Waybill> {
|
||||
WaybillVO copy(Long id);
|
||||
boolean changeRoute(Waybill waybill);
|
||||
boolean maintainMileage(WaybillMileageRequest request);
|
||||
|
||||
/**
|
||||
* 只写运单里程(定向更新),用于打卡链路采集里程。
|
||||
* <p>
|
||||
* 不做 maintainMileage 的「仅已完成 / 未生成结算单」前置——末位打卡发生在完成之前;
|
||||
* 结算状态由调用方按各自入口的规则判定,也不触发费用重算,由调用方决定是否同步。
|
||||
* </p>
|
||||
*/
|
||||
void savePunchMileage(Long waybillId, BigDecimal mileage);
|
||||
|
||||
boolean cancel(Long id);
|
||||
boolean reassign(Waybill waybill);
|
||||
|
||||
|
||||
+22
-1
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
import org.springblade.transport.support.LiveProcessConfigSupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.VehiclePlateSupport;
|
||||
import org.springblade.transport.support.WaybillCargoSupport;
|
||||
import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO;
|
||||
@@ -70,6 +71,7 @@ import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
@@ -157,6 +159,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
private final WaybillNodePunchMapper nodePunchMapper;
|
||||
private final ExceptionDisposalMapper exceptionDisposalMapper;
|
||||
private final ITripQueryService tripQueryService;
|
||||
private final IReceivablePayableDetailService receivablePayableDetailService;
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO currentTask() {
|
||||
@@ -497,6 +500,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
: legacyNodePunchPhotos(dto.getPhotos(), WaybillProcessSupport.nodeStringList(nodeCfg, "voucherTypes"));
|
||||
assertNodePunchPhotos(photoItems, nodeCfg, typedPhotoItems);
|
||||
|
||||
// 里程:末位可打卡节点 + 运单计费规则含里程(应收或应付任一侧)时必填。
|
||||
// 必须在完成判定之前落库——应收应付在本次打卡触发的完成里生成,读到的得是新里程。
|
||||
String submitNodeKey = WaybillProcessSupport.nodeKey(nodeCfg);
|
||||
if (WaybillProcessSupport.isLastDriverPunchNode(processJson, submitNodeKey)
|
||||
&& receivablePayableDetailService.requiresMileage(waybill)) {
|
||||
BigDecimal mileage = TransportBusinessSupport.requireMileage(dto.getMileage());
|
||||
waybillService.savePunchMileage(waybill.getId(), mileage);
|
||||
waybill.setMileage(mileage);
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
WaybillNodePunch punch = new WaybillNodePunch();
|
||||
punch.setWaybillId(waybill.getId());
|
||||
@@ -1564,6 +1577,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
Map<String, WaybillNodePunch> latestNodePunchByCode = latestNodePunchMap(waybill.getId());
|
||||
// 未开始运输的运单在途卡仍下发,但不可提交:司机要看得到这张卡,才知道为什么不能打
|
||||
String notStartedReason = WaybillProcessSupport.punchNotStartedReason(waybill.getBusinessStatus());
|
||||
// 计费规则是否含里程与节点无关,整条运单算一次;放在循环外避免逐节点回查合同。
|
||||
boolean lastNodeNeedsMileage = receivablePayableDetailService.requiresMileage(waybill);
|
||||
|
||||
for (Map<String, Object> cfg : punchConfigs) {
|
||||
boolean isTransit = WaybillProcessSupport.isTransitNodePublic(cfg);
|
||||
@@ -1622,9 +1637,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
// 末位卡上打一下就完成这张运单:判据与下方 submitNode 的自动完成同一份
|
||||
// (末位可打卡节点 + 到货/卸货/签收 + 回单不要求业务员确认),客户端只按它把按钮文案改成「完成运单」。
|
||||
// 两处判据必须同源,否则会出现「按钮写着完成运单、打下去这张运单没完成」。
|
||||
vo.setFinishOnPunch(WaybillProcessSupport.isLastDriverPunchNode(processJson, vo.getKey())
|
||||
boolean lastPunchNode = WaybillProcessSupport.isLastDriverPunchNode(processJson, vo.getKey());
|
||||
vo.setFinishOnPunch(lastPunchNode
|
||||
&& WaybillProcessSupport.isDeliveryNode(vo.getKey(), vo.getName())
|
||||
&& WaybillProcessSupport.returnNeedsNoConfirmation(processJson));
|
||||
// 里程同样只在末位卡采集:末位判定与上面同一个变量,避免两处各算一次走偏。
|
||||
if (lastPunchNode && lastNodeNeedsMileage) {
|
||||
vo.setNeedMileage(true);
|
||||
vo.setMileage(waybill.getMileage());
|
||||
}
|
||||
if (done) {
|
||||
if (punched.getPunchTime() != null) {
|
||||
LocalDateTime ldt = punched.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
|
||||
+73
-1
@@ -200,6 +200,60 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresMileage(Waybill waybill) {
|
||||
if (waybill == null) {
|
||||
return false;
|
||||
}
|
||||
ContractManage receivableContract = Func.isEmpty(waybill.getContractId())
|
||||
? null : contractManageService.getById(waybill.getContractId());
|
||||
if (hasMileageBillingRule(receivableContract, waybill)) {
|
||||
return true;
|
||||
}
|
||||
ContractManage payableContract;
|
||||
try {
|
||||
payableContract = resolveCarrierContract(waybill);
|
||||
} catch (ServiceException exception) {
|
||||
// 承运商合同数据有问题时不在打卡页抛错:费用生成会带着明确文案拒绝,
|
||||
// 打卡页只回答"这一趟要不要填里程",不该被合同脏数据卡住。
|
||||
log.warn("判定运单是否含里程计费时承运商合同解析失败,按不含里程处理,waybillId:{},原因:{}",
|
||||
waybill.getId(), exception.getMessage());
|
||||
return false;
|
||||
}
|
||||
return hasMileageBillingRule(payableContract, waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 这套合同命中的计费规则里,是否有里程类要素且规则本身命中该运单。
|
||||
* <p>
|
||||
* 方案解析与规则命中都复用费用生成那两条私有方法,保证「打卡时要里程」与「结算时按里程算」同一口径。
|
||||
*/
|
||||
private boolean hasMileageBillingRule(ContractManage contract, Waybill waybill) {
|
||||
if (contract == null || waybill == null) {
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> plan = resolveDefaultBillingPlan(
|
||||
parseList(contract.getBillingPlanJson()), waybill.getTransportType());
|
||||
if (plan == null || !(plan.get("rules") instanceof List<?> rules)) {
|
||||
return false;
|
||||
}
|
||||
for (Object value : rules) {
|
||||
if (!(value instanceof Map<?, ?> raw)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> rule = toRuleMap(raw);
|
||||
if (!isMileageBasedElement(stringValue(rule, "billingElement", ""))) {
|
||||
continue;
|
||||
}
|
||||
boolean matched = feeWaybills(rule, waybill).stream()
|
||||
.anyMatch(feeWaybill -> matchesBillingRule(raw, rule, feeWaybill));
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void syncMileageFromWaybill(Long waybillId, BigDecimal mileage) {
|
||||
@@ -1052,7 +1106,25 @@ public class ReceivablePayableDetailServiceImpl
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
public void generateForCompletedWaybills(List<Long> waybillIds) {
|
||||
generateAutomaticWaybillDetails(loadWaybills(waybillIds), true,
|
||||
generateForCompletedWaybills(waybillIds, Map.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
public void generateForCompletedWaybills(List<Long> waybillIds, Map<Long, BigDecimal> mileageOverrides) {
|
||||
List<Waybill> waybills = loadWaybills(waybillIds);
|
||||
// 独立事务读的是调用方提交前的数据:司机打卡刚写的里程在这里不可见,
|
||||
// 不覆盖就会按 null 里程算出 0 元运费。
|
||||
if (Func.isNotEmpty(mileageOverrides)) {
|
||||
waybills.forEach(waybill -> {
|
||||
BigDecimal mileage = mileageOverrides.get(waybill.getId());
|
||||
if (mileage != null) {
|
||||
waybill.setMileage(mileage);
|
||||
}
|
||||
});
|
||||
}
|
||||
generateAutomaticWaybillDetails(waybills, true,
|
||||
this::resolveWaybillCarrierContract, AuthUtil.getUserId(), new Date());
|
||||
}
|
||||
|
||||
|
||||
+74
-8
@@ -421,6 +421,16 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
if (processNodes.isEmpty()) {
|
||||
processNodes = WaybillProcessSupport.listDriverPunchNodes(processJson);
|
||||
}
|
||||
// 里程只在末位打卡节点采集:判据与自动完成、司机端 needMileage 同源(司机可打卡节点的末位)。
|
||||
List<Map<String, Object>> driverPunchNodes = WaybillProcessSupport.listDriverPunchNodes(processJson);
|
||||
String lastPunchKey = driverPunchNodes.isEmpty() ? null
|
||||
: WaybillProcessSupport.nodeKey(driverPunchNodes.get(driverPunchNodes.size() - 1));
|
||||
boolean lastNodeNeedsMileage = lastPunchKey != null
|
||||
&& receivablePayableDetailService.requiresMileage(waybill);
|
||||
// 已生成结算单的运单,里程只能看不能改(与「维护里程」同一条判据)
|
||||
boolean punchMileageLocked = lastNodeNeedsMileage
|
||||
&& receivablePayableDetailService.settlementLinkedWaybillIds(List.of(waybill.getId()))
|
||||
.contains(waybill.getId());
|
||||
|
||||
for (Map<String, Object> node : processNodes) {
|
||||
String nodeCode = WaybillProcessSupport.nodeKey(node);
|
||||
@@ -430,6 +440,11 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
item.setNodeCode(nodeCode);
|
||||
item.setNodeName(nodeName);
|
||||
item.setType(isTransit ? "enroute" : "node");
|
||||
if (lastNodeNeedsMileage && !isTransit && lastPunchKey.equals(nodeCode)) {
|
||||
item.setNeedMileage(true);
|
||||
item.setMileage(waybill.getMileage());
|
||||
item.setMileageLocked(punchMileageLocked);
|
||||
}
|
||||
item.setExceptionFlag(false);
|
||||
item.setPhotos(new ArrayList<>());
|
||||
|
||||
@@ -541,8 +556,46 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return revertIfReturnConfirmPreconditionLost(waybill, punched,
|
||||
saveEnroutePunchRecord(waybill, request, punched));
|
||||
}
|
||||
return revertIfReturnConfirmPreconditionLost(waybill, punched,
|
||||
saveNodePunchRecord(waybill, request, punched));
|
||||
boolean saved = saveNodePunchRecord(waybill, request, punched);
|
||||
if (saved) {
|
||||
applyPunchRecordMileage(waybill, request, punched);
|
||||
}
|
||||
return revertIfReturnConfirmPreconditionLost(waybill, punched, saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端订正打卡时写里程:只在末位打卡节点、且本次是打卡(非取消)时生效。
|
||||
* <p>
|
||||
* 里程留空(null)表示不改动;已生成结算单时拒绝改值,但其余字段的保存不受影响。
|
||||
* 写入后同步未结算明细的里程并按新里程重算,与「维护里程」同一条链路。
|
||||
*/
|
||||
private void applyPunchRecordMileage(Waybill waybill, WaybillPunchRecordSaveRequest request, boolean punched) {
|
||||
if (!punched || request.getMileage() == null) {
|
||||
return;
|
||||
}
|
||||
String processJson = resolveLiveProcessJson(waybill);
|
||||
Map<String, Object> nodeConfig = findEnabledNodeConfig(
|
||||
processJson, request.getNodeCode(), request.getNodeName());
|
||||
String nodeCode = nodeConfig == null
|
||||
? TransportBusinessSupport.trimToEmpty(request.getNodeCode())
|
||||
: WaybillProcessSupport.nodeKey(nodeConfig);
|
||||
// 非末位节点收到里程就忽略:客户端不该传,后端也不该因此拒掉整次订正。
|
||||
if (!WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeCode)) {
|
||||
return;
|
||||
}
|
||||
BigDecimal mileage = TransportBusinessSupport.requireMileage(request.getMileage());
|
||||
BigDecimal current = waybill.getMileage();
|
||||
// 同值放行:订正其它字段时前端会带回落库现值,不该被当成"改里程"。
|
||||
if (current != null && current.compareTo(mileage) == 0) {
|
||||
return;
|
||||
}
|
||||
if (receivablePayableDetailService.settlementLinkedWaybillIds(List.of(waybill.getId()))
|
||||
.contains(waybill.getId())) {
|
||||
throw new ServiceException("该运单已生成结算单,无法修改里程");
|
||||
}
|
||||
savePunchMileage(waybill.getId(), mileage);
|
||||
waybill.setMileage(mileage);
|
||||
receivablePayableDetailService.syncMileageFromWaybill(waybill.getId(), mileage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1450,11 +1503,8 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
.contains(waybill.getId())) {
|
||||
throw new ServiceException("该运单已生成结算单,无法维护里程");
|
||||
}
|
||||
BigDecimal mileage = request.getMileage();
|
||||
if (mileage == null || mileage.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| mileage.stripTrailingZeros().scale() > 0 || mileage.stripTrailingZeros().precision() > 10) {
|
||||
throw new ServiceException("里程必须为不超过10位的正整数");
|
||||
}
|
||||
// 与打卡采集共用同一套校验(正整数、有效位 ≤10),两处文案不许分叉
|
||||
BigDecimal mileage = TransportBusinessSupport.requireMileage(request.getMileage());
|
||||
String mileageRemark = TransportBusinessSupport.trimToNull(request.getMileageRemark());
|
||||
TransportBusinessSupport.validateLength(mileageRemark, 200, "里程维护备注不能超过200字");
|
||||
waybill.setMileage(mileage);
|
||||
@@ -1466,6 +1516,18 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void savePunchMileage(Long waybillId, BigDecimal mileage) {
|
||||
if (waybillId == null || mileage == null) {
|
||||
return;
|
||||
}
|
||||
// 只 set 里程列:整行 updateById 会把并发改过的其它字段一并覆盖回去。
|
||||
update(Wrappers.<Waybill>lambdaUpdate()
|
||||
.set(Waybill::getMileage, mileage)
|
||||
.eq(Waybill::getId, waybillId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean cancel(Long id) {
|
||||
@@ -1608,7 +1670,11 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
waybill.setEndDate(LocalDate.now());
|
||||
boolean updated = updateById(waybill);
|
||||
if (updated) {
|
||||
receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId()));
|
||||
// 计费生成是独立事务,读不到本事务里刚写的里程:把当前里程显式带过去,
|
||||
// 否则司机末位打卡写入的里程会被当成 null,按里程的费用算成 0 元。
|
||||
Map<Long, BigDecimal> mileageOverrides = waybill.getMileage() == null
|
||||
? Map.of() : Map.of(waybill.getId(), waybill.getMileage());
|
||||
receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId()), mileageOverrides);
|
||||
loadingManageService.completeIfAllWaybillsCompleted(waybill.getLoadingNo());
|
||||
}
|
||||
return updated;
|
||||
|
||||
+30
@@ -252,4 +252,34 @@ public final class TransportBusinessSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 里程(公里):正整数、有效位不超过 10 位。
|
||||
* <p>
|
||||
* 打卡采集与「维护里程」共用这一条判据,两处文案必须一致。空值按「没有填」拒回,
|
||||
* 非数字按格式错误拒回——两者给司机/业务员的下一步动作不同。
|
||||
*/
|
||||
public static BigDecimal requireMileage(String value) {
|
||||
String text = trimToNull(value);
|
||||
if (text == null) {
|
||||
throw new ServiceException("请输入里程");
|
||||
}
|
||||
try {
|
||||
return requireMileage(new BigDecimal(text));
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new ServiceException("里程必须为不超过10位的正整数");
|
||||
}
|
||||
}
|
||||
|
||||
public static BigDecimal requireMileage(BigDecimal mileage) {
|
||||
if (mileage == null) {
|
||||
throw new ServiceException("请输入里程");
|
||||
}
|
||||
if (mileage.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| mileage.stripTrailingZeros().scale() > 0
|
||||
|| mileage.stripTrailingZeros().precision() > 10) {
|
||||
throw new ServiceException("里程必须为不超过10位的正整数");
|
||||
}
|
||||
return mileage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -102,7 +102,8 @@ class DriverNodePunchPhotoTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
|
||||
when(driverService.getOne(any())).thenReturn(driver());
|
||||
when(waybillService.getOne(any())).thenReturn(waybill());
|
||||
|
||||
+2
-1
@@ -99,7 +99,8 @@ class DriverPunchFinishFlagTest {
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
|
||||
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ class DriverPunchNodeCardTest {
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
|
||||
Waybill waybill = waybill();
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill);
|
||||
|
||||
+2
-1
@@ -92,7 +92,8 @@ class DriverTripAcceptRejectTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
tripQueryService);
|
||||
tripQueryService,
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(certificationGate.isDriverAvailable(any())).thenReturn(true);
|
||||
lenient().when(waybillService.update(any())).thenReturn(true);
|
||||
|
||||
+2
-1
@@ -108,7 +108,8 @@ class DriverTripCardTest {
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
tripQueryService);
|
||||
tripQueryService,
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(enroutePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(enroutePunchMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
+2
-1
@@ -123,7 +123,8 @@ class DriverWaybillAutoCompleteTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
|
||||
when(driverService.getOne(any())).thenReturn(driver());
|
||||
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
+2
-1
@@ -80,7 +80,8 @@ class DriverWaybillCardLocationTest {
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
|
||||
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
+2
-1
@@ -62,7 +62,8 @@ class DriverWaybillNodePunchTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-1
@@ -74,7 +74,8 @@ class DriverWaybillOwnershipTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-1
@@ -90,7 +90,8 @@ class DriverWaybillRejectDetachTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(waybillService.updateById(any())).thenReturn(true);
|
||||
// syncDriverAcceptState 是 IWaybillService 的方法,mock 默认返 null 会让调用方 NPE;
|
||||
|
||||
+2
-1
@@ -88,7 +88,8 @@ class DriverWaybillTabStatusTest {
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
mock(ITripQueryService.class),
|
||||
mock(org.springblade.transport.service.IReceivablePayableDetailService.class));
|
||||
|
||||
when(driverService.getOne(any())).thenReturn(driver());
|
||||
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
+7
-4
@@ -27,10 +27,13 @@ import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -131,7 +134,7 @@ class WaybillConfirmReturnTest {
|
||||
assertEquals(true, result);
|
||||
verify(waybillMapper).updateById(org.mockito.ArgumentMatchers.<Waybill>argThat(waybill ->
|
||||
"completed".equals(waybill.getBusinessStatus()) && WAYBILL_ID.equals(waybill.getId())));
|
||||
verify(receivablePayableDetailService).generateForCompletedWaybills(List.of(WAYBILL_ID));
|
||||
verify(receivablePayableDetailService).generateForCompletedWaybills(List.of(WAYBILL_ID), Map.of());
|
||||
verify(loadingManageService).completeIfAllWaybillsCompleted(any());
|
||||
}
|
||||
|
||||
@@ -156,7 +159,7 @@ class WaybillConfirmReturnTest {
|
||||
|
||||
assertEquals("当前状态不允许确认回单", exception.getMessage());
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
verify(receivablePayableDetailService, never()).generateForCompletedWaybills(any());
|
||||
verify(receivablePayableDetailService, never()).generateForCompletedWaybills(anyList(), anyMap());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +225,7 @@ class WaybillConfirmReturnTest {
|
||||
|
||||
verify(waybillMapper).updateById(org.mockito.ArgumentMatchers.<Waybill>argThat(waybill ->
|
||||
"completed".equals(waybill.getBusinessStatus())));
|
||||
verify(receivablePayableDetailService).generateForCompletedWaybills(List.of(WAYBILL_ID));
|
||||
verify(receivablePayableDetailService).generateForCompletedWaybills(List.of(WAYBILL_ID), Map.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -248,7 +251,7 @@ class WaybillConfirmReturnTest {
|
||||
|
||||
verify(waybillMapper).updateById(org.mockito.ArgumentMatchers.<Waybill>argThat(waybill ->
|
||||
"cancelled".equals(waybill.getBusinessStatus()) && WAYBILL_ID.equals(waybill.getId())));
|
||||
verify(receivablePayableDetailService, never()).generateForCompletedWaybills(any());
|
||||
verify(receivablePayableDetailService, never()).generateForCompletedWaybills(anyList(), anyMap());
|
||||
verify(loadingManageService, never()).completeIfAllWaybillsCompleted(any());
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillEnroutePunch;
|
||||
import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
@@ -20,6 +21,7 @@ import java.lang.reflect.Field;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -57,6 +59,7 @@ class WaybillPunchRecordListTest {
|
||||
private WaybillMapper waybillMapper;
|
||||
private WaybillNodePunchMapper nodePunchMapper;
|
||||
private WaybillEnroutePunchMapper enroutePunchMapper;
|
||||
private IReceivablePayableDetailService receivablePayableDetailService;
|
||||
private WaybillServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
@@ -74,10 +77,12 @@ class WaybillPunchRecordListTest {
|
||||
waybillMapper = mock(WaybillMapper.class);
|
||||
nodePunchMapper = mock(WaybillNodePunchMapper.class);
|
||||
enroutePunchMapper = mock(WaybillEnroutePunchMapper.class);
|
||||
receivablePayableDetailService = mock(IReceivablePayableDetailService.class);
|
||||
service = new WaybillServiceImpl();
|
||||
inject("baseMapper", waybillMapper);
|
||||
inject("waybillNodePunchMapper", nodePunchMapper);
|
||||
inject("waybillEnroutePunchMapper", enroutePunchMapper);
|
||||
inject("receivablePayableDetailService", receivablePayableDetailService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,6 +192,44 @@ class WaybillPunchRecordListTest {
|
||||
assertEquals(7001L, load.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("计费含里程:里程只挂在末位可打卡节点上,其它节点不带")
|
||||
void mileageOnlyOnLastDriverPunchNode() {
|
||||
Waybill waybill = waybill(CONFIRM_ACCEPT_JSON, "running", "accepted");
|
||||
waybill.setMileage(new BigDecimal("128"));
|
||||
stubWaybill(waybill);
|
||||
when(receivablePayableDetailService.requiresMileage(waybill)).thenReturn(true);
|
||||
|
||||
List<WaybillPunchRecordItemVO> records = service.listPunchRecords(WAYBILL_ID).getRecords();
|
||||
WaybillPunchRecordItemVO accept = records.stream()
|
||||
.filter(record -> "accept".equals(record.getNodeCode())).findFirst()
|
||||
.orElseThrow(() -> new AssertionError("时间轴里没有接单节点"));
|
||||
WaybillPunchRecordItemVO load = records.stream()
|
||||
.filter(record -> "load".equals(record.getNodeCode())).findFirst()
|
||||
.orElseThrow(() -> new AssertionError("时间轴里没有装货节点"));
|
||||
|
||||
// 接单不参与司机打卡序列,末位是装货
|
||||
assertFalse(Boolean.TRUE.equals(accept.getNeedMileage()));
|
||||
assertTrue(Boolean.TRUE.equals(load.getNeedMileage()));
|
||||
assertEquals(0, new BigDecimal("128").compareTo(load.getMileage()));
|
||||
assertFalse(Boolean.TRUE.equals(load.getMileageLocked()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("计费不含里程:末位节点也不带里程标记")
|
||||
void noMileageFlagWhenBillingHasNoMileage() {
|
||||
Waybill waybill = waybill(CONFIRM_ACCEPT_JSON, "running", "accepted");
|
||||
stubWaybill(waybill);
|
||||
when(receivablePayableDetailService.requiresMileage(waybill)).thenReturn(false);
|
||||
|
||||
WaybillPunchRecordItemVO load = service.listPunchRecords(WAYBILL_ID).getRecords().stream()
|
||||
.filter(record -> "load".equals(record.getNodeCode())).findFirst()
|
||||
.orElseThrow(() -> new AssertionError("时间轴里没有装货节点"));
|
||||
|
||||
assertFalse(Boolean.TRUE.equals(load.getNeedMileage()));
|
||||
assertNull(load.getMileage());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private void stubWaybill(Waybill waybill) {
|
||||
|
||||
Reference in New Issue
Block a user