Merge remote-tracking branch 'websoft/dev'
# Conflicts: # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
This commit is contained in:
+1
-1
@@ -72,7 +72,7 @@ public class ManageWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/home")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名")
|
||||
@Operation(summary = "首页聚合", description = "统计 + 异常/风险/重新派单角标 + 待处理事项(异常处置≠已完成,或司机已拒单待重新派单)+ 当前用户名")
|
||||
public R<AdminHomeVO> home() {
|
||||
return R.data(manageWaybillService.home());
|
||||
}
|
||||
|
||||
+47
-5
@@ -126,6 +126,11 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
detail.setSubmitUser(record.getSubmitUser());
|
||||
// 面向前端的展示字段必须由服务端填好,否则审核页「提交人」一栏恒为空白
|
||||
detail.setSubmitUserName(UserCache.getUserRealName(record.getSubmitUser()));
|
||||
// 审核留痕同理:审核人姓名由服务端查好,前端不做用户ID → 姓名的二次请求。
|
||||
// 记录还在「审核中」时审核人与审核时间都为空,由前端显示占位符
|
||||
detail.setAuditUser(record.getAuditUser());
|
||||
detail.setAuditTime(record.getAuditTime());
|
||||
detail.setAuditUserName(UserCache.getUserRealName(record.getAuditUser()));
|
||||
detail.setPendingSnapshotJson(record.getSnapshotJson());
|
||||
|
||||
// 生效值一侧:从未认证通过时为 null,前端按「无对比基准」处理
|
||||
@@ -191,7 +196,6 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
private void updateCertTarget(TransportCertRecord record, boolean pass, String rejectReason) {
|
||||
boolean vehicleTarget = TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType());
|
||||
Integer status = pass ? TransportCertRecord.STATUS_APPROVED : TransportCertRecord.STATUS_REJECTED;
|
||||
String reason = pass ? null : CertificationRuleSupport.reasonForReject(rejectReason);
|
||||
// 只有通过才切指针;驳回时保持 null,框架的 not_null 更新策略会跳过该列
|
||||
Long newEffectiveId = pass ? record.getId() : null;
|
||||
|
||||
@@ -199,22 +203,25 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
TransportVehicle vehicle = new TransportVehicle();
|
||||
vehicle.setId(record.getBizId());
|
||||
vehicle.setCertificationStatus(status);
|
||||
vehicle.setCertificationRejectReason(reason);
|
||||
if (pass) {
|
||||
vehicle.setEffectiveCertRecordId(newEffectiveId);
|
||||
// 通过即生效:把这次快照的证件内容一并写回车辆档案,让「车辆档案 = 生效值」成立。
|
||||
// 不写回的话,审核通过之后管理端与司机端看到的仍是提交之前的旧值——
|
||||
// 状态显示「已通过」、内容却还是旧的,是最容易让人误判的一种不一致。
|
||||
// 与指针合并成同一次 UPDATE,不额外多写一次同一行。
|
||||
// 与指针合并成同一次 UPDATE。
|
||||
applySnapshotToVehicle(vehicle, record.getSnapshotJson());
|
||||
} else {
|
||||
vehicle.setCertificationRejectReason(CertificationRuleSupport.reasonForReject(rejectReason));
|
||||
}
|
||||
requireTargetUpdated(transportVehicleService.updateById(vehicle), "车辆", record.getBizId());
|
||||
if (pass) {
|
||||
clearVehicleRejectReason(record.getBizId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
Driver driver = new Driver();
|
||||
driver.setId(record.getBizId());
|
||||
driver.setCertificationStatus(status);
|
||||
driver.setCertificationRejectReason(reason);
|
||||
// 必须带上 driving_vehicle 原值:该列是 FieldStrategy.ALWAYS,
|
||||
// 更新时忽略 null 直接写,patch 里不带它就会把司机的车辆绑定清成 NULL,
|
||||
// 而车辆列表正是按这一列匹配的(清空即「暂无绑定车辆」)。
|
||||
@@ -224,10 +231,45 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
// 通过即生效:与车辆侧同理,把这次快照的证件内容一并写回司机档案。
|
||||
// 不写回的话,司机档案里始终是管理端当初录入的旧值(甚至为空),
|
||||
// 而认证状态已是「已通过」——状态与内容不一致最容易让人误判。
|
||||
// 与指针合并成同一次 UPDATE,不额外多写一次同一行。
|
||||
// 与指针合并成同一次 UPDATE。
|
||||
applySnapshotToDriver(driver, record.getSnapshotJson());
|
||||
} else {
|
||||
driver.setCertificationRejectReason(CertificationRuleSupport.reasonForReject(rejectReason));
|
||||
}
|
||||
requireTargetUpdated(driverService.updateById(driver), "司机", record.getBizId());
|
||||
if (pass) {
|
||||
clearDriverRejectReason(record.getBizId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空车辆档案上的驳回原因展示缓存
|
||||
* <p>
|
||||
* 该列没有驳回原因时必须是 NULL(建表即 DEFAULT NULL)。但框架的 not_null 更新策略
|
||||
* 会跳过 patch 里的 null 字段,交给 updateById 的 null 到不了 SQL,
|
||||
* 「已通过」的车辆会一直挂着上一次的驳回文案,故单独发一条只改该列的语句。
|
||||
* 它不碰生效快照指针。
|
||||
*
|
||||
* @param vehicleId 车辆主键
|
||||
*/
|
||||
private void clearVehicleRejectReason(Long vehicleId) {
|
||||
transportVehicleService.update(Wrappers.<TransportVehicle>lambdaUpdate()
|
||||
.eq(TransportVehicle::getId, vehicleId)
|
||||
.set(TransportVehicle::getCertificationRejectReason, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空司机档案上的驳回原因展示缓存
|
||||
* <p>
|
||||
* 与车辆侧同理:null 交给 updateById 到不了 SQL,只能单独发一条只改该列的语句。
|
||||
* 它不碰生效快照指针,也不碰车辆绑定。
|
||||
*
|
||||
* @param driverId 司机主键
|
||||
*/
|
||||
private void clearDriverRejectReason(Long driverId) {
|
||||
driverService.update(Wrappers.<Driver>lambdaUpdate()
|
||||
.eq(Driver::getId, driverId)
|
||||
.set(Driver::getCertificationRejectReason, null));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+26
-11
@@ -186,7 +186,13 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
String selfPhone = resolveSelfPhone(userId);
|
||||
String hint = Func.isEmpty(mobileHint) ? null : mobileHint.trim();
|
||||
if (Func.isNotEmpty(hint)) {
|
||||
if (Func.isNotEmpty(selfPhone) && !selfPhone.equals(hint)) {
|
||||
if (Func.isEmpty(selfPhone)) {
|
||||
// 解析不出本人号码时不能拿前端传的 mobile 去查:
|
||||
// currentDriver 会直接按 mobile 等值查询,传入他人号码即可读到他人档案。
|
||||
// 这里返回空让它退回按 user_id 查,宁可查不到也不越权。
|
||||
return null;
|
||||
}
|
||||
if (!selfPhone.equals(hint)) {
|
||||
throw new ServiceException("只能查询本人司机档案");
|
||||
}
|
||||
return hint;
|
||||
@@ -196,21 +202,30 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
|
||||
private String resolveSelfPhone(Long userId) {
|
||||
String account = AuthUtil.getUserAccount();
|
||||
if (Func.isNotEmpty(account) && account.matches("^1\\d{10}$")) {
|
||||
if (isMobileLike(account)) {
|
||||
return account.trim();
|
||||
}
|
||||
// 用户中心的 phone 带 @Sensitive(MOBILE),经 Feign 回来是「175****12082」这类脱敏串,
|
||||
// 拿它去 eq(mobile) 永远查不到、拿它比对本人的真实号码又必然不等。
|
||||
// 因此这里只取未脱敏的 account;脱敏的 phone 一律不用。
|
||||
R<User> result = userClient.userInfoById(userId);
|
||||
if (result == null || !R.isSuccess(result) || result.getData() == null) {
|
||||
return Func.isEmpty(account) ? null : account.trim();
|
||||
}
|
||||
User user = result.getData();
|
||||
if (Func.isNotEmpty(user.getPhone())) {
|
||||
return user.getPhone().trim();
|
||||
}
|
||||
if (Func.isNotEmpty(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
|
||||
return user.getAccount().trim();
|
||||
if (result != null && R.isSuccess(result) && result.getData() != null) {
|
||||
String userAccount = result.getData().getAccount();
|
||||
if (isMobileLike(userAccount)) {
|
||||
return userAccount.trim();
|
||||
}
|
||||
}
|
||||
return Func.isEmpty(account) ? null : account.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是「可作为司机档案手机号使用」的号码串。
|
||||
* <p>
|
||||
* 只判 1 开头的 11~15 位纯数字:库里既有 11 位真实手机号,也有 12 位账号
|
||||
* (如 175874212082),写死 11 位会让这类账号的司机解析不出本人号码。
|
||||
*/
|
||||
private boolean isMobileLike(String value) {
|
||||
return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -376,6 +376,13 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
patch.setDrivingVehicle(current.getDrivingVehicle());
|
||||
}
|
||||
driverService.updateById(patch);
|
||||
if (Func.isBlank(rejectReason)) {
|
||||
// 重新提交时驳回原因应为空,但框架的 not_null 更新策略会跳过 patch 里的 null,
|
||||
// 只写 null 到不了 SQL,旧驳回文案会一直挂到审核通过,故单独清一次
|
||||
driverService.update(Wrappers.<Driver>lambdaUpdate()
|
||||
.eq(Driver::getId, driverId)
|
||||
.set(Driver::getCertificationRejectReason, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,6 +396,12 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
patch.setCertificationStatus(status);
|
||||
patch.setCertificationRejectReason(rejectReason);
|
||||
transportVehicleService.updateById(patch);
|
||||
if (Func.isBlank(rejectReason)) {
|
||||
// 与司机侧同理:null 交给 updateById 到不了 SQL,旧驳回文案会一直挂着
|
||||
transportVehicleService.update(Wrappers.<TransportVehicle>lambdaUpdate()
|
||||
.eq(TransportVehicle::getId, vehicleId)
|
||||
.set(TransportVehicle::getCertificationRejectReason, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+50
-110
@@ -56,7 +56,6 @@ 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.WaybillTrackVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
@@ -130,12 +129,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO currentTask() {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// 进行中:running,或无需确认接单但仍为 pending 的历史数据
|
||||
List<Waybill> candidates = waybillService.list(scopedQuery(phone)
|
||||
List<Waybill> candidates = waybillService.list(scopedQuery(plates)
|
||||
.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING))
|
||||
.orderByDesc(Waybill::getUpdateTime)
|
||||
.last("LIMIT 20"));
|
||||
@@ -151,12 +150,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillPreviewVO pendingPreview(Integer size) {
|
||||
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
return preview;
|
||||
}
|
||||
int limit = normalizePreviewSize(size);
|
||||
List<Waybill> pendingList = waybillService.list(scopedQuery(phone)
|
||||
List<Waybill> pendingList = waybillService.list(scopedQuery(plates)
|
||||
.in(Waybill::getBusinessStatus, PENDING_STATUSES)
|
||||
.orderByDesc(Waybill::getCreateTime));
|
||||
List<Waybill> needAccept = pendingList.stream()
|
||||
@@ -171,11 +170,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillTabCountsVO tabCounts() {
|
||||
DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
return vo;
|
||||
}
|
||||
List<Waybill> waybills = waybillService.list(scopedQuery(phone)
|
||||
List<Waybill> waybills = waybillService.list(scopedQuery(plates)
|
||||
.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES));
|
||||
long pending = 0;
|
||||
long doing = 0;
|
||||
@@ -207,12 +206,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);
|
||||
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
List<String> plates = currentBoundPlates();
|
||||
if (plates.isEmpty()) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(phone);
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(plates);
|
||||
// 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页
|
||||
applyStatusFilterForQuery(wrapper, status);
|
||||
applyKeyword(wrapper, keyword);
|
||||
@@ -235,74 +234,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO detail(Long id) {
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
return toCard(normalizeAcceptStatus(waybill), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillTrackVO track(Long id) {
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
LocalDate[] range = resolveTrackDateRange(waybill);
|
||||
return waybillService.trackVehicle(waybill.getId(), range[0].toString(), range[1].toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序行驶轨迹查询区间:
|
||||
* 1/2 有打卡 → 首条打卡日为开始;多于 1 条结束取末条日,仅 1 条结束取今天
|
||||
* 3 无打卡有计划开始日 → 计划开始日到今天
|
||||
* 4 无打卡无计划 → 3 天前到今天
|
||||
*/
|
||||
private LocalDate[] resolveTrackDateRange(Waybill waybill) {
|
||||
LocalDate today = LocalDate.now();
|
||||
List<Date> punchTimes = listAllPunchTimes(waybill.getId());
|
||||
if (!punchTimes.isEmpty()) {
|
||||
LocalDate start = toLocalDate(punchTimes.get(0));
|
||||
LocalDate end = punchTimes.size() > 1
|
||||
? toLocalDate(punchTimes.get(punchTimes.size() - 1))
|
||||
: today;
|
||||
return clampTrackRange(start, end, today);
|
||||
}
|
||||
LocalDate planStart = waybill.getEstimatedStartTime();
|
||||
if (planStart != null) {
|
||||
return clampTrackRange(planStart, today, today);
|
||||
}
|
||||
return clampTrackRange(today.minusDays(3), today, today);
|
||||
}
|
||||
|
||||
private LocalDate[] clampTrackRange(LocalDate start, LocalDate end, LocalDate today) {
|
||||
LocalDate safeStart = start == null ? today.minusDays(3) : start;
|
||||
LocalDate safeEnd = end == null ? today : end;
|
||||
if (safeEnd.isBefore(safeStart)) {
|
||||
safeEnd = safeStart;
|
||||
}
|
||||
return new LocalDate[]{safeStart, safeEnd};
|
||||
}
|
||||
|
||||
private List<Date> listAllPunchTimes(Long waybillId) {
|
||||
if (waybillId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Date> times = new ArrayList<>();
|
||||
nodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
|
||||
.eq(WaybillNodePunch::getWaybillId, waybillId)
|
||||
.isNotNull(WaybillNodePunch::getPunchTime)
|
||||
.orderByAsc(WaybillNodePunch::getPunchTime))
|
||||
.forEach(punch -> times.add(punch.getPunchTime()));
|
||||
enroutePunchMapper.selectList(Wrappers.<WaybillEnroutePunch>lambdaQuery()
|
||||
.eq(WaybillEnroutePunch::getWaybillId, waybillId)
|
||||
.isNotNull(WaybillEnroutePunch::getPunchTime)
|
||||
.orderByAsc(WaybillEnroutePunch::getPunchTime))
|
||||
.forEach(punch -> times.add(punch.getPunchTime()));
|
||||
times.sort(Date::compareTo);
|
||||
return times;
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Date date) {
|
||||
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO detailPunchSnapshot(Long id) {
|
||||
if (id == null) {
|
||||
@@ -322,7 +258,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId());
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver));
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
@@ -371,7 +307,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
throw new ServiceException("节点编码不能为空");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId());
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver));
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
@@ -434,7 +370,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
// 认证门槛:司机本人须已认证,且承运车辆须已认证。
|
||||
// 判据是各自持有的生效快照指针,故「已认证的车重新提交认证」不会拦下接单。
|
||||
assertCertified(driver);
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
waybillService.syncDriverAcceptState(waybill);
|
||||
assertAcceptable(waybill);
|
||||
Date now = new Date();
|
||||
@@ -484,8 +420,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean reject(Long id, String reason) {
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
waybillService.syncDriverAcceptState(waybill);
|
||||
assertRejectable(waybill);
|
||||
String rejectReason = Func.isEmpty(reason) ? null : reason.trim();
|
||||
@@ -508,8 +444,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
if (!STATUS_RUNNING.equals(waybill.getBusinessStatus())
|
||||
&& !STATUS_PENDING.equals(waybill.getBusinessStatus())
|
||||
@@ -547,15 +483,14 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
}
|
||||
|
||||
private Waybill loadDriverWaybill(Long id) {
|
||||
private Waybill loadDriverWaybill(Long id, List<String> plates) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
throw new ServiceException("当前用户手机号为空,无法查找运单");
|
||||
if (plates.isEmpty()) {
|
||||
throw new ServiceException("当前司机未绑定车辆");
|
||||
}
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(phone).eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
if (waybill == null) {
|
||||
throw new ServiceException("运单不存在或无权操作");
|
||||
}
|
||||
@@ -612,13 +547,24 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 司机可见运单范围:运单司机手机号(driver_phone)等于当前用户手机号。
|
||||
* 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。
|
||||
* 绑定来源:blade_transport_driver.driving_vehicle
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(String phone) {
|
||||
String normalizedPhone = Func.toStr(phone, "").trim();
|
||||
return Wrappers.<Waybill>lambdaQuery().and(w -> w
|
||||
.eq(Waybill::getDriverPhone, normalizedPhone)
|
||||
.or().apply("REPLACE(IFNULL(driver_phone,''),' ','') = {0}", normalizedPhone));
|
||||
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 void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
|
||||
@@ -634,19 +580,13 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
.or().like(Waybill::getVehicleNo, kw));
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户手机号:优先司机档案 mobile,其次 JWT 账号(11 位手机号)。
|
||||
*/
|
||||
private String currentUserPhone() {
|
||||
/** 当前登录司机绑定的规范化车牌列表;无司机或无绑定车牌则空 */
|
||||
private List<String> currentBoundPlates() {
|
||||
Driver driver = currentDriver();
|
||||
if (driver != null && Func.isNotEmpty(driver.getMobile())) {
|
||||
return driver.getMobile().trim();
|
||||
if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) {
|
||||
return List.of();
|
||||
}
|
||||
String account = AuthUtil.getUserAccount();
|
||||
if (Func.isNotEmpty(account) && account.matches("^1\\d{10}$")) {
|
||||
return account.trim();
|
||||
}
|
||||
return null;
|
||||
return splitPlates(driver.getDrivingVehicle());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -869,9 +809,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public void assertCurrentDriverOwnsWaybill(Long waybillId) {
|
||||
// 归属口径与司机打卡、司机接单一致:按当前用户手机号匹配运单 driverPhone
|
||||
requireCurrentDriver();
|
||||
loadDriverWaybill(waybillId);
|
||||
// 归属口径与司机打卡、司机接单一致:都按司机档案自述绑定的车牌判定
|
||||
Driver driver = requireCurrentDriver();
|
||||
loadDriverWaybill(waybillId, currentBoundPlates(driver));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+94
-4
@@ -57,6 +57,7 @@ import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
@@ -85,6 +86,13 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
private static final String DISPOSAL_PROCESSING = "processing";
|
||||
private static final String RISK_PENDING = "pending";
|
||||
|
||||
/** 小程序首页待处理事项的类型:异常待处置 */
|
||||
private static final String TODO_EXCEPTION = "exception";
|
||||
/** 小程序首页待处理事项的类型:重新派单待处理 */
|
||||
private static final String TODO_REASSIGN = "reassign";
|
||||
/** 重派条目跳转页码:参数 id 是运单主键(不是运单号),该页据此回显运单;buyerPaid 恒为 0,与列表卡片一致 */
|
||||
private static final String REASSIGN_TARGET_URL = "/pages/waybill/reassign?id=%s&buyerPaid=0";
|
||||
|
||||
private static final String EXCEPTION_YES = "exception";
|
||||
private static final String EXCEPTION_NO = "normal";
|
||||
private static final String TRANSPORT_COMMON = "common";
|
||||
@@ -122,8 +130,9 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||
badges.setException(home.getStats().getException());
|
||||
badges.setRisk(countPendingRisks());
|
||||
badges.setPendingReassign(countPendingReassignments());
|
||||
home.setBadges(badges);
|
||||
home.setFeed(buildExceptionFeed());
|
||||
home.setFeed(buildAdminFeed());
|
||||
return home;
|
||||
}
|
||||
|
||||
@@ -561,18 +570,66 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING));
|
||||
}
|
||||
|
||||
private List<AdminTodoItemVO> buildExceptionFeed() {
|
||||
/** 待重新派单:司机已拒单,且运单尚未取消/完成 */
|
||||
private long countPendingReassignments() {
|
||||
return waybillService.count(pendingReassignQuery());
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> pendingReassignQuery() {
|
||||
return Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED)
|
||||
.in(Waybill::getBusinessStatus, STATUS_PENDING, STATUS_RUNNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页待处理事项:异常待处置 + 重新派单待处理。
|
||||
* 两类各按自己的业务发生时间(上报时刻 / 拒单时刻)倒序,合并后再整体倒序取前 N 条。
|
||||
* 排序时间随条目一起携带,不塞进 id 字段——id 在两型下各自是处置主键与运单主键。
|
||||
*/
|
||||
List<AdminTodoItemVO> buildAdminFeed() {
|
||||
List<TimedTodo> merged = new ArrayList<>();
|
||||
merged.addAll(buildExceptionFeed());
|
||||
merged.addAll(buildReassignFeed());
|
||||
merged.sort(java.util.Comparator.comparing(
|
||||
(TimedTodo entry) -> entry.time, java.util.Comparator.nullsFirst(java.util.Comparator.naturalOrder())));
|
||||
java.util.Collections.reverse(merged);
|
||||
int to = Math.min(merged.size(), FEED_LIMIT);
|
||||
return merged.subList(0, to).stream().map(TimedTodo::item).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** 待处理事项 + 它的业务发生时间(仅用于排序) */
|
||||
private record TimedTodo(AdminTodoItemVO item, LocalDateTime time) {
|
||||
}
|
||||
|
||||
private List<TimedTodo> buildExceptionFeed() {
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||
.last("LIMIT " + FEED_LIMIT));
|
||||
return list.stream().map(this::toTodoItem).collect(Collectors.toList());
|
||||
return list.stream()
|
||||
.map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<TimedTodo> buildReassignFeed() {
|
||||
List<Waybill> list = waybillService.list(pendingReassignQuery()
|
||||
.orderByDesc(Waybill::getDriverRejectTime)
|
||||
.last("LIMIT " + FEED_LIMIT));
|
||||
return list.stream()
|
||||
.map(waybill -> new TimedTodo(toReassignTodoItem(waybill), rejectTimeOf(waybill)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private LocalDateTime exceptionTimeOf(ExceptionDisposal disposal) {
|
||||
return disposal.getReportTime() != null
|
||||
? disposal.getReportTime()
|
||||
: toLocalDateTime(disposal.getCreateTime());
|
||||
}
|
||||
|
||||
private AdminTodoItemVO toTodoItem(ExceptionDisposal disposal) {
|
||||
AdminTodoItemVO item = new AdminTodoItemVO();
|
||||
item.setId(disposal.getId());
|
||||
item.setType("exception");
|
||||
item.setType(TODO_EXCEPTION);
|
||||
item.setTitle("异常待处置");
|
||||
item.setTimeAgo(formatTimeAgo(disposal.getReportTime() != null
|
||||
? disposal.getReportTime()
|
||||
@@ -585,6 +642,38 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
return item;
|
||||
}
|
||||
|
||||
private AdminTodoItemVO toReassignTodoItem(Waybill waybill) {
|
||||
AdminTodoItemVO item = new AdminTodoItemVO();
|
||||
item.setId(waybill.getId());
|
||||
item.setType(TODO_REASSIGN);
|
||||
item.setTitle("重新派单待处理");
|
||||
item.setTimeAgo(formatTimeAgo(rejectTimeOf(waybill)));
|
||||
item.setDesc(buildReassignDesc(waybill));
|
||||
item.setWaybillNo(Func.toStr(waybill.getWaybillNo(), ""));
|
||||
item.setActionLabel("重新派单");
|
||||
item.setTargetUrl(String.format(REASSIGN_TARGET_URL, waybill.getId()));
|
||||
return item;
|
||||
}
|
||||
|
||||
/** 拒单时刻是「被拒了多久」的唯一依据;为空(历史数据)才回退到最后修改时间 */
|
||||
private LocalDateTime rejectTimeOf(Waybill waybill) {
|
||||
if (waybill.getDriverRejectTime() != null) {
|
||||
return toLocalDateTime(waybill.getDriverRejectTime());
|
||||
}
|
||||
return toLocalDateTime(waybill.getUpdateTime());
|
||||
}
|
||||
|
||||
/** 拒单原因的输入是可选的,没填时正文不能留下空的「:」 */
|
||||
private String buildReassignDesc(Waybill waybill) {
|
||||
String driver = Func.toStr(waybill.getDriverName(), "原司机");
|
||||
String text = "运单已指派" + driver + ",被拒绝,请重新派单";
|
||||
String reason = Func.toStr(waybill.getDriverRejectReason(), "").trim();
|
||||
if (Func.isNotBlank(reason)) {
|
||||
text = text + ":" + reason;
|
||||
}
|
||||
return text.length() > 80 ? text.substring(0, 80) + "…" : text;
|
||||
}
|
||||
|
||||
private String buildExceptionDesc(ExceptionDisposal disposal) {
|
||||
String reporter = Func.toStr(disposal.getReporterName(), "司机");
|
||||
String type = Func.toStr(disposal.getExceptionType(), "异常");
|
||||
@@ -606,6 +695,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
return Func.toStr(AuthUtil.getUserName(), "");
|
||||
}
|
||||
|
||||
/** 相对时间文案;时间取不到时留空,避免把「现在」当成发生时刻显示成「刚刚」 */
|
||||
private String formatTimeAgo(LocalDateTime time) {
|
||||
if (time == null) {
|
||||
return "";
|
||||
|
||||
+46
@@ -53,6 +53,7 @@ public final class WaybillProcessSupport {
|
||||
public static final String ACCEPT_REJECTED = "rejected";
|
||||
private static final String CONFIRM_YES = "yes";
|
||||
private static final String CONFIRM_NO_ACCEPT = "no_confirm_accept";
|
||||
private static final String CONFIRM_NO_COMPLETE = "no_confirm_complete";
|
||||
private static final String NODE_TRANSIT = "transit";
|
||||
private static final String NODE_TRANSIT_NAME = "在途";
|
||||
/** 异常处置终态:completed=已完成,revoked=已撤销。只有终态解除打卡门禁 */
|
||||
@@ -244,6 +245,21 @@ public final class WaybillProcessSupport {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本次打卡的节点是否是司机可打卡节点的末位。
|
||||
* <p>
|
||||
* 末位取「司机可打卡节点」序列:接单与回单不打卡,不参与排位——
|
||||
* 回单只要启用就必然排在配置最末,按全部启用节点取末位会永远取到回单。
|
||||
*/
|
||||
public static boolean isLastDriverPunchNode(String processJson, String nodeCode) {
|
||||
List<Map<String, Object>> nodes = listDriverPunchNodes(processJson);
|
||||
if (nodes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String code = stringVal(nodeCode);
|
||||
return !code.isEmpty() && code.equals(nodeKey(nodes.get(nodes.size() - 1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用中的过程节点(按配置顺序,含接单/回单)。
|
||||
*/
|
||||
@@ -591,6 +607,36 @@ public final class WaybillProcessSupport {
|
||||
return isAcceptNode(stringVal(node.get("key")), stringVal(node.get("name")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否到货 / 卸货 / 签收节点:key 或中文名任一命中即算。
|
||||
*/
|
||||
public static boolean isDeliveryNode(String nodeCode, String nodeName) {
|
||||
String key = stringVal(nodeCode);
|
||||
String name = stringVal(nodeName);
|
||||
return "arrival".equals(key) || "到货".equals(name)
|
||||
|| "unload".equals(key) || "卸货".equals(name)
|
||||
|| "sign".equals(key) || "签收".equals(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回单是否无需确认完成:配置里没有回单节点、回单未启用,或启用且配置为「无需确认完成」。
|
||||
* <p>
|
||||
* 未启用的回单节点不看 confirmMode——没有回单环节就不会有人来确认;
|
||||
* 启用但 confirmMode 缺失按「需确认完成」算。
|
||||
*/
|
||||
public static boolean returnNeedsNoConfirmation(String processJson) {
|
||||
for (Map<String, Object> node : parseProcessNodes(processJson)) {
|
||||
if (!isReturnNode(node)) {
|
||||
continue;
|
||||
}
|
||||
if (!isEnabled(node)) {
|
||||
return true;
|
||||
}
|
||||
return CONFIRM_NO_COMPLETE.equals(stringVal(node.get("confirmMode")));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isTransitNode(Map<String, Object> node) {
|
||||
String key = stringVal(node.get("key"));
|
||||
String name = stringVal(node.get("name"));
|
||||
|
||||
Reference in New Issue
Block a user