diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IDriverClient.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IDriverClient.java new file mode 100644 index 00000000..cdaf96ce --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IDriverClient.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *
+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *
+ * 1. This software is for development use only under a valid license + * from BladeX. + *
+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *
+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *
+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *
+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.feign; + +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.DriverEnsureProfileDTO; +import org.springblade.transport.pojo.vo.DriverEnsureProfileVO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * 司机档案 Feign 接口 + * + * @author Chill + */ +@FeignClient(value = "blade-transport") +public interface IDriverClient { + + String API_PREFIX = "/feign/client/driver"; + String ENSURE_PROFILE = API_PREFIX + "/ensure-profile"; + + /** + * 查找或创建司机档案,并把档案绑定到指定小程序账号 + *
+ * 幂等入口:按 user_id 命中时原样返回;冲突(同号多档案 / 已被其他账号占用)
+ * 不新建、不覆盖,通过返回值的 message 说明原因,不抛异常给登录方。
+ *
+ * @param param 租户、小程序账号与手机号
+ * @return 档案ID与本次处置标志
+ */
+ @PostMapping(ENSURE_PROFILE)
+ R
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 司机档案「查找或创建并绑定账号」参数
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "司机档案查找或创建参数")
+public class DriverEnsureProfileDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "租户ID,为空时取当前登录租户;仍为空则拒绝写库")
+ private String tenantId;
+
+ @Schema(description = "关联小程序用户ID(blade_mp_user.id)")
+ private Long mpUserId;
+
+ @Schema(description = "手机号,仅在按账号未命中时用于查找已有档案")
+ private String phone;
+
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java
index fc7c3ce8..9c2007c1 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java
@@ -40,6 +40,7 @@ import org.springblade.transport.jackson.FlexibleLocalDateDeserializer;
import java.io.Serial;
import java.time.LocalDate;
+import java.time.LocalDateTime;
/**
* 司机管理实体类
@@ -223,6 +224,17 @@ public class Driver extends TenantEntity {
@Schema(description = "关联小程序用户ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
+ /**
+ * 档案来源:login_auto(登录自动建)/ login_bound(绑到已有档案)/
+ * admin(后台建)/ cert_backfill(存量订正);NULL 表示来源未知
+ */
+ @Schema(description = "档案来源")
+ private String profileSource;
+ /**
+ * 档案与账号建立关联的时点
+ */
+ @Schema(description = "账号绑定时间")
+ private LocalDateTime accountBoundTime;
/**
* 与联系人关系
*/
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnsureProfileVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnsureProfileVO.java
new file mode 100644
index 00000000..8859abad
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnsureProfileVO.java
@@ -0,0 +1,58 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 司机档案「查找或创建并绑定账号」结果
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "司机档案查找或创建结果")
+public class DriverEnsureProfileVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "命中的司机档案ID,冲突时为null")
+ private Long driverId;
+
+ @Schema(description = "本次新建了档案")
+ private Boolean created;
+
+ @Schema(description = "本次把已有档案绑定到了账号")
+ private Boolean boundExisting;
+
+ @Schema(description = "冲突原因;成功时为null")
+ private String message;
+
+}
diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java
index 0a0a6e4b..b381750b 100644
--- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java
+++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java
@@ -24,6 +24,7 @@ package org.springblade.system.feign;
import org.springblade.core.launch.constant.AppConstant;
import org.springblade.core.tool.api.R;
+import org.springblade.system.pojo.dto.MpUserEnsureDTO;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springframework.cloud.openfeign.FeignClient;
@@ -46,6 +47,7 @@ public interface IMpUserClient {
String USER_BY_ID = API_PREFIX + "/user-by-id";
String USER_BY_PHONE = API_PREFIX + "/user-by-phone";
String USER_BY_ACCOUNT = API_PREFIX + "/user-by-account";
+ String USER_BY_OPENID = API_PREFIX + "/user-by-openid";
String USER_INFO_BY_ID = API_PREFIX + "/info-by-id";
String USER_INFO_BY_PHONE = API_PREFIX + "/info-by-phone";
String USER_INFO_BY_ACCOUNT = API_PREFIX + "/info-by-account";
@@ -53,6 +55,7 @@ public interface IMpUserClient {
String UPDATE_USER = API_PREFIX + "/update-user";
String BIND_OPENID = API_PREFIX + "/bind-openid";
String SET_PASSWORD = API_PREFIX + "/set-password";
+ String ENSURE_USER = API_PREFIX + "/ensure-user";
/**
* 按主键查询
@@ -72,6 +75,18 @@ public interface IMpUserClient {
@GetMapping(USER_BY_ACCOUNT)
R
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 小程序用户「查找或创建」参数
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "小程序用户查找或创建参数")
+public class MpUserEnsureDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "租户ID,为空时依次取当前登录租户与默认租户")
+ private String tenantId;
+
+ @Schema(description = "手机号")
+ private String phone;
+
+ @Schema(description = "微信小程序openid,非空时随首次建号一并写入")
+ private String openid;
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java
index 4d8eabfe..fae7ebfa 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java
@@ -26,6 +26,7 @@ import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
+import org.springblade.system.pojo.dto.MpUserEnsureDTO;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.service.IMpUserService;
@@ -65,6 +66,18 @@ public class MpUserClient implements IMpUserClient {
return R.data(mpUserService.getByAccount(tenantId, account));
}
+ @Override
+ @GetMapping(USER_BY_OPENID)
+ public R
+ * 与 {@link #submit} 的提交式语义相反:手机号已存在时直接返回已有账号,且不改其身份。
+ */
+ MpUser ensureUser(MpUserEnsureDTO param);
+
/**
* 按主键组装登录信息
*/
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java
index 93f744ba..a0a12bab 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java
@@ -25,6 +25,7 @@ package org.springblade.system.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springblade.common.utils.PasswordRuleUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
@@ -36,6 +37,7 @@ import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.cache.MpUserCache;
import org.springblade.system.mapper.MpUserMapper;
+import org.springblade.system.pojo.dto.MpUserEnsureDTO;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.enums.MpUserIdentity;
@@ -53,10 +55,16 @@ import java.util.Objects;
*
* @author Chill
*/
+@Slf4j
@Service
@RequiredArgsConstructor
public class MpUserServiceImpl extends BaseServiceImpl
+ * 写库前必须得到非空值:唯一键 (tenant_id, phone) 对 NULL 不去重,租户为 NULL
+ * 会为同一手机号造出重复账号。
+ */
+ private String resolveEnsureTenantId(String tenantId) {
+ String resolved = trimToNull(tenantId);
+ if (resolved == null) {
+ resolved = trimToNull(AuthUtil.getTenantId());
+ }
+ if (resolved == null) {
+ resolved = DEFAULT_TENANT_ID;
+ }
+ return resolved;
+ }
+
@Override
public MpUserInfo userInfo(Long userId) {
if (Func.isEmpty(userId)) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/feign/DriverClient.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/feign/DriverClient.java
new file mode 100644
index 00000000..c5ab2d70
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/feign/DriverClient.java
@@ -0,0 +1,61 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.feign;
+
+import io.swagger.v3.oas.annotations.Hidden;
+import lombok.AllArgsConstructor;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springblade.transport.pojo.dto.DriverEnsureProfileDTO;
+import org.springblade.transport.pojo.vo.DriverEnsureProfileVO;
+import org.springblade.transport.service.IDriverService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 司机档案 Feign 实现类
+ *
+ * @author Chill
+ */
+@NonDS
+@Hidden
+@RestController
+@AllArgsConstructor
+public class DriverClient implements IDriverClient {
+
+ private final IDriverService driverService;
+
+ @Override
+ @PostMapping(ENSURE_PROFILE)
+ public R
- * 司机申报的实名内容会写回档案,其中身份证号是管理端建档时的查重判据,
- * 而 {@code blade_transport_driver.id_card_no} 上没有唯一索引:若申报的证件号
- * 已属于另一条档案,直接写会静默产生两条同证件号的档案,后续建档查重、
+ * 司机申报的实名内容会写回档案,其中身份证号是管理端建档时的查重判据。
+ * 该列在基线建表脚本与本地库上有唯一索引 {@code uk_transport_driver_id_card},
+ * 但迁移历史里没有任何一次添加它的记录,生产库是否已建未核实;
+ * 应用层查重仍不能省:重复证件号一旦落库,后续建档查重、
* 按证件号识别同一人都将失效。此处阻断审核并整体回滚,交由管理端先处理档案归属。
*
* 证件号与当前档案相同(绝大多数情况)、或库里尚无该号时不阻断;
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 825c3bc0..1a7cb6a0 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
@@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
+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;
@@ -45,6 +46,7 @@ import org.springblade.transport.excel.DriverExcel;
import org.springblade.transport.mapper.DriverMapper;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
+import org.springblade.transport.pojo.vo.DriverEnsureProfileVO;
import org.springblade.transport.pojo.vo.DriverExpiryStatVO;
import org.springblade.transport.pojo.vo.DriverVO;
import org.springblade.transport.service.IDriverService;
@@ -53,6 +55,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@@ -61,6 +64,7 @@ import java.util.Objects;
*
* @author Chill
*/
+@Slf4j
@Service
@RequiredArgsConstructor
public class DriverServiceImpl extends BaseServiceImpl
+ * 不走 {@link #submit}:那条路径要求 15 项必填,自动建档必然被拦。这里只满足表约束——
+ * id_card_no 留 NULL(唯一索引对 NULL 不去重,且 NULL 是「未采集」的判据),
+ * 其余 NOT NULL 列写空串或可读占位。
+ */
+ private DriverEnsureProfileVO createPlaceholderProfile(String tenantId, Long mpUserId, String mobile) {
+ Driver driver = new Driver();
+ driver.setTenantId(tenantId);
+ driver.setUserId(mpUserId);
+ driver.setDriverName("司机" + mobileTail(mobile));
+ driver.setMobile(mobile);
+ driver.setDrivingType("");
+ driver.setDriverType("");
+ driver.setOrganizationName("");
+ driver.setEmergencyContactName("");
+ driver.setEmergencyContactMobile("");
+ driver.setStatus(DEFAULT_ENABLED_STATUS);
+ driver.setProfileSource(PROFILE_SOURCE_LOGIN_AUTO);
+ driver.setAccountBoundTime(LocalDateTime.now());
+ try {
+ save(driver);
+ } catch (Exception exception) {
+ // 并发登录撞 uk_transport_driver_user:重查一次,说明另一个请求已经建好档案
+ Driver concurrent = findDriverByUserId(mpUserId);
+ if (concurrent != null) {
+ return ensureResult(concurrent.getId(), false, false, null);
+ }
+ log.error("自动创建司机档案失败,租户={} 账号={}", tenantId, mpUserId, exception);
+ throw new ServiceException("创建司机档案失败");
+ }
+ return ensureResult(driver.getId(), true, false, null);
+ }
+
+ /**
+ * 把已有档案绑定到账号:只 patch 关联字段与留痕,不覆盖档案内容
+ */
+ private DriverEnsureProfileVO bindExistingProfile(Long driverId, Long mpUserId) {
+ Driver patch = new Driver();
+ patch.setId(driverId);
+ patch.setUserId(mpUserId);
+ patch.setProfileSource(PROFILE_SOURCE_LOGIN_BOUND);
+ patch.setAccountBoundTime(LocalDateTime.now());
+ // 只改 user_id 也必须带 driving_vehicle 原值,理由见 carryDrivingVehicle
+ carryDrivingVehicle(driverId, patch);
+ try {
+ updateById(patch);
+ } catch (Exception exception) {
+ // 并发绑定撞 uk_transport_driver_user:另一个请求已经把这个账号绑到档案上
+ Driver concurrent = findDriverByUserId(mpUserId);
+ if (concurrent != null) {
+ return ensureResult(concurrent.getId(), false, false, null);
+ }
+ log.error("绑定司机档案失败,档案={} 账号={}", driverId, mpUserId, exception);
+ throw new ServiceException("绑定司机档案失败");
+ }
+ return ensureResult(driverId, false, true, null);
+ }
+
+ private DriverEnsureProfileVO ensureResult(Long driverId, boolean created, boolean boundExisting, String message) {
+ DriverEnsureProfileVO result = new DriverEnsureProfileVO();
+ result.setDriverId(driverId);
+ result.setCreated(created);
+ result.setBoundExisting(boundExisting);
+ result.setMessage(message);
+ return result;
+ }
+
+ private String mobileTail(String mobile) {
+ return mobile.length() <= 4 ? mobile : mobile.substring(mobile.length() - 4);
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public boolean changeStatus(Long id, Integer status) {
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverEnsureProfileTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverEnsureProfileTest.java
new file mode 100644
index 00000000..874fde21
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverEnsureProfileTest.java
@@ -0,0 +1,255 @@
+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.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.system.feign.ISysClient;
+import org.springblade.transport.mapper.DriverMapper;
+import org.springblade.transport.pojo.entity.Driver;
+import org.springblade.transport.pojo.vo.DriverEnsureProfileVO;
+import org.springframework.dao.DuplicateKeyException;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+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.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 登录自动建档与绑定已有档案的测试。
+ *
+ * 守住三条不变式:按 user_id 幂等重入不重复建档、同号冲突一律不绑定也不新建、
+ * 绑定那次 patch 必须带上 driving_vehicle 原值(该列 {@code FieldStrategy.ALWAYS}
+ * 会把缺失的列写成 NULL,司机端的车辆绑定会整块消失)。
+ */
+class DriverEnsureProfileTest {
+
+ private static final Long MP_USER_ID = 7001L;
+ private static final Long OTHER_USER_ID = 7002L;
+ private static final Long DRIVER_ID = 9001L;
+ private static final Long NEW_DRIVER_ID = 9002L;
+ private static final String TENANT_ID = "000000";
+ private static final String MOBILE = "15975348524";
+ private static final String PLATE = "桂A32523";
+
+ private DriverMapper driverMapper;
+ private DriverServiceImpl service;
+
+ @BeforeAll
+ static void initTableInfo() {
+ // save 走 MyBatis-Plus 的实体元数据缓存
+ 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);
+ IMpUserClient mpUserClient = mock(IMpUserClient.class);
+ ISysClient sysClient = mock(ISysClient.class);
+ service = new DriverServiceImpl(mpUserClient, sysClient);
+ inject("baseMapper", driverMapper);
+ }
+
+ // ------------------------------------------------------------ 新建占位档案
+
+ @Test
+ @DisplayName("无账号档案、无同号档案:按票 01 的占位值新建,created=true")
+ void createsPlaceholderProfile() {
+ // 第一次按 user_id 查、第二次按手机号查,都没有
+ when(driverMapper.selectList(any())).thenReturn(Collections.emptyList(), Collections.emptyList());
+ when(driverMapper.insert(any(Driver.class))).thenAnswer(invocation -> {
+ Driver inserted = invocation.getArgument(0);
+ inserted.setId(NEW_DRIVER_ID);
+ return 1;
+ });
+
+ DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
+
+ assertTrue(result.getCreated());
+ assertFalse(result.getBoundExisting());
+ assertNull(result.getMessage());
+ assertEquals(NEW_DRIVER_ID, result.getDriverId());
+
+ ArgumentCaptor