🐛 合并 websoft/dev:改路线重推站名并回退进行中运单配载准入
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
@@ -863,6 +863,10 @@ public class ExceptionDisposalServiceImpl
|
||||
if (Func.isNotBlank(plan.routeJson())) {
|
||||
waybill.setRouteJson(plan.routeJson());
|
||||
}
|
||||
waybill.setDepartureName(TransportBusinessSupport.syncRegionName(
|
||||
waybill.getDepartureName(), waybill.getDepartureAddress(), plan.departureAddress()));
|
||||
waybill.setArrivalName(TransportBusinessSupport.syncRegionName(
|
||||
waybill.getArrivalName(), waybill.getArrivalAddress(), plan.arrivalAddress()));
|
||||
waybill.setDepartureAddress(plan.departureAddress());
|
||||
waybill.setArrivalAddress(plan.arrivalAddress());
|
||||
waybill.setTaskInfoJson(plan.taskInfoJson());
|
||||
|
||||
+2
-2
@@ -884,8 +884,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
String prefix = "运单" + waybill.getWaybillNo();
|
||||
return prefix + switch (reason) {
|
||||
case NOT_ROAD -> "运输方式不是公路,不可配载";
|
||||
case NOT_LOADABLE_STATUS -> "当前状态不是待执行或进行中,不可配载";
|
||||
case ON_OTHER_MANIFEST -> "已关联其他有效配载单";
|
||||
case NOT_PENDING -> "当前状态不是待执行,不可配载";
|
||||
case ON_OTHER_MANIFEST -> "已关联其他配载单";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -1510,13 +1510,23 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
if ("completed".equals(oldRecord.getBusinessStatus()) || "cancelled".equals(oldRecord.getBusinessStatus())) {
|
||||
throw new ServiceException("当前运单状态不允许变更运输路线");
|
||||
}
|
||||
String previousDepartureAddress = oldRecord.getDepartureAddress();
|
||||
String previousArrivalAddress = oldRecord.getArrivalAddress();
|
||||
oldRecord.setRouteJson(TransportBusinessSupport.trimToNull(waybill.getRouteJson()));
|
||||
oldRecord.setDepartureAddress(TransportBusinessSupport.trimToNull(waybill.getDepartureAddress()));
|
||||
oldRecord.setArrivalAddress(TransportBusinessSupport.trimToNull(waybill.getArrivalAddress()));
|
||||
oldRecord.setDepartureName(TransportBusinessSupport.syncRegionName(
|
||||
oldRecord.getDepartureName(), previousDepartureAddress, oldRecord.getDepartureAddress()));
|
||||
oldRecord.setArrivalName(TransportBusinessSupport.syncRegionName(
|
||||
oldRecord.getArrivalName(), previousArrivalAddress, oldRecord.getArrivalAddress()));
|
||||
oldRecord.setTaskInfoJson(TransportBusinessSupport.trimToNull(waybill.getTaskInfoJson()));
|
||||
TransportBusinessSupport.validateLength(oldRecord.getRouteJson(), 8000, "路线信息不能超过8000字");
|
||||
TransportBusinessSupport.validateLength(oldRecord.getDepartureAddress(), 255, "发货地址不能超过255字");
|
||||
TransportBusinessSupport.validateLength(oldRecord.getArrivalAddress(), 255, "收货地址不能超过255字");
|
||||
TransportBusinessSupport.validateLength(oldRecord.getDepartureName(),
|
||||
TransportBusinessSupport.REGION_NAME_MAX_LENGTH, "发货地不能超过100字");
|
||||
TransportBusinessSupport.validateLength(oldRecord.getArrivalName(),
|
||||
TransportBusinessSupport.REGION_NAME_MAX_LENGTH, "收货地不能超过100字");
|
||||
TransportBusinessSupport.validateLength(oldRecord.getTaskInfoJson(), 8000, "任务信息不能超过8000字");
|
||||
return updateById(oldRecord);
|
||||
}
|
||||
|
||||
+10
-13
@@ -29,7 +29,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 可配载准入判据:公路 + 待执行/进行中 + 未关联其它配载单。
|
||||
* 可配载准入判据:公路 + 待执行 + 未关联其它配载单。
|
||||
* <p>
|
||||
* 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单,
|
||||
* 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。
|
||||
@@ -38,11 +38,8 @@ import java.util.Locale;
|
||||
*/
|
||||
public final class LoadableAdmissionSupport {
|
||||
|
||||
/** 可配载业务状态:待执行、进行中;已完成、已取消、草稿与调度中间态一律不可配载 */
|
||||
public static final List<String> LOADABLE_BUSINESS_STATUSES = List.of(
|
||||
WaybillProcessSupport.STATUS_PENDING,
|
||||
WaybillProcessSupport.STATUS_RUNNING
|
||||
);
|
||||
/** 可配载只放行待执行:已完成、已取消、草稿与调度中间态一律不可配载 */
|
||||
public static final String LOADABLE_BUSINESS_STATUS = WaybillProcessSupport.STATUS_PENDING;
|
||||
|
||||
/**
|
||||
* 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。
|
||||
@@ -59,7 +56,7 @@ public final class LoadableAdmissionSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 不可配载的原因,按判定顺序排列:公路 → 待执行/进行中 → 未关联其它配载单。
|
||||
* 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。
|
||||
* <p>
|
||||
* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。
|
||||
*/
|
||||
@@ -68,8 +65,8 @@ public final class LoadableAdmissionSupport {
|
||||
/** 运输方式不是公路 */
|
||||
NOT_ROAD,
|
||||
|
||||
/** 业务状态不是待执行或进行中 */
|
||||
NOT_LOADABLE_STATUS,
|
||||
/** 业务状态不是待执行 */
|
||||
NOT_PENDING,
|
||||
|
||||
/** 已关联其它配载单 */
|
||||
ON_OTHER_MANIFEST
|
||||
@@ -122,8 +119,8 @@ public final class LoadableAdmissionSupport {
|
||||
if (waybill == null || !isRoadTransport(waybill.getTransportType())) {
|
||||
return UnloadableReason.NOT_ROAD;
|
||||
}
|
||||
if (!LOADABLE_BUSINESS_STATUSES.contains(text(waybill.getBusinessStatus()))) {
|
||||
return UnloadableReason.NOT_LOADABLE_STATUS;
|
||||
if (!LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) {
|
||||
return UnloadableReason.NOT_PENDING;
|
||||
}
|
||||
String loadingNo = text(waybill.getLoadingNo());
|
||||
if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) {
|
||||
@@ -151,13 +148,13 @@ public final class LoadableAdmissionSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把整条可配载判据加到运单查询上:公路 + 待执行/进行中 + 未配载。
|
||||
* 把整条可配载判据加到运单查询上:公路 + 待执行 + 未配载。
|
||||
*
|
||||
* @param wrapper 运单列表查询条件
|
||||
*/
|
||||
public static void applyLoadableConditions(LambdaQueryWrapper<Waybill> wrapper) {
|
||||
applyRoadTransportCondition(wrapper);
|
||||
wrapper.in(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUSES);
|
||||
wrapper.eq(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUS);
|
||||
wrapper.and(nested -> nested.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, ""));
|
||||
}
|
||||
|
||||
|
||||
+71
@@ -37,6 +37,7 @@ import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@@ -50,8 +51,18 @@ public final class TransportBusinessSupport {
|
||||
public static final String DATA_SOURCE_MANUAL = "手工创建";
|
||||
public static final String DATA_SOURCE_PLAN_DISPATCH = "计划调度";
|
||||
public static final String DATA_SOURCE_EXTERNAL = "外部系统";
|
||||
/** 站点名(省+市+区县)列宽:blade_waybill.departure_name / arrival_name 为 varchar(100) */
|
||||
public static final int REGION_NAME_MAX_LENGTH = 100;
|
||||
|
||||
private static final Pattern PHONE_PATTERN = Pattern.compile("^(1\\d{10}|0\\d{2,3}-?\\d{7,8})$");
|
||||
/** 省级前缀:主体不含市/区/县,避免把「北京市朝阳区某省道」当省级 */
|
||||
private static final Pattern PROVINCE_PATTERN = Pattern.compile("^[^市区县]{2,10}?(?:省|特别行政区|自治区)");
|
||||
/** 市级:地级市名 2~4 字,「市场」的市不算(地址里常见「农贸市场」「超市」) */
|
||||
private static final Pattern CITY_PATTERN = Pattern.compile("^[^区县旗市]{2,4}?市(?!场)");
|
||||
/** 市级:自治州/地区/盟 的名字可以更长 */
|
||||
private static final Pattern PREFECTURE_PATTERN = Pattern.compile("^[^区县旗市]{2,10}?(?:自治州|地区|盟)");
|
||||
/** 区县:县级市不单独进站名,只取到地级市 */
|
||||
private static final Pattern DISTRICT_PATTERN = Pattern.compile("^[^市县]{2,10}?(?:区|县|旗)");
|
||||
private static final BigDecimal MIN_LONGITUDE = new BigDecimal("-180");
|
||||
private static final BigDecimal MAX_LONGITUDE = new BigDecimal("180");
|
||||
private static final BigDecimal MIN_LATITUDE = new BigDecimal("-90");
|
||||
@@ -426,4 +437,64 @@ public final class TransportBusinessSupport {
|
||||
return mileage == null ? null : mileage.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按地址推导站名:省 + 市 + 区/县/旗。
|
||||
* <p>
|
||||
* 库里的站名就是这个口径(如「内蒙古自治区鄂尔多斯市鄂托克旗」「北京市朝阳区」),
|
||||
* 只写地址不写站名会让司机端继续显示旧城市。地址里的「/」是分隔写法,推导时去掉;
|
||||
* 认不出城市的退回整段地址,不拼半截区县名。
|
||||
*/
|
||||
public static String deriveRegionName(String address) {
|
||||
String value = trimToNull(address);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String rest = value.replace("/", "");
|
||||
String province = matchPrefix(rest, PROVINCE_PATTERN);
|
||||
if (!province.isEmpty()) {
|
||||
rest = rest.substring(province.length());
|
||||
}
|
||||
String city = matchPrefix(rest, CITY_PATTERN);
|
||||
if (city.isEmpty()) {
|
||||
city = matchPrefix(rest, PREFECTURE_PATTERN);
|
||||
}
|
||||
if (city.isEmpty()) {
|
||||
return truncateRegionName(value);
|
||||
}
|
||||
StringBuilder name = new StringBuilder(province).append(city);
|
||||
String district = matchPrefix(rest.substring(city.length()), DISTRICT_PATTERN);
|
||||
if (!district.isEmpty()) {
|
||||
name.append(district);
|
||||
}
|
||||
return truncateRegionName(name.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址变了才按新地址重推站名,地址没动保留库里原有的站名。
|
||||
*
|
||||
* @param currentName 库里当前的站名
|
||||
* @param previousAddress 改动前的地址
|
||||
* @param nextAddress 改动后的地址
|
||||
* @return 新地址对应的站名,地址被清空时返回 null
|
||||
*/
|
||||
public static String syncRegionName(String currentName, String previousAddress, String nextAddress) {
|
||||
String next = trimToNull(nextAddress);
|
||||
if (next == null) {
|
||||
return null;
|
||||
}
|
||||
if (Objects.equals(trimToNull(previousAddress), next)) {
|
||||
return trimToNull(currentName);
|
||||
}
|
||||
return deriveRegionName(next);
|
||||
}
|
||||
|
||||
private static String matchPrefix(String text, Pattern pattern) {
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
return matcher.find() ? matcher.group() : "";
|
||||
}
|
||||
|
||||
private static String truncateRegionName(String value) {
|
||||
return value.length() > REGION_NAME_MAX_LENGTH ? value.substring(0, REGION_NAME_MAX_LENGTH) : value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -257,14 +257,14 @@ class DriverTripCardTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首页当前任务取运输中运次,最多一条")
|
||||
@DisplayName("首页当前任务取运输中与待回单确认运次,最多一条")
|
||||
void tripCurrentTaskTakesRunningTrip() {
|
||||
stubTripPage(1, loadingTrip());
|
||||
|
||||
DriverWaybillCardVO card = runAsDriver(() -> service.tripCurrentTask());
|
||||
|
||||
assertNotNull(card);
|
||||
assertEquals(1, capturedQuery().getStatus());
|
||||
assertEquals(List.of(1, 4), capturedQuery().getStatuses(), "只剩待回单确认的司机首页仍有当前任务");
|
||||
assertEquals(1, capturedQuery().getSize());
|
||||
|
||||
Page<TripVO> empty = new Page<>(1, 1, 0);
|
||||
|
||||
+23
@@ -313,6 +313,8 @@ class ExceptionDisposalRouteAdjustTest {
|
||||
|
||||
assertEquals(START, updated.getDepartureAddress());
|
||||
assertEquals(NEW_END, updated.getArrivalAddress());
|
||||
assertEquals("南宁市邕宁区", updated.getDepartureName(), "站名按新地址重推,不能停在旧城市");
|
||||
assertEquals("成都市新都区", updated.getArrivalName());
|
||||
Map<String, Object> taskInfo = JsonUtil.toMap(updated.getTaskInfoJson());
|
||||
Map<?, ?> latest = (Map<?, ?>) ((List<?>) taskInfo.get("routeChangeRecords")).get(0);
|
||||
assertEquals(routeContent(START, TRANSIT, NEW_END), latest.get("content"));
|
||||
@@ -320,6 +322,27 @@ class ExceptionDisposalRouteAdjustTest {
|
||||
verify(loadingManageMapper, never()).selectOne(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("只改到达地时发货地站名保留库里的原值,改了的到达地按新地址重推")
|
||||
void adjustRouteKeepsDepartureNameWhenAddressUnchanged() {
|
||||
stubPendingDisposal();
|
||||
Waybill waybill = waybill(null, "in_progress", segmentRouteJson());
|
||||
waybill.setDepartureAddress(START);
|
||||
waybill.setDepartureName("南宁市邕宁区");
|
||||
waybill.setArrivalAddress(END);
|
||||
waybill.setArrivalName("成都市双流区");
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill);
|
||||
|
||||
asScheduler(() -> service.adjustRoute(request(
|
||||
node("start", START, null),
|
||||
node("transit", TRANSIT, null),
|
||||
node("end", NEW_END, null))));
|
||||
|
||||
Waybill updated = updatedWaybill();
|
||||
assertEquals("南宁市邕宁区", updated.getDepartureName(), "地址没动,站名不得被改写");
|
||||
assertEquals("成都市新都区", updated.getArrivalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运单已关联配载单但配载单查不到时拒绝,不越权改运单")
|
||||
void adjustRouteRejectsWaybillBranchWhenLoadingNoPresent() {
|
||||
|
||||
+1
-1
@@ -369,7 +369,7 @@ class LoadingManageChildStatusWriteTest {
|
||||
waybill.setProjectId(projectId);
|
||||
waybill.setProcessJson(processJson);
|
||||
waybill.setDeptId(DEPT_ID_LONG);
|
||||
// 可配载放行公路 + 待执行/进行中 + 未配载,夹具默认待执行
|
||||
// 可配载只放行公路 + 待执行 + 未配载,夹具的票都是这种票
|
||||
waybill.setTransportType("公路运输");
|
||||
waybill.setBusinessStatus("pending");
|
||||
return waybill;
|
||||
|
||||
+2
-2
@@ -168,7 +168,7 @@ class ManageTripListDetailTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待处理列表:两档与「未拒单」交给同一次共享取数;待重新派单仍走子单被拒那条路")
|
||||
@DisplayName("待处理列表:三档(待接单/进行中/待回单确认)与「未拒单」交给同一次共享取数;待重新派单仍走子单被拒那条路")
|
||||
void pendingListUsesSharedQuery() {
|
||||
when(tripQueryService.page(any())).thenReturn(pageOf(0));
|
||||
when(waybillService.page(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
@@ -177,7 +177,7 @@ class ManageTripListDetailTest {
|
||||
|
||||
ArgumentCaptor<TripQueryVO> captor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService, times(1)).page(captor.capture());
|
||||
assertEquals(List.of(0, 1), captor.getValue().getStatuses(), "待处理只要待接单与进行中两档");
|
||||
assertEquals(List.of(0, 1, 4), captor.getValue().getStatuses(), "待处理含待接单、进行中、待回单确认三档");
|
||||
assertTrue(captor.getValue().isExcludeRejectedTrips(), "未拒单也交给共享取数");
|
||||
|
||||
service.pendingList(1, 10, null, true);
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.mapper.WaybillMapper;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 运单「变更运输路线」的站名同步测试。
|
||||
* <p>
|
||||
* 司机端与运单列表读的是 departure_name / arrival_name,PC 后台改路线只提交地址。
|
||||
* 只写地址不写站名,页面上就还是旧城市(YD-VF-ADMIN-R1 改成北京通州后仍显示福建省三明市)。
|
||||
* 站名一律由后端按新地址推导,且只在地址真的变了才重推——地址没动时不得把库里已有的站名写坏。
|
||||
*/
|
||||
class WaybillChangeRouteStationNameTest {
|
||||
|
||||
private static final Long WAYBILL_ID = 7301L;
|
||||
private static final Long DEPT_ID = 100L;
|
||||
private static final String OLD_START = "福建省三明市梅列区列东街道88号";
|
||||
private static final String OLD_END = "福建省三明市三元区江滨路9号";
|
||||
private static final String NEW_END = "北京市通州区西集镇辛集路";
|
||||
|
||||
private WaybillMapper waybillMapper;
|
||||
private WaybillServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
// updateById 判主键、读实体元数据需要 MyBatis-Plus 的缓存
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
waybillMapper = mock(WaybillMapper.class);
|
||||
service = new WaybillServiceImpl();
|
||||
inject("baseMapper", waybillMapper);
|
||||
when(waybillMapper.updateById(any(Waybill.class))).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("改到达地:地址写新值,到达站名按新地址重推,发货站名原样保留")
|
||||
void changeRouteDerivesArrivalName() {
|
||||
Waybill oldRecord = waybill();
|
||||
oldRecord.setDepartureAddress(OLD_START);
|
||||
oldRecord.setDepartureName("福建省三明市梅列区");
|
||||
oldRecord.setArrivalAddress(OLD_END);
|
||||
oldRecord.setArrivalName("福建省三明市三元区");
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(oldRecord);
|
||||
|
||||
asManager(() -> assertTrue(service.changeRoute(request(OLD_START, NEW_END))));
|
||||
|
||||
Waybill updated = updatedWaybill();
|
||||
assertEquals(NEW_END, updated.getArrivalAddress());
|
||||
assertEquals("北京市通州区", updated.getArrivalName(), "站名不能停在旧城市");
|
||||
assertEquals(OLD_START, updated.getDepartureAddress());
|
||||
assertEquals("福建省三明市梅列区", updated.getDepartureName(), "地址没动,站名不得被改写");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("只改路线节点与任务信息:地址未变,站名一个都不动")
|
||||
void changeRouteKeepsNamesWhenAddressesUnchanged() {
|
||||
Waybill oldRecord = waybill();
|
||||
oldRecord.setDepartureAddress(OLD_START);
|
||||
oldRecord.setDepartureName("福建省三明市梅列区");
|
||||
oldRecord.setArrivalAddress(OLD_END);
|
||||
oldRecord.setArrivalName("福建省三明市三元区");
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(oldRecord);
|
||||
|
||||
asManager(() -> assertTrue(service.changeRoute(request(OLD_START, OLD_END))));
|
||||
|
||||
Waybill updated = updatedWaybill();
|
||||
assertEquals("福建省三明市梅列区", updated.getDepartureName());
|
||||
assertEquals("福建省三明市三元区", updated.getArrivalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已配载的运单不允许在这里改路线,站名也不写")
|
||||
void changeRouteRejectsLoadedWaybill() {
|
||||
Waybill oldRecord = waybill();
|
||||
oldRecord.setLoadingNo("PZ-20261024-001");
|
||||
oldRecord.setDepartureAddress(OLD_START);
|
||||
oldRecord.setArrivalAddress(OLD_END);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(oldRecord);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> asManager(() -> service.changeRoute(request(OLD_START, NEW_END))));
|
||||
|
||||
assertEquals("运单已关联配载单,请在配载单中修改", exception.getMessage());
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private Waybill waybill() {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD20261024001");
|
||||
waybill.setBusinessStatus("running");
|
||||
waybill.setDeptId(DEPT_ID);
|
||||
waybill.setDeptName("测试组织");
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private Waybill request(String departureAddress, String arrivalAddress) {
|
||||
Waybill request = new Waybill();
|
||||
request.setId(WAYBILL_ID);
|
||||
request.setRouteJson("[{\"address\":\"" + departureAddress + "\"}]");
|
||||
request.setDepartureAddress(departureAddress);
|
||||
request.setArrivalAddress(arrivalAddress);
|
||||
return request;
|
||||
}
|
||||
|
||||
private Waybill updatedWaybill() {
|
||||
ArgumentCaptor<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
|
||||
verify(waybillMapper).updateById(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private void asManager(Runnable action) {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(String.valueOf(DEPT_ID));
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
+5
-3
@@ -68,7 +68,7 @@ class WaybillLoadableQueryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("开关开启:公路词表、待执行/进行中、未配载一次全部进 wrapper,接单状态不参与")
|
||||
@DisplayName("开关开启:公路词表、待执行、未配载一次全部进 wrapper,接单状态不参与")
|
||||
void loadableSwitchAppliesWholePredicate() {
|
||||
LambdaQueryWrapper<Waybill> wrapper = capturedQuery(loadableQuery());
|
||||
String sql = String.valueOf(wrapper.getTargetSql());
|
||||
@@ -79,8 +79,10 @@ class WaybillLoadableQueryTest {
|
||||
assertTrue(params.contains("%" + keyword + "%"),
|
||||
"公路词表要整份进条件,只写 road 会漏掉中文取值的运单:" + params);
|
||||
}
|
||||
assertTrue(sql.contains("business_status") && params.contains("pending") && params.contains("running"),
|
||||
"放行待执行与进行中:" + sql + " " + params);
|
||||
assertTrue(sql.contains("business_status =") && params.contains("pending"),
|
||||
"只放行待执行:" + sql + " " + params);
|
||||
assertFalse(params.contains("running"),
|
||||
"进行中一律不可配载,不能沿用「待执行与进行中都可配载」的旧口径:" + params);
|
||||
assertTrue(sql.contains("loading_no IS NULL") && sql.contains("loading_no ="),
|
||||
"未配载要同时覆盖 NULL 与空串:" + sql);
|
||||
assertFalse(sql.contains("driver_accept_status"),
|
||||
|
||||
+8
-9
@@ -50,12 +50,10 @@ class LoadableAdmissionSupportTest {
|
||||
// ------------------------------------------------------------ 单张运单准入
|
||||
|
||||
@Test
|
||||
@DisplayName("公路 + 待执行/进行中 + 未配载才可配载")
|
||||
@DisplayName("公路 + 待执行 + 未配载才可配载")
|
||||
void loadableOnlyWhenAllConditionsHold() {
|
||||
assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"), null));
|
||||
assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null), null));
|
||||
assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null),
|
||||
"进行中也可配载");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,10 +64,12 @@ class LoadableAdmissionSupportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("非公路、非待执行/进行中、已关联配载单各自单独即可拒绝")
|
||||
@DisplayName("非公路、非待执行、已关联配载单各自单独即可拒绝")
|
||||
void eachConditionRejectsOnItsOwn() {
|
||||
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending"), null),
|
||||
"非公路不可配载");
|
||||
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null),
|
||||
"进行中不可配载:不需要接单的项目保存后即为进行中,那是过程配置的产物,不代表可配载");
|
||||
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "draft", null, null), null));
|
||||
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "completed", null, null), null));
|
||||
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null), null));
|
||||
@@ -79,19 +79,18 @@ class LoadableAdmissionSupportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("不可配载原因按 公路 → 待执行/进行中 → 未关联其它配载单 取第一条")
|
||||
@DisplayName("不可配载原因按 公路 → 待执行 → 未关联其它配载单 取第一条")
|
||||
void unloadableReasonFollowsPredicateOrder() {
|
||||
assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_ROAD,
|
||||
LoadableAdmissionSupport.unloadableReason(
|
||||
waybill("railway", "completed", "PZ20260101-002", null), "PZ20260101-001"));
|
||||
assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_LOADABLE_STATUS,
|
||||
waybill("railway", "running", "PZ20260101-002", null), "PZ20260101-001"));
|
||||
assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_PENDING,
|
||||
LoadableAdmissionSupport.unloadableReason(
|
||||
waybill("road", "completed", "PZ20260101-002", null), "PZ20260101-001"));
|
||||
waybill("road", "running", "PZ20260101-002", null), "PZ20260101-001"));
|
||||
assertEquals(LoadableAdmissionSupport.UnloadableReason.ON_OTHER_MANIFEST,
|
||||
LoadableAdmissionSupport.unloadableReason(
|
||||
waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001"));
|
||||
assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "pending", null, null), null));
|
||||
assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "running", null, null), null));
|
||||
assertNull(LoadableAdmissionSupport.unloadableReason(
|
||||
waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001"));
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* 站名推导测试。
|
||||
* <p>
|
||||
* 司机端与列表展示的是运单的 departure_name / arrival_name,不是地址;改路线只写地址、
|
||||
* 不写站名,页面上就还是旧城市。库里的站名口径是「省 + 市 + 区/县/旗」,
|
||||
* 例如 内蒙古自治区鄂尔多斯市鄂托克旗、北京市朝阳区、广西壮族自治区北海市合浦县。
|
||||
*/
|
||||
class TransportBusinessSupportRegionNameTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("省+市+区:只取行政区划,街道门牌不进站名")
|
||||
void provinceCityDistrict() {
|
||||
assertEquals("北京市朝阳区",
|
||||
TransportBusinessSupport.deriveRegionName("北京市朝阳区十八里店乡周庄村1号院"));
|
||||
assertEquals("福建省三明市梅列区",
|
||||
TransportBusinessSupport.deriveRegionName("福建省三明市梅列区列东街道88号"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("自治区+市+旗、市+县:旗与县同样算到区县一级")
|
||||
void autonomousRegionCityBanner() {
|
||||
assertEquals("内蒙古自治区鄂尔多斯市鄂托克旗",
|
||||
TransportBusinessSupport.deriveRegionName("内蒙古自治区鄂尔多斯市鄂托克旗棋盘井镇运煤专线"));
|
||||
assertEquals("广西壮族自治区北海市合浦县",
|
||||
TransportBusinessSupport.deriveRegionName("广西壮族自治区北海市合浦县廉州镇还珠大道"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("直辖市与自治州:省这一级可以没有")
|
||||
void municipalityAndAutonomousPrefecture() {
|
||||
assertEquals("天津市北辰区",
|
||||
TransportBusinessSupport.deriveRegionName("天津市北辰区青光镇津围公路"));
|
||||
assertEquals("湖北省恩施土家族苗族自治州",
|
||||
TransportBusinessSupport.deriveRegionName("湖北省恩施土家族苗族自治州恩施市舞阳大道"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("斜杠分隔的地址写法:拼站名时去掉斜杠")
|
||||
void slashSeparatedAddress() {
|
||||
assertEquals("南宁市邕宁区",
|
||||
TransportBusinessSupport.deriveRegionName("南宁市/邕宁区/那赖二路神网数码物流园"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("只到市一级:不硬凑区县")
|
||||
void cityOnly() {
|
||||
assertEquals("贵州省贵阳市", TransportBusinessSupport.deriveRegionName("贵州省贵阳市新店镇农贸市场"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("「市场」「超市」里的市不是城市名,县级市也只取到地级市")
|
||||
void marketNameIsNotACity() {
|
||||
assertEquals("贵州省贵阳市",
|
||||
TransportBusinessSupport.deriveRegionName("贵州省贵阳市城东农贸市场3号门"));
|
||||
assertEquals("江苏省苏州市",
|
||||
TransportBusinessSupport.deriveRegionName("江苏省苏州市昆山市玉山镇紫竹路"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("地址里的「省道」不当省级:仍按市、区推导")
|
||||
void provinceRoadIsNotProvince() {
|
||||
assertEquals("北京市朝阳区", TransportBusinessSupport.deriveRegionName("北京市朝阳区某省道88号"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("认不出城市时退回整段地址,不拼半截区县名")
|
||||
void fallsBackToFullAddress() {
|
||||
assertEquals("新店镇农贸市场3号门", TransportBusinessSupport.deriveRegionName("新店镇农贸市场3号门"));
|
||||
assertEquals("南宁吴圩国际机场货运站", TransportBusinessSupport.deriveRegionName("南宁吴圩国际机场货运站"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空地址没有站名")
|
||||
void blankAddress() {
|
||||
assertNull(TransportBusinessSupport.deriveRegionName(null));
|
||||
assertNull(TransportBusinessSupport.deriveRegionName(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("站名超出列宽时按 100 字截断")
|
||||
void truncatesToColumnWidth() {
|
||||
String address = "新店镇" + "甲乙丙丁".repeat(30);
|
||||
|
||||
String name = TransportBusinessSupport.deriveRegionName(address);
|
||||
|
||||
assertEquals(TransportBusinessSupport.REGION_NAME_MAX_LENGTH, name.length());
|
||||
assertEquals(address.substring(0, TransportBusinessSupport.REGION_NAME_MAX_LENGTH), name);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("地址没变时保留库里原有的站名")
|
||||
void keepsNameWhenAddressUnchanged() {
|
||||
assertEquals("福建省三明市", TransportBusinessSupport.syncRegionName(
|
||||
"福建省三明市", "福建省三明市梅列区列东街道88号", "福建省三明市梅列区列东街道88号"));
|
||||
assertNull(TransportBusinessSupport.syncRegionName(
|
||||
null, "福建省三明市梅列区列东街道88号", "福建省三明市梅列区列东街道88号"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("地址变了按新地址重推站名,与旧站名无关")
|
||||
void derivesNameWhenAddressChanged() {
|
||||
assertEquals("北京市通州区", TransportBusinessSupport.syncRegionName(
|
||||
"福建省三明市", "福建省三明市梅列区列东街道88号", "北京市通州区西集镇辛集路"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("地址被清空时站名一起清空")
|
||||
void clearsNameWhenAddressCleared() {
|
||||
assertNull(TransportBusinessSupport.syncRegionName(
|
||||
"福建省三明市", "福建省三明市梅列区列东街道88号", null));
|
||||
assertNull(TransportBusinessSupport.syncRegionName(
|
||||
"福建省三明市", "福建省三明市梅列区列东街道88号", " "));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user