✨ 司机个人认证新增出生年月与住址字段

个人认证快照(PersonCertSubmitRequest)增加 birthday / addressRegion / address,
审核通过时写回司机档案的 birthday / address_region / address 三列。

住址拆成两段是与管理端对齐:blade_transport_driver 有 address_region(地址区划)
与 address(详细地址)两列,管理端的 address_region 存的是各级区划名称直接拼接的
串(如「广西壮族自治区柳州市柳北区」),不是编码,这里沿用同一形状,两端才互相认得。

写回时区划与详细地址都过 blankToNull:框架的 not_null 更新策略只跳过 null,
司机端这两项都是选填,没填时提交的是空串,直接写会把管理端已填的住址清空。

OCR 身份证别名补上「出生」「住址」;birthday 拼写不以 Date 结尾,还要一并纳入
isDateField,否则「1979年11月17日」会原样下发,填不进日期框。
This commit is contained in:
2026-10-17 12:38:02 +08:00
parent 584d03508d
commit 60e2afe938
5 changed files with 55 additions and 1 deletions
@@ -51,6 +51,12 @@ public class PersonCertSubmitRequest implements Serializable {
private String realName;
@Schema(description = "身份证号")
private String idCardNo;
@Schema(description = "出生年月")
private LocalDate birthday;
@Schema(description = "住址-区划:省市区名称拼接串")
private String addressRegion;
@Schema(description = "住址-详细地址")
private String address;
@Schema(description = "身份证正面照")
private String idCardFrontUrl;
@Schema(description = "身份证反面照")
@@ -354,6 +354,10 @@ public class CertAuditServiceImpl implements ICertAuditService {
requireIdCardNotTaken(snapshot.getIdCardNo(), driver.getId());
driver.setDriverName(snapshot.getRealName());
driver.setIdCardNo(snapshot.getIdCardNo());
driver.setBirthday(snapshot.getBirthday());
// 空串不是「没填」:框架的 not_null 策略只跳过 null,写空串会把管理端填过的住址清掉
driver.setAddressRegion(blankToNull(snapshot.getAddressRegion()));
driver.setAddress(blankToNull(snapshot.getAddress()));
driver.setIdCardFront(snapshot.getIdCardFrontUrl());
driver.setIdCardBack(snapshot.getIdCardBackUrl());
driver.setIdCardValidStart(snapshot.getIdCardValidStart());
@@ -279,6 +279,10 @@ public final class OcrFieldSupport {
Map<String, List<String>> aliases = new LinkedHashMap<>();
aliases.put("realName", List.of("姓名"));
aliases.put("idCardNo", List.of("公民身份号码", "公民身份证号码", "身份证号", "身份证号码"));
// 身份证正面的「出生」是一整串日期(1990年1月1日),按 birthday 归一成 yyyy-MM-dd;
// 住址是身份证上的登记住址,写进 driver.address
aliases.put("birthday", List.of("出生", "出生年月", "出生日期"));
aliases.put("address", List.of("住址", "地址"));
aliases.put("idCardValidStart", List.of("签发日期", "有效起始日期", "有效期限起"));
aliases.put("idCardValidEnd", List.of("失效日期", "有效期限", "有效期限止"));
return aliases;
@@ -319,7 +323,8 @@ public final class OcrFieldSupport {
}
private static boolean isDateField(String key) {
return key.endsWith("Date") || key.endsWith("ValidStart") || key.endsWith("ValidEnd");
// birthday 的语义是日期但拼写不以 Date 结尾,漏了它会把「1990年1月1日」原样下发
return "birthday".equals(key) || key.endsWith("Date") || key.endsWith("ValidStart") || key.endsWith("ValidEnd");
}
private static boolean isNumberField(String key) {
@@ -18,6 +18,7 @@ import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ITransportCertRecordService;
import org.springblade.transport.service.ITransportVehicleService;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
@@ -239,6 +240,15 @@ class PersonCertRoundTripTest {
if (patch.getIdCardNo() != null) {
target.setIdCardNo(patch.getIdCardNo());
}
if (patch.getBirthday() != null) {
target.setBirthday(patch.getBirthday());
}
if (patch.getAddressRegion() != null) {
target.setAddressRegion(patch.getAddressRegion());
}
if (patch.getAddress() != null) {
target.setAddress(patch.getAddress());
}
if (patch.getIdCardFront() != null) {
target.setIdCardFront(patch.getIdCardFront());
}
@@ -287,6 +297,9 @@ class PersonCertRoundTripTest {
@DisplayName("审核通过:证件内容写回司机档案,档案与生效值一致")
void approveWritesCertContentBackToDriver() {
PersonCertSubmitRequest request = personCert("张三", "110101199001011234");
request.setBirthday(LocalDate.of(1990, 1, 1));
request.setAddressRegion("广西壮族自治区柳州市柳北区");
request.setAddress("跃进路88号");
request.setHeadPhoto("http://example.com/head.jpg");
request.setDriverClass("A2");
request.setDriverFileNo("450100769875");
@@ -302,6 +315,9 @@ class PersonCertRoundTripTest {
// 档案里必须是这次提交的内容:只切指针、不写内容的话,
// 认证状态显示「已通过」而档案仍是旧值,管理端与司机端会各看各的
assertEquals(LocalDate.of(1990, 1, 1), driver.getBirthday());
assertEquals("广西壮族自治区柳州市柳北区", driver.getAddressRegion());
assertEquals("跃进路88号", driver.getAddress());
assertEquals("http://example.com/head.jpg", driver.getHeadPhoto());
assertEquals("A2", driver.getDrivingType());
assertEquals("450100769875", driver.getDrivingLicenseNo());
@@ -313,6 +329,25 @@ class PersonCertRoundTripTest {
assertEquals("http://example.com/q-back.jpg", driver.getQualificationBack());
}
@Test
@DisplayName("审核通过:住址留空不清空管理端已填的住址")
void approveKeepsAddressWhenSnapshotBlank() {
driver.setAddressRegion("广西壮族自治区柳州市柳北区");
driver.setAddress("跃进路88号");
PersonCertSubmitRequest request = personCert("张三", "110101199001011234");
request.setAddressRegion(" ");
request.setAddress("");
Long recordId = driverCertService.submitPersonCert(request);
certAuditService.audit(recordId, true, null);
// 框架的 not_null 更新策略只跳过 null:写空串会把管理端填过的住址清掉
assertEquals("广西壮族自治区柳州市柳北区", driver.getAddressRegion(),
"区划是选填项,没选就保持档案原值");
assertEquals("跃进路88号", driver.getAddress(),
"详细地址是选填项,没填就保持档案原值");
}
@Test
@DisplayName("审核通过:姓名与身份证号跟着认证资料走,档案与生效值不再各说各话")
void approveOverwritesIdentity() {
@@ -124,6 +124,8 @@ class OcrFieldSupportTest {
{
"words_result": {
"姓名": {"words": "黄宇富"},
"出生": {"words": "1979年11月17日"},
"住址": {"words": "广西壮族自治区柳州市柳北区跃进路88号"},
"公民身份号码": {"words": "452730197911172318"},
"签发日期": {"words": "2015-03-12"},
"失效日期": {"words": "2035-03-12"}
@@ -133,6 +135,8 @@ class OcrFieldSupportTest {
Map<String, Object> person = OcrFieldSupport.extract(BaiduOcrType.ID_CARD, "front", idCard);
assertEquals("黄宇富", person.get("realName"));
assertEquals("452730197911172318", person.get("idCardNo"));
assertEquals("1979-11-17", person.get("birthday"), "「出生」是一串中文日期,要归一成 yyyy-MM-dd 才能填进日期框");
assertEquals("广西壮族自治区柳州市柳北区跃进路88号", person.get("address"));
assertEquals("2015-03-12", person.get("idCardValidStart"));
assertEquals("2035-03-12", person.get("idCardValidEnd"));