♻️ 车牌软关联逻辑抽为 VehiclePlateSupport 并补测试

司机 driving_vehicle 与车辆表 plate_no 之间没有外键,只有文本关联,且库中存在
间隔符/横线/大小写差异,一个字段还可能并列多辆车。认证提交的越权校验
(只能操作本人绑定车辆)依赖这套匹配:漏判会让司机改不了自己的车,
误判会让司机改到别人的车,故先把规则集中并锁住行为。

- 新增 VehiclePlateSupport:splitPlates / normalize / isBound;
  isBound 对空值一律判不绑定(不能因为双方都为空就放行,那等于越权)。
- DriverAppServiceImpl 原先自己实现了一份 splitPlates/normalizePlate,
  改为委托同一个实现,避免两处口径分叉。
- 6 个用例覆盖分隔符变体、去重、规范化、脏数据与绑定判定。

可离线执行:mvn -o test -pl blade-service/blade-transport -am
This commit is contained in:
2026-10-02 16:08:47 +08:00
parent 9f3914463e
commit 8a2d83b3cd
3 changed files with 206 additions and 29 deletions
@@ -37,16 +37,13 @@ import org.springblade.transport.pojo.vo.DriverVO;
import org.springblade.transport.service.IDriverAppService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.transport.support.VehiclePlateSupport;
import org.springblade.transport.wrapper.DriverWrapper;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -72,7 +69,7 @@ public class DriverAppServiceImpl implements IDriverAppService {
if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) {
return List.of();
}
List<String> plates = splitPlates(driver.getDrivingVehicle());
List<String> plates = VehiclePlateSupport.splitPlates(driver.getDrivingVehicle());
if (plates.isEmpty()) {
return List.of();
}
@@ -119,30 +116,6 @@ public class DriverAppServiceImpl implements IDriverAppService {
return driver;
}
private List<String> splitPlates(String drivingVehicle) {
String normalized = drivingVehicle.replace(",", ",").replace("、", ",").replace(";", ",")
.replace(";", ",").replace("/", ",").replace("|", ",");
Set<String> plates = new LinkedHashSet<>();
for (String part : Func.toStrList(",", normalized)) {
if (Func.isEmpty(part)) {
continue;
}
String plate = normalizePlate(part);
if (Func.isNotEmpty(plate)) {
plates.add(plate);
}
}
return new ArrayList<>(plates);
}
/** 车牌规范化:去空格/横线/间隔符并转大写,便于与车辆表关联 */
private String normalizePlate(String plateNo) {
if (Func.isEmpty(plateNo)) {
return "";
}
return plateNo.trim().replaceAll("[\\s\\-·•..]", "").toUpperCase(Locale.ROOT);
}
private DriverVehicleCardVO toCard(TransportVehicle vehicle) {
DriverVehicleCardVO card = new DriverVehicleCardVO();
card.setId(vehicle.getId());
@@ -0,0 +1,113 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support;
import org.springblade.core.tool.utils.Func;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* 司机档案与车辆表之间的车牌软关联支持类
* <p>
* 司机「驾驶车辆」与车辆表「车牌号」之间**没有外键**,只有文本关联,
* 且库中实际存在写法差异:间隔符(·、•、.)、横线、空格、大小写不一致,
* 以及一个字段里用逗号/顿号/分号/斜杠/竖线并列多辆车。
* 认证提交时的「只能操作本人绑定车辆」校验依赖本类,因此匹配规则集中在此处维护,
* 避免各业务各自实现一套导致口径分叉。
*
* @author Chill
*/
public final class VehiclePlateSupport {
/**
* 并列车牌的常见分隔符(中英文顿号、逗号、分号、斜杠、竖线)
*/
private static final String SEPARATOR_PATTERN = "[,,、;;/|]";
/**
* 车牌中需要剔除的排版字符(各类空白、横线与间隔符)
*/
private static final String NOISE_PATTERN = "[\\s\\-·•..]";
private VehiclePlateSupport() {
}
/**
* 把「驾驶车辆」字段拆成规范化后的车牌列表(已去重、保持原顺序)
*
* @param drivingVehicle 司机档案里的驾驶车辆字段,可能并列多辆
* @return 规范化车牌列表;空值返回空列表
*/
public static List<String> splitPlates(String drivingVehicle) {
if (Func.isEmpty(drivingVehicle)) {
return new ArrayList<>();
}
String normalized = drivingVehicle.replaceAll(SEPARATOR_PATTERN, ",");
Set<String> plates = new LinkedHashSet<>();
for (String part : Func.toStrList(",", normalized)) {
String plate = normalize(part);
if (Func.isNotEmpty(plate)) {
plates.add(plate);
}
}
return new ArrayList<>(plates);
}
/**
* 车牌规范化:去掉排版字符并转大写,使不同写法能比对
*
* @param plateNo 原始车牌
* @return 规范化结果;空值返回空串
*/
public static String normalize(String plateNo) {
if (Func.isEmpty(plateNo)) {
return "";
}
return plateNo.trim().replaceAll(NOISE_PATTERN, "").toUpperCase(Locale.ROOT);
}
/**
* 判定某车牌是否属于该司机绑定的车辆
* <p>
* 空值一律判为「不绑定」——不能因为双方都为空就放行,那等于越权。
*
* @param drivingVehicle 司机档案里的驾驶车辆字段
* @param plateNo 待判定的车牌
* @return 是否绑定
*/
public static boolean isBound(String drivingVehicle, String plateNo) {
String target = normalize(plateNo);
if (Func.isEmpty(target)) {
return false;
}
return splitPlates(drivingVehicle).contains(target);
}
}
@@ -0,0 +1,91 @@
package org.springblade.transport.support;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 车牌匹配逻辑测试。
* <p>
* 司机档案的 {@code driving_vehicle} 与车辆表的 {@code plate_no} 是**文本软关联**,
* 无外键,且库里已知存在间隔符/横线/全角等写法差异(例如「沪AE23411」在车辆表中不存在,
* 而「桂A·32523」这类带间隔符的写法要靠规范化才能匹配上)。
* 认证提交的越权校验依赖这套匹配,匹配漏判会让司机操作不了自己的车,
* 误判则会让司机改到别人的车,故用测试锁住行为。
*/
class VehiclePlateSupportTest {
@Test
@DisplayName("单个车牌:原样返回规范化结果")
void singlePlate() {
assertEquals(List.of("桂A32523"), VehiclePlateSupport.splitPlates("桂A32523"));
}
@Test
@DisplayName("多个车牌:中英文逗号、顿号、分号、斜杠、竖线都算分隔符")
void multipleSeparators() {
assertEquals(
List.of("桂A32523", "桂B11111", "桂C22222"),
VehiclePlateSupport.splitPlates("桂A32523,桂B11111、桂C22222"));
assertEquals(
List.of("桂A32523", "桂B11111"),
VehiclePlateSupport.splitPlates("桂A32523;桂B11111"));
assertEquals(
List.of("桂A32523", "桂B11111"),
VehiclePlateSupport.splitPlates("桂A32523/桂B11111"));
assertEquals(
List.of("桂A32523", "桂B11111"),
VehiclePlateSupport.splitPlates("桂A32523|桂B11111"));
}
@Test
@DisplayName("去重:同一车牌写两次只保留一条")
void dedupe() {
assertEquals(List.of("桂A32523"), VehiclePlateSupport.splitPlates("桂A32523,桂A32523"));
}
@Test
@DisplayName("规范化:去空格/横线/间隔符并转大写")
void normalize() {
assertEquals("桂A32523", VehiclePlateSupport.normalize(" 桂a32523 "));
assertEquals("桂A32523", VehiclePlateSupport.normalize("桂A-32523"));
assertEquals("桂A32523", VehiclePlateSupport.normalize("桂A·32523"));
assertEquals("桂A32523", VehiclePlateSupport.normalize("桂A•32523"));
assertEquals("桂A32523", VehiclePlateSupport.normalize("桂A.32523"));
assertEquals("桂A32523", VehiclePlateSupport.normalize("桂A 32523"));
}
@Test
@DisplayName("空值与脏数据:不抛异常,返回空串或空列表")
void emptyAndDirty() {
assertEquals("", VehiclePlateSupport.normalize(null));
assertEquals("", VehiclePlateSupport.normalize(" "));
assertTrue(VehiclePlateSupport.splitPlates(null).isEmpty());
assertTrue(VehiclePlateSupport.splitPlates("").isEmpty());
assertTrue(VehiclePlateSupport.splitPlates(",,、;;").isEmpty());
}
@Test
@DisplayName("绑定判定:写法差异不影响匹配(越权校验的关键)")
void isBound() {
// 司机档案里带间隔符,车辆表里是干净写法
assertTrue(VehiclePlateSupport.isBound("桂A·32523", "桂A32523"));
// 大小写差异
assertTrue(VehiclePlateSupport.isBound("桂a32523", "桂A32523"));
// 多车:命中其中一辆即可
assertTrue(VehiclePlateSupport.isBound("桂A32523,桂B11111", "桂B11111"));
// 未绑定
assertFalse(VehiclePlateSupport.isBound("桂A32523", "桂B11111"));
// 空值一律不绑定(不能因为双方都为空就放行——那等于越权)
assertFalse(VehiclePlateSupport.isBound(null, "桂A32523"));
assertFalse(VehiclePlateSupport.isBound("桂A32523", null));
assertFalse(VehiclePlateSupport.isBound(null, null));
assertFalse(VehiclePlateSupport.isBound("", ""));
}
}