Merge remote-tracking branch 'websoft/dev'
This commit is contained in:
+3
@@ -150,6 +150,9 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "运输方式文案:公路运输等")
|
||||
private String transportType;
|
||||
|
||||
@Schema(description = "运输组织:common普通 / load配载")
|
||||
private String transportOrgType;
|
||||
|
||||
@Schema(description = "计划发货时间")
|
||||
private String planShipTime;
|
||||
|
||||
|
||||
+7
@@ -94,6 +94,13 @@ public class PreSettlementController extends BladeController {
|
||||
return R.data(preSettlementService.contractOptions(keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/next-no")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "获取最新预结算单号")
|
||||
public R<String> nextNo() {
|
||||
return R.data(preSettlementService.nextNo());
|
||||
}
|
||||
|
||||
@GetMapping("/fee-options")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "费用类型及费用项")
|
||||
|
||||
+2
-2
@@ -130,9 +130,9 @@ public interface IDriverWaybillService {
|
||||
boolean isNodePunched(Long waybillId, String nodeCode);
|
||||
|
||||
/**
|
||||
* 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。
|
||||
* 断言当前登录司机对该运单有归属:运单司机手机号须等于当前用户手机号。
|
||||
* <p>
|
||||
* 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案,
|
||||
* 无司机档案、无手机号、手机号不匹配都会抛异常。管理端与调度员账号没有司机档案,
|
||||
* 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。
|
||||
*/
|
||||
void assertCurrentDriverOwnsWaybill(Long waybillId);
|
||||
|
||||
+5
@@ -33,6 +33,11 @@ public interface IPreSettlementService extends BaseService<PreSettlement> {
|
||||
|
||||
PreSettlementVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 预览下一条预结算单号。仅供新增页面展示,真正落库的单号仍由保存时生成。
|
||||
*/
|
||||
String nextNo();
|
||||
|
||||
default List<Map<String, Object>> contractOptions(String keyword) {
|
||||
return contractOptions(keyword, null);
|
||||
}
|
||||
|
||||
+19
@@ -113,6 +113,8 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
|
||||
Driver driver = new Driver();
|
||||
driver.setId(id);
|
||||
driver.setStatus(status);
|
||||
// 只改 status 也必须带上 driving_vehicle 原值,理由见 carryDrivingVehicle
|
||||
carryDrivingVehicle(id, driver);
|
||||
return updateById(driver);
|
||||
}
|
||||
|
||||
@@ -446,9 +448,26 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
|
||||
Driver patch = new Driver();
|
||||
patch.setId(driver.getId());
|
||||
patch.setUserId(userId);
|
||||
// 同上:这次只改 user_id,不带 driving_vehicle 原值同样会把车辆绑定清掉
|
||||
carryDrivingVehicle(driver.getId(), patch);
|
||||
updateById(patch);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把库里的 driving_vehicle 原值带进 patch。
|
||||
* <p>
|
||||
* 该列标了 {@code FieldStrategy.ALWAYS},更新时忽略 null 直接写:patch 里不带这一列,
|
||||
* 一次只改别的列的更新就会把司机的车辆绑定清成 NULL,而司机端车辆列表正是按这一列
|
||||
* 的车牌匹配出来的(清空即「暂无绑定车辆」)。读不到该司机时保持原样,
|
||||
* 让更新自己去失败。
|
||||
*/
|
||||
private void carryDrivingVehicle(Long driverId, Driver patch) {
|
||||
Driver current = getById(driverId);
|
||||
if (current != null) {
|
||||
patch.setDrivingVehicle(current.getDrivingVehicle());
|
||||
}
|
||||
}
|
||||
|
||||
private Long defaultZero(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
+125
-63
@@ -102,6 +102,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
private static final String STATUS_CANCELLED = "cancelled";
|
||||
|
||||
/** 运输组织类型:运单有配载单号即配载(load),否则普通(common) */
|
||||
private static final String TRANSPORT_COMMON = "common";
|
||||
private static final String TRANSPORT_LOAD = "load";
|
||||
|
||||
/** 小程序「待接单」对应的后端业务状态 */
|
||||
private static final List<String> PENDING_STATUSES = Arrays.asList(
|
||||
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
|
||||
@@ -110,6 +114,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
private static final List<String> TAB_ALL_STATUSES = Arrays.asList(
|
||||
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED
|
||||
);
|
||||
/** 允许完成运单的业务状态 */
|
||||
private static final List<String> COMPLETABLE_STATUSES = Arrays.asList(
|
||||
STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
|
||||
);
|
||||
|
||||
private static final int DEFAULT_PREVIEW_SIZE = 2;
|
||||
private static final int MAX_PREVIEW_SIZE = 20;
|
||||
@@ -134,12 +142,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO currentTask() {
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return null;
|
||||
}
|
||||
// 进行中:running,或无需确认接单但仍为 pending 的历史数据
|
||||
List<Waybill> candidates = waybillService.list(scopedQuery(plates)
|
||||
List<Waybill> candidates = waybillService.list(scopedQuery(phone)
|
||||
.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING))
|
||||
.orderByDesc(Waybill::getUpdateTime)
|
||||
.last("LIMIT 20"));
|
||||
@@ -155,12 +163,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillPreviewVO pendingPreview(Integer size) {
|
||||
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return preview;
|
||||
}
|
||||
int limit = normalizePreviewSize(size);
|
||||
List<Waybill> pendingList = waybillService.list(scopedQuery(plates)
|
||||
List<Waybill> pendingList = waybillService.list(scopedQuery(phone)
|
||||
.in(Waybill::getBusinessStatus, PENDING_STATUSES)
|
||||
.orderByDesc(Waybill::getCreateTime));
|
||||
List<Waybill> needAccept = pendingList.stream()
|
||||
@@ -175,11 +183,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillTabCountsVO tabCounts() {
|
||||
DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO();
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return vo;
|
||||
}
|
||||
List<Waybill> waybills = waybillService.list(scopedQuery(plates)
|
||||
List<Waybill> waybills = waybillService.list(scopedQuery(phone)
|
||||
.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES));
|
||||
long pending = 0;
|
||||
long doing = 0;
|
||||
@@ -211,12 +219,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
|
||||
Page<DriverWaybillCardVO> empty = new Page<>(pageNo, pageSize);
|
||||
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(plates);
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(phone);
|
||||
// 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页
|
||||
applyStatusFilterForQuery(wrapper, status);
|
||||
applyKeyword(wrapper, keyword);
|
||||
@@ -239,15 +247,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO detail(Long id) {
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
return toCard(normalizeAcceptStatus(waybill), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillTrackVO track(Long id) {
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
LocalDate[] range = resolveTrackDateRange(waybill);
|
||||
return waybillService.trackVehicle(waybill.getId(), range[0].toString(), range[1].toString());
|
||||
}
|
||||
@@ -309,7 +317,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver));
|
||||
// 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明)
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), true);
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
@@ -358,7 +367,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
throw new ServiceException("节点编码不能为空");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver));
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId());
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
@@ -411,7 +420,20 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
nodePunchMapper.insert(punch);
|
||||
|
||||
advanceCurrentProcessNode(waybill, nodeCfg);
|
||||
return toNodePunchVO(punch);
|
||||
// 末位打卡节点是到货/卸货/签收、且回单无需确认完成时,本次打卡一并把运单置为完成;
|
||||
// isLastDriverPunchNode 与 returnNeedsNoConfirmation 传的就是上面那份 processJson,未再调 resolveProcessJson 取过程配置。
|
||||
String nodeKey = WaybillProcessSupport.nodeKey(nodeCfg);
|
||||
String nodeName = WaybillProcessSupport.nodeName(nodeCfg);
|
||||
boolean completed = false;
|
||||
if (WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeKey)
|
||||
&& WaybillProcessSupport.isDeliveryNode(nodeKey, nodeName)
|
||||
&& WaybillProcessSupport.returnNeedsNoConfirmation(processJson)
|
||||
&& canAutoCompleteWaybill(waybill)) {
|
||||
completed = waybillService.completeWithoutDeptCheck(waybill.getId());
|
||||
}
|
||||
DriverNodePunchVO vo = toNodePunchVO(punch);
|
||||
vo.setWaybillCompleted(completed);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -421,7 +443,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
// 认证门槛:司机本人须已认证,且承运车辆须已认证。
|
||||
// 判据是各自持有的生效快照指针,故「已认证的车重新提交认证」不会拦下接单。
|
||||
assertCertified(driver);
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
waybillService.syncDriverAcceptState(waybill);
|
||||
assertAcceptable(waybill);
|
||||
Date now = new Date();
|
||||
@@ -471,8 +493,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean reject(Long id, String reason) {
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
waybillService.syncDriverAcceptState(waybill);
|
||||
assertRejectable(waybill);
|
||||
String rejectReason = Func.isEmpty(reason) ? null : reason.trim();
|
||||
@@ -495,13 +517,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
if (!STATUS_RUNNING.equals(waybill.getBusinessStatus())
|
||||
&& !STATUS_PENDING.equals(waybill.getBusinessStatus())
|
||||
&& !STATUS_WAITING_DISPATCH.equals(waybill.getBusinessStatus())
|
||||
&& !STATUS_DISPATCHING.equals(waybill.getBusinessStatus())) {
|
||||
if (!COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())) {
|
||||
throw new ServiceException("当前运单状态不允许完成");
|
||||
}
|
||||
// 有未了结异常时不允许完成——完成接口不要求节点全部打卡,不挡住就能绕过打卡门禁
|
||||
@@ -520,6 +539,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) {
|
||||
throw new ServiceException("该运单已接单");
|
||||
}
|
||||
// 拒单后运单已对本人脱离,不给自己接回来的入口;要改主意只能由调度重新派单
|
||||
if (WaybillProcessSupport.isRejected(waybill.getDriverAcceptStatus())) {
|
||||
throw new ServiceException("该运单已拒单,请联系调度重新派单");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertRejectable(Waybill waybill) {
|
||||
@@ -534,14 +557,21 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
}
|
||||
|
||||
private Waybill loadDriverWaybill(Long id, List<String> plates) {
|
||||
private Waybill loadDriverWaybill(Long id) {
|
||||
return loadDriverWaybill(id, false);
|
||||
}
|
||||
|
||||
/** @param includeRejected 仅 submitEnroute 传 true,见 scopedQuery 的重载说明 */
|
||||
private Waybill loadDriverWaybill(Long id, boolean includeRejected) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
if (plates.isEmpty()) {
|
||||
throw new ServiceException("当前司机未绑定车辆");
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
throw new ServiceException("当前用户手机号为空,无法查找运单");
|
||||
}
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(phone, includeRejected)
|
||||
.eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
if (waybill == null) {
|
||||
throw new ServiceException("运单不存在或无权操作");
|
||||
}
|
||||
@@ -573,8 +603,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
switch (status) {
|
||||
case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES);
|
||||
case 1 -> wrapper.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING,
|
||||
STATUS_WAITING_DISPATCH, STATUS_DISPATCHING));
|
||||
case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES);
|
||||
case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED);
|
||||
case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED);
|
||||
default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES);
|
||||
@@ -598,24 +627,31 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。
|
||||
* 绑定来源:blade_transport_driver.driving_vehicle
|
||||
* 司机可见运单范围:运单司机手机号(driver_phone)等于当前用户手机号,
|
||||
* 且该运单未被这个司机拒单。
|
||||
* <p>
|
||||
* 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。
|
||||
* 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates) {
|
||||
return Wrappers.<Waybill>lambdaQuery().and(w -> {
|
||||
w.in(Waybill::getVehicleNo, plates)
|
||||
.or().in(Waybill::getTrailerVehicleNo, plates);
|
||||
for (String plate : plates) {
|
||||
w.or().apply(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}",
|
||||
plate
|
||||
);
|
||||
w.or().apply(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(trailer_vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}",
|
||||
plate
|
||||
);
|
||||
}
|
||||
});
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(String phone) {
|
||||
return scopedQuery(phone, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param includeRejected true 时不排除已拒单的运单,仅限在途打卡使用:
|
||||
* 拒单的运单不可能处于运输中,收口拦不到合法操作,
|
||||
* 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(String phone, boolean includeRejected) {
|
||||
String normalizedPhone = Func.toStr(phone, "").trim();
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery().and(w -> w
|
||||
.eq(Waybill::getDriverPhone, normalizedPhone)
|
||||
.or().apply("REPLACE(IFNULL(driver_phone,''),' ','') = {0}", normalizedPhone));
|
||||
if (!includeRejected) {
|
||||
wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus)
|
||||
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
|
||||
@@ -631,13 +667,26 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
.or().like(Waybill::getVehicleNo, kw));
|
||||
}
|
||||
|
||||
/** 当前登录司机绑定的规范化车牌列表;无司机或无绑定车牌则空 */
|
||||
private List<String> currentBoundPlates() {
|
||||
/**
|
||||
* 当前用户手机号:司机档案 mobile 优先,其次小程序用户账号。
|
||||
* <p>
|
||||
* 派单时运单 driver_phone 取自司机档案 mobile,故档案有手机号就以它为准。
|
||||
*/
|
||||
private String currentUserPhone() {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) {
|
||||
return List.of();
|
||||
if (driver != null && isMobileLike(driver.getMobile())) {
|
||||
return driver.getMobile().trim();
|
||||
}
|
||||
return splitPlates(driver.getDrivingVehicle());
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId == null || userId <= 0) {
|
||||
return null;
|
||||
}
|
||||
R<MpUser> result = mpUserClient.userById(userId);
|
||||
if (result != null && R.isSuccess(result) && result.getData() != null
|
||||
&& isMobileLike(result.getData().getAccount())) {
|
||||
return result.getData().getAccount().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -703,10 +752,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
DriverWaybillCardVO card = new DriverWaybillCardVO();
|
||||
card.setId(waybill.getId());
|
||||
card.setWaybillNo(waybill.getWaybillNo());
|
||||
card.setFromName(Func.toStr(waybill.getDepartureName(), ""));
|
||||
card.setToName(Func.toStr(waybill.getArrivalName(), ""));
|
||||
card.setFromAddress(Func.toStr(waybill.getDepartureAddress(), card.getFromName()));
|
||||
card.setToAddress(Func.toStr(waybill.getArrivalAddress(), card.getToName()));
|
||||
// 批量导入的运单只有发货/到货地址、没有地点名称,名称为空时回退到地址,
|
||||
// 否则司机端首页、运单列表与打卡页的地点位置都是空的
|
||||
card.setFromName(Func.toStrWithEmpty(waybill.getDepartureName(), Func.toStr(waybill.getDepartureAddress(), "")));
|
||||
card.setToName(Func.toStrWithEmpty(waybill.getArrivalName(), Func.toStr(waybill.getArrivalAddress(), "")));
|
||||
card.setFromAddress(Func.toStrWithEmpty(waybill.getDepartureAddress(), card.getFromName()));
|
||||
card.setToAddress(Func.toStrWithEmpty(waybill.getArrivalAddress(), card.getToName()));
|
||||
card.setCargoNames(splitCargoNames(waybill.getCargoName()));
|
||||
card.setCargoCategory(Func.toStr(waybill.getCargoType(), ""));
|
||||
card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()));
|
||||
@@ -734,6 +785,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
card.setCargoQuantity(weightText);
|
||||
card.setTotalWeight(weightText);
|
||||
card.setTransportType(toTransportTypeLabel(waybill.getTransportType()));
|
||||
card.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON);
|
||||
card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime()));
|
||||
card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime()));
|
||||
card.setRemark(Func.toStr(waybill.getRemark(), ""));
|
||||
@@ -880,9 +932,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public void assertCurrentDriverOwnsWaybill(Long waybillId) {
|
||||
// 归属口径与司机打卡、司机接单一致:都按司机档案自述绑定的车牌判定
|
||||
Driver driver = requireCurrentDriver();
|
||||
loadDriverWaybill(waybillId, currentBoundPlates(driver));
|
||||
// 归属口径与司机打卡、司机接单一致:按当前用户手机号匹配运单司机手机号
|
||||
requireCurrentDriver();
|
||||
loadDriverWaybill(waybillId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1091,6 +1143,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return countByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。
|
||||
* <p>
|
||||
* 不满足返回 false,由调用方静默跳过,打卡不受影响。
|
||||
*/
|
||||
private boolean canAutoCompleteWaybill(Waybill waybill) {
|
||||
return COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())
|
||||
&& unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言该运单没有未了结的异常。
|
||||
* <p>
|
||||
|
||||
+5
@@ -211,6 +211,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextNo() {
|
||||
return nextPreSettlementNo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> contractOptions(String keyword, Long projectId) {
|
||||
LambdaQueryWrapper<ContractManage> wrapper = Wrappers.<ContractManage>lambdaQuery()
|
||||
|
||||
+1
@@ -3104,6 +3104,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
map.put("contractId", detail.getContractId());
|
||||
map.put("contractNo", detail.getContractNo());
|
||||
map.put("contractName", detail.getContractName());
|
||||
map.put("sourceType", detail.getSourceType());
|
||||
map.put("preSettlementNo", detail.getPreSettlementNo());
|
||||
map.put("formalSettlementNo", detail.getFormalSettlementNo());
|
||||
map.put("waybillNo", detail.getWaybillNo());
|
||||
|
||||
+3
@@ -58,6 +58,7 @@ class DriverNodePunchPhotoTest {
|
||||
private static final Long WAYBILL_ID = 5601L;
|
||||
private static final Long USER_ID = 7001L;
|
||||
private static final String PLATE = "桂AAW3255";
|
||||
private static final String PHONE = "13800138003";
|
||||
private static final String CLOCK_PHOTO = "https://oss/clock.jpg";
|
||||
private static final String WAYBILL_PHOTO = "https://oss/waybill.jpg";
|
||||
private static final String RECEIPT_PHOTO = "https://oss/receipt.jpg";
|
||||
@@ -308,6 +309,7 @@ class DriverNodePunchPhotoTest {
|
||||
driver.setId(9001L);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
driver.setMobile(PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
@@ -316,6 +318,7 @@ class DriverNodePunchPhotoTest {
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610070001");
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setProcessJson(PROCESS_JSON);
|
||||
waybill.setBusinessStatus("running");
|
||||
waybill.setIsDeleted(0);
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
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.core.tool.api.R;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
import org.springblade.system.pojo.entity.MpUser;
|
||||
import org.springblade.transport.mapper.DriverMapper;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.LocalDate;
|
||||
|
||||
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.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>
|
||||
* 守住的是一条不变式:司机档案的行更新一律不得把「驾驶车辆」绑定写掉。
|
||||
* 该列标了 {@code FieldStrategy.ALWAYS},patch 里不带它就写成 NULL,
|
||||
* 司机端车辆列表随即变成「暂无绑定车辆」,而司机在界面上无法自行恢复。
|
||||
*/
|
||||
class DriverServiceImplTest {
|
||||
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final Long USER_ID = 7001L;
|
||||
private static final String TENANT_ID = "000000";
|
||||
private static final String PLATE = "桂A32523";
|
||||
private static final String MOBILE = "15975348524";
|
||||
|
||||
private DriverMapper driverMapper;
|
||||
private IMpUserClient mpUserClient;
|
||||
private ISysClient sysClient;
|
||||
private DriverServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
// saveOrUpdate 需要 MyBatis-Plus 的实体元数据缓存
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Driver.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
driverMapper = mock(DriverMapper.class);
|
||||
mpUserClient = mock(IMpUserClient.class);
|
||||
sysClient = mock(ISysClient.class);
|
||||
service = new DriverServiceImpl(mpUserClient, sysClient);
|
||||
inject("baseMapper", driverMapper);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 启用 / 停用
|
||||
|
||||
@Test
|
||||
@DisplayName("启用/停用司机:patch 必须带上驾驶车辆原值,否则这次更新就把车辆绑定清成 NULL")
|
||||
void changeStatusKeepsDrivingVehicle() {
|
||||
when(driverMapper.selectById(DRIVER_ID)).thenReturn(driver(PLATE));
|
||||
when(driverMapper.updateById(any(Driver.class))).thenReturn(1);
|
||||
|
||||
assertTrue(service.changeStatus(DRIVER_ID, 2));
|
||||
|
||||
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
|
||||
verify(driverMapper).updateById(captor.capture());
|
||||
assertEquals(2, captor.getValue().getStatus());
|
||||
assertEquals(PLATE, captor.getValue().getDrivingVehicle(),
|
||||
"只改 status 的 patch 不带 driving_vehicle 会把司机的车辆绑定写掉");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("启用/停用司机:状态取值不合法时拒绝,且不写库")
|
||||
void changeStatusRejectsInvalidStatus() {
|
||||
assertThrows(ServiceException.class, () -> service.changeStatus(DRIVER_ID, 9));
|
||||
verify(driverMapper, never()).updateById(any(Driver.class));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 保存司机
|
||||
|
||||
@Test
|
||||
@DisplayName("保存司机:关联小程序账号的那次 patch 也必须带上驾驶车辆原值")
|
||||
void submitKeepsDrivingVehicleWhenLinkingMpUser() {
|
||||
Driver driver = driver(PLATE);
|
||||
// 提交前校验按身份证号与主键各查一次
|
||||
when(driverMapper.selectByIdIncludingDeleted(DRIVER_ID)).thenReturn(driver);
|
||||
when(driverMapper.selectByIdCardNoIncludingDeleted(driver.getIdCardNo())).thenReturn(null);
|
||||
// 完整对象更新:saveOrUpdate 判存在走更新
|
||||
when(driverMapper.selectById(DRIVER_ID)).thenReturn(driver);
|
||||
when(driverMapper.updateById(any(Driver.class))).thenReturn(1);
|
||||
// 先查不到小程序用户 → 建号 → 回查拿到新建用户
|
||||
MpUser created = new MpUser();
|
||||
created.setId(USER_ID);
|
||||
when(mpUserClient.userByPhone(TENANT_ID, MOBILE)).thenReturn(null, R.data(created));
|
||||
when(mpUserClient.saveUser(any(MpUser.class))).thenReturn(R.data(true));
|
||||
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID);
|
||||
assertTrue(service.submit(driver));
|
||||
}
|
||||
|
||||
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
|
||||
verify(driverMapper, times(2)).updateById(captor.capture());
|
||||
// captor 存的是引用:完整对象更新带走的是 driver 本身,而它随后会被就地写上 userId,
|
||||
// 所以只能用对象身份把「只带少数几列的 patch」摘出来,不能按 userId 认
|
||||
Driver linkPatch = captor.getAllValues().stream()
|
||||
.filter(patch -> patch != driver)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("没有发出关联小程序账号的那次 patch"));
|
||||
assertEquals(USER_ID, linkPatch.getUserId());
|
||||
assertEquals(PLATE, linkPatch.getDrivingVehicle(),
|
||||
"关联账号的 patch 不带 driving_vehicle 会把司机的车辆绑定写掉");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("保存司机:司机不存在时拒绝,不写库")
|
||||
void submitRejectsUnknownDriver() {
|
||||
Driver driver = driver(PLATE);
|
||||
when(driverMapper.selectByIdIncludingDeleted(DRIVER_ID)).thenReturn(null);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.submit(driver));
|
||||
verify(driverMapper, never()).updateById(any(Driver.class));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private Driver driver(String drivingVehicle) {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(DRIVER_ID);
|
||||
driver.setIsDeleted(0);
|
||||
driver.setDriverName("张三");
|
||||
driver.setIdCardNo("110101199001011234");
|
||||
driver.setGender("男");
|
||||
driver.setPosts("司机");
|
||||
driver.setDrivingType("A2");
|
||||
driver.setDrivingLicenseNo("110101199001011234");
|
||||
driver.setDrivingLicenseStartDate(LocalDate.of(2020, 1, 1));
|
||||
driver.setDrivingLicenseLongTerm(1);
|
||||
driver.setQualificationType("道路运输");
|
||||
driver.setQualificationNo("Q123456");
|
||||
driver.setQualificationLongTerm(1);
|
||||
driver.setDriverType("自有");
|
||||
driver.setMobile(MOBILE);
|
||||
driver.setOrganizationName("测试组织");
|
||||
driver.setEmergencyContactName("李四");
|
||||
driver.setEmergencyContactMobile("13800000000");
|
||||
driver.setIdCardFront("https://oss/id-front.jpg");
|
||||
driver.setIdCardBack("https://oss/id-back.jpg");
|
||||
driver.setDrivingLicenseFront("https://oss/dl-front.jpg");
|
||||
driver.setDrivingLicenseBack("https://oss/dl-back.jpg");
|
||||
driver.setDrivingVehicle(drivingVehicle);
|
||||
return driver;
|
||||
}
|
||||
|
||||
private void inject(String field, Object value) throws Exception {
|
||||
Class<?> type = service.getClass();
|
||||
while (type != null) {
|
||||
try {
|
||||
Field declared = type.getDeclaredField(field);
|
||||
declared.setAccessible(true);
|
||||
declared.set(service, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("未找到字段:" + field);
|
||||
}
|
||||
|
||||
}
|
||||
+3
@@ -49,6 +49,7 @@ class DriverWaybillAutoCompleteTest {
|
||||
private static final Long WAYBILL_ID = 5801L;
|
||||
private static final Long USER_ID = 7001L;
|
||||
private static final String PLATE = "桂AAW3255";
|
||||
private static final String PHONE = "13800138001";
|
||||
private static final String CLOCK_PHOTO = "https://oss/clock.jpg";
|
||||
|
||||
/** 末位打卡节点是「签收」,没有回单节点 */
|
||||
@@ -219,6 +220,7 @@ class DriverWaybillAutoCompleteTest {
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610080001");
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setProcessJson(processJson);
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setIsDeleted(0);
|
||||
@@ -237,6 +239,7 @@ class DriverWaybillAutoCompleteTest {
|
||||
driver.setId(9001L);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
driver.setMobile(PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -57,6 +57,7 @@ class DriverWaybillRejectDetachTest {
|
||||
private static final Long USER_ID = 7401L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PLATE = "沪AE31312";
|
||||
private static final String PHONE = "13800138002";
|
||||
private static final Long REJECTED_ID = 7201L;
|
||||
|
||||
private IDriverService driverService;
|
||||
@@ -97,7 +98,7 @@ class DriverWaybillRejectDetachTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首页待接预览的取单条件排除已拒单,且排除子句与车牌条件并列")
|
||||
@DisplayName("首页待接预览的取单条件排除已拒单,且排除子句与取单条件并列")
|
||||
void pendingPreviewQueryExcludesRejected() {
|
||||
when(waybillService.list(ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any())).thenReturn(List.of());
|
||||
|
||||
@@ -107,7 +108,7 @@ class DriverWaybillRejectDetachTest {
|
||||
assertTrue(sql.contains("driver_accept_status"),
|
||||
"取单条件必须排除已拒单:" + sql);
|
||||
assertTrue(sql.toUpperCase().contains("AND"),
|
||||
"排除子句不能与车牌条件「或」在一起,否则等于没排除:" + sql);
|
||||
"排除子句不能与取单条件「或」在一起,否则等于没排除:" + sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,6 +213,7 @@ class DriverWaybillRejectDetachTest {
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄大富");
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
driver.setMobile(PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
@@ -224,6 +226,7 @@ class DriverWaybillRejectDetachTest {
|
||||
waybill.setId(id);
|
||||
waybill.setWaybillNo(no);
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setDriverAcceptStatus(acceptStatus);
|
||||
waybill.setIsDeleted(0);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- 收票登记:补金蝶单据字段
|
||||
-- 已有环境里的 blade_invoice_receipt 由更早一版的建表语句创建,缺 kingdee_bill_no /
|
||||
-- kingdee_status;建表语句用的是 CREATE TABLE IF NOT EXISTS,对已存在的表是空操作,
|
||||
-- 所以只跑建表脚本补不上这两列,必须走 ALTER。
|
||||
-- 以下语句可重复执行。
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶单据编号'' AFTER `current_processor`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND COLUMN_NAME = 'kingdee_bill_no'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
|
||||
-- 存量行取默认值 unsynced;收票记录只有 synced / failed / unsynced 三种取值
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''unsynced'' COMMENT ''金蝶同步状态:unsynced未同步、synced已同步、failed同步失败'' AFTER `kingdee_bill_no`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND COLUMN_NAME = 'kingdee_status'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_receipt` ADD KEY `idx_invoice_receipt_kingdee` (`kingdee_status`)',
|
||||
'SELECT 1')
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND INDEX_NAME = 'idx_invoice_receipt_kingdee'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
|
||||
-- 旧列 kingdee_received_invoice_no / kingdee_received_invoice_id / kingdee_push_status /
|
||||
-- kingdee_push_msg / kingdee_push_time 在代码里已无任何引用,且取值集合与新模型对不上
|
||||
-- (旧值 pushed / assigned 在新模型里不存在),故不搬运数据,保持原样不动。
|
||||
Reference in New Issue
Block a user