diff --git a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java index e75bdb50..f7e6386a 100644 --- a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java +++ b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java @@ -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 diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java index 8e207ad1..3c190cc4 100644 --- a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java @@ -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 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 loadPermission(String roleId) { if (Func.isBlank(roleId)) { return Collections.emptyList(); diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java index 1bdd67bf..33de5291 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java @@ -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; * 微信小程序手机号一键登录。 *

* 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 mpUserResult = mpUserClient.userInfoByPhone(request.getTenantId(), phone); + if (mpUserResult == null || !mpUserResult.isSuccess() || mpUserResult.getData() == null + || mpUserResult.getData().getUser() == null) { throw new UserInvalidException("用户不存在,无法登录"); } - R bindResult = userClient.bindWxMiniOpenId( + OAuth2User user = TokenUtil.convertMpUser(mpUserResult.getData()); + if (!userService.validateUser(user)) { + throw new UserInvalidException("用户身份无效,无法登录"); + } + + R 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 失败"; diff --git a/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java b/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java index 1efee4d4..fa5cbc46 100644 --- a/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java +++ b/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java @@ -18,9 +18,6 @@ * Using this software signifies agreement to this License, and the software * must not be used for illegal purposes. *

- * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is - * not liable for any claims arising from secondary or illegal development. - *

* Author: Chill Zhuang (bladejava@qq.com) */ package org.springblade.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 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 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 mpResult = mpUserClient.userInfoByAccount(tenantId, username); + if (mpResult == null || !mpResult.isSuccess() || mpResult.getData() == null) { + return null; + } + return TokenUtil.convertMpUser(mpResult.getData()); + } + // 获取用户信息 R 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 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 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 result = mpUserClient.userInfoByPhone(tenantId, phone); + if (result == null || !result.isSuccess() || result.getData() == null) { + return null; + } + return TokenUtil.convertMpUser(result.getData()); + } + } diff --git a/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java b/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java index b39311da..35a61d95 100644 --- a/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java +++ b/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java @@ -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; + } + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/jackson/FlexibleLocalDateDeserializer.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/jackson/FlexibleLocalDateDeserializer.java new file mode 100644 index 00000000..9d16ee83 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/jackson/FlexibleLocalDateDeserializer.java @@ -0,0 +1,148 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 / 司机侧日期归一化规则。 + *

+ * 兼容写法: + *

+ * + * @author Chill + */ +public class FlexibleLocalDateDeserializer extends JsonDeserializer { + + /** + * 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 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 matchDates(String text) { + List 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"); + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java index d1c91951..fc7c3ce8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java @@ -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; /** * 与联系人关系 diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java index 1fe16eb9..71f5958a 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java @@ -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; /** * 机动车登记本图片 diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/MpUserCache.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/MpUserCache.java new file mode 100644 index 00000000..ccdff3cc --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/MpUserCache.java @@ -0,0 +1,114 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 result = getMpUserClient().userById(userId); + if (result == null || !result.isSuccess()) { + return null; + } + return result.getData(); + } + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserDisplayCache.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserDisplayCache.java new file mode 100644 index 00000000..0a63d35c --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserDisplayCache.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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; + +/** + * 用户展示名解析(系统用户优先,未命中再查小程序用户) + *

+ * 小程序链路写入的审计人/提交人 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); + } + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java new file mode 100644 index 00000000..0a0a6e4b --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java @@ -0,0 +1,121 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 userById(@RequestParam("userId") Long userId); + + /** + * 按租户 + 手机号查询 + */ + @GetMapping(USER_BY_PHONE) + R userByPhone(@RequestParam("tenantId") String tenantId, @RequestParam("phone") String phone); + + /** + * 按租户 + 账号查询 + */ + @GetMapping(USER_BY_ACCOUNT) + R userByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account); + + /** + * 按主键查询登录信息 + */ + @GetMapping(USER_INFO_BY_ID) + R userInfoById(@RequestParam("userId") Long userId); + + /** + * 按租户 + 手机号查询登录信息 + */ + @GetMapping(USER_INFO_BY_PHONE) + R userInfoByPhone(@RequestParam("tenantId") String tenantId, @RequestParam("phone") String phone); + + /** + * 按租户 + 账号查询登录信息 + */ + @GetMapping(USER_INFO_BY_ACCOUNT) + R userInfoByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account); + + /** + * 新增小程序用户 + */ + @PostMapping(SAVE_USER) + R saveUser(@RequestBody MpUser user); + + /** + * 更新小程序用户 + */ + @PostMapping(UPDATE_USER) + R updateUser(@RequestBody MpUser user); + + /** + * 绑定/更新微信小程序 openid + */ + @PostMapping(BIND_OPENID) + R bindOpenId(@RequestParam("tenantId") String tenantId, + @RequestParam("userId") Long userId, + @RequestParam("openid") String openid); + + /** + * 设置小程序用户密码 + */ + @PostMapping(SET_PASSWORD) + R setPassword(@RequestParam("userId") Long userId, + @RequestParam("password") String password, + @RequestParam("password2") String password2); + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java new file mode 100644 index 00000000..6d613811 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java @@ -0,0 +1,131 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.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; + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserInfo.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserInfo.java new file mode 100644 index 00000000..46bfb423 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserInfo.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 identities; + + /** + * 权限标识集合 + */ + @Schema(description = "权限集合") + private List permissions; + + /** + * 拓展信息 + */ + @Schema(description = "拓展信息") + private Kv detail; + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/enums/MpUserIdentity.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/enums/MpUserIdentity.java new file mode 100644 index 00000000..12c88b0f --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/enums/MpUserIdentity.java @@ -0,0 +1,148 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 split(String identities) { + if (Func.isBlank(identities)) { + return Collections.emptyList(); + } + Set 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 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 toPermissions(String identities) { + List permissions = new ArrayList<>(); + if (has(identities, ADMIN)) { + permissions.add(PERMISSION_MP_AUTH); + } + return permissions; + } + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/utils/MpUserAuth.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/utils/MpUserAuth.java new file mode 100644 index 00000000..b1b54e77 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/utils/MpUserAuth.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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(); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index 697e5d9d..34a468ad 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -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 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 { * 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密) *

* 对外路径:/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; + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java index ecf7b715..cae8c860 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java @@ -43,7 +43,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** - * 用户手机号变更(小程序「修改手机号」) + * 小程序用户手机号变更(写入 blade_mp_user) *

* 对外路径:/blade-system/user/phone/** ;网关别名 /blade-user/phone/** 亦可到达。 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java new file mode 100644 index 00000000..4d8eabfe --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java @@ -0,0 +1,110 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 userById(Long userId) { + return R.data(mpUserService.getById(userId)); + } + + @Override + @GetMapping(USER_BY_PHONE) + public R userByPhone(String tenantId, String phone) { + return R.data(mpUserService.getByPhone(tenantId, phone)); + } + + @Override + @GetMapping(USER_BY_ACCOUNT) + public R userByAccount(String tenantId, String account) { + return R.data(mpUserService.getByAccount(tenantId, account)); + } + + @Override + @GetMapping(USER_INFO_BY_ID) + public R userInfoById(Long userId) { + return R.data(mpUserService.userInfo(userId)); + } + + @Override + @GetMapping(USER_INFO_BY_PHONE) + public R userInfoByPhone(String tenantId, String phone) { + return R.data(mpUserService.userInfoByPhone(tenantId, phone)); + } + + @Override + @GetMapping(USER_INFO_BY_ACCOUNT) + public R userInfoByAccount(String tenantId, String account) { + return R.data(mpUserService.userInfoByAccount(tenantId, account)); + } + + @Override + @PostMapping(SAVE_USER) + public R saveUser(@RequestBody MpUser user) { + return R.data(mpUserService.submit(user)); + } + + @Override + @PostMapping(UPDATE_USER) + public R updateUser(@RequestBody MpUser user) { + return R.data(mpUserService.updateUser(user)); + } + + @Override + @PostMapping(BIND_OPENID) + public R bindOpenId(String tenantId, Long userId, String openid) { + return R.data(mpUserService.bindOpenId(tenantId, userId, openid)); + } + + @Override + @PostMapping(SET_PASSWORD) + public R setPassword(Long userId, String password, String password2) { + return R.data(mpUserService.setPassword(userId, password, password2)); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserMapper.java new file mode 100644 index 00000000..f6e53d79 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserMapper.java @@ -0,0 +1,34 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 { +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java new file mode 100644 index 00000000..f05932ea --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 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); + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java new file mode 100644 index 00000000..da400e69 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java @@ -0,0 +1,272 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * 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 implements IMpUserService { + + @Override + public MpUser getByPhone(String tenantId, String phone) { + if (Func.isBlank(tenantId) || Func.isBlank(phone)) { + return null; + } + return getOne(Wrappers.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.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.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 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 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 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; + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java index f6f26d92..605bfc08 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java @@ -18,9 +18,6 @@ * Using this software signifies agreement to this License, and the software * must not be used for illegal purposes. *

- * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is - * not liable for any claims arising from secondary or illegal development. - *

* Author: Chill Zhuang (bladejava@qq.com) */ package org.springblade.system.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.lambdaQuery() - .eq(User::getTenantId, tenantId) - .eq(User::getPhone, phone) - .ne(User::getId, excludeUserId) + Long phoneCount = mpUserService.count( + Wrappers.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.lambdaQuery() - .eq(User::getTenantId, tenantId) - .eq(User::getAccount, account) - .ne(User::getId, excludeUserId) + Long accountCount = mpUserService.count( + Wrappers.lambdaQuery() + .eq(MpUser::getTenantId, tenantId) + .eq(MpUser::getAccount, account) + .ne(MpUser::getId, excludeUserId) ); if (accountCount != null && accountCount > 0L) { throw new ServiceException(StringUtil.format("当前用户 [{}] 已存在!", account)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java index 0a36f125..09a6ece6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java @@ -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())); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java index 1d09822f..148ce41e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java @@ -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 result = userClient.userInfoById(userId); + R result = mpUserClient.userById(userId); if (result != null && R.isSuccess(result) && result.getData() != null) { String userAccount = result.getData().getAccount(); if (isMobileLike(userAccount)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java index f172a547..ca7ef54e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java @@ -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、类型也相同,传错不会报任何错, * 只会让「提交人」一栏显示成一串数字。 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java index 3aca721d..97e7186b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java @@ -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 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 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 imp } } - R 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 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 byId = userClient.userInfoById(driver.getUserId()); + R 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 byAccount = userClient.userByAccount(tenantId, mobile); - if (byAccount != null && R.isSuccess(byAccount) && byAccount.getData() != null - && Func.isNotEmpty(byAccount.getData().getId())) { - return byAccount.getData(); - } - R byPhone = userClient.userInfoByPhone(tenantId, mobile, UserType.WEB.getName()); + private MpUser findExistingMpUserByPhone(String tenantId, String mobile) { + R 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 saveResult = null; + R 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 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 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 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 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 imp user.setName(driverName); user.setRealName(driverName); user.setSex(UserSex.getCodeByName(gender)); - user.setRoleId(roleId); user.setDeptId(deptId); + user.setIdentities(identities); return user; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index 83987141..9158ce0d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -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 punchDates = new ArrayList<>(); + nodePunchMapper.selectList(Wrappers.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.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.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 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 splitPlates(String drivingVehicle) { return VehiclePlateSupport.splitPlates(drivingVehicle); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index e8842af0..d86e008c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -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()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java index 1f4868f2..71327729 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java @@ -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); } /** 相对时间文案;时间取不到时留空,避免把「现在」当成发生时刻显示成「刚刚」 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java index 60b9cea4..f6a8d7e2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java @@ -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 authUtil = mockStatic(AuthUtil.class); - MockedStatic userCache = mockStatic(UserCache.class)) { + MockedStatic 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); } } diff --git a/doc/sql/transport/blade_mp_user_20261012.sql b/doc/sql/transport/blade_mp_user_20261012.sql new file mode 100644 index 00000000..9d34d49c --- /dev/null +++ b/doc/sql/transport/blade_mp_user_20261012.sql @@ -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); diff --git a/doc/sql/transport/blade_mp_user_migrate_driver_20261012.sql b/doc/sql/transport/blade_mp_user_migrate_driver_20261012.sql new file mode 100644 index 00000000..9f3063b5 --- /dev/null +++ b/doc/sql/transport/blade_mp_user_migrate_driver_20261012.sql @@ -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` <> '';