diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java
index e037a882..1a1283de 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java
@@ -68,6 +68,9 @@ public class LoadingManage extends TenantEntity {
@Schema(description = "联系电话")
private String driverPhone;
+ @Schema(description = "司机ID")
+ private Long driverId;
+
@Schema(description = "押运人")
private String escortName;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java
index 750dd6d0..912b2b17 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java
@@ -34,8 +34,10 @@ import java.util.Set;
/**
* 运次查询条件
*
- * 司机端用 {@link #forDriver(String)} 按司机手机号收口;管理端用 {@link #forAdmin()} 不按司机收口。
- * 两者都不给时收口为空集——收口一旦缺省就等于把别人的运次发出去,故缺省不放行。
+ * 司机端用 {@link #forDriver(Long)} 按司机档案 ID 收口;管理端用 {@link #forAdmin()} 不按司机收口。
+ * 两者都不给时收口为空集。
+ *
+ * 归属判据只有司机档案 ID;driver_phone 是派单时的号码快照,不参与判定。
*/
@Data
@Schema(description = "运次查询条件")
@@ -47,8 +49,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 +94,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;
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java
index 6040145d..e43b197f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java
@@ -54,9 +54,10 @@ public class DriverAppController extends BladeController {
@GetMapping("/mine")
@ApiOperationSupport(order = 1)
- @Operation(summary = "当前登录司机档案", description = "按手机号匹配 blade_transport_driver.mobile;可传 mobile,未传则从登录态解析")
+ @Operation(summary = "当前登录司机档案", description = "按登录用户的 user_id 匹配 blade_transport_driver.user_id")
+ // mobile:旧版小程序仍会带这个参数,已不参与解析,保留只为不让老客户端报错
public R mine(@RequestParam(required = false) String mobile) {
- return R.data(driverAppService.currentByPhone(mobile));
+ return R.data(driverAppService.current());
}
@GetMapping("/vehicles")
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
index 06eaf200..96a6e25d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
@@ -65,14 +65,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/current-task")
@ApiOperationSupport(order = 1)
- @Operation(summary = "首页:当前运输中任务", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=running 的最新一条运单")
+ @Operation(summary = "首页:当前运输中任务", description = "当前司机档案(按登录用户解析)名下、businessStatus=running 的最新一条运单;归属判据是运单 driver_id")
public R currentTask() {
return R.data(driverWaybillService.currentTask());
}
@GetMapping("/pending-preview")
@ApiOperationSupport(order = 2)
- @Operation(summary = "首页:待接运单预览", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=pending 的预览列表与总数")
+ @Operation(summary = "首页:待接运单预览", description = "当前司机档案名下、businessStatus=pending 的预览列表与总数;归属判据是运单 driver_id")
public R pendingPreview(
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
return R.data(driverWaybillService.pendingPreview(size));
@@ -80,14 +80,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/counts")
@ApiOperationSupport(order = 3)
- @Operation(summary = "运单 Tab 统计", description = "仅统计当前用户手机号匹配运单 driverPhone 的运单:全部 / 待接单 / 进行中 / 已完成")
+ @Operation(summary = "运单 Tab 统计", description = "仅统计当前司机档案名下(运单 driver_id)的运单:全部 / 待接单 / 进行中 / 已完成")
public R counts() {
return R.data(driverWaybillService.tabCounts());
}
@GetMapping("/page")
@ApiOperationSupport(order = 4)
- @Operation(summary = "运单分页列表", description = "仅返回运单 driverPhone 等于当前用户手机号的数据;status:空=全部,0待接单,1运输中,2已完成")
+ @Operation(summary = "运单分页列表", description = "仅返回运单 driver_id 等于当前司机档案的数据;status:空=全部,0待接单,1运输中,2已完成")
public R> page(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@@ -107,14 +107,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/task/current")
@ApiOperationSupport(order = 21)
- @Operation(summary = "首页:当前运输中运次", description = "当前用户手机号可见的运输中或待确认(待回单确认)运次(有配载单的一趟车算一个)最新一条")
+ @Operation(summary = "首页:当前运输中运次", description = "当前司机档案可见的运输中或待确认(待回单确认)运次(有配载单的一趟车算一个)最新一条;配载段按配载单 driver_id,未配载段按运单 driver_id")
public R tripCurrentTask() {
return R.data(driverWaybillService.tripCurrentTask());
}
@GetMapping("/task/pending-preview")
@ApiOperationSupport(order = 22)
- @Operation(summary = "首页:待接运次预览", description = "当前用户手机号可见的待接运次预览与总数(按运次计)")
+ @Operation(summary = "首页:待接运次预览", description = "当前司机档案可见的待接运次预览与总数(按运次计)")
public R tripPendingPreview(
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
return R.data(driverWaybillService.tripPendingPreview(size));
@@ -166,7 +166,7 @@ public class DriverWaybillController extends BladeController {
@PostMapping("/track")
@ApiOperationSupport(order = 11)
- @Operation(summary = "司机端行驶轨迹", description = "按当前用户手机号校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
+ @Operation(summary = "司机端行驶轨迹", description = "按当前司机档案校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
public R track(@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
return R.data(driverWaybillService.track(id));
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java
index d7c7d34a..9e6d5b12 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java
@@ -34,11 +34,11 @@ import java.util.List;
public interface IDriverAppService {
/**
- * 当前登录用户对应的司机档案(按手机号匹配 blade_transport_driver.mobile)
+ * 当前登录用户对应的司机档案(按 user_id 匹配 blade_transport_driver.user_id)
*
- * @param mobile 小程序可显式传入当前用户手机号;为空时从登录态解析
+ * @return 司机档案;未登录抛异常,查不到返回 null
*/
- DriverVO currentByPhone(String mobile);
+ DriverVO current();
/**
* 当前司机绑定的车辆列表(driver.driving_vehicle ↔ vehicle.plate_no)
@@ -49,10 +49,9 @@ public interface IDriverAppService {
* 当前登录用户对应的司机**实体**
*
* 认证提交要写回司机表,且越权校验要读 {@code driving_vehicle},
- * 故这里返回实体而非视图对象。解析顺序:先按登录手机号匹配,
- * 匹配不到再按 {@code user_id} 兜底。
+ * 故这里返回实体而非视图对象。解析口径与 {@link #current()} 一致。
*
- * @return 司机实体;未登录或查不到时返回 null
+ * @return 司机实体;未登录抛异常,查不到返回 null
*/
Driver currentDriverEntity();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java
index 73924e3e..d2962801 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java
@@ -111,4 +111,16 @@ public interface IDriverService extends BaseService {
*/
List exportDriver(Wrapper queryWrapper);
+ /**
+ * 按手机号解析司机档案 ID
+ *
+ * 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认,
+ * 0 条(承运商司机、未登录过小程序的司机)与多条(历史脏数据)都返回 null,不抛异常。
+ *
+ * @param tenantId 租户ID,为空时取当前登录租户
+ * @param mobile 手机号
+ * @return 司机档案ID;无法唯一确定时为 null
+ */
+ Long resolveDriverIdByMobile(String tenantId, String mobile);
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITripQueryService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITripQueryService.java
index dbc87bde..c1c8ecb6 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITripQueryService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITripQueryService.java
@@ -41,7 +41,7 @@ public interface ITripQueryService {
*
* 库层只取回收口与行级筛选命中的运单行,分组、分档、筛选、排序与分页都在服务层算完再做。
*
- * @param query 查询条件,收口由 driverPhone 或 allDrivers 给出
+ * @param query 查询条件,收口由 driverId(司机档案)或 allDrivers 给出
* @return 运次分页
*/
IPage page(TripQueryVO query);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java
index 148ce41e..c60245f8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java
@@ -27,10 +27,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
-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.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
@@ -57,17 +54,16 @@ public class DriverAppServiceImpl implements IDriverAppService {
private final IDriverService driverService;
private final ITransportVehicleService transportVehicleService;
- private final IMpUserClient mpUserClient;
@Override
- public DriverVO currentByPhone(String mobile) {
- Driver driver = currentDriver(mobile);
+ public DriverVO current() {
+ Driver driver = currentDriver();
return driver == null ? null : DriverWrapper.build().entityVO(driver);
}
@Override
public List myVehicles() {
- Driver driver = currentDriver(null);
+ Driver driver = currentDriver();
if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) {
return List.of();
}
@@ -92,28 +88,23 @@ public class DriverAppServiceImpl implements IDriverAppService {
@Override
public Driver currentDriverEntity() {
- return currentDriver(null);
+ return currentDriver();
}
- private Driver currentDriver(String mobileHint) {
+ /**
+ * 当前司机的唯一解析入口:只按登录用户的 {@code user_id} 查档案。
+ *
+ * 手机号不能用来认人:同一号码可能存在多条档案,按号码查「最近更新的一条」
+ * 取到的可能是别人的档案;号码本身也会被改。
+ */
+ private Driver currentDriver() {
Long userId = AuthUtil.getUserId();
if (userId == null || userId <= 0) {
throw new ServiceException("未登录");
}
- String phone = resolvePhone(userId, mobileHint);
- Driver driver = null;
- if (Func.isNotEmpty(phone)) {
- driver = driverService.getOne(Wrappers.lambdaQuery()
- .eq(Driver::getMobile, phone)
- .orderByDesc(Driver::getUpdateTime)
- .last("LIMIT 1"));
- }
- if (driver == null) {
- driver = driverService.getOne(Wrappers.lambdaQuery()
- .eq(Driver::getUserId, userId)
- .last("LIMIT 1"));
- }
- return driver;
+ return driverService.getOne(Wrappers.lambdaQuery()
+ .eq(Driver::getUserId, userId)
+ .last("LIMIT 1"));
}
private DriverVehicleCardVO toCard(TransportVehicle vehicle) {
@@ -178,53 +169,4 @@ public class DriverAppServiceImpl implements IDriverAppService {
return date == null ? "" : date.toString();
}
- /**
- * 解析用于匹配司机档案的手机号。
- * 优先级:前端传入且与本人一致的 mobile → JWT account(司机账号多为手机号)→ 用户中心 phone/account
- */
- private String resolvePhone(Long userId, String mobileHint) {
- String selfPhone = resolveSelfPhone(userId);
- String hint = Func.isEmpty(mobileHint) ? null : mobileHint.trim();
- if (Func.isNotEmpty(hint)) {
- if (Func.isEmpty(selfPhone)) {
- // 解析不出本人号码时不能拿前端传的 mobile 去查:
- // currentDriver 会直接按 mobile 等值查询,传入他人号码即可读到他人档案。
- // 这里返回空让它退回按 user_id 查,宁可查不到也不越权。
- return null;
- }
- if (!selfPhone.equals(hint)) {
- throw new ServiceException("只能查询本人司机档案");
- }
- return hint;
- }
- return selfPhone;
- }
-
- private String resolveSelfPhone(Long userId) {
- String account = AuthUtil.getUserAccount();
- if (isMobileLike(account)) {
- return account.trim();
- }
- // 小程序用户 phone 带 @Sensitive(MOBILE),经 Feign 回来是脱敏串,
- // 因此这里只取未脱敏的 account;脱敏的 phone 一律不用。
- R result = mpUserClient.userById(userId);
- if (result != null && R.isSuccess(result) && result.getData() != null) {
- String userAccount = result.getData().getAccount();
- if (isMobileLike(userAccount)) {
- return userAccount.trim();
- }
- }
- return Func.isEmpty(account) ? null : account.trim();
- }
-
- /**
- * 是否是「可作为司机档案手机号使用」的号码串。
- *
- * 只判 1 开头的 11~15 位纯数字:库里既有 11 位真实手机号,也有 12 位账号
- * (如 175874212082),写死 11 位会让这类账号的司机解析不出本人号码。
- */
- private boolean isMobileLike(String value) {
- return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
- }
-
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
index 2d4dc598..fa3a1c80 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
@@ -25,6 +25,7 @@
*/
package org.springblade.transport.service.impl;
+import cn.hutool.core.util.DesensitizedUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -171,6 +172,31 @@ public class DriverServiceImpl extends BaseServiceImpl imp
return bindExistingProfile(existing.getId(), mpUserId);
}
+ @Override
+ public Long resolveDriverIdByMobile(String tenantId, String mobile) {
+ String resolvedTenantId = trimToNull(tenantId);
+ if (resolvedTenantId == null) {
+ resolvedTenantId = trimToNull(AuthUtil.getTenantId());
+ }
+ String mobileSnapshot = trimToNull(mobile);
+ // 脱敏手机号(如 138****8000)与空号都不是可用业务数据,查不到也不该查
+ if (resolvedTenantId == null || mobileSnapshot == null || mobileSnapshot.contains("*")) {
+ return null;
+ }
+ // 手机号会被二次放号,同租户内可能残留多条历史档案:只有唯一命中才认,不猜
+ List matchedDrivers = list(Wrappers.lambdaQuery()
+ .select(Driver::getId)
+ .eq(Driver::getTenantId, resolvedTenantId)
+ .eq(Driver::getMobile, mobileSnapshot));
+ if (matchedDrivers.size() != 1) {
+ log.warn("司机档案解析未唯一命中,driver_id 留空。租户={} 手机号={} 命中={} 条",
+ resolvedTenantId, DesensitizedUtil.mobilePhone(mobileSnapshot), matchedDrivers.size());
+ return null;
+ }
+ Driver matchedDriver = matchedDrivers.get(0);
+ return matchedDriver == null ? null : matchedDriver.getId();
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public DriverSyncMobileVO syncMobile(String tenantId, Long mpUserId, String oldPhone, String newPhone) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
index afd82e56..9433a569 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
@@ -33,10 +33,7 @@ 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;
@@ -154,7 +151,6 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private final IWaybillService waybillService;
private final IDriverService driverService;
- private final IMpUserClient mpUserClient;
private final IProcessConfigService processConfigService;
private final ITransportVehicleService transportVehicleService;
private final CertificationGate certificationGate;
@@ -166,12 +162,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillCardVO currentTask() {
- String phone = currentUserPhone();
- if (Func.isEmpty(phone)) {
+ Driver driver = currentDriver();
+ if (driver == null) {
return null;
}
// 进行中:running,或无需确认接单但仍为 pending 的历史数据
- List candidates = waybillService.list(scopedQuery(phone)
+ List candidates = waybillService.list(scopedQuery(driver.getId())
.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING))
.orderByDesc(Waybill::getUpdateTime)
.last("LIMIT 20"));
@@ -187,12 +183,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillPreviewVO pendingPreview(Integer size) {
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
- String phone = currentUserPhone();
- if (Func.isEmpty(phone)) {
+ Driver driver = currentDriver();
+ if (driver == null) {
return preview;
}
int limit = normalizePreviewSize(size);
- List pendingList = waybillService.list(scopedQuery(phone)
+ List pendingList = waybillService.list(scopedQuery(driver.getId())
.in(Waybill::getBusinessStatus, PENDING_STATUSES)
.orderByDesc(Waybill::getCreateTime));
List needAccept = pendingList.stream()
@@ -207,11 +203,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillTabCountsVO tabCounts() {
DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO();
- String phone = currentUserPhone();
- if (Func.isEmpty(phone)) {
+ Driver driver = currentDriver();
+ if (driver == null) {
return vo;
}
- List waybills = waybillService.list(scopedQuery(phone)
+ List waybills = waybillService.list(scopedQuery(driver.getId())
.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES));
long pending = 0;
long doing = 0;
@@ -243,12 +239,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
Page empty = new Page<>(pageNo, pageSize);
- String phone = currentUserPhone();
- if (Func.isEmpty(phone)) {
+ Driver driver = currentDriver();
+ if (driver == null) {
return empty;
}
- LambdaQueryWrapper wrapper = scopedQuery(phone);
+ LambdaQueryWrapper wrapper = scopedQuery(driver.getId());
// 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页
applyStatusFilterForQuery(wrapper, status);
applyKeyword(wrapper, keyword);
@@ -271,18 +267,17 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public DriverWaybillCardVO detail(Long id) {
- requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id);
return toCard(normalizeAcceptStatus(waybill), true);
}
@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);
@@ -294,11 +289,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));
@@ -313,11 +308,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());
@@ -328,11 +323,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public IPage 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);
@@ -347,15 +342,14 @@ 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);
}
@Override
public WaybillTrackVO track(Long id) {
- requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id);
LocalDate[] range = resolveTrackDateRange(waybill);
return waybillService.trackVehicle(waybill.getId(), range[0].toString(), range[1].toString());
@@ -742,7 +736,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
* 子单集合是司机可见的那一批:已拒单的子单不在其中,否则接单会把本人拒掉的单重新拉回来。
*/
private List tripChildren(String tripId) {
- TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
+ TripQueryVO query = TripQueryVO.forDriver(requireCurrentDriver().getId());
query.setTripId(tripId);
TripVO trip = tripQueryService.detail(query);
List children = trip.getWaybills() == null ? List.of() : trip.getWaybills();
@@ -822,13 +816,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
if (id == null) {
throw new ServiceException("运单ID不能为空");
}
- String phone = currentUserPhone();
- if (Func.isEmpty(phone)) {
- throw new ServiceException("当前用户手机号为空,无法查找运单");
- }
+ Driver driver = requireCurrentDriver();
// MySQL 的 FOR UPDATE 必须跟在 LIMIT 之后
String tail = forUpdate ? "LIMIT 1 FOR UPDATE" : "LIMIT 1";
- Waybill waybill = waybillService.getOne(scopedQuery(phone, includeRejected)
+ Waybill waybill = waybillService.getOne(scopedQuery(driver.getId(), includeRejected)
.eq(Waybill::getId, id).last(tail));
if (waybill == null) {
throw new ServiceException("运单不存在或无权操作");
@@ -885,14 +876,17 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
/**
- * 司机可见运单范围:运单司机手机号(driver_phone)等于当前用户手机号,
+ * 司机可见运单范围:运单归属档案(driver_id)就是当前司机档案,
* 且该运单未被这个司机拒单。
*
+ * driver_id 必须有值且大于 0 才算有主:存量数据的 -1 是哨兵值,与 0 一样等同无主。
+ * 运单上的 driver_phone 只是派单快照,不参与归属判定——司机改号不影响可见性。
+ *
* 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。
* 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。
*/
- private LambdaQueryWrapper scopedQuery(String phone) {
- return scopedQuery(phone, false);
+ private LambdaQueryWrapper scopedQuery(Long driverId) {
+ return scopedQuery(driverId, false);
}
/**
@@ -900,11 +894,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
* 拒单的运单不可能处于运输中,收口拦不到合法操作,
* 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤
*/
- private LambdaQueryWrapper scopedQuery(String phone, boolean includeRejected) {
- String normalizedPhone = Func.toStr(phone, "").trim();
- LambdaQueryWrapper wrapper = Wrappers.lambdaQuery().and(w -> w
- .eq(Waybill::getDriverPhone, normalizedPhone)
- .or().apply("REPLACE(IFNULL(driver_phone,''),' ','') = {0}", normalizedPhone));
+ private LambdaQueryWrapper scopedQuery(Long driverId, boolean includeRejected) {
+ LambdaQueryWrapper wrapper = Wrappers.lambdaQuery()
+ .gt(Waybill::getDriverId, 0)
+ .eq(Waybill::getDriverId, driverId);
if (!includeRejected) {
wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus)
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
@@ -926,69 +919,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
/**
- * 当前用户手机号:司机档案 mobile 优先,其次小程序用户账号。
+ * 当前登录司机:只按登录用户 ID 查司机档案,查不到即「无归属」。
*
- * 派单时运单 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 result = mpUserClient.userById(userId);
- if (result != null && R.isSuccess(result) && result.getData() != null
- && isMobileLike(result.getData().getAccount())) {
- return result.getData().getAccount().trim();
- }
- return null;
- }
-
- /**
- * 当前登录司机:优先 userId,其次手机号(JWT 账号 / 小程序用户 account)匹配 driver.mobile
+ * 归属判据:driver_id = 当前登录人的司机档案 id;driver_phone 不参与判定。
*/
private Driver currentDriver() {
Long userId = AuthUtil.getUserId();
if (userId == null || userId <= 0) {
return null;
}
- Driver driver = driverService.getOne(Wrappers.lambdaQuery()
+ return driverService.getOne(Wrappers.lambdaQuery()
.eq(Driver::getUserId, userId)
.last("LIMIT 1"));
- if (driver != null) {
- return driver;
- }
- String phone = resolveSelfPhone(userId);
- if (Func.isNotEmpty(phone)) {
- return driverService.getOne(Wrappers.lambdaQuery()
- .eq(Driver::getMobile, phone)
- .orderByDesc(Driver::getUpdateTime)
- .last("LIMIT 1"));
- }
- return null;
- }
-
- /**
- * 解析当前小程序用户可用于匹配司机档案的手机号
- */
- private String resolveSelfPhone(Long userId) {
- String account = AuthUtil.getUserAccount();
- if (isMobileLike(account)) {
- return account.trim();
- }
- R result = mpUserClient.userById(userId);
- if (result != null && R.isSuccess(result) && result.getData() != null
- && isMobileLike(result.getData().getAccount())) {
- return result.getData().getAccount().trim();
- }
- return Func.isEmpty(account) ? null : account.trim();
- }
-
- private boolean isMobileLike(String value) {
- return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
}
private List splitPlates(String drivingVehicle) {
@@ -1308,8 +1250,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
@Override
public void assertCurrentDriverOwnsWaybill(Long waybillId) {
- // 归属口径与司机打卡、司机接单一致:按当前用户手机号匹配运单司机手机号
- requireCurrentDriver();
+ // 归属口径与司机打卡、司机接单一致:运单 driver_id 必须是当前登录司机的档案(查不到档案即无归属)
loadDriverWaybill(waybillId);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java
index 0777be99..63a9cbf4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java
@@ -4,6 +4,7 @@
*/
package org.springblade.transport.service.impl;
+import cn.hutool.core.util.DesensitizedUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -11,6 +12,7 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
+import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
@@ -29,6 +31,7 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.service.IContractManageService;
+import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService;
@@ -89,6 +92,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl selectLoadingManagePage(IPage page, LoadingManageVO loadingManage) {
IPage entityPage = page(page, buildQuery(loadingManage));
@@ -139,16 +145,17 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds = new ArrayList<>();
// 新建暂存为草稿;编辑已有单据时保持原业务状态(如待执行),避免被改回草稿
String businessStatus = STATUS_DRAFT;
- if (Func.isNotEmpty(loadingManage.getId())) {
- LoadingManage oldRecord = loadEditable(loadingManage.getId(), true);
- oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
- loadingManage.setLoadingNo(oldRecord.getLoadingNo());
- loadingManage.setDeptId(oldRecord.getDeptId());
- loadingManage.setDeptName(oldRecord.getDeptName());
- if (Func.isNotEmpty(oldRecord.getBusinessStatus())) {
- businessStatus = oldRecord.getBusinessStatus();
+ LoadingManage stored = Func.isEmpty(loadingManage.getId()) ? null : loadEditable(loadingManage.getId(), true);
+ if (stored != null) {
+ oldWaybillIds = waybillIds(stored.getWaybillIdsJson());
+ loadingManage.setLoadingNo(stored.getLoadingNo());
+ loadingManage.setDeptId(stored.getDeptId());
+ loadingManage.setDeptName(stored.getDeptName());
+ if (Func.isNotEmpty(stored.getBusinessStatus())) {
+ businessStatus = stored.getBusinessStatus();
}
}
+ applyDriverOwnership(loadingManage, stored);
prepareCreateOrUpdate(loadingManage);
loadingManage.setBusinessStatus(businessStatus);
if (Func.isEmpty(loadingManage.getLoadingNo())) {
@@ -200,6 +207,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
loadingManage.setLoadingNo(oldRecord.getLoadingNo());
@@ -781,6 +793,32 @@ public class LoadingManageServiceImpl extends BaseServiceImpl
+ * 判定顺序:调用方带了正数档案 ID 就用它;更新时 driver_phone 与库中一致且库中 driver_id 非空就保留库中值;
+ * 其余情况(新建、换号、原本无主)按手机号在租户内唯一匹配司机档案,匹配不到落 null 并留 WARN,不阻断保存。
+ */
+ private void applyDriverOwnership(LoadingManage loadingManage, LoadingManage stored) {
+ Long driverId = loadingManage.getDriverId();
+ if (driverId != null && driverId > 0) {
+ return;
+ }
+ if (stored != null && Func.isNotEmpty(stored.getDriverId())
+ && Objects.equals(TransportBusinessSupport.trimToNull(stored.getDriverPhone()), loadingManage.getDriverPhone())) {
+ loadingManage.setDriverId(stored.getDriverId());
+ return;
+ }
+ String tenantId = Func.isNotEmpty(loadingManage.getTenantId()) ? loadingManage.getTenantId() : AuthUtil.getTenantId();
+ Long resolvedDriverId = driverService.resolveDriverIdByMobile(tenantId, loadingManage.getDriverPhone());
+ loadingManage.setDriverId(resolvedDriverId);
+ if (resolvedDriverId == null) {
+ log.warn("配载单 {} 的司机手机号未唯一匹配司机档案,driver_id 落空。租户={} 手机号={}",
+ Func.isEmpty(loadingManage.getLoadingNo()) ? "(未编号)" : loadingManage.getLoadingNo(),
+ tenantId, DesensitizedUtil.mobilePhone(loadingManage.getDriverPhone()));
+ }
+ }
+
private void validate(LoadingManage loadingManage, boolean draftMode) {
TransportBusinessSupport.validateRequired(loadingManage.getLoadingSubNos(), "配载子单号不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getTransportType(), "运输类型不能为空");
@@ -1150,6 +1188,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl implements IMasterOrderService {
private static final String CARRIER_SELF = "自运";
@@ -58,13 +62,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl
- * 手机号允许带空格,与运单列表的收口判据一致。
+ * 归属判据只有司机档案 ID(null 或非正数视为无主),driver_phone 不参与判定。
+ * 无主的运次由 {@link #scoped} 在外层挡掉。
*/
private void applyScope(LambdaQueryWrapper 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 +786,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
}
/**
- * 按运次主键取配载单:主键命中配载单即配载运次,再按司机收口
+ * 按运次主键取配载单:主键命中配载单即配载运次,再按配载单的司机档案 ID 收口
*/
private LoadingManage visibleLoading(Long id, TripQueryVO condition) {
LoadingManage loading = loadingManageService.getById(id);
@@ -796,7 +796,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 +813,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 与收口条件相等。
+ *
+ * 条件侧缺省(空、0、负数)一律不可见——没有有效档案 ID 就等于没有收口条件,
+ * 此时任何行都不能算作「本人」,否则业务主键未回填的历史数据会把运次漏给所有人。
+ */
+ private boolean ownedBy(Long ownerId, Long expectedId) {
+ return expectedId != null && expectedId > 0 && expectedId.equals(ownerId);
}
private TripQueryVO condition(TripQueryVO query) {
@@ -835,7 +839,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());
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java
index 61604120..5eabbc36 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
@@ -41,6 +42,7 @@ import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
import org.springblade.transport.service.ICustomerArchiveService;
+import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ILoadingManageService;
@@ -68,6 +70,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** 运单批次服务实现。 */
+@Slf4j
@Service
@RequiredArgsConstructor
public class WaybillImportBatchServiceImpl extends BaseServiceImpl implements IWaybillImportBatchService {
@@ -101,6 +104,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl rowWaybills = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
try {
- rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
+ Waybill builtWaybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
+ fillDriverOwnership(builtWaybill, index + 1);
+ rowWaybills.add(builtWaybill);
} catch (Exception exception) {
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
}
@@ -485,6 +491,26 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl 0) {
+ return;
+ }
+ // 解析入口只返回 null 或唯一命中的档案 ID,不需要再按哨兵归一
+ Long resolvedDriverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), waybill.getDriverPhone());
+ if (resolvedDriverId != null) {
+ waybill.setDriverId(resolvedDriverId);
+ return;
+ }
+ waybill.setDriverId(null);
+ log.warn("第{}行 运单 {} 的司机手机号未唯一匹配司机档案,driver_id 留空(不阻断导入)",
+ rowNo, waybill.getWaybillNo());
+ }
+
private Waybill buildWaybill(Map row, WaybillImportBatch batch, Long carrierContractId, boolean draft) {
if (row == null) throw new ServiceException("运单数据不正确");
Waybill waybill = new Waybill();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
index bc263482..611b4e5f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
@@ -22,6 +22,7 @@
*/
package org.springblade.transport.service.impl;
+import cn.hutool.core.util.DesensitizedUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -61,6 +62,7 @@ import org.springblade.transport.pojo.vo.WaybillPunchPhotoVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.pojo.vo.WaybillVO;
+import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IProjectApplyService;
@@ -110,6 +112,9 @@ public class WaybillServiceImpl extends BaseServiceImpl
@jakarta.annotation.Resource
private ILoadingManageService loadingManageService;
+ @jakarta.annotation.Resource
+ private IDriverService driverService;
+
@jakarta.annotation.Resource
private IProcessConfigService processConfigService;
@@ -1610,7 +1615,18 @@ public class WaybillServiceImpl extends BaseServiceImpl
TransportBusinessSupport.validateRequired(driverPhone, "手机号不能为空");
TransportBusinessSupport.validateRequired(vehicleNo, "车牌号不能为空");
- waybill.setDriverId(request.getDriverId());
+ // 归属主键是档案 ID,手机号只是派单时的号码快照:调用方带了档案 ID 就用它,
+ // 否则按手机号解析;解析入口只返回 null 或唯一命中的档案 ID,承运商司机、未建档司机
+ // 解析为空,派单照旧成功,落空时留 WARN
+ Long driverId = request.getDriverId();
+ if (driverId == null || driverId <= 0) {
+ driverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), driverPhone);
+ if (driverId == null) {
+ log.warn("运单 {} 的司机手机号未唯一匹配司机档案,driver_id 落空(不阻断派单)。租户={} 手机号={}",
+ waybill.getWaybillNo(), waybill.getTenantId(), DesensitizedUtil.mobilePhone(driverPhone));
+ }
+ }
+ waybill.setDriverId(driverId);
waybill.setDriverName(driverName);
waybill.setDriverPhone(driverPhone);
waybill.setVehicleNo(vehicleNo);
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverAppServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverAppServiceImplTest.java
index 9878ad99..746259c8 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverAppServiceImplTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverAppServiceImplTest.java
@@ -1,15 +1,16 @@
package org.springblade.transport.service.impl;
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+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.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
-import org.springblade.core.tool.api.R;
-import org.springblade.system.feign.IMpUserClient;
-import org.springblade.system.pojo.entity.MpUser;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
@@ -17,19 +18,23 @@ import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ITransportVehicleService;
import java.time.LocalDate;
+import java.util.Date;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
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.when;
/**
- * 司机端车辆卡片(重新认证表单的数据来源)测试。
+ * 司机端「当前司机」解析与车辆卡片(重新认证表单的数据来源)测试。
*
- * 卡片字段会**直接进表单**,所以这里盯的不是「显示得好不好看」,而是两条会让回显出错的契约:
+ * 解析侧盯住 ADR-0028 的唯一入口:登录用户 → {@code user_id} → 档案,手机号只是档案上的一个字段,
+ * 不参与查找;卡片字段会**直接进表单**,所以这里盯的不是「显示得好不好看」,而是两条会让回显出错的契约:
* 日期字段只能是 {@code yyyy-MM-dd} 或空串(不能是「长期」这类文字);
* 标了长期有效的字段不能再下发一个作废的止期。
*/
@@ -42,11 +47,17 @@ class DriverAppServiceImplTest {
private ITransportVehicleService transportVehicleService;
private DriverAppServiceImpl service;
+ @BeforeAll
+ static void initTableInfo() {
+ // 实体元数据就绪后,LambdaQueryWrapper 才能把 Driver::getUserId 解析成列名
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Driver.class);
+ }
+
@BeforeEach
void setUp() {
driverService = mock(IDriverService.class);
transportVehicleService = mock(ITransportVehicleService.class);
- service = new DriverAppServiceImpl(driverService, transportVehicleService, mock(IMpUserClient.class));
+ service = new DriverAppServiceImpl(driverService, transportVehicleService);
}
@Test
@@ -85,33 +96,69 @@ class DriverAppServiceImplTest {
assertEquals("2028-12-04", card.getRoadTransportCertStartDate());
}
- // ---------------------------------------------------------------- 手机号解析
+ // ---------------------------------------------------------------- 当前司机解析
@Test
- @DisplayName("12 位手机号也能解析出本人号码——库里存在 12 位司机账号,写死 11 位会让他们查不到自己的档案")
- void twelveDigitAccountIsUsableAsPhone() {
- assertEquals("黄大富", currentByPhone("175874212082", "175874212082", null).getDriverName());
+ @DisplayName("同号两条档案时取登录人自己那条——按手机号取「最近更新的一条」会把冒名者当成当前司机")
+ void currentDriverComesFromUserIdNotMobile() {
+ Driver owner = driver(9001L, USER_ID, "黄大富", MOBILE);
+ owner.setUpdateTime(new Date(1735689600000L));
+ Driver impostor = driver(9002L, 7002L, "冒名者", MOBILE);
+ impostor.setUpdateTime(new Date(1767225600000L));
+
+ when(driverService.getOne(any(LambdaQueryWrapper.class))).thenAnswer(invocation -> {
+ LambdaQueryWrapper wrapper = invocation.getArgument(0);
+ String sqlSegment = wrapper.getSqlSegment();
+ if (sqlSegment.contains("mobile")) {
+ return impostor;
+ }
+ return sqlSegment.contains("user_id") ? owner : null;
+ });
+
+ try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
+ authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
+ // 司机账号本身就是手机号:手机号优先的实现正是靠它命中那条同号档案
+ authUtil.when(AuthUtil::getUserAccount).thenReturn(MOBILE);
+ assertEquals("黄大富", service.current().getDriverName());
+ }
}
@Test
- @DisplayName("小程序用户 phone 是脱敏串时改用其 account——脱敏 phone 既查不到档案,也会把本人真实号码误判成越权")
- void maskedUserPhoneFallsBackToAccount() {
- MpUser user = new MpUser();
- user.setPhone("175****12082");
- user.setAccount("175874212082");
+ @DisplayName("没有档案时返回空数据而不是异常——没档案的登录用户看到的是空页面、也不能操作")
+ void missingProfileReturnsEmptyInsteadOfFailing() {
+ when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
- assertEquals("黄大富", currentByPhone(null, null, user).getDriverName());
+ try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
+ authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
+ assertNull(service.current(), "查不到档案时档案为空");
+ assertNull(service.currentDriverEntity(), "查不到档案时实体为空");
+ assertTrue(service.myVehicles().isEmpty(), "查不到档案时车辆列表为空");
+ }
}
@Test
- @DisplayName("传他人手机号一律拒绝——不能因为解析不到本人号码就放行,那等于允许按手机号查任意档案")
- void otherPeoplesMobileIsRejected() {
- assertEquals("只能查询本人司机档案", assertThrows(ServiceException.class,
- () -> currentByPhone("13900000001", "175874212082", null)).getMessage());
+ @DisplayName("未登录直接抛异常,不能当成「没有档案」静默返回空")
+ void anonymousIsRejected() {
+ try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
+ authUtil.when(AuthUtil::getUserId).thenReturn(null);
+ assertEquals("未登录", assertThrows(ServiceException.class,
+ () -> service.current()).getMessage());
+ assertEquals("未登录", assertThrows(ServiceException.class, service::myVehicles).getMessage());
+ assertEquals("未登录", assertThrows(ServiceException.class, service::currentDriverEntity).getMessage());
+ }
}
// ---------------------------------------------------------------- 辅助
+ private Driver driver(Long id, Long userId, String driverName, String mobile) {
+ Driver driver = new Driver();
+ driver.setId(id);
+ driver.setUserId(userId);
+ driver.setDriverName(driverName);
+ driver.setMobile(mobile);
+ return driver;
+ }
+
/** 造一辆只关心有效期三个字段的车 */
private TransportVehicle vehicle(LocalDate licenseStart, LocalDate licenseEnd, Integer licenseLongTerm) {
TransportVehicle vehicle = new TransportVehicle();
@@ -129,13 +176,9 @@ class DriverAppServiceImplTest {
return vehicle;
}
- /** 按当前登录司机的手机号跑一次「我的车辆」,取第一张卡片 */
+ /** 按当前登录司机跑一次「我的车辆」,取第一张卡片 */
private DriverVehicleCardVO firstCard(TransportVehicle vehicle) {
- Driver driver = new Driver();
- driver.setId(9001L);
- driver.setUserId(USER_ID);
- driver.setDriverName("韦司机");
- driver.setMobile(MOBILE);
+ Driver driver = driver(9001L, USER_ID, "韦司机", MOBILE);
driver.setDrivingVehicle(vehicle.getPlateNo());
when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(driver);
@@ -143,39 +186,9 @@ class DriverAppServiceImplTest {
try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
- // 账号本身就是手机号,resolvePhone 走这条短路分支,不再回调用户中心
- authUtil.when(AuthUtil::getUserAccount).thenReturn(MOBILE);
List cards = service.myVehicles();
assertEquals(1, cards.size(), "应当只返回绑定到本人的这一辆车");
return cards.get(0);
}
}
-
- /**
- * 按给定的登录态跑一次 currentByPhone。
- *
- * @param mobileHint 前端传入的 mobile,可为 null
- * @param account JWT 里的账号,可为 null
- * @param user 小程序用户,可为 null(表示走不到小程序用户中心)
- */
- private Driver currentByPhone(String mobileHint, String account, MpUser user) {
- IMpUserClient mpUserClient = mock(IMpUserClient.class);
- if (user != null) {
- when(mpUserClient.userById(USER_ID)).thenReturn(R.data(user));
- }
- DriverAppServiceImpl scoped = new DriverAppServiceImpl(driverService, transportVehicleService, mpUserClient);
-
- Driver driver = new Driver();
- driver.setId(9002L);
- driver.setUserId(USER_ID);
- driver.setDriverName("黄大富");
- driver.setMobile("175874212082");
- when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(driver);
-
- try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
- authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
- authUtil.when(AuthUtil::getUserAccount).thenReturn(account);
- return scoped.currentByPhone(mobileHint);
- }
- }
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java
index bc544d0a..98dcafb8 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java
@@ -12,7 +12,6 @@ import org.mockito.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
-import org.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -95,7 +94,6 @@ class DriverNodePunchPhotoTest {
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchFinishFlagTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchFinishFlagTest.java
index 06667c50..132a3b98 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchFinishFlagTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchFinishFlagTest.java
@@ -7,7 +7,6 @@ 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.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -92,7 +91,6 @@ class DriverPunchFinishFlagTest {
service = new DriverWaybillServiceImpl(
waybillService,
mock(IDriverService.class),
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java
index 0009525f..25c4b5ed 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java
@@ -10,7 +10,6 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springblade.core.tool.jackson.JsonUtil;
-import org.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -87,7 +86,6 @@ class DriverPunchNodeCardTest {
service = new DriverWaybillServiceImpl(
waybillService,
mock(IDriverService.class),
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java
new file mode 100644
index 00000000..89ba9503
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java
@@ -0,0 +1,131 @@
+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.springblade.system.feign.IMpUserClient;
+import org.springblade.system.feign.ISysClient;
+import org.springblade.transport.mapper.DriverMapper;
+import org.springblade.transport.pojo.entity.Driver;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 手机号 -> 司机档案 ID 解析入口的测试。
+ *
+ * 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认,0 条与多条都不认。
+ * 解析结果是"能不能写归属"的判断,不是业务校验,因此任何入参都只返回 null,不抛异常。
+ */
+class DriverResolveMobileTest {
+
+ private static final String TENANT_ID = "000000";
+ private static final String MOBILE = "13800138000";
+
+ private DriverMapper driverMapper;
+ private DriverServiceImpl service;
+
+ @BeforeAll
+ static void initTableInfo() {
+ MybatisConfiguration configuration = new MybatisConfiguration();
+ MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
+ assistant.setCurrentNamespace("test");
+ TableInfoHelper.initTableInfo(assistant, Driver.class);
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ driverMapper = mock(DriverMapper.class);
+ service = new DriverServiceImpl(mock(IMpUserClient.class), mock(ISysClient.class));
+ inject("baseMapper", driverMapper);
+ }
+
+ @Test
+ @DisplayName("租户内唯一命中:返回档案 ID")
+ void returnsProfileIdWhenUnique() {
+ when(driverMapper.selectList(any())).thenReturn(List.of(profile(9001L)));
+
+ assertEquals(9001L, service.resolveDriverIdByMobile(TENANT_ID, MOBILE));
+ }
+
+ @Test
+ @DisplayName("租户内没有档案:返回 null")
+ void returnsNullWhenAbsent() {
+ when(driverMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+ assertNull(service.resolveDriverIdByMobile(TENANT_ID, MOBILE));
+ }
+
+ @Test
+ @DisplayName("租户内多条档案(历史同号):返回 null,不猜")
+ void returnsNullWhenMultiple() {
+ List profiles = new ArrayList<>();
+ profiles.add(profile(9001L));
+ profiles.add(profile(9002L));
+ when(driverMapper.selectList(any())).thenReturn(profiles);
+
+ assertNull(service.resolveDriverIdByMobile(TENANT_ID, MOBILE));
+ }
+
+ @Test
+ @DisplayName("手机号为空或脱敏串:返回 null,且不查库")
+ void returnsNullWithoutQueryForBlankOrMaskedMobile() {
+ assertNull(service.resolveDriverIdByMobile(TENANT_ID, null));
+ assertNull(service.resolveDriverIdByMobile(TENANT_ID, " "));
+ assertNull(service.resolveDriverIdByMobile(TENANT_ID, "138****8000"));
+ verify(driverMapper, never()).selectList(any());
+ }
+
+ @Test
+ @DisplayName("租户为空:取当前登录租户,仍不抛异常")
+ void fallsBackToCurrentTenant() {
+ when(driverMapper.selectList(any())).thenReturn(List.of(profile(9001L)));
+
+ Long driverId = null;
+ try (org.mockito.MockedStatic authUtil
+ = org.mockito.Mockito.mockStatic(org.springblade.core.secure.utils.AuthUtil.class)) {
+ authUtil.when(org.springblade.core.secure.utils.AuthUtil::getTenantId).thenReturn(TENANT_ID);
+ driverId = service.resolveDriverIdByMobile(null, MOBILE);
+ }
+ assertEquals(9001L, driverId);
+ }
+
+ private Driver profile(Long id) {
+ Driver profile = new Driver();
+ profile.setId(id);
+ profile.setIsDeleted(0);
+ profile.setTenantId(TENANT_ID);
+ profile.setMobile(MOBILE);
+ return profile;
+ }
+
+ 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);
+ }
+
+}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java
index 35568abb..16b484b9 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java
@@ -105,6 +105,17 @@ class DriverSyncMobileTest {
verify(driverMapper, never()).updateById(any(Driver.class));
}
+ @Test
+ @DisplayName("改号链路不持有运单/配载服务:换号只改档案,不回写单据上的手机号快照")
+ void phoneSyncDoesNotDependOnWaybillServices() {
+ for (Field field : DriverServiceImpl.class.getDeclaredFields()) {
+ String typeName = field.getType().getSimpleName();
+ assertFalse(typeName.contains("Waybill") || typeName.contains("LoadingManage"),
+ "改号链路引入了 " + typeName + ":换号不得顺手回写运单/配载单的司机快照,"
+ + "否则司机档案与历史单据的归属会一起漂移");
+ }
+ }
+
@Test
@DisplayName("档案 mobile 与账号旧号不一致:不动档案,返回可读原因(不抛异常)")
void skipsWhenProfileMobileDiffersFromOldPhone() {
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripAcceptRejectTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripAcceptRejectTest.java
index 05c3af55..cad12c15 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripAcceptRejectTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripAcceptRejectTest.java
@@ -14,7 +14,6 @@ import org.mockito.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
-import org.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -57,7 +56,10 @@ class DriverTripAcceptRejectTest {
private static final Long USER_ID = 7401L;
private static final Long DRIVER_ID = 9001L;
- private static final String PHONE = "17587421208";
+ /** 派单时落在运次上的号码快照。 */
+ private static final String SNAPSHOT_PHONE = "17587421208";
+ /** 司机改号后档案与登录账号上的号码。 */
+ private static final String CURRENT_PHONE = "17687421208";
private static final String TRIP_ID = "2101146192739459074";
private static final Long PENDING_ID = 2101144166395699202L;
private static final Long SECOND_ID = 2101145869819994113L;
@@ -85,7 +87,6 @@ class DriverTripAcceptRejectTest {
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
certificationGate,
@@ -121,7 +122,8 @@ class DriverTripAcceptRejectTest {
"两张子运单与接单司机一起下发:" + capturedUpdateParams());
assertTrue(capturedWhereSql().contains("(id IN ("), capturedWhereSql());
assertEquals(TRIP_ID, capturedQuery().getTripId());
- assertEquals(PHONE, capturedQuery().getDriverPhone(), "归属仍按司机手机号收口");
+ assertEquals(DRIVER_ID, capturedQuery().getDriverId(),
+ "运次查询按司机档案 ID 收口:手机号换了也认得出本人");
}
@Test
@@ -340,6 +342,16 @@ class DriverTripAcceptRejectTest {
verify(waybillService, never()).update(any());
}
+ @Test
+ @DisplayName("没有司机档案的账号不能运次接单:不按手机号回退、不提供代报")
+ void tripAcceptWithoutDriverProfileIsRefused() {
+ when(driverService.getOne(any())).thenReturn(null);
+
+ assertEquals("未找到当前登录司机档案", assertThrows(ServiceException.class,
+ () -> runAsDriver(() -> service.tripAccept(TRIP_ID))).getMessage());
+ verify(waybillService, never()).update(any());
+ }
+
// ------------------------------------------------------------ 夹具
private T runAsDriver(Supplier action) {
@@ -373,7 +385,7 @@ class DriverTripAcceptRejectTest {
private void stubTrip(TripVO.TripWaybillVO[] leading, TripVO.TripWaybillVO... children) {
TripVO trip = new TripVO();
trip.setId(TRIP_ID);
- trip.setDriverPhone(PHONE);
+ trip.setDriverPhone(SNAPSHOT_PHONE);
List all = new java.util.ArrayList<>(List.of(leading));
all.addAll(List.of(children));
trip.setWaybills(all);
@@ -400,7 +412,7 @@ class DriverTripAcceptRejectTest {
driver.setUserId(USER_ID);
driver.setDriverName("黄宇富");
driver.setDrivingVehicle("桂A32523");
- driver.setMobile(PHONE);
+ driver.setMobile(CURRENT_PHONE);
return driver;
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java
index 8ac29d0f..c0034f02 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java
@@ -10,7 +10,6 @@ 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.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -74,7 +73,6 @@ class DriverTripCardTest {
private IWaybillService waybillService;
private IDriverService driverService;
- private IMpUserClient mpUserClient;
private ITripQueryService tripQueryService;
private WaybillEnroutePunchMapper enroutePunchMapper;
private WaybillNodePunchMapper nodePunchMapper;
@@ -94,14 +92,12 @@ class DriverTripCardTest {
void setUp() {
waybillService = mock(IWaybillService.class);
driverService = mock(IDriverService.class);
- mpUserClient = mock(IMpUserClient.class);
tripQueryService = mock(ITripQueryService.class);
enroutePunchMapper = mock(WaybillEnroutePunchMapper.class);
nodePunchMapper = mock(WaybillNodePunchMapper.class);
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mpUserClient,
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
@@ -125,7 +121,7 @@ class DriverTripCardTest {
IPage 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 +330,9 @@ class DriverTripCardTest {
}
@Test
- @DisplayName("收口缺失时不放行:空页、零统计、当前任务为空,且不查运次")
- void missingPhoneIsRefused() {
+ @DisplayName("收口缺失时不放行:没有司机档案就空页、零统计、当前任务为空,且不查运次")
+ void missingDriverProfileIsRefused() {
when(driverService.getOne(any())).thenReturn(null);
- when(mpUserClient.userById(any())).thenReturn(null);
IPage page = runAsDriver(() -> service.tripPage(null, null, null, null));
DriverWaybillPreviewVO preview = runAsDriver(() -> service.tripPendingPreview(null));
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java
index 42b742fe..ab2bd8e5 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java
@@ -11,7 +11,6 @@ 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.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -116,7 +115,6 @@ class DriverWaybillAutoCompleteTest {
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillCardLocationTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillCardLocationTest.java
index 13ace47b..af981add 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillCardLocationTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillCardLocationTest.java
@@ -7,7 +7,6 @@ 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.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -73,7 +72,6 @@ class DriverWaybillCardLocationTest {
service = new DriverWaybillServiceImpl(
waybillService,
mock(IDriverService.class),
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillNodePunchTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillNodePunchTest.java
index f1223d3c..8c4ad4ec 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillNodePunchTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillNodePunchTest.java
@@ -7,7 +7,6 @@ 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.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -55,7 +54,6 @@ class DriverWaybillNodePunchTest {
service = new DriverWaybillServiceImpl(
mock(IWaybillService.class),
mock(IDriverService.class),
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java
index 7f3f6cae..2dc1e234 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java
@@ -12,7 +12,6 @@ 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.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -27,26 +26,34 @@ import org.springblade.transport.support.CertificationGate;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
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.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 司机对运单的归属校验(异常上报等按运单操作的前置条件)。
*
- * 口径:当前用户手机号匹配运单 {@code driverPhone}。
+ * 口径:登录用户按 {@code user_id} 解析到司机档案,运单归属键是运单上的 {@code driver_id};
+ * {@code driver_phone} 只是派单快照,不参与判定。
* 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。
*/
class DriverWaybillOwnershipTest {
private static final Long WAYBILL_ID = 5301L;
private static final Long USER_ID = 7001L;
- private static final String PHONE = "13800138000";
+ private static final Long DRIVER_ID = 9001L;
+ private static final Long OTHER_DRIVER_ID = 9002L;
+ /** 派单时落在运单上的号码快照。 */
+ private static final String SNAPSHOT_PHONE = "13800138000";
+ /** 司机改号后档案与登录账号上的号码。 */
+ private static final String CURRENT_PHONE = "13900139000";
private IDriverService driverService;
private IWaybillService waybillService;
@@ -58,6 +65,7 @@ class DriverWaybillOwnershipTest {
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
assistant.setCurrentNamespace("test");
TableInfoHelper.initTableInfo(assistant, Waybill.class);
+ TableInfoHelper.initTableInfo(assistant, Driver.class);
}
@BeforeEach
@@ -67,7 +75,6 @@ class DriverWaybillOwnershipTest {
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
@@ -91,33 +98,26 @@ class DriverWaybillOwnershipTest {
}
@Test
- @DisplayName("司机档案与登录态都没有手机号时被拒")
- void withoutPhoneIsRejected() {
- Driver driver = driver(null);
- when(driverService.getOne(any())).thenReturn(driver);
+ @DisplayName("司机档案只按登录用户 ID 解析,不按手机号回退")
+ void driverProfileResolvedByUserIdOnly() {
+ when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
+ when(waybillService.getOne(any())).thenReturn(waybill());
- ServiceException exception = assertThrows(ServiceException.class,
- () -> runOwnershipCheck(WAYBILL_ID));
+ assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
- assertEquals("当前用户手机号为空,无法查找运单", exception.getMessage());
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+ verify(driverService, times(1)).getOne(captor.capture());
+ LambdaQueryWrapper wrapper = captor.getValue();
+ assertTrue(wrapper.getTargetSql().contains("user_id"),
+ "司机档案必须按登录用户 ID 解析:" + wrapper.getTargetSql());
+ assertFalse(wrapper.getTargetSql().contains("mobile"),
+ "司机档案不允许再按手机号回退:" + wrapper.getTargetSql());
}
@Test
- @DisplayName("运单司机手机号与当前用户不一致时被拒")
- void waybillOfAnotherPhoneIsRejected() {
- when(driverService.getOne(any())).thenReturn(driver(PHONE));
- when(waybillService.getOne(any())).thenReturn(null);
-
- ServiceException exception = assertThrows(ServiceException.class,
- () -> runOwnershipCheck(WAYBILL_ID));
-
- assertEquals("运单不存在或无权操作", exception.getMessage());
- }
-
- @Test
- @DisplayName("本人手机号运单放行,且查询确实按司机手机号收敛")
- void ownWaybillPasses() {
- when(driverService.getOne(any())).thenReturn(driver(PHONE));
+ @DisplayName("司机改号后本人运单仍放行,归属只认 driver_id")
+ void ownWaybillPassesAfterNumberChange() {
+ when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
when(waybillService.getOne(any())).thenReturn(waybill());
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
@@ -125,17 +125,55 @@ class DriverWaybillOwnershipTest {
ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(waybillService).getOne(captor.capture());
LambdaQueryWrapper wrapper = captor.getValue();
- assertTrue(wrapper.getTargetSql().contains("driver_phone"),
- "归属查询必须落在运单司机手机号上:" + wrapper.getTargetSql());
- assertTrue(wrapper.getParamNameValuePairs().values().stream()
- .anyMatch(PHONE::equals),
- "归属查询必须带上当前用户手机号:" + wrapper.getParamNameValuePairs());
+ // 护栏:归属查询必须落在运单 driver_id 上,不能再出现手机号比较,否则改号即失效。
+ assertTrue(wrapper.getTargetSql().contains("driver_id"),
+ "归属查询必须落在运单司机档案 ID 上:" + wrapper.getTargetSql());
+ assertFalse(wrapper.getTargetSql().contains("driver_phone"),
+ "归属查询不能再比较运单司机手机号快照:" + wrapper.getTargetSql());
+ assertTrue(wrapper.getParamNameValuePairs().values().contains(DRIVER_ID),
+ "归属查询必须带上当前司机档案 ID:" + wrapper.getParamNameValuePairs());
+ assertFalse(wrapper.getParamNameValuePairs().values().contains(SNAPSHOT_PHONE),
+ "快照号不参与归属判定:" + wrapper.getParamNameValuePairs());
+ assertFalse(wrapper.getParamNameValuePairs().values().contains(CURRENT_PHONE),
+ "档案手机号不参与归属判定:" + wrapper.getParamNameValuePairs());
+ }
+
+ @Test
+ @DisplayName("同号换人:同手机号但不同档案看不到对方的运单")
+ void waybillOwnedByAnotherDriverWithSamePhoneIsRejected() {
+ when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
+ // 库里存在该运单,但归属档案是另一个同号档案,按 driver_id 收口后查不到。
+ when(waybillService.getOne(any())).thenReturn(null);
+
+ ServiceException exception = assertThrows(ServiceException.class,
+ () -> runOwnershipCheck(WAYBILL_ID));
+
+ assertEquals("运单不存在或无权操作", exception.getMessage());
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+ verify(waybillService).getOne(captor.capture());
+ LambdaQueryWrapper wrapper = captor.getValue();
+ // 参数槽由 SQL 片段构建时填充:不先取一次片段,参数表是空的
+ wrapper.getTargetSql();
+ assertTrue(wrapper.getParamNameValuePairs().values().contains(DRIVER_ID),
+ "收口条件必须是当前档案 ID:" + wrapper.getParamNameValuePairs());
+ assertFalse(wrapper.getParamNameValuePairs().values().contains(OTHER_DRIVER_ID),
+ "不能把别人的档案 ID 当查询条件:" + wrapper.getParamNameValuePairs());
+ }
+
+ @Test
+ @DisplayName("司机档案没有手机号也能按档案 ID 认领自己的运单")
+ void profileWithoutPhoneStillOwnsByDriverId() {
+ when(driverService.getOne(any())).thenReturn(driver(null));
+ when(waybillService.getOne(any())).thenReturn(waybill());
+
+ assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
}
@Test
@DisplayName("运单ID为空时被拒")
void withoutWaybillIdIsRejected() {
- when(driverService.getOne(any())).thenReturn(driver(PHONE));
+ when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
ServiceException exception = assertThrows(ServiceException.class,
() -> runOwnershipCheck(null));
@@ -148,14 +186,14 @@ class DriverWaybillOwnershipTest {
private void runOwnershipCheck(Long waybillId) {
try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
- authUtil.when(AuthUtil::getUserAccount).thenReturn(PHONE);
+ authUtil.when(AuthUtil::getUserAccount).thenReturn(CURRENT_PHONE);
service.assertCurrentDriverOwnsWaybill(waybillId);
}
}
private Driver driver(String mobile) {
Driver driver = new Driver();
- driver.setId(9001L);
+ driver.setId(DRIVER_ID);
driver.setUserId(USER_ID);
driver.setDriverName("韦司机");
driver.setMobile(mobile);
@@ -166,7 +204,8 @@ class DriverWaybillOwnershipTest {
Waybill waybill = new Waybill();
waybill.setId(WAYBILL_ID);
waybill.setWaybillNo("YD202610070001");
- waybill.setDriverPhone(PHONE);
+ waybill.setDriverId(DRIVER_ID);
+ waybill.setDriverPhone(SNAPSHOT_PHONE);
waybill.setIsDeleted(0);
return waybill;
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java
index 49502666..dc4b349c 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java
@@ -15,7 +15,6 @@ import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
-import org.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -34,6 +33,7 @@ import org.springblade.transport.support.CertificationGate;
import java.util.List;
import java.util.function.Supplier;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -58,7 +58,10 @@ class DriverWaybillRejectDetachTest {
private static final Long USER_ID = 7401L;
private static final Long DRIVER_ID = 9001L;
private static final String PLATE = "沪AE31312";
- private static final String PHONE = "13800138002";
+ /** 派单时落在运单上的号码快照。 */
+ private static final String SNAPSHOT_PHONE = "13800138002";
+ /** 司机改号后档案与登录账号上的号码。 */
+ private static final String CURRENT_PHONE = "13900139002";
private static final Long REJECTED_ID = 7201L;
private IDriverService driverService;
@@ -83,7 +86,6 @@ class DriverWaybillRejectDetachTest {
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
certificationGate,
@@ -155,6 +157,29 @@ class DriverWaybillRejectDetachTest {
assertEquals(2, page.getRecords().size());
}
+ @Test
+ @DisplayName("司机改号后运单仍可见,卡片仍展示派单时的号码快照")
+ void numberChangeKeepsWaybillsVisible() {
+ // 同一个档案:档案与登录账号已换成新号,运单快照还是派单时的旧号
+ when(waybillService.list(ArgumentMatchers.>any()))
+ .thenReturn(List.of(waybill(7202L, "YD-20261001-0002", "pending", "pending")));
+
+ IPage page = runAsDriver(() -> service.page(1, 10, null, null));
+
+ assertEquals(1, page.getTotal(), "改号不影响本人运单的可见性");
+ assertEquals(SNAPSHOT_PHONE, page.getRecords().get(0).getDriverPhone(),
+ "卡片展示的仍是运单上的号码快照,不改成本人新号");
+ }
+
+ @Test
+ @DisplayName("重新派回后运单恢复可见:归属看新的 driver_id,不看历史拒单")
+ void reassignedWaybillBecomesVisibleAgain() {
+ when(waybillService.getOne(any()))
+ .thenReturn(waybill(REJECTED_ID, "YD-20261010-0001", "pending", "pending"));
+
+ assertDoesNotThrow(() -> runAsDriver(() -> service.detail(REJECTED_ID)));
+ }
+
@Test
@DisplayName("已拒单运单的详情不可达:取单条件已把它挡住")
void detailOfRejectedIsUnreachable() {
@@ -218,7 +243,7 @@ class DriverWaybillRejectDetachTest {
driver.setUserId(USER_ID);
driver.setDriverName("黄大富");
driver.setDrivingVehicle(PLATE);
- driver.setMobile(PHONE);
+ driver.setMobile(CURRENT_PHONE);
return driver;
}
@@ -231,7 +256,8 @@ class DriverWaybillRejectDetachTest {
waybill.setId(id);
waybill.setWaybillNo(no);
waybill.setVehicleNo(PLATE);
- waybill.setDriverPhone(PHONE);
+ waybill.setDriverId(DRIVER_ID);
+ waybill.setDriverPhone(SNAPSHOT_PHONE);
waybill.setBusinessStatus(businessStatus);
waybill.setDriverAcceptStatus(acceptStatus);
waybill.setIsDeleted(0);
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java
index 1789c892..029adb95 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java
@@ -12,7 +12,6 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springblade.core.secure.utils.AuthUtil;
-import org.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -35,6 +34,7 @@ import java.util.List;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -51,8 +51,12 @@ class DriverWaybillTabStatusTest {
private static final Long WAYBILL_ID = 5901L;
private static final Long USER_ID = 7001L;
+ private static final Long DRIVER_ID = 9001L;
private static final String PLATE = "桂AAW3256";
- private static final String PHONE = "13800138002";
+ /** 派单时落在运单上的号码快照。 */
+ private static final String SNAPSHOT_PHONE = "13800138002";
+ /** 司机改号后档案与登录账号上的号码。 */
+ private static final String CURRENT_PHONE = "13900139002";
private IWaybillService waybillService;
private IDriverService driverService;
@@ -81,7 +85,6 @@ class DriverWaybillTabStatusTest {
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
- mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
@@ -141,6 +144,27 @@ class DriverWaybillTabStatusTest {
assertEquals(1, counts.getAll());
}
+ @Test
+ @DisplayName("司机改号后三档计数不变,计数查询按 driver_id 收口")
+ void tabCountsAfterNumberChangeScopedByDriverId() {
+ DriverWaybillTabCountsVO counts = runAsDriver(service::tabCounts);
+
+ assertEquals(1, counts.getDoing(), "改号不影响本人运单的分档");
+ assertEquals(1, counts.getAll());
+
+ LambdaQueryWrapper wrapper = firstQueriedWrapper();
+ // 护栏:计数查询必须落在运单 driver_id 上;比对手机号的话,改号后司机的数字会清零
+ String sql = wrapper.getTargetSql();
+ assertTrue(sql.contains("driver_id"), "计数查询必须按司机档案 ID 收口:" + sql);
+ assertFalse(sql.contains("driver_phone"), "计数查询不能再比较运单手机号快照:" + sql);
+ assertTrue(wrapper.getParamNameValuePairs().values().contains(DRIVER_ID),
+ "计数查询必须带上当前司机档案 ID:" + wrapper.getParamNameValuePairs());
+ assertFalse(wrapper.getParamNameValuePairs().values().contains(SNAPSHOT_PHONE),
+ "快照号不参与计数:" + wrapper.getParamNameValuePairs());
+ assertFalse(wrapper.getParamNameValuePairs().values().contains(CURRENT_PHONE),
+ "档案手机号不参与计数:" + wrapper.getParamNameValuePairs());
+ }
+
// ------------------------------------------------------------ 夹具
private T runAsDriver(Supplier action) {
@@ -167,6 +191,15 @@ class DriverWaybillTabStatusTest {
return statuses;
}
+ @SuppressWarnings("unchecked")
+ private LambdaQueryWrapper firstQueriedWrapper() {
+ assertEquals(1, queried.size(), "一次列表查询");
+ LambdaQueryWrapper wrapper = (LambdaQueryWrapper) queried.get(0);
+ // 参数槽由 SQL 片段构建时填充:不先取一次片段,参数表是空的
+ wrapper.getSqlSegment();
+ return wrapper;
+ }
+
@SuppressWarnings("unchecked")
private static Wrapper anyWrapper() {
return org.mockito.ArgumentMatchers.>any();
@@ -177,7 +210,8 @@ class DriverWaybillTabStatusTest {
waybill.setId(WAYBILL_ID);
waybill.setWaybillNo("YD202610080002");
waybill.setVehicleNo(PLATE);
- waybill.setDriverPhone(PHONE);
+ waybill.setDriverId(DRIVER_ID);
+ waybill.setDriverPhone(SNAPSHOT_PHONE);
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
waybill.setIsDeleted(0);
return waybill;
@@ -185,10 +219,10 @@ class DriverWaybillTabStatusTest {
private Driver driver() {
Driver driver = new Driver();
- driver.setId(9001L);
+ driver.setId(DRIVER_ID);
driver.setUserId(USER_ID);
driver.setDrivingVehicle(PLATE);
- driver.setMobile(PHONE);
+ driver.setMobile(CURRENT_PHONE);
return driver;
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java
index a8eccbcd..b71ce064 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java
@@ -19,6 +19,7 @@ import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
+import org.springblade.transport.service.IDriverService;
import java.lang.reflect.Field;
import java.util.List;
@@ -81,6 +82,7 @@ class LoadingManageAdmissionTest {
inject("baseMapper", loadingManageMapper);
inject("waybillMapper", waybillMapper);
inject("contractManageService", contractManageService);
+ inject("driverService", mock(IDriverService.class));
authUtil = mockStatic(AuthUtil.class);
authUtil.when(AuthUtil::getDeptId).thenReturn(String.valueOf(DEPT_ID));
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java
index 10dab615..3213ac95 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java
@@ -1,5 +1,7 @@
package org.springblade.transport.service.impl;
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -15,16 +17,20 @@ import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
+import org.springblade.transport.mapper.LoadingManageMapper;
import org.springblade.transport.mapper.WaybillMapper;
import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.ProcessConfig;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
+import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService;
+import org.springblade.transport.support.LogCapture;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.util.regex.Matcher;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
@@ -32,9 +38,11 @@ import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -60,6 +68,12 @@ class LoadingManageChildStatusWriteTest {
private static final String LOADING_NO = "PZ-20261018-001";
private static final Long LOADING_ID = 1800000000000000001L;
private static final String DRIVER_PHONE = "13800138000";
+ private static final String TENANT_ID = "000000";
+ private static final Long RESOLVED_DRIVER_ID = 9321L;
+ private static final Long MANUAL_DRIVER_ID = 7001L;
+ private static final Long OTHER_PROFILE_ID = 8888L;
+ private static final String OTHER_DRIVER_PHONE = "13900139000";
+ private static final Long SOURCE_LOADING_ID = 1800000000000000002L;
private static final Long NEED_ACCEPT_WAYBILL_ID = 3101L;
private static final Long NO_ACCEPT_WAYBILL_ID = 3102L;
@@ -80,6 +94,7 @@ class LoadingManageChildStatusWriteTest {
"carrier_type",
"carrier_contract_id",
"carrier_name",
+ "driver_id",
"driver_name",
"driver_phone",
"vehicle_no",
@@ -104,6 +119,8 @@ class LoadingManageChildStatusWriteTest {
private IProcessConfigService processConfigService;
private IContractManageService contractManageService;
private IReceivablePayableDetailService receivablePayableDetailService;
+ private IDriverService driverService;
+ private LoadingManageMapper loadingManageMapper;
private LoadingManageServiceImpl service;
private MockedStatic authUtil;
@@ -115,6 +132,7 @@ class LoadingManageChildStatusWriteTest {
assistant.setCurrentNamespace("test");
TableInfoHelper.initTableInfo(assistant, Waybill.class);
TableInfoHelper.initTableInfo(assistant, ProcessConfig.class);
+ TableInfoHelper.initTableInfo(assistant, LoadingManage.class);
}
@BeforeEach
@@ -123,14 +141,19 @@ class LoadingManageChildStatusWriteTest {
processConfigService = mock(IProcessConfigService.class);
contractManageService = mock(IContractManageService.class);
receivablePayableDetailService = mock(IReceivablePayableDetailService.class);
+ driverService = mock(IDriverService.class);
+ loadingManageMapper = mock(LoadingManageMapper.class);
service = spy(new LoadingManageServiceImpl());
inject(service, "waybillMapper", waybillMapper);
inject(service, "processConfigService", processConfigService);
inject(service, "contractManageService", contractManageService);
inject(service, "receivablePayableDetailService", receivablePayableDetailService);
+ inject(service, "driverService", driverService);
+ inject(service, "baseMapper", loadingManageMapper);
doReturnTrue(service);
authUtil = mockStatic(AuthUtil.class);
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
+ authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID);
}
@AfterEach
@@ -309,6 +332,179 @@ class LoadingManageChildStatusWriteTest {
verify(processConfigService, never()).list(ArgumentMatchers.>any());
}
+ @Test
+ @DisplayName("提交配载单:按手机号解析到档案 ID 时,配载单与子单一起记住归属")
+ void submitWritesResolvedDriverId() {
+ stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
+ stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
+
+ assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
+
+ assertEquals(RESOLVED_DRIVER_ID, capturedLoading().getDriverId());
+ assertEquals(RESOLVED_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"),
+ "子单必须与配载单写同一个归属,否则司机端按归属查不到这张票");
+ }
+
+ @Test
+ @DisplayName("解析不到档案(承运商司机):提交照常成功,归属留空")
+ void submitAllowsUnresolvedDriverId() {
+ stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
+ stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
+
+ assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
+
+ assertNull(capturedLoading().getDriverId());
+ assertNull(statusSyncUpdates().get(0).setValue("driver_id"));
+ }
+
+ @Test
+ @DisplayName("进行中编辑保留的子单:任务字段更新也带上司机归属,改派后不留旧值")
+ void runningEditCarriesDriverIdToKeptWaybills() {
+ LoadingManage record = runningRecord();
+ stubLoadingRecord(record);
+ stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
+
+ assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
+
+ CapturedUpdate taskUpdate = waybillUpdates().stream()
+ .filter(update -> update.columns().contains("driver_name"))
+ .findFirst()
+ .orElseThrow();
+ assertEquals(List.of(NEED_ACCEPT_WAYBILL_ID), taskUpdate.whereIds());
+ assertEquals(RESOLVED_DRIVER_ID, taskUpdate.setValue("driver_id"));
+ }
+
+ @Test
+ @DisplayName("复制配载单:沿用源单的司机归属,不按手机号重解析覆盖")
+ void copyKeepsSourceDriverId() {
+ LoadingManage source = copySource();
+ source.setDriverId(555L);
+ stubCopyDependencies(source);
+
+ service.copy(SOURCE_LOADING_ID);
+
+ assertEquals(555L, capturedSaved().getDriverId());
+ }
+
+ @Test
+ @DisplayName("复制配载单:源单没有归属时,用手机号解析出的档案 ID")
+ void copyFallsBackToResolvedDriverId() {
+ LoadingManage source = copySource();
+ stubCopyDependencies(source);
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
+
+ service.copy(SOURCE_LOADING_ID);
+
+ assertEquals(RESOLVED_DRIVER_ID, capturedSaved().getDriverId());
+ }
+
+ @Test
+ @DisplayName("已归属的单子号码没变:无关字段的保存不重解析,归属原样保留")
+ void unrelatedEditKeepsStoredDriverId() {
+ LoadingManage record = runningRecord();
+ record.setDriverId(RESOLVED_DRIVER_ID);
+ stubLoadingRecord(record);
+ stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
+ // 这一次谁都解析不到:只要实现还按手机号重解析,归属就会被清空
+ when(driverService.resolveDriverIdByMobile(any(), any())).thenReturn(null);
+
+ assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
+
+ assertEquals(RESOLVED_DRIVER_ID, capturedLoading().getDriverId());
+ assertEquals(RESOLVED_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"));
+ verify(driverService, never()).resolveDriverIdByMobile(any(), any());
+ }
+
+ @Test
+ @DisplayName("人工补过归属的单子:号码恰好命中别人的档案,保存也不改归属")
+ void unrelatedEditKeepsManualOwnership() {
+ LoadingManage record = runningRecord();
+ record.setDriverId(MANUAL_DRIVER_ID);
+ stubLoadingRecord(record);
+ stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
+ when(driverService.resolveDriverIdByMobile(any(), any())).thenReturn(OTHER_PROFILE_ID);
+
+ assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
+
+ assertEquals(MANUAL_DRIVER_ID, capturedLoading().getDriverId(),
+ "人工归属不能被一次无关保存改到同号的另一个档案上");
+ assertEquals(MANUAL_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"));
+ }
+
+ @Test
+ @DisplayName("-1 哨兵的归属:号码没变的保存原样保留,不抹成 NULL")
+ void sentinelDriverIdSurvivesUnrelatedEdit() {
+ LoadingManage record = runningRecord();
+ record.setDriverId(-1L);
+ stubLoadingRecord(record);
+ stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
+ when(driverService.resolveDriverIdByMobile(any(), any())).thenReturn(null);
+
+ assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
+
+ assertEquals(-1L, capturedLoading().getDriverId());
+ }
+
+ @Test
+ @DisplayName("换司机:号码变了就按新号码重新解析,归属跟着换")
+ void changingPhoneResolvesNewDriverId() {
+ LoadingManage record = pendingRecord();
+ record.setDriverId(RESOLVED_DRIVER_ID);
+ stubLoadingRecord(record);
+ stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
+ stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
+ when(driverService.resolveDriverIdByMobile(any(), eq(OTHER_DRIVER_PHONE))).thenReturn(OTHER_PROFILE_ID);
+ LoadingManage form = editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID);
+ form.setDriverPhone(OTHER_DRIVER_PHONE);
+
+ assertTrue(service.submit(form));
+
+ assertEquals(OTHER_PROFILE_ID, capturedLoading().getDriverId());
+ assertEquals(OTHER_PROFILE_ID, statusSyncUpdates().get(0).setValue("driver_id"));
+ }
+
+ @Test
+ @DisplayName("号码改成一个解析不到的号:归属落空,子单同步落空")
+ void changingPhoneToUnresolvedNumberClearsDriverId() {
+ LoadingManage record = pendingRecord();
+ record.setDriverId(RESOLVED_DRIVER_ID);
+ stubLoadingRecord(record);
+ stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
+ stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
+ when(driverService.resolveDriverIdByMobile(any(), eq(OTHER_DRIVER_PHONE))).thenReturn(null);
+ LoadingManage form = editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID);
+ form.setDriverPhone(OTHER_DRIVER_PHONE);
+
+ assertTrue(service.submit(form));
+
+ assertNull(capturedLoading().getDriverId());
+ assertNull(statusSyncUpdates().get(0).setValue("driver_id"));
+ }
+
+ @Test
+ @DisplayName("解析不到档案:留一条脱敏 WARN,保存照常成功")
+ void unresolvedPhoneLogsMaskedWarning() {
+ stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
+ stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
+ try (LogCapture logs = LogCapture.open(LoadingManageServiceImpl.class)) {
+ assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
+
+ String warning = logs.events().stream()
+ .filter(event -> event.getLevel() == Level.WARN)
+ .map(ILoggingEvent::getFormattedMessage)
+ .filter(message -> message.contains("司机档案"))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("解析失败必须留 WARN,实际日志:" + logs.messages()));
+ assertTrue(warning.contains("138****8000"), "WARN 里的号码必须脱敏,实际:" + warning);
+ assertFalse(warning.contains(DRIVER_PHONE), "WARN 里不能出现完整号码,实际:" + warning);
+ assertTrue(warning.contains(LOADING_NO), "WARN 要带业务标识(配载单号),实际:" + warning);
+ }
+ }
+
// ------------------------------------------------------------ 夹具
private LoadingManage loadingForm(Long... waybillIds) {
@@ -375,10 +571,54 @@ class LoadingManageChildStatusWriteTest {
return waybill;
}
- private String capturedLoadingStatus() {
+ private LoadingManage runningRecord() {
+ LoadingManage record = loadingRecord("running", NEED_ACCEPT_WAYBILL_ID);
+ record.setVehicleNo("桂A12345");
+ record.setDriverName("张三");
+ record.setDriverPhone(DRIVER_PHONE);
+ return record;
+ }
+
+ private LoadingManage pendingRecord() {
+ LoadingManage record = loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID);
+ record.setVehicleNo("桂A12345");
+ record.setDriverName("张三");
+ record.setDriverPhone(DRIVER_PHONE);
+ return record;
+ }
+
+ private LoadingManage copySource() {
+ LoadingManage source = loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID);
+ source.setLoadingSubNos("YD-" + NEED_ACCEPT_WAYBILL_ID);
+ source.setTransportType("公路运输");
+ source.setVehicleNo("桂A12345");
+ source.setDriverName("张三");
+ source.setDriverPhone(DRIVER_PHONE);
+ source.setDepartureAddress("南宁");
+ source.setArrivalAddress("广州");
+ return source;
+ }
+
+ private void stubCopyDependencies(LoadingManage source) {
+ source.setId(SOURCE_LOADING_ID);
+ doReturn(source).when(service).getById(SOURCE_LOADING_ID);
+ doReturn(null).when(service).detail(any());
+ }
+
+ private LoadingManage capturedSaved() {
+ ArgumentCaptor captor = ArgumentCaptor.forClass(LoadingManage.class);
+ verify(service, atLeastOnce()).save(captor.capture());
+ return captor.getValue();
+ }
+
+ private LoadingManage capturedLoading() {
ArgumentCaptor captor = ArgumentCaptor.forClass(LoadingManage.class);
verify(service, atLeastOnce()).saveOrUpdate(captor.capture());
- return captor.getValue().getBusinessStatus();
+ return captor.getValue();
+ }
+
+ private String capturedLoadingStatus() {
+ return capturedLoading().getBusinessStatus();
}
private void stubWaybills(Waybill... waybills) {
@@ -438,18 +678,28 @@ class LoadingManageChildStatusWriteTest {
}
private static void inject(LoadingManageServiceImpl target, String fieldName, Object value) {
- try {
- Field field = LoadingManageServiceImpl.class.getDeclaredField(fieldName);
- field.setAccessible(true);
- field.set(target, value);
- } catch (ReflectiveOperationException exception) {
- throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
+ Class> type = LoadingManageServiceImpl.class;
+ while (type != null) {
+ try {
+ Field field = type.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ return;
+ } catch (NoSuchFieldException ignored) {
+ type = type.getSuperclass();
+ } catch (ReflectiveOperationException exception) {
+ throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
+ }
}
+ throw new IllegalStateException("注入测试替身失败:" + fieldName);
}
/** 一条被捕获的批量更新:SET 的列 → 值、WHERE 的运单 id */
private record CapturedUpdate(String sqlSet, String whereSql, Map params, List columns) {
+ private static final java.util.regex.Pattern PARAM_REF =
+ java.util.regex.Pattern.compile("#\\{ew\\.paramNameValuePairs\\.([^}]+)}");
+
@SuppressWarnings("unchecked")
static CapturedUpdate of(Wrapper wrapper) {
LambdaUpdateWrapper update = (LambdaUpdateWrapper) wrapper;
@@ -478,8 +728,20 @@ class LoadingManageChildStatusWriteTest {
return null;
}
+ /** 只认 WHERE 段用到的参数:SET 段的 driver_id 也是 Long,混进来会当成运单 id */
List whereIds() {
- return params.values().stream().filter(Long.class::isInstance).map(Long.class::cast).toList();
+ Set setParamKeys = new java.util.HashSet<>();
+ Matcher matcher = PARAM_REF.matcher(sqlSet);
+ while (matcher.find()) {
+ setParamKeys.add(matcher.group(1));
+ }
+ List ids = new java.util.ArrayList<>();
+ for (Map.Entry entry : params.entrySet()) {
+ if (entry.getValue() instanceof Long longValue && !setParamKeys.contains(entry.getKey())) {
+ ids.add(longValue);
+ }
+ }
+ return ids;
}
}
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java
new file mode 100644
index 00000000..c9290709
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java
@@ -0,0 +1,143 @@
+package org.springblade.transport.service.impl;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springblade.transport.pojo.entity.MasterOrder;
+import org.springblade.transport.pojo.entity.Waybill;
+import org.springblade.transport.service.IContractManageService;
+import org.springblade.transport.service.IDriverService;
+import org.springblade.transport.service.IProjectApplyService;
+import org.springblade.transport.service.ITransportPlanService;
+import org.springblade.transport.service.IWaybillService;
+import org.springblade.transport.support.LogCapture;
+
+import java.lang.reflect.Method;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 多联总单派单建运单时写司机档案 ID 的测试。
+ *
+ * 派单请求可能来自调度(带档案 ID),也可能只填了姓名手机号(手工派单、承运商司机),
+ * 后者按手机号解析;解析不到也不能挡住派单。
+ */
+class MasterOrderDriverIdTest {
+
+ private static final String DRIVER_PHONE = "13800138000";
+ private static final Long RESOLVED_DRIVER_ID = 9321L;
+
+ private IWaybillService waybillService;
+ private IDriverService driverService;
+ private MasterOrderServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ waybillService = mock(IWaybillService.class);
+ driverService = mock(IDriverService.class);
+ service = new MasterOrderServiceImpl(waybillService, mock(ITransportPlanService.class),
+ mock(IProjectApplyService.class), mock(IContractManageService.class), driverService);
+ when(waybillService.submit(any(Waybill.class))).thenReturn(true);
+ }
+
+ @Test
+ @DisplayName("调度带了档案 ID:按它写,不再按手机号解析")
+ void keepsExplicitDriverId() throws Exception {
+ Map dispatch = dispatch();
+ dispatch.put("driverId", 7001L);
+
+ assertTrue(createWaybill(dispatch));
+
+ assertEquals(7001L, submittedWaybill().getDriverId());
+ verify(driverService, never()).resolveDriverIdByMobile(any(), any());
+ }
+
+ @Test
+ @DisplayName("只填了手机号:派单时解析出档案 ID 写入运单")
+ void resolvesDriverIdFromPhone() throws Exception {
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
+
+ assertTrue(createWaybill(dispatch()));
+
+ assertEquals(RESOLVED_DRIVER_ID, submittedWaybill().getDriverId());
+ }
+
+ @Test
+ @DisplayName("解析不到档案(承运商司机):运单照常提交,归属留空")
+ void allowsDispatchWithoutProfile() throws Exception {
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
+
+ assertTrue(createWaybill(dispatch()));
+
+ assertNull(submittedWaybill().getDriverId());
+ verify(waybillService).submit(any(Waybill.class));
+ }
+
+ @Test
+ @DisplayName("解析不到档案:落空时留一条脱敏 WARN,派单照常成功")
+ void unresolvedProfileLogsMaskedWarning() throws Exception {
+ when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
+ try (LogCapture logs = LogCapture.open(MasterOrderServiceImpl.class)) {
+ assertTrue(createWaybill(dispatch()));
+
+ String warning = logs.events().stream()
+ .filter(event -> event.getLevel() == Level.WARN)
+ .map(ILoggingEvent::getFormattedMessage)
+ .filter(message -> message.contains("司机档案"))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("解析失败必须留 WARN,实际日志:" + logs.messages()));
+ assertTrue(warning.contains("138****8000"), "WARN 里的号码必须脱敏,实际:" + warning);
+ assertFalse(warning.contains(DRIVER_PHONE), "WARN 里不能出现完整号码,实际:" + warning);
+ assertTrue(warning.contains("ZD-20261027-001"), "WARN 要带业务标识(总单号),实际:" + warning);
+ }
+ }
+
+ // ------------------------------------------------------------ 夹具
+
+ private boolean createWaybill(Map dispatch) throws Exception {
+ MasterOrder masterOrder = new MasterOrder();
+ masterOrder.setId(1L);
+ masterOrder.setMasterNo("ZD-20261027-001");
+ masterOrder.setProjectId(2101L);
+ masterOrder.setProjectName("测试项目");
+ Method method = MasterOrderServiceImpl.class
+ .getDeclaredMethod("createWaybill", MasterOrder.class, List.class);
+ method.setAccessible(true);
+ return (boolean) method.invoke(service, masterOrder, List.of(dispatch));
+ }
+
+ private Map dispatch() {
+ Map dispatch = new LinkedHashMap<>();
+ dispatch.put("documentType", "运单");
+ dispatch.put("transportType", "公路运输");
+ dispatch.put("carrierType", "自运");
+ dispatch.put("driverName", "张三");
+ dispatch.put("driverPhone", DRIVER_PHONE);
+ dispatch.put("vehicleNo", "桂A12345");
+ dispatch.put("quantity", 1);
+ dispatch.put("unitPrice", 100);
+ return dispatch;
+ }
+
+ private Waybill submittedWaybill() {
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Waybill.class);
+ verify(waybillService).submit(captor.capture());
+ return captor.getValue();
+ }
+
+}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java
index f7d77747..aa52fec8 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java
@@ -14,6 +14,7 @@ import org.springblade.transport.pojo.entity.TransportPlan;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IContractManageService;
+import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
@@ -67,7 +68,8 @@ class MasterOrderProgressTest {
waybillService,
transportPlanService,
mock(IProjectApplyService.class),
- mock(IContractManageService.class));
+ mock(IContractManageService.class),
+ mock(IDriverService.class));
inject("baseMapper", masterOrderMapper);
when(transportPlanService.list(anyWrapper())).thenReturn(List.of());
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java
index d717921a..b12f1624 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java
@@ -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