🔀 合并 ticket 03:异常详情下发配载单、子运单与可调整站点
This commit is contained in:
+37
@@ -30,6 +30,7 @@ import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -93,4 +94,40 @@ public class ExceptionDisposalVO extends ExceptionDisposal {
|
||||
@Schema(description = "货物信息:{name, weight}")
|
||||
private Map<String, String> cargo;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "关联运单的配载单号,没有配载单时为空")
|
||||
private String loadingNo;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "配载单下的子运单号,按配载单运单集合的顺序排列")
|
||||
private List<String> childWaybillNos;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "可调整的路线站点,少于两个站时为空")
|
||||
private List<RouteNode> routeNodes;
|
||||
|
||||
/**
|
||||
* 一个可调整的站点:站序声明、地址、该站的装卸标记与项目名
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "路线站点")
|
||||
public static class RouteNode implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "站点类型:start 首站、transit 中间站、end 末站")
|
||||
private String kind;
|
||||
|
||||
@Schema(description = "站点地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "该站的装卸标记,形如「装1」「卸2」,数字是配载单子运单序号")
|
||||
private List<String> tags;
|
||||
|
||||
@Schema(description = "该站装卸的子运单项目名,按标记顺序去重")
|
||||
private List<String> projects;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+205
-4
@@ -61,10 +61,12 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -605,8 +607,7 @@ public class ExceptionDisposalServiceImpl
|
||||
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);
|
||||
int order = tagOrder(tag);
|
||||
if (order < 1 || order > childCount) {
|
||||
throw new ServiceException("路线节点未关联子运单");
|
||||
}
|
||||
@@ -631,7 +632,7 @@ public class ExceptionDisposalServiceImpl
|
||||
/** 配载单当前路线的站点地址:优先站点行,没有站点行时回退到发货 / 途经 / 到货地址三列 */
|
||||
private List<String> loadingStationAddresses(LoadingManage loading) {
|
||||
List<String> addresses = new ArrayList<>();
|
||||
for (Map<String, Object> row : jsonObjectList(loading.getRouteJson())) {
|
||||
for (Map<String, Object> row : loadingStationRows(loading)) {
|
||||
addAddress(addresses, text(row.get("address")));
|
||||
}
|
||||
if (!addresses.isEmpty()) {
|
||||
@@ -978,7 +979,7 @@ public class ExceptionDisposalServiceImpl
|
||||
return vo;
|
||||
}
|
||||
|
||||
/** 按关联运单补齐详情页路线 / 货物展示字段 */
|
||||
/** 按关联运单补齐详情页路线 / 货物 / 配载单展示字段 */
|
||||
private void fillRouteAndCargo(ExceptionDisposalVO vo, Long waybillId) {
|
||||
if (vo == null || waybillId == null) {
|
||||
return;
|
||||
@@ -996,6 +997,206 @@ public class ExceptionDisposalServiceImpl
|
||||
cargo.put("name", Func.toStr(waybill.getCargoName(), ""));
|
||||
cargo.put("weight", formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()));
|
||||
vo.setCargo(cargo);
|
||||
|
||||
fillRouteNodes(vo, waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情页的可编辑站点与配载单信息。
|
||||
* <p>
|
||||
* 配载单是这趟车的执行主体,站点取它的站点行;运单没有配载单号、或配载单已被删除 / 解绑时退回
|
||||
* 运单自己的路线段——历史异常仍要能打开,不能因为配载单不在了就打不开详情。
|
||||
*/
|
||||
private void fillRouteNodes(ExceptionDisposalVO vo, Waybill waybill) {
|
||||
LoadingManage loading = findLoading(waybill.getLoadingNo());
|
||||
if (loading == null) {
|
||||
vo.setChildWaybillNos(new ArrayList<>());
|
||||
vo.setRouteNodes(waybillRouteNodes(waybill));
|
||||
return;
|
||||
}
|
||||
Map<Integer, Waybill> childWaybills = childWaybillsByOrder(loading);
|
||||
vo.setLoadingNo(loading.getLoadingNo());
|
||||
vo.setChildWaybillNos(childWaybillNos(childWaybills));
|
||||
List<Map<String, Object>> rows = loadingStationRows(loading);
|
||||
vo.setRouteNodes(loadingRouteNodes(rows, childWaybills));
|
||||
vo.setRoute(loadingRoute(rows, loading, waybill));
|
||||
}
|
||||
|
||||
/** 配载单的站点行:只有带地址的行才算一个站,缺地址的历史行不参与 */
|
||||
private List<Map<String, Object>> loadingStationRows(LoadingManage loading) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : jsonObjectList(loading.getRouteJson())) {
|
||||
if (Func.isNotBlank(text(row.get("address")))) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private List<ExceptionDisposalVO.RouteNode> loadingRouteNodes(List<Map<String, Object>> rows,
|
||||
Map<Integer, Waybill> childWaybills) {
|
||||
List<ExceptionDisposalVO.RouteNode> nodes = new ArrayList<>(rows.size());
|
||||
for (Map<String, Object> row : rows) {
|
||||
List<String> tags = stringList(row.get("tags"));
|
||||
ExceptionDisposalVO.RouteNode node = new ExceptionDisposalVO.RouteNode();
|
||||
node.setAddress(text(row.get("address")));
|
||||
node.setTags(tags);
|
||||
node.setProjects(stationProjects(tags, childWaybills));
|
||||
nodes.add(node);
|
||||
}
|
||||
return stationKinds(nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 没有配载单时从路线段推导站点:首段出发地是首站,其后每段的到达地各成一站。
|
||||
* 地址口径与改路线时的比对一致(地址优先、站名兜底),否则用户什么都没改直接提交也会被判成改过地址。
|
||||
*/
|
||||
private List<ExceptionDisposalVO.RouteNode> waybillRouteNodes(Waybill waybill) {
|
||||
List<ExceptionDisposalVO.RouteNode> nodes = new ArrayList<>();
|
||||
for (String address : waybillStationAddresses(waybill)) {
|
||||
if (Func.isBlank(address)) {
|
||||
continue;
|
||||
}
|
||||
ExceptionDisposalVO.RouteNode node = new ExceptionDisposalVO.RouteNode();
|
||||
node.setAddress(address);
|
||||
node.setTags(new ArrayList<>());
|
||||
node.setProjects(new ArrayList<>());
|
||||
nodes.add(node);
|
||||
}
|
||||
return stationKinds(nodes);
|
||||
}
|
||||
|
||||
/** 站序声明:首站 start、末站 end、其余 transit;不足两个站就没有首末可言,不给前端调整入口 */
|
||||
private List<ExceptionDisposalVO.RouteNode> stationKinds(List<ExceptionDisposalVO.RouteNode> nodes) {
|
||||
if (nodes.size() < 2) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
for (int index = 0; index < nodes.size(); index++) {
|
||||
nodes.get(index).setKind(index == 0 ? NODE_KIND_START
|
||||
: (index == nodes.size() - 1 ? NODE_KIND_END : NODE_KIND_TRANSIT));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点标记指向配载单运单集合里的第 N 张子运单,取其项目名去重。
|
||||
* 标记对不上子运单(越界、子运单已不存在)时跳过,一个坏标记不该让整个详情打不开。
|
||||
*/
|
||||
private List<String> stationProjects(List<String> tags, Map<Integer, Waybill> childWaybills) {
|
||||
Set<String> projects = new LinkedHashSet<>();
|
||||
for (String tag : tags) {
|
||||
Waybill child = childWaybills.get(tagOrder(tag) - 1);
|
||||
if (child == null) {
|
||||
continue;
|
||||
}
|
||||
String projectName = Func.toStr(child.getProjectName(), "").trim();
|
||||
if (Func.isNotBlank(projectName)) {
|
||||
projects.add(projectName);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(projects);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按配载单运单集合的位次取子运单,位次从 0 起(站点标记里的 N 减一)。
|
||||
* 位次不能因为某张子运单查不到而前移,否则标记会指到别的子运单上。
|
||||
*/
|
||||
private Map<Integer, Waybill> childWaybillsByOrder(LoadingManage loading) {
|
||||
Map<Integer, Waybill> childWaybills = new LinkedHashMap<>();
|
||||
List<Long> waybillIds = waybillIds(loading.getWaybillIdsJson());
|
||||
List<Long> queryIds = waybillIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (queryIds.isEmpty()) {
|
||||
return childWaybills;
|
||||
}
|
||||
Map<Long, Waybill> waybillsById = new HashMap<>();
|
||||
List<Waybill> found = waybillService.listByIds(queryIds);
|
||||
if (found != null) {
|
||||
for (Waybill child : found) {
|
||||
if (child != null && !Objects.equals(child.getIsDeleted(), 1)) {
|
||||
waybillsById.put(child.getId(), child);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int order = 0; order < waybillIds.size(); order++) {
|
||||
Waybill child = waybillsById.get(waybillIds.get(order));
|
||||
if (child != null) {
|
||||
childWaybills.put(order, child);
|
||||
}
|
||||
}
|
||||
return childWaybills;
|
||||
}
|
||||
|
||||
/** 配载单运单集合的元素是运单主键;元素不是主键时留空占位,后面的位次不能因此前移 */
|
||||
private List<Long> waybillIds(String waybillIdsJson) {
|
||||
List<Long> waybillIds = new ArrayList<>();
|
||||
if (Func.isBlank(waybillIdsJson)) {
|
||||
return waybillIds;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(waybillIdsJson, List.class);
|
||||
if (parsed instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
long waybillId = Func.toLong(text(item), 0L);
|
||||
waybillIds.add(waybillId > 0 ? waybillId : null);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 运单集合坏了就没有子运单可言,详情退回运单自己的路线,不影响打开
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return waybillIds;
|
||||
}
|
||||
|
||||
/** 子运单号按配载单运单集合的顺序给出;查不到的子运单跳过,不占位 */
|
||||
private List<String> childWaybillNos(Map<Integer, Waybill> childWaybills) {
|
||||
List<String> childWaybillNos = new ArrayList<>(childWaybills.size());
|
||||
for (Waybill child : childWaybills.values()) {
|
||||
String waybillNo = Func.toStr(child.getWaybillNo(), "").trim();
|
||||
if (Func.isNotBlank(waybillNo)) {
|
||||
childWaybillNos.add(waybillNo);
|
||||
}
|
||||
}
|
||||
return childWaybillNos;
|
||||
}
|
||||
|
||||
/** 有配载单时路线摘要取这趟车自己的首末站:站点行的展示地址优先,没有站点行回落到地址列 */
|
||||
private Map<String, String> loadingRoute(List<Map<String, Object>> rows, LoadingManage loading, Waybill waybill) {
|
||||
String start;
|
||||
String end;
|
||||
if (rows.isEmpty()) {
|
||||
start = firstNonBlank(loading.getDepartureAddress(), waybill.getDepartureName(), "");
|
||||
end = firstNonBlank(loading.getArrivalAddress(), waybill.getArrivalName(), "");
|
||||
} else {
|
||||
start = stationDisplayAddress(rows.get(0));
|
||||
end = stationDisplayAddress(rows.get(rows.size() - 1));
|
||||
}
|
||||
Map<String, String> route = new HashMap<>(2);
|
||||
route.put("start", start);
|
||||
route.put("end", end);
|
||||
return route;
|
||||
}
|
||||
|
||||
private String stationDisplayAddress(Map<String, Object> row) {
|
||||
return firstNonBlank(text(row.get("displayAddress")), text(row.get("address")), "");
|
||||
}
|
||||
|
||||
/** 站点标记里的数字是配载单运单集合里的位次(1 起),取不到数字时返回 0 */
|
||||
private int tagOrder(String tag) {
|
||||
String digits = Func.toStr(tag, "").replaceAll("\\D+", "");
|
||||
return Func.isEmpty(digits) ? 0 : Func.toInt(digits, 0);
|
||||
}
|
||||
|
||||
private List<String> stringList(Object value) {
|
||||
List<String> texts = new ArrayList<>();
|
||||
if (value instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
String text = text(item);
|
||||
if (Func.isNotBlank(text)) {
|
||||
texts.add(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
private String formatWeight(BigDecimal quantity, String unit) {
|
||||
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
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.MockedStatic;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
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.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.ExceptionDisposalVO;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 异常详情下发的配载单、子运单与可编辑站点。
|
||||
* <p>
|
||||
* 有配载单时站点取配载单的站点行(这趟车的执行主体就是配载单);没有配载单才退回运单的路线段,
|
||||
* 站点地址必须沿用改路线时的比对口径(地址优先、站名兜底),否则原样提交会被判成改过地址。
|
||||
*/
|
||||
class ExceptionDisposalDetailPayloadTest {
|
||||
|
||||
private static final Long DISPOSAL_ID = 9101L;
|
||||
private static final Long WAYBILL_ID = 9201L;
|
||||
private static final String LOADING_NO = "PZ20261023009";
|
||||
private static final String WAYBILL_NO = "YD202610230009";
|
||||
private static final Long CHILD_ID_FIRST = 9301L;
|
||||
private static final Long CHILD_ID_SECOND = 9302L;
|
||||
private static final String CHILD_NO_FIRST = "YD202610230011";
|
||||
private static final String CHILD_NO_SECOND = "YD202610230012";
|
||||
private static final String PROJECT_FIRST = "冷链项目";
|
||||
private static final String PROJECT_SECOND = "普货项目";
|
||||
private static final String START = "南宁市/邕宁区/那赖二路神网数码物流园";
|
||||
private static final String TRANSIT = "钦州市/沙坪坝区/泰安大道物流园";
|
||||
private static final String END = "成都市/双流区/口岸路成藏宝物流园";
|
||||
private static final String WAYBILL_START_NAME = "南宁";
|
||||
private static final String WAYBILL_END_NAME = "成都";
|
||||
|
||||
private ExceptionDisposalMapper disposalMapper;
|
||||
private ExceptionDisposalFollowRecordMapper followRecordMapper;
|
||||
private IWaybillService waybillService;
|
||||
private LoadingManageMapper loadingManageMapper;
|
||||
private ExceptionDisposalServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
// 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class);
|
||||
TableInfoHelper.initTableInfo(assistant, ExceptionDisposalFollowRecord.class);
|
||||
TableInfoHelper.initTableInfo(assistant, LoadingManage.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
disposalMapper = mock(ExceptionDisposalMapper.class);
|
||||
followRecordMapper = mock(ExceptionDisposalFollowRecordMapper.class);
|
||||
waybillService = mock(IWaybillService.class);
|
||||
loadingManageMapper = mock(LoadingManageMapper.class);
|
||||
service = new ExceptionDisposalServiceImpl(
|
||||
followRecordMapper, waybillService, mock(IDriverWaybillService.class), loadingManageMapper);
|
||||
inject("baseMapper", disposalMapper);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 有配载单
|
||||
|
||||
@Test
|
||||
@DisplayName("有配载单:下发配载单号、按运单集合顺序的子运单号,站点与项目取自站点行")
|
||||
void loadingDetailPublishesChildWaybillsAndStationNodes() {
|
||||
stubDisposal();
|
||||
stubWaybill(LOADING_NO, null);
|
||||
stubLoading(List.of(
|
||||
stationRow(START, null, List.of("装1", "卸1")),
|
||||
stationRow(TRANSIT, null, List.of("卸1", "装2")),
|
||||
stationRow(END, null, List.of("卸2"))));
|
||||
// 数据库不保证按主键集合的顺序返回:倒序回来,下发仍要按配载单运单集合的顺序
|
||||
when(waybillService.listByIds(any())).thenReturn(List.of(
|
||||
child(CHILD_ID_SECOND, CHILD_NO_SECOND, PROJECT_SECOND),
|
||||
child(CHILD_ID_FIRST, CHILD_NO_FIRST, PROJECT_FIRST)));
|
||||
|
||||
ExceptionDisposalVO vo = detailResponse();
|
||||
|
||||
assertEquals(LOADING_NO, vo.getLoadingNo());
|
||||
assertEquals(List.of(CHILD_NO_FIRST, CHILD_NO_SECOND), vo.getChildWaybillNos(),
|
||||
"子运单号顺序就是站点标记里数字的顺序");
|
||||
|
||||
List<ExceptionDisposalVO.RouteNode> nodes = vo.getRouteNodes();
|
||||
assertEquals(3, nodes.size(), "站点数与配载单路线一致");
|
||||
assertEquals(List.of("start", "transit", "end"), kinds(nodes));
|
||||
assertEquals(START, nodes.get(0).getAddress());
|
||||
assertEquals(TRANSIT, nodes.get(1).getAddress());
|
||||
assertEquals(END, nodes.get(2).getAddress());
|
||||
assertEquals(List.of("装1", "卸1"), nodes.get(0).getTags());
|
||||
assertEquals(List.of(PROJECT_FIRST), nodes.get(0).getProjects(), "同一站的同一项目只出现一次");
|
||||
assertEquals(List.of(PROJECT_FIRST, PROJECT_SECOND), nodes.get(1).getProjects(),
|
||||
"标记按顺序映射到子运单");
|
||||
assertEquals(List.of(PROJECT_SECOND), nodes.get(2).getProjects());
|
||||
assertEquals(START, vo.getRoute().get("start"));
|
||||
assertEquals(END, vo.getRoute().get("end"));
|
||||
|
||||
verify(disposalMapper, never()).updateById(any(ExceptionDisposal.class));
|
||||
verify(loadingManageMapper, never()).updateById(any(LoadingManage.class));
|
||||
verify(waybillService, never()).updateById(any(Waybill.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("路线摘要用站点行的展示地址,站点地址仍取地址列")
|
||||
void routeSummaryPrefersDisplayAddress() {
|
||||
stubDisposal();
|
||||
stubWaybill(LOADING_NO, null);
|
||||
stubLoading(List.of(
|
||||
stationRow(START, START + "(展示)", List.of("装1")),
|
||||
stationRow(END, null, List.of("卸2"))));
|
||||
when(waybillService.listByIds(any())).thenReturn(List.of(
|
||||
child(CHILD_ID_FIRST, CHILD_NO_FIRST, PROJECT_FIRST),
|
||||
child(CHILD_ID_SECOND, CHILD_NO_SECOND, PROJECT_SECOND)));
|
||||
|
||||
ExceptionDisposalVO vo = detailResponse();
|
||||
|
||||
assertEquals(START + "(展示)", vo.getRoute().get("start"), "摘要展示用站点行的展示地址");
|
||||
assertEquals(END, vo.getRoute().get("end"));
|
||||
assertEquals(START, vo.getRouteNodes().get(0).getAddress(),
|
||||
"站点地址是改路线时比对用的地址列,不能换成展示地址");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("只有一个站时不给出可编辑站点")
|
||||
void singleStationGetsNoRouteNodes() {
|
||||
stubDisposal();
|
||||
stubWaybill(LOADING_NO, null);
|
||||
stubLoading(List.of(stationRow(START, null, List.of("装1"))));
|
||||
when(waybillService.listByIds(any())).thenReturn(List.of(
|
||||
child(CHILD_ID_FIRST, CHILD_NO_FIRST, PROJECT_FIRST)));
|
||||
|
||||
ExceptionDisposalVO vo = detailResponse();
|
||||
|
||||
assertTrue(vo.getRouteNodes().isEmpty(), "没有首末站就没有可调整的站序");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("子运单查不到时跳过,不打乱其余子运单的顺序")
|
||||
void missingChildWaybillIsSkipped() {
|
||||
stubDisposal();
|
||||
stubWaybill(LOADING_NO, null);
|
||||
stubLoading(List.of(
|
||||
stationRow(START, null, List.of("装1")),
|
||||
stationRow(TRANSIT, null, List.of("卸1", "装2")),
|
||||
stationRow(END, null, List.of("卸2"))));
|
||||
// 第一张子运单已不在(被删或运单集合与运单实际不一致),只剩第二张
|
||||
when(waybillService.listByIds(any())).thenReturn(List.of(
|
||||
child(CHILD_ID_SECOND, CHILD_NO_SECOND, PROJECT_SECOND)));
|
||||
|
||||
ExceptionDisposalVO vo = detailResponse();
|
||||
|
||||
assertEquals(List.of(CHILD_NO_SECOND), vo.getChildWaybillNos());
|
||||
assertEquals(3, vo.getRouteNodes().size(), "站点不因子运单缺失而消失");
|
||||
assertTrue(vo.getRouteNodes().get(0).getProjects().isEmpty(), "找不到子运单的标记给空项目");
|
||||
assertEquals(List.of(PROJECT_SECOND), vo.getRouteNodes().get(1).getProjects());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 无配载单
|
||||
|
||||
@Test
|
||||
@DisplayName("配载单已不存在时退回运单路线段,历史异常照常打开")
|
||||
void missingLoadingFallsBackToWaybillRouteSegments() {
|
||||
stubDisposal();
|
||||
// 运单还挂着配载单号,配载单已被删除或解绑:按没有配载单处理
|
||||
stubWaybill("PZ202610230404", segmentRouteJson());
|
||||
|
||||
ExceptionDisposalVO vo = assertDoesNotThrow(this::detailResponse);
|
||||
|
||||
assertNull(vo.getLoadingNo());
|
||||
assertTrue(vo.getChildWaybillNos().isEmpty());
|
||||
List<ExceptionDisposalVO.RouteNode> nodes = vo.getRouteNodes();
|
||||
assertEquals(3, nodes.size(), "首段出发地是首站,其后每段到达地各一站");
|
||||
assertEquals(List.of("start", "transit", "end"), kinds(nodes));
|
||||
assertEquals(START, nodes.get(0).getAddress());
|
||||
assertEquals(List.of(), nodes.get(0).getTags(), "运单路线段没有装卸归属");
|
||||
assertEquals(List.of(), nodes.get(0).getProjects());
|
||||
assertEquals(WAYBILL_START_NAME, vo.getRoute().get("start"), "无配载单时摘要维持运单起讫名");
|
||||
assertEquals(WAYBILL_END_NAME, vo.getRoute().get("end"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无配载单的站点地址地址优先、站名兜底,与改路线时的比对口径一致")
|
||||
void waybillNodesPreferAddressOverName() {
|
||||
stubDisposal();
|
||||
stubWaybill(null, JsonUtil.toJson(List.of(
|
||||
segment(WAYBILL_START_NAME, START, "钦州", TRANSIT),
|
||||
segment("钦州", TRANSIT, "贵阳", null))));
|
||||
|
||||
ExceptionDisposalVO vo = detailResponse();
|
||||
|
||||
List<ExceptionDisposalVO.RouteNode> nodes = vo.getRouteNodes();
|
||||
assertEquals(List.of(START, TRANSIT, "贵阳"),
|
||||
nodes.stream().map(ExceptionDisposalVO.RouteNode::getAddress).toList(),
|
||||
"原样提交时站点地址要与运单段地址一致,缺地址才用站名兜底");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private void stubDisposal() {
|
||||
ExceptionDisposal disposal = new ExceptionDisposal();
|
||||
disposal.setId(DISPOSAL_ID);
|
||||
disposal.setDisposalStatus("pending");
|
||||
disposal.setWaybillId(WAYBILL_ID);
|
||||
disposal.setIsDeleted(0);
|
||||
when(disposalMapper.selectById(DISPOSAL_ID)).thenReturn(disposal);
|
||||
}
|
||||
|
||||
private void stubWaybill(String loadingNo, String routeJson) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo(WAYBILL_NO);
|
||||
waybill.setLoadingNo(loadingNo);
|
||||
waybill.setRouteJson(routeJson);
|
||||
waybill.setDepartureName(WAYBILL_START_NAME);
|
||||
waybill.setArrivalName(WAYBILL_END_NAME);
|
||||
waybill.setIsDeleted(0);
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill);
|
||||
}
|
||||
|
||||
private void stubLoading(List<Map<String, Object>> rows) {
|
||||
LoadingManage loading = new LoadingManage();
|
||||
loading.setId(9401L);
|
||||
loading.setLoadingNo(LOADING_NO);
|
||||
loading.setWaybillIdsJson("[" + CHILD_ID_FIRST + "," + CHILD_ID_SECOND + "]");
|
||||
loading.setRouteJson(JsonUtil.toJson(rows));
|
||||
loading.setIsDeleted(0);
|
||||
when(loadingManageMapper.selectOne(any())).thenReturn(loading);
|
||||
}
|
||||
|
||||
private Waybill child(Long id, String waybillNo, String projectName) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(id);
|
||||
waybill.setWaybillNo(waybillNo);
|
||||
waybill.setProjectName(projectName);
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private Map<String, Object> stationRow(String address, String displayAddress, List<String> tags) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("address", address);
|
||||
if (displayAddress != null) {
|
||||
row.put("displayAddress", displayAddress);
|
||||
}
|
||||
row.put("tags", tags);
|
||||
row.put("locked", false);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 运单的路线段:两段三站,带地址的段 */
|
||||
private String segmentRouteJson() {
|
||||
return JsonUtil.toJson(List.of(
|
||||
segment(WAYBILL_START_NAME, START, TRANSIT, TRANSIT),
|
||||
segment(TRANSIT, TRANSIT, WAYBILL_END_NAME, END)));
|
||||
}
|
||||
|
||||
private Map<String, Object> segment(String departureName, String departureAddress,
|
||||
String arrivalName, String arrivalAddress) {
|
||||
Map<String, Object> segment = new LinkedHashMap<>();
|
||||
segment.put("departureName", departureName);
|
||||
segment.put("departureAddress", departureAddress);
|
||||
segment.put("arrivalName", arrivalName);
|
||||
if (arrivalAddress != null) {
|
||||
segment.put("arrivalAddress", arrivalAddress);
|
||||
}
|
||||
return segment;
|
||||
}
|
||||
|
||||
private List<String> kinds(List<ExceptionDisposalVO.RouteNode> nodes) {
|
||||
return nodes.stream().map(ExceptionDisposalVO.RouteNode::getKind).toList();
|
||||
}
|
||||
|
||||
private ExceptionDisposalVO detailResponse() {
|
||||
// 详情里的创建人 / 更新人姓名走缓存,单测不装配 Spring 上下文
|
||||
try (MockedStatic<UserDisplayCache> ignored = mockStatic(UserDisplayCache.class)) {
|
||||
return service.detail(DISPOSAL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user