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/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/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/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); - } - } }