🔀 合并登录建号建档两个 feign 入口:ensureUser 与 ensureProfile
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* 查找或创建司机档案,并把档案绑定到指定小程序账号
|
||||
* <p>
|
||||
* 幂等入口:按 user_id 命中时原样返回;冲突(同号多档案 / 已被其他账号占用)
|
||||
* 不新建、不覆盖,通过返回值的 message 说明原因,不抛异常给登录方。
|
||||
*
|
||||
* @param param 租户、小程序账号与手机号
|
||||
* @return 档案ID与本次处置标志
|
||||
*/
|
||||
@PostMapping(ENSURE_PROFILE)
|
||||
R<DriverEnsureProfileVO> ensureProfile(@RequestBody DriverEnsureProfileDTO param);
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+12
@@ -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;
|
||||
/**
|
||||
* 与联系人关系
|
||||
*/
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+15
@@ -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<MpUser> userByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account);
|
||||
|
||||
/**
|
||||
* 按租户 + openid 查询
|
||||
*/
|
||||
@GetMapping(USER_BY_OPENID)
|
||||
R<MpUser> userByOpenid(@RequestParam("tenantId") String tenantId, @RequestParam("openid") String openid);
|
||||
|
||||
/**
|
||||
* 查找或创建小程序用户(登录路径专用,已存在时不抛异常、不改身份)
|
||||
*/
|
||||
@PostMapping(ENSURE_USER)
|
||||
R<MpUser> ensureUser(@RequestBody MpUserEnsureDTO param);
|
||||
|
||||
/**
|
||||
* 按主键查询登录信息
|
||||
*/
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+13
@@ -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<MpUser> userByOpenid(String tenantId, String openid) {
|
||||
return R.data(mpUserService.getByOpenid(tenantId, openid));
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(ENSURE_USER)
|
||||
public R<MpUser> ensureUser(@RequestBody MpUserEnsureDTO param) {
|
||||
return R.data(mpUserService.ensureUser(param));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(USER_INFO_BY_ID)
|
||||
public R<MpUserInfo> userInfoById(Long userId) {
|
||||
|
||||
+13
@@ -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> {
|
||||
*/
|
||||
MpUser getByAccount(String tenantId, String account);
|
||||
|
||||
/**
|
||||
* 按租户 + 微信小程序 openid 查询
|
||||
*/
|
||||
MpUser getByOpenid(String tenantId, String openid);
|
||||
|
||||
/**
|
||||
* 查找或创建小程序用户(登录路径专用)
|
||||
* <p>
|
||||
* 与 {@link #submit} 的提交式语义相反:手机号已存在时直接返回已有账号,且不改其身份。
|
||||
*/
|
||||
MpUser ensureUser(MpUserEnsureDTO param);
|
||||
|
||||
/**
|
||||
* 按主键组装登录信息
|
||||
*/
|
||||
|
||||
+83
@@ -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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> imp
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MpUser getByOpenid(String tenantId, String openid) {
|
||||
if (Func.isBlank(tenantId) || Func.isBlank(openid)) {
|
||||
return null;
|
||||
}
|
||||
return getOne(Wrappers.<MpUser>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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录路径的租户解析:入参 → 当前登录租户 → 默认租户
|
||||
* <p>
|
||||
* 写库前必须得到非空值:唯一键 (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)) {
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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<DriverEnsureProfileVO> ensureProfile(@RequestBody DriverEnsureProfileDTO param) {
|
||||
if (param == null) {
|
||||
return R.fail("司机档案参数不能为空");
|
||||
}
|
||||
return R.data(driverService.ensureProfile(param.getTenantId(), param.getMpUserId(), param.getPhone()));
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -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<Driver> {
|
||||
*/
|
||||
boolean submit(Driver driver);
|
||||
|
||||
/**
|
||||
* 查找或创建司机档案并绑定小程序账号(登录路径专用)
|
||||
*
|
||||
* @param tenantId 租户ID,必须非空
|
||||
* @param mpUserId 小程序用户ID
|
||||
* @param phone 手机号
|
||||
* @return 档案ID与本次处置标志;冲突时 message 携带可读原因
|
||||
*/
|
||||
DriverEnsureProfileVO ensureProfile(String tenantId, Long mpUserId, String phone);
|
||||
|
||||
/**
|
||||
* 修改状态
|
||||
*
|
||||
|
||||
+7
-5
@@ -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 {
|
||||
/**
|
||||
* 身份证号不得落在另一名司机名下
|
||||
* <p>
|
||||
* 司机申报的实名内容会写回档案,其中身份证号是管理端建档时的查重判据,
|
||||
* 而 {@code blade_transport_driver.id_card_no} 上没有唯一索引:若申报的证件号
|
||||
* 已属于另一条档案,直接写会静默产生两条同证件号的档案,后续建档查重、
|
||||
* 司机申报的实名内容会写回档案,其中身份证号是管理端建档时的查重判据。
|
||||
* 该列在基线建表脚本与本地库上有唯一索引 {@code uk_transport_driver_id_card},
|
||||
* 但迁移历史里没有任何一次添加它的记录,生产库是否已建未核实;
|
||||
* 应用层查重仍不能省:重复证件号一旦落库,后续建档查重、
|
||||
* 按证件号识别同一人都将失效。此处阻断审核并整体回滚,交由管理端先处理档案归属。
|
||||
* <p>
|
||||
* 证件号与当前档案相同(绝大多数情况)、或库里尚无该号时不阻断;
|
||||
|
||||
+140
@@ -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<DriverMapper, Driver> implements IDriverService {
|
||||
@@ -74,6 +78,16 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> 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<DriverMapper, Driver> 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<Driver> sameMobile = list(Wrappers.<Driver>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.<Driver>lambdaQuery()
|
||||
.eq(Driver::getUserId, mpUserId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建占位档案(票 01 定值)
|
||||
* <p>
|
||||
* 不走 {@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) {
|
||||
|
||||
+255
@@ -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;
|
||||
|
||||
/**
|
||||
* 登录自动建档与绑定已有档案的测试。
|
||||
* <p>
|
||||
* 守住三条不变式:按 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<Driver> 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<Driver> 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> 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user