🐛 修车辆重新认证长期有效字段回显,有效期不再要求起期

- 卡片接口不再把「长期」当日期下发,也不在长期有效时下发上一版证件残留的止期:
  两者都会被「重新认证」表单原样回填再原样提交,前者进日期框后按 LocalDate 反序列化直接报错,
  后者会让一辆长期有效的车带着一个早已作废的止期一路写回车辆行。
- 车辆有效期校验收敛为「止期必填、起期可空」,和管理端车辆表单同一口径:
  起期只在两端都有值时才比较先后。司机端表单本来也不采集起期。
- 补 6 个测试:3 个盯卡片回显(含长期有效 + 残留止期、无止期两种),
  3 个盯有效期校验(起期可空、止期必填、长期有效时止期可空)。
This commit is contained in:
2026-10-03 17:46:53 +08:00
parent 04a7846328
commit 11e62c172c
4 changed files with 208 additions and 13 deletions
@@ -155,20 +155,27 @@ public class DriverAppServiceImpl implements IDriverAppService {
}
/**
* 日期字段的展示值:长期有效显示「长期」,没填显示空串,其余显示日期
* 日期字段的下发值:只下发 {@code yyyy-MM-dd} 或空串,**从不下发「长期」这类文字**
* <p>
* 卡片是「重新认证」表单的回填来源,空值必须是空串而不是「null」这类字符串,
* 否则用户会在日期框里看到一个字面量 null。
* 卡片是「重新认证」表单的回填来源,值会直接进日期框,所以两条约束必须守住:
* <ul>
* <li>长期有效时下发**空串**:长期与否由同名字段 {@code xxxLongTerm} 表达,
* 界面上显示「长期」是前端的事。把「长期」当日期下发,用户关掉长期有效开关后
* 日期框里会留着一个字面量「长期」,提交时后端按 {@code LocalDate} 反序列化直接报错。</li>
* <li>长期有效时**不下发止期**:止期在「长期」语义下已作废(管理端勾长期有效时也会清掉它),
* 下发旧值会被表单原样回传、审核通过后又写回车辆行,形成「长期有效 + 一个过期止期」
* 的脏数据——车辆行上一次长期有效的提交正是这样留下 2040-08-14 的。</li>
* </ul>
*
* @param date 日期
* @param longTerm 长期有效标志:1 表示长期
* @return 展示文本
* @return {@code yyyy-MM-dd} 或空串
*/
private String dateText(LocalDate date, Integer longTerm) {
if (date != null) {
return date.toString();
if (Integer.valueOf(1).equals(longTerm)) {
return "";
}
return Integer.valueOf(1).equals(longTerm) ? "长期" : "";
return date == null ? "" : date.toString();
}
/**
@@ -161,23 +161,27 @@ public class DriverCertServiceImpl implements IDriverCertService {
}
/**
* 有效期校验:未勾「长期有效」时起止必填,且止不得早于起
* 有效期校验:未勾「长期有效」时**止期**必填;勾了长期有效则止期也不要求
* <p>
* 起期不再是必填:司机端表单不采集它(管理端车辆表单同样只填止期),
* 存量车辆档案里起期列全是空的,强制必填会让每一次重新认证都卡在提交前。
* 起止先后只在**两端都有值**时才比较——只有止期时没有可比的对象。
*
* @param start 有效期起
* @param start 有效期起,可为空
* @param end 有效期止
* @param longTerm 长期有效标志:1 表示长期
* @param emptyText 缺值提示
* @param rangeText 区间颠倒提示
* @param emptyText 缺止期时的提示
* @param rangeText 区间颠倒时的提示
*/
private void requireDateRange(LocalDate start, LocalDate end, Integer longTerm,
String emptyText, String rangeText) {
if (Integer.valueOf(1).equals(longTerm)) {
return;
}
if (start == null || end == null) {
if (end == null) {
throw new ServiceException(emptyText);
}
if (end.isBefore(start)) {
if (start != null && end.isBefore(start)) {
throw new ServiceException(rangeText);
}
}
@@ -0,0 +1,125 @@
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.secure.utils.AuthUtil;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.system.feign.IUserClient;
import java.time.LocalDate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
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>
* 卡片字段会**直接进表单**,所以这里盯的不是「显示得好不好看」,而是两条会让回显出错的契约:
* 日期字段只能是 {@code yyyy-MM-dd} 或空串(不能是「长期」这类文字);
* 标了长期有效的字段不能再下发一个作废的止期。
*/
class DriverAppServiceImplTest {
private static final String MOBILE = "15577122721";
private static final Long USER_ID = 7001L;
private IDriverService driverService;
private ITransportVehicleService transportVehicleService;
private DriverAppServiceImpl service;
@BeforeEach
void setUp() {
driverService = mock(IDriverService.class);
transportVehicleService = mock(ITransportVehicleService.class);
service = new DriverAppServiceImpl(driverService, transportVehicleService, mock(IUserClient.class));
}
@Test
@DisplayName("长期有效且没有止期时下发空串,不能下发「长期」——否则它会被当日期填进日期框")
void longTermWithoutEndDateSendsEmptyText() {
DriverVehicleCardVO card = firstCard(vehicle(LocalDate.of(2022, 8, 17), null, 1));
assertEquals("", card.getDrivingLicenseEndDate(), "长期有效时日期字段只能是空串");
assertEquals(1, card.getDrivingLicenseLongTerm(), "长期有效标志本身要照常下发");
assertEquals("2022-08-17", card.getDrivingLicenseStartDate(), "起期有值时照常下发");
}
@Test
@DisplayName("长期有效时即使库里还留着止期也不下发:那个止期是上一版证件作废的值")
void longTermWithStaleEndDateSendsEmptyText() {
DriverVehicleCardVO card = firstCard(vehicle(null, LocalDate.of(2040, 8, 14), 1));
assertEquals("", card.getDrivingLicenseEndDate());
assertEquals("", card.getRoadTransportCertEndDate());
assertEquals("", card.getCompulsoryScrapDate());
}
@Test
@DisplayName("未标长期有效时照常下发止期,空值一律是空串而不是 null 字面量")
void normalVehicleSendsRealDates() {
TransportVehicle vehicle = vehicle(null, LocalDate.of(2030, 8, 14), 0);
vehicle.setCompulsoryScrapDate(LocalDate.of(2035, 1, 1));
vehicle.setRoadTransportCertEndDate(null);
vehicle.setRoadTransportCertLongTerm(0);
vehicle.setRoadTransportCertStartDate(LocalDate.of(2028, 12, 4));
DriverVehicleCardVO card = firstCard(vehicle);
assertEquals("2030-08-14", card.getDrivingLicenseEndDate());
assertEquals("2035-01-01", card.getCompulsoryScrapDate());
assertEquals("", card.getRoadTransportCertEndDate(), "没填的止期是空串");
assertEquals("2028-12-04", card.getRoadTransportCertStartDate());
}
// ---------------------------------------------------------------- 辅助
/** 造一辆只关心有效期三个字段的车 */
private TransportVehicle vehicle(LocalDate licenseStart, LocalDate licenseEnd, Integer licenseLongTerm) {
TransportVehicle vehicle = new TransportVehicle();
vehicle.setId(8001L);
vehicle.setPlateNo("桂AAW3255");
vehicle.setDrivingLicenseStartDate(licenseStart);
vehicle.setDrivingLicenseEndDate(licenseEnd);
vehicle.setDrivingLicenseLongTerm(licenseLongTerm);
// 三组有效期都按同一个长期有效标志造,便于一次断言三处
vehicle.setCompulsoryScrapDate(null);
vehicle.setCompulsoryScrapLongTerm(licenseLongTerm);
vehicle.setRoadTransportCertStartDate(LocalDate.of(2028, 12, 3));
vehicle.setRoadTransportCertEndDate(LocalDate.of(2043, 8, 18));
vehicle.setRoadTransportCertLongTerm(licenseLongTerm);
return vehicle;
}
/** 按当前登录司机的手机号跑一次「我的车辆」,取第一张卡片 */
private DriverVehicleCardVO firstCard(TransportVehicle vehicle) {
Driver driver = new Driver();
driver.setId(9001L);
driver.setUserId(USER_ID);
driver.setDriverName("韦司机");
driver.setMobile(MOBILE);
driver.setDrivingVehicle(vehicle.getPlateNo());
when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(driver);
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of(vehicle));
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);
}
}
}
@@ -519,6 +519,65 @@ class DriverCertServiceImplTest {
assertTrue(exception.getMessage().contains("所属组织"));
}
@Test
@DisplayName("有效期只填止期:起期为空也允许提交(司机端不采集起期)")
void submitVehicleAcceptsBlankStartDate() {
Driver driver = driver(null);
when(driverAppService.currentDriverEntity()).thenReturn(driver);
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of());
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setDrivingLicenseStartDate(null);
request.setRoadTransportCertStartDate(null);
service.submitVehicleCert(request);
ArgumentCaptor<TransportVehicle> vehicleCaptor = ArgumentCaptor.forClass(TransportVehicle.class);
verify(transportVehicleService).save(vehicleCaptor.capture());
assertNotNull(vehicleCaptor.getValue().getDrivingLicenseEndDate(), "止期照常写入车辆行");
assertNull(vehicleCaptor.getValue().getDrivingLicenseStartDate(), "起期为空就是空,不该编一个值出来");
}
@Test
@DisplayName("有效期未勾长期有效时止期必填:起期可以空,止期不能")
void submitVehicleRejectsBlankEndDate() {
Driver driver = driver(null);
when(driverAppService.currentDriverEntity()).thenReturn(driver);
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of());
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setDrivingLicenseStartDate(null);
request.setDrivingLicenseEndDate(null);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.submitVehicleCert(request));
assertEquals("请选择行驶证有效期", exception.getMessage());
verify(transportVehicleService, never()).save(any(TransportVehicle.class));
}
@Test
@DisplayName("长期有效时止期可以为空,此时不再比较起止先后")
void submitVehicleAllowsLongTermWithoutEndDate() {
Driver driver = driver(null);
when(driverAppService.currentDriverEntity()).thenReturn(driver);
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of());
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setDrivingLicenseStartDate(LocalDate.of(2020, 1, 1));
request.setDrivingLicenseEndDate(null);
request.setDrivingLicenseLongTerm(1);
service.submitVehicleCert(request);
ArgumentCaptor<TransportCertRecord> captor = ArgumentCaptor.forClass(TransportCertRecord.class);
verify(certRecordService).save(captor.capture());
assertTrue(captor.getValue().getSnapshotJson().contains("\"drivingLicenseLongTerm\":1"),
"长期有效标志要落进快照");
}
/**
* 一份字段齐全的车辆认证提交内容
* <p>