♻️ 运次收口判据由司机手机号改为司机档案 ID

- TripQueryVO 的收口字段由 driverPhone 换成 driverId,删除按手机号构造收口条件的入口,
  让编译器暴露全部调用点;forAdmin() 与出参 driverPhone 快照不变
- 运次可见范围两段同时切:已配载段按配载单 lm.driver_id 子查询,未配载段按运单 driver_id
- 详情复核改为比较档案 ID(ownedBy)、删除 samePhone;收口缺省(空/0/负数)仍返回空集
- 司机端六个运次入口改用当前司机档案 ID;删除 currentUserPhone/isMobileLike 手机号兜底
- 测试夹具改为同时铺设 driverId 与派单快照号,新增改号仍可见、同号换人、管理端、缺省收口四组用例
This commit is contained in:
2026-10-27 21:07:07 +08:00
parent b10e4f33b4
commit 01b4dd0b5b
7 changed files with 220 additions and 121 deletions
@@ -34,8 +34,11 @@ import java.util.Set;
/**
* 运次查询条件
* <p>
* 司机端用 {@link #forDriver(String)} 按司机手机号收口;管理端用 {@link #forAdmin()} 不按司机收口。
* 司机端用 {@link #forDriver(Long)} 按司机档案 ID 收口;管理端用 {@link #forAdmin()} 不按司机收口。
* 两者都不给时收口为空集——收口一旦缺省就等于把别人的运次发出去,故缺省不放行。
* <p>
* 判据是司机档案 ID 而不是手机号:手机号在配载单/运单上只是派单时的快照,司机换号后并不跟随,
* 按号收口会让改过号的司机看不到自己手上的运次,号码被回收给别人时还会反向串号。
*/
@Data
@Schema(description = "运次查询条件")
@@ -47,8 +50,8 @@ public class TripQueryVO implements Serializable {
@Schema(description = "运次主键(详情按它取单个运次)")
private String tripId;
@Schema(description = "司机手机号")
private String driverPhone;
@Schema(description = "司机档案 ID(司机端收口判据,空或非正数一律不放行)")
private Long driverId;
@Schema(description = "是否不按司机收口(管理端)")
private boolean allDrivers;
@@ -92,9 +95,9 @@ public class TripQueryVO implements Serializable {
@Schema(description = "每页条数(按运次,不是运单)")
private Integer size;
public static TripQueryVO forDriver(String driverPhone) {
public static TripQueryVO forDriver(Long driverId) {
TripQueryVO query = new TripQueryVO();
query.setDriverPhone(driverPhone);
query.setDriverId(driverId);
return query;
}
@@ -33,10 +33,8 @@ import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -277,11 +275,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillCardVO tripCurrentTask() {
String phone = currentUserPhone();
if (Func.isEmpty(phone)) {
Driver driver = currentDriver();
if (driver == null) {
return null;
}
TripQueryVO query = TripQueryVO.forDriver(phone);
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
// 待确认(待回单确认)的趟车司机仍需回到卡片上继续操作,与进行中并列作为当前任务
query.setStatuses(List.of(TRIP_STATUS_RUNNING, TRIP_STATUS_PENDING_RETURN_CONFIRM));
query.setCurrent(1);
@@ -293,11 +291,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillPreviewVO tripPendingPreview(Integer size) {
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
String phone = currentUserPhone();
if (Func.isEmpty(phone)) {
Driver driver = currentDriver();
if (driver == null) {
return preview;
}
TripQueryVO query = TripQueryVO.forDriver(phone);
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
query.setStatus(TRIP_STATUS_PENDING);
query.setCurrent(1);
query.setSize(normalizePreviewSize(size));
@@ -312,11 +310,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillTabCountsVO tripTabCounts() {
DriverWaybillTabCountsVO counts = new DriverWaybillTabCountsVO();
String phone = currentUserPhone();
if (Func.isEmpty(phone)) {
Driver driver = currentDriver();
if (driver == null) {
return counts;
}
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forDriver(phone));
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forDriver(driver.getId()));
counts.setPending(tripCounts.getPending());
counts.setDoing(tripCounts.getDoing());
counts.setConfirm(tripCounts.getConfirm());
@@ -327,11 +325,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public IPage<DriverWaybillCardVO> tripPage(Integer current, Integer size, Integer status, String keyword) {
String phone = currentUserPhone();
if (Func.isEmpty(phone)) {
Driver driver = currentDriver();
if (driver == null) {
return new Page<>(normalizePageNo(current), normalizePageSize(size));
}
TripQueryVO query = TripQueryVO.forDriver(phone);
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
query.setStatus(status);
query.setKeyword(keyword);
query.setCurrent(current);
@@ -346,8 +344,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillCardVO tripDetail(String tripId) {
requireCurrentDriver();
TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
Driver driver = requireCurrentDriver();
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
query.setTripId(tripId);
return toTripCard(tripQueryService.detail(query), true);
}
@@ -740,7 +738,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
* 子单集合是司机可见的那一批:已拒单的子单不在其中,否则接单会把本人拒掉的单重新拉回来。
*/
private List<TripVO.TripWaybillVO> tripChildren(String tripId) {
TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
TripQueryVO query = TripQueryVO.forDriver(requireCurrentDriver().getId());
query.setTripId(tripId);
TripVO trip = tripQueryService.detail(query);
List<TripVO.TripWaybillVO> children = trip.getWaybills() == null ? List.of() : trip.getWaybills();
@@ -922,28 +920,6 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
.or().like(Waybill::getVehicleNo, kw));
}
/**
* 当前用户手机号:司机档案 mobile 优先,其次小程序用户账号。
* <p>
* 派单时运单 driver_phone 取自司机档案 mobile,故档案有手机号就以它为准。
*/
private String currentUserPhone() {
Driver driver = currentDriver();
if (driver != null && isMobileLike(driver.getMobile())) {
return driver.getMobile().trim();
}
Long userId = AuthUtil.getUserId();
if (userId == null || userId <= 0) {
return null;
}
R<MpUser> result = mpUserClient.userById(userId);
if (result != null && R.isSuccess(result) && result.getData() != null
&& isMobileLike(result.getData().getAccount())) {
return result.getData().getAccount().trim();
}
return null;
}
/**
* 当前登录司机:只按登录用户 ID 查司机档案,查不到即「无归属」。
* <p>
@@ -960,10 +936,6 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
.last("LIMIT 1"));
}
private boolean isMobileLike(String value) {
return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
}
private List<String> splitPlates(String drivingVehicle) {
return VehiclePlateSupport.splitPlates(drivingVehicle);
}
@@ -389,22 +389,24 @@ public class TripQueryServiceImpl implements ITripQueryService {
}
/**
* 运次可见范围:配载段按配载单的司机手机号收口,未配载段按运单的司机手机号收口。
* 运次可见范围:配载段按配载单的司机档案 ID 收口,未配载段按运单的司机档案 ID 收口。
* <p>
* 手机号允许带空格,与运单列表的收口判据一致。
* 判据是司机档案 ID 而不是手机号:司机换号后档案 ID 不变,而派单时落在配载单/运单上的手机号
* 是快照,按号收口会让改过号的司机看不到自己手上的运次;号码被回收给别人时又会反向串号。
* 两段必须同时切:只切一段,另一段就会把不属于该司机的运次放进来。
* 缺省收口(空、0、负数)不放行,由 {@link #scoped} 在外层挡掉。
*/
private void applyScope(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
if (condition.isAllDrivers()) {
return;
}
String phone = Func.toStr(condition.getDriverPhone(), "").trim();
Long driverId = condition.getDriverId();
wrapper.and(scope -> scope
.apply("(IFNULL(loading_no, '') <> '' AND loading_no IN (SELECT lm.loading_no FROM blade_loading_manage lm"
+ " WHERE lm.is_deleted = 0 AND lm.driver_phone = {0}))", phone)
+ " WHERE lm.is_deleted = 0 AND lm.driver_id = {0}))", driverId)
.or(lone -> lone
.apply("IFNULL(loading_no, '') = ''")
.and(row -> row.eq(Waybill::getDriverPhone, phone)
.or().apply("REPLACE(IFNULL(driver_phone, ''), ' ', '') = {0}", phone))));
.and(row -> row.eq(Waybill::getDriverId, driverId))));
}
/**
@@ -786,7 +788,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
}
/**
* 按运次主键取配载单:主键命中配载单即配载运次,再按司机收口
* 按运次主键取配载单:主键命中配载单即配载运次,再按配载单的司机档案 ID 收口
*/
private LoadingManage visibleLoading(Long id, TripQueryVO condition) {
LoadingManage loading = loadingManageService.getById(id);
@@ -796,7 +798,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
if (condition.isAllDrivers()) {
return loading;
}
return samePhone(loading.getDriverPhone(), condition.getDriverPhone()) ? loading : null;
return ownedBy(loading.getDriverId(), condition.getDriverId()) ? loading : null;
}
/**
@@ -813,19 +815,23 @@ public class TripQueryServiceImpl implements ITripQueryService {
if (condition.isAllDrivers()) {
return waybill;
}
return samePhone(waybill.getDriverPhone(), condition.getDriverPhone()) ? waybill : null;
return ownedBy(waybill.getDriverId(), condition.getDriverId()) ? waybill : null;
}
/** 收口条件是否成立:管理端不收口;司机端必须给出有效的档案 ID,缺省一律拒绝 */
private boolean scoped(TripQueryVO condition) {
return condition.isAllDrivers() || Func.isNotEmpty(Func.toStr(condition.getDriverPhone(), "").trim());
Long driverId = condition.getDriverId();
return condition.isAllDrivers() || (driverId != null && driverId > 0);
}
private boolean samePhone(String left, String right) {
String expected = Func.toStr(right, "").trim().replace(" ", "");
if (Func.isEmpty(expected)) {
return false;
}
return expected.equals(Func.toStr(left, "").trim().replace(" ", ""));
/**
* 归属判据:行的司机档案 ID 与收口条件相等。
* <p>
* 条件侧缺省(空、0、负数)一律不可见——没有有效档案 ID 就等于没有收口条件,
* 此时任何行都不能算作「本人」,否则业务主键未回填的历史数据会把运次漏给所有人。
*/
private boolean ownedBy(Long ownerId, Long expectedId) {
return expectedId != null && expectedId > 0 && expectedId.equals(ownerId);
}
private TripQueryVO condition(TripQueryVO query) {
@@ -835,7 +841,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
/** 去掉 Tab 条件、其余照搬:统计四档时不按某一档收窄 */
private TripQueryVO withoutTab(TripQueryVO source) {
TripQueryVO copy = new TripQueryVO();
copy.setDriverPhone(source.getDriverPhone());
copy.setDriverId(source.getDriverId());
copy.setAllDrivers(source.isAllDrivers());
copy.setDeptIds(source.getDeptIds());
copy.setProjectIds(source.getProjectIds());
@@ -124,8 +124,8 @@ class DriverTripAcceptRejectTest {
"两张子运单与接单司机一起下发:" + capturedUpdateParams());
assertTrue(capturedWhereSql().contains("(id IN ("), capturedWhereSql());
assertEquals(TRIP_ID, capturedQuery().getTripId());
assertEquals(CURRENT_PHONE, capturedQuery().getDriverPhone(),
"运次侧本票不动:仍按档案当前手机号收口(票 04 统一改成档案 ID)");
assertEquals(DRIVER_ID, capturedQuery().getDriverId(),
"运次查询按司机档案 ID 收口:手机号换了也认得出本人");
}
@Test
@@ -125,7 +125,7 @@ class DriverTripCardTest {
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(1, 10, null, null));
given = capturedQuery();
assertEquals(PHONE, given.getDriverPhone());
assertEquals(DRIVER_ID, given.getDriverId(), "运次列表按司机档案 ID 收口");
assertEquals(1, given.getCurrent());
assertEquals(10, given.getSize(), "一页 10 条即 10 个运次");
assertNull(given.getStatus());
@@ -334,10 +334,9 @@ class DriverTripCardTest {
}
@Test
@DisplayName("收口缺失时不放行:空页、零统计、当前任务为空,且不查运次")
void missingPhoneIsRefused() {
@DisplayName("收口缺失时不放行:没有司机档案就空页、零统计、当前任务为空,且不查运次")
void missingDriverProfileIsRefused() {
when(driverService.getOne(any())).thenReturn(null);
when(mpUserClient.userById(any())).thenReturn(null);
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(null, null, null, null));
DriverWaybillPreviewVO preview = runAsDriver(() -> service.tripPendingPreview(null));
@@ -50,7 +50,10 @@ import static org.mockito.Mockito.when;
*/
class TripQueryServiceImplTest {
/** 派单时的司机手机号快照:只用于展示,归属判据是下面的档案 ID */
private static final String PHONE = "17587421208";
private static final Long DRIVER_ID = 9001L;
private static final Long OTHER_DRIVER_ID = 9002L;
private static final String LOADING_NO = "PZ-20261017-001";
private static final Long LOADING_ID = 2111350784070275123L;
private static final Long WAYBILL_ID = 2110690671695380513L;
@@ -85,7 +88,7 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("段一在库层只做收口与行级筛选,分组与分档不在 SQL 里")
void rowsAreFetchedWithoutDatabaseGrouping() {
TripQueryVO query = TripQueryVO.forDriver(PHONE);
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setStatus(1);
query.setCurrent(1);
query.setSize(10);
@@ -98,9 +101,9 @@ class TripQueryServiceImplTest {
assertFalse(sql.contains("LIMIT"), "分页在服务层,不在 wrapper 里拼:" + sql);
assertFalse(sql.contains("MAX(create_time)"), "排序键在服务层算:" + sql);
assertTrue(sql.contains("blade_loading_manage"), "配载段按配载单的司机收口:" + sql);
assertTrue(sql.contains("lm.driver_phone = ?"), "司机手机号要作为参数下发:" + sql);
assertTrue(sql.contains("REPLACE(IFNULL(driver_phone, ''), ' ', '') = ?"),
"未配载段按运单司机收口,且容忍手机号带空格:" + sql);
assertTrue(sql.contains("lm.driver_id = ?"), "配载段按配载单的司机档案 ID 收口:" + sql);
assertTrue(sql.contains("driver_id = ?"), "未配载段按运单的司机档案 ID 收口:" + sql);
assertFalse(sql.contains("driver_phone"), "手机号不再是归属判据:" + sql);
assertTrue(sql.contains("business_status IN"), "草稿子单不成运次:" + sql);
assertTrue(sql.contains("driver_accept_status"), "被该司机拒单的子单不进运次:" + sql);
}
@@ -108,7 +111,7 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("段一只 select 用得上的列,大 JSON 列不拖回来")
void onlyNeededColumnsAreSelected() {
service.page(TripQueryVO.forDriver(PHONE));
service.page(TripQueryVO.forDriver(DRIVER_ID));
String select = String.valueOf(capturedRowQuery().getSqlSelect());
assertTrue(select.contains("process_json"), "分档要按过程配置推导,必须取:" + select);
@@ -124,7 +127,7 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("组织收口给了就落在库层运单行上,分档与分页都由缩过的行集合算")
void deptScopeIsAppliedOnRows() {
TripQueryVO query = TripQueryVO.forDriver(PHONE);
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setDeptIds(List.of(1123598813738675202L, 1123598813738675201L));
service.page(query);
@@ -134,7 +137,7 @@ class TripQueryServiceImplTest {
Set<Object> values = new HashSet<>(capturedRowQuery().getParamNameValuePairs().values());
assertTrue(values.contains(1123598813738675202L) && values.contains(1123598813738675201L),
"组织值按参数下发:" + values);
assertTrue(capturedRowSql().contains("driver_phone"), "组织收口不顶替司机收口:两种收口同时生效");
assertTrue(capturedRowSql().contains("driver_id"), "组织收口不顶替司机收口:两种收口同时生效");
}
@Test
@@ -152,7 +155,7 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("组织收口缺省时不下发组织条件:超管与司机端都按不传处理")
void deptScopeIsOptional() {
service.page(TripQueryVO.forDriver(PHONE));
service.page(TripQueryVO.forDriver(DRIVER_ID));
assertFalse(capturedRowSql().contains("dept_id"), "不给组织集合即不过滤:" + capturedRowSql());
}
@@ -165,7 +168,7 @@ class TripQueryServiceImplTest {
waybill(2L, "YD-2", null, "completed", "accepted", at(2000L), ACCEPT_NO),
waybill(3L, "YD-3", null, "running", "accepted", at(1000L), ACCEPT_NO));
TripQueryVO doing = TripQueryVO.forDriver(PHONE);
TripQueryVO doing = TripQueryVO.forDriver(DRIVER_ID);
doing.setStatus(1);
IPage<TripVO> doingPage = service.page(doing);
@@ -175,11 +178,11 @@ class TripQueryServiceImplTest {
assertTrue(doingPage.getRecords().stream().allMatch(trip -> Integer.valueOf(1).equals(trip.getStatus())),
"每张卡的 status 都要等于这一档");
TripQueryVO done = TripQueryVO.forDriver(PHONE);
TripQueryVO done = TripQueryVO.forDriver(DRIVER_ID);
done.setStatus(2);
assertEquals(List.of("2"), service.page(done).getRecords().stream().map(TripVO::getId).toList());
TripQueryVO all = TripQueryVO.forDriver(PHONE);
TripQueryVO all = TripQueryVO.forDriver(DRIVER_ID);
all.setStatus(null);
assertEquals(3, service.page(all).getTotal(), "不传 Tab 为全部(不含已取消)");
}
@@ -192,12 +195,12 @@ class TripQueryServiceImplTest {
waybill(2L, "YD-2", null, "running", null, at(2000L), ACCEPT_YES),
waybill(1L, "YD-1", null, "pending", "pending", at(1000L), ACCEPT_NO));
TripQueryVO pending = TripQueryVO.forDriver(PHONE);
TripQueryVO pending = TripQueryVO.forDriver(DRIVER_ID);
pending.setStatus(0);
assertEquals(List.of("2"), service.page(pending).getRecords().stream().map(TripVO::getId).toList(),
"库值 running 但要接单未接 → 落待接单档");
TripQueryVO doing = TripQueryVO.forDriver(PHONE);
TripQueryVO doing = TripQueryVO.forDriver(DRIVER_ID);
doing.setStatus(1);
assertEquals(List.of("1"), service.page(doing).getRecords().stream().map(TripVO::getId).toList(),
"库值 pending 但不需要接单 → 落进行中档");
@@ -212,8 +215,8 @@ class TripQueryServiceImplTest {
List.of(
waybill(1L, "YD-1", LOADING_NO, "running", null, at(3000L), ACCEPT_YES),
waybill(2L, "YD-2", LOADING_NO, "running", null, at(2000L), ACCEPT_YES)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
TripQueryVO query = TripQueryVO.forDriver(PHONE);
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setKeyword("PZ-20261017");
TripVO trip = service.page(query).getRecords().get(0);
@@ -232,7 +235,7 @@ class TripQueryServiceImplTest {
void unloadedWaybillIsItsOwnTrip() {
stubRows(waybill(WAYBILL_ID, "YD-20260920-0003", null, "running", null, at(1000L), ACCEPT_NO));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(PHONE));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(DRIVER_ID));
assertEquals(1, page.getTotal());
assertEquals(1, page.getRecords().size());
@@ -256,7 +259,7 @@ class TripQueryServiceImplTest {
row.setMasterNo("DL-20261001-0001");
stubRows(row);
TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO trip = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals(Boolean.TRUE, trip.getMultimodal(), "挂总单即含多式联运");
assertEquals("common", trip.getTransportOrgType(), "运输类型是组织形态维,与多式联运各算一个值");
@@ -270,9 +273,9 @@ class TripQueryServiceImplTest {
stubRows(
onMasterOrder,
waybill(2092186187470467073L, "YD-2", LOADING_NO, "running", null, at(1000L), ACCEPT_NO));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO trip = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals("load", trip.getTransportOrgType());
assertEquals(Boolean.TRUE, trip.getMultimodal(), "两个事实叠加,不做互斥单选");
@@ -283,7 +286,7 @@ class TripQueryServiceImplTest {
void unloadedTripStateComesFromFetchedWaybill() {
stubRows(waybill(WAYBILL_ID, "YD-20260920-0003", null, "pending", "pending", at(1000L), ACCEPT_YES));
TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO trip = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals(Boolean.TRUE, trip.getRequireAccept());
assertEquals("pending", trip.getAcceptStatus());
@@ -300,7 +303,7 @@ class TripQueryServiceImplTest {
row.setEstimatedEndTime(LocalDate.of(2026, 8, 24));
stubRows(row);
TripVO listed = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO listed = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals(LocalDate.of(2026, 8, 7), listed.getEstimatedStartDate(), "列表卡片与详情同源,不能只在列表补");
assertEquals(LocalDate.of(2026, 8, 24), listed.getEstimatedEndDate());
@@ -319,7 +322,7 @@ class TripQueryServiceImplTest {
void loadingMissingIsDropped() {
stubRows(waybill(2092186187239780353L, "YD-20260825-0006", LOADING_NO, "running", null, at(1000L), ACCEPT_YES));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(PHONE));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(DRIVER_ID));
assertTrue(page.getRecords().isEmpty(), "配载单取不到就不给这一趟车");
assertEquals(1, page.getTotal(), "总数按运次算,卡片少一张时总数仍在");
@@ -335,9 +338,9 @@ class TripQueryServiceImplTest {
waybill(2092186187470467073L, "YD-20260825-0007", LOADING_NO, "running", null, earlier, ACCEPT_NO),
waybill(2092186187571130369L, "YD-20260825-0008", LOADING_NO, "running", null, earlier, ACCEPT_NO),
waybill(2102780956493299713L, "YD-20260923-0008", LOADING_NO, "running", null, latest, ACCEPT_NO));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO trip = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals(String.valueOf(LOADING_ID), trip.getId());
assertEquals(LOADING_NO, trip.getLoadingNo());
@@ -358,9 +361,9 @@ class TripQueryServiceImplTest {
stubRows(
waybill(2092186187239780353L, "YD-1", LOADING_NO, "pending", "pending", at(2000L), ACCEPT_YES),
waybill(2092186187470467073L, "YD-2", LOADING_NO, "running", null, at(1000L), ACCEPT_NO));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO trip = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals(Boolean.TRUE, trip.getRequireAccept(), "跨项目按 OR 合并:一张要接单整趟车都要接单");
assertEquals("pending", trip.getAcceptStatus());
@@ -372,8 +375,8 @@ class TripQueryServiceImplTest {
void repeatedReadsDoNotDriftAndNeverWrite() {
stubOneLoadedTrip();
TripVO first = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO second = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0);
TripVO first = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
TripVO second = service.page(TripQueryVO.forDriver(DRIVER_ID)).getRecords().get(0);
assertEquals(first.getStatus(), second.getStatus());
assertEquals(first.getAcceptStatus(), second.getAcceptStatus());
@@ -412,19 +415,19 @@ class TripQueryServiceImplTest {
waybill(2L, "YD-2", null, "running", null, at(2000L), ACCEPT_YES),
waybill(1L, "YD-1", null, "running", null, at(1000L), ACCEPT_YES));
TripQueryVO firstPage = TripQueryVO.forDriver(PHONE);
TripQueryVO firstPage = TripQueryVO.forDriver(DRIVER_ID);
firstPage.setCurrent(1);
firstPage.setSize(2);
assertEquals(3, service.page(firstPage).getTotal(), "总数是过滤后的运次数,不是运单数");
assertEquals(List.of("3", "2"), service.page(firstPage).getRecords().stream().map(TripVO::getId).toList());
TripQueryVO secondPage = TripQueryVO.forDriver(PHONE);
TripQueryVO secondPage = TripQueryVO.forDriver(DRIVER_ID);
secondPage.setCurrent(2);
secondPage.setSize(2);
assertEquals(List.of("1"), service.page(secondPage).getRecords().stream().map(TripVO::getId).toList(),
"第 2 页接着第 1 页,不重复不遗漏");
TripQueryVO bySeven = TripQueryVO.forDriver(PHONE);
TripQueryVO bySeven = TripQueryVO.forDriver(DRIVER_ID);
bySeven.setCurrent(1);
bySeven.setSize(7);
assertEquals(3, service.page(bySeven).getTotal(), "换每页条数总数不变");
@@ -440,7 +443,7 @@ class TripQueryServiceImplTest {
waybill(2L, "YD-2", null, "running", null, at(2000L), ACCEPT_NO),
waybill(1L, "YD-1", null, "completed", "accepted", at(1000L), ACCEPT_NO));
TripTabCountsVO counts = service.tabCounts(TripQueryVO.forDriver(PHONE));
TripTabCountsVO counts = service.tabCounts(TripQueryVO.forDriver(DRIVER_ID));
assertEquals(1, counts.getPending());
assertEquals(2, counts.getDoing());
@@ -476,7 +479,7 @@ class TripQueryServiceImplTest {
stubRows(
waybill(2L, "YD-2", LOADING_NO, "completed", "accepted", at(2000L), ACCEPT_NO),
waybill(1L, "YD-1", LOADING_NO, "running", null, at(1000L), ACCEPT_NO));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripQueryVO query = TripQueryVO.forAdmin();
query.setException("exception");
query.setExceptionWaybillIds(java.util.Set.of(2L));
@@ -572,7 +575,7 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("详情与列表同口径:子单快照没有接单节点、项目生效配置要求接单时,详情也算待接单")
void detailUsesLiveProcessConfig() {
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(PHONE));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(DRIVER_ID));
Waybill child = waybill(2092186187239780353L, "YD-20260915-0001", LOADING_NO, "running", "pending",
at(2000L), ACCEPT_NO);
child.setProjectId(7001L);
@@ -590,7 +593,7 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("详情仍是只读取数:生效配置只写回内存里的行,不落库")
void detailStaysReadOnly() {
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(PHONE));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(DRIVER_ID));
Waybill child = waybill(2092186187239780353L, "YD-20260915-0001", LOADING_NO, "running", "pending",
at(2000L), ACCEPT_NO);
child.setProjectId(7001L);
@@ -611,7 +614,7 @@ class TripQueryServiceImplTest {
when(loadingManageService.getById(WAYBILL_ID)).thenReturn(null);
when(waybillService.getById(WAYBILL_ID))
.thenReturn(waybill(WAYBILL_ID, "YD-20260920-0003", null, "running", null, at(1000L), ACCEPT_YES));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(PHONE));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(DRIVER_ID));
stubChildren();
TripVO lone = service.detail(tripQuery(String.valueOf(WAYBILL_ID)));
@@ -627,13 +630,13 @@ class TripQueryServiceImplTest {
@Test
@DisplayName("详情的收口与列表一致:别人的运次、子单全被拒单的配载运次都不可达")
void detailRefusesForeignTrips() {
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading("13800000001"));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(OTHER_DRIVER_ID));
when(waybillService.getById(WAYBILL_ID))
.thenReturn(waybill(WAYBILL_ID, "YD-20260920-0003", null, "running", null, at(1000L), ACCEPT_YES));
assertThrows(ServiceException.class, () -> service.detail(tripQuery(String.valueOf(LOADING_ID))));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(PHONE));
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(DRIVER_ID));
when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of());
assertThrows(ServiceException.class, () -> service.detail(tripQuery(String.valueOf(LOADING_ID))));
@@ -647,14 +650,119 @@ class TripQueryServiceImplTest {
void rejectedChildrenLeaveTheTrip() {
// 段一的可见口径已经把被拒子单挡在库层,取回来是空则这趟车不存在
stubRows();
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(PHONE));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(DRIVER_ID));
assertTrue(page.getRecords().isEmpty(), "可见子单为空时不给没有子单的运次");
assertEquals(0, page.getTotal());
}
// ------------------------------------------------------------ 收口判据:司机档案 ID
@Test
@DisplayName("司机改号后运次仍可见:判据是档案 ID,派单快照号不再参与")
void driverScopeSurvivesPhoneChange() {
Waybill lone = waybill(7001L, "YD-LONE-0001", null, "running", null, at(1000L), ACCEPT_YES);
stubRows(
waybill(2092186187239780353L, "YD-20260825-0006", LOADING_NO, "running", null, at(2000L), ACCEPT_YES),
lone);
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(DRIVER_ID));
when(waybillService.getById(7001L)).thenReturn(lone);
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setCurrent(1);
query.setSize(10);
IPage<TripVO> page = service.page(query);
assertEquals(2, page.getTotal(), "档案换号不影响归属:已配载与未配载两段都要按档案 ID 命中");
assertEquals(Set.of(LOADING_NO, "YD-LONE-0001"),
page.getRecords().stream().map(TripVO::getTripNo).collect(java.util.stream.Collectors.toSet()));
assertTrue(page.getRecords().stream().allMatch(trip -> PHONE.equals(trip.getDriverPhone())),
"响应里的司机手机号仍是派单快照,改号后不变");
assertEquals(LOADING_NO, service.detail(tripQuery(String.valueOf(LOADING_ID))).getTripNo());
assertEquals(PHONE, service.detail(tripQuery(String.valueOf(LOADING_ID))).getDriverPhone(),
"已配载运次详情仍回派单快照号");
assertEquals(PHONE, service.detail(tripQuery(String.valueOf(7001L))).getDriverPhone(),
"未配载运次详情仍回派单快照号");
}
@Test
@DisplayName("同号换人不放行:手机号相同、档案 ID 不同的运次仍然看不到")
void samePhoneDoesNotGrantAccess() {
// 派单快照号一模一样,档案 ID 却是别人
Waybill foreignLone = waybill(7002L, "YD-LONE-0002", null, "running", null, at(1000L), ACCEPT_YES);
foreignLone.setDriverId(OTHER_DRIVER_ID);
when(waybillService.getById(7002L)).thenReturn(foreignLone);
assertThrows(ServiceException.class, () -> service.detail(tripQuery(String.valueOf(7002L))),
"快照号相同不代表同一个司机");
// 已配载段同样只认配载单上的档案 ID
when(loadingManageService.getById(LOADING_ID)).thenReturn(loading(OTHER_DRIVER_ID));
assertThrows(ServiceException.class, () -> service.detail(tripQuery(String.valueOf(LOADING_ID))),
"配载单归别人时详情同样拒绝");
}
@Test
@DisplayName("两段各按自己的 driver_id 判:已配载看配载单,未配载看运单")
void driverScopeReadsDriverIdOnBothSegments() {
Waybill loadedRow = waybill(2092186187239780353L, "YD-20260825-0006", LOADING_NO, "running", null,
at(2000L), ACCEPT_YES);
// 运单行归别人、配载单归本人:已配载段只认配载单
loadedRow.setDriverId(OTHER_DRIVER_ID);
stubRows(loadedRow);
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setCurrent(1);
query.setSize(10);
assertEquals(1, service.page(query).getTotal(), "已配载段按配载单的 driver_id 收口,不看运单行的");
Waybill lone = waybill(7003L, "YD-LONE-0003", null, "running", null, at(1000L), ACCEPT_YES);
// 运单归本人、手机号却完全对不上:未配载段只认运单的 driver_id
lone.setDriverPhone("13800000003");
stubRows(lone);
IPage<TripVO> page = service.page(query);
assertEquals(1, page.getTotal(), "未配载段按运单的 driver_id 收口,不看手机号");
assertEquals("13800000003", page.getRecords().get(0).getDriverPhone(), "展示的仍是运单上的派单快照号");
}
@Test
@DisplayName("管理端不按司机收口:带上任何 driverId 都照样全出")
void adminScopeIgnoresDriverId() {
stubRows(
waybill(2L, "YD-2", null, "running", null, at(2000L), ACCEPT_YES),
waybill(1L, "YD-1", null, "running", null, at(1000L), ACCEPT_YES));
for (Long driverId : java.util.Arrays.asList(null, 0L, -1L, OTHER_DRIVER_ID)) {
TripQueryVO admin = TripQueryVO.forAdmin();
admin.setDriverId(driverId);
assertEquals(2, service.page(admin).getTotal(), "管理端收口与司机无关:" + driverId);
}
}
@Test
@DisplayName("收口缺省(空、0、负数)一律空集:不放行也不抛异常")
void missingDriverIdIsRefused() {
stubRows(waybill(1L, "YD-1", null, "running", null, at(1000L), ACCEPT_YES));
for (Long driverId : java.util.Arrays.asList(null, 0L, -1L, -9001L)) {
TripQueryVO query = TripQueryVO.forDriver(driverId);
IPage<TripVO> page = service.page(query);
assertEquals(0, page.getTotal(), "收口缺失不能退回「不按司机收口」:" + driverId);
assertTrue(page.getRecords().isEmpty());
assertEquals(0, service.tabCounts(query).getAll(), "统计同样为零:" + driverId);
assertThrows(ServiceException.class, () -> service.detail(query), "详情直接拒绝:" + driverId);
}
verify(waybillService, never()).list(anyWaybillWrapper());
}
// ------------------------------------------------------------ 夹具
@SuppressWarnings("unchecked")
@@ -712,7 +820,7 @@ class TripQueryServiceImplTest {
}
private List<TripVO> orgTypeTrips(String orgType) {
TripQueryVO query = TripQueryVO.forDriver(PHONE);
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setTransportOrgType(orgType);
query.setCurrent(1);
query.setSize(50);
@@ -737,7 +845,7 @@ class TripQueryServiceImplTest {
stubRows(
waybill(2092186187239780353L, "YD-20260825-0006", LOADING_NO, "running", null, at(2000L), ACCEPT_YES),
waybill(2092186187470467073L, "YD-20260825-0007", LOADING_NO, "running", "pending", at(1000L), ACCEPT_NO));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE)));
when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(DRIVER_ID)));
}
private void stubChildren() {
@@ -756,23 +864,25 @@ class TripQueryServiceImplTest {
}
private TripQueryVO tripQuery(String tripId) {
TripQueryVO query = TripQueryVO.forDriver(PHONE);
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setTripId(tripId);
return query;
}
private LoadingManage loading(String loadingNo, Long loadingId) {
LoadingManage loading = loading(PHONE);
LoadingManage loading = loading(DRIVER_ID);
loading.setId(loadingId);
loading.setLoadingNo(loadingNo);
return loading;
}
private LoadingManage loading(String driverPhone) {
private LoadingManage loading(Long driverId) {
LoadingManage loading = new LoadingManage();
loading.setId(LOADING_ID);
loading.setLoadingNo(LOADING_NO);
loading.setDriverPhone(driverPhone);
loading.setDriverId(driverId);
// 配载单上留的是派单时的手机号快照,档案改号后它不变
loading.setDriverPhone(PHONE);
loading.setDriverName("黄宇富");
loading.setVehicleNo("桂A32523");
loading.setProjectName("武宣建筑原料运输项目");
@@ -791,6 +901,8 @@ class TripQueryServiceImplTest {
waybill.setLoadingNo(loadingNo);
waybill.setBusinessStatus(businessStatus);
waybill.setDriverAcceptStatus(acceptStatus);
waybill.setDriverId(DRIVER_ID);
// 运单上留的是派单时的手机号快照,档案改号后它不变
waybill.setDriverPhone(PHONE);
waybill.setProcessJson(processJson);
waybill.setCreateTime(createTime);
@@ -45,6 +45,7 @@ import static org.mockito.Mockito.when;
class TripReadServiceBoundaryTest {
private static final String PHONE = "17587421208";
private static final Long DRIVER_ID = 9001L;
private static final String LOADING_NO = "PZ-20261017-001";
/** 19 位雪花号,按数字下发会被双精度取整 */
private static final Long LOADING_ID = 2111350784070275074L;
@@ -100,7 +101,7 @@ class TripReadServiceBoundaryTest {
child(CHILD_B, "cancelled", null, ACCEPT_NO, at(1787671422000L)));
// 「全部」按定义不含已取消档,别在它上面找这趟车;这一趟车本身仍要算成已取消
TripQueryVO query = TripQueryVO.forDriver(PHONE);
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setStatus(3);
TripVO trip = onlyTrip(query);
@@ -205,7 +206,7 @@ class TripReadServiceBoundaryTest {
void emptyChildrenDoNotThrow() {
stubLoadedTrip();
IPage<TripVO> page = service.page(TripQueryVO.forDriver(PHONE));
IPage<TripVO> page = service.page(TripQueryVO.forDriver(DRIVER_ID));
assertTrue(page.getRecords().isEmpty(), "配载单在、可见子单为空时不给没有子单的运次");
@@ -239,6 +240,8 @@ class TripReadServiceBoundaryTest {
bare.setBusinessStatus("pending");
bare.setDriverAcceptStatus("pending");
bare.setProcessJson(ACCEPT_YES);
bare.setDriverId(DRIVER_ID);
// 运单上留的是派单时的手机号快照,档案改号后它不变
bare.setDriverPhone(PHONE);
when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of(bare));
@@ -368,7 +371,7 @@ class TripReadServiceBoundaryTest {
}
private TripVO onlyTrip() {
return onlyTrip(TripQueryVO.forDriver(PHONE));
return onlyTrip(TripQueryVO.forDriver(DRIVER_ID));
}
private TripVO onlyTrip(TripQueryVO query) {
@@ -378,7 +381,7 @@ class TripReadServiceBoundaryTest {
}
private TripQueryVO detailQuery() {
TripQueryVO query = TripQueryVO.forDriver(PHONE);
TripQueryVO query = TripQueryVO.forDriver(DRIVER_ID);
query.setTripId(String.valueOf(LOADING_ID));
return query;
}
@@ -387,6 +390,8 @@ class TripReadServiceBoundaryTest {
LoadingManage loading = new LoadingManage();
loading.setId(LOADING_ID);
loading.setLoadingNo(LOADING_NO);
loading.setDriverId(DRIVER_ID);
// 配载单上留的是派单时的手机号快照
loading.setDriverPhone(PHONE);
loading.setDriverName("黄宇富");
loading.setVehicleNo("桂A32523");
@@ -401,6 +406,8 @@ class TripReadServiceBoundaryTest {
waybill.setLoadingNo(LOADING_NO);
waybill.setBusinessStatus(businessStatus);
waybill.setDriverAcceptStatus(acceptStatus);
waybill.setDriverId(DRIVER_ID);
// 运单上留的是派单时的手机号快照
waybill.setDriverPhone(PHONE);
waybill.setProcessJson(processJson);
waybill.setCreateTime(createTime);