⚡ 小程序司机端运单改为按手机号匹配,取消车牌过滤

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-08 19:37:26 +08:00
co-authored by Cursor
parent 5496f487c7
commit d5c6d10352
3 changed files with 71 additions and 78 deletions
@@ -64,14 +64,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/current-task") @GetMapping("/current-task")
@ApiOperationSupport(order = 1) @ApiOperationSupport(order = 1)
@Operation(summary = "首页:当前运输中任务", description = "当前司机绑定车牌下 businessStatus=running 的最新一条运单") @Operation(summary = "首页:当前运输中任务", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=running 的最新一条运单")
public R<DriverWaybillCardVO> currentTask() { public R<DriverWaybillCardVO> currentTask() {
return R.data(driverWaybillService.currentTask()); return R.data(driverWaybillService.currentTask());
} }
@GetMapping("/pending-preview") @GetMapping("/pending-preview")
@ApiOperationSupport(order = 2) @ApiOperationSupport(order = 2)
@Operation(summary = "首页:待接运单预览", description = "当前司机绑定车牌下 businessStatus=pending 的预览列表与总数") @Operation(summary = "首页:待接运单预览", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=pending 的预览列表与总数")
public R<DriverWaybillPreviewVO> pendingPreview( public R<DriverWaybillPreviewVO> pendingPreview(
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) { @Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
return R.data(driverWaybillService.pendingPreview(size)); return R.data(driverWaybillService.pendingPreview(size));
@@ -79,14 +79,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/counts") @GetMapping("/counts")
@ApiOperationSupport(order = 3) @ApiOperationSupport(order = 3)
@Operation(summary = "运单 Tab 统计", description = "仅统计当前司机绑定车牌对应的运单:全部 / 待接单 / 进行中 / 已完成") @Operation(summary = "运单 Tab 统计", description = "仅统计当前用户手机号匹配运单 driverPhone 的运单:全部 / 待接单 / 进行中 / 已完成")
public R<DriverWaybillTabCountsVO> counts() { public R<DriverWaybillTabCountsVO> counts() {
return R.data(driverWaybillService.tabCounts()); return R.data(driverWaybillService.tabCounts());
} }
@GetMapping("/page") @GetMapping("/page")
@ApiOperationSupport(order = 4) @ApiOperationSupport(order = 4)
@Operation(summary = "运单分页列表", description = "仅返回当前司机绑定车牌(driving_vehicle)匹配运单 vehicleNo/trailerVehicleNo 的数据;status:空=全部,0待接单,1运输中,2已完成") @Operation(summary = "运单分页列表", description = "仅返回运单 driverPhone 等于当前用户手机号的数据;status:空=全部,0待接单,1运输中,2已完成")
public R<IPage<DriverWaybillCardVO>> page( public R<IPage<DriverWaybillCardVO>> page(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current, @Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size, @Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@@ -129,12 +129,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override @Override
public DriverWaybillCardVO currentTask() { public DriverWaybillCardVO currentTask() {
List<String> plates = currentBoundPlates(); String phone = currentUserPhone();
if (plates.isEmpty()) { if (Func.isEmpty(phone)) {
return null; return null;
} }
// 进行中:running,或无需确认接单但仍为 pending 的历史数据 // 进行中:running,或无需确认接单但仍为 pending 的历史数据
List<Waybill> candidates = waybillService.list(scopedQuery(plates) List<Waybill> candidates = waybillService.list(scopedQuery(phone)
.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING)) .in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING))
.orderByDesc(Waybill::getUpdateTime) .orderByDesc(Waybill::getUpdateTime)
.last("LIMIT 20")); .last("LIMIT 20"));
@@ -150,12 +150,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override @Override
public DriverWaybillPreviewVO pendingPreview(Integer size) { public DriverWaybillPreviewVO pendingPreview(Integer size) {
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO(); DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
List<String> plates = currentBoundPlates(); String phone = currentUserPhone();
if (plates.isEmpty()) { if (Func.isEmpty(phone)) {
return preview; return preview;
} }
int limit = normalizePreviewSize(size); int limit = normalizePreviewSize(size);
List<Waybill> pendingList = waybillService.list(scopedQuery(plates) List<Waybill> pendingList = waybillService.list(scopedQuery(phone)
.in(Waybill::getBusinessStatus, PENDING_STATUSES) .in(Waybill::getBusinessStatus, PENDING_STATUSES)
.orderByDesc(Waybill::getCreateTime)); .orderByDesc(Waybill::getCreateTime));
List<Waybill> needAccept = pendingList.stream() List<Waybill> needAccept = pendingList.stream()
@@ -170,11 +170,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override @Override
public DriverWaybillTabCountsVO tabCounts() { public DriverWaybillTabCountsVO tabCounts() {
DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO(); DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO();
List<String> plates = currentBoundPlates(); String phone = currentUserPhone();
if (plates.isEmpty()) { if (Func.isEmpty(phone)) {
return vo; return vo;
} }
List<Waybill> waybills = waybillService.list(scopedQuery(plates) List<Waybill> waybills = waybillService.list(scopedQuery(phone)
.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES)); .in(Waybill::getBusinessStatus, TAB_ALL_STATUSES));
long pending = 0; long pending = 0;
long doing = 0; long doing = 0;
@@ -206,12 +206,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE); int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
Page<DriverWaybillCardVO> empty = new Page<>(pageNo, pageSize); Page<DriverWaybillCardVO> empty = new Page<>(pageNo, pageSize);
List<String> plates = currentBoundPlates(); String phone = currentUserPhone();
if (plates.isEmpty()) { if (Func.isEmpty(phone)) {
return empty; return empty;
} }
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(plates); LambdaQueryWrapper<Waybill> wrapper = scopedQuery(phone);
// 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页 // 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页
applyStatusFilterForQuery(wrapper, status); applyStatusFilterForQuery(wrapper, status);
applyKeyword(wrapper, keyword); applyKeyword(wrapper, keyword);
@@ -234,8 +234,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override @Override
public DriverWaybillCardVO detail(Long id) { public DriverWaybillCardVO detail(Long id) {
Driver driver = requireCurrentDriver(); requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(id);
return toCard(normalizeAcceptStatus(waybill), true); return toCard(normalizeAcceptStatus(waybill), true);
} }
@@ -258,7 +258,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
throw new ServiceException("运单ID不能为空"); throw new ServiceException("运单ID不能为空");
} }
Driver driver = requireCurrentDriver(); Driver driver = requireCurrentDriver();
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(dto.getWaybillId());
waybill = normalizeAcceptStatus(waybill); waybill = normalizeAcceptStatus(waybill);
String processJson = resolveProcessJson(waybill); String processJson = resolveProcessJson(waybill);
@@ -307,7 +307,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
throw new ServiceException("节点编码不能为空"); throw new ServiceException("节点编码不能为空");
} }
Driver driver = requireCurrentDriver(); Driver driver = requireCurrentDriver();
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(dto.getWaybillId());
waybill = normalizeAcceptStatus(waybill); waybill = normalizeAcceptStatus(waybill);
String processJson = resolveProcessJson(waybill); String processJson = resolveProcessJson(waybill);
@@ -370,7 +370,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
// 认证门槛:司机本人须已认证,且承运车辆须已认证。 // 认证门槛:司机本人须已认证,且承运车辆须已认证。
// 判据是各自持有的生效快照指针,故「已认证的车重新提交认证」不会拦下接单。 // 判据是各自持有的生效快照指针,故「已认证的车重新提交认证」不会拦下接单。
assertCertified(driver); assertCertified(driver);
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(id);
waybillService.syncDriverAcceptState(waybill); waybillService.syncDriverAcceptState(waybill);
assertAcceptable(waybill); assertAcceptable(waybill);
Date now = new Date(); Date now = new Date();
@@ -420,8 +420,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean reject(Long id, String reason) { public boolean reject(Long id, String reason) {
Driver driver = requireCurrentDriver(); requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(id);
waybillService.syncDriverAcceptState(waybill); waybillService.syncDriverAcceptState(waybill);
assertRejectable(waybill); assertRejectable(waybill);
String rejectReason = Func.isEmpty(reason) ? null : reason.trim(); String rejectReason = Func.isEmpty(reason) ? null : reason.trim();
@@ -444,8 +444,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
if (id == null) { if (id == null) {
throw new ServiceException("运单ID不能为空"); throw new ServiceException("运单ID不能为空");
} }
Driver driver = requireCurrentDriver(); requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(id);
waybill = normalizeAcceptStatus(waybill); waybill = normalizeAcceptStatus(waybill);
if (!STATUS_RUNNING.equals(waybill.getBusinessStatus()) if (!STATUS_RUNNING.equals(waybill.getBusinessStatus())
&& !STATUS_PENDING.equals(waybill.getBusinessStatus()) && !STATUS_PENDING.equals(waybill.getBusinessStatus())
@@ -483,14 +483,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
} }
} }
private Waybill loadDriverWaybill(Long id, List<String> plates) { private Waybill loadDriverWaybill(Long id) {
if (id == null) { if (id == null) {
throw new ServiceException("运单ID不能为空"); throw new ServiceException("运单ID不能为空");
} }
if (plates.isEmpty()) { String phone = currentUserPhone();
throw new ServiceException("当前司机未绑定车辆"); if (Func.isEmpty(phone)) {
throw new ServiceException("当前用户手机号为空,无法查找运单");
} }
Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1")); Waybill waybill = waybillService.getOne(scopedQuery(phone).eq(Waybill::getId, id).last("LIMIT 1"));
if (waybill == null) { if (waybill == null) {
throw new ServiceException("运单不存在或无权操作"); throw new ServiceException("运单不存在或无权操作");
} }
@@ -547,24 +548,13 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
} }
/** /**
* 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。 * 司机可见运单范围:运单司机手机号(driver_phone)等于当前用户手机号。
* 绑定来源:blade_transport_driver.driving_vehicle
*/ */
private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates) { private LambdaQueryWrapper<Waybill> scopedQuery(String phone) {
return Wrappers.<Waybill>lambdaQuery().and(w -> { String normalizedPhone = Func.toStr(phone, "").trim();
w.in(Waybill::getVehicleNo, plates) return Wrappers.<Waybill>lambdaQuery().and(w -> w
.or().in(Waybill::getTrailerVehicleNo, plates); .eq(Waybill::getDriverPhone, normalizedPhone)
for (String plate : plates) { .or().apply("REPLACE(IFNULL(driver_phone,''),' ','') = {0}", normalizedPhone));
w.or().apply(
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}",
plate
);
w.or().apply(
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(trailer_vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}",
plate
);
}
});
} }
private void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) { private void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
@@ -580,13 +570,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
.or().like(Waybill::getVehicleNo, kw)); .or().like(Waybill::getVehicleNo, kw));
} }
/** 当前登录司机绑定的规范化车牌列表;无司机或无绑定车牌则空 */ /**
private List<String> currentBoundPlates() { * 当前用户手机号:优先司机档案 mobile,其次 JWT 账号(11 位手机号)。
*/
private String currentUserPhone() {
Driver driver = currentDriver(); Driver driver = currentDriver();
if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) { if (driver != null && Func.isNotEmpty(driver.getMobile())) {
return List.of(); return driver.getMobile().trim();
} }
return splitPlates(driver.getDrivingVehicle()); String account = AuthUtil.getUserAccount();
if (Func.isNotEmpty(account) && account.matches("^1\\d{10}$")) {
return account.trim();
}
return null;
} }
/** /**
@@ -809,9 +805,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override @Override
public void assertCurrentDriverOwnsWaybill(Long waybillId) { public void assertCurrentDriverOwnsWaybill(Long waybillId) {
// 归属口径与司机打卡、司机接单一致:都按司机档案自述绑定的车牌判定 // 归属口径与司机打卡、司机接单一致:按当前用户手机号匹配运单 driverPhone
Driver driver = requireCurrentDriver(); requireCurrentDriver();
loadDriverWaybill(waybillId, currentBoundPlates(driver)); loadDriverWaybill(waybillId);
} }
/** /**
@@ -23,9 +23,6 @@ import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.CertificationGate; import org.springblade.transport.support.CertificationGate;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -40,15 +37,14 @@ import static org.mockito.Mockito.when;
/** /**
* 司机对运单的归属校验(异常上报等按运单操作的前置条件)。 * 司机对运单的归属校验(异常上报等按运单操作的前置条件)。
* <p> * <p>
* 口径只有一条:司机档案里自述绑定的车牌要与运单的主车或挂车车牌匹配。 * 口径:当前用户手机号匹配运单 {@code driverPhone}。
* 它不是「运单上的承运司机」——那两件事互不推导;也不是「谁提交了这份运单材料」。
* 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。 * 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。
*/ */
class DriverWaybillOwnershipTest { class DriverWaybillOwnershipTest {
private static final Long WAYBILL_ID = 5301L; private static final Long WAYBILL_ID = 5301L;
private static final Long USER_ID = 7001L; private static final Long USER_ID = 7001L;
private static final String PLATE = "桂AAW3255"; private static final String PHONE = "13800138000";
private IDriverService driverService; private IDriverService driverService;
private IWaybillService waybillService; private IWaybillService waybillService;
@@ -90,20 +86,21 @@ class DriverWaybillOwnershipTest {
} }
@Test @Test
@DisplayName("司机档案里没有绑定车辆时被拒") @DisplayName("司机档案与登录态都没有手机号时被拒")
void withoutBoundPlateIsRejected() { void withoutPhoneIsRejected() {
when(driverService.getOne(any())).thenReturn(driver(null)); Driver driver = driver(null);
when(driverService.getOne(any())).thenReturn(driver);
ServiceException exception = assertThrows(ServiceException.class, ServiceException exception = assertThrows(ServiceException.class,
() -> runOwnershipCheck(WAYBILL_ID)); () -> runOwnershipCheck(WAYBILL_ID));
assertEquals("当前司机未绑定车辆", exception.getMessage()); assertEquals("当前用户手机号为空,无法查找运单", exception.getMessage());
} }
@Test @Test
@DisplayName("运单不在该司机绑定的车牌下时被拒") @DisplayName("运单司机手机号与当前用户不一致时被拒")
void waybillOfAnotherPlateIsRejected() { void waybillOfAnotherPhoneIsRejected() {
when(driverService.getOne(any())).thenReturn(driver(PLATE)); when(driverService.getOne(any())).thenReturn(driver(PHONE));
when(waybillService.getOne(any())).thenReturn(null); when(waybillService.getOne(any())).thenReturn(null);
ServiceException exception = assertThrows(ServiceException.class, ServiceException exception = assertThrows(ServiceException.class,
@@ -113,9 +110,9 @@ class DriverWaybillOwnershipTest {
} }
@Test @Test
@DisplayName("本车运单放行,且查询确实按司机绑定的车牌收敛") @DisplayName("本人手机号运单放行,且查询确实按司机手机号收敛")
void ownWaybillPasses() { void ownWaybillPasses() {
when(driverService.getOne(any())).thenReturn(driver(PLATE)); when(driverService.getOne(any())).thenReturn(driver(PHONE));
when(waybillService.getOne(any())).thenReturn(waybill()); when(waybillService.getOne(any())).thenReturn(waybill());
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID)); assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
@@ -123,18 +120,17 @@ class DriverWaybillOwnershipTest {
ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(waybillService).getOne(captor.capture()); verify(waybillService).getOne(captor.capture());
LambdaQueryWrapper<Waybill> wrapper = captor.getValue(); LambdaQueryWrapper<Waybill> wrapper = captor.getValue();
assertTrue(wrapper.getTargetSql().contains("vehicle_no"), assertTrue(wrapper.getTargetSql().contains("driver_phone"),
"归属查询必须落在运单车牌上:" + wrapper.getTargetSql()); "归属查询必须落在运单司机手机号上:" + wrapper.getTargetSql());
assertTrue(wrapper.getParamNameValuePairs().values().stream() assertTrue(wrapper.getParamNameValuePairs().values().stream()
.anyMatch(value -> PLATE.equals(value) .anyMatch(PHONE::equals),
|| (value instanceof Collection<?> values && values.contains(PLATE))), "归属查询必须带上当前用户手机号:" + wrapper.getParamNameValuePairs());
"归属查询必须带上司机绑定的车牌:" + wrapper.getParamNameValuePairs());
} }
@Test @Test
@DisplayName("运单ID为空时被拒") @DisplayName("运单ID为空时被拒")
void withoutWaybillIdIsRejected() { void withoutWaybillIdIsRejected() {
when(driverService.getOne(any())).thenReturn(driver(PLATE)); when(driverService.getOne(any())).thenReturn(driver(PHONE));
ServiceException exception = assertThrows(ServiceException.class, ServiceException exception = assertThrows(ServiceException.class,
() -> runOwnershipCheck(null)); () -> runOwnershipCheck(null));
@@ -147,16 +143,17 @@ class DriverWaybillOwnershipTest {
private void runOwnershipCheck(Long waybillId) { private void runOwnershipCheck(Long waybillId) {
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) { try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID); authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
authUtil.when(AuthUtil::getUserAccount).thenReturn(PHONE);
service.assertCurrentDriverOwnsWaybill(waybillId); service.assertCurrentDriverOwnsWaybill(waybillId);
} }
} }
private Driver driver(String drivingVehicle) { private Driver driver(String mobile) {
Driver driver = new Driver(); Driver driver = new Driver();
driver.setId(9001L); driver.setId(9001L);
driver.setUserId(USER_ID); driver.setUserId(USER_ID);
driver.setDriverName("韦司机"); driver.setDriverName("韦司机");
driver.setDrivingVehicle(drivingVehicle); driver.setMobile(mobile);
return driver; return driver;
} }
@@ -164,7 +161,7 @@ class DriverWaybillOwnershipTest {
Waybill waybill = new Waybill(); Waybill waybill = new Waybill();
waybill.setId(WAYBILL_ID); waybill.setId(WAYBILL_ID);
waybill.setWaybillNo("YD202610070001"); waybill.setWaybillNo("YD202610070001");
waybill.setVehicleNo(PLATE); waybill.setDriverPhone(PHONE);
waybill.setIsDeleted(0); waybill.setIsDeleted(0);
return waybill; return waybill;
} }