From 3475ec1f877561a9af8f5c4399bc2b91c4704e0f Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Fri, 23 Oct 2026 15:48:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E8=B4=A6=E5=8F=B7=E6=9F=A5=E6=89=BE=E6=88=96?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E4=B8=8E=E6=8C=89openid=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/feign/IMpUserClient.java | 15 ++++ .../system/pojo/dto/MpUserEnsureDTO.java | 55 ++++++++++++ .../system/feign/MpUserClient.java | 13 +++ .../system/service/IMpUserService.java | 13 +++ .../service/impl/MpUserServiceImpl.java | 83 +++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/MpUserEnsureDTO.java 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 userByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account); + /** + * 按租户 + openid 查询 + */ + @GetMapping(USER_BY_OPENID) + R userByOpenid(@RequestParam("tenantId") String tenantId, @RequestParam("openid") String openid); + + /** + * 查找或创建小程序用户(登录路径专用,已存在时不抛异常、不改身份) + */ + @PostMapping(ENSURE_USER) + R ensureUser(@RequestBody MpUserEnsureDTO param); + /** * 按主键查询登录信息 */ diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/MpUserEnsureDTO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/MpUserEnsureDTO.java new file mode 100644 index 00000000..adb27378 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/MpUserEnsureDTO.java @@ -0,0 +1,55 @@ +/** + * 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.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 userByOpenid(String tenantId, String openid) { + return R.data(mpUserService.getByOpenid(tenantId, openid)); + } + + @Override + @PostMapping(ENSURE_USER) + public R ensureUser(@RequestBody MpUserEnsureDTO param) { + return R.data(mpUserService.ensureUser(param)); + } + @Override @GetMapping(USER_INFO_BY_ID) public R userInfoById(Long userId) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java index c0294ced..6b20f61d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java @@ -24,6 +24,7 @@ package org.springblade.system.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; +import org.springblade.system.pojo.dto.MpUserEnsureDTO; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; @@ -47,6 +48,18 @@ public interface IMpUserService extends BaseService { */ MpUser getByAccount(String tenantId, String account); + /** + * 按租户 + 微信小程序 openid 查询 + */ + MpUser getByOpenid(String tenantId, String openid); + + /** + * 查找或创建小程序用户(登录路径专用) + *

+ * 与 {@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 implements IMpUserService { + /** + * 兜底租户:与 blade_mp_user.tenant_id 的建表默认值一致 + */ + private static final String DEFAULT_TENANT_ID = "000000"; + @Override public MpUser getByPhone(String tenantId, String phone) { if (Func.isBlank(tenantId) || Func.isBlank(phone)) { @@ -81,6 +89,81 @@ public class MpUserServiceImpl extends BaseServiceImpl imp .last("LIMIT 1")); } + @Override + public MpUser getByOpenid(String tenantId, String openid) { + if (Func.isBlank(tenantId) || Func.isBlank(openid)) { + return null; + } + return getOne(Wrappers.lambdaQuery() + .eq(MpUser::getTenantId, tenantId) + .eq(MpUser::getOpenid, openid) + .orderByDesc(MpUser::getCreateTime) + .last("LIMIT 1")); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public MpUser ensureUser(MpUserEnsureDTO param) { + if (param == null) { + throw new ServiceException("小程序用户参数不能为空"); + } + String tenantId = resolveEnsureTenantId(param.getTenantId()); + String phone = trimToNull(param.getPhone()); + if (Func.isBlank(phone)) { + throw new ServiceException("手机号不能为空"); + } + // 先查后插:命中即返回,不新建也不改身份。租户为空时唯一键对 NULL 不去重, + // 故 resolveEnsureTenantId 已在写库前把租户兜到非空。 + MpUser existed = getByPhone(tenantId, phone); + if (existed != null && Func.isNotEmpty(existed.getId())) { + return existed; + } + MpUser user = new MpUser(); + user.setTenantId(tenantId); + user.setPhone(phone); + user.setAccount(phone); + user.setIdentities(MpUserIdentity.DRIVER.getCode()); + user.setOpenid(trimToNull(param.getOpenid())); + user.setStatus(1); + prepare(user); + validate(user); + // 登录路径不设密码:密码留空,避免调用方误传弱口令 + user.setPassword(null); + try { + save(user); + } catch (Exception exception) { + // 并发建号撞 (tenant_id, phone) 唯一键:重读一次返回,登录方不该因重试失败 + MpUser concurrent = getByPhone(tenantId, phone); + if (concurrent != null && Func.isNotEmpty(concurrent.getId())) { + return concurrent; + } + log.warn("小程序用户建号失败,租户={} 手机号={}", tenantId, phone, exception); + throw new ServiceException("创建小程序用户失败"); + } + if (Func.isEmpty(user.getId())) { + throw new ServiceException("创建小程序用户失败"); + } + MpUserCache.clear(); + return user; + } + + /** + * 登录路径的租户解析:入参 → 当前登录租户 → 默认租户 + *

+ * 写库前必须得到非空值:唯一键 (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)) { From 49be9c6d52f609287951528ddab4078b9cd0f5dc Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Fri, 23 Oct 2026 15:48:57 +0800 Subject: [PATCH 2/3] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E5=8F=B8?= =?UTF-8?q?=E6=9C=BA=E6=A1=A3=E6=A1=88=E7=99=BB=E5=BD=95=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=BB=BA=E6=A1=A3=E4=B8=8E=E8=B4=A6=E5=8F=B7=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/feign/IDriverClient.java | 58 ++++++++ .../pojo/dto/DriverEnsureProfileDTO.java | 55 +++++++ .../transport/pojo/entity/Driver.java | 12 ++ .../pojo/vo/DriverEnsureProfileVO.java | 58 ++++++++ .../transport/feign/DriverClient.java | 61 ++++++++ .../transport/service/IDriverService.java | 11 ++ .../service/impl/CertAuditServiceImpl.java | 12 +- .../service/impl/DriverServiceImpl.java | 140 ++++++++++++++++++ 8 files changed, 402 insertions(+), 5 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IDriverClient.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/DriverEnsureProfileDTO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnsureProfileVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/feign/DriverClient.java 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 ensureProfile(@RequestBody DriverEnsureProfileDTO param); + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/DriverEnsureProfileDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/DriverEnsureProfileDTO.java new file mode 100644 index 00000000..3844e1b9 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/DriverEnsureProfileDTO.java @@ -0,0 +1,55 @@ +/** + * 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.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/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 ensureProfile(@RequestBody DriverEnsureProfileDTO param) { + if (param == null) { + return R.fail("司机档案参数不能为空"); + } + return R.data(driverService.ensureProfile(param.getTenantId(), param.getMpUserId(), param.getPhone())); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java index 45200175..8864ed8f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java @@ -30,6 +30,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.DriverExcel; import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.vo.DriverEnsureProfileVO; import org.springblade.transport.pojo.vo.DriverExpiryStatVO; import org.springblade.transport.pojo.vo.DriverVO; @@ -59,6 +60,16 @@ public interface IDriverService extends BaseService { */ boolean submit(Driver driver); + /** + * 查找或创建司机档案并绑定小程序账号(登录路径专用) + * + * @param tenantId 租户ID,必须非空 + * @param mpUserId 小程序用户ID + * @param phone 手机号 + * @return 档案ID与本次处置标志;冲突时 message 携带可读原因 + */ + DriverEnsureProfileVO ensureProfile(String tenantId, Long mpUserId, String phone); + /** * 修改状态 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java index 247239d0..cb634ce5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java @@ -349,8 +349,9 @@ public class CertAuditServiceImpl implements ICertAuditService { if (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo())) { return; } - // 身份证号要落到档案上,先确认它没有落在别人名下:该列没有唯一索引, - // 直接写会静默产生两条同证件号的档案,管理端的证件号查重从此失效 + // 身份证号要落到档案上,先确认它没有落在别人名下:该列在基线建表脚本与本地库上 + // 有唯一索引 uk_transport_driver_id_card,但迁移历史里没有任何一次添加它的记录, + // 生产库是否已建未核实;应用层查重不能省,否则重复证件号会静默落库 requireIdCardNotTaken(snapshot.getIdCardNo(), driver.getId()); driver.setDriverName(snapshot.getRealName()); driver.setIdCardNo(snapshot.getIdCardNo()); @@ -382,9 +383,10 @@ public class CertAuditServiceImpl implements ICertAuditService { /** * 身份证号不得落在另一名司机名下 *

- * 司机申报的实名内容会写回档案,其中身份证号是管理端建档时的查重判据, - * 而 {@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 implements IDriverService { @@ -74,6 +78,16 @@ public class DriverServiceImpl extends BaseServiceImpl imp private static final int REMARK_MAX_LENGTH = 200; private static final int DEFAULT_ENABLED_STATUS = 1; private static final int DEFAULT_FALSE = 0; + /** + * 档案来源:登录时自动新建 + */ + private static final String PROFILE_SOURCE_LOGIN_AUTO = "login_auto"; + /** + * 档案来源:绑定到已有档案 + */ + private static final String PROFILE_SOURCE_LOGIN_BOUND = "login_bound"; + private static final String CONFLICT_MOBILE_OCCUPIED = "该手机号已被其他账号占用,请联系管理员"; + private static final String CONFLICT_MOBILE_MULTIPLE = "该手机号存在多条司机档案,请联系管理员核对"; private final IMpUserClient mpUserClient; private final ISysClient sysClient; @@ -101,6 +115,132 @@ public class DriverServiceImpl extends BaseServiceImpl imp return true; } + @Override + @Transactional(rollbackFor = Exception.class) + public DriverEnsureProfileVO ensureProfile(String tenantId, Long mpUserId, String phone) { + String resolvedTenantId = trimToNull(tenantId); + if (resolvedTenantId == null) { + resolvedTenantId = trimToNull(AuthUtil.getTenantId()); + } + // 租户是档案归属与手机号查重的范围,宁可让本次建档失败(登录层只记 WARN), + // 也不能写出 tenant_id 为空的档案——那会让下一条同号档案无法被查到 + if (resolvedTenantId == null) { + throw new ServiceException("租户信息为空,无法创建司机档案"); + } + if (Func.isEmpty(mpUserId)) { + throw new ServiceException("小程序用户ID不能为空"); + } + String mobile = trimToNull(phone); + if (mobile == null) { + throw new ServiceException("手机号不能为空"); + } + // 查找顺序固定为 user_id → phone:user_id 是稳定标识; + // phone 会被运营商二次放号,拿它优先会把别人档案绑给新持卡人 + Driver bound = findDriverByUserId(mpUserId); + if (bound != null) { + return ensureResult(bound.getId(), false, false, null); + } + List sameMobile = list(Wrappers.lambdaQuery() + .eq(Driver::getTenantId, resolvedTenantId) + .eq(Driver::getMobile, mobile)); + if (sameMobile.isEmpty()) { + return createPlaceholderProfile(resolvedTenantId, mpUserId, mobile); + } + if (sameMobile.size() > 1) { + return ensureResult(null, false, false, CONFLICT_MOBILE_MULTIPLE); + } + Driver existing = sameMobile.get(0); + if (Func.isNotEmpty(existing.getUserId())) { + if (Objects.equals(existing.getUserId(), mpUserId)) { + return ensureResult(existing.getId(), false, false, null); + } + return ensureResult(null, false, false, CONFLICT_MOBILE_OCCUPIED); + } + return bindExistingProfile(existing.getId(), mpUserId); + } + + /** + * 按小程序账号反查档案;user_id 有全局唯一索引,故不叠加租户条件 + */ + private Driver findDriverByUserId(Long mpUserId) { + return getOne(Wrappers.lambdaQuery() + .eq(Driver::getUserId, mpUserId) + .last("LIMIT 1")); + } + + /** + * 新建占位档案(票 01 定值) + *

+ * 不走 {@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) { From 9f849f55be2dfc3724933fa2342be20daa8c9b02 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Fri, 23 Oct 2026 15:48:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=8F=B8=E6=9C=BA=E6=A1=A3=E6=A1=88=E8=87=AA=E5=8A=A8=E5=BB=BA?= =?UTF-8?q?=E6=A1=A3=E4=B8=8E=E7=BB=91=E5=AE=9A=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/DriverEnsureProfileTest.java | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverEnsureProfileTest.java 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 captor = ArgumentCaptor.forClass(Driver.class); + verify(driverMapper).insert(captor.capture()); + Driver saved = captor.getValue(); + assertEquals(TENANT_ID, saved.getTenantId(), "tenant_id 必须显式写入且非空"); + assertEquals(MP_USER_ID, saved.getUserId()); + assertEquals(MOBILE, saved.getMobile()); + assertEquals("司机8524", saved.getDriverName()); + assertNull(saved.getIdCardNo(), "身份证号留 NULL 才是「未采集」,也是补全判据"); + assertEquals("", saved.getDrivingType()); + assertEquals("", saved.getDriverType()); + assertEquals("", saved.getOrganizationName()); + assertEquals("", saved.getEmergencyContactName()); + assertEquals("", saved.getEmergencyContactMobile()); + assertEquals(1, saved.getStatus()); + assertEquals("login_auto", saved.getProfileSource()); + assertNotNull(saved.getAccountBoundTime()); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + // ------------------------------------------------------------ 绑定已有档案 + + @Test + @DisplayName("命中同号未绑定的档案:绑定并留痕,patch 必须带上 driving_vehicle 原值") + void bindsExistingProfileAndKeepsDrivingVehicle() { + Driver existing = driver(DRIVER_ID, null, PLATE); + when(driverMapper.selectList(any())).thenReturn(Collections.emptyList(), List.of(existing)); + when(driverMapper.selectById(DRIVER_ID)).thenReturn(existing); + when(driverMapper.updateById(any(Driver.class))).thenReturn(1); + + DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE); + + assertFalse(result.getCreated()); + assertTrue(result.getBoundExisting()); + assertNull(result.getMessage()); + assertEquals(DRIVER_ID, result.getDriverId()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverMapper).updateById(captor.capture()); + Driver patch = captor.getValue(); + assertEquals(MP_USER_ID, patch.getUserId()); + assertEquals("login_bound", patch.getProfileSource()); + assertNotNull(patch.getAccountBoundTime()); + assertEquals(PLATE, patch.getDrivingVehicle(), + "绑定 patch 不带 driving_vehicle 会把司机的车辆绑定写掉"); + assertNull(patch.getMobile(), "绑定不得覆盖档案上的手机号"); + verify(driverMapper, never()).insert(any(Driver.class)); + } + + // ------------------------------------------------------------ 幂等重入 + + @Test + @DisplayName("按 user_id 命中:直接返回该档案,不新建、不改手机号") + void returnsExistingProfileForRepeatLogin() { + when(driverMapper.selectList(any())).thenReturn(List.of(driver(DRIVER_ID, MP_USER_ID, PLATE))); + + DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE); + + assertFalse(result.getCreated()); + assertFalse(result.getBoundExisting()); + assertNull(result.getMessage()); + assertEquals(DRIVER_ID, result.getDriverId()); + verify(driverMapper, never()).insert(any(Driver.class)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + @Test + @DisplayName("并发重复建档撞唯一键:重查后按已存在的档案返回,不把异常抛给登录方") + void toleratesConcurrentInsert() { + Driver concurrent = driver(DRIVER_ID, MP_USER_ID, PLATE); + when(driverMapper.selectList(any())).thenReturn( + Collections.emptyList(), + Collections.emptyList(), + List.of(concurrent)); + when(driverMapper.insert(any(Driver.class))).thenThrow(new DuplicateKeyException("uk_transport_driver_user")); + + DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE); + + assertFalse(result.getCreated()); + assertFalse(result.getBoundExisting()); + assertEquals(DRIVER_ID, result.getDriverId()); + } + + // ------------------------------------------------------------ 冲突 + + @Test + @DisplayName("同手机号多条档案:拒绝绑定也不新建,返回可读原因") + void rejectsMultipleProfilesWithSameMobile() { + when(driverMapper.selectList(any())).thenReturn( + Collections.emptyList(), + List.of(driver(DRIVER_ID, null, PLATE), driver(9003L, null, ""))); + + DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE); + + assertFalse(result.getCreated()); + assertFalse(result.getBoundExisting()); + assertNull(result.getDriverId()); + assertEquals("该手机号存在多条司机档案,请联系管理员核对", result.getMessage()); + verify(driverMapper, never()).insert(any(Driver.class)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + @Test + @DisplayName("同手机号档案已属于其他账号:不覆盖,返回可读原因") + void rejectsMobileOwnedByAnotherAccount() { + when(driverMapper.selectList(any())).thenReturn( + Collections.emptyList(), + List.of(driver(DRIVER_ID, OTHER_USER_ID, PLATE))); + + DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE); + + assertFalse(result.getCreated()); + assertFalse(result.getBoundExisting()); + assertNull(result.getDriverId()); + assertEquals("该手机号已被其他账号占用,请联系管理员", result.getMessage()); + verify(driverMapper, never()).insert(any(Driver.class)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + // ------------------------------------------------------------ 入参兜底 + + @Test + @DisplayName("租户为空且当前上下文也没有租户:拒绝写库") + void rejectsBlankTenant() { + try (MockedStatic authUtil = mockStatic(AuthUtil.class)) { + authUtil.when(AuthUtil::getTenantId).thenReturn(null); + + assertThrows(ServiceException.class, () -> service.ensureProfile(null, MP_USER_ID, MOBILE)); + } + verify(driverMapper, never()).insert(any(Driver.class)); + } + + // ------------------------------------------------------------ 夹具 + + private Driver driver(Long id, Long userId, String drivingVehicle) { + Driver driver = new Driver(); + driver.setId(id); + driver.setIsDeleted(0); + driver.setTenantId(TENANT_ID); + driver.setUserId(userId); + driver.setMobile(MOBILE); + driver.setDrivingVehicle(drivingVehicle); + return driver; + } + + private void inject(String field, Object value) throws Exception { + Class type = service.getClass(); + while (type != null) { + try { + Field declared = type.getDeclaredField(field); + declared.setAccessible(true); + declared.set(service, value); + return; + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + throw new IllegalStateException("未找到字段:" + field); + } + +}