✨ 司机端小程序接口按运次下发列表与详情,并支持整单接单与拒单
新增 /waybill/task/{current,pending-preview,counts,page,detail,accept,reject}:
列表一页若干个运次,详情按是否配载单下发整趟车站点(route_json 顺序,
装N/卸N 指向配载单运单集合里的第 N 张子运单,每站货物取子运单);
一次接单把该运次未走完的子运单一起置为已接单,一次拒单整单拒绝。
旧的运单级端点与写入口语义不变。
This commit is contained in:
+23
@@ -153,6 +153,26 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "运输组织:common普通 / load配载")
|
||||
private String transportOrgType;
|
||||
|
||||
/* ===== 运次(一趟车)字段:运次接口返回,旧运单接口不填 ===== */
|
||||
|
||||
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
|
||||
private String tripId;
|
||||
|
||||
@Schema(description = "配载单号,未配载为空")
|
||||
private String loadingNo;
|
||||
|
||||
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
|
||||
private String tripNo;
|
||||
|
||||
@Schema(description = "途经站点:整趟车中间各站按顺序以「 → 」连接")
|
||||
private String viaName;
|
||||
|
||||
@Schema(description = "站点数(整趟车,含起终点)")
|
||||
private Integer stationCount;
|
||||
|
||||
@Schema(description = "货物汇总(整趟车,同名货物合并且单位一致时取数量合计)")
|
||||
private String cargoSummary;
|
||||
|
||||
@Schema(description = "计划发货时间")
|
||||
private String planShipTime;
|
||||
|
||||
@@ -168,6 +188,9 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "装卸点(详情返回)")
|
||||
private List<DriverRoutePointVO> routePoints;
|
||||
|
||||
@Schema(description = "站点任务安排(详情返回,按实际途经顺序,第 1 站即 ①;有配载时含每站装卸与货物条目)")
|
||||
private List<TripStationVO> stations;
|
||||
|
||||
@Schema(description = "过程配置 JSON(详情返回,供前端兜底推导打卡节点)")
|
||||
private String processJson;
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 运次站点(任务安排)
|
||||
* <p>
|
||||
* 有配载单时按配载单的路线信息给出实际途经顺序,第 1 站即 ①;站点上的「装N / 卸N」指向配载单运单集合里的第 N 张子运单,
|
||||
* 同一站可以既卸又装(装卸)。没有路线信息时回退到子运单的起讫点去重成站。
|
||||
* <p>
|
||||
* 站点上的货物条目取子运单,不取配载单的货物信息:配载单的货物条数与子运单不一定同数,行序也对不上站点编号。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次站点")
|
||||
public class TripStationVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "站序,从 1 开始(展示为 ①②③)")
|
||||
private Integer index;
|
||||
|
||||
@Schema(description = "站名:展示地址优先,回退详细地址")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "站点标记原文:装N / 卸N")
|
||||
private List<String> tags;
|
||||
|
||||
@Schema(description = "操作类型:load 装货 / unload 卸货 / load_unload 装卸,无从判定时为空")
|
||||
private String operationType;
|
||||
|
||||
@Schema(description = "操作类型文案:装货 / 卸货 / 装卸")
|
||||
private String operationTypeLabel;
|
||||
|
||||
@Schema(description = "该站涉及的货物条目")
|
||||
private List<TripCargoVO> cargos;
|
||||
|
||||
/**
|
||||
* 站点货物条目
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次站点货物条目")
|
||||
public static class TripCargoVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "所属子运单主键,按字符串下发")
|
||||
private String waybillId;
|
||||
|
||||
@Schema(description = "所属运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "货主名称")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "数量")
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Schema(description = "数量单位")
|
||||
private String quantityUnit;
|
||||
|
||||
@Schema(description = "该站的操作:load 装货 / unload 卸货")
|
||||
private String operation;
|
||||
|
||||
@Schema(description = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -28,6 +28,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -107,12 +108,45 @@ public class TripVO implements Serializable {
|
||||
@Schema(description = "到货地")
|
||||
private String arrivalAddress;
|
||||
|
||||
@Schema(description = "起点名称:整趟车首站,展示地址优先、回退详细地址")
|
||||
private String departureName;
|
||||
|
||||
@Schema(description = "途经站点:中间各站按顺序以「 → 」连接,无中间站为空")
|
||||
private String transitName;
|
||||
|
||||
@Schema(description = "终点名称:整趟车末站,展示地址优先、回退详细地址")
|
||||
private String arrivalName;
|
||||
|
||||
@Schema(description = "站点数(整趟车,含起终点)")
|
||||
private int stationCount;
|
||||
|
||||
@Schema(description = "货物汇总:按货物名合并数量,同名同单位取合计")
|
||||
private String cargoSummary;
|
||||
|
||||
@Schema(description = "合计重量:全部子运单同单位时为数量合计,单位不一致时为空")
|
||||
private BigDecimal totalQuantity;
|
||||
|
||||
@Schema(description = "合计重量的单位,与 totalQuantity 同时为空或同时有值")
|
||||
private String quantityUnit;
|
||||
|
||||
@Schema(description = "计划发货日期(配载单,未配载为空)")
|
||||
private LocalDate estimatedStartDate;
|
||||
|
||||
@Schema(description = "计划完成日期(配载单,未配载为空)")
|
||||
private LocalDate estimatedEndDate;
|
||||
|
||||
@Schema(description = "任务备注(配载单的运输说明)")
|
||||
private String taskRemark;
|
||||
|
||||
@Schema(description = "路线信息,仅单个运次详情返回(列表不带,省流量)")
|
||||
private String routeJson;
|
||||
|
||||
@Schema(description = "货物信息,仅单个运次详情返回(列表不带,省流量)")
|
||||
private String goodsJson;
|
||||
|
||||
@Schema(description = "站点(任务安排,按实际途经顺序),仅单个运次详情返回;列表用起/经/终与站点数")
|
||||
private List<TripStationVO> stations;
|
||||
|
||||
@Schema(description = "子运单(可见子单,按创建时间升序)")
|
||||
private List<TripWaybillVO> waybills;
|
||||
|
||||
|
||||
+59
@@ -105,6 +105,65 @@ public class DriverWaybillController extends BladeController {
|
||||
return R.data(driverWaybillService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/task/current")
|
||||
@ApiOperationSupport(order = 21)
|
||||
@Operation(summary = "首页:当前运输中运次", description = "当前用户手机号可见的运输中运次(有配载单的一趟车算一个)最新一条")
|
||||
public R<DriverWaybillCardVO> tripCurrentTask() {
|
||||
return R.data(driverWaybillService.tripCurrentTask());
|
||||
}
|
||||
|
||||
@GetMapping("/task/pending-preview")
|
||||
@ApiOperationSupport(order = 22)
|
||||
@Operation(summary = "首页:待接运次预览", description = "当前用户手机号可见的待接运次预览与总数(按运次计)")
|
||||
public R<DriverWaybillPreviewVO> tripPendingPreview(
|
||||
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
|
||||
return R.data(driverWaybillService.tripPendingPreview(size));
|
||||
}
|
||||
|
||||
@GetMapping("/task/counts")
|
||||
@ApiOperationSupport(order = 23)
|
||||
@Operation(summary = "运次 Tab 统计", description = "按运次统计:全部 = 待接单 + 进行中 + 已完成(不含已取消)")
|
||||
public R<DriverWaybillTabCountsVO> tripCounts() {
|
||||
return R.data(driverWaybillService.tripTabCounts());
|
||||
}
|
||||
|
||||
@GetMapping("/task/page")
|
||||
@ApiOperationSupport(order = 24)
|
||||
@Operation(summary = "运次分页列表", description = "一页即若干个运次;status:空=全部,0待接单,1运输中,2已完成;keyword 支持运单号/配载单号/起终点/车牌")
|
||||
public R<IPage<DriverWaybillCardVO>> tripPage(
|
||||
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||
@Parameter(description = "每页运次数") @RequestParam(required = false) Integer size,
|
||||
@Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
|
||||
@Parameter(description = "关键字:运单号/配载单号/起终点/车牌") @RequestParam(required = false) String keyword) {
|
||||
Integer statusCode = parseStatus(status);
|
||||
return R.data(driverWaybillService.tripPage(current, size, statusCode, keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/task/detail")
|
||||
@ApiOperationSupport(order = 25)
|
||||
@Operation(summary = "司机运次详情", description = "按运次主键(字符串)取整趟车;有配载单时下发站点 ①②③(stations)与每站货物条目")
|
||||
public R<DriverWaybillCardVO> tripDetail(
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键)", required = true) @RequestParam String id) {
|
||||
return R.data(driverWaybillService.tripDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/task/accept")
|
||||
@ApiOperationSupport(order = 26)
|
||||
@Operation(summary = "运次确认接单", description = "一次接单把该运次全部子运单置为已接单;任一子单已接单即视为整单已接单")
|
||||
public R tripAccept(
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键)", required = true) @RequestParam String id) {
|
||||
return R.status(driverWaybillService.tripAccept(id));
|
||||
}
|
||||
|
||||
@PostMapping("/task/reject")
|
||||
@ApiOperationSupport(order = 27)
|
||||
@Operation(summary = "运次拒绝接单", description = "整单拒绝:该运次全部未终态子运单一起置为已拒绝,此后该运次对本人不可见")
|
||||
public R tripReject(
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键)", required = true) @RequestParam String id,
|
||||
@Parameter(description = "拒绝原因") @RequestParam(required = false) String reason) {
|
||||
return R.status(driverWaybillService.tripReject(id, reason));
|
||||
}
|
||||
|
||||
@PostMapping("/track")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "司机端行驶轨迹", description = "按当前用户手机号校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
|
||||
|
||||
+47
@@ -71,6 +71,53 @@ public interface IDriverWaybillService {
|
||||
*/
|
||||
DriverWaybillCardVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 当前登录司机的运输中运次(一趟车,最多一条)
|
||||
*/
|
||||
DriverWaybillCardVO tripCurrentTask();
|
||||
|
||||
/**
|
||||
* 当前登录司机的待接运次预览
|
||||
*
|
||||
* @param size 预览条数,默认 2
|
||||
*/
|
||||
DriverWaybillPreviewVO tripPendingPreview(Integer size);
|
||||
|
||||
/**
|
||||
* 运次 Tab 统计:全部 / 待接单 / 进行中 / 已完成(全部 = 前三者之和,不含已取消)
|
||||
*/
|
||||
DriverWaybillTabCountsVO tripTabCounts();
|
||||
|
||||
/**
|
||||
* 司机运次分页:一页即若干个运次(有配载单的一趟车算一个)
|
||||
*
|
||||
* @param current 当前页,从 1 开始
|
||||
* @param size 每页条数(按运次,默认 10)
|
||||
* @param status 小程序状态:空=全部,0待接单,1运输中,2已完成
|
||||
* @param keyword 关键字(运单号 / 配载单号 / 起终点 / 车牌,可选)
|
||||
*/
|
||||
IPage<DriverWaybillCardVO> tripPage(Integer current, Integer size, Integer status, String keyword);
|
||||
|
||||
/**
|
||||
* 司机运次详情:按运次主键(字符串)取整趟车,有配载单时下发站点 ①②③
|
||||
*/
|
||||
DriverWaybillCardVO tripDetail(String tripId);
|
||||
|
||||
/**
|
||||
* 运次确认接单:一次把该运次全部子运单置为已接单,司机字段不动
|
||||
*
|
||||
* @param tripId 运次主键(配载单主键或运单主键)
|
||||
*/
|
||||
boolean tripAccept(String tripId);
|
||||
|
||||
/**
|
||||
* 运次拒绝接单:整单拒绝,该运次此后对本人不可见,等调度重新派单
|
||||
*
|
||||
* @param tripId 运次主键(配载单主键或运单主键)
|
||||
* @param reason 拒绝原因,可空,最长 200 字
|
||||
*/
|
||||
boolean tripReject(String tripId, String reason);
|
||||
|
||||
/**
|
||||
* 司机端运单行驶轨迹:按打卡记录 / 计划时间自动推导起止日期后查询 LBS 轨迹。
|
||||
* <p>
|
||||
|
||||
+317
@@ -59,10 +59,15 @@ import org.springblade.transport.pojo.vo.DriverPunchPhotoVO;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripQueryVO;
|
||||
import org.springblade.transport.pojo.vo.TripStationVO;
|
||||
import org.springblade.transport.pojo.vo.TripTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripVO;
|
||||
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.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -130,6 +135,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
/** 被异常阻断时的不可打卡原因(司机端展示) */
|
||||
private static final String LOCK_REASON_EXCEPTION = "该运单存在未处理异常,处理完成后才能打卡";
|
||||
|
||||
/** 运次状态码:0 待接单 / 1 运输中 / 2 已完成 / 3 已取消,与运单侧同一套 */
|
||||
private static final int TRIP_STATUS_PENDING = 0;
|
||||
private static final int TRIP_STATUS_RUNNING = 1;
|
||||
|
||||
private final IWaybillService waybillService;
|
||||
private final IDriverService driverService;
|
||||
private final IMpUserClient mpUserClient;
|
||||
@@ -139,6 +148,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
private final WaybillEnroutePunchMapper enroutePunchMapper;
|
||||
private final WaybillNodePunchMapper nodePunchMapper;
|
||||
private final ExceptionDisposalMapper exceptionDisposalMapper;
|
||||
private final ITripQueryService tripQueryService;
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO currentTask() {
|
||||
@@ -252,6 +262,81 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return toCard(normalizeAcceptStatus(waybill), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO tripCurrentTask() {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return null;
|
||||
}
|
||||
TripQueryVO query = TripQueryVO.forDriver(phone);
|
||||
query.setStatus(TRIP_STATUS_RUNNING);
|
||||
query.setCurrent(1);
|
||||
query.setSize(1);
|
||||
IPage<TripVO> page = tripQueryService.page(query);
|
||||
return page.getRecords().isEmpty() ? null : toTripCard(page.getRecords().get(0), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillPreviewVO tripPendingPreview(Integer size) {
|
||||
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return preview;
|
||||
}
|
||||
TripQueryVO query = TripQueryVO.forDriver(phone);
|
||||
query.setStatus(TRIP_STATUS_PENDING);
|
||||
query.setCurrent(1);
|
||||
query.setSize(normalizePreviewSize(size));
|
||||
IPage<TripVO> page = tripQueryService.page(query);
|
||||
preview.setTotal(page.getTotal());
|
||||
preview.setRecords(page.getRecords().stream()
|
||||
.map(trip -> toTripCard(trip, false))
|
||||
.collect(Collectors.toList()));
|
||||
return preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillTabCountsVO tripTabCounts() {
|
||||
DriverWaybillTabCountsVO counts = new DriverWaybillTabCountsVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return counts;
|
||||
}
|
||||
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forDriver(phone));
|
||||
counts.setPending(tripCounts.getPending());
|
||||
counts.setDoing(tripCounts.getDoing());
|
||||
counts.setDone(tripCounts.getDone());
|
||||
counts.setAll(tripCounts.getAll());
|
||||
return counts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<DriverWaybillCardVO> tripPage(Integer current, Integer size, Integer status, String keyword) {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return new Page<>(normalizePageNo(current), normalizePageSize(size));
|
||||
}
|
||||
TripQueryVO query = TripQueryVO.forDriver(phone);
|
||||
query.setStatus(status);
|
||||
query.setKeyword(keyword);
|
||||
query.setCurrent(current);
|
||||
query.setSize(size);
|
||||
IPage<TripVO> trips = tripQueryService.page(query);
|
||||
Page<DriverWaybillCardVO> page = new Page<>(trips.getCurrent(), trips.getSize(), trips.getTotal());
|
||||
page.setRecords(trips.getRecords().stream()
|
||||
.map(trip -> toTripCard(trip, false))
|
||||
.collect(Collectors.toList()));
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO tripDetail(String tripId) {
|
||||
requireCurrentDriver();
|
||||
TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
|
||||
query.setTripId(tripId);
|
||||
return toTripCard(tripQueryService.detail(query), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillTrackVO track(Long id) {
|
||||
requireCurrentDriver();
|
||||
@@ -557,6 +642,118 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean tripAccept(String tripId) {
|
||||
Driver driver = requireCurrentDriver();
|
||||
// 认证门槛按整车一次过:一次接单只校验一次司机与车辆
|
||||
assertCertified(driver);
|
||||
List<TripVO.TripWaybillVO> children = tripChildren(tripId);
|
||||
assertTripAcceptable(children);
|
||||
List<Long> pendingIds = writeCandidateIds(children, "当前运单状态不允许接单");
|
||||
Date now = new Date();
|
||||
return waybillService.update(Wrappers.<Waybill>lambdaUpdate()
|
||||
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_ACCEPTED)
|
||||
.set(Waybill::getDriverAcceptTime, now)
|
||||
.set(Waybill::getDriverAcceptDriverId, driver.getId())
|
||||
.set(Waybill::getDriverRejectTime, null)
|
||||
.set(Waybill::getDriverRejectReason, null)
|
||||
.set(Waybill::getBusinessStatus, STATUS_RUNNING)
|
||||
.in(Waybill::getId, pendingIds));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean tripReject(String tripId, String reason) {
|
||||
requireCurrentDriver();
|
||||
List<TripVO.TripWaybillVO> children = tripChildren(tripId);
|
||||
assertTripRejectable(children);
|
||||
String rejectReason = Func.isEmpty(reason) ? null : reason.trim();
|
||||
if (Func.isNotEmpty(rejectReason) && rejectReason.length() > 200) {
|
||||
throw new ServiceException("拒绝原因不能超过200字");
|
||||
}
|
||||
List<Long> pendingIds = writeCandidateIds(children, "当前运单状态不允许拒绝接单");
|
||||
Date now = new Date();
|
||||
// 整单拒绝落在每张未终态子运单上,一次一条语句写完,接单时刻与拒单时刻全趟车一致
|
||||
return waybillService.update(Wrappers.<Waybill>lambdaUpdate()
|
||||
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)
|
||||
.set(Waybill::getDriverAcceptTime, null)
|
||||
.set(Waybill::getDriverAcceptDriverId, null)
|
||||
.set(Waybill::getDriverRejectTime, now)
|
||||
.set(Waybill::getDriverRejectReason, rejectReason)
|
||||
.set(Waybill::getBusinessStatus, STATUS_PENDING)
|
||||
.in(Waybill::getId, pendingIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运次 → 子运单集合:配载的运次走运次读服务解析(配载单主键命中后按配载单号取子运单),
|
||||
* 未配载的就是那张运单本身。收口与列表一致——别人的运次在这里已经被挡住。
|
||||
* <p>
|
||||
* 子单集合是司机可见的那一批:已拒单的子单不在其中,否则接单会把本人拒掉的单重新拉回来。
|
||||
*/
|
||||
private List<TripVO.TripWaybillVO> tripChildren(String tripId) {
|
||||
TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
|
||||
query.setTripId(tripId);
|
||||
TripVO trip = tripQueryService.detail(query);
|
||||
List<TripVO.TripWaybillVO> children = trip.getWaybills() == null ? List.of() : trip.getWaybills();
|
||||
if (children.isEmpty()) {
|
||||
throw new ServiceException("运次不存在或无权操作");
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接单校验按整车:有子单没走完才可接单,任一子单的项目要接单整趟车就要接单,任一子单已接单即整单已接单
|
||||
*/
|
||||
private void assertTripAcceptable(List<TripVO.TripWaybillVO> children) {
|
||||
if (children.stream().allMatch(this::isTerminalChild)) {
|
||||
throw new ServiceException("当前运单状态不允许接单");
|
||||
}
|
||||
if (children.stream().noneMatch(child -> Boolean.TRUE.equals(child.getRequireAccept()))) {
|
||||
throw new ServiceException("该运单无需确认接单");
|
||||
}
|
||||
if (children.stream().anyMatch(child -> WaybillProcessSupport.isAccepted(child.getAcceptStatus()))) {
|
||||
throw new ServiceException("该运单已接单");
|
||||
}
|
||||
if (children.stream().anyMatch(child -> WaybillProcessSupport.isRejected(child.getAcceptStatus()))) {
|
||||
throw new ServiceException("该运单已拒单,请联系调度重新派单");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒单校验按整车:有子单没走完才可拒绝,任一子单已接单则整单不能拒绝
|
||||
*/
|
||||
private void assertTripRejectable(List<TripVO.TripWaybillVO> children) {
|
||||
if (children.stream().allMatch(this::isTerminalChild)) {
|
||||
throw new ServiceException("当前运单状态不允许拒绝接单");
|
||||
}
|
||||
if (children.stream().noneMatch(child -> Boolean.TRUE.equals(child.getRequireAccept()))) {
|
||||
throw new ServiceException("该运单无需确认接单");
|
||||
}
|
||||
if (children.stream().anyMatch(child -> WaybillProcessSupport.isAccepted(child.getAcceptStatus()))) {
|
||||
throw new ServiceException("该运单已接单,无法拒绝");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可写入的子运单:已走完的子单不动——把已完成/已取消的子单改回运输中或待执行等于回退历史状态
|
||||
*/
|
||||
private List<Long> writeCandidateIds(List<TripVO.TripWaybillVO> children, String emptyMessage) {
|
||||
List<Long> ids = children.stream()
|
||||
.filter(child -> !isTerminalChild(child))
|
||||
.map(child -> parseId(child.getId()))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (ids.isEmpty()) {
|
||||
throw new ServiceException(emptyMessage);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private boolean isTerminalChild(TripVO.TripWaybillVO child) {
|
||||
return WaybillProcessSupport.isTerminalBusinessStatus(child.getBusinessStatus());
|
||||
}
|
||||
|
||||
private Waybill loadDriverWaybill(Long id) {
|
||||
return loadDriverWaybill(id, false);
|
||||
}
|
||||
@@ -828,6 +1025,126 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return List.of(load, unload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运次卡片
|
||||
* <p>
|
||||
* 未配载的运次就是一张运单:详情照运单详情下发(打卡节点、在途打卡、过程配置都还在),
|
||||
* 只是状态按运次口径只读推导,不走会落库的接单态归一化。
|
||||
* 配载的运次按配载单下发整趟车的信息,运单级字段(运费、当前节点、打卡节点)留空——
|
||||
* 一趟车里每张子单各有各的项目配置与打卡节点。
|
||||
*/
|
||||
private DriverWaybillCardVO toTripCard(TripVO trip, boolean withDetail) {
|
||||
DriverWaybillCardVO card = Func.isEmpty(trip.getLoadingNo()) && withDetail
|
||||
? unloadedTripDetail(trip)
|
||||
: new DriverWaybillCardVO();
|
||||
fillTripFields(card, trip, withDetail);
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* 未配载运次的详情照运单详情拼,只读:不调用 normalizeAcceptStatus
|
||||
*/
|
||||
private DriverWaybillCardVO unloadedTripDetail(TripVO trip) {
|
||||
Long waybillId = parseId(trip.getId());
|
||||
Waybill waybill = waybillId == null ? null : waybillService.getById(waybillId);
|
||||
return waybill == null ? new DriverWaybillCardVO() : toCard(waybill, true);
|
||||
}
|
||||
|
||||
private void fillTripFields(DriverWaybillCardVO card, TripVO trip, boolean withDetail) {
|
||||
List<TripVO.TripWaybillVO> children = trip.getWaybills() == null ? List.of() : trip.getWaybills();
|
||||
card.setTripId(trip.getId());
|
||||
card.setTripNo(Func.toStr(trip.getTripNo(), ""));
|
||||
card.setLoadingNo(Func.toStr(trip.getLoadingNo(), ""));
|
||||
// 旧字段 id / waybillNo 保留原语义:未配载就是这张运单,配载取下第一张可见子运单(运次操作请用 tripId)
|
||||
card.setId(parseId(children.isEmpty() ? trip.getId() : children.get(0).getId()));
|
||||
card.setWaybillNo(children.isEmpty() ? card.getTripNo() : Func.toStr(children.get(0).getWaybillNo(), ""));
|
||||
card.setStatus(trip.getStatus());
|
||||
card.setRequireAccept(trip.getRequireAccept());
|
||||
card.setAcceptStatus(trip.getAcceptStatus());
|
||||
card.setTransportOrgType(trip.getTransportOrgType());
|
||||
card.setTransportType(toTransportTypeLabel(trip.getTransportType()));
|
||||
card.setDriverName(Func.toStr(trip.getDriverName(), ""));
|
||||
card.setDriverPhone(Func.toStr(trip.getDriverPhone(), ""));
|
||||
card.setVehicleNo(Func.toStr(trip.getVehicleNo(), ""));
|
||||
card.setCreateTime(formatDateTime(trip.getSortTime()));
|
||||
card.setPublishTime(card.getCreateTime());
|
||||
// 计划时间与备注:配载运次取配载单,未配载的照运单卡片自己的值,不回填空
|
||||
if (Func.isNotEmpty(card.getLoadingNo())) {
|
||||
card.setPlanShipTime(formatLocalDateYmd(trip.getEstimatedStartDate()));
|
||||
card.setPlanFinishTime(formatLocalDateYmd(trip.getEstimatedEndDate()));
|
||||
card.setRemark(Func.toStr(trip.getTaskRemark(), ""));
|
||||
}
|
||||
card.setFromName(Func.toStrWithEmpty(trip.getDepartureName(), Func.toStr(trip.getDepartureAddress(), "")));
|
||||
card.setFromAddress(Func.toStrWithEmpty(trip.getDepartureAddress(), card.getFromName()));
|
||||
card.setToName(Func.toStrWithEmpty(trip.getArrivalName(), Func.toStr(trip.getArrivalAddress(), "")));
|
||||
card.setToAddress(Func.toStrWithEmpty(trip.getArrivalAddress(), card.getToName()));
|
||||
card.setViaName(Func.toStr(trip.getTransitName(), ""));
|
||||
card.setStationCount(trip.getStationCount());
|
||||
card.setPickupAddress(card.getFromAddress());
|
||||
card.setUnloadAddress(card.getToAddress());
|
||||
// 货物名可能是一格里的多个名字,按既有拆分口径摊平后再去重
|
||||
List<String> cargoNames = children.stream()
|
||||
.flatMap(child -> splitCargoNames(child.getCargoName()).stream())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
card.setCargoNames(cargoNames);
|
||||
card.setCargoName(String.join("、", cargoNames));
|
||||
card.setCargoCategory(children.stream()
|
||||
.map(TripVO.TripWaybillVO::getCargoType)
|
||||
.filter(Func::isNotEmpty)
|
||||
.distinct()
|
||||
.collect(Collectors.joining("、")));
|
||||
card.setCargoSummary(Func.toStr(trip.getCargoSummary(), ""));
|
||||
String weight = trip.getTotalQuantity() == null
|
||||
? card.getCargoSummary()
|
||||
: formatWeight(trip.getTotalQuantity(), trip.getQuantityUnit());
|
||||
card.setWeight(weight);
|
||||
card.setCargoQuantity(weight);
|
||||
card.setTotalWeight(weight);
|
||||
if (withDetail) {
|
||||
card.setStations(trip.getStations());
|
||||
card.setRoutePoints(toTripRoutePoints(trip.getStations()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情里的点位列:整趟车的全部站点,站序即途经顺序
|
||||
*/
|
||||
private List<DriverWaybillCardVO.DriverRoutePointVO> toTripRoutePoints(List<TripStationVO> stations) {
|
||||
if (Func.isEmpty(stations)) {
|
||||
return List.of();
|
||||
}
|
||||
return stations.stream().map(station -> {
|
||||
DriverWaybillCardVO.DriverRoutePointVO point = new DriverWaybillCardVO.DriverRoutePointVO();
|
||||
point.setName(Func.toStr(station.getName(), ""));
|
||||
point.setAddress(Func.toStrWithEmpty(station.getAddress(), point.getName()));
|
||||
point.setStatus("pending");
|
||||
return point;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Long parseId(String value) {
|
||||
if (Func.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(value.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int normalizePageNo(Integer current) {
|
||||
return current == null || current < 1 ? 1 : current;
|
||||
}
|
||||
|
||||
private int normalizePageSize(Integer size) {
|
||||
if (size == null || size < 1) {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
return Math.min(size, MAX_PAGE_SIZE);
|
||||
}
|
||||
|
||||
private String toTransportTypeLabel(String transportType) {
|
||||
if (Func.isBlank(transportType)) {
|
||||
return "";
|
||||
|
||||
+2
-2
@@ -135,13 +135,13 @@ final class TripAggregation {
|
||||
for (Waybill child : children) {
|
||||
String status = child == null ? null : child.getBusinessStatus();
|
||||
total++;
|
||||
if (TERMINAL_STATUSES.contains(status)) {
|
||||
if (status != null && TERMINAL_STATUSES.contains(status)) {
|
||||
terminal++;
|
||||
}
|
||||
if (COMPLETED.equals(status)) {
|
||||
completed++;
|
||||
}
|
||||
if (PENDING_STATUSES.contains(status)) {
|
||||
if (status != null && PENDING_STATUSES.contains(status)) {
|
||||
pending++;
|
||||
}
|
||||
}
|
||||
|
||||
+98
-9
@@ -32,6 +32,7 @@ import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.TripQueryVO;
|
||||
import org.springblade.transport.pojo.vo.TripStationVO;
|
||||
import org.springblade.transport.pojo.vo.TripTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripVO;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
@@ -40,6 +41,7 @@ import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
@@ -129,7 +131,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
LoadingManage loading = visibleLoading(id, condition);
|
||||
if (loading != null) {
|
||||
List<Waybill> children = loadChildren(Set.of(loading.getLoadingNo()))
|
||||
List<Waybill> children = loadChildren(Set.of(loading.getLoadingNo()), Set.of())
|
||||
.getOrDefault(loading.getLoadingNo(), List.of());
|
||||
if (children.isEmpty()) {
|
||||
// 子单全部被拒单(或已释放)的配载运次对司机不可见,与列表口径一致
|
||||
@@ -218,6 +220,9 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
|
||||
/**
|
||||
* 把这一页的运次行组装成运次:先批量取配载单与子运单,再逐行拼装
|
||||
* <p>
|
||||
* 页里的行只带分组键与排序键,子运单要按主键/配载单号回查一次:未配载的运次就是那张运单本身,
|
||||
* 状态、接单需求与货物都得从那行真身来,不能拿分组行凑。
|
||||
*/
|
||||
private List<TripVO> assemble(List<Waybill> rows) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
@@ -227,13 +232,25 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
.map(Waybill::getLoadingNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Set<Long> loneIds = rows.stream()
|
||||
.filter(row -> Func.isEmpty(row.getLoadingNo()))
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<String, LoadingManage> loadings = loadLoadings(loadingNos);
|
||||
Map<String, List<Waybill>> children = loadChildren(loadingNos);
|
||||
Map<String, List<Waybill>> children = loadChildren(loadingNos, loneIds);
|
||||
Map<Long, Waybill> loneWaybills = new LinkedHashMap<>();
|
||||
children.values().forEach(group -> group.forEach(child -> loneWaybills.put(child.getId(), child)));
|
||||
List<TripVO> trips = new ArrayList<>(rows.size());
|
||||
for (Waybill row : rows) {
|
||||
String loadingNo = Func.toStr(row.getLoadingNo(), "");
|
||||
if (Func.isEmpty(loadingNo)) {
|
||||
trips.add(waybillTrip(row, false));
|
||||
Waybill waybill = loneWaybills.get(row.getId());
|
||||
if (waybill == null) {
|
||||
// 运单在两次查询之间被删或被拒单:这一页不给出没有子单的运次
|
||||
continue;
|
||||
}
|
||||
trips.add(waybillTrip(waybill, false));
|
||||
continue;
|
||||
}
|
||||
LoadingManage loading = loadings.get(loadingNo);
|
||||
@@ -265,24 +282,26 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 段二:按本页运次的配载单号一次取回子运单,不逐条查
|
||||
* 段二:按本页运次的配载单号与未配载运单主键一次取回子运单,不逐条查
|
||||
* <p>
|
||||
* 子单归属一律以配载单号为准:库层就是按配载单号分组聚合的,服务层复核用别的关联关系,
|
||||
* 同一页里就会出现「Tab 说它在进行中、卡片按子单算出来是待接单」。
|
||||
* 配载单上的运单号集合只是派单时的快照,子单被释放(配载单号清空)后不再属于该运次。
|
||||
*/
|
||||
private Map<String, List<Waybill>> loadChildren(Set<String> loadingNos) {
|
||||
if (loadingNos.isEmpty()) {
|
||||
private Map<String, List<Waybill>> loadChildren(Set<String> loadingNos, Set<Long> loneIds) {
|
||||
if (loadingNos.isEmpty() && loneIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Waybill> children = waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
.in(Waybill::getLoadingNo, loadingNos)
|
||||
.in(Waybill::getBusinessStatus, TripAggregation.CHILD_STATUSES)
|
||||
.and(visible -> visible.isNull(Waybill::getDriverAcceptStatus)
|
||||
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)));
|
||||
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED))
|
||||
.and(scope -> scope.in(Func.isNotEmpty(loadingNos), Waybill::getLoadingNo, loadingNos)
|
||||
.or(Func.isNotEmpty(loneIds), lone -> lone.in(Waybill::getId, loneIds))));
|
||||
Map<String, List<Waybill>> grouped = new LinkedHashMap<>();
|
||||
for (Waybill child : children) {
|
||||
grouped.computeIfAbsent(child.getLoadingNo(), key -> new ArrayList<>()).add(child);
|
||||
String key = Func.isEmpty(child.getLoadingNo()) ? String.valueOf(child.getId()) : child.getLoadingNo();
|
||||
grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(child);
|
||||
}
|
||||
grouped.values().forEach(group -> group.sort(
|
||||
Comparator.comparing(Waybill::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder()))));
|
||||
@@ -310,11 +329,15 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
trip.setDepartureAddress(Func.toStr(loading.getDepartureAddress(), ""));
|
||||
trip.setTransitAddress(Func.toStr(loading.getTransitAddress(), ""));
|
||||
trip.setArrivalAddress(Func.toStr(loading.getArrivalAddress(), ""));
|
||||
trip.setEstimatedStartDate(loading.getEstimatedStartDate());
|
||||
trip.setEstimatedEndDate(loading.getEstimatedEndDate());
|
||||
trip.setTaskRemark(Func.toStr(loading.getTaskRemark(), ""));
|
||||
if (withPayload) {
|
||||
trip.setRouteJson(loading.getRouteJson());
|
||||
trip.setGoodsJson(loading.getGoodsJson());
|
||||
}
|
||||
fillAggregation(trip, children);
|
||||
fillRouteAndCargo(trip, loading, children, withPayload);
|
||||
return trip;
|
||||
}
|
||||
|
||||
@@ -336,11 +359,13 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
trip.setCargoType(Func.toStr(waybill.getCargoType(), ""));
|
||||
trip.setDepartureAddress(Func.toStr(waybill.getDepartureAddress(), ""));
|
||||
trip.setArrivalAddress(Func.toStr(waybill.getArrivalAddress(), ""));
|
||||
trip.setTaskRemark(Func.toStr(waybill.getTaskRemark(), ""));
|
||||
if (withPayload) {
|
||||
trip.setRouteJson(waybill.getRouteJson());
|
||||
trip.setGoodsJson(waybill.getGoodsJson());
|
||||
}
|
||||
fillAggregation(trip, List.of(waybill));
|
||||
fillRouteAndCargo(trip, null, List.of(waybill), withPayload);
|
||||
return trip;
|
||||
}
|
||||
|
||||
@@ -359,6 +384,70 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
trip.setWaybills(children.stream().map(this::toWaybillVO).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点与货物都按整趟车算:站点明细只在详情下发,列表带走起/经/终、站点数与货物汇总
|
||||
*/
|
||||
private void fillRouteAndCargo(TripVO trip, LoadingManage loading, List<Waybill> children, boolean withPayload) {
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading, children);
|
||||
trip.setDepartureName(TripStationAssembler.departureName(stations));
|
||||
trip.setTransitName(TripStationAssembler.transitName(stations));
|
||||
trip.setArrivalName(TripStationAssembler.arrivalName(stations));
|
||||
trip.setStationCount(stations.size());
|
||||
if (withPayload) {
|
||||
trip.setStations(stations);
|
||||
}
|
||||
trip.setCargoSummary(cargoSummary(children));
|
||||
fillTotalQuantity(trip, children);
|
||||
}
|
||||
|
||||
/**
|
||||
* 货物汇总:同名货物合并且单位一致时取数量合计,没有数量的只列货名
|
||||
*/
|
||||
private String cargoSummary(List<Waybill> children) {
|
||||
Map<String, Map<String, BigDecimal>> grouped = new LinkedHashMap<>();
|
||||
for (Waybill child : children) {
|
||||
String name = Func.toStrWithEmpty(child.getCargoName(), "货物");
|
||||
String unit = Func.toStr(child.getQuantityUnit(), "");
|
||||
Map<String, BigDecimal> byUnit = grouped.computeIfAbsent(name, key -> new LinkedHashMap<>());
|
||||
BigDecimal quantity = child.getQuantity() == null ? BigDecimal.ZERO : child.getQuantity();
|
||||
byUnit.merge(unit, quantity, BigDecimal::add);
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
grouped.forEach((name, byUnit) -> byUnit.forEach((unit, quantity) ->
|
||||
parts.add(quantity.signum() == 0 ? name : name + " " + formatQuantity(quantity, unit))));
|
||||
return String.join("、", parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合计重量:只有全部子运单同单位时才能相加,单位不一致时留空(汇总文案里仍分单位列出)
|
||||
*/
|
||||
private void fillTotalQuantity(TripVO trip, List<Waybill> children) {
|
||||
String unit = null;
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (Waybill child : children) {
|
||||
String childUnit = Func.toStr(child.getQuantityUnit(), "");
|
||||
if (child.getQuantity() == null || Func.isEmpty(childUnit)) {
|
||||
return;
|
||||
}
|
||||
if (unit == null) {
|
||||
unit = childUnit;
|
||||
} else if (!unit.equals(childUnit)) {
|
||||
return;
|
||||
}
|
||||
total = total.add(child.getQuantity());
|
||||
}
|
||||
if (unit == null) {
|
||||
return;
|
||||
}
|
||||
trip.setTotalQuantity(total);
|
||||
trip.setQuantityUnit(unit);
|
||||
}
|
||||
|
||||
private String formatQuantity(BigDecimal quantity, String unit) {
|
||||
String value = quantity.stripTrailingZeros().toPlainString();
|
||||
return Func.isEmpty(unit) ? value : value + unit;
|
||||
}
|
||||
|
||||
private TripVO.TripWaybillVO toWaybillVO(Waybill waybill) {
|
||||
TripVO.TripWaybillVO child = new TripVO.TripWaybillVO();
|
||||
child.setId(tripIdOf(waybill.getId()));
|
||||
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.TripStationVO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 运次站点拼装:把一趟车的实际途经顺序与每站的装卸、货物条目拼出来
|
||||
* <p>
|
||||
* 站点顺序优先取配载单的路线信息(有序站点数组);站点标记「装N / 卸N」的 N 指向配载单运单集合里的第 N 张子运单。
|
||||
* 没有路线信息时回退到子运单的起讫点去重成站(保留首次出现顺序),再退到配载单自己的发货/途经/到货地址;
|
||||
* 都没有则给空站点数组,前端按空站点渲染而不是崩。
|
||||
* <p>
|
||||
* 货物条目取子运单而不是配载单的货物信息:配载单的货物条数与子运单不一定同数,行序也对不上站点编号。
|
||||
*/
|
||||
final class TripStationAssembler {
|
||||
|
||||
private static final String LOAD = "load";
|
||||
private static final String UNLOAD = "unload";
|
||||
private static final String LOAD_UNLOAD = "load_unload";
|
||||
/** 途经站点之间的连接符 */
|
||||
private static final String TRANSIT_JOINER = " → ";
|
||||
/** 站点去重用的键分隔符:站名与地址都为空的两个点不该被当成同一个 */
|
||||
private static final String KEY_SEPARATOR = "\u0000";
|
||||
|
||||
private TripStationAssembler() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼装整趟车的站点,站序从 1 开始
|
||||
*
|
||||
* @param loading 配载单,未配载传 null
|
||||
* @param children 该运次的可见子运单
|
||||
*/
|
||||
static List<TripStationVO> assemble(LoadingManage loading, List<Waybill> children) {
|
||||
List<TripStationVO> stations = fromRoute(loading, children);
|
||||
if (stations.isEmpty()) {
|
||||
stations = fromWaybillPoints(children);
|
||||
}
|
||||
if (stations.isEmpty() && loading != null) {
|
||||
stations = fromLoadingAddresses(loading);
|
||||
}
|
||||
for (int i = 0; i < stations.size(); i++) {
|
||||
stations.get(i).setIndex(i + 1);
|
||||
}
|
||||
return stations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 起点名称:首站站名,没有站点时为空
|
||||
*/
|
||||
static String departureName(List<TripStationVO> stations) {
|
||||
return stations.isEmpty() ? "" : Func.toStr(stations.get(0).getName(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 终点名称:末站站名,没有站点时为空
|
||||
*/
|
||||
static String arrivalName(List<TripStationVO> stations) {
|
||||
return stations.isEmpty() ? "" : Func.toStr(stations.get(stations.size() - 1).getName(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 途经站点:中间各站按顺序连接,不足三站为空
|
||||
*/
|
||||
static String transitName(List<TripStationVO> stations) {
|
||||
if (stations.size() < 3) {
|
||||
return "";
|
||||
}
|
||||
List<String> names = new ArrayList<>();
|
||||
for (int i = 1; i < stations.size() - 1; i++) {
|
||||
String name = Func.toStr(stations.get(i).getName(), "");
|
||||
if (Func.isNotEmpty(name)) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
return String.join(TRANSIT_JOINER, names);
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点来自配载单的路线信息:按数组顺序成站,标记里第 N 张子运单按配载单运单集合的顺序取
|
||||
*/
|
||||
private static List<TripStationVO> fromRoute(LoadingManage loading, List<Waybill> children) {
|
||||
if (loading == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Map<String, Object>> route = objectList(loading.getRouteJson());
|
||||
if (route.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> waybillIds = stringList(loading.getWaybillIdsJson());
|
||||
Map<String, Waybill> byId = new LinkedHashMap<>();
|
||||
for (Waybill child : children) {
|
||||
if (child.getId() != null) {
|
||||
byId.put(String.valueOf(child.getId()), child);
|
||||
}
|
||||
}
|
||||
List<TripStationVO> stations = new ArrayList<>();
|
||||
for (Map<String, Object> element : route) {
|
||||
String address = stringValue(element.get("address"));
|
||||
String display = stringValue(element.get("displayAddress"));
|
||||
TripStationVO station = new TripStationVO();
|
||||
station.setName(Func.toStrWithEmpty(display, address));
|
||||
station.setAddress(Func.toStrWithEmpty(address, station.getName()));
|
||||
List<String> tags = stringList(element.get("tags"));
|
||||
station.setTags(tags);
|
||||
List<TripStationVO.TripCargoVO> cargos = new ArrayList<>();
|
||||
boolean load = false;
|
||||
boolean unload = false;
|
||||
for (String tag : tags) {
|
||||
boolean loadTag = tag.contains("装");
|
||||
boolean unloadTag = tag.contains("卸");
|
||||
if (!loadTag && !unloadTag) {
|
||||
continue;
|
||||
}
|
||||
Waybill child = childOfTag(tag, waybillIds, byId);
|
||||
if (child == null) {
|
||||
continue;
|
||||
}
|
||||
load |= loadTag;
|
||||
unload |= unloadTag;
|
||||
addCargo(cargos, child, loadTag ? LOAD : UNLOAD);
|
||||
if (loadTag && unloadTag) {
|
||||
addCargo(cargos, child, UNLOAD);
|
||||
}
|
||||
}
|
||||
fillOperation(station, load, unload);
|
||||
station.setCargos(cargos);
|
||||
stations.add(station);
|
||||
}
|
||||
return stations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点回退到子运单的起讫点:按子运单顺序每张的起点、终点依次成站,同名同址只留首次出现
|
||||
*/
|
||||
private static List<TripStationVO> fromWaybillPoints(List<Waybill> children) {
|
||||
Map<String, TripStationVO> stations = new LinkedHashMap<>();
|
||||
Map<String, boolean[]> operations = new LinkedHashMap<>();
|
||||
for (Waybill child : children) {
|
||||
String fromName = pointName(child.getDepartureName(), child.getDepartureAddress());
|
||||
String fromAddress = Func.toStrWithEmpty(child.getDepartureAddress(), fromName);
|
||||
String toName = pointName(child.getArrivalName(), child.getArrivalAddress());
|
||||
String toAddress = Func.toStrWithEmpty(child.getArrivalAddress(), toName);
|
||||
point(stations, operations, fromName, fromAddress, true, child);
|
||||
point(stations, operations, toName, toAddress, false, child);
|
||||
}
|
||||
operations.forEach((key, flags) -> fillOperation(stations.get(key), flags[0], flags[1]));
|
||||
return new ArrayList<>(stations.values());
|
||||
}
|
||||
|
||||
private static void point(Map<String, TripStationVO> stations, Map<String, boolean[]> operations,
|
||||
String name, String address, boolean load, Waybill child) {
|
||||
if (Func.isEmpty(name) && Func.isEmpty(address)) {
|
||||
return;
|
||||
}
|
||||
String key = name + KEY_SEPARATOR + address;
|
||||
TripStationVO station = stations.get(key);
|
||||
if (station == null) {
|
||||
station = new TripStationVO();
|
||||
station.setName(name);
|
||||
station.setAddress(address);
|
||||
station.setTags(new ArrayList<>());
|
||||
station.setCargos(new ArrayList<>());
|
||||
stations.put(key, station);
|
||||
operations.put(key, new boolean[2]);
|
||||
}
|
||||
boolean[] flags = operations.get(key);
|
||||
flags[0] |= load;
|
||||
flags[1] |= !load;
|
||||
addCargo(station.getCargos(), child, load ? LOAD : UNLOAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点再退到配载单自己的发货 / 途经 / 到货地址
|
||||
*/
|
||||
private static List<TripStationVO> fromLoadingAddresses(LoadingManage loading) {
|
||||
List<TripStationVO> stations = new ArrayList<>();
|
||||
for (String address : List.of(
|
||||
Func.toStr(loading.getDepartureAddress(), ""),
|
||||
Func.toStr(loading.getTransitAddress(), ""),
|
||||
Func.toStr(loading.getArrivalAddress(), ""))) {
|
||||
if (Func.isEmpty(address)) {
|
||||
continue;
|
||||
}
|
||||
TripStationVO station = new TripStationVO();
|
||||
station.setName(address);
|
||||
station.setAddress(address);
|
||||
station.setTags(new ArrayList<>());
|
||||
station.setCargos(new ArrayList<>());
|
||||
stations.add(station);
|
||||
}
|
||||
return stations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点标记里第 N 张子运单:N 是配载单运单集合里的位次,不是可见子单里的位次
|
||||
*/
|
||||
private static Waybill childOfTag(String tag, List<String> waybillIds, Map<String, Waybill> byId) {
|
||||
String digits = tag.replaceAll("\\D+", "");
|
||||
if (Func.isEmpty(digits)) {
|
||||
return null;
|
||||
}
|
||||
int order = Func.toInt(digits, 0);
|
||||
if (order < 1 || order > waybillIds.size()) {
|
||||
return null;
|
||||
}
|
||||
return byId.get(waybillIds.get(order - 1));
|
||||
}
|
||||
|
||||
private static void addCargo(List<TripStationVO.TripCargoVO> cargos, Waybill child, String operation) {
|
||||
if (child == null) {
|
||||
return;
|
||||
}
|
||||
String waybillId = child.getId() == null ? null : String.valueOf(child.getId());
|
||||
boolean exists = cargos.stream().anyMatch(cargo ->
|
||||
Objects.equals(cargo.getWaybillId(), waybillId) && Objects.equals(cargo.getOperation(), operation));
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
TripStationVO.TripCargoVO cargo = new TripStationVO.TripCargoVO();
|
||||
cargo.setWaybillId(waybillId);
|
||||
cargo.setWaybillNo(child.getWaybillNo());
|
||||
cargo.setCargoName(child.getCargoName());
|
||||
cargo.setCustomerName(child.getCustomerName());
|
||||
cargo.setQuantity(child.getQuantity());
|
||||
cargo.setQuantityUnit(child.getQuantityUnit());
|
||||
cargo.setOperation(operation);
|
||||
cargo.setProjectName(child.getProjectName());
|
||||
cargos.add(cargo);
|
||||
}
|
||||
|
||||
private static void fillOperation(TripStationVO station, boolean load, boolean unload) {
|
||||
if (load && unload) {
|
||||
station.setOperationType(LOAD_UNLOAD);
|
||||
station.setOperationTypeLabel("装卸");
|
||||
} else if (load) {
|
||||
station.setOperationType(LOAD);
|
||||
station.setOperationTypeLabel("装货");
|
||||
} else if (unload) {
|
||||
station.setOperationType(UNLOAD);
|
||||
station.setOperationTypeLabel("卸货");
|
||||
} else {
|
||||
station.setOperationType(null);
|
||||
station.setOperationTypeLabel("");
|
||||
}
|
||||
}
|
||||
|
||||
private static String pointName(String name, String address) {
|
||||
return Func.toStrWithEmpty(name, Func.toStr(address, ""));
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> objectList(String json) {
|
||||
if (Func.isEmpty(json)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, List.class);
|
||||
if (parsed instanceof List<?> list) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
if (item instanceof Map<?, ?> map) {
|
||||
Map<String, Object> element = new LinkedHashMap<>();
|
||||
map.forEach((key, value) -> element.put(String.valueOf(key), value));
|
||||
result.add(element);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 路线信息坏了不该让整趟车读不出来,按没有路线信息回退
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private static List<String> stringList(Object value) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (value instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
String text = stringValue(item);
|
||||
if (Func.isNotEmpty(text)) {
|
||||
result.add(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
String text = stringValue(value);
|
||||
if (Func.isEmpty(text)) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(text, List.class);
|
||||
if (parsed instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
String item2 = stringValue(item);
|
||||
if (Func.isNotEmpty(item2)) {
|
||||
result.add(item2);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 不是 JSON 数组就当逗号分隔处理
|
||||
}
|
||||
return Func.toStrList(",", text);
|
||||
}
|
||||
|
||||
private static String stringValue(Object value) {
|
||||
return value == null ? "" : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -28,6 +28,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -100,7 +101,8 @@ class DriverNodePunchPhotoTest {
|
||||
mock(CertificationGate.class),
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper);
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
|
||||
when(driverService.getOne(any())).thenReturn(driver());
|
||||
when(waybillService.getOne(any())).thenReturn(waybill());
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -92,7 +93,8 @@ class DriverPunchNodeCardTest {
|
||||
mock(CertificationGate.class),
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper);
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
|
||||
Waybill waybill = waybill();
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill);
|
||||
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.TripQueryVO;
|
||||
import org.springblade.transport.pojo.vo.TripVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 运次级接单与拒单:一次写完整趟车、写不上已完成/已取消的子单、司机字段一概不动。
|
||||
* <p>
|
||||
* 运次的解析与收口在运次读服务里,这里以 mock 给出「该司机可见的运次」,只验运次级校验与写入口径。
|
||||
*/
|
||||
class DriverTripAcceptRejectTest {
|
||||
|
||||
private static final Long USER_ID = 7401L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PHONE = "17587421208";
|
||||
private static final String TRIP_ID = "2101146192739459074";
|
||||
private static final Long PENDING_ID = 2101144166395699202L;
|
||||
private static final Long SECOND_ID = 2101145869819994113L;
|
||||
|
||||
private IWaybillService waybillService;
|
||||
private IDriverService driverService;
|
||||
private CertificationGate certificationGate;
|
||||
private ITripQueryService tripQueryService;
|
||||
private DriverWaybillServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
waybillService = mock(IWaybillService.class);
|
||||
driverService = mock(IDriverService.class);
|
||||
certificationGate = mock(CertificationGate.class);
|
||||
tripQueryService = mock(ITripQueryService.class);
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
certificationGate,
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
tripQueryService);
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(certificationGate.isDriverAvailable(any())).thenReturn(true);
|
||||
lenient().when(waybillService.update(any())).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次接单:一次把两张子运单都写成已接单 + 运输中 + 同一接单时刻,司机字段一个不碰")
|
||||
void tripAcceptWritesWholeTrip() {
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "pending", "pending"),
|
||||
requireAcceptChild(SECOND_ID, "running", null));
|
||||
|
||||
boolean result = runAsDriver(() -> service.tripAccept(TRIP_ID));
|
||||
|
||||
assertTrue(result);
|
||||
assertEquals(List.of(
|
||||
"driver_accept_status",
|
||||
"driver_accept_time",
|
||||
"driver_accept_driver_id",
|
||||
"driver_reject_time",
|
||||
"driver_reject_reason",
|
||||
"business_status"), updatedColumns(), "只写接单相关的列,司机字段一概不动");
|
||||
assertTrue(capturedUpdateParams().containsAll(List.of(PENDING_ID, SECOND_ID, DRIVER_ID)),
|
||||
"两张子运单与接单司机一起下发:" + capturedUpdateParams());
|
||||
assertTrue(capturedWhereSql().contains("(id IN ("), capturedWhereSql());
|
||||
assertEquals(TRIP_ID, capturedQuery().getTripId());
|
||||
assertEquals(PHONE, capturedQuery().getDriverPhone(), "归属仍按司机手机号收口");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("任一子单已接单 → 整单已接单,不再重复写")
|
||||
void tripAcceptRefusesSecondCall() {
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "running", "accepted"),
|
||||
requireAcceptChild(SECOND_ID, "running", "accepted"));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID)));
|
||||
|
||||
assertEquals("该运单已接单", exception.getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无接单节点的运次不进接单流程:给出「该运单无需确认接单」")
|
||||
void tripAcceptWithoutAcceptNodeIsRefused() {
|
||||
TripVO.TripWaybillVO first = child(PENDING_ID, "running", null, false);
|
||||
TripVO.TripWaybillVO second = child(SECOND_ID, "running", null, false);
|
||||
stubTrip(first, second);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID)));
|
||||
|
||||
assertEquals("该运单无需确认接单", exception.getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("接单只写没走完的子单,已完成的子单不回到运输中")
|
||||
void tripAcceptSkipsTerminalChildren() {
|
||||
// 已完成的子单(自动完成,没有接单状态)不进写入集合;另一张还在途,整趟车仍可接单
|
||||
stubTrip(new TripVO.TripWaybillVO[]{
|
||||
child(PENDING_ID, "completed", null, true),
|
||||
requireAcceptChild(SECOND_ID, "running", null)});
|
||||
|
||||
runAsDriver(() -> service.tripAccept(TRIP_ID));
|
||||
|
||||
assertTrue(capturedUpdateParams().contains(SECOND_ID));
|
||||
assertFalse(capturedUpdateParams().contains(PENDING_ID), "已完成的子单不进写入集合:" + capturedUpdateParams());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("子单全走完的运次不允许接单")
|
||||
void tripAcceptAllTerminalIsRefused() {
|
||||
stubTrip(new TripVO.TripWaybillVO[]{
|
||||
child(PENDING_ID, "completed", "accepted", true),
|
||||
child(SECOND_ID, "cancelled", null, true)});
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID)));
|
||||
|
||||
assertEquals("当前运单状态不允许接单", exception.getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机与车辆的认证门槛按整车只校验一次")
|
||||
void tripAcceptChecksCertificationOnce() {
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "pending", "pending"),
|
||||
requireAcceptChild(SECOND_ID, "pending", "pending"));
|
||||
when(certificationGate.isDriverAvailable(any())).thenReturn(false);
|
||||
when(certificationGate.driverUnavailableReason()).thenReturn("司机认证未通过");
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID)));
|
||||
|
||||
assertEquals("司机认证未通过", exception.getMessage());
|
||||
verify(certificationGate, times(1)).isDriverAvailable(any());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次拒单:两张子运单写同一个原因、同一拒单时刻,接单记录清空,司机字段不动")
|
||||
void tripRejectWritesWholeTrip() {
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "pending", "pending"),
|
||||
requireAcceptChild(SECOND_ID, "running", null));
|
||||
|
||||
boolean result = runAsDriver(() -> service.tripReject(TRIP_ID, " 车坏了 "));
|
||||
|
||||
assertTrue(result);
|
||||
assertEquals(List.of(
|
||||
"driver_accept_status",
|
||||
"driver_accept_time",
|
||||
"driver_accept_driver_id",
|
||||
"driver_reject_time",
|
||||
"driver_reject_reason",
|
||||
"business_status"), updatedColumns(), "只写拒单相关的列,司机字段一概不动");
|
||||
assertTrue(capturedUpdateParams().contains("车坏了"), "拒单原因原样下发(去首尾空格):" + capturedUpdateParams());
|
||||
assertTrue(capturedUpdateParams().containsAll(List.of(PENDING_ID, SECOND_ID)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("拒单原因可空,超 200 字直接拒绝且不写库")
|
||||
void tripRejectReasonValidation() {
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "pending", "pending"));
|
||||
|
||||
assertTrue(runAsDriver(() -> service.tripReject(TRIP_ID, null)));
|
||||
assertTrue(capturedUpdateParams().stream().noneMatch(""::equals), "空原因按 null 下发");
|
||||
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "pending", "pending"));
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripReject(TRIP_ID, "x".repeat(201))));
|
||||
|
||||
assertEquals("拒绝原因不能超过200字", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("任一子单已接单 → 整单不能拒单")
|
||||
void tripRejectRefusesAcceptedTrip() {
|
||||
stubTrip(requireAcceptChild(PENDING_ID, "running", "accepted"),
|
||||
requireAcceptChild(SECOND_ID, "pending", "pending"));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripReject(TRIP_ID, "不要了")));
|
||||
|
||||
assertEquals("该运单已接单,无法拒绝", exception.getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("子单全走完的运次不允许拒单")
|
||||
void tripRejectAllTerminalIsRefused() {
|
||||
stubTrip(new TripVO.TripWaybillVO[]{
|
||||
child(PENDING_ID, "completed", null, true),
|
||||
child(SECOND_ID, "completed", null, true)});
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripReject(TRIP_ID, "不要了")));
|
||||
|
||||
assertEquals("当前运单状态不允许拒绝接单", exception.getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配载运单只写它自己")
|
||||
void unloadedTripWritesItselfOnly() {
|
||||
TripVO trip = new TripVO();
|
||||
trip.setId(String.valueOf(PENDING_ID));
|
||||
trip.setWaybills(List.of(requireAcceptChild(PENDING_ID, "pending", "pending")));
|
||||
when(tripQueryService.detail(any())).thenReturn(trip);
|
||||
|
||||
assertTrue(runAsDriver(() -> service.tripAccept(String.valueOf(PENDING_ID))));
|
||||
|
||||
assertTrue(capturedUpdateParams().contains(PENDING_ID));
|
||||
assertEquals(1, capturedUpdateParams().stream().filter(PENDING_ID::equals).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("越权与不存在的运次一律被拒,且不写库")
|
||||
void foreignTripIsRefused() {
|
||||
when(tripQueryService.detail(any())).thenThrow(new ServiceException("运次不存在或无权操作"));
|
||||
|
||||
assertEquals("运次不存在或无权操作", assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID))).getMessage());
|
||||
assertEquals("运次不存在或无权操作", assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripReject(TRIP_ID, null))).getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次里一张可见子单都没有时不给出写入入口")
|
||||
void emptyChildrenAreRefused() {
|
||||
TripVO trip = new TripVO();
|
||||
trip.setId(TRIP_ID);
|
||||
when(tripQueryService.detail(any())).thenReturn(trip);
|
||||
|
||||
assertEquals("运次不存在或无权操作", assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID))).getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private <T> T runAsDriver(Supplier<T> action) {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
private void stubTrip(TripVO.TripWaybillVO... children) {
|
||||
stubTrip(new TripVO.TripWaybillVO[0], children);
|
||||
}
|
||||
|
||||
private void stubTrip(TripVO.TripWaybillVO[] leading, TripVO.TripWaybillVO... children) {
|
||||
TripVO trip = new TripVO();
|
||||
trip.setId(TRIP_ID);
|
||||
trip.setDriverPhone(PHONE);
|
||||
List<TripVO.TripWaybillVO> all = new java.util.ArrayList<>(List.of(leading));
|
||||
all.addAll(List.of(children));
|
||||
trip.setWaybills(all);
|
||||
when(tripQueryService.detail(any())).thenReturn(trip);
|
||||
}
|
||||
|
||||
private TripVO.TripWaybillVO requireAcceptChild(Long id, String businessStatus, String acceptStatus) {
|
||||
return child(id, businessStatus, acceptStatus, true);
|
||||
}
|
||||
|
||||
private TripVO.TripWaybillVO child(Long id, String businessStatus, String acceptStatus, boolean requireAccept) {
|
||||
TripVO.TripWaybillVO child = new TripVO.TripWaybillVO();
|
||||
child.setId(String.valueOf(id));
|
||||
child.setWaybillNo("YD-" + id);
|
||||
child.setBusinessStatus(businessStatus);
|
||||
child.setAcceptStatus(acceptStatus);
|
||||
child.setRequireAccept(requireAccept);
|
||||
return child;
|
||||
}
|
||||
|
||||
private Driver driver() {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(DRIVER_ID);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄宇富");
|
||||
driver.setDrivingVehicle("桂A32523");
|
||||
driver.setMobile(PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
private TripQueryVO capturedQuery() {
|
||||
ArgumentCaptor<TripQueryVO> captor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService, org.mockito.Mockito.atLeastOnce()).detail(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private LambdaUpdateWrapper<Waybill> capturedUpdate() {
|
||||
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(waybillService, org.mockito.Mockito.atLeastOnce()).update(captor.capture());
|
||||
return (LambdaUpdateWrapper<Waybill>) captor.getValue();
|
||||
}
|
||||
|
||||
private String capturedWhereSql() {
|
||||
return capturedUpdate().getTargetSql();
|
||||
}
|
||||
|
||||
/** SET 子句里真正被写的列名:司机字段是否被碰,看这张清单最直白 */
|
||||
private List<String> updatedColumns() {
|
||||
return java.util.Arrays.stream(capturedUpdate().getSqlSet().split(","))
|
||||
.map(part -> part.substring(0, part.indexOf('=')).trim())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** SET 与 WHERE 的参数都是渲染时才进参数表的,取值前先把两段都渲染一遍 */
|
||||
private Collection<Object> capturedUpdateParams() {
|
||||
LambdaUpdateWrapper<Waybill> update = capturedUpdate();
|
||||
update.getSqlSet();
|
||||
update.getTargetSql();
|
||||
return update.getParamNameValuePairs().values();
|
||||
}
|
||||
|
||||
}
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripQueryVO;
|
||||
import org.springblade.transport.pojo.vo.TripStationVO;
|
||||
import org.springblade.transport.pojo.vo.TripTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 司机端运次接口:五个入口切运次口径后的卡片与分页,以及未配载运次照运单详情的只读下发。
|
||||
* <p>
|
||||
* 运次侧的取数与聚合在运次服务里,这里只验司机端的映射与收口;运次服务以 mock 给出「库层已经算好的运次」。
|
||||
*/
|
||||
class DriverTripCardTest {
|
||||
|
||||
private static final Long USER_ID = 7401L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PHONE = "17587421208";
|
||||
private static final String LOADING_NO = "PZ-20261017-001";
|
||||
private static final Long LOADING_ID = 2111350784070275123L;
|
||||
private static final Long WAYBILL_ID = 2101508430981652482L;
|
||||
private static final String ROUTE_JSON = "["
|
||||
+ "{\"address\":\"广西壮族自治区南宁市南宁吴圩机场\",\"displayAddress\":\"广西壮族自治区南宁市南宁吴圩机场\",\"tags\":[\"装1\"]},"
|
||||
+ "{\"address\":\"广西壮族自治区北海市合浦县白沙镇广西新福兴硅科技有限公司\","
|
||||
+ "\"displayAddress\":\"广西壮族自治区北海市合浦县白沙镇广西新福兴硅科技有限公司\",\"tags\":[\"卸1\",\"装2\",\"装3\",\"装4\"]},"
|
||||
+ "{\"address\":\"广西壮族自治区南宁市宾阳县宾州镇宾阳县宾阳县人民政府\","
|
||||
+ "\"displayAddress\":\"广西壮族自治区南宁市宾阳县宾州镇宾阳县宾阳县人民政府\",\"tags\":[\"卸2\",\"卸3\",\"卸4\"]}]";
|
||||
private static final String WAYBILL_IDS_JSON =
|
||||
"[\"2102780956493299713\", \"2092186187239780353\", \"2092186187470467073\", \"2092186187571130369\"]";
|
||||
|
||||
private IWaybillService waybillService;
|
||||
private IDriverService driverService;
|
||||
private IMpUserClient mpUserClient;
|
||||
private ITripQueryService tripQueryService;
|
||||
private WaybillEnroutePunchMapper enroutePunchMapper;
|
||||
private WaybillNodePunchMapper nodePunchMapper;
|
||||
private DriverWaybillServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
com.baomidou.mybatisplus.core.MybatisConfiguration configuration =
|
||||
new com.baomidou.mybatisplus.core.MybatisConfiguration();
|
||||
org.apache.ibatis.builder.MapperBuilderAssistant assistant =
|
||||
new org.apache.ibatis.builder.MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
waybillService = mock(IWaybillService.class);
|
||||
driverService = mock(IDriverService.class);
|
||||
mpUserClient = mock(IMpUserClient.class);
|
||||
tripQueryService = mock(ITripQueryService.class);
|
||||
enroutePunchMapper = mock(WaybillEnroutePunchMapper.class);
|
||||
nodePunchMapper = mock(WaybillNodePunchMapper.class);
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mpUserClient,
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
tripQueryService);
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(enroutePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(enroutePunchMapper.selectOne(any())).thenReturn(null);
|
||||
lenient().when(nodePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次列表按运次分页:一页 10 个运次,卡片带运次号、整趟车起/经/终与站点数")
|
||||
void tripPageMapsTripCard() {
|
||||
stubTripPage(1, loadingTrip());
|
||||
TripQueryVO given = null;
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(1, 10, null, null));
|
||||
|
||||
given = capturedQuery();
|
||||
assertEquals(PHONE, given.getDriverPhone());
|
||||
assertEquals(1, given.getCurrent());
|
||||
assertEquals(10, given.getSize(), "一页 10 条即 10 个运次");
|
||||
assertNull(given.getStatus());
|
||||
assertNull(given.getTripId());
|
||||
|
||||
DriverWaybillCardVO card = page.getRecords().get(0);
|
||||
assertEquals(String.valueOf(LOADING_ID), card.getTripId());
|
||||
assertEquals(LOADING_NO, card.getLoadingNo());
|
||||
assertEquals(LOADING_NO, card.getTripNo(), "有配载取配载单号");
|
||||
assertEquals("load", card.getTransportOrgType());
|
||||
assertEquals(1, card.getStatus(), "四张子单都在运输中");
|
||||
assertEquals(Boolean.FALSE, card.getRequireAccept());
|
||||
assertEquals("广西壮族自治区南宁市南宁吴圩机场", card.getFromName());
|
||||
assertEquals("广西壮族自治区北海市合浦县白沙镇广西新福兴硅科技有限公司", card.getViaName());
|
||||
assertEquals("广西壮族自治区南宁市宾阳县宾州镇宾阳县宾阳县人民政府", card.getToName());
|
||||
assertEquals(3, card.getStationCount());
|
||||
assertEquals(List.of("石英砂A", "技嘉4070显卡"), card.getCargoNames());
|
||||
assertEquals("石英砂A 131.84吨、技嘉4070显卡 30吨", card.getCargoSummary());
|
||||
assertEquals("161.84吨", card.getWeight(), "合计重量按同单位相加");
|
||||
assertEquals("161.84吨", card.getTotalWeight());
|
||||
assertEquals("桂A32523", card.getVehicleNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次列表不因配载卡去回查运单,也不写库")
|
||||
void tripPageDoesNotLookUpWaybillsOrWrite() {
|
||||
stubTripPage(1, loadingTrip());
|
||||
|
||||
runAsDriver(() -> service.tripPage(1, 10, 1, "PZ-20261017"));
|
||||
|
||||
verify(waybillService, never()).getById(any());
|
||||
verify(waybillService, never()).list(anyWrapper());
|
||||
verify(waybillService, never()).updateById(any());
|
||||
verify(waybillService, never()).update(any(), any());
|
||||
verify(waybillService, never()).syncDriverAcceptState(any());
|
||||
assertEquals(1, capturedQuery().getStatus());
|
||||
assertEquals("PZ-20261017", capturedQuery().getKeyword());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次卡片总数就是运次数:总数与每页条数按运次传下去")
|
||||
void tripPageTotalCountsTrips() {
|
||||
Page<TripVO> trips = new Page<>(1, 10, 71);
|
||||
trips.setRecords(List.of(loadingTrip()));
|
||||
when(tripQueryService.page(any())).thenReturn(trips);
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(1, 10, null, null));
|
||||
|
||||
assertEquals(71, page.getTotal(), "总数按运次算");
|
||||
assertEquals(10, page.getSize());
|
||||
assertEquals(1, page.getRecords().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配载运单仍是独立运次:主键是运单主键的字符串形式,未被取整")
|
||||
void unloadedTripKeepsStringPrimaryKey() {
|
||||
stubTripPage(1, unloadedTrip());
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(1, 10, null, null));
|
||||
|
||||
DriverWaybillCardVO card = page.getRecords().get(0);
|
||||
assertEquals("2101508430981652482", card.getTripId());
|
||||
assertEquals(19, card.getTripId().length());
|
||||
assertEquals(Long.valueOf(WAYBILL_ID), card.getId(), "旧字段仍是运单主键");
|
||||
assertEquals("", card.getLoadingNo());
|
||||
assertEquals("YD-20260920-0003", card.getTripNo());
|
||||
assertEquals("common", card.getTransportOrgType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Tab 统计按运次计,全部=待接单+进行中+已完成")
|
||||
void tripTabCountsArePerTrip() {
|
||||
TripTabCountsVO counts = new TripTabCountsVO();
|
||||
counts.setPending(3);
|
||||
counts.setDoing(24);
|
||||
counts.setDone(44);
|
||||
counts.setAll(71);
|
||||
when(tripQueryService.tabCounts(any())).thenReturn(counts);
|
||||
|
||||
DriverWaybillTabCountsVO result = runAsDriver(() -> service.tripTabCounts());
|
||||
|
||||
assertEquals(3, result.getPending());
|
||||
assertEquals(24, result.getDoing());
|
||||
assertEquals(44, result.getDone());
|
||||
assertEquals(71, result.getAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首页当前任务取运输中运次,最多一条")
|
||||
void tripCurrentTaskTakesRunningTrip() {
|
||||
stubTripPage(1, loadingTrip());
|
||||
|
||||
DriverWaybillCardVO card = runAsDriver(() -> service.tripCurrentTask());
|
||||
|
||||
assertNotNull(card);
|
||||
assertEquals(1, capturedQuery().getStatus());
|
||||
assertEquals(1, capturedQuery().getSize());
|
||||
|
||||
Page<TripVO> empty = new Page<>(1, 1, 0);
|
||||
when(tripQueryService.page(any())).thenReturn(empty);
|
||||
assertNull(runAsDriver(() -> service.tripCurrentTask()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首页待接预览按运次计,总数是待接运次数")
|
||||
void tripPendingPreviewCountsTrips() {
|
||||
Page<TripVO> trips = new Page<>(1, 2, 3);
|
||||
trips.setRecords(List.of(loadingTrip(), unloadedTrip()));
|
||||
when(tripQueryService.page(any())).thenReturn(trips);
|
||||
|
||||
DriverWaybillPreviewVO preview = runAsDriver(() -> service.tripPendingPreview(null));
|
||||
|
||||
assertEquals(3, preview.getTotal());
|
||||
assertEquals(2, preview.getRecords().size());
|
||||
assertEquals(0, capturedQuery().getStatus());
|
||||
assertEquals(2, capturedQuery().getSize(), "不传时预览两条");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运次详情的站点是整趟车:三站按途经顺序、第二站装卸、货物明细带运单号")
|
||||
void tripDetailCarriesWholeCarStations() {
|
||||
stubTripDetail(loadingTrip());
|
||||
|
||||
DriverWaybillCardVO card = runAsDriver(() -> service.tripDetail(String.valueOf(LOADING_ID)));
|
||||
|
||||
assertEquals(String.valueOf(LOADING_ID), card.getTripId());
|
||||
assertEquals(3, card.getRoutePoints().size(), "装卸点是整趟车的三站,不再只有本票两点");
|
||||
assertEquals(3, card.getStations().size());
|
||||
assertEquals(1, card.getStations().get(0).getIndex());
|
||||
assertEquals("装货", card.getStations().get(0).getOperationTypeLabel());
|
||||
assertEquals("装卸", card.getStations().get(1).getOperationTypeLabel());
|
||||
assertEquals("卸货", card.getStations().get(2).getOperationTypeLabel());
|
||||
assertEquals("YD-20260923-0008", card.getStations().get(1).getCargos().get(0).getWaybillNo(),
|
||||
"货物明细里能看到运单号");
|
||||
assertEquals("广西壮族自治区南宁市南宁吴圩机场", card.getRoutePoints().get(0).getName());
|
||||
verify(waybillService, never()).getById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配载运次详情照运单详情下发,且只读:不调用会落库的接单态归一化")
|
||||
void unloadedTripDetailStaysReadOnly() {
|
||||
TripVO trip = unloadedTrip();
|
||||
stubTripDetail(trip);
|
||||
Waybill waybill = unloadedWaybill();
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill);
|
||||
|
||||
DriverWaybillCardVO first = runAsDriver(() -> service.tripDetail(String.valueOf(WAYBILL_ID)));
|
||||
DriverWaybillCardVO second = runAsDriver(() -> service.tripDetail(String.valueOf(WAYBILL_ID)));
|
||||
|
||||
assertEquals("2101508430981652482", first.getTripId());
|
||||
assertEquals("", first.getLoadingNo());
|
||||
assertNotNull(first.getProcessJson(), "运单详情的过程配置照旧下发");
|
||||
assertNotNull(first.getPunchNodes());
|
||||
assertEquals(2, first.getStations().size());
|
||||
assertEquals(2, first.getRoutePoints().size());
|
||||
assertEquals("运单备注", first.getRemark(), "未配载运次的备注仍是运单备注");
|
||||
assertEquals("2026-09-20", first.getPlanShipTime(), "未配载运次的计划时间仍是运单的计划时间");
|
||||
assertEquals("在途", first.getCurrentNode(), "未配载运次的当前节点照旧");
|
||||
assertEquals(first.getStatus(), second.getStatus(), "连读两次状态不漂移");
|
||||
verify(waybillService, never()).syncDriverAcceptState(any());
|
||||
verify(waybillService, never()).updateById(any());
|
||||
verify(waybillService, never()).update(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("收口缺失时不放行:空页、零统计、当前任务为空,且不查运次")
|
||||
void missingPhoneIsRefused() {
|
||||
when(driverService.getOne(any())).thenReturn(null);
|
||||
when(mpUserClient.userById(any())).thenReturn(null);
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(null, null, null, null));
|
||||
DriverWaybillPreviewVO preview = runAsDriver(() -> service.tripPendingPreview(null));
|
||||
DriverWaybillTabCountsVO counts = runAsDriver(() -> service.tripTabCounts());
|
||||
|
||||
assertEquals(0, page.getTotal());
|
||||
assertTrue(page.getRecords().isEmpty());
|
||||
assertEquals(10, page.getSize());
|
||||
assertEquals(0, preview.getTotal());
|
||||
assertTrue(preview.getRecords().isEmpty());
|
||||
assertEquals(0, counts.getAll());
|
||||
assertNull(runAsDriver(() -> service.tripCurrentTask()));
|
||||
assertThrows(ServiceException.class, () -> runAsDriver(() -> service.tripDetail("1")));
|
||||
verify(tripQueryService, never()).page(any());
|
||||
verify(tripQueryService, never()).tabCounts(any());
|
||||
verify(tripQueryService, never()).detail(any());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill> anyWrapper() {
|
||||
return org.mockito.ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any();
|
||||
}
|
||||
|
||||
private <T> T runAsDriver(Supplier<T> action) {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
private void stubTripDetail(TripVO trip) {
|
||||
when(tripQueryService.detail(any())).thenReturn(trip);
|
||||
}
|
||||
|
||||
private void stubTripPage(long total, TripVO... trips) { Page<TripVO> page = new Page<>(1, 10, total);
|
||||
page.setRecords(List.of(trips));
|
||||
when(tripQueryService.page(any())).thenReturn(page);
|
||||
}
|
||||
|
||||
private TripQueryVO capturedQuery() {
|
||||
ArgumentCaptor<TripQueryVO> captor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService, org.mockito.Mockito.atLeastOnce()).page(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private Driver driver() {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(DRIVER_ID);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄宇富");
|
||||
driver.setDrivingVehicle("桂A32523");
|
||||
driver.setMobile(PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
/** 本地库实测的 PZ-20261017-001:4 张子单、3 站、状态运输中 */
|
||||
private TripVO loadingTrip() {
|
||||
// 回查到的子单按创建时间升序(与服务取子单的顺序一致)
|
||||
List<Waybill> children = List.of(
|
||||
tripChild("2092186187239780353", "YD-20260825-0006", "石英砂A", "广西武宣冠超矿业有限公司", "42.10"),
|
||||
tripChild("2092186187470467073", "YD-20260825-0007", "石英砂A", "广西武宣冠超矿业有限公司", "47.57"),
|
||||
tripChild("2092186187571130369", "YD-20260825-0008", "石英砂A", "广西武宣冠超矿业有限公司", "42.17"),
|
||||
tripChild("2102780956493299713", "YD-20260923-0008", "技嘉4070显卡", "9/20测试mk001", "30.00"));
|
||||
LoadingManage loading = new LoadingManage();
|
||||
loading.setId(LOADING_ID);
|
||||
loading.setLoadingNo(LOADING_NO);
|
||||
loading.setRouteJson(ROUTE_JSON);
|
||||
loading.setWaybillIdsJson(WAYBILL_IDS_JSON);
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading, children);
|
||||
|
||||
TripVO trip = new TripVO();
|
||||
trip.setId(String.valueOf(LOADING_ID));
|
||||
trip.setLoadingNo(LOADING_NO);
|
||||
trip.setTripNo(LOADING_NO);
|
||||
trip.setTransportOrgType("load");
|
||||
trip.setStatus(1);
|
||||
trip.setRequireAccept(false);
|
||||
trip.setSortTime(new Date(1787671422000L));
|
||||
trip.setDepartureName(TripStationAssembler.departureName(stations));
|
||||
trip.setTransitName(TripStationAssembler.transitName(stations));
|
||||
trip.setArrivalName(TripStationAssembler.arrivalName(stations));
|
||||
trip.setStationCount(stations.size());
|
||||
trip.setStations(stations);
|
||||
trip.setCargoSummary("石英砂A 131.84吨、技嘉4070显卡 30吨");
|
||||
trip.setTotalQuantity(new BigDecimal("161.84"));
|
||||
trip.setQuantityUnit("吨");
|
||||
trip.setDriverName("黄宇富");
|
||||
trip.setDriverPhone(PHONE);
|
||||
trip.setVehicleNo("桂A32523");
|
||||
trip.setProjectName("武宣建筑原料运输项目");
|
||||
trip.setTaskRemark("多点装卸测试");
|
||||
trip.setWaybills(children.stream().map(this::tripChildVO).toList());
|
||||
return trip;
|
||||
}
|
||||
|
||||
private TripVO unloadedTrip() {
|
||||
Waybill waybill = unloadedWaybill();
|
||||
TripVO trip = new TripVO();
|
||||
trip.setId(String.valueOf(WAYBILL_ID));
|
||||
trip.setTripNo("YD-20260920-0003");
|
||||
trip.setTransportOrgType("common");
|
||||
trip.setStatus(0);
|
||||
trip.setRequireAccept(true);
|
||||
trip.setAcceptStatus("pending");
|
||||
trip.setSortTime(new Date(1787629629000L));
|
||||
trip.setDepartureName("南宁武鸣");
|
||||
trip.setArrivalName("钦州港");
|
||||
trip.setStationCount(2);
|
||||
trip.setCargoSummary("石灰石 30吨");
|
||||
trip.setTotalQuantity(new BigDecimal("30"));
|
||||
trip.setQuantityUnit("吨");
|
||||
trip.setDriverName("黄宇富");
|
||||
trip.setDriverPhone(PHONE);
|
||||
trip.setVehicleNo("桂A32523");
|
||||
trip.setWaybills(List.of(tripChildVO(waybill)));
|
||||
trip.setStations(TripStationAssembler.assemble(null, List.of(waybill)));
|
||||
return trip;
|
||||
}
|
||||
|
||||
private Waybill unloadedWaybill() {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD-20260920-0003");
|
||||
waybill.setBusinessStatus("pending");
|
||||
waybill.setDriverAcceptStatus("pending");
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setVehicleNo("桂A32523");
|
||||
waybill.setDepartureName("南宁武鸣");
|
||||
waybill.setDepartureAddress("南宁武鸣");
|
||||
waybill.setArrivalName("钦州港");
|
||||
waybill.setArrivalAddress("钦州港");
|
||||
waybill.setCargoName("石灰石");
|
||||
waybill.setQuantity(new BigDecimal("30"));
|
||||
waybill.setQuantityUnit("吨");
|
||||
waybill.setProcessJson("[{\"key\":\"accept\",\"name\":\"接单\",\"enabled\":true,\"confirmMode\":\"yes\"}]");
|
||||
waybill.setCreateTime(new Date(1787629629000L));
|
||||
waybill.setRemark("运单备注");
|
||||
waybill.setTaskRemark("任务备注");
|
||||
waybill.setCurrentProcessNode("在途");
|
||||
waybill.setEstimatedStartTime(java.time.LocalDate.of(2026, 9, 20));
|
||||
waybill.setEstimatedEndTime(java.time.LocalDate.of(2026, 9, 22));
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private Waybill tripChild(String id, String waybillNo, String cargoName, String customerName, String quantity) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(Long.valueOf(id));
|
||||
waybill.setWaybillNo(waybillNo);
|
||||
waybill.setCargoName(cargoName);
|
||||
waybill.setCustomerName(customerName);
|
||||
waybill.setQuantity(new BigDecimal(quantity));
|
||||
waybill.setQuantityUnit("吨");
|
||||
waybill.setBusinessStatus("running");
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private TripVO.TripWaybillVO tripChildVO(Waybill waybill) {
|
||||
TripVO.TripWaybillVO child = new TripVO.TripWaybillVO();
|
||||
child.setId(String.valueOf(waybill.getId()));
|
||||
child.setWaybillNo(waybill.getWaybillNo());
|
||||
child.setCargoName(waybill.getCargoName());
|
||||
child.setCargoType("矿石");
|
||||
child.setQuantity(waybill.getQuantity());
|
||||
child.setQuantityUnit(waybill.getQuantityUnit());
|
||||
child.setBusinessStatus(waybill.getBusinessStatus());
|
||||
return child;
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -24,6 +24,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -100,7 +101,8 @@ class DriverWaybillAutoCompleteTest {
|
||||
mock(CertificationGate.class),
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper);
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
|
||||
when(driverService.getOne(any())).thenReturn(driver());
|
||||
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -78,7 +79,8 @@ class DriverWaybillCardLocationTest {
|
||||
mock(CertificationGate.class),
|
||||
enroutePunchMapper,
|
||||
nodePunchMapper,
|
||||
exceptionDisposalMapper);
|
||||
exceptionDisposalMapper,
|
||||
mock(ITripQueryService.class));
|
||||
|
||||
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -60,7 +61,8 @@ class DriverWaybillNodePunchTest {
|
||||
mock(CertificationGate.class),
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
nodePunchMapper,
|
||||
mock(ExceptionDisposalMapper.class));
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
mock(ITripQueryService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
@@ -72,7 +73,8 @@ class DriverWaybillOwnershipTest {
|
||||
mock(CertificationGate.class),
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class));
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
mock(ITripQueryService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+3
-1
@@ -28,6 +28,7 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -88,7 +89,8 @@ class DriverWaybillRejectDetachTest {
|
||||
certificationGate,
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class));
|
||||
mock(ExceptionDisposalMapper.class),
|
||||
mock(ITripQueryService.class));
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(waybillService.updateById(any())).thenReturn(true);
|
||||
// syncDriverAcceptState 是 IWaybillService 的方法,mock 默认返 null 会让调用方 NPE;
|
||||
|
||||
+36
-2
@@ -132,7 +132,10 @@ class TripQueryServiceImplTest {
|
||||
@Test
|
||||
@DisplayName("未配载运单自成一个运次,主键是 19 位运单主键的字符串形式")
|
||||
void unloadedWaybillIsItsOwnTrip() {
|
||||
stubPage(71, waybill(WAYBILL_ID, "YD-20260920-0003", null, "running", null, null));
|
||||
// 页里的分组行只投影出主键与分组键,子运单按主键回查后才是真身
|
||||
stubPage(71, waybill(WAYBILL_ID, null, null, null, null, null));
|
||||
when(waybillService.list(anyWaybillWrapper()))
|
||||
.thenReturn(List.of(waybill(WAYBILL_ID, "YD-20260920-0003", null, "running", null, null)));
|
||||
|
||||
IPage<TripVO> page = service.page(TripQueryVO.forDriver(PHONE));
|
||||
|
||||
@@ -150,6 +153,33 @@ class TripQueryServiceImplTest {
|
||||
assertEquals("2110690671695380513", trip.getWaybills().get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配载运次的状态与接单需求取回查到的运单真身,分组行里没有状态列")
|
||||
void unloadedTripStateComesFromFetchedWaybill() {
|
||||
stubPage(1, waybill(WAYBILL_ID, null, null, null, null, null));
|
||||
when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of(
|
||||
waybill(WAYBILL_ID, "YD-20260920-0003", null, "pending", "pending", null, ACCEPT_YES)));
|
||||
|
||||
TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
|
||||
|
||||
assertEquals(Boolean.TRUE, trip.getRequireAccept());
|
||||
assertEquals("pending", trip.getAcceptStatus());
|
||||
assertEquals(0, trip.getStatus());
|
||||
assertEquals(1, trip.getWaybillCount());
|
||||
assertEquals("YD-20260920-0003", trip.getTripNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配载运单在两次查询之间被删或被拒单时不给出没有子单的运次")
|
||||
void loneWaybillMissingInSecondQueryIsDropped() {
|
||||
stubPage(1, waybill(WAYBILL_ID, null, null, null, null, null));
|
||||
when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of());
|
||||
|
||||
IPage<TripVO> page = service.page(TripQueryVO.forDriver(PHONE));
|
||||
|
||||
assertTrue(page.getRecords().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("配载运次:主键取配载单主键,子单一次批量取回,状态与排序键按子单聚合")
|
||||
void loadingTripAggregatesChildren() {
|
||||
@@ -234,9 +264,13 @@ class TripQueryServiceImplTest {
|
||||
@DisplayName("分页即分运次:总数与顺序照库层给的来,服务端不重排")
|
||||
void pagingIsPerTrip() {
|
||||
stubPage(71,
|
||||
waybill(3L, null, null, null, null, null),
|
||||
waybill(2L, null, null, null, null, null),
|
||||
waybill(1L, null, null, null, null, null));
|
||||
when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of(
|
||||
waybill(3L, "YD-3", null, "running", null, null),
|
||||
waybill(2L, "YD-2", null, "completed", null, null),
|
||||
waybill(1L, "YD-1", null, "running", null, null));
|
||||
waybill(1L, "YD-1", null, "running", null, null)));
|
||||
TripQueryVO query = TripQueryVO.forDriver(PHONE);
|
||||
query.setCurrent(2);
|
||||
query.setSize(10);
|
||||
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.TripStationVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 运次站点拼装:配载单路线信息 → 站点 ①②③,站点标记指向配载单运单集合里的第 N 张子运单。
|
||||
* <p>
|
||||
* 夹具用的是本地库实测的配载单数据(路线信息、运单集合顺序与四张子运单的货名/货主/数量),
|
||||
* 单元测试里没有库,所以「第 N 张」只能按运单集合的位次断言。
|
||||
*/
|
||||
class TripStationAssemblerTest {
|
||||
|
||||
/** PZ-20261017-001 的路线信息:三站,标记分别是 装1 / 卸1,装2,装3,装4 / 卸2,卸3,卸4 */
|
||||
private static final String ROUTE_JSON = "["
|
||||
+ "{\"address\":\"广西壮族自治区南宁市南宁吴圩机场\",\"displayAddress\":\"广西壮族自治区南宁市南宁吴圩机场\","
|
||||
+ "\"tags\":[\"装1\"],\"locked\":false},"
|
||||
+ "{\"address\":\"广西壮族自治区北海市合浦县白沙镇广西新福兴硅科技有限公司\","
|
||||
+ "\"displayAddress\":\"广西壮族自治区北海市合浦县白沙镇广西新福兴硅科技有限公司\","
|
||||
+ "\"tags\":[\"卸1\",\"装2\",\"装3\",\"装4\"],\"locked\":false},"
|
||||
+ "{\"address\":\"广西壮族自治区南宁市宾阳县宾州镇宾阳县宾阳县人民政府\","
|
||||
+ "\"displayAddress\":\"广西壮族自治区南宁市宾阳县宾州镇宾阳县宾阳县人民政府\","
|
||||
+ "\"tags\":[\"卸2\",\"卸3\",\"卸4\"],\"locked\":false}]";
|
||||
|
||||
private static final String WAYBILL_IDS_JSON =
|
||||
"[\"2102780956493299713\", \"2092186187239780353\", \"2092186187470467073\", \"2092186187571130369\"]";
|
||||
|
||||
@Test
|
||||
@DisplayName("路线信息成三站:第一站装货、第二站卸货并装货、第三站卸货,货物条目取子运单")
|
||||
void stationsFollowRouteTags() {
|
||||
LoadingManage loading = loading(ROUTE_JSON, WAYBILL_IDS_JSON);
|
||||
List<Waybill> children = List.of(
|
||||
child("2102780956493299713", "YD-20260923-0008", "技嘉4070显卡", "9/20测试mk001", "30.00"),
|
||||
child("2092186187239780353", "YD-20260825-0006", "石英砂A", "广西武宣冠超矿业有限公司", "42.10"),
|
||||
child("2092186187470467073", "YD-20260825-0007", "石英砂A", "广西武宣冠超矿业有限公司", "47.57"),
|
||||
child("2092186187571130369", "YD-20260825-0008", "石英砂A", "广西武宣冠超矿业有限公司", "42.17"));
|
||||
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading, children);
|
||||
|
||||
assertEquals(3, stations.size());
|
||||
assertEquals(List.of(1, 2, 3), stations.stream().map(TripStationVO::getIndex).toList());
|
||||
|
||||
TripStationVO first = stations.get(0);
|
||||
assertEquals("广西壮族自治区南宁市南宁吴圩机场", first.getName(), "站名取展示地址");
|
||||
assertEquals("装货", first.getOperationTypeLabel());
|
||||
assertEquals("load", first.getOperationType());
|
||||
assertEquals(List.of("装1"), first.getTags());
|
||||
assertEquals(1, first.getCargos().size());
|
||||
assertEquals("YD-20260923-0008", first.getCargos().get(0).getWaybillNo());
|
||||
assertEquals("技嘉4070显卡", first.getCargos().get(0).getCargoName());
|
||||
assertEquals("9/20测试mk001", first.getCargos().get(0).getCustomerName());
|
||||
assertEquals(0, new BigDecimal("30.00").compareTo(first.getCargos().get(0).getQuantity()));
|
||||
assertEquals("load", first.getCargos().get(0).getOperation());
|
||||
|
||||
TripStationVO second = stations.get(1);
|
||||
assertEquals("装卸", second.getOperationTypeLabel(), "同一站既卸又装");
|
||||
assertEquals(4, second.getCargos().size(), "卸 1 张 + 装 3 张");
|
||||
assertEquals("unload", second.getCargos().get(0).getOperation());
|
||||
assertEquals("YD-20260923-0008", second.getCargos().get(0).getWaybillNo());
|
||||
assertEquals(List.of("YD-20260825-0006", "YD-20260825-0007", "YD-20260825-0008"),
|
||||
second.getCargos().subList(1, 4).stream().map(TripStationVO.TripCargoVO::getWaybillNo).toList());
|
||||
assertEquals(List.of("42.10", "47.57", "42.17"),
|
||||
second.getCargos().subList(1, 4).stream()
|
||||
.map(cargo -> cargo.getQuantity().toPlainString()).toList());
|
||||
assertTrue(second.getCargos().subList(1, 4).stream()
|
||||
.allMatch(cargo -> "load".equals(cargo.getOperation())));
|
||||
|
||||
TripStationVO third = stations.get(2);
|
||||
assertEquals("卸货", third.getOperationTypeLabel());
|
||||
assertEquals(3, third.getCargos().size());
|
||||
assertTrue(third.getCargos().stream().allMatch(cargo -> "unload".equals(cargo.getOperation())));
|
||||
|
||||
assertEquals(first.getName(), TripStationAssembler.departureName(stations));
|
||||
assertEquals(second.getName(), TripStationAssembler.transitName(stations), "中间站按顺序连接");
|
||||
assertEquals(third.getName(), TripStationAssembler.arrivalName(stations));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("站点标记的第 N 张按配载单运单集合的位次算,被拒单看不到的子单只是缺一条明细")
|
||||
void tagOrderFollowsWaybillIdsNotVisibleChildren() {
|
||||
LoadingManage loading = loading(ROUTE_JSON, WAYBILL_IDS_JSON);
|
||||
// 第 1 张子单(2102780956493299713)在当前可见子单里不存在,但它仍占运单集合的第 1 位
|
||||
List<Waybill> children = List.of(
|
||||
child("2092186187239780353", "YD-20260825-0006", "石英砂A", "广西武宣冠超矿业有限公司", "42.10"));
|
||||
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading, children);
|
||||
|
||||
assertTrue(stations.get(0).getCargos().isEmpty(), "标记指向的子单不可见时该站没有明细");
|
||||
assertEquals("load", stations.get(1).getCargos().get(0).getOperation());
|
||||
assertEquals("YD-20260825-0006", stations.get(1).getCargos().get(0).getWaybillNo(),
|
||||
"装2 仍指向运单集合的第 2 张");
|
||||
assertEquals(1, stations.get(2).getCargos().size());
|
||||
assertEquals("unload", stations.get(2).getCargos().get(0).getOperation());
|
||||
assertEquals("YD-20260825-0006", stations.get(2).getCargos().get(0).getWaybillNo(),
|
||||
"卸2 也指向运单集合的第 2 张");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("没有路线信息时回退子运单起讫点去重成站,保留首次出现顺序")
|
||||
void fallsBackToWaybillPoints() {
|
||||
LoadingManage loading = loading(null, WAYBILL_IDS_JSON);
|
||||
List<Waybill> children = List.of(
|
||||
points("2102786022868598785", "YD-20260923-0010", "广西壮族自治区南宁市宾阳县", "广西壮族自治区来宾市武宣县"),
|
||||
points("2102786023208337409", "YD-20260923-0011", "广西壮族自治区来宾市武宣县", "福建省三明市三明沙县机场"),
|
||||
points("2102786023485161474", "YD-20260923-0012", "福建省三明市三明沙县机场", "广西壮族自治区北海市合浦县白沙镇"));
|
||||
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading, children);
|
||||
|
||||
assertEquals(List.of(
|
||||
"广西壮族自治区南宁市宾阳县",
|
||||
"广西壮族自治区来宾市武宣县",
|
||||
"福建省三明市三明沙县机场",
|
||||
"广西壮族自治区北海市合浦县白沙镇"),
|
||||
stations.stream().map(TripStationVO::getName).toList());
|
||||
assertEquals("装货", stations.get(0).getOperationTypeLabel());
|
||||
assertEquals("装卸", stations.get(1).getOperationTypeLabel(), "上一张的终点=下一张的起点");
|
||||
assertEquals("卸货", stations.get(3).getOperationTypeLabel());
|
||||
assertEquals(2, stations.get(1).getCargos().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("起讫点没有名称时用地址,名称与地址都没有才退到配载单自己的地址")
|
||||
void fallsBackToAddressThenLoadingAddress() {
|
||||
LoadingManage loading = loading(null, WAYBILL_IDS_JSON);
|
||||
loading.setDepartureAddress("配载单发货地址");
|
||||
loading.setTransitAddress("配载单途经地址");
|
||||
loading.setArrivalAddress("配载单到货地址");
|
||||
List<Waybill> addressOnly = List.of(points("1", "YD-1", null, null));
|
||||
|
||||
List<TripStationVO> fromLoading = TripStationAssembler.assemble(loading, addressOnly);
|
||||
|
||||
assertEquals(List.of("配载单发货地址", "配载单途经地址", "配载单到货地址"),
|
||||
fromLoading.stream().map(TripStationVO::getName).toList());
|
||||
assertNull(fromLoading.get(0).getOperationType(), "退到配载单地址时无从判定站点操作");
|
||||
assertTrue(fromLoading.get(0).getCargos().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("站点、起讫点、货物全为空时不抛异常,下发空站点数组")
|
||||
void emptyEverythingStaysEmpty() {
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading(null, null), List.of(points("1", "YD-1", null, null)));
|
||||
|
||||
assertTrue(stations.isEmpty());
|
||||
assertEquals("", TripStationAssembler.departureName(stations));
|
||||
assertEquals("", TripStationAssembler.transitName(stations));
|
||||
assertEquals("", TripStationAssembler.arrivalName(stations));
|
||||
assertEquals("", TripStationAssembler.transitName(TripStationAssembler.assemble(null, List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配载运单成两点:起点装货、终点卸货")
|
||||
void unloadedWaybillBecomesTwoPoints() {
|
||||
List<Waybill> children = List.of(points("2101508430981652482", "YD-20260920-0003", "南宁武鸣", "钦州港"));
|
||||
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(null, children);
|
||||
|
||||
assertEquals(2, stations.size());
|
||||
assertEquals("南宁武鸣", stations.get(0).getName());
|
||||
assertEquals("装货", stations.get(0).getOperationTypeLabel());
|
||||
assertEquals("钦州港", stations.get(1).getName());
|
||||
assertEquals("卸货", stations.get(1).getOperationTypeLabel());
|
||||
assertNotNull(stations.get(0).getCargos());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("路线信息坏掉时按没有路线信息处理,不抛异常")
|
||||
void brokenRouteJsonFallsBack() {
|
||||
List<Waybill> children = List.of(points("1", "YD-1", "起点", "终点"));
|
||||
|
||||
List<TripStationVO> stations = TripStationAssembler.assemble(loading("{不是数组", WAYBILL_IDS_JSON), children);
|
||||
|
||||
assertEquals(2, stations.size());
|
||||
assertEquals("起点", stations.get(0).getName());
|
||||
}
|
||||
|
||||
private LoadingManage loading(String routeJson, String waybillIdsJson) {
|
||||
LoadingManage loading = new LoadingManage();
|
||||
loading.setRouteJson(routeJson);
|
||||
loading.setWaybillIdsJson(waybillIdsJson);
|
||||
return loading;
|
||||
}
|
||||
|
||||
private Waybill child(String id, String waybillNo, String cargoName, String customerName, String quantity) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(Long.valueOf(id));
|
||||
waybill.setWaybillNo(waybillNo);
|
||||
waybill.setCargoName(cargoName);
|
||||
waybill.setCustomerName(customerName);
|
||||
waybill.setQuantity(new BigDecimal(quantity));
|
||||
waybill.setQuantityUnit("吨");
|
||||
waybill.setProjectName("武宣建筑原料运输项目");
|
||||
return waybill;
|
||||
}
|
||||
|
||||
/** 只有起讫点的子运单(名称与地址都取自参数) */
|
||||
private Waybill points(String id, String waybillNo, String departure, String arrival) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(Long.valueOf(id));
|
||||
waybill.setWaybillNo(waybillNo);
|
||||
waybill.setDepartureName(departure);
|
||||
waybill.setDepartureAddress(departure);
|
||||
waybill.setArrivalName(arrival);
|
||||
waybill.setArrivalAddress(arrival);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user