Merge remote-tracking branch 'websoft/dev'

This commit is contained in:
2026-10-16 11:47:52 +08:00
13 changed files with 409 additions and 67 deletions
@@ -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 = "费用类型及费用项")
@@ -130,9 +130,9 @@ public interface IDriverWaybillService {
boolean isNodePunched(Long waybillId, String nodeCode);
/**
* 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。
* 断言当前登录司机对该运单有归属:运单司机手机号须等于当前用户手机号。
* <p>
* 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案,
* 无司机档案、无手机号、手机号不匹配都会抛异常。管理端与调度员账号没有司机档案,
* 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。
*/
void assertCurrentDriverOwnsWaybill(Long waybillId);
@@ -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);
}
@@ -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;
}
@@ -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>
@@ -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()
@@ -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());