🐛 改路线时按新地址重推运单站名

- TransportBusinessSupport 新增 deriveRegionName / syncRegionName:按「省+市+区县旗」推导站名,
  与库里存量口径一致,县级市只取到地级市,认不出行政区划退回整段地址并截断 100 字
- WaybillServiceImpl.changeRoute(PC 后台改路线)与
  ExceptionDisposalServiceImpl.applyWaybillRoute(小程序「同意并修改路线」)写地址时同步写站名;
  地址没变保留库里原站名,避免清洗旧的手填站名
- 此前两处都只写 departure_address / arrival_address,站名一直是派单时的旧值,
  司机端列表与详情标题改完仍是旧城市
- 补 TransportBusinessSupportRegionNameTest、WaybillChangeRouteStationNameTest 与异常处置站名断言
This commit is contained in:
2026-10-27 15:44:17 +08:00
parent dca59720bf
commit c82657ca56
6 changed files with 397 additions and 0 deletions
@@ -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());
@@ -1478,13 +1478,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);
}
@@ -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 int DATA_SCOPE_ALL = 2;
private static final BigDecimal MIN_LONGITUDE = new BigDecimal("-180");
@@ -291,4 +302,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;
}
}