🔀 合并票 08:司机端档案解析收口到 user_id
This commit is contained in:
+3
-2
@@ -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<DriverVO> mine(@RequestParam(required = false) String mobile) {
|
||||
return R.data(driverAppService.currentByPhone(mobile));
|
||||
return R.data(driverAppService.current());
|
||||
}
|
||||
|
||||
@GetMapping("/vehicles")
|
||||
|
||||
+5
-6
@@ -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 {
|
||||
* 当前登录用户对应的司机**实体**
|
||||
* <p>
|
||||
* 认证提交要写回司机表,且越权校验要读 {@code driving_vehicle},
|
||||
* 故这里返回实体而非视图对象。解析顺序:先按登录手机号匹配,
|
||||
* 匹配不到再按 {@code user_id} 兜底。
|
||||
* 故这里返回实体而非视图对象。解析口径与 {@link #current()} 一致。
|
||||
*
|
||||
* @return 司机实体;未登录或查不到时返回 null
|
||||
* @return 司机实体;未登录抛异常,查不到返回 null
|
||||
*/
|
||||
Driver currentDriverEntity();
|
||||
|
||||
|
||||
+14
-72
@@ -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<DriverVehicleCardVO> 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} 查档案。
|
||||
* <p>
|
||||
* 手机号不能用来认人:同一号码可能存在多条档案,按号码查「最近更新的一条」
|
||||
* 取到的可能是别人的档案;号码本身也会被改。
|
||||
*/
|
||||
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.<Driver>lambdaQuery()
|
||||
.eq(Driver::getMobile, phone)
|
||||
.orderByDesc(Driver::getUpdateTime)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
if (driver == null) {
|
||||
driver = driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getUserId, userId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
return driver;
|
||||
return driverService.getOne(Wrappers.<Driver>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<MpUser> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是「可作为司机档案手机号使用」的号码串。
|
||||
* <p>
|
||||
* 只判 1 开头的 11~15 位纯数字:库里既有 11 位真实手机号,也有 12 位账号
|
||||
* (如 175874212082),写死 11 位会让这类账号的司机解析不出本人号码。
|
||||
*/
|
||||
private boolean isMobileLike(String value) {
|
||||
return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+69
-56
@@ -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;
|
||||
|
||||
/**
|
||||
* 司机端车辆卡片(重新认证表单的数据来源)测试。
|
||||
* 司机端「当前司机」解析与车辆卡片(重新认证表单的数据来源)测试。
|
||||
* <p>
|
||||
* 卡片字段会**直接进表单**,所以这里盯的不是「显示得好不好看」,而是两条会让回显出错的契约:
|
||||
* 解析侧盯住 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<Driver> wrapper = invocation.getArgument(0);
|
||||
String sqlSegment = wrapper.getSqlSegment();
|
||||
if (sqlSegment.contains("mobile")) {
|
||||
return impostor;
|
||||
}
|
||||
return sqlSegment.contains("user_id") ? owner : null;
|
||||
});
|
||||
|
||||
try (MockedStatic<AuthUtil> 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> 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> 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> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
// 账号本身就是手机号,resolvePhone 走这条短路分支,不再回调用户中心
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(MOBILE);
|
||||
List<DriverVehicleCardVO> 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> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(account);
|
||||
return scoped.currentByPhone(mobileHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user