✨ 新增异常处置同意并调整路线接口
This commit is contained in:
+9
@@ -34,6 +34,7 @@ import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRouteAdjustRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
@@ -131,4 +132,12 @@ public class ExceptionDisposalController extends BladeController {
|
||||
return R.success("已撤销");
|
||||
}
|
||||
|
||||
@PostMapping("/route-adjust")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "同意并调整路线", description = "调度端同意上报的路线修改:先写跟进记录,再改路线,最后了结;仅需登录态")
|
||||
public R adjustRoute(@RequestBody ExceptionDisposalRouteAdjustRequest request) {
|
||||
exceptionDisposalService.adjustRoute(request);
|
||||
return R.success("已调整路线");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRouteAdjustRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
@@ -68,4 +69,10 @@ public interface IExceptionDisposalService extends BaseService<ExceptionDisposal
|
||||
*/
|
||||
void revoke(Long id);
|
||||
|
||||
/**
|
||||
* 同意并调整路线:先写一条调整说明的跟进记录,再改载体路线,最后了结。
|
||||
* 有配载单时改配载单的站点行路线,没有配载单时只按路线段改运单的地址。
|
||||
*/
|
||||
void adjustRoute(ExceptionDisposalRouteAdjustRequest request);
|
||||
|
||||
}
|
||||
|
||||
+493
-8
@@ -29,22 +29,28 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserDisplayCache;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.LoadingManageMapper;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRouteAdjustRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalFollowRecordVO;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -54,6 +60,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -74,6 +81,7 @@ public class ExceptionDisposalServiceImpl
|
||||
private static final String STATUS_PROCESSING = "processing";
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
private static final String STATUS_REVOKED = "revoked";
|
||||
private static final String STATUS_CANCELLED = "cancelled";
|
||||
|
||||
/** latest_follow_content 与 follow_content 两列均为 varchar(500) */
|
||||
private static final int FOLLOW_CONTENT_MAX_LENGTH = 500;
|
||||
@@ -81,16 +89,38 @@ public class ExceptionDisposalServiceImpl
|
||||
/** 拒绝原因长度上限,与上报说明、异常原因同量级,且远小于跟进记录列宽 */
|
||||
private static final int REJECT_REASON_MAX_LENGTH = 200;
|
||||
|
||||
/** 路线与任务信息的列宽上限,与 PC 改路线同一口径 */
|
||||
private static final int ROUTE_JSON_MAX_LENGTH = 8000;
|
||||
private static final int TASK_INFO_JSON_MAX_LENGTH = 8000;
|
||||
private static final int ADDRESS_MAX_LENGTH = 255;
|
||||
|
||||
private static final String NODE_KIND_START = "start";
|
||||
private static final String NODE_KIND_TRANSIT = "transit";
|
||||
private static final String NODE_KIND_END = "end";
|
||||
|
||||
/** 途经站地址在配载单上的连接符,与 PC 同步地址列时一致 */
|
||||
private static final String TRANSIT_ADDRESS_JOINER = ",";
|
||||
|
||||
/** 跟进记录与变更记录共用的同一句结论 */
|
||||
private static final String ROUTE_CHANGE_UNCHANGED = "同意调整路线,未修改路线";
|
||||
private static final String ROUTE_CHANGE_PREFIX = "同意调整路线:";
|
||||
|
||||
/** task_info_json 里存路线变更记录数组的键,与 PC 同一份结构 */
|
||||
private static final String TASK_INFO_ROUTE_CHANGE_RECORDS = "routeChangeRecords";
|
||||
|
||||
private final ExceptionDisposalFollowRecordMapper followRecordMapper;
|
||||
private final IWaybillService waybillService;
|
||||
private final IDriverWaybillService driverWaybillService;
|
||||
private final LoadingManageMapper loadingManageMapper;
|
||||
|
||||
public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper,
|
||||
IWaybillService waybillService,
|
||||
IDriverWaybillService driverWaybillService) {
|
||||
IDriverWaybillService driverWaybillService,
|
||||
LoadingManageMapper loadingManageMapper) {
|
||||
this.followRecordMapper = followRecordMapper;
|
||||
this.waybillService = waybillService;
|
||||
this.driverWaybillService = driverWaybillService;
|
||||
this.loadingManageMapper = loadingManageMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -419,14 +449,8 @@ public class ExceptionDisposalServiceImpl
|
||||
}
|
||||
|
||||
String rejectContent = "拒绝:" + rejectReason;
|
||||
ExceptionDisposalFollowRecord record = new ExceptionDisposalFollowRecord();
|
||||
record.setDisposalId(disposal.getId());
|
||||
record.setFollowContent(rejectContent);
|
||||
// 拒绝弹窗只有原因、没有附件,attachments_json 不写
|
||||
record.setFollowUser(AuthUtil.getUserId());
|
||||
record.setFollowUserName(UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
|
||||
record.setFollowTime(LocalDateTime.now());
|
||||
followRecordMapper.insert(record);
|
||||
insertFollowRecord(disposal.getId(), rejectContent);
|
||||
|
||||
// 记录先于状态落库,且同在一个事务里:结案了却没有拒绝记录,事后无从知道是谁以什么理由拒绝的
|
||||
disposal.setDisposalStatus(STATUS_COMPLETED);
|
||||
@@ -451,6 +475,467 @@ public class ExceptionDisposalServiceImpl
|
||||
updateById(disposal);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void adjustRoute(ExceptionDisposalRouteAdjustRequest request) {
|
||||
if (request == null || request.getId() == null) {
|
||||
throw new ServiceException("请选择需要调整路线的异常");
|
||||
}
|
||||
ExceptionDisposal disposal = getExisting(request.getId());
|
||||
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
|
||||
throw new ServiceException("该异常已处理");
|
||||
}
|
||||
Waybill waybill = disposal.getWaybillId() == null ? null : waybillService.getById(disposal.getWaybillId());
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("关联运单不存在");
|
||||
}
|
||||
// 路线改在谁身上跟着配载单走:配载单是这趟车的执行主体,司机端打卡读的就是它的路线
|
||||
LoadingManage loading = findLoading(waybill.getLoadingNo());
|
||||
if (loading == null && Func.isNotBlank(waybill.getLoadingNo())) {
|
||||
throw new ServiceException("运单已关联配载单,请在配载单中修改");
|
||||
}
|
||||
// 校验与拼装全部在写入之前完成:任何一条不通过都不该留下半条处置记录
|
||||
List<Station> stations = validateRouteNodes(request.getRouteNodes(), loading);
|
||||
List<String> sourceAddresses = loading != null
|
||||
? loadingStationAddresses(loading)
|
||||
: waybillStationAddresses(waybill);
|
||||
List<String> targetAddresses = stations.stream().map(Station::address).toList();
|
||||
// 首站地址只读;原数据里首站地址缺失时无从比对,放行而不是拒绝整次调整
|
||||
if (!sourceAddresses.isEmpty() && Func.isNotBlank(sourceAddresses.get(0))
|
||||
&& !Objects.equals(sourceAddresses.get(0), targetAddresses.get(0))) {
|
||||
throw new ServiceException("起始地址不允许修改");
|
||||
}
|
||||
String businessStatus = loading != null ? loading.getBusinessStatus() : waybill.getBusinessStatus();
|
||||
if (STATUS_COMPLETED.equals(businessStatus) || STATUS_CANCELLED.equals(businessStatus)) {
|
||||
throw new ServiceException("当前状态不允许变更运输路线");
|
||||
}
|
||||
RouteChangePlan plan = planRouteChange(loading, waybill, stations, sourceAddresses, targetAddresses);
|
||||
|
||||
// 事务内顺序固定:跟进记录 → 载体路线 → 异常状态。
|
||||
// 少了任何一步都读不出「谁在什么时候把路线改成了什么」,所以任一步失败都整体回滚
|
||||
insertFollowRecord(disposal.getId(), plan.followContent());
|
||||
if (loading != null) {
|
||||
applyLoadingRoute(loading, plan);
|
||||
} else {
|
||||
applyWaybillRoute(waybill, plan);
|
||||
}
|
||||
disposal.setDisposalStatus(STATUS_COMPLETED);
|
||||
disposal.setLatestFollowContent(plan.followContent());
|
||||
updateById(disposal);
|
||||
}
|
||||
|
||||
/** 一条跟进记录:处置记录里「先写记录再改状态」的顺序由调用方的事务保证 */
|
||||
private void insertFollowRecord(Long disposalId, String content) {
|
||||
ExceptionDisposalFollowRecord record = new ExceptionDisposalFollowRecord();
|
||||
record.setDisposalId(disposalId);
|
||||
record.setFollowContent(content);
|
||||
record.setFollowUser(AuthUtil.getUserId());
|
||||
record.setFollowUserName(UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
|
||||
record.setFollowTime(LocalDateTime.now());
|
||||
followRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
/** 配载单按配载单号取,逻辑删除的视为没有 */
|
||||
private LoadingManage findLoading(String loadingNo) {
|
||||
if (Func.isBlank(loadingNo)) {
|
||||
return null;
|
||||
}
|
||||
return loadingManageMapper.selectOne(Wrappers.<LoadingManage>lambdaQuery()
|
||||
.eq(LoadingManage::getLoadingNo, loadingNo.trim())
|
||||
.eq(LoadingManage::getIsDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并规范化站点:站数至少两个、首站 start、末站 end、其余 transit,地址都不能为空。
|
||||
* <p>
|
||||
* kind 不额外接受「按位置猜」的写法:站序就是落库顺序,kind 与位置不一致时写下去的首末站地址会是错的。
|
||||
*/
|
||||
private List<Station> validateRouteNodes(List<ExceptionDisposalRouteAdjustRequest.RouteNode> routeNodes,
|
||||
LoadingManage loading) {
|
||||
if (Func.isEmpty(routeNodes) || routeNodes.size() < 2) {
|
||||
throw new ServiceException("路线节点至少需要两个");
|
||||
}
|
||||
List<Station> stations = new ArrayList<>(routeNodes.size());
|
||||
for (int index = 0; index < routeNodes.size(); index++) {
|
||||
ExceptionDisposalRouteAdjustRequest.RouteNode node = routeNodes.get(index);
|
||||
if (node == null) {
|
||||
throw new ServiceException("请完善路线节点");
|
||||
}
|
||||
String address = Func.toStr(node.getAddress(), "").trim();
|
||||
if (Func.isBlank(address)) {
|
||||
throw new ServiceException("站点地址不能为空");
|
||||
}
|
||||
String expectedKind = index == 0
|
||||
? NODE_KIND_START
|
||||
: (index == routeNodes.size() - 1 ? NODE_KIND_END : NODE_KIND_TRANSIT);
|
||||
if (!expectedKind.equals(Func.toStr(node.getKind(), "").trim())) {
|
||||
throw new ServiceException("路线节点类型不正确");
|
||||
}
|
||||
stations.add(new Station(address, normalizedTags(node.getTags())));
|
||||
}
|
||||
assertTagsMatchChildWaybills(stations, loading);
|
||||
return stations;
|
||||
}
|
||||
|
||||
private List<String> normalizedTags(List<String> tags) {
|
||||
List<String> normalized = new ArrayList<>();
|
||||
if (tags == null) {
|
||||
return normalized;
|
||||
}
|
||||
for (String tag : tags) {
|
||||
String text = Func.toStr(tag, "").trim();
|
||||
if (Func.isNotBlank(text)) {
|
||||
normalized.add(text);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点标记里的数字是配载单运单集合里的位次(1 起),越界的标记会让司机端找不到对应子运单,
|
||||
* 那样的路线读不出装卸与货物,宁可报错也不落库。
|
||||
* <p>
|
||||
* 没有配载单就没有子运单序号可言:运单的路线段没有装卸归属,站点标记不参与落库。
|
||||
*/
|
||||
private void assertTagsMatchChildWaybills(List<Station> stations, LoadingManage loading) {
|
||||
if (loading == null) {
|
||||
return;
|
||||
}
|
||||
int childCount = childWaybillCount(loading.getWaybillIdsJson());
|
||||
for (Station station : stations) {
|
||||
for (String tag : station.tags()) {
|
||||
String digits = tag.replaceAll("\\D+", "");
|
||||
int order = Func.isEmpty(digits) ? 0 : Func.toInt(digits, 0);
|
||||
if (order < 1 || order > childCount) {
|
||||
throw new ServiceException("路线节点未关联子运单");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 配载单运单集合的元素个数,即站点标记可用的位次上限 */
|
||||
private int childWaybillCount(String waybillIdsJson) {
|
||||
if (Func.isBlank(waybillIdsJson)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
List<?> waybillIds = JsonUtil.parse(waybillIdsJson, List.class);
|
||||
return waybillIds == null ? 0 : waybillIds.size();
|
||||
} catch (Exception ignored) {
|
||||
// 运单集合不是 JSON 数组时按没有子运单处理,站点标记一律对不上
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 配载单当前路线的站点地址:优先站点行,没有站点行时回退到发货 / 途经 / 到货地址三列 */
|
||||
private List<String> loadingStationAddresses(LoadingManage loading) {
|
||||
List<String> addresses = new ArrayList<>();
|
||||
for (Map<String, Object> row : jsonObjectList(loading.getRouteJson())) {
|
||||
addAddress(addresses, text(row.get("address")));
|
||||
}
|
||||
if (!addresses.isEmpty()) {
|
||||
return addresses;
|
||||
}
|
||||
return addressColumns(loading.getDepartureAddress(), loading.getTransitAddress(), loading.getArrivalAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* 运单当前路线的站点地址:首段出发地是首站,其后每段的到达地各成一站。
|
||||
* 段里缺地址时退化用段名——运单详情与调整页用的也是这两列。
|
||||
*/
|
||||
private List<String> waybillStationAddresses(Waybill waybill) {
|
||||
List<Map<String, Object>> segments = routeSegments(waybill.getRouteJson());
|
||||
if (segments.isEmpty()) {
|
||||
return addressColumns(waybill.getDepartureAddress(), null, waybill.getArrivalAddress());
|
||||
}
|
||||
List<String> addresses = new ArrayList<>(segments.size() + 1);
|
||||
Map<String, Object> firstSegment = segments.get(0);
|
||||
addresses.add(firstNonBlank(text(firstSegment.get("departureAddress")),
|
||||
text(firstSegment.get("departureName")), Func.toStr(waybill.getDepartureAddress(), "")));
|
||||
for (Map<String, Object> segment : segments) {
|
||||
addresses.add(firstNonBlank(text(segment.get("arrivalAddress")), text(segment.get("arrivalName")), ""));
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/** 发货 / 途经 / 到货三列拼成站点地址,途经列按中英文逗号拆开 */
|
||||
private List<String> addressColumns(String departureAddress, String transitAddress, String arrivalAddress) {
|
||||
List<String> addresses = new ArrayList<>(3);
|
||||
addAddress(addresses, departureAddress);
|
||||
if (Func.isNotBlank(transitAddress)) {
|
||||
for (String transit : transitAddress.split("[,,]")) {
|
||||
addAddress(addresses, transit.trim());
|
||||
}
|
||||
}
|
||||
addAddress(addresses, arrivalAddress);
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private void addAddress(List<String> addresses, String address) {
|
||||
if (Func.isNotBlank(address)) {
|
||||
addresses.add(address.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把校验过的站点拼成一次写入所需的全部内容:跟进记录文案、载体要写的路线与地址、合并后的任务信息。
|
||||
* 长度口径与 PC 改路线一致。
|
||||
*/
|
||||
private RouteChangePlan planRouteChange(LoadingManage loading, Waybill waybill, List<Station> stations,
|
||||
List<String> sourceAddresses, List<String> targetAddresses) {
|
||||
String followContent = truncate(routeChangeContent(sourceAddresses, targetAddresses));
|
||||
String departureAddress = targetAddresses.get(0);
|
||||
String arrivalAddress = targetAddresses.get(targetAddresses.size() - 1);
|
||||
String routeJson;
|
||||
String transitAddress = null;
|
||||
String sourceTaskInfoJson;
|
||||
if (loading != null) {
|
||||
routeJson = stationRowsJson(stations);
|
||||
transitAddress = targetAddresses.subList(1, targetAddresses.size() - 1).stream()
|
||||
.collect(Collectors.joining(TRANSIT_ADDRESS_JOINER));
|
||||
sourceTaskInfoJson = loading.getTaskInfoJson();
|
||||
} else {
|
||||
// 运单的 route_json 是路线段形状,司机端按段解析:只按段改地址,绝不整份换成站点行。
|
||||
// 没变就一个键都不动,避免把历史数据里缺省的字段顺手补齐成另一份结构。
|
||||
routeJson = sourceAddresses.equals(targetAddresses)
|
||||
? null
|
||||
: rewriteWaybillRouteSegments(waybill.getRouteJson(), targetAddresses);
|
||||
sourceTaskInfoJson = waybill.getTaskInfoJson();
|
||||
}
|
||||
String taskInfoJson = appendRouteChangeRecord(sourceTaskInfoJson, followContent);
|
||||
|
||||
TransportBusinessSupport.validateLength(routeJson, ROUTE_JSON_MAX_LENGTH, "路线信息不能超过8000字");
|
||||
TransportBusinessSupport.validateLength(departureAddress, ADDRESS_MAX_LENGTH, "发货地址不能超过255字");
|
||||
TransportBusinessSupport.validateLength(arrivalAddress, ADDRESS_MAX_LENGTH, "收货地址不能超过255字");
|
||||
TransportBusinessSupport.validateLength(taskInfoJson, TASK_INFO_JSON_MAX_LENGTH, "任务信息不能超过8000字");
|
||||
return new RouteChangePlan(followContent, routeJson, departureAddress, transitAddress,
|
||||
arrivalAddress, taskInfoJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一份结论既进跟进记录,也进 task_info_json 的变更记录:
|
||||
* 首末站地址变了逐条写「出发地 / 到货地 旧 → 新」,否则说明途经站变化,完全没变给固定的一句。
|
||||
*/
|
||||
private String routeChangeContent(List<String> sourceAddresses, List<String> targetAddresses) {
|
||||
if (sourceAddresses.equals(targetAddresses)) {
|
||||
return ROUTE_CHANGE_UNCHANGED;
|
||||
}
|
||||
List<String> parts = new ArrayList<>(2);
|
||||
String sourceStart = firstOf(sourceAddresses);
|
||||
String targetStart = firstOf(targetAddresses);
|
||||
if (!Objects.equals(sourceStart, targetStart)) {
|
||||
parts.add("出发地 " + displayAddress(sourceStart) + " → " + displayAddress(targetStart));
|
||||
}
|
||||
String sourceEnd = lastOf(sourceAddresses);
|
||||
String targetEnd = lastOf(targetAddresses);
|
||||
if (!Objects.equals(sourceEnd, targetEnd)) {
|
||||
parts.add("到货地 " + displayAddress(sourceEnd) + " → " + displayAddress(targetEnd));
|
||||
}
|
||||
if (parts.isEmpty()) {
|
||||
// 首末站都没动,变化只能落在途经站:地址细节塞不进 500 字的跟进记录,按站数说明
|
||||
parts.add("途经站由 " + transitCount(sourceAddresses) + " 个改为 " + transitCount(targetAddresses) + " 个");
|
||||
}
|
||||
return ROUTE_CHANGE_PREFIX + String.join(";", parts);
|
||||
}
|
||||
|
||||
private String firstOf(List<String> addresses) {
|
||||
return addresses.isEmpty() ? "" : addresses.get(0);
|
||||
}
|
||||
|
||||
private String lastOf(List<String> addresses) {
|
||||
return addresses.isEmpty() ? "" : addresses.get(addresses.size() - 1);
|
||||
}
|
||||
|
||||
private int transitCount(List<String> addresses) {
|
||||
return Math.max(addresses.size() - 2, 0);
|
||||
}
|
||||
|
||||
private String displayAddress(String address) {
|
||||
return Func.isBlank(address) ? "-" : address;
|
||||
}
|
||||
|
||||
/** 配载单的路线是站点行:地址、展示地址、该站装卸的子运单标记 */
|
||||
private String stationRowsJson(List<Station> stations) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>(stations.size());
|
||||
for (Station station : stations) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(4);
|
||||
row.put("address", station.address());
|
||||
// 站点行没有独立的站名,展示地址与地址取同一份文本
|
||||
row.put("displayAddress", station.address());
|
||||
row.put("tags", station.tags());
|
||||
// locked 只影响 PC 编辑弹窗能否拖动,司机端不读;这里写入的路线始终可继续调整
|
||||
row.put("locked", false);
|
||||
rows.add(row);
|
||||
}
|
||||
return JsonUtil.toJson(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按路线段改写运单路线:第 i 段的到达地对应第 i+1 站,首段出发地对应首站。
|
||||
* 其它键(segmentNo、transportType、未知键)原样保留。
|
||||
* <p>
|
||||
* 站数变了或现有 route_json 不是可识别的段数组时返回 null,调用方跳过路线改写:
|
||||
* 段形状没有装卸归属,说不出哪一段被删掉或被挪动,硬写等于重写这趟车的行程;
|
||||
* 历史节点行(PC 改运单路线写下的 address + tags)同理不动,改写会让司机端读不出路线。
|
||||
*/
|
||||
private String rewriteWaybillRouteSegments(String routeJson, List<String> targetAddresses) {
|
||||
List<Map<String, Object>> segments = routeSegments(routeJson);
|
||||
if (segments.isEmpty() || segments.size() + 1 != targetAddresses.size()) {
|
||||
return null;
|
||||
}
|
||||
List<Map<String, Object>> rewritten = new ArrayList<>(segments.size());
|
||||
for (int index = 0; index < segments.size(); index++) {
|
||||
Map<String, Object> segment = new LinkedHashMap<>(segments.get(index));
|
||||
rewriteSegmentPoint(segment, "departure", index == 0 ? targetAddresses.get(0) : null);
|
||||
rewriteSegmentPoint(segment, "arrival", targetAddresses.get(index + 1));
|
||||
rewritten.add(segment);
|
||||
}
|
||||
return JsonUtil.toJson(rewritten);
|
||||
}
|
||||
|
||||
private void rewriteSegmentPoint(Map<String, Object> segment, String point, String targetAddress) {
|
||||
if (targetAddress == null) {
|
||||
return;
|
||||
}
|
||||
String currentAddress = firstNonBlank(text(segment.get(point + "Address")),
|
||||
text(segment.get(point + "Name")), "");
|
||||
if (Objects.equals(currentAddress, targetAddress)) {
|
||||
return;
|
||||
}
|
||||
// 站名与地址是同一份文本:调整页只有一个地址输入框,改了地址站名也得跟着变
|
||||
segment.put(point + "Address", targetAddress);
|
||||
segment.put(point + "Name", targetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别路线段形状的 route_json。运单历史上存在两种形状,另一种是 PC 写下的节点行
|
||||
* (address + tags + 运单号),司机端按段解析读不出来,因此这里只认段:元素带段字段、
|
||||
* 且不带 address / tags。
|
||||
*/
|
||||
private List<Map<String, Object>> routeSegments(String routeJson) {
|
||||
List<Map<String, Object>> elements = jsonObjectList(routeJson);
|
||||
for (Map<String, Object> element : elements) {
|
||||
if (element.containsKey("address") || element.containsKey("tags")) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (!element.containsKey("departureAddress") && !element.containsKey("departureName")
|
||||
&& !element.containsKey("arrivalAddress") && !element.containsKey("arrivalName")) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务信息是整列覆盖:读出原对象,只把本次变更记录插到最前面,其余键原样写回。
|
||||
* 原来的内容不是 JSON 对象(坏数据或历史数组)时按空对象重建,只丢这一列的无法解析部分,
|
||||
* 不能让一次路线调整因为附件信息列而失败。
|
||||
*/
|
||||
private String appendRouteChangeRecord(String taskInfoJson, String content) {
|
||||
Map<String, Object> taskInfo = parseTaskInfo(taskInfoJson);
|
||||
List<Object> records = new ArrayList<>();
|
||||
Map<String, Object> record = new LinkedHashMap<>(4);
|
||||
record.put("changeTime", DateUtil.formatDateTime(LocalDateTime.now()));
|
||||
record.put("changeUser", UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
|
||||
record.put("content", content);
|
||||
record.put("remark", "");
|
||||
records.add(record);
|
||||
Object existing = taskInfo.get(TASK_INFO_ROUTE_CHANGE_RECORDS);
|
||||
if (existing instanceof List<?> existingRecords) {
|
||||
records.addAll(existingRecords);
|
||||
}
|
||||
taskInfo.put(TASK_INFO_ROUTE_CHANGE_RECORDS, records);
|
||||
return JsonUtil.toJson(taskInfo);
|
||||
}
|
||||
|
||||
/** 任务信息只认 JSON 对象:数组与坏数据都按空对象重建,这一列不能挡住整次路线调整 */
|
||||
private Map<String, Object> parseTaskInfo(String taskInfoJson) {
|
||||
Map<String, Object> taskInfo = new LinkedHashMap<>();
|
||||
if (Func.isBlank(taskInfoJson)) {
|
||||
return taskInfo;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(taskInfoJson, Object.class);
|
||||
if (parsed instanceof Map<?, ?> map) {
|
||||
map.forEach((key, value) -> taskInfo.put(String.valueOf(key), value));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
taskInfo.clear();
|
||||
}
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private void applyLoadingRoute(LoadingManage loading, RouteChangePlan plan) {
|
||||
loading.setRouteJson(plan.routeJson());
|
||||
loading.setDepartureAddress(plan.departureAddress());
|
||||
loading.setTransitAddress(TransportBusinessSupport.trimToNull(plan.transitAddress()));
|
||||
loading.setArrivalAddress(plan.arrivalAddress());
|
||||
loading.setTaskInfoJson(plan.taskInfoJson());
|
||||
if (loadingManageMapper.updateById(loading) <= 0) {
|
||||
throw new ServiceException("变更运输路线失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyWaybillRoute(Waybill waybill, RouteChangePlan plan) {
|
||||
if (Func.isNotBlank(plan.routeJson())) {
|
||||
waybill.setRouteJson(plan.routeJson());
|
||||
}
|
||||
waybill.setDepartureAddress(plan.departureAddress());
|
||||
waybill.setArrivalAddress(plan.arrivalAddress());
|
||||
waybill.setTaskInfoJson(plan.taskInfoJson());
|
||||
if (!waybillService.updateById(waybill)) {
|
||||
throw new ServiceException("变更运输路线失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析成对象数组;坏 JSON 与空值都按空数组处理,路线坏了不该让整次操作崩掉 */
|
||||
private List<Map<String, Object>> jsonObjectList(String json) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
if (Func.isBlank(json)) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, List.class);
|
||||
if (parsed instanceof List<?> list) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String text(Object value) {
|
||||
return value == null ? "" : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
private String firstNonBlank(String first, String second, String fallback) {
|
||||
if (Func.isNotBlank(first)) {
|
||||
return first;
|
||||
}
|
||||
return Func.isNotBlank(second) ? second : fallback;
|
||||
}
|
||||
|
||||
/** 规范化后的一个站点:地址 + 该站的装卸标记 */
|
||||
private record Station(String address, List<String> tags) {
|
||||
}
|
||||
|
||||
/** 一次路线调整要落库的全部内容,写入阶段只负责 set 与 update */
|
||||
private record RouteChangePlan(String followContent,
|
||||
String routeJson,
|
||||
String departureAddress,
|
||||
String transitAddress,
|
||||
String arrivalAddress,
|
||||
String taskInfoJson) {
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ExceptionDisposal> buildQuery(ExceptionDisposalVO query) {
|
||||
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.eq(ExceptionDisposal::getIsDeleted, 0)
|
||||
|
||||
Reference in New Issue
Block a user