✨ 小程序用户独立为 blade_mp_user,并兼容司机/车辆 OCR 宽松日期

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-12 13:02:08 +08:00
parent 42ca5ee7bc
commit e19c3b100a
34 changed files with 1892 additions and 170 deletions
@@ -51,6 +51,7 @@ import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.system.feign.IAuthLockClient;
import org.springblade.system.feign.IAuthLogClient;
import org.springblade.core.tenant.BladeTenantProperties;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.IUserClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -117,8 +118,8 @@ public class BladeAuthConfiguration {
}
@Bean
public OAuth2UserService oAuth2UserService(IUserClient userClient) {
return new BladeUserDetailService(userClient);
public OAuth2UserService oAuth2UserService(IUserClient userClient, IMpUserClient mpUserClient) {
return new BladeUserDetailService(userClient, mpUserClient);
}
@Primary
@@ -30,9 +30,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@@ -72,10 +74,26 @@ public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice<Object>
if (userInfo == null) {
return body;
}
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
if (isMpUser(bladeUser)) {
// 小程序用户:按身份直接生成权限,不走系统角色菜单
userInfo.setPermission(MpUserIdentity.toPermissions(bladeUser.getRoleName()));
} else {
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
}
return userInfo;
}
private boolean isMpUser(BladeUser bladeUser) {
Kv detail = bladeUser.getDetail();
if (detail != null && MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(detail.get("type")))) {
return true;
}
// 兼容:无系统角色且 roleName 为小程序身份
return Func.isBlank(bladeUser.getRoleId())
&& Func.isNotBlank(bladeUser.getRoleName())
&& !MpUserIdentity.split(bladeUser.getRoleName()).isEmpty();
}
private List<String> loadPermission(String roleId) {
if (Func.isBlank(roleId)) {
return Collections.emptyList();
@@ -2,10 +2,12 @@ package org.springblade.auth.granter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springblade.auth.utils.TokenUtil;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
@@ -13,7 +15,8 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.thirdparty.wechat.constant.WechatMiniConstant;
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
@@ -25,7 +28,8 @@ import org.springframework.stereotype.Component;
* 微信小程序手机号一键登录。
* <p>
* grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
* 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。
* 流程:换 openid + 手机号 → 按手机号查小程序用户(blade_mp_user,不存在则拒绝)
* → 记录 openid → 按身份发令牌(不依赖系统角色)。
*/
@Slf4j
@Component
@@ -33,17 +37,17 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
private final OAuth2UserService userService;
private final IWechatMiniService wechatMiniService;
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
public WechatMiniTokenGranter(OAuth2ClientService clientService,
OAuth2UserService userService,
PasswordHandler passwordHandler,
IWechatMiniService wechatMiniService,
IUserClient userClient) {
IMpUserClient mpUserClient) {
super(clientService, userService, passwordHandler);
this.userService = userService;
this.wechatMiniService = wechatMiniService;
this.userClient = userClient;
this.mpUserClient = mpUserClient;
}
@Override
@@ -54,7 +58,7 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
@Override
public OAuth2User user(OAuth2Request request) {
// 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败
var oauthClient = client(request);
OAuth2Client oauthClient = client(request);
HttpServletRequest httpRequest = WebUtil.getRequest();
String loginCode = httpRequest.getParameter("loginCode");
@@ -78,16 +82,21 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
throw new UserInvalidException("未获取到微信手机号");
}
OAuth2User user = userService.loadByPhone(phone, request);
if (!userService.validateUser(user)) {
R<MpUserInfo> mpUserResult = mpUserClient.userInfoByPhone(request.getTenantId(), phone);
if (mpUserResult == null || !mpUserResult.isSuccess() || mpUserResult.getData() == null
|| mpUserResult.getData().getUser() == null) {
throw new UserInvalidException("用户不存在,无法登录");
}
R<Boolean> bindResult = userClient.bindWxMiniOpenId(
OAuth2User user = TokenUtil.convertMpUser(mpUserResult.getData());
if (!userService.validateUser(user)) {
throw new UserInvalidException("用户身份无效,无法登录");
}
R<Boolean> bindResult = mpUserClient.bindOpenId(
request.getTenantId(),
Func.toLong(user.getUserId()),
session.getOpenid(),
phone
session.getOpenid()
);
if (bindResult == null || !bindResult.isSuccess()) {
String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败";
@@ -18,9 +18,6 @@
* 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.auth.service;
@@ -35,8 +32,11 @@ import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.entity.UserInfo;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.enums.UserType;
import java.util.Optional;
@@ -49,6 +49,7 @@ import java.util.Optional;
@RequiredArgsConstructor
public class BladeUserDetailService implements OAuth2UserService {
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
@Override
public OAuth2User loadByUserId(String userId, OAuth2Request request) {
@@ -57,13 +58,29 @@ public class BladeUserDetailService implements OAuth2UserService {
.filter(s -> !StringUtil.isBlank(s))
.orElse(UserType.WEB.getName());
// 获取用户信息
R<UserInfo> result = userClient.userInfo(Func.toLong(userId), userType);
if (!result.isSuccess()) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
// 小程序刷新/鉴权:直接查 blade_mp_user
if (MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(userType)) {
OAuth2User mpUser = loadMpUserById(userId);
if (mpUser == null) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
return mpUser;
}
// 构建oauth2用户信息
return TokenUtil.convertUser(result.getData(), request);
// 系统用户
R<UserInfo> result = userClient.userInfo(Func.toLong(userId), userType);
if (result != null && result.isSuccess() && result.getData() != null
&& result.getData().getUser() != null) {
return TokenUtil.convertUser(result.getData(), request);
}
// 兼容:未带 userType=mp 时,回落查询小程序用户(令牌刷新)
OAuth2User mpUser = loadMpUserById(userId);
if (mpUser != null) {
return mpUser;
}
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
return null;
}
@Override
@@ -74,6 +91,15 @@ public class BladeUserDetailService implements OAuth2UserService {
.orElse(UserType.WEB.getName());
String tenantId = request.getTenantId();
// 小程序用户:按账号查 blade_mp_user
if (MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(userType)) {
R<MpUserInfo> mpResult = mpUserClient.userInfoByAccount(tenantId, username);
if (mpResult == null || !mpResult.isSuccess() || mpResult.getData() == null) {
return null;
}
return TokenUtil.convertMpUser(mpResult.getData());
}
// 获取用户信息
R<UserInfo> result = userClient.userInfo(tenantId, username, userType);
if (!result.isSuccess()) {
@@ -91,6 +117,11 @@ public class BladeUserDetailService implements OAuth2UserService {
.orElse(UserType.WEB.getName());
String tenantId = request.getTenantId();
// 小程序用户类型:仅查 blade_mp_user
if (MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(userType)) {
return loadMpUserByPhone(tenantId, phone);
}
// 获取用户信息
R<UserInfo> result = userClient.userInfoByPhone(tenantId, phone, userType);
if (!result.isSuccess()) {
@@ -108,4 +139,28 @@ public class BladeUserDetailService implements OAuth2UserService {
.isPresent(); // 如果上述条件都满足,则返回true,否则返回false
}
private OAuth2User loadMpUserById(String userId) {
if (Func.isBlank(userId)) {
return null;
}
try {
R<MpUserInfo> result = mpUserClient.userInfoById(Func.toLong(userId));
if (result != null && result.isSuccess() && result.getData() != null
&& result.getData().getUser() != null) {
return TokenUtil.convertMpUser(result.getData());
}
} catch (Exception exception) {
// 非小程序用户场景忽略,继续走系统用户
}
return null;
}
private OAuth2User loadMpUserByPhone(String tenantId, String phone) {
R<MpUserInfo> result = mpUserClient.userInfoByPhone(tenantId, phone);
if (result == null || !result.isSuccess() || result.getData() == null) {
return null;
}
return TokenUtil.convertMpUser(result.getData());
}
}
@@ -28,12 +28,14 @@ package org.springblade.auth.utils;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.impl.OAuth2UserDetail;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.UserInfo;
import java.util.Collections;
import org.springblade.system.pojo.enums.MpUserIdentity;
/**
* 认证工具类
@@ -75,6 +77,7 @@ public class TokenUtil {
userDetail.setRealName(user.getRealName());
userDetail.setAccount(user.getAccount());
userDetail.setPassword(user.getPassword());
userDetail.setPhone(user.getPhone());
userDetail.setDeptId(user.getDeptId());
userDetail.setPostId(user.getPostId());
userDetail.setRoleId(user.getRoleId());
@@ -85,4 +88,37 @@ public class TokenUtil {
return userDetail;
}
/**
* 小程序用户转换为OAuth2标准用户(身份直接写入 authorities,不走系统角色)
*
* @param userInfo 小程序用户信息
* @return OAuth2User
*/
public static OAuth2User convertMpUser(MpUserInfo userInfo) {
if (userInfo == null || userInfo.getUser() == null) {
return null;
}
MpUser user = userInfo.getUser();
OAuth2UserDetail userDetail = new OAuth2UserDetail();
userDetail.setUserId(String.valueOf(user.getId()));
userDetail.setTenantId(user.getTenantId());
userDetail.setName(user.getName());
userDetail.setRealName(user.getRealName());
userDetail.setAccount(user.getAccount());
userDetail.setPassword(user.getPassword());
userDetail.setPhone(user.getPhone());
userDetail.setDeptId(user.getDeptId());
userDetail.setPostId(user.getPostId());
// 小程序用户不绑定系统角色,roleId 留空;身份放入 roleName/authorities
userDetail.setRoleId("");
userDetail.setRoleName(Func.join(userInfo.getIdentities()));
userDetail.setAvatar(user.getAvatar());
userDetail.setAuthorities(userInfo.getIdentities());
userDetail.setPermissions(userInfo.getPermissions());
userDetail.setDetail(userInfo.getDetail() == null
? Kv.create().set("type", MpUserIdentity.DETAIL_TYPE)
: userInfo.getDetail());
return userDetail;
}
}
@@ -0,0 +1,148 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.jackson;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* LocalDate 宽松反序列化,对齐 OCR / 司机侧日期归一化规则。
* <p>
* 兼容写法:
* <ul>
* <li>ISO:{@code yyyy-MM-dd}</li>
* <li>紧凑:{@code yyyyMMdd}</li>
* <li>点分 / 斜杠 / 年月日:{@code 2018.3.9}、{@code 2026/03/17}、{@code 2030年05月20日}</li>
* <li>任意非数字分隔:年与月、月与日之间只要不是数字即可</li>
* <li>区间文本:按字段名取起期(first)或止期(last)</li>
* </ul>
*
* @author Chill
*/
public class FlexibleLocalDateDeserializer extends JsonDeserializer<LocalDate> {
/**
* 4 位年 + 1~2 位月 + 1~2 位日,中间允许任意非数字分隔(与 OcrFieldSupport 一致)
*/
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})\\D+(\\d{1,2})\\D+(\\d{1,2})");
private static final Pattern COMPACT_PATTERN = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})");
@Override
public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException {
String text = parser.getValueAsString();
if (text == null) {
return null;
}
String value = text.trim();
if (value.isEmpty()) {
return null;
}
// 「长期」等非日期文案:按空值处理,避免把无法解析的字符串打成 400
if (value.contains("长期") || value.contains("永久")) {
return null;
}
boolean preferLast = isEndDateField(parser.getCurrentName());
LocalDate parsed = parseFlexible(value, preferLast);
if (parsed != null) {
return parsed;
}
return (LocalDate) context.handleWeirdStringValue(LocalDate.class, value,
"期望日期格式 yyyy-MM-dd / yyyyMMdd / yyyy年M月d日 等 OCR 常见写法");
}
/**
* 解析宽松日期文本;区间时按 preferLast 取首段或末段。
*
* @param value 原始文本
* @param preferLast true 取止期,false 取起期
* @return 合法 LocalDate;无法识别时返回 null
*/
public static LocalDate parseFlexible(String value, boolean preferLast) {
if (value == null) {
return null;
}
String text = value.trim();
if (text.isEmpty()) {
return null;
}
List<LocalDate> candidates = matchDates(text);
if (!candidates.isEmpty()) {
return preferLast ? candidates.get(candidates.size() - 1) : candidates.get(0);
}
// 去掉所有非数字后恰为 8 位时按 yyyyMMdd 处理(如 20260317、2026-0317 混杂)
String compact = text.replaceAll("\\D", "");
if (compact.length() == 8) {
return toLocalDate(compact.substring(0, 4), compact.substring(4, 6), compact.substring(6, 8));
}
Matcher compactMatcher = COMPACT_PATTERN.matcher(text);
if (compactMatcher.find()) {
return toLocalDate(compactMatcher.group(1), compactMatcher.group(2), compactMatcher.group(3));
}
return null;
}
private static List<LocalDate> matchDates(String text) {
List<LocalDate> dates = new ArrayList<>();
Matcher matcher = DATE_PATTERN.matcher(text);
while (matcher.find()) {
LocalDate date = toLocalDate(matcher.group(1), matcher.group(2), matcher.group(3));
if (date != null) {
dates.add(date);
}
}
return dates;
}
private static LocalDate toLocalDate(String yearText, String monthText, String dayText) {
try {
int year = Integer.parseInt(yearText);
int month = Integer.parseInt(monthText);
int day = Integer.parseInt(dayText);
return LocalDate.of(year, month, day);
} catch (NumberFormatException | DateTimeException exception) {
return null;
}
}
/**
* 止期字段名通常以 End / EndDate 结尾,区间文本应取最后一段日期。
*/
private static boolean isEndDateField(String fieldName) {
if (fieldName == null || fieldName.isEmpty()) {
return false;
}
return fieldName.endsWith("End") || fieldName.endsWith("EndDate")
|| fieldName.contains("EndDate") || fieldName.contains("ValidEnd");
}
}
@@ -29,12 +29,14 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import org.springblade.transport.jackson.FlexibleLocalDateDeserializer;
import java.io.Serial;
import java.time.LocalDate;
@@ -67,6 +69,7 @@ public class Driver extends TenantEntity {
* 出生年月
*/
@Schema(description = "出生年月")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate birthday;
/**
* 性别
@@ -118,11 +121,13 @@ public class Driver extends TenantEntity {
* 身份证有效期起
*/
@Schema(description = "身份证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate idCardValidStart;
/**
* 身份证有效期止
*/
@Schema(description = "身份证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate idCardValidEnd;
/**
* 身份证长期有效
@@ -148,11 +153,13 @@ public class Driver extends TenantEntity {
* 驾驶证有效期起
*/
@Schema(description = "驾驶证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseStartDate;
/**
* 驾驶证有效期止
*/
@Schema(description = "驾驶证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseEndDate;
/**
* 驾驶证长期有效
@@ -183,6 +190,7 @@ public class Driver extends TenantEntity {
* 从业资格证有效期止
*/
@Schema(description = "从业资格证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate qualificationEndDate;
/**
* 从业资格证长期有效
@@ -210,9 +218,10 @@ public class Driver extends TenantEntity {
@Schema(description = "手机号")
private String mobile;
/**
* 关联系统用户ID
* 关联小程序用户ID(blade_mp_user.id)
*/
@Schema(description = "关联系统用户ID")
@Schema(description = "关联小程序用户ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 与联系人关系
@@ -27,12 +27,14 @@ package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import org.springblade.transport.jackson.FlexibleLocalDateDeserializer;
import java.io.Serial;
import java.time.LocalDate;
@@ -125,6 +127,7 @@ public class TransportVehicle extends TenantEntity {
* 强制报废日期
*/
@Schema(description = "强制报废日期")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate compulsoryScrapDate;
/**
* 强制报废长期有效
@@ -145,11 +148,13 @@ public class TransportVehicle extends TenantEntity {
* 行驶证有效期起
*/
@Schema(description = "行驶证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseStartDate;
/**
* 行驶证有效期止
*/
@Schema(description = "行驶证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseEndDate;
/**
* 行驶证长期有效
@@ -185,11 +190,13 @@ public class TransportVehicle extends TenantEntity {
* 道路运输证有效期起
*/
@Schema(description = "道路运输证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate roadTransportCertStartDate;
/**
* 道路运输证有效期止
*/
@Schema(description = "道路运输证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate roadTransportCertEndDate;
/**
* 道路运输证长期有效
@@ -205,6 +212,7 @@ public class TransportVehicle extends TenantEntity {
* 道路运输年审有效期
*/
@Schema(description = "道路运输年审有效期")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate annualReviewEndDate;
/**
* 道路运输年审长期有效
@@ -220,6 +228,7 @@ public class TransportVehicle extends TenantEntity {
* 机动车登记日期
*/
@Schema(description = "机动车登记日期")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate registrationDate;
/**
* 机动车登记本图片
@@ -0,0 +1,114 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.cache;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUser;
/**
* 小程序用户缓存
*
* @author Chill
*/
@Slf4j
public class MpUserCache {
public static final String MP_USER_CACHE = "blade:mp_user";
private static final String MP_USER_CACHE_ID = "mp-user:id:";
private static IMpUserClient mpUserClient;
private static IMpUserClient getMpUserClient() {
if (mpUserClient == null) {
mpUserClient = SpringUtil.getBean(IMpUserClient.class);
}
return mpUserClient;
}
/**
* 按主键获取小程序用户
*/
public static MpUser getUser(Long userId) {
if (userId == null) {
return null;
}
try {
return CacheUtil.get(MP_USER_CACHE, MP_USER_CACHE_ID, userId, () -> loadUserById(userId));
} catch (RuntimeException exception) {
log.warn("小程序用户缓存读取失败,改为直连查询, userId={}", userId, exception);
return loadUserById(userId);
}
}
/**
* 获取小程序用户真实姓名(优先 realName → name → account → id)
*/
public static String getUserRealName(Long userId) {
if (userId == null) {
return StringPool.EMPTY;
}
try {
MpUser user = getUser(userId);
if (user == null) {
return Func.toStr(userId);
}
if (StringUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StringUtil.isNotBlank(user.getName())) {
return user.getName();
}
if (StringUtil.isNotBlank(user.getAccount())) {
return user.getAccount();
}
return Func.toStr(userId);
} catch (RuntimeException exception) {
log.warn("翻译小程序用户姓名失败,回退为用户ID, userId={}", userId, exception);
return Func.toStr(userId);
}
}
/**
* 清理小程序用户缓存
*/
public static void clear() {
CacheUtil.clear(MP_USER_CACHE);
CacheUtil.clear(MP_USER_CACHE, Boolean.FALSE);
}
private static MpUser loadUserById(Long userId) {
R<MpUser> result = getMpUserClient().userById(userId);
if (result == null || !result.isSuccess()) {
return null;
}
return result.getData();
}
}
@@ -0,0 +1,74 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.cache;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.User;
/**
* 用户展示名解析(系统用户优先,未命中再查小程序用户)
* <p>
* 小程序链路写入的审计人/提交人 ID 落在 blade_mp_user,需用本工具翻译姓名。
*
* @author Chill
*/
public final class UserDisplayCache {
private UserDisplayCache() {
}
/**
* 翻译用户姓名:先 blade_user,再 blade_mp_user,都没有则回退 ID
*/
public static String getUserRealName(Long userId) {
if (userId == null) {
return StringPool.EMPTY;
}
User user = UserCache.getUser(userId);
if (user != null && Func.isNotEmpty(user.getId())) {
return resolveName(user.getRealName(), user.getName(), user.getAccount(), userId);
}
MpUser mpUser = MpUserCache.getUser(userId);
if (mpUser != null && Func.isNotEmpty(mpUser.getId())) {
return resolveName(mpUser.getRealName(), mpUser.getName(), mpUser.getAccount(), userId);
}
return Func.toStr(userId);
}
private static String resolveName(String realName, String name, String account, Long userId) {
if (StringUtil.isNotBlank(realName)) {
return realName;
}
if (StringUtil.isNotBlank(name)) {
return name;
}
if (StringUtil.isNotBlank(account)) {
return account;
}
return Func.toStr(userId);
}
}
@@ -0,0 +1,121 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.feign;
import org.springblade.core.launch.constant.AppConstant;
import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 小程序用户 Feign 接口
*
* @author Chill
*/
@FeignClient(
value = AppConstant.APPLICATION_SYSTEM_NAME
)
public interface IMpUserClient {
String API_PREFIX = "/feign/client/mp-user";
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_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";
String SAVE_USER = API_PREFIX + "/save-user";
String UPDATE_USER = API_PREFIX + "/update-user";
String BIND_OPENID = API_PREFIX + "/bind-openid";
String SET_PASSWORD = API_PREFIX + "/set-password";
/**
* 按主键查询
*/
@GetMapping(USER_BY_ID)
R<MpUser> userById(@RequestParam("userId") Long userId);
/**
* 按租户 + 手机号查询
*/
@GetMapping(USER_BY_PHONE)
R<MpUser> userByPhone(@RequestParam("tenantId") String tenantId, @RequestParam("phone") String phone);
/**
* 按租户 + 账号查询
*/
@GetMapping(USER_BY_ACCOUNT)
R<MpUser> userByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account);
/**
* 按主键查询登录信息
*/
@GetMapping(USER_INFO_BY_ID)
R<MpUserInfo> userInfoById(@RequestParam("userId") Long userId);
/**
* 按租户 + 手机号查询登录信息
*/
@GetMapping(USER_INFO_BY_PHONE)
R<MpUserInfo> userInfoByPhone(@RequestParam("tenantId") String tenantId, @RequestParam("phone") String phone);
/**
* 按租户 + 账号查询登录信息
*/
@GetMapping(USER_INFO_BY_ACCOUNT)
R<MpUserInfo> userInfoByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account);
/**
* 新增小程序用户
*/
@PostMapping(SAVE_USER)
R<Boolean> saveUser(@RequestBody MpUser user);
/**
* 更新小程序用户
*/
@PostMapping(UPDATE_USER)
R<Boolean> updateUser(@RequestBody MpUser user);
/**
* 绑定/更新微信小程序 openid
*/
@PostMapping(BIND_OPENID)
R<Boolean> bindOpenId(@RequestParam("tenantId") String tenantId,
@RequestParam("userId") Long userId,
@RequestParam("openid") String openid);
/**
* 设置小程序用户密码
*/
@PostMapping(SET_PASSWORD)
R<Boolean> setPassword(@RequestParam("userId") Long userId,
@RequestParam("password") String password,
@RequestParam("password2") String password2);
}
@@ -0,0 +1,131 @@
/**
* 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.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import org.springblade.core.tool.jackson.Sensitive;
import org.springblade.core.tool.sensitive.SensitiveType;
import java.io.Serial;
import java.util.Date;
/**
* 小程序用户实体(仅用于小程序登录,与 blade_user 分离)
*
* @author Chill
*/
@Data
@TableName("blade_mp_user")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "小程序用户")
public class MpUser extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户编号
*/
@Schema(description = "用户编号")
private String code;
/**
* 账号
*/
@Schema(description = "账号")
private String account;
/**
* 密码
*/
@Schema(description = "密码")
private String password;
/**
* 昵称
*/
@Schema(description = "昵称")
private String name;
/**
* 真名
*/
@Schema(description = "真名")
private String realName;
/**
* 头像
*/
@Schema(description = "头像")
private String avatar;
/**
* 邮箱
*/
@Sensitive(type = SensitiveType.EMAIL)
@Schema(description = "邮箱")
private String email;
/**
* 手机
*/
@Sensitive(type = SensitiveType.MOBILE)
@Schema(description = "手机")
private String phone;
/**
* 生日
*/
@Schema(description = "生日")
private Date birthday;
/**
* 性别
*/
@Schema(description = "性别")
private Integer sex;
/**
* 微信小程序 openid
*/
@Schema(description = "微信小程序openid")
private String openid;
/**
* 小程序身份,逗号分隔:driver,admin
*/
@Schema(description = "小程序身份")
private String identities;
/**
* 部门id
*/
@Schema(description = "部门id")
private String deptId;
/**
* 岗位id
*/
@Schema(description = "岗位id")
private String postId;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
}
@@ -0,0 +1,69 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springblade.core.tool.support.Kv;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 小程序用户登录信息
*
* @author Chill
*/
@Data
@Schema(description = "小程序用户信息")
public class MpUserInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户基础信息
*/
@Schema(description = "小程序用户")
private MpUser user;
/**
* 身份编码集合
*/
@Schema(description = "身份集合")
private List<String> identities;
/**
* 权限标识集合
*/
@Schema(description = "权限集合")
private List<String> permissions;
/**
* 拓展信息
*/
@Schema(description = "拓展信息")
private Kv detail;
}
@@ -0,0 +1,148 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 小程序用户身份枚举(可叠加,后续可扩展)
*
* @author Chill
*/
@Getter
@AllArgsConstructor
public enum MpUserIdentity {
/**
* 司机
*/
DRIVER("driver", "司机"),
/**
* 管理员(调度端)
*/
ADMIN("admin", "管理员"),
;
/**
* 身份编码
*/
private final String code;
/**
* 身份名称
*/
private final String label;
/**
* 管理员身份对应的小程序权限码(兼容原 mp_auth)
*/
public static final String PERMISSION_MP_AUTH = "mp_auth";
/**
* detail 中标记小程序用户的 type 值
*/
public static final String DETAIL_TYPE = "mp";
/**
* 解析身份编码
*/
public static MpUserIdentity of(String code) {
if (Func.isBlank(code)) {
return null;
}
String normalized = code.trim().toLowerCase(Locale.ROOT);
return Arrays.stream(values())
.filter(item -> item.code.equals(normalized))
.findFirst()
.orElse(null);
}
/**
* 拆分身份串为编码列表(去重、保序)
*/
public static List<String> split(String identities) {
if (Func.isBlank(identities)) {
return Collections.emptyList();
}
Set<String> identitySet = new LinkedHashSet<>();
for (String part : Func.toStrList(identities)) {
if (Func.isBlank(part)) {
continue;
}
String code = part.trim().toLowerCase(Locale.ROOT);
if (of(code) != null || StringUtil.isNotBlank(code)) {
identitySet.add(code);
}
}
return new ArrayList<>(identitySet);
}
/**
* 合并身份,确保包含指定身份
*/
public static String merge(String identities, String... appendCodes) {
Set<String> identitySet = new LinkedHashSet<>(split(identities));
if (appendCodes != null) {
for (String code : appendCodes) {
if (Func.isNotBlank(code)) {
identitySet.add(code.trim().toLowerCase(Locale.ROOT));
}
}
}
return identitySet.stream().collect(Collectors.joining(","));
}
/**
* 是否包含指定身份
*/
public static boolean has(String identities, MpUserIdentity identity) {
if (identity == null) {
return false;
}
return split(identities).contains(identity.getCode());
}
/**
* 根据身份生成小程序权限码(不再依赖系统角色菜单)
*/
public static List<String> toPermissions(String identities) {
List<String> permissions = new ArrayList<>();
if (has(identities, ADMIN)) {
permissions.add(PERMISSION_MP_AUTH);
}
return permissions;
}
}
@@ -0,0 +1,65 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.utils;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.pojo.enums.MpUserIdentity;
/**
* 小程序登录态识别工具
*
* @author Chill
*/
public final class MpUserAuth {
private MpUserAuth() {
}
/**
* 当前登录用户是否为小程序用户
*/
public static boolean isMpUser() {
return isMpUser(AuthUtil.getUser());
}
/**
* 判断指定登录用户是否为小程序用户
*/
public static boolean isMpUser(BladeUser bladeUser) {
if (bladeUser == null) {
return false;
}
Kv detail = bladeUser.getDetail();
if (detail != null && MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(detail.get("type")))) {
return true;
}
// 兼容:无系统角色且 roleName 为小程序身份
return Func.isBlank(bladeUser.getRoleId())
&& Func.isNotBlank(bladeUser.getRoleName())
&& !MpUserIdentity.split(bladeUser.getRoleName()).isEmpty();
}
}
@@ -53,11 +53,14 @@ import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.system.excel.UserExcel;
import org.springblade.system.excel.UserImporter;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.service.IOASyncService;
import org.springblade.system.service.IMpUserService;
import org.springblade.system.service.IUserService;
import org.springblade.system.utils.MpUserAuth;
import org.springblade.system.wrapper.UserWrapper;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -79,6 +82,7 @@ import java.util.Map;
public class UserController {
private final IUserService userService;
private final IMpUserService mpUserService;
private final IOASyncService oaSyncService;
/**
@@ -101,6 +105,10 @@ public class UserController {
@Operation(summary = "查看详情", description = "传入id")
@GetMapping("/info")
public R<UserVO> info(BladeUser user) {
if (MpUserAuth.isMpUser(user)) {
MpUser mpUser = mpUserService.getById(user.getUserId());
return R.data(toMpUserVO(mpUser));
}
User detail = userService.getById(user.getUserId());
return R.data(UserWrapper.build().entityVO(detail));
}
@@ -252,6 +260,7 @@ public class UserController {
* 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密)
* <p>
* 对外路径:/blade-system/user/password ;网关别名 /blade-user/password 亦可到达。
* 小程序用户写入 blade_mp_user,系统用户仍写入 blade_user。
*/
@PostMapping("/password")
@ApiOperationSupport(order = 10)
@@ -259,6 +268,9 @@ public class UserController {
public R password(BladeUser user,
@Parameter(description = "新密码", required = true) @RequestParam String password,
@Parameter(description = "确认密码", required = true) @RequestParam String password2) {
if (MpUserAuth.isMpUser(user)) {
return R.status(mpUserService.setPassword(user.getUserId(), password, password2));
}
return R.status(userService.setPassword(user.getUserId(), password, password2));
}
@@ -438,4 +450,34 @@ public class UserController {
return R.data(list);
}
/**
* 小程序用户转 UserVO(兼容原 /user/info 响应结构)
*/
private UserVO toMpUserVO(MpUser mpUser) {
if (mpUser == null) {
return null;
}
UserVO userVO = new UserVO();
userVO.setId(mpUser.getId());
userVO.setTenantId(mpUser.getTenantId());
userVO.setCode(mpUser.getCode());
userVO.setAccount(mpUser.getAccount());
userVO.setName(mpUser.getName());
userVO.setRealName(mpUser.getRealName());
userVO.setAvatar(mpUser.getAvatar());
userVO.setEmail(mpUser.getEmail());
userVO.setPhone(mpUser.getPhone());
userVO.setBirthday(mpUser.getBirthday());
userVO.setSex(mpUser.getSex());
userVO.setDeptId(mpUser.getDeptId());
userVO.setPostId(mpUser.getPostId());
userVO.setRemark(mpUser.getRemark());
userVO.setStatus(mpUser.getStatus());
userVO.setCreateTime(mpUser.getCreateTime());
userVO.setUpdateTime(mpUser.getUpdateTime());
// 身份放入 roleName,便于前端兼容展示
userVO.setRoleName(mpUser.getIdentities());
return userVO;
}
}
@@ -43,7 +43,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户手机号变更(小程序「修改手机号」)
* 小程序用户手机号变更(写入 blade_mp_user)
* <p>
* 对外路径:/blade-system/user/phone/** ;网关别名 /blade-user/phone/** 亦可到达。
*
@@ -0,0 +1,110 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.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.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.service.IMpUserService;
import org.springframework.web.bind.annotation.GetMapping;
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 MpUserClient implements IMpUserClient {
private final IMpUserService mpUserService;
@Override
@GetMapping(USER_BY_ID)
public R<MpUser> userById(Long userId) {
return R.data(mpUserService.getById(userId));
}
@Override
@GetMapping(USER_BY_PHONE)
public R<MpUser> userByPhone(String tenantId, String phone) {
return R.data(mpUserService.getByPhone(tenantId, phone));
}
@Override
@GetMapping(USER_BY_ACCOUNT)
public R<MpUser> userByAccount(String tenantId, String account) {
return R.data(mpUserService.getByAccount(tenantId, account));
}
@Override
@GetMapping(USER_INFO_BY_ID)
public R<MpUserInfo> userInfoById(Long userId) {
return R.data(mpUserService.userInfo(userId));
}
@Override
@GetMapping(USER_INFO_BY_PHONE)
public R<MpUserInfo> userInfoByPhone(String tenantId, String phone) {
return R.data(mpUserService.userInfoByPhone(tenantId, phone));
}
@Override
@GetMapping(USER_INFO_BY_ACCOUNT)
public R<MpUserInfo> userInfoByAccount(String tenantId, String account) {
return R.data(mpUserService.userInfoByAccount(tenantId, account));
}
@Override
@PostMapping(SAVE_USER)
public R<Boolean> saveUser(@RequestBody MpUser user) {
return R.data(mpUserService.submit(user));
}
@Override
@PostMapping(UPDATE_USER)
public R<Boolean> updateUser(@RequestBody MpUser user) {
return R.data(mpUserService.updateUser(user));
}
@Override
@PostMapping(BIND_OPENID)
public R<Boolean> bindOpenId(String tenantId, Long userId, String openid) {
return R.data(mpUserService.bindOpenId(tenantId, userId, openid));
}
@Override
@PostMapping(SET_PASSWORD)
public R<Boolean> setPassword(Long userId, String password, String password2) {
return R.data(mpUserService.setPassword(userId, password, password2));
}
}
@@ -0,0 +1,34 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.system.pojo.entity.MpUser;
/**
* 小程序用户 Mapper
*
* @author Chill
*/
public interface MpUserMapper extends BaseMapper<MpUser> {
}
@@ -0,0 +1,81 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.service;
import org.springblade.core.mp.base.BaseService;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
/**
* 小程序用户 服务类
*
* @author Chill
*/
public interface IMpUserService extends BaseService<MpUser> {
/**
* 按租户 + 手机号查询
*/
MpUser getByPhone(String tenantId, String phone);
/**
* 按租户 + 账号查询
*/
MpUser getByAccount(String tenantId, String account);
/**
* 按主键组装登录信息
*/
MpUserInfo userInfo(Long userId);
/**
* 按租户 + 手机号组装登录信息
*/
MpUserInfo userInfoByPhone(String tenantId, String phone);
/**
* 按租户 + 账号组装登录信息
*/
MpUserInfo userInfoByAccount(String tenantId, String account);
/**
* 保存小程序用户
*/
boolean submit(MpUser user);
/**
* 更新小程序用户
*/
boolean updateUser(MpUser user);
/**
* 绑定/更新 openid
*/
boolean bindOpenId(String tenantId, Long userId, String openid);
/**
* 当前小程序用户设置密码
*/
boolean setPassword(Long userId, String password, String password2);
}
@@ -0,0 +1,272 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.common.utils.PasswordRuleUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.DigestUtil;
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.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.service.IMpUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
/**
* 小程序用户 服务实现类
*
* @author Chill
*/
@Service
@RequiredArgsConstructor
public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> implements IMpUserService {
@Override
public MpUser getByPhone(String tenantId, String phone) {
if (Func.isBlank(tenantId) || Func.isBlank(phone)) {
return null;
}
return getOne(Wrappers.<MpUser>lambdaQuery()
.eq(MpUser::getTenantId, tenantId)
.eq(MpUser::getPhone, phone)
.orderByDesc(MpUser::getCreateTime)
.last("LIMIT 1"));
}
@Override
public MpUser getByAccount(String tenantId, String account) {
if (Func.isBlank(tenantId) || Func.isBlank(account)) {
return null;
}
return getOne(Wrappers.<MpUser>lambdaQuery()
.eq(MpUser::getTenantId, tenantId)
.eq(MpUser::getAccount, account)
.orderByDesc(MpUser::getCreateTime)
.last("LIMIT 1"));
}
@Override
public MpUserInfo userInfo(Long userId) {
if (Func.isEmpty(userId)) {
return null;
}
return buildUserInfo(getById(userId));
}
@Override
public MpUserInfo userInfoByPhone(String tenantId, String phone) {
return buildUserInfo(getByPhone(tenantId, phone));
}
@Override
public MpUserInfo userInfoByAccount(String tenantId, String account) {
return buildUserInfo(getByAccount(tenantId, account));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(MpUser user) {
prepare(user);
validate(user);
MpUser existed = getByPhone(user.getTenantId(), user.getPhone());
if (existed != null && Func.isNotEmpty(existed.getId())) {
throw new ServiceException("该手机号已存在小程序用户");
}
boolean saved = save(user);
if (saved) {
MpUserCache.clear();
}
return saved;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(MpUser user) {
if (user == null || Func.isEmpty(user.getId())) {
throw new ServiceException("小程序用户主键不能为空");
}
prepare(user);
validate(user);
MpUser existed = getByPhone(user.getTenantId(), user.getPhone());
if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(user.getId())) {
throw new ServiceException("该手机号已存在小程序用户");
}
boolean updated = updateById(user);
if (updated) {
MpUserCache.clear();
}
return updated;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean bindOpenId(String tenantId, Long userId, String openid) {
if (Func.isBlank(tenantId) || Func.isEmpty(userId) || Func.isBlank(openid)) {
throw new ServiceException("绑定 openid 参数不完整");
}
MpUser user = getById(userId);
if (user == null || Func.isEmpty(user.getId())) {
throw new ServiceException("小程序用户不存在");
}
if (!tenantId.equals(user.getTenantId())) {
throw new ServiceException("租户不匹配,无法绑定 openid");
}
MpUser byOpenId = getOne(Wrappers.<MpUser>lambdaQuery()
.eq(MpUser::getTenantId, tenantId)
.eq(MpUser::getOpenid, openid)
.last("LIMIT 1"));
if (byOpenId != null && !byOpenId.getId().equals(userId)) {
throw new ServiceException("该微信已绑定其他小程序用户");
}
MpUser patch = new MpUser();
patch.setId(userId);
patch.setOpenid(openid);
boolean updated = updateById(patch);
if (updated) {
MpUserCache.clear();
}
return updated;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean setPassword(Long userId, String password, String password2) {
if (!Objects.equals(password, password2)) {
throw new ServiceException("两次输入密码不一致!");
}
if (!PasswordRuleUtil.isValid(password)) {
throw new ServiceException(PasswordRuleUtil.RULE_MESSAGE);
}
MpUser user = getById(userId);
if (user == null || Func.isEmpty(user.getId())) {
throw new ServiceException("小程序用户不存在");
}
MpUser patch = new MpUser();
patch.setId(userId);
patch.setPassword(DigestUtil.encrypt(password));
patch.setUpdateTime(DateUtil.now());
boolean updated = updateById(patch);
if (updated) {
MpUserCache.clear();
}
return updated;
}
private MpUserInfo buildUserInfo(MpUser user) {
if (user == null || Func.isEmpty(user.getId())) {
return null;
}
List<String> identities = MpUserIdentity.split(user.getIdentities());
if (identities.isEmpty()) {
throw new ServiceException("小程序用户未配置身份,无法登录");
}
MpUserInfo userInfo = new MpUserInfo();
userInfo.setUser(user);
userInfo.setIdentities(identities);
userInfo.setPermissions(MpUserIdentity.toPermissions(user.getIdentities()));
userInfo.setDetail(Kv.create()
.set("type", MpUserIdentity.DETAIL_TYPE)
.set("identities", user.getIdentities())
.set("openid", Func.toStr(user.getOpenid(), "")));
return userInfo;
}
private void prepare(MpUser user) {
if (user == null) {
return;
}
user.setAccount(trimToNull(user.getAccount()));
user.setPhone(trimToNull(user.getPhone()));
user.setName(trimToNull(user.getName()));
user.setRealName(trimToNull(user.getRealName()));
user.setAvatar(trimToNull(user.getAvatar()));
user.setEmail(trimToNull(user.getEmail()));
user.setOpenid(trimToNull(user.getOpenid()));
user.setDeptId(trimToNull(user.getDeptId()));
user.setPostId(trimToNull(user.getPostId()));
user.setRemark(trimToNull(user.getRemark()));
user.setIdentities(normalizeIdentities(user.getIdentities()));
if (Func.isBlank(user.getAccount()) && Func.isNotBlank(user.getPhone())) {
user.setAccount(user.getPhone());
}
if (Func.isBlank(user.getName()) && Func.isNotBlank(user.getRealName())) {
user.setName(user.getRealName());
}
if (user.getStatus() == null) {
user.setStatus(1);
}
}
private void validate(MpUser user) {
if (user == null) {
throw new ServiceException("小程序用户不能为空");
}
if (Func.isBlank(user.getTenantId())) {
throw new ServiceException("租户信息不能为空");
}
if (Func.isBlank(user.getPhone())) {
throw new ServiceException("手机号不能为空");
}
if (Func.isBlank(user.getIdentities())) {
throw new ServiceException("小程序身份不能为空");
}
List<String> identities = MpUserIdentity.split(user.getIdentities());
if (identities.isEmpty()) {
throw new ServiceException("小程序身份不能为空");
}
for (String code : identities) {
if (MpUserIdentity.of(code) == null) {
throw new ServiceException("不支持的小程序身份:" + code);
}
}
}
private String normalizeIdentities(String identities) {
List<String> identityList = MpUserIdentity.split(identities);
if (identityList.isEmpty()) {
return null;
}
return StringUtil.join(identityList);
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimValue = value.trim();
return trimValue.isEmpty() ? null : trimValue;
}
}
@@ -18,9 +18,6 @@
* 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.service.impl;
@@ -28,7 +25,6 @@ package org.springblade.system.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.secure.utils.AuthUtil;
@@ -37,21 +33,21 @@ import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.resource.feign.ISmsClient;
import org.springblade.resource.utils.SmsUtil;
import org.springblade.system.cache.MpUserCache;
import org.springblade.system.pojo.dto.PhoneChangeDTO;
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.service.IMpUserService;
import org.springblade.system.service.IUserPhoneService;
import org.springblade.system.service.IUserService;
import org.springblade.system.utils.MpUserAuth;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.util.regex.Pattern;
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
/**
* 用户手机号变更服务实现
* 小程序用户手机号变更服务实现(写入 blade_mp_user)
*
* @author Chill
*/
@@ -68,24 +64,22 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
/**
* 原手机号已校验凭证(Redis)
*/
private static final String PHONE_CHANGE_VERIFIED_KEY = "blade:user:phone:change:verified:";
private static final String PHONE_CHANGE_VERIFIED_KEY = "blade:mp-user:phone:change:verified:";
private static final Duration PHONE_CHANGE_VERIFIED_TTL = Duration.ofMinutes(15);
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
private final IUserService userService;
private final IMpUserService mpUserService;
private final ISmsClient smsClient;
private final BladeRedis bladeRedis;
@Override
public R sendCode(String phone) {
assertMpLogin();
String normalizedPhone = normalizePhone(phone);
Long userId = AuthUtil.getUserId();
if (Func.isEmpty(userId)) {
throw new ServiceException("请先登录");
}
User currentUser = requireCurrentUser(userId);
MpUser currentUser = requireCurrentUser(userId);
String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId());
boolean isCurrentPhone = StringUtil.equals(normalizedPhone, Func.toStr(currentUser.getPhone()));
if (!isCurrentPhone) {
@@ -100,16 +94,14 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
@Override
public boolean verifyOldPhone(PhoneVerifyDTO phoneVerify) {
assertMpLogin();
Long userId = AuthUtil.getUserId();
if (Func.isEmpty(userId)) {
throw new ServiceException("请先登录");
}
String id = Func.toStr(phoneVerify.getId()).trim();
String code = Func.toStr(phoneVerify.getCode()).trim();
if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) {
throw new ServiceException("请先获取并填写验证码");
}
User currentUser = requireCurrentUser(userId);
MpUser currentUser = requireCurrentUser(userId);
String oldPhone = Func.toStr(currentUser.getPhone()).trim();
if (StringUtil.isBlank(oldPhone)) {
throw new ServiceException("当前账号未绑定手机号");
@@ -122,10 +114,8 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean changePhone(PhoneChangeDTO phoneChange) {
assertMpLogin();
Long userId = AuthUtil.getUserId();
if (Func.isEmpty(userId)) {
throw new ServiceException("请先登录");
}
String verified = Func.toStr(bladeRedis.get(PHONE_CHANGE_VERIFIED_KEY + userId));
if (!StringUtil.equals(verified, "1")) {
throw new ServiceException("请先完成原手机号验证");
@@ -136,7 +126,7 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) {
throw new ServiceException("请先获取并填写验证码");
}
User currentUser = requireCurrentUser(userId);
MpUser currentUser = requireCurrentUser(userId);
String oldPhone = Func.toStr(currentUser.getPhone()).trim();
if (StringUtil.equals(newPhone, oldPhone)) {
throw new ServiceException("新手机号不可与当前手机号相同");
@@ -145,27 +135,49 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
assertPhoneAvailable(tenantId, newPhone, userId);
validateSms(tenantId, id, code, newPhone);
User updateUser = new User();
MpUser updateUser = new MpUser();
updateUser.setId(userId);
updateUser.setTenantId(tenantId);
updateUser.setPhone(newPhone);
updateUser.setName(currentUser.getName());
updateUser.setRealName(currentUser.getRealName());
updateUser.setIdentities(currentUser.getIdentities());
updateUser.setSex(currentUser.getSex());
updateUser.setDeptId(currentUser.getDeptId());
updateUser.setPostId(currentUser.getPostId());
updateUser.setAvatar(currentUser.getAvatar());
updateUser.setEmail(currentUser.getEmail());
updateUser.setRemark(currentUser.getRemark());
updateUser.setStatus(currentUser.getStatus());
// 账号若等于原手机号,同步更新,保证短信登录可用
if (StringUtil.isNotBlank(oldPhone) && StringUtil.equals(oldPhone, Func.toStr(currentUser.getAccount()))) {
assertAccountAvailable(tenantId, newPhone, userId);
updateUser.setAccount(newPhone);
} else {
updateUser.setAccount(currentUser.getAccount());
}
boolean updated = userService.updateById(updateUser);
boolean updated = mpUserService.updateUser(updateUser);
if (!updated) {
throw new ServiceException("手机号修改失败");
}
bladeRedis.del(PHONE_CHANGE_VERIFIED_KEY + userId);
CacheUtil.clear(USER_CACHE);
MpUserCache.clear();
return true;
}
private User requireCurrentUser(Long userId) {
User user = userService.getById(userId);
private void assertMpLogin() {
if (Func.isEmpty(AuthUtil.getUserId())) {
throw new ServiceException("请先登录");
}
if (!MpUserAuth.isMpUser()) {
throw new ServiceException("仅支持小程序用户修改手机号");
}
}
private MpUser requireCurrentUser(Long userId) {
MpUser user = mpUserService.getById(userId);
if (user == null) {
throw new ServiceException("用户不存在");
throw new ServiceException("小程序用户不存在");
}
return user;
}
@@ -178,11 +190,11 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
}
private void assertPhoneAvailable(String tenantId, String phone, Long excludeUserId) {
Long phoneCount = userService.count(
Wrappers.<User>lambdaQuery()
.eq(User::getTenantId, tenantId)
.eq(User::getPhone, phone)
.ne(User::getId, excludeUserId)
Long phoneCount = mpUserService.count(
Wrappers.<MpUser>lambdaQuery()
.eq(MpUser::getTenantId, tenantId)
.eq(MpUser::getPhone, phone)
.ne(MpUser::getId, excludeUserId)
);
if (phoneCount != null && phoneCount > 0L) {
throw new ServiceException(StringUtil.format("当前手机 [{}] 已存在!", phone));
@@ -190,11 +202,11 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
}
private void assertAccountAvailable(String tenantId, String account, Long excludeUserId) {
Long accountCount = userService.count(
Wrappers.<User>lambdaQuery()
.eq(User::getTenantId, tenantId)
.eq(User::getAccount, account)
.ne(User::getId, excludeUserId)
Long accountCount = mpUserService.count(
Wrappers.<MpUser>lambdaQuery()
.eq(MpUser::getTenantId, tenantId)
.eq(MpUser::getAccount, account)
.ne(MpUser::getId, excludeUserId)
);
if (accountCount != null && accountCount > 0L) {
throw new ServiceException(StringUtil.format("当前用户 [{}] 已存在!", account));
@@ -33,6 +33,7 @@ import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.system.cache.UserDisplayCache;
import org.springblade.transport.mapper.TransportCertRecordMapper;
import org.springblade.transport.pojo.dto.PersonCertSubmitRequest;
import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest;
@@ -124,10 +125,8 @@ public class CertAuditServiceImpl implements ICertAuditService {
detail.setStatusText(CertificationRuleSupport.statusText(record.getStatus()));
detail.setSubmitTime(record.getSubmitTime());
detail.setSubmitUser(record.getSubmitUser());
// 面向前端的展示字段必须由服务端填好,否则审核页「提交人」一栏恒为空白
detail.setSubmitUserName(UserCache.getUserRealName(record.getSubmitUser()));
// 审核留痕同理:审核人姓名由服务端查好,前端不做用户ID → 姓名的二次请求。
// 记录还在「审核中」时审核人与审核时间都为空,由前端显示占位符
// 提交人来自小程序用户;审核人来自后台系统用户
detail.setSubmitUserName(UserDisplayCache.getUserRealName(record.getSubmitUser()));
detail.setAuditUser(record.getAuditUser());
detail.setAuditTime(record.getAuditTime());
detail.setAuditUserName(UserCache.getUserRealName(record.getAuditUser()));
@@ -29,8 +29,8 @@ import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
@@ -57,7 +57,7 @@ public class DriverAppServiceImpl implements IDriverAppService {
private final IDriverService driverService;
private final ITransportVehicleService transportVehicleService;
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
@Override
public DriverVO currentByPhone(String mobile) {
@@ -205,10 +205,9 @@ public class DriverAppServiceImpl implements IDriverAppService {
if (isMobileLike(account)) {
return account.trim();
}
// 用户中心的 phone 带 @Sensitive(MOBILE),经 Feign 回来是「175****12082」这类脱敏串,
// 拿它去 eq(mobile) 永远查不到、拿它比对本人的真实号码又必然不等。
// 小程序用户 phone 带 @Sensitive(MOBILE),经 Feign 回来是脱敏串,
// 因此这里只取未脱敏的 account;脱敏的 phone 一律不用。
R<User> result = userClient.userInfoById(userId);
R<MpUser> result = mpUserClient.userById(userId);
if (result != null && R.isSuccess(result) && result.getData() != null) {
String userAccount = result.getData().getAccount();
if (isMobileLike(userAccount)) {
@@ -327,7 +327,7 @@ public class DriverCertServiceImpl implements IDriverCertService {
* 状态必须显式设置为待审:框架在插入时会把空的 {@code status} 静默填成 1,
* 而 1 在本表是「已通过」,那会让一次提交凭空变成审核通过。
*
* @param submitUser 提交人**用户ID**(blade_user.id)——审核页要靠它翻译成姓名。
* @param submitUser 提交人**用户ID**(blade_mp_user.id)——审核页要靠它翻译成姓名。
* 不要传司机档案ID:两者都是雪花ID、类型也相同,传错不会报任何错,
* 只会让「提交人」一栏显示成一串数字。
*/
@@ -36,12 +36,11 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.UserInfo;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.enums.UserSex;
import org.springblade.system.pojo.enums.UserType;
import org.springblade.transport.excel.DriverExcel;
import org.springblade.transport.mapper.DriverMapper;
import org.springblade.transport.pojo.entity.Driver;
@@ -76,7 +75,7 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
private static final int DEFAULT_ENABLED_STATUS = 1;
private static final int DEFAULT_FALSE = 0;
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
private final ISysClient sysClient;
@Override
@@ -310,13 +309,13 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
tenantId = driver.getTenantId();
}
if (Func.isEmpty(tenantId)) {
throw new ServiceException("租户信息为空,无法同步系统用户");
throw new ServiceException("租户信息为空,无法同步小程序用户");
}
String mobile = driver.getMobile();
if (Func.isEmpty(mobile) || mobile.length() < 6) {
throw new ServiceException("手机号长度不足,无法同步系统用户");
throw new ServiceException("手机号长度不足,无法同步小程序用户");
}
// 编辑时前端可能不传 userId,从库中补齐已关联用户
// 编辑时前端可能不传 userId,从库中补齐已关联小程序用户
if (Func.isEmpty(driver.getUserId()) && Func.isNotEmpty(driver.getId())) {
Driver dbDriver = getById(driver.getId());
if (dbDriver != null && Func.isNotEmpty(dbDriver.getUserId())) {
@@ -324,95 +323,83 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
}
}
R<String> roleResult = sysClient.getRoleIdByAlias(tenantId, "driver");
if (roleResult == null || !R.isSuccess(roleResult) || Func.isEmpty(roleResult.getData())) {
throw new ServiceException("未配置角色别名为driver的角色,请先在系统角色中维护");
}
String roleId = roleResult.getData();
// 部门可选:匹配到则写入小程序用户,匹配不到不阻断同步
String deptId = null;
R<String> deptResult = sysClient.getDeptIds(tenantId, driver.getOrganizationName());
if (deptResult == null || !R.isSuccess(deptResult) || Func.isEmpty(deptResult.getData())) {
throw new ServiceException("所属组织未匹配到系统部门,无法同步用户");
if (deptResult != null && R.isSuccess(deptResult) && Func.isNotEmpty(deptResult.getData())) {
deptId = deptResult.getData();
}
String deptId = deptResult.getData();
User existing = findExistingUser(driver, tenantId, mobile);
MpUser existing = findExistingMpUser(driver, tenantId, mobile);
if (existing == null || Func.isEmpty(existing.getId())) {
// 司机未关联有效用户时新建;初始密码由系统用户保存逻辑按默认参数/强度规则填充
createAndLinkDriverUser(driver, tenantId, mobile, roleId, deptId);
createAndLinkDriverMpUser(driver, tenantId, mobile, deptId);
} else {
updateAndLinkDriverUser(driver, existing, tenantId, mobile, roleId, deptId);
updateAndLinkDriverMpUser(driver, existing, tenantId, mobile, deptId);
}
}
private User findExistingUser(Driver driver, String tenantId, String mobile) {
private MpUser findExistingMpUser(Driver driver, String tenantId, String mobile) {
if (Func.isNotEmpty(driver.getUserId())) {
R<User> byId = userClient.userInfoById(driver.getUserId());
R<MpUser> byId = mpUserClient.userById(driver.getUserId());
if (byId != null && R.isSuccess(byId) && byId.getData() != null && Func.isNotEmpty(byId.getData().getId())) {
return byId.getData();
}
// 关联的用户已不存在,清空后按手机号重建
// 关联的小程序用户已不存在,清空后按手机号重建
driver.setUserId(null);
}
return findExistingUserByAccountOrPhone(tenantId, mobile);
return findExistingMpUserByPhone(tenantId, mobile);
}
private User findExistingUserByAccountOrPhone(String tenantId, String mobile) {
R<User> byAccount = userClient.userByAccount(tenantId, mobile);
if (byAccount != null && R.isSuccess(byAccount) && byAccount.getData() != null
&& Func.isNotEmpty(byAccount.getData().getId())) {
return byAccount.getData();
}
R<UserInfo> byPhone = userClient.userInfoByPhone(tenantId, mobile, UserType.WEB.getName());
private MpUser findExistingMpUserByPhone(String tenantId, String mobile) {
R<MpUser> byPhone = mpUserClient.userByPhone(tenantId, mobile);
if (byPhone != null && R.isSuccess(byPhone) && byPhone.getData() != null
&& byPhone.getData().getUser() != null && Func.isNotEmpty(byPhone.getData().getUser().getId())) {
return byPhone.getData().getUser();
&& Func.isNotEmpty(byPhone.getData().getId())) {
return byPhone.getData();
}
return null;
}
private void createAndLinkDriverUser(Driver driver, String tenantId, String mobile, String roleId, String deptId) {
User user = buildSyncUser(null, tenantId, mobile, driver.getDriverName(), driver.getGender(), roleId, deptId);
// 不传密码:交由 UserServiceImpl.saveUser 使用系统默认初始密码或自动生成合规密码
user.setUserType(UserType.WEB.getCategory());
private void createAndLinkDriverMpUser(Driver driver, String tenantId, String mobile, String deptId) {
MpUser user = buildSyncMpUser(null, tenantId, mobile, driver.getDriverName(), driver.getGender(),
deptId, MpUserIdentity.DRIVER.getCode());
R<Boolean> saveResult = null;
R<Boolean> saveResult;
try {
saveResult = userClient.saveUser(user);
saveResult = mpUserClient.saveUser(user);
} catch (Exception ex) {
User existed = findExistingUserByAccountOrPhone(tenantId, mobile);
MpUser existed = findExistingMpUserByPhone(tenantId, mobile);
if (existed != null) {
updateAndLinkDriverUser(driver, existed, tenantId, mobile, roleId, deptId);
updateAndLinkDriverMpUser(driver, existed, tenantId, mobile, deptId);
return;
}
throw new ServiceException(Func.isNotEmpty(ex.getMessage()) ? ex.getMessage() : "同步系统用户失败");
throw new ServiceException(Func.isNotEmpty(ex.getMessage()) ? ex.getMessage() : "同步小程序用户失败");
}
if (!isFeignSuccess(saveResult)) {
User existed = findExistingUserByAccountOrPhone(tenantId, mobile);
MpUser existed = findExistingMpUserByPhone(tenantId, mobile);
if (existed != null) {
updateAndLinkDriverUser(driver, existed, tenantId, mobile, roleId, deptId);
updateAndLinkDriverMpUser(driver, existed, tenantId, mobile, deptId);
return;
}
throw new ServiceException(resolveFeignError(saveResult, "同步系统用户失败"));
throw new ServiceException(resolveFeignError(saveResult, "同步小程序用户失败"));
}
R<User> created = userClient.userByAccount(tenantId, mobile);
User createdUser = (created != null && R.isSuccess(created)) ? created.getData() : null;
MpUser createdUser = findExistingMpUserByPhone(tenantId, mobile);
if (createdUser == null || Func.isEmpty(createdUser.getId())) {
createdUser = findExistingUserByAccountOrPhone(tenantId, mobile);
}
if (createdUser == null || Func.isEmpty(createdUser.getId())) {
throw new ServiceException("系统用户创建成功但无法回查用户ID");
throw new ServiceException("小程序用户创建成功但无法回查用户ID");
}
linkDriverUserId(driver, createdUser.getId());
}
private void updateAndLinkDriverUser(Driver driver, User existing, String tenantId, String mobile, String roleId, String deptId) {
User user = buildSyncUser(existing.getId(), tenantId, mobile, driver.getDriverName(), driver.getGender(), roleId, deptId);
R<Boolean> updateResult = userClient.updateUser(user);
// updateById 无字段变化时可能返回 false,但业务上仍视为成功,以接口 code 为准
private void updateAndLinkDriverMpUser(Driver driver, MpUser existing, String tenantId, String mobile, String deptId) {
// 保留已有管理员等身份,确保包含司机身份
String identities = MpUserIdentity.merge(existing.getIdentities(), MpUserIdentity.DRIVER.getCode());
MpUser user = buildSyncMpUser(existing.getId(), tenantId, mobile, driver.getDriverName(), driver.getGender(),
deptId, identities);
// 更新时不覆盖 openid
user.setOpenid(null);
R<Boolean> updateResult = mpUserClient.updateUser(user);
if (!isFeignSuccess(updateResult)) {
throw new ServiceException(resolveFeignError(updateResult, "同步更新系统用户失败"));
throw new ServiceException(resolveFeignError(updateResult, "同步更新小程序用户失败"));
}
linkDriverUserId(driver, existing.getId());
}
@@ -433,9 +420,9 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
return msg;
}
private User buildSyncUser(Long userId, String tenantId, String mobile, String driverName, String gender,
String roleId, String deptId) {
User user = new User();
private MpUser buildSyncMpUser(Long userId, String tenantId, String mobile, String driverName, String gender,
String deptId, String identities) {
MpUser user = new MpUser();
user.setId(userId);
user.setTenantId(tenantId);
user.setAccount(mobile);
@@ -443,8 +430,8 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
user.setName(driverName);
user.setRealName(driverName);
user.setSex(UserSex.getCodeByName(gender));
user.setRoleId(roleId);
user.setDeptId(deptId);
user.setIdentities(identities);
return user;
}
@@ -32,7 +32,10 @@ import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
@@ -56,6 +59,7 @@ import org.springblade.transport.pojo.vo.DriverPunchPhotoVO;
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO;
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IDriverWaybillService;
import org.springblade.transport.service.IProcessConfigService;
@@ -120,6 +124,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private final IWaybillService waybillService;
private final IDriverService driverService;
private final IMpUserClient mpUserClient;
private final IProcessConfigService processConfigService;
private final ITransportVehicleService transportVehicleService;
private final CertificationGate certificationGate;
@@ -239,6 +244,52 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
return toCard(normalizeAcceptStatus(waybill), true);
}
@Override
public WaybillTrackVO track(Long id) {
Driver driver = requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
LocalDate[] range = resolveTrackDateRange(waybill);
return waybillService.trackVehicle(waybill.getId(), range[0].toString(), range[1].toString());
}
/**
* 按打卡记录 / 计划时间推导轨迹查询起止日期
*/
private LocalDate[] resolveTrackDateRange(Waybill waybill) {
LocalDate today = LocalDate.now();
List<LocalDate> punchDates = new ArrayList<>();
nodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
.eq(WaybillNodePunch::getWaybillId, waybill.getId())
.isNotNull(WaybillNodePunch::getPunchTime)
.orderByAsc(WaybillNodePunch::getPunchTime)).forEach(punch -> {
if (punch.getPunchTime() != null) {
punchDates.add(punch.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
});
enroutePunchMapper.selectList(Wrappers.<WaybillEnroutePunch>lambdaQuery()
.eq(WaybillEnroutePunch::getWaybillId, waybill.getId())
.isNotNull(WaybillEnroutePunch::getPunchTime)
.orderByAsc(WaybillEnroutePunch::getPunchTime)).forEach(punch -> {
if (punch.getPunchTime() != null) {
punchDates.add(punch.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
});
punchDates.sort(LocalDate::compareTo);
if (punchDates.size() > 1) {
return new LocalDate[]{punchDates.get(0), punchDates.get(punchDates.size() - 1)};
}
if (punchDates.size() == 1) {
return new LocalDate[]{punchDates.get(0), today};
}
if (waybill.getEstimatedStartTime() != null) {
return new LocalDate[]{waybill.getEstimatedStartTime(), today};
}
if (waybill.getStartDate() != null) {
return new LocalDate[]{waybill.getStartDate(), today};
}
return new LocalDate[]{today.minusDays(3), today};
}
@Override
public DriverWaybillCardVO detailPunchSnapshot(Long id) {
if (id == null) {
@@ -590,7 +641,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
/**
* 当前登录司机:优先 userId,其次 JWT 账号(手机号)匹配 driver.mobile
* 当前登录司机:优先 userId,其次手机号(JWT 账号 / 小程序用户 account)匹配 driver.mobile
*/
private Driver currentDriver() {
Long userId = AuthUtil.getUserId();
@@ -603,16 +654,36 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
if (driver != null) {
return driver;
}
String account = AuthUtil.getUserAccount();
if (Func.isNotEmpty(account) && account.matches("^1\\d{10}$")) {
String phone = resolveSelfPhone(userId);
if (Func.isNotEmpty(phone)) {
return driverService.getOne(Wrappers.<Driver>lambdaQuery()
.eq(Driver::getMobile, account.trim())
.eq(Driver::getMobile, phone)
.orderByDesc(Driver::getUpdateTime)
.last("LIMIT 1"));
}
return null;
}
/**
* 解析当前小程序用户可用于匹配司机档案的手机号
*/
private String resolveSelfPhone(Long userId) {
String account = AuthUtil.getUserAccount();
if (isMobileLike(account)) {
return account.trim();
}
R<MpUser> result = mpUserClient.userById(userId);
if (result != null && R.isSuccess(result) && result.getData() != null
&& isMobileLike(result.getData().getAccount())) {
return result.getData().getAccount().trim();
}
return Func.isEmpty(account) ? null : account.trim();
}
private boolean isMobileLike(String value) {
return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
}
private List<String> splitPlates(String drivingVehicle) {
return VehiclePlateSupport.splitPlates(drivingVehicle);
}
@@ -31,7 +31,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.system.cache.UserDisplayCache;
import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
@@ -131,7 +131,7 @@ public class ExceptionDisposalServiceImpl
disposal.setDisposalStatus(STATUS_PENDING);
disposal.setReportTime(LocalDateTime.now());
disposal.setReporterId(AuthUtil.getUserId());
disposal.setReporterName(UserCache.getUserRealName(AuthUtil.getUserId()));
disposal.setReporterName(UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
fillFromWaybill(disposal, request.getWaybillId(), request.getWaybillNo());
@@ -267,7 +267,7 @@ public class ExceptionDisposalServiceImpl
record.setDisposalId(disposal.getId());
record.setFollowContent(changeLog);
record.setFollowUser(AuthUtil.getUserId());
record.setFollowUserName(UserCache.getUserRealName(AuthUtil.getUserId()));
record.setFollowUserName(UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
record.setFollowTime(LocalDateTime.now());
followRecordMapper.insert(record);
}
@@ -344,7 +344,7 @@ public class ExceptionDisposalServiceImpl
record.setFollowContent(Func.isBlank(followContent) ? "已上传跟进附件" : followContent);
record.setAttachmentsJson(hasAttachments ? request.getAttachmentsJson() : null);
record.setFollowUser(AuthUtil.getUserId());
record.setFollowUserName(UserCache.getUserRealName(AuthUtil.getUserId()));
record.setFollowUserName(UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
record.setFollowTime(LocalDateTime.now());
followRecordMapper.insert(record);
}
@@ -417,8 +417,8 @@ public class ExceptionDisposalServiceImpl
if (vo == null) {
return null;
}
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setCreateUserName(UserDisplayCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserDisplayCache.getUserRealName(entity.getUpdateUser()));
vo.setDisposalStatusName(statusName(entity.getDisposalStatus()));
vo.setScenePhotoList(splitPhotos(entity.getScenePhotos()));
fillRouteAndCargo(vo, entity.getWaybillId());
@@ -30,7 +30,7 @@ import lombok.RequiredArgsConstructor;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.system.cache.UserDisplayCache;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.ExceptionDisposal;
import org.springblade.transport.pojo.entity.RiskDisposal;
@@ -688,11 +688,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
private String resolveUserName() {
String realName = UserCache.getUserRealName(AuthUtil.getUserId());
if (Func.isNotBlank(realName)) {
String realName = UserDisplayCache.getUserRealName(AuthUtil.getUserId());
if (Func.isNotBlank(realName) && !Func.toStr(AuthUtil.getUserId()).equals(realName)) {
return realName;
}
return Func.toStr(AuthUtil.getUserName(), "");
String jwtName = Func.toStr(AuthUtil.getNickName(), AuthUtil.getUserName());
return Func.toStr(jwtName, realName);
}
/** 相对时间文案;时间取不到时留空,避免把「现在」当成发生时刻显示成「刚刚」 */
@@ -28,6 +28,7 @@ package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.system.cache.UserDisplayCache;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
import org.springblade.transport.support.CertificationRuleSupport;
@@ -62,9 +63,10 @@ public class TransportCertRecordWrapper extends BaseEntityWrapper<TransportCertR
* @return 同一个对象,便于链式调用
*/
public TransportCertRecordVO fillDisplayFields(TransportCertRecordVO transportCertRecordVO) {
transportCertRecordVO.setCreateUserName(UserCache.getUserRealName(transportCertRecordVO.getCreateUser()));
transportCertRecordVO.setUpdateUserName(UserCache.getUserRealName(transportCertRecordVO.getUpdateUser()));
transportCertRecordVO.setSubmitUserName(UserCache.getUserRealName(transportCertRecordVO.getSubmitUser()));
// create/update/submit 可能来自小程序用户;audit 一般为后台系统用户
transportCertRecordVO.setCreateUserName(UserDisplayCache.getUserRealName(transportCertRecordVO.getCreateUser()));
transportCertRecordVO.setUpdateUserName(UserDisplayCache.getUserRealName(transportCertRecordVO.getUpdateUser()));
transportCertRecordVO.setSubmitUserName(UserDisplayCache.getUserRealName(transportCertRecordVO.getSubmitUser()));
transportCertRecordVO.setAuditUserName(UserCache.getUserRealName(transportCertRecordVO.getAuditUser()));
// 状态文案统一在此翻译,避免小程序、Web、接口各处各造一套说法
transportCertRecordVO.setStatusText(CertificationRuleSupport.statusText(transportCertRecordVO.getStatus()));
@@ -8,13 +8,13 @@ import org.mockito.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.system.feign.IUserClient;
import java.time.LocalDate;
import java.util.List;
@@ -46,7 +46,7 @@ class DriverAppServiceImplTest {
void setUp() {
driverService = mock(IDriverService.class);
transportVehicleService = mock(ITransportVehicleService.class);
service = new DriverAppServiceImpl(driverService, transportVehicleService, mock(IUserClient.class));
service = new DriverAppServiceImpl(driverService, transportVehicleService, mock(IMpUserClient.class));
}
@Test
@@ -94,9 +94,9 @@ class DriverAppServiceImplTest {
}
@Test
@DisplayName("用户中心 phone 是脱敏串时改用其 account——脱敏 phone 既查不到档案,也会把本人真实号码误判成越权")
@DisplayName("小程序用户 phone 是脱敏串时改用其 account——脱敏 phone 既查不到档案,也会把本人真实号码误判成越权")
void maskedUserPhoneFallsBackToAccount() {
User user = new User();
MpUser user = new MpUser();
user.setPhone("175****12082");
user.setAccount("175874212082");
@@ -156,14 +156,14 @@ class DriverAppServiceImplTest {
*
* @param mobileHint 前端传入的 mobile,可为 null
* @param account JWT 里的账号,可为 null
* @param user 用户中心返回的用户,可为 null(表示走不到用户中心)
* @param user 小程序用户,可为 null(表示走不到小程序用户中心)
*/
private Driver currentByPhone(String mobileHint, String account, User user) {
IUserClient userClient = mock(IUserClient.class);
private Driver currentByPhone(String mobileHint, String account, MpUser user) {
IMpUserClient mpUserClient = mock(IMpUserClient.class);
if (user != null) {
when(userClient.userInfoById(USER_ID)).thenReturn(R.data(user));
when(mpUserClient.userById(USER_ID)).thenReturn(R.data(user));
}
DriverAppServiceImpl scoped = new DriverAppServiceImpl(driverService, transportVehicleService, userClient);
DriverAppServiceImpl scoped = new DriverAppServiceImpl(driverService, transportVehicleService, mpUserClient);
Driver driver = new Driver();
driver.setId(9002L);
@@ -178,5 +178,4 @@ class DriverAppServiceImplTest {
return scoped.currentByPhone(mobileHint);
}
}
}
@@ -12,7 +12,7 @@ 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.cache.UserCache;
import org.springblade.system.cache.UserDisplayCache;
import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.pojo.entity.ExceptionDisposal;
@@ -320,9 +320,9 @@ class ExceptionDisposalReportGateTest {
private void runSubmit(ExceptionDisposal request) {
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class);
MockedStatic<UserCache> userCache = mockStatic(UserCache.class)) {
MockedStatic<UserDisplayCache> userCache = mockStatic(UserDisplayCache.class)) {
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
userCache.when(() -> UserCache.getUserRealName(USER_ID)).thenReturn("韦司机");
userCache.when(() -> UserDisplayCache.getUserRealName(USER_ID)).thenReturn("韦司机");
service.submitReport(request);
}
}
@@ -0,0 +1,45 @@
-- ----------------------------
-- 小程序用户表(与 blade_user 分离,仅用于小程序登录)
-- 身份 identities:逗号分隔,当前支持 driver(司机)、admin(管理员),可同时拥有,后续可扩展
-- ----------------------------
CREATE TABLE IF NOT EXISTS `blade_mp_user` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000' COMMENT '租户ID',
`code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户编号',
`account` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号(通常为手机号)',
`password` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码(小程序登录可不使用)',
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称',
`real_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真名',
`avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
`email` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`phone` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机',
`birthday` datetime NULL DEFAULT NULL COMMENT '生日',
`sex` int NULL DEFAULT NULL COMMENT '性别',
`openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序 openid',
`identities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '小程序身份,逗号分隔:driver,admin',
`dept_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id',
`post_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '岗位id',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_user` bigint NULL DEFAULT NULL COMMENT '创建人',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_user` bigint NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`status` int NULL DEFAULT 1 COMMENT '状态',
`is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_mp_user_tenant_phone` (`tenant_id`, `phone`) USING BTREE,
UNIQUE KEY `uk_mp_user_tenant_openid` (`tenant_id`, `openid`) USING BTREE,
KEY `idx_mp_user_account` (`tenant_id`, `account`) USING BTREE,
KEY `idx_mp_user_identities` (`identities`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '小程序用户表';
-- 司机表 user_id 语义变更为关联小程序用户
ALTER TABLE `blade_transport_driver`
MODIFY COLUMN `user_id` bigint(20) DEFAULT NULL COMMENT '关联小程序用户ID(blade_mp_user.id)';
-- 示例:创建小程序管理员(调度端),identities 可同时包含 driver,admin
-- INSERT INTO `blade_mp_user`
-- (`id`, `tenant_id`, `account`, `name`, `real_name`, `phone`, `identities`, `create_time`, `status`, `is_deleted`)
-- VALUES
-- (2091900000000000001, '000000', '13800000000', '调度管理员', '调度管理员', '13800000000', 'admin', NOW(), 1, 0);
@@ -0,0 +1,61 @@
-- ----------------------------
-- 将已关联 blade_user 的司机迁移到 blade_mp_user(可选,按环境执行)
-- 注意:执行前请先执行 blade_mp_user_20261012.sql 建表
-- 策略:小程序用户主键使用司机表 id(避免与 blade_user.id 冲突导致令牌刷新串用户)
-- 迁移后回写 driver.user_id;原 blade_user 账号不会自动删除,请人工确认后再清理
-- 调度端管理员需另行在 blade_mp_user 中维护 identities 含 admin 的账号
-- ----------------------------
-- 1) 按司机写入小程序用户
INSERT INTO `blade_mp_user` (
`id`, `tenant_id`, `account`, `name`, `real_name`, `phone`, `sex`,
`openid`, `identities`, `dept_id`,
`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`,
`status`, `is_deleted`
)
SELECT
d.`id` AS `id`,
IFNULL(d.`tenant_id`, '000000') AS `tenant_id`,
d.`mobile` AS `account`,
d.`driver_name` AS `name`,
d.`driver_name` AS `real_name`,
d.`mobile` AS `phone`,
CASE
WHEN d.`gender` IN ('男', '1') THEN 1
WHEN d.`gender` IN ('女', '2') THEN 2
ELSE NULL
END AS `sex`,
oauth.`uuid` AS `openid`,
'driver' AS `identities`,
u.`dept_id` AS `dept_id`,
d.`create_user`,
d.`create_dept`,
IFNULL(d.`create_time`, NOW()) AS `create_time`,
d.`update_user`,
IFNULL(d.`update_time`, NOW()) AS `update_time`,
IFNULL(d.`status`, 1) AS `status`,
0 AS `is_deleted`
FROM `blade_transport_driver` d
LEFT JOIN `blade_user` u ON u.`id` = d.`user_id` AND u.`is_deleted` = 0
LEFT JOIN `blade_user_oauth` oauth
ON oauth.`user_id` = d.`user_id`
AND oauth.`source` = 'WECHAT_MINI'
AND oauth.`is_deleted` = 0
WHERE d.`is_deleted` = 0
AND d.`mobile` IS NOT NULL
AND d.`mobile` <> ''
AND NOT EXISTS (
SELECT 1 FROM `blade_mp_user` m
WHERE m.`id` = d.`id`
OR (m.`tenant_id` = IFNULL(d.`tenant_id`, '000000') AND m.`phone` = d.`mobile` AND m.`is_deleted` = 0)
);
-- 2) 回写司机关联的小程序用户ID
UPDATE `blade_transport_driver` d
INNER JOIN `blade_mp_user` m
ON m.`id` = d.`id`
OR (m.`tenant_id` = IFNULL(d.`tenant_id`, '000000') AND m.`phone` = d.`mobile` AND m.`is_deleted` = 0)
SET d.`user_id` = m.`id`
WHERE d.`is_deleted` = 0
AND d.`mobile` IS NOT NULL
AND d.`mobile` <> '';