1、新增小程序相关接口
2、调整OA
This commit is contained in:
@@ -75,6 +75,10 @@
|
|||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-mk-api</artifactId>
|
<artifactId>blade-mk-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-wechat-api</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-resource-api</artifactId>
|
<artifactId>blade-resource-api</artifactId>
|
||||||
|
|||||||
@@ -32,4 +32,10 @@ package org.springblade.auth.constant;
|
|||||||
*/
|
*/
|
||||||
public interface BladeAuthConstant {
|
public interface BladeAuthConstant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序/登录短信验证码资源编号(对应后台 /resource/sms 的 smsCode)
|
||||||
|
*/
|
||||||
|
String LOGIN_SMS_CODE = "ali_reg";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.auth.endpoint;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.utils.BeanUtil;
|
||||||
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
import org.springblade.system.feign.ISysClient;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增强 /oauth/user-info 响应,补充 permission 权限标识字段
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestControllerAdvice(assignableTypes = OAuth2TokenEndPoint.class)
|
||||||
|
public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||||
|
|
||||||
|
private final ISysClient sysClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
return returnType.getMethod() != null && "userInfo".equals(returnType.getMethod().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||||
|
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||||
|
ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
if (!(body instanceof BladeUser bladeUser)) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
OAuth2UserInfoVO userInfo = BeanUtil.copyProperties(bladeUser, OAuth2UserInfoVO.class);
|
||||||
|
if (userInfo == null) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> loadPermission(String roleId) {
|
||||||
|
if (Func.isBlank(roleId)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
R<List<String>> result = sysClient.getPermissions(roleId);
|
||||||
|
if (result != null && result.isSuccess() && result.getData() != null) {
|
||||||
|
return result.getData();
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.warn("加载用户权限标识失败, roleId={}", roleId, exception);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.auth.endpoint;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springblade.core.secure.BladeUser;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth用户信息(含权限标识,供小程序等客户端使用)
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "OAuth用户信息")
|
||||||
|
public class OAuth2UserInfoVO extends BladeUser {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限标识集合(菜单按钮 code)
|
||||||
|
*/
|
||||||
|
@Schema(description = "权限标识集合")
|
||||||
|
private List<String> permission;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -28,13 +28,14 @@ package org.springblade.auth.endpoint;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import org.springblade.auth.constant.BladeAuthConstant;
|
||||||
import org.springblade.core.oauth2.props.OAuth2Properties;
|
import org.springblade.core.oauth2.props.OAuth2Properties;
|
||||||
import org.springblade.core.oauth2.provider.OAuth2Request;
|
import org.springblade.core.oauth2.provider.OAuth2Request;
|
||||||
import org.springblade.core.oauth2.service.OAuth2User;
|
import org.springblade.core.oauth2.service.OAuth2User;
|
||||||
import org.springblade.core.oauth2.service.OAuth2UserService;
|
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.core.tool.utils.SM2Util;
|
import org.springblade.core.tool.utils.SM2Util;
|
||||||
import org.springblade.core.tool.utils.StringPool;
|
|
||||||
import org.springblade.core.tool.utils.StringUtil;
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springblade.resource.feign.ISmsClient;
|
import org.springblade.resource.feign.ISmsClient;
|
||||||
import org.springblade.resource.utils.SmsUtil;
|
import org.springblade.resource.utils.SmsUtil;
|
||||||
@@ -73,11 +74,14 @@ public class Oauth2SmsEndpoint {
|
|||||||
* 短信验证码发送
|
* 短信验证码发送
|
||||||
*
|
*
|
||||||
* @param tenantId 租户ID
|
* @param tenantId 租户ID
|
||||||
* @param phone 手机号
|
* @param phone 手机号(SM2 加密)
|
||||||
|
* @param code 短信资源编号,默认 ali_reg(后台 /resource/sms)
|
||||||
*/
|
*/
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@PostMapping("/oauth/sms/send-validate")
|
@PostMapping("/oauth/sms/send-validate")
|
||||||
public R sendValidate(@RequestParam String tenantId, @RequestParam String phone) {
|
public R sendValidate(@RequestParam String tenantId,
|
||||||
|
@RequestParam String phone,
|
||||||
|
@RequestParam(required = false) String code) {
|
||||||
// 校验手机加密认证,防止恶意发送验证码
|
// 校验手机加密认证,防止恶意发送验证码
|
||||||
String decryptedPhone = SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey());
|
String decryptedPhone = SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey());
|
||||||
if (StringUtil.isBlank(decryptedPhone)) {
|
if (StringUtil.isBlank(decryptedPhone)) {
|
||||||
@@ -90,8 +94,9 @@ public class Oauth2SmsEndpoint {
|
|||||||
if (oAuth2User == null) {
|
if (oAuth2User == null) {
|
||||||
return R.fail(USER_PHONE_NOT_FOUND);
|
return R.fail(USER_PHONE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
// 用户存在则发送验证码
|
// 使用指定短信资源(默认 ali_reg)
|
||||||
R result = smsClient.sendValidate(tenantId, StringPool.EMPTY, decryptedPhone);
|
String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE);
|
||||||
|
R result = smsClient.sendValidate(tenantId, smsResourceCode, decryptedPhone);
|
||||||
return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL);
|
return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
package org.springblade.auth.granter;
|
package org.springblade.auth.granter;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springblade.auth.constant.BladeAuthConstant;
|
||||||
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
|
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
|
||||||
import org.springblade.core.oauth2.exception.UserInvalidException;
|
import org.springblade.core.oauth2.exception.UserInvalidException;
|
||||||
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
|
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
|
||||||
@@ -37,8 +38,8 @@ import org.springblade.core.oauth2.service.OAuth2User;
|
|||||||
import org.springblade.core.oauth2.service.OAuth2UserService;
|
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||||
import org.springblade.core.sms.model.SmsCode;
|
import org.springblade.core.sms.model.SmsCode;
|
||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.core.tool.utils.SM2Util;
|
import org.springblade.core.tool.utils.SM2Util;
|
||||||
import org.springblade.core.tool.utils.StringPool;
|
|
||||||
import org.springblade.core.tool.utils.StringUtil;
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springblade.core.tool.utils.WebUtil;
|
import org.springblade.core.tool.utils.WebUtil;
|
||||||
import org.springblade.resource.feign.ISmsClient;
|
import org.springblade.resource.feign.ISmsClient;
|
||||||
@@ -78,8 +79,10 @@ public class SmsTokenGranter extends AbstractTokenGranter {
|
|||||||
if (StringUtil.isBlank(decryptedPhone)) {
|
if (StringUtil.isBlank(decryptedPhone)) {
|
||||||
throw new UserInvalidException(OAuth2TokenConstant.USER_PHONE_NOT_FOUND);
|
throw new UserInvalidException(OAuth2TokenConstant.USER_PHONE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
// 获取短信验证信息
|
// 与发送时使用同一短信资源编号(默认 ali_reg)
|
||||||
R result = smsClient.validateMessage(tenantId, StringPool.EMPTY, smsCode.getId(), smsCode.getValue(), decryptedPhone);
|
HttpServletRequest httpRequest = WebUtil.getRequest();
|
||||||
|
String smsResourceCode = Func.toStr(httpRequest.getParameter("code"), BladeAuthConstant.LOGIN_SMS_CODE);
|
||||||
|
R result = smsClient.validateMessage(tenantId, smsResourceCode, smsCode.getId(), smsCode.getValue(), decryptedPhone);
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT);
|
throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package org.springblade.auth.granter;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
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.OAuth2ClientService;
|
||||||
|
import org.springblade.core.oauth2.service.OAuth2User;
|
||||||
|
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||||
|
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.thirdparty.wechat.constant.WechatMiniConstant;
|
||||||
|
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
|
||||||
|
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
|
||||||
|
import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO;
|
||||||
|
import org.springblade.thirdparty.wechat.service.IWechatMiniService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序手机号一键登录。
|
||||||
|
* <p>
|
||||||
|
* grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
|
||||||
|
* 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class WechatMiniTokenGranter extends AbstractTokenGranter {
|
||||||
|
|
||||||
|
private final OAuth2UserService userService;
|
||||||
|
private final IWechatMiniService wechatMiniService;
|
||||||
|
private final IUserClient userClient;
|
||||||
|
|
||||||
|
public WechatMiniTokenGranter(OAuth2ClientService clientService,
|
||||||
|
OAuth2UserService userService,
|
||||||
|
PasswordHandler passwordHandler,
|
||||||
|
IWechatMiniService wechatMiniService,
|
||||||
|
IUserClient userClient) {
|
||||||
|
super(clientService, userService, passwordHandler);
|
||||||
|
this.userService = userService;
|
||||||
|
this.wechatMiniService = wechatMiniService;
|
||||||
|
this.userClient = userClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String type() {
|
||||||
|
return WechatMiniConstant.GRANT_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2User user(OAuth2Request request) {
|
||||||
|
// 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败
|
||||||
|
var oauthClient = client(request);
|
||||||
|
|
||||||
|
HttpServletRequest httpRequest = WebUtil.getRequest();
|
||||||
|
String loginCode = httpRequest.getParameter("loginCode");
|
||||||
|
String phoneCode = httpRequest.getParameter("phoneCode");
|
||||||
|
if (StringUtil.isBlank(loginCode) || StringUtil.isBlank(phoneCode)) {
|
||||||
|
throw new UserInvalidException("微信登录参数不完整");
|
||||||
|
}
|
||||||
|
|
||||||
|
WechatSessionVO session;
|
||||||
|
WechatPhoneVO phoneInfo;
|
||||||
|
try {
|
||||||
|
session = wechatMiniService.code2Session(loginCode);
|
||||||
|
phoneInfo = wechatMiniService.getPhoneNumber(phoneCode);
|
||||||
|
} catch (WechatMiniException e) {
|
||||||
|
log.warn("微信小程序登录失败: {}", e.getMessage());
|
||||||
|
throw new UserInvalidException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
String phone = Func.toStr(phoneInfo.getPurePhoneNumber(), phoneInfo.getPhoneNumber());
|
||||||
|
if (StringUtil.isBlank(phone)) {
|
||||||
|
throw new UserInvalidException("未获取到微信手机号");
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2User user = userService.loadByPhone(phone, request);
|
||||||
|
if (!userService.validateUser(user)) {
|
||||||
|
throw new UserInvalidException("用户不存在,无法登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
R<Boolean> bindResult = userClient.bindWxMiniOpenId(
|
||||||
|
request.getTenantId(),
|
||||||
|
Func.toLong(user.getUserId()),
|
||||||
|
session.getOpenid(),
|
||||||
|
phone
|
||||||
|
);
|
||||||
|
if (bindResult == null || !bindResult.isSuccess()) {
|
||||||
|
String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败";
|
||||||
|
log.warn("绑定微信 openid 失败 userId={} openid={} msg={}", user.getUserId(), session.getOpenid(), msg);
|
||||||
|
throw new UserInvalidException(StringUtil.isBlank(msg) ? "绑定 openid 失败" : msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setClient(oauthClient);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+14
-1
@@ -25,11 +25,17 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.auth.service;
|
package org.springblade.auth.service;
|
||||||
|
|
||||||
|
import org.springblade.core.oauth2.constant.OAuth2GranterConstant;
|
||||||
import org.springblade.core.oauth2.provider.OAuth2Request;
|
import org.springblade.core.oauth2.provider.OAuth2Request;
|
||||||
import org.springblade.core.oauth2.service.OAuth2Client;
|
import org.springblade.core.oauth2.service.OAuth2Client;
|
||||||
import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService;
|
import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService;
|
||||||
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
import org.springblade.core.tool.utils.StringPool;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BladeClientDetailService
|
* BladeClientDetailService
|
||||||
*
|
*
|
||||||
@@ -57,6 +63,13 @@ public class BladeClientDetailService extends OAuth2ClientDetailService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean validateGranter(OAuth2Client client, String grantType) {
|
public boolean validateGranter(OAuth2Client client, String grantType) {
|
||||||
return super.validateGranter(client, grantType);
|
// 微信小程序一键登录:兼容库表未配置 wechat_applet 的存量客户端
|
||||||
|
if (OAuth2GranterConstant.WECHAT_APPLET.equals(grantType) || "wechat_mini".equals(grantType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(client)
|
||||||
|
.map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA))
|
||||||
|
.anyMatch(s -> s.trim().equals(grantType)))
|
||||||
|
.orElse(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class AuthProvider {
|
|||||||
DEFAULT_SKIP_URL.add("/oauth/sms/**");
|
DEFAULT_SKIP_URL.add("/oauth/sms/**");
|
||||||
DEFAULT_SKIP_URL.add("/oauth/clear-cache/**");
|
DEFAULT_SKIP_URL.add("/oauth/clear-cache/**");
|
||||||
DEFAULT_SKIP_URL.add("/oauth/user-info");
|
DEFAULT_SKIP_URL.add("/oauth/user-info");
|
||||||
|
DEFAULT_SKIP_URL.add("/oauth/logout/**");
|
||||||
DEFAULT_SKIP_URL.add("/oauth/render/**");
|
DEFAULT_SKIP_URL.add("/oauth/render/**");
|
||||||
DEFAULT_SKIP_URL.add("/oauth/callback/**");
|
DEFAULT_SKIP_URL.add("/oauth/callback/**");
|
||||||
DEFAULT_SKIP_URL.add("/oauth/revoke/**");
|
DEFAULT_SKIP_URL.add("/oauth/revoke/**");
|
||||||
|
|||||||
+10
@@ -73,6 +73,7 @@ public interface ISysClient {
|
|||||||
String REGION = API_PREFIX + "/region";
|
String REGION = API_PREFIX + "/region";
|
||||||
String FEE_ITEMS = API_PREFIX + "/fee-items";
|
String FEE_ITEMS = API_PREFIX + "/fee-items";
|
||||||
String CARGO_TYPES = API_PREFIX + "/cargo-types";
|
String CARGO_TYPES = API_PREFIX + "/cargo-types";
|
||||||
|
String PERMISSIONS = API_PREFIX + "/permissions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取菜单
|
* 获取菜单
|
||||||
@@ -316,4 +317,13 @@ public interface ISysClient {
|
|||||||
@GetMapping(CARGO_TYPES)
|
@GetMapping(CARGO_TYPES)
|
||||||
R<List<CargoType>> getCargoTypes();
|
R<List<CargoType>> getCargoTypes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色权限标识集合(按钮编号)
|
||||||
|
*
|
||||||
|
* @param roleId 角色id
|
||||||
|
* @return 权限标识
|
||||||
|
*/
|
||||||
|
@GetMapping(PERMISSIONS)
|
||||||
|
R<List<String>> getPermissions(@RequestParam("roleId") String roleId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -174,4 +174,9 @@ public class ISysClientFallback implements ISysClient {
|
|||||||
return R.fail("获取数据失败");
|
return R.fail("获取数据失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<List<String>> getPermissions(String roleId) {
|
||||||
|
return R.fail("获取数据失败");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OA人员分页同步结果
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "OA人员分页同步结果")
|
||||||
|
public class OaPersonSyncPageVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "当前页")
|
||||||
|
private Integer current;
|
||||||
|
|
||||||
|
@Schema(description = "每页条数")
|
||||||
|
private Integer size;
|
||||||
|
|
||||||
|
@Schema(description = "OA人员总条数")
|
||||||
|
private Long total;
|
||||||
|
|
||||||
|
@Schema(description = "本页从OA拉取的条数")
|
||||||
|
private Integer fetchedCount;
|
||||||
|
|
||||||
|
@Schema(description = "本页同步成功条数")
|
||||||
|
private Integer syncedCount;
|
||||||
|
|
||||||
|
@Schema(description = "本页跳过条数")
|
||||||
|
private Integer skippedCount;
|
||||||
|
|
||||||
|
@Schema(description = "是否已到最后一页")
|
||||||
|
private Boolean finished;
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端司机搜索项")
|
||||||
|
public class AdminDriverOptionVO implements Serializable {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "司机ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "姓名")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "绑定车牌")
|
||||||
|
private String vehicleNo;
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端首页快捷入口角标
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端首页角标")
|
||||||
|
public class AdminHomeBadgesVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "异常处置待办数(disposalStatus≠completed)")
|
||||||
|
private long exception;
|
||||||
|
|
||||||
|
@Schema(description = "风险记录待办数(disposalStatus=pending)")
|
||||||
|
private long risk;
|
||||||
|
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端首页顶部统计(对齐小程序 admin/home)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端首页运单状态统计")
|
||||||
|
public class AdminHomeStatsVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "运输中(businessStatus=running)")
|
||||||
|
private long transporting;
|
||||||
|
|
||||||
|
@Schema(description = "待接单(businessStatus=pending)")
|
||||||
|
private long pendingAccept;
|
||||||
|
|
||||||
|
@Schema(description = "在途异常(异常处置状态≠已完成)")
|
||||||
|
private long exception;
|
||||||
|
|
||||||
|
@Schema(description = "已完成(businessStatus=completed)")
|
||||||
|
private long completed;
|
||||||
|
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端首页聚合数据(统计 + 角标 + 待处理 feed + 用户名)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端首页聚合")
|
||||||
|
public class AdminHomeVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "当前登录用户姓名")
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Schema(description = "顶部运单状态统计")
|
||||||
|
private AdminHomeStatsVO stats = new AdminHomeStatsVO();
|
||||||
|
|
||||||
|
@Schema(description = "快捷入口角标")
|
||||||
|
private AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||||
|
|
||||||
|
@Schema(description = "待处理事项(异常处置状态≠已完成)")
|
||||||
|
private List<AdminTodoItemVO> feed = new ArrayList<>();
|
||||||
|
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端首页「待处理事项」条目
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端首页待处理事项")
|
||||||
|
public class AdminTodoItemVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "条目ID(异常处置ID)")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "类型:exception=异常待处置 / reassign=重新派单待处理")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "相对时间文案,如「2分钟」")
|
||||||
|
private String timeAgo;
|
||||||
|
|
||||||
|
@Schema(description = "摘要描述")
|
||||||
|
private String desc;
|
||||||
|
|
||||||
|
@Schema(description = "运单号")
|
||||||
|
private String waybillNo;
|
||||||
|
|
||||||
|
@Schema(description = "操作按钮文案")
|
||||||
|
private String actionLabel;
|
||||||
|
|
||||||
|
@Schema(description = "跳转路径(小程序内路径)")
|
||||||
|
private String targetUrl;
|
||||||
|
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端车牌搜索项")
|
||||||
|
public class AdminVehicleOptionVO implements Serializable {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "车牌号")
|
||||||
|
private String vehicleNo;
|
||||||
|
|
||||||
|
@Schema(description = "关联司机名(可选)")
|
||||||
|
private String driverName;
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端运单列表卡片(对齐小程序 admin/waybill-list)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端运单列表卡片")
|
||||||
|
public class AdminWaybillCardVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "运单ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "运单号")
|
||||||
|
private String waybillNo;
|
||||||
|
|
||||||
|
@Schema(description = "起点")
|
||||||
|
private String fromName;
|
||||||
|
|
||||||
|
@Schema(description = "终点")
|
||||||
|
private String toName;
|
||||||
|
|
||||||
|
@Schema(description = "货物名称")
|
||||||
|
private String cargo;
|
||||||
|
|
||||||
|
@Schema(description = "重量(带单位)")
|
||||||
|
private String weight;
|
||||||
|
|
||||||
|
@Schema(description = "计划开始时间")
|
||||||
|
private String planTime;
|
||||||
|
|
||||||
|
@Schema(description = "计划结束时间")
|
||||||
|
private String planTimeEnd;
|
||||||
|
|
||||||
|
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "承运方")
|
||||||
|
private String carrierName;
|
||||||
|
|
||||||
|
@Schema(description = "司机姓名")
|
||||||
|
private String driverName;
|
||||||
|
|
||||||
|
@Schema(description = "车牌号")
|
||||||
|
private String vehicleNo;
|
||||||
|
|
||||||
|
@Schema(description = "是否有未完成异常")
|
||||||
|
private Boolean hasException;
|
||||||
|
|
||||||
|
@Schema(description = "运输组织类型:common普通 / load配载")
|
||||||
|
private String transportType;
|
||||||
|
|
||||||
|
@Schema(description = "运输方式:road / 公路运输 / 铁路运输 等")
|
||||||
|
private String transportMode;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private String createTime;
|
||||||
|
|
||||||
|
@Schema(description = "需重新派单(司机已拒单)")
|
||||||
|
private Boolean needReassign;
|
||||||
|
|
||||||
|
@Schema(description = "采购方是否已付款(预留)")
|
||||||
|
private Boolean buyerPaid;
|
||||||
|
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端运单详情(对齐小程序 pages/waybill/detail)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度端运单详情")
|
||||||
|
public class AdminWaybillDetailVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "运单ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "运单号")
|
||||||
|
private String waybillNo;
|
||||||
|
|
||||||
|
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "起点名称")
|
||||||
|
private String fromName;
|
||||||
|
|
||||||
|
@Schema(description = "终点名称")
|
||||||
|
private String toName;
|
||||||
|
|
||||||
|
@Schema(description = "起点地址")
|
||||||
|
private String fromAddress;
|
||||||
|
|
||||||
|
@Schema(description = "终点地址")
|
||||||
|
private String toAddress;
|
||||||
|
|
||||||
|
@Schema(description = "装货地址")
|
||||||
|
private String pickupAddress;
|
||||||
|
|
||||||
|
@Schema(description = "卸货地址")
|
||||||
|
private String unloadAddress;
|
||||||
|
|
||||||
|
@Schema(description = "货物名称")
|
||||||
|
private String cargoName;
|
||||||
|
|
||||||
|
@Schema(description = "货物数量(带单位)")
|
||||||
|
private String cargoQuantity;
|
||||||
|
|
||||||
|
@Schema(description = "重量(带单位)")
|
||||||
|
private String weight;
|
||||||
|
|
||||||
|
@Schema(description = "合计货重")
|
||||||
|
private String totalWeight;
|
||||||
|
|
||||||
|
@Schema(description = "运输方式文案:公路运输 / 铁路运输 等")
|
||||||
|
private String transportType;
|
||||||
|
|
||||||
|
@Schema(description = "运输方式字典值:road 等")
|
||||||
|
private String transportMode;
|
||||||
|
|
||||||
|
@Schema(description = "运输组织:common普通 / load配载")
|
||||||
|
private String transportOrgType;
|
||||||
|
|
||||||
|
@Schema(description = "计划发货时间")
|
||||||
|
private String planShipTime;
|
||||||
|
|
||||||
|
@Schema(description = "计划完成时间")
|
||||||
|
private String planFinishTime;
|
||||||
|
|
||||||
|
@Schema(description = "承运方")
|
||||||
|
private String carrierName;
|
||||||
|
|
||||||
|
@Schema(description = "司机姓名")
|
||||||
|
private String driverName;
|
||||||
|
|
||||||
|
@Schema(description = "司机联系方式")
|
||||||
|
private String driverPhone;
|
||||||
|
|
||||||
|
@Schema(description = "车牌号")
|
||||||
|
private String vehicleNo;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "是否有未完成异常")
|
||||||
|
private Boolean hasException;
|
||||||
|
|
||||||
|
@Schema(description = "最近一条未完成异常处置ID(有异常时返回)")
|
||||||
|
private Long exceptionId;
|
||||||
|
|
||||||
|
@Schema(description = "需重新派单")
|
||||||
|
private Boolean needReassign;
|
||||||
|
|
||||||
|
@Schema(description = "司机接单状态:pending/accepted/rejected")
|
||||||
|
private String acceptStatus;
|
||||||
|
|
||||||
|
@Schema(description = "司机拒绝接单原因")
|
||||||
|
private String rejectReason;
|
||||||
|
|
||||||
|
@Schema(description = "原指派司机ID")
|
||||||
|
private Long driverId;
|
||||||
|
|
||||||
|
@Schema(description = "装卸点列表")
|
||||||
|
private List<AdminRoutePointVO> routePoints;
|
||||||
|
|
||||||
|
@Schema(description = "过程打卡节点(与司机端 punchNodes 同结构)")
|
||||||
|
private List<DriverPunchNodeVO> punchNodes;
|
||||||
|
|
||||||
|
@Schema(description = "途打卡记录")
|
||||||
|
private List<DriverEnrouteRecordVO> enrouteRecords;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "装卸点")
|
||||||
|
public static class AdminRoutePointVO implements Serializable {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "点位名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "详细地址")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "状态:pending/done/active")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+51
@@ -133,4 +133,55 @@ public class DriverWaybillCardVO implements Serializable {
|
|||||||
@Schema(description = "过程配置中 punch=是 的打卡节点列表(详情返回)")
|
@Schema(description = "过程配置中 punch=是 的打卡节点列表(详情返回)")
|
||||||
private List<DriverPunchNodeVO> punchNodes;
|
private List<DriverPunchNodeVO> punchNodes;
|
||||||
|
|
||||||
|
/* ===== pages/waybill/detail 接单查看字段 ===== */
|
||||||
|
|
||||||
|
@Schema(description = "货物名称")
|
||||||
|
private String cargoName;
|
||||||
|
|
||||||
|
@Schema(description = "装货地址")
|
||||||
|
private String pickupAddress;
|
||||||
|
|
||||||
|
@Schema(description = "卸货地址")
|
||||||
|
private String unloadAddress;
|
||||||
|
|
||||||
|
@Schema(description = "货物数量(带单位)")
|
||||||
|
private String cargoQuantity;
|
||||||
|
|
||||||
|
@Schema(description = "运输方式文案:公路运输等")
|
||||||
|
private String transportType;
|
||||||
|
|
||||||
|
@Schema(description = "计划发货时间")
|
||||||
|
private String planShipTime;
|
||||||
|
|
||||||
|
@Schema(description = "计划完成时间")
|
||||||
|
private String planFinishTime;
|
||||||
|
|
||||||
|
@Schema(description = "合计货重")
|
||||||
|
private String totalWeight;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "装卸点(详情返回)")
|
||||||
|
private List<DriverRoutePointVO> routePoints;
|
||||||
|
|
||||||
|
@Schema(description = "过程配置 JSON(详情返回,供前端兜底推导打卡节点)")
|
||||||
|
private String processJson;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "司机端装卸点")
|
||||||
|
public static class DriverRoutePointVO implements Serializable {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "点位名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "详细地址")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "状态:pending/done/active")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -61,6 +61,7 @@ public interface IUserClient {
|
|||||||
String SAVE_IAM_USER = API_PREFIX + "/save-iam-user";
|
String SAVE_IAM_USER = API_PREFIX + "/save-iam-user";
|
||||||
String REGISTER_USER = API_PREFIX + "/register-user";
|
String REGISTER_USER = API_PREFIX + "/register-user";
|
||||||
String REMOVE_USER = API_PREFIX + "/remove-user";
|
String REMOVE_USER = API_PREFIX + "/remove-user";
|
||||||
|
String BIND_WX_MINI_OPENID = API_PREFIX + "/bind-wx-mini-openid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户信息
|
* 获取用户信息
|
||||||
@@ -187,4 +188,18 @@ public interface IUserClient {
|
|||||||
@PostMapping(REMOVE_USER)
|
@PostMapping(REMOVE_USER)
|
||||||
R<Boolean> removeUser(@RequestParam("tenantIds") String tenantIds);
|
R<Boolean> removeUser(@RequestParam("tenantIds") String tenantIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定微信小程序 openid(写入 blade_user_oauth,source=WECHAT_MINI)
|
||||||
|
*
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param openid 微信 openid
|
||||||
|
* @param phone 手机号(可选,写入 username)
|
||||||
|
*/
|
||||||
|
@PostMapping(BIND_WX_MINI_OPENID)
|
||||||
|
R<Boolean> bindWxMiniOpenId(@RequestParam("tenantId") String tenantId,
|
||||||
|
@RequestParam("userId") Long userId,
|
||||||
|
@RequestParam("openid") String openid,
|
||||||
|
@RequestParam(value = "phone", required = false) String phone);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更换手机号参数
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "更换手机号参数")
|
||||||
|
public class PhoneChangeDTO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "新手机号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String newPhone;
|
||||||
|
|
||||||
|
@Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "新手机号短信验证码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原手机号短信校验参数
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "原手机号短信校验参数")
|
||||||
|
public class PhoneVerifyDTO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
@@ -45,6 +45,10 @@
|
|||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-user-api</artifactId>
|
<artifactId>blade-user-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-resource-api</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-process-api</artifactId>
|
<artifactId>blade-process-api</artifactId>
|
||||||
|
|||||||
+23
-4
@@ -54,7 +54,9 @@ import org.springblade.core.tool.utils.StringPool;
|
|||||||
import org.springblade.system.excel.UserExcel;
|
import org.springblade.system.excel.UserExcel;
|
||||||
import org.springblade.system.excel.UserImporter;
|
import org.springblade.system.excel.UserImporter;
|
||||||
import org.springblade.system.pojo.entity.User;
|
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.pojo.vo.UserVO;
|
||||||
|
import org.springblade.system.service.IOASyncService;
|
||||||
import org.springblade.system.service.IUserService;
|
import org.springblade.system.service.IUserService;
|
||||||
import org.springblade.system.wrapper.UserWrapper;
|
import org.springblade.system.wrapper.UserWrapper;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -77,6 +79,7 @@ import java.util.Map;
|
|||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
private final IUserService userService;
|
private final IUserService userService;
|
||||||
|
private final IOASyncService oaSyncService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询单条
|
* 查询单条
|
||||||
@@ -158,14 +161,16 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步IAM账号。
|
* 从OA按页同步人员,并按公司/部门生成组织后绑定到三级部门。
|
||||||
*/
|
*/
|
||||||
@IsAdmin
|
@IsAdmin
|
||||||
@PostMapping("/sync-iam-accounts")
|
@PostMapping("/sync-iam-accounts")
|
||||||
@ApiOperationSupport(order = 6)
|
@ApiOperationSupport(order = 6)
|
||||||
@Operation(summary = "同步IAM账号")
|
@Operation(summary = "同步OA人员")
|
||||||
public R<Integer> syncIamAccounts() {
|
public R<OaPersonSyncPageVO> syncIamAccounts(
|
||||||
return R.data(userService.syncIamAccounts());
|
@RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@RequestParam(defaultValue = "50") Integer size) {
|
||||||
|
return R.data(oaSyncService.syncPersonFromUserList(current, size));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,6 +232,20 @@ public class UserController {
|
|||||||
return R.status(temp);
|
return R.status(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密)
|
||||||
|
* <p>
|
||||||
|
* 对外路径:/blade-system/user/password ;网关别名 /blade-user/password 亦可到达。
|
||||||
|
*/
|
||||||
|
@PostMapping("/password")
|
||||||
|
@ApiOperationSupport(order = 10)
|
||||||
|
@Operation(summary = "设置登录密码", description = "当前登录用户设置密码,无需原密码")
|
||||||
|
public R password(BladeUser user,
|
||||||
|
@Parameter(description = "新密码", required = true) @RequestParam String password,
|
||||||
|
@Parameter(description = "确认密码", required = true) @RequestParam String password2) {
|
||||||
|
return R.status(userService.setPassword(user.getUserId(), password, password2));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员修改密码
|
* 管理员修改密码
|
||||||
*/
|
*/
|
||||||
|
|||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.controller;
|
||||||
|
|
||||||
|
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springblade.core.tenant.annotation.NonDS;
|
||||||
|
import org.springblade.core.tool.api.R;
|
||||||
|
import org.springblade.system.pojo.dto.PhoneChangeDTO;
|
||||||
|
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
|
||||||
|
import org.springblade.system.service.IUserPhoneService;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户手机号变更(小程序「修改手机号」)
|
||||||
|
* <p>
|
||||||
|
* 对外路径:/blade-system/user/phone/** ;网关别名 /blade-user/phone/** 亦可到达。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@NonDS
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RequestMapping("/user/phone")
|
||||||
|
@Tag(name = "用户手机号", description = "修改手机号")
|
||||||
|
public class UserPhoneController {
|
||||||
|
|
||||||
|
private final IUserPhoneService userPhoneService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码(需登录)
|
||||||
|
* <p>
|
||||||
|
* 当前手机号、未占用的新手机号均可发送;新号若已被其他账号占用则拒绝。
|
||||||
|
*/
|
||||||
|
@PostMapping("/send-code")
|
||||||
|
@ApiOperationSupport(order = 1)
|
||||||
|
@Operation(summary = "发送手机号变更验证码", description = "传入明文手机号,返回短信校验 id")
|
||||||
|
public R sendCode(@Parameter(description = "手机号", required = true) @RequestParam String phone) {
|
||||||
|
return userPhoneService.sendCode(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验原手机号验证码(修改手机号第 1 步)
|
||||||
|
*/
|
||||||
|
@PostMapping("/verify-old")
|
||||||
|
@ApiOperationSupport(order = 2)
|
||||||
|
@Operation(summary = "校验原手机号验证码", description = "传入发送验证码返回的 id 与验证码")
|
||||||
|
public R verifyOld(@Valid @RequestBody PhoneVerifyDTO phoneVerify) {
|
||||||
|
return R.status(userPhoneService.verifyOldPhone(phoneVerify));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定新手机号(修改手机号第 3 步,需先完成 verify-old)
|
||||||
|
*/
|
||||||
|
@PostMapping("/change")
|
||||||
|
@ApiOperationSupport(order = 3)
|
||||||
|
@Operation(summary = "更换手机号", description = "传入新手机号及短信校验 id、验证码")
|
||||||
|
public R change(@Valid @RequestBody PhoneChangeDTO phoneChange) {
|
||||||
|
return R.status(userPhoneService.changePhone(phoneChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+25
-3
@@ -64,11 +64,11 @@ public interface UserConvert {
|
|||||||
@Mapping(target = "password", ignore = true)
|
@Mapping(target = "password", ignore = true)
|
||||||
@Mapping(target = "birthday", ignore = true)
|
@Mapping(target = "birthday", ignore = true)
|
||||||
@Mapping(target = "sex", ignore = true)
|
@Mapping(target = "sex", ignore = true)
|
||||||
|
@Mapping(target = "account", ignore = true)
|
||||||
@Mapping(source = "workcode", target = "code")
|
@Mapping(source = "workcode", target = "code")
|
||||||
@Mapping(source = "lastname", target = "name")
|
@Mapping(source = "lastname", target = "name")
|
||||||
@Mapping(source = "lastname", target = "realName")
|
@Mapping(source = "lastname", target = "realName")
|
||||||
@Mapping(source = "mobile", target = "phone")
|
@Mapping(source = "mobile", target = "phone")
|
||||||
@Mapping(source = "mobile", target = "account")
|
|
||||||
@Mapping(source = "email", target = "email")
|
@Mapping(source = "email", target = "email")
|
||||||
User baseConvert(OAPersonResponse person);
|
User baseConvert(OAPersonResponse person);
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ public interface UserConvert {
|
|||||||
*/
|
*/
|
||||||
default User person2user(OAPersonResponse person, String defaultPassword) {
|
default User person2user(OAPersonResponse person, String defaultPassword) {
|
||||||
User user = baseConvert(person);
|
User user = baseConvert(person);
|
||||||
|
user.setAccount(resolveAccount(person));
|
||||||
// 密码
|
// 密码
|
||||||
user.setPassword(defaultPassword);
|
user.setPassword(defaultPassword);
|
||||||
// 性别
|
// 性别
|
||||||
@@ -102,6 +102,28 @@ public interface UserConvert {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析本系统登录账号:优先 OA loginid,其次工号,最后手机号
|
||||||
|
*
|
||||||
|
* @param person OA人员
|
||||||
|
* @return 账号,无法识别时返回 null
|
||||||
|
*/
|
||||||
|
default String resolveAccount(OAPersonResponse person) {
|
||||||
|
if (person == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(person.getLoginid())) {
|
||||||
|
return person.getLoginid().trim();
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(person.getWorkcode())) {
|
||||||
|
return person.getWorkcode().trim();
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(person.getMobile())) {
|
||||||
|
return person.getMobile().trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* oa人员转本系统用户部门
|
* oa人员转本系统用户部门
|
||||||
* @param person
|
* @param person
|
||||||
@@ -120,7 +142,7 @@ public interface UserConvert {
|
|||||||
// 排序
|
// 排序
|
||||||
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
|
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
|
||||||
// 用户id
|
// 用户id
|
||||||
userDept.setUserId(userMap.get(person.getMobile()));
|
userDept.setUserId(userMap.get(resolveAccount(person)));
|
||||||
userDept.setSyncTime(new Date());
|
userDept.setSyncTime(new Date());
|
||||||
return userDept;
|
return userDept;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,4 +231,10 @@ public class SysClient implements ISysClient {
|
|||||||
.orderByAsc(CargoType::getCargoCode)));
|
.orderByAsc(CargoType::getCargoCode)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@GetMapping(PERMISSIONS)
|
||||||
|
public R<List<String>> getPermissions(String roleId) {
|
||||||
|
return R.data(menuService.permissionCodes(roleId));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,4 +133,10 @@ public class UserClient implements IUserClient {
|
|||||||
return R.data(service.remove(Wrappers.<User>query().lambda().in(User::getTenantId, Func.toStrList(tenantIds))));
|
return R.data(service.remove(Wrappers.<User>query().lambda().in(User::getTenantId, Func.toStrList(tenantIds))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@PostMapping(BIND_WX_MINI_OPENID)
|
||||||
|
public R<Boolean> bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) {
|
||||||
|
return R.data(service.bindWxMiniOpenId(tenantId, userId, openid, phone));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -77,6 +77,14 @@ public interface IMenuService extends IService<Menu> {
|
|||||||
*/
|
*/
|
||||||
List<MenuVO> buttons(String roleId);
|
List<MenuVO> buttons(String roleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限标识集合(按钮编号,与前端 GetButtons 叶子 code 一致)
|
||||||
|
*
|
||||||
|
* @param roleId 角色id
|
||||||
|
* @return 权限标识
|
||||||
|
*/
|
||||||
|
List<String> permissionCodes(String roleId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树形结构
|
* 树形结构
|
||||||
*
|
*
|
||||||
|
|||||||
+18
@@ -1,5 +1,7 @@
|
|||||||
package org.springblade.system.service;
|
package org.springblade.system.service;
|
||||||
|
|
||||||
|
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* oa同步接口
|
* oa同步接口
|
||||||
* @author bfhuange
|
* @author bfhuange
|
||||||
@@ -18,4 +20,20 @@ public interface IOASyncService {
|
|||||||
* @param syncAll 是否同步所有
|
* @param syncAll 是否同步所有
|
||||||
*/
|
*/
|
||||||
void syncPersonAndPushMK(boolean syncAll);
|
void syncPersonAndPushMK(boolean syncAll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 OA 人员接口全量同步组织与人员,不推送 MK
|
||||||
|
*
|
||||||
|
* @return 处理的人员数量
|
||||||
|
*/
|
||||||
|
int syncPersonFromUserList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按页从 OA 人员接口同步组织与人员
|
||||||
|
*
|
||||||
|
* @param current 当前页,从 1 开始
|
||||||
|
* @param size 每页条数
|
||||||
|
* @return 本页同步结果
|
||||||
|
*/
|
||||||
|
OaPersonSyncPageVO syncPersonFromUserList(int current, int size);
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.service;
|
||||||
|
|
||||||
|
import org.springblade.core.tool.api.R;
|
||||||
|
import org.springblade.system.pojo.dto.PhoneChangeDTO;
|
||||||
|
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户手机号变更服务
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
public interface IUserPhoneService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送变更手机号短信验证码
|
||||||
|
*
|
||||||
|
* @param phone 明文手机号
|
||||||
|
* @return 含短信校验 id 的响应
|
||||||
|
*/
|
||||||
|
R sendCode(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验原手机号验证码,通过后写入短期凭证
|
||||||
|
*
|
||||||
|
* @param phoneVerify 校验参数
|
||||||
|
* @return 是否通过
|
||||||
|
*/
|
||||||
|
boolean verifyOldPhone(PhoneVerifyDTO phoneVerify);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验新手机号验证码并更换手机号
|
||||||
|
*
|
||||||
|
* @param phoneChange 更换参数
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean changePhone(PhoneChangeDTO phoneChange);
|
||||||
|
|
||||||
|
}
|
||||||
+5
@@ -188,6 +188,11 @@ public interface IUserService extends BaseService<User> {
|
|||||||
*/
|
*/
|
||||||
UserInfo userInfo(UserOauth userOauth);
|
UserInfo userInfo(UserOauth userOauth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定微信小程序 openid 到已有用户(blade_user_oauth,source=WECHAT_MINI)
|
||||||
|
*/
|
||||||
|
boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据租户与账号获取用户
|
* 根据租户与账号获取用户
|
||||||
*
|
*
|
||||||
|
|||||||
+29
@@ -164,6 +164,35 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IM
|
|||||||
return menuWrapper.listNodeVO(buttons);
|
return menuWrapper.listNodeVO(buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> permissionCodes(String roleId) {
|
||||||
|
List<String> permissionCodes = new ArrayList<>();
|
||||||
|
// Feign 调用时无登录态,不能走 AuthUtil.isAdministrator() 分支;按 roleId 取按钮权限
|
||||||
|
List<Menu> buttons = StringUtil.isBlank(roleId)
|
||||||
|
? Collections.emptyList()
|
||||||
|
: baseMapper.buttons(Func.toLongList(roleId));
|
||||||
|
MenuWrapper menuWrapper = new MenuWrapper();
|
||||||
|
collectLeafPermissionCodes(menuWrapper.listNodeVO(buttons), permissionCodes);
|
||||||
|
return permissionCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归收集按钮树叶子节点的权限编号(与前端 SET_PERMISSION 逻辑一致)
|
||||||
|
*/
|
||||||
|
private void collectLeafPermissionCodes(List<MenuVO> menuList, List<String> permissionCodes) {
|
||||||
|
if (menuList == null || menuList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (MenuVO menu : menuList) {
|
||||||
|
List<MenuVO> children = menu.getChildren();
|
||||||
|
if (children != null && !children.isEmpty()) {
|
||||||
|
collectLeafPermissionCodes(children, permissionCodes);
|
||||||
|
} else if (StringUtil.isNotBlank(menu.getCode())) {
|
||||||
|
permissionCodes.add(menu.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TreeNode> tree() {
|
public List<TreeNode> tree() {
|
||||||
return ForestNodeMerger.merge(baseMapper.tree());
|
return ForestNodeMerger.merge(baseMapper.tree());
|
||||||
|
|||||||
+108
-181
@@ -11,16 +11,14 @@ import org.springblade.common.constant.DictTypeEnum;
|
|||||||
import org.springblade.core.cache.utils.CacheUtil;
|
import org.springblade.core.cache.utils.CacheUtil;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.tool.utils.DateUtil;
|
import org.springblade.core.tool.utils.DateUtil;
|
||||||
import org.springblade.core.tool.utils.DigestUtil;
|
|
||||||
import org.springblade.system.cache.DictCache;
|
import org.springblade.system.cache.DictCache;
|
||||||
import org.springblade.system.cache.ParamCache;
|
import org.springblade.system.cache.ParamCache;
|
||||||
import org.springblade.system.convert.DeptConvert;
|
import org.springblade.system.convert.DeptConvert;
|
||||||
import org.springblade.system.convert.UserConvert;
|
|
||||||
import org.springblade.system.log.ComposeLogUtil;
|
import org.springblade.system.log.ComposeLogUtil;
|
||||||
import org.springblade.system.pojo.entity.*;
|
import org.springblade.system.pojo.entity.*;
|
||||||
import org.springblade.system.pojo.enums.DataSync;
|
import org.springblade.system.pojo.enums.DataSync;
|
||||||
import org.springblade.system.pojo.enums.DeptCategory;
|
import org.springblade.system.pojo.enums.DeptCategory;
|
||||||
import org.springblade.system.pojo.vo.UserDeptIdsVO;
|
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
|
||||||
import org.springblade.system.service.*;
|
import org.springblade.system.service.*;
|
||||||
import org.springblade.system.util.DataSyncRecordUtils;
|
import org.springblade.system.util.DataSyncRecordUtils;
|
||||||
import org.springblade.thirdparty.oa.constant.OAConstant;
|
import org.springblade.thirdparty.oa.constant.OAConstant;
|
||||||
@@ -29,6 +27,8 @@ import org.springblade.thirdparty.oa.feign.IOAClient;
|
|||||||
import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse;
|
import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse;
|
||||||
import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse;
|
import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse;
|
||||||
import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
|
import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
|
||||||
|
import org.springblade.thirdparty.oa.pojo.response.OAResponse;
|
||||||
|
import org.springblade.thirdparty.oa.pojo.response.OAResponseData;
|
||||||
import org.springblade.thirdparty.oa.pojo.search.OACompanySearch;
|
import org.springblade.thirdparty.oa.pojo.search.OACompanySearch;
|
||||||
import org.springblade.thirdparty.oa.pojo.search.OADepartmentSearch;
|
import org.springblade.thirdparty.oa.pojo.search.OADepartmentSearch;
|
||||||
import org.springblade.thirdparty.oa.pojo.search.OAPersonSearch;
|
import org.springblade.thirdparty.oa.pojo.search.OAPersonSearch;
|
||||||
@@ -38,6 +38,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -54,20 +55,14 @@ import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
public class OASyncServiceImpl implements IOASyncService {
|
public class OASyncServiceImpl implements IOASyncService {
|
||||||
/**
|
|
||||||
* 用户表部门id最大长度
|
|
||||||
*/
|
|
||||||
private static final int MAX_DEPT_ID_LENGTH = 2000;
|
|
||||||
|
|
||||||
private final IOAClient oaClient;
|
private final IOAClient oaClient;
|
||||||
private final DeptConvert deptConvert;
|
private final DeptConvert deptConvert;
|
||||||
private final IDeptService deptService;
|
private final IDeptService deptService;
|
||||||
private final IUserService userService;
|
|
||||||
private final UserConvert userConvert;
|
|
||||||
private final IUserDeptService userDeptService;
|
|
||||||
private final IRoleService roleService;
|
private final IRoleService roleService;
|
||||||
private final IMKPushService mkPushService;
|
private final IMKPushService mkPushService;
|
||||||
private final IDataSyncRecordService dataSyncRecordService;
|
private final IDataSyncRecordService dataSyncRecordService;
|
||||||
|
private final OaUserListSyncHelper oaUserListSyncHelper;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
@@ -104,6 +99,31 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int syncPersonFromUserList() {
|
||||||
|
AtomicInteger syncedCount = new AtomicInteger();
|
||||||
|
try {
|
||||||
|
ComposeLogUtil.addLog(log);
|
||||||
|
this.syncAndRecord(DataSyncRecordUtils::createOAPersonFetch, startTime ->
|
||||||
|
syncedCount.set(this.syncPersonFromOa(null)), true);
|
||||||
|
return syncedCount.get();
|
||||||
|
} finally {
|
||||||
|
ComposeLogUtil.removeLastLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public OaPersonSyncPageVO syncPersonFromUserList(int current, int size) {
|
||||||
|
try {
|
||||||
|
ComposeLogUtil.addLog(log);
|
||||||
|
return this.syncPersonFromOaPage(current, size);
|
||||||
|
} finally {
|
||||||
|
ComposeLogUtil.removeLastLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步并记录
|
* 同步并记录
|
||||||
*
|
*
|
||||||
@@ -213,174 +233,93 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
* @param startTime 查询开始时间
|
* @param startTime 查询开始时间
|
||||||
*/
|
*/
|
||||||
private void syncPerson(Date startTime) {
|
private void syncPerson(Date startTime) {
|
||||||
String subCompanyIds = getSubCompanyIds();
|
this.syncPersonFromOa(startTime);
|
||||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
|
||||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 1. 设置查询参数
|
|
||||||
OAPersonSearch personSearch = new OAPersonSearch();
|
|
||||||
personSearch.setCurPage(1);
|
|
||||||
personSearch.setSubcompanyid1(subCompanyIds);
|
|
||||||
if (startTime != null) {
|
|
||||||
// 开始时间不为空,设置修改时间参数
|
|
||||||
personSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
|
||||||
}
|
|
||||||
// 2. 分页查询并处理数据
|
|
||||||
OAUtils.pageSyncHandler(personSearch, param -> oaClient.queryPersonPage(new OASearch<>(param)), response -> {
|
|
||||||
ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(response));
|
|
||||||
return new ServiceException("调用OA接口查询人员信息失败");
|
|
||||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(this::handlePerson);
|
|
||||||
// 清除用户缓存
|
|
||||||
CacheUtil.clear(USER_CACHE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理oa人员
|
* 从 OA 人员列表同步组织与人员
|
||||||
* @param oaPersons
|
*
|
||||||
|
* @param startTime 增量查询开始时间,为空则全量
|
||||||
|
* @return 处理的人员数量
|
||||||
*/
|
*/
|
||||||
private void handlePerson(List<OAPersonResponse> oaPersons) {
|
private int syncPersonFromOa(Date startTime) {
|
||||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
OAPersonSearch personSearch = buildPersonSearch(startTime);
|
||||||
// 数据为空,直接返回
|
List<OAPersonResponse> oaPersons = new ArrayList<>();
|
||||||
return;
|
OAUtils.pageSyncHandler(personSearch, param -> oaClient.queryPersonPage(new OASearch<>(param)), response -> {
|
||||||
|
ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(response));
|
||||||
|
return new ServiceException("调用OA接口查询人员信息失败");
|
||||||
|
}, 10000, ComposeLogUtil.getLastLog()::info).accept(oaPersons::addAll);
|
||||||
|
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||||
|
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||||
|
CacheUtil.clear(USER_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||||
|
return personSyncCount.getSyncedCount();
|
||||||
}
|
}
|
||||||
// 根公司id
|
|
||||||
String rootCompanyId = getRootCompanyId();
|
|
||||||
if (rootCompanyId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 根公司下要同步的部门id
|
|
||||||
Set<String> rootCompanyDeptIds = getRootCompanyDeptIds(rootCompanyId);
|
|
||||||
oaPersons = oaPersons.stream()
|
|
||||||
// 公司不是根公司,或者部门在根公司下要同步的部门列表中
|
|
||||||
.filter(oaPerson -> !rootCompanyId.equals(oaPerson.getSubcompanyid1()) || rootCompanyDeptIds.contains(oaPerson.getDepartmentid()))
|
|
||||||
.toList();
|
|
||||||
// 根据手机号转成set
|
|
||||||
TreeSet<OAPersonResponse> oaPersonSet = CollectionUtil.toTreeSet(oaPersons, Comparator.comparing(OAPersonResponse::getMobile));
|
|
||||||
// 默认密码
|
|
||||||
String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
|
||||||
// 转换数据
|
|
||||||
List<User> users = oaPersonSet.stream()
|
|
||||||
// 只需要手机不为空的
|
|
||||||
.filter(person -> StringUtils.isNotBlank(person.getMobile()))
|
|
||||||
.map(person -> userConvert.person2user(person, defaultPassword))
|
|
||||||
.toList();
|
|
||||||
List<String> phones = users.stream()
|
|
||||||
.map(User::getPhone)
|
|
||||||
.filter(StringUtils::isNotBlank)
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
// 查询所有用户
|
|
||||||
Map<String, Long> userMap = userService.list(Wrappers.<User>lambdaQuery()
|
|
||||||
//.eq(User::getIsDeleted, BladeConstant.DB_NOT_DELETED)
|
|
||||||
.in(User::getPhone, phones)
|
|
||||||
).stream()
|
|
||||||
// 解密手机号
|
|
||||||
.peek(userService::decryptPhone)
|
|
||||||
.collect(Collectors.toMap(User::getPhone, User::getId, (a, b) -> b));
|
|
||||||
// 数据库存在的所有用户id
|
|
||||||
Set<Long> existsUserIds = new HashSet<>(userMap.values());
|
|
||||||
users.forEach(user -> {
|
|
||||||
if (userMap.containsKey(user.getPhone())) {
|
|
||||||
// 根据手机号获取对应的用户id
|
|
||||||
user.setId(userMap.get(user.getPhone()));
|
|
||||||
// 清空密码,不修改密码
|
|
||||||
user.setPassword(null);
|
|
||||||
} else {
|
|
||||||
// 没有就生成一个id
|
|
||||||
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
|
||||||
userService.encryptPhone(user);
|
|
||||||
userMap.put(user.getPhone(), user.getId());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 不存在的新增
|
/**
|
||||||
List<User> addUsers = users.stream()
|
* 按页从 OA 人员列表同步组织与人员
|
||||||
.filter(user -> !existsUserIds.contains(user.getId()))
|
*
|
||||||
.toList();
|
* @param current 当前页
|
||||||
if (CollectionUtil.isNotEmpty(addUsers)) {
|
* @param size 每页条数
|
||||||
ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size());
|
* @return 本页同步结果
|
||||||
userService.saveBatch(addUsers);
|
*/
|
||||||
|
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size) {
|
||||||
|
int pageNo = current < 1 ? 1 : current;
|
||||||
|
int pageSize = size < 1 ? 50 : Math.min(size, 200);
|
||||||
|
OAPersonSearch personSearch = buildPersonSearch(null);
|
||||||
|
personSearch.setCurPage(pageNo);
|
||||||
|
personSearch.setPageSize(pageSize);
|
||||||
|
OAResponse<OAPersonResponse> oaResponse = oaClient.queryPersonPage(new OASearch<>(personSearch));
|
||||||
|
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
|
||||||
|
ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(oaResponse));
|
||||||
|
throw new ServiceException("调用OA接口查询人员信息失败");
|
||||||
}
|
}
|
||||||
// 存在的修改
|
OAResponseData<OAPersonResponse> responseData = oaResponse.getData();
|
||||||
List<User> updateUsers = users.stream()
|
List<OAPersonResponse> oaPersons = responseData.getDataList() == null
|
||||||
.filter(user -> existsUserIds.contains(user.getId()))
|
? Collections.emptyList() : responseData.getDataList();
|
||||||
.toList();
|
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||||
if (CollectionUtil.isNotEmpty(updateUsers)) {
|
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||||
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
|
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||||
userService.updateBatchById(updateUsers);
|
CacheUtil.clear(USER_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||||
|
OaPersonSyncPageVO pageVO = new OaPersonSyncPageVO();
|
||||||
|
pageVO.setCurrent(pageNo);
|
||||||
|
pageVO.setSize(pageSize);
|
||||||
|
pageVO.setTotal(totalSize);
|
||||||
|
pageVO.setFetchedCount(oaPersons.size());
|
||||||
|
pageVO.setSyncedCount(personSyncCount.getSyncedCount());
|
||||||
|
pageVO.setSkippedCount(personSyncCount.getSkippedCount());
|
||||||
|
boolean finished = oaPersons.isEmpty()
|
||||||
|
|| oaPersons.size() < pageSize
|
||||||
|
|| (long) pageNo * pageSize >= totalSize;
|
||||||
|
pageVO.setFinished(finished);
|
||||||
|
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 {}/{},成功{},跳过{}",
|
||||||
|
pageNo, totalSize, personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
|
||||||
|
return pageVO;
|
||||||
}
|
}
|
||||||
// 没有手机号的数据 = 手机号为空的数量
|
|
||||||
long noPhoneNum = oaPersons.stream()
|
|
||||||
.map(OAPersonResponse::getMobile)
|
|
||||||
.filter(StringUtils::isBlank)
|
|
||||||
.count();
|
|
||||||
ComposeLogUtil.getLastLog().info("没有手机号的数据:{}", noPhoneNum);
|
|
||||||
|
|
||||||
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
/**
|
||||||
.in(UserDept::getUserId, userMap.values())
|
* 组装 OA 人员分页查询参数
|
||||||
).stream()
|
*
|
||||||
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (a, b) -> b));
|
* @param startTime 增量查询开始时间
|
||||||
// 数据库存在的所有用户部门id
|
* @return 查询参数
|
||||||
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
|
*/
|
||||||
|
private OAPersonSearch buildPersonSearch(Date startTime) {
|
||||||
List<UserDept> userDeptList = oaPersons.stream()
|
OAPersonSearch personSearch = new OAPersonSearch();
|
||||||
// 只要包含用户手机号的
|
personSearch.setCurPage(1);
|
||||||
.filter(oaPerson -> userMap.containsKey(oaPerson.getMobile()))
|
personSearch.setPageSize(200);
|
||||||
.map(oaPerson -> userConvert.person2userDept(oaPerson, userMap))
|
personSearch.setCreated("");
|
||||||
.toList();
|
personSearch.setWorkcode("");
|
||||||
userDeptList.forEach(userDept -> {
|
personSearch.setSubcompanyid1("");
|
||||||
String userDeptKey = getUserDeptKey(userDept);
|
personSearch.setDepartmentid("");
|
||||||
if (userDeptMap.containsKey(userDeptKey)) {
|
personSearch.setJobtitleid("");
|
||||||
// 根据key获取用户部门id
|
personSearch.setId("");
|
||||||
userDept.setId(userDeptMap.get(userDeptKey));
|
personSearch.setLoginid("");
|
||||||
} else {
|
personSearch.setIsadaccount("");
|
||||||
// 没有就生成一个id
|
personSearch.setModified(startTime == null ? "" : DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||||
userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
return personSearch;
|
||||||
}
|
|
||||||
});
|
|
||||||
// 不存在的新增
|
|
||||||
List<UserDept> addList = userDeptList.stream()
|
|
||||||
.filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
|
|
||||||
.toList();
|
|
||||||
if (CollectionUtil.isNotEmpty(addList)) {
|
|
||||||
ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size());
|
|
||||||
userDeptService.saveBatch(addList);
|
|
||||||
}
|
|
||||||
// 存在的修改
|
|
||||||
List<UserDept> updateList = userDeptList.stream()
|
|
||||||
.filter(userDept -> existsUserDeptIds.contains(userDept.getId()))
|
|
||||||
.toList();
|
|
||||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
|
||||||
ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size());
|
|
||||||
userDeptService.updateBatchById(updateList);
|
|
||||||
}
|
|
||||||
// 回写部门id到用户表
|
|
||||||
Collection<Long> userIds = userMap.values();
|
|
||||||
List<UserDeptIdsVO> list = userDeptService.queryUserDeptIds(userIds);
|
|
||||||
// 查询没有角色的用户id
|
|
||||||
Set<Long> noRoleUserIds = userService.list(Wrappers.<User>lambdaQuery()
|
|
||||||
.in(User::getId, userIds)
|
|
||||||
.isNull(User::getRoleId)
|
|
||||||
).stream()
|
|
||||||
.map(User::getId)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
// 获取默认角色id
|
|
||||||
String defaultRoleId = getDefaultRoleId();
|
|
||||||
List<User> updateUserParams = list.stream()
|
|
||||||
// 过滤掉空部门id及长度超长的
|
|
||||||
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
|
|
||||||
.map(userDeptIds -> {
|
|
||||||
User user = new User();
|
|
||||||
user.setId(userDeptIds.getUserId());
|
|
||||||
user.setDeptId(userDeptIds.getDeptIds());
|
|
||||||
user.setDeptCodes(userDeptIds.getDeptCodes());
|
|
||||||
if (noRoleUserIds.contains(userDeptIds.getUserId())) {
|
|
||||||
user.setRoleId(defaultRoleId);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}).toList();
|
|
||||||
userService.updateBatchById(updateUserParams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -634,16 +573,4 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
// 部门编码去掉前缀,就是oa的id
|
// 部门编码去掉前缀,就是oa的id
|
||||||
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
|
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户部门唯一标识,用户id+公司编码+部门编码
|
|
||||||
* @param userDept
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String getUserDeptKey(UserDept userDept) {
|
|
||||||
if (userDept == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return userDept.getUserId() + userDept.getCompanyCode() + userDept.getDeptCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+609
@@ -0,0 +1,609 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springblade.common.constant.DataStatusEnum;
|
||||||
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
|
import org.springblade.core.tool.constant.BladeConstant;
|
||||||
|
import org.springblade.core.tool.utils.DigestUtil;
|
||||||
|
import org.springblade.system.cache.ParamCache;
|
||||||
|
import org.springblade.system.convert.UserConvert;
|
||||||
|
import org.springblade.system.log.ComposeLogUtil;
|
||||||
|
import org.springblade.system.pojo.entity.Dept;
|
||||||
|
import org.springblade.system.pojo.entity.Role;
|
||||||
|
import org.springblade.system.pojo.entity.User;
|
||||||
|
import org.springblade.system.pojo.entity.UserDept;
|
||||||
|
import org.springblade.system.pojo.enums.DeptCategory;
|
||||||
|
import org.springblade.system.pojo.vo.UserDeptIdsVO;
|
||||||
|
import org.springblade.system.service.IDeptService;
|
||||||
|
import org.springblade.system.service.IRoleService;
|
||||||
|
import org.springblade.system.service.IUserDeptService;
|
||||||
|
import org.springblade.system.service.IUserService;
|
||||||
|
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
||||||
|
import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_PASSWORD;
|
||||||
|
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_ROLE;
|
||||||
|
import static org.springblade.common.constant.CommonConstant.DEFAULT_ROLE;
|
||||||
|
import static org.springblade.common.constant.CommonConstant.YES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 OA 人员列表提取组织并同步人员
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OaUserListSyncHelper {
|
||||||
|
|
||||||
|
private static final int MAX_DEPT_ID_LENGTH = 2000;
|
||||||
|
|
||||||
|
private final IDeptService deptService;
|
||||||
|
private final IUserService userService;
|
||||||
|
private final UserConvert userConvert;
|
||||||
|
private final IUserDeptService userDeptService;
|
||||||
|
private final IRoleService roleService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从人员数据提取二级公司、三级部门,并挂到「桂物物流集团」下
|
||||||
|
*
|
||||||
|
* @param oaPersons OA人员
|
||||||
|
* @return 组织索引
|
||||||
|
*/
|
||||||
|
public OaOrgIndex syncOrgsFromPersons(List<OAPersonResponse> oaPersons) {
|
||||||
|
OaOrgIndex orgIndex = new OaOrgIndex();
|
||||||
|
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||||
|
return orgIndex;
|
||||||
|
}
|
||||||
|
Date orgSyncStart = new Date();
|
||||||
|
Dept rootCompany = this.getOrCreateRootCompany();
|
||||||
|
String tenantId = resolveTenantId();
|
||||||
|
Map<String, OAPersonResponse> companyPersonMap = new LinkedHashMap<>();
|
||||||
|
Map<String, OAPersonResponse> departmentPersonMap = new LinkedHashMap<>();
|
||||||
|
for (OAPersonResponse oaPerson : oaPersons) {
|
||||||
|
String companyKey = resolveCompanyKey(oaPerson);
|
||||||
|
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||||
|
companyPersonMap.putIfAbsent(companyKey, oaPerson);
|
||||||
|
}
|
||||||
|
String departmentKey = resolveDepartmentKey(oaPerson);
|
||||||
|
if (StringUtils.isNotBlank(departmentKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())
|
||||||
|
&& StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||||
|
departmentPersonMap.putIfAbsent(departmentKey, oaPerson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Dept> existingCompanyByCode = new HashMap<>();
|
||||||
|
Map<String, Dept> existingCompanyByName = new HashMap<>();
|
||||||
|
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.eq(Dept::getParentId, rootCompany.getId())
|
||||||
|
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||||
|
).forEach(dept -> {
|
||||||
|
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||||
|
existingCompanyByCode.put(dept.getDeptCode(), dept);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(dept.getDeptName())) {
|
||||||
|
existingCompanyByName.put(dept.getDeptName(), dept);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<Dept> addCompanies = new ArrayList<>();
|
||||||
|
List<Dept> updateCompanies = new ArrayList<>();
|
||||||
|
companyPersonMap.forEach((companyKey, oaPerson) -> {
|
||||||
|
String oaCode = buildCompanyCode(oaPerson, companyKey);
|
||||||
|
Dept existing = existingCompanyByCode.get(oaCode);
|
||||||
|
if (existing == null) {
|
||||||
|
existing = existingCompanyByName.get(oaPerson.getSubcompanyname());
|
||||||
|
}
|
||||||
|
Dept company = this.upsertOrg(oaPerson.getSubcompanyname(), oaCode, existing, rootCompany, tenantId,
|
||||||
|
DeptCategory.COMPANY, addCompanies, updateCompanies);
|
||||||
|
orgIndex.companyByOaId.put(companyKey, company);
|
||||||
|
});
|
||||||
|
this.saveOrgs(addCompanies, updateCompanies, DeptCategory.COMPANY);
|
||||||
|
|
||||||
|
Map<String, Dept> existingDeptByCode = new HashMap<>();
|
||||||
|
Map<String, Dept> existingDeptByParentAndName = new HashMap<>();
|
||||||
|
List<Long> companyIds = orgIndex.companyByOaId.values().stream()
|
||||||
|
.map(Dept::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (CollectionUtil.isNotEmpty(companyIds)) {
|
||||||
|
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.in(Dept::getParentId, companyIds)
|
||||||
|
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||||
|
).forEach(dept -> {
|
||||||
|
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||||
|
existingDeptByCode.put(dept.getDeptCode(), dept);
|
||||||
|
}
|
||||||
|
if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getDeptName())) {
|
||||||
|
existingDeptByParentAndName.put(dept.getParentId() + "#" + dept.getDeptName(), dept);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
List<Dept> addDepartments = new ArrayList<>();
|
||||||
|
List<Dept> updateDepartments = new ArrayList<>();
|
||||||
|
departmentPersonMap.forEach((departmentKey, oaPerson) -> {
|
||||||
|
Dept parentCompany = orgIndex.findCompany(oaPerson);
|
||||||
|
if (parentCompany == null || parentCompany.getId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String oaCode = buildDepartmentCode(oaPerson, departmentKey);
|
||||||
|
Dept existing = existingDeptByCode.get(oaCode);
|
||||||
|
if (existing == null) {
|
||||||
|
existing = existingDeptByParentAndName.get(parentCompany.getId() + "#" + oaPerson.getDepartmentname());
|
||||||
|
}
|
||||||
|
Dept department = this.upsertOrg(oaPerson.getDepartmentname(), oaCode, existing, parentCompany, tenantId,
|
||||||
|
DeptCategory.DEPT, addDepartments, updateDepartments);
|
||||||
|
orgIndex.deptByOaId.put(departmentKey, department);
|
||||||
|
orgIndex.deptByCompanyAndName.put(resolveCompanyKey(oaPerson) + "#" + oaPerson.getDepartmentname(), department);
|
||||||
|
});
|
||||||
|
this.saveOrgs(addDepartments, updateDepartments, DeptCategory.DEPT);
|
||||||
|
deptService.updateAncestors(orgSyncStart);
|
||||||
|
ComposeLogUtil.getLastLog().info("同步人员提取组织完成,二级公司{}个,三级部门{}个",
|
||||||
|
orgIndex.companyByOaId.size(), orgIndex.deptByOaId.size());
|
||||||
|
return orgIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步人员并绑定到三级部门
|
||||||
|
*
|
||||||
|
* @param oaPersons OA人员
|
||||||
|
* @param orgIndex 组织索引
|
||||||
|
* @return 本批同步成功与跳过数量
|
||||||
|
*/
|
||||||
|
public PersonSyncCount handlePerson(List<OAPersonResponse> oaPersons, OaOrgIndex orgIndex) {
|
||||||
|
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||||
|
return new PersonSyncCount(0, 0);
|
||||||
|
}
|
||||||
|
Map<String, OAPersonResponse> uniquePersonMap = new LinkedHashMap<>();
|
||||||
|
int skippedCount = 0;
|
||||||
|
for (OAPersonResponse oaPerson : oaPersons) {
|
||||||
|
String account = userConvert.resolveAccount(oaPerson);
|
||||||
|
if (StringUtils.isBlank(account)) {
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uniquePersonMap.putIfAbsent(account, oaPerson);
|
||||||
|
}
|
||||||
|
if (uniquePersonMap.isEmpty()) {
|
||||||
|
ComposeLogUtil.getLastLog().warn("OA人员均缺少loginid/工号/手机号,跳过人员同步");
|
||||||
|
return new PersonSyncCount(0, skippedCount);
|
||||||
|
}
|
||||||
|
String tenantId = resolveTenantId();
|
||||||
|
String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
||||||
|
List<User> users = uniquePersonMap.values().stream()
|
||||||
|
.map(person -> {
|
||||||
|
User user = userConvert.person2user(person, defaultPassword);
|
||||||
|
user.setTenantId(tenantId);
|
||||||
|
return user;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
List<String> accounts = users.stream()
|
||||||
|
.map(User::getAccount)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
List<String> phones = users.stream()
|
||||||
|
.map(User::getPhone)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
Map<String, User> existingByAccount = new HashMap<>();
|
||||||
|
Map<String, User> existingByPhone = new HashMap<>();
|
||||||
|
userService.list(Wrappers.<User>lambdaQuery()
|
||||||
|
.and(wrapper -> {
|
||||||
|
wrapper.in(User::getAccount, accounts);
|
||||||
|
if (CollectionUtil.isNotEmpty(phones)) {
|
||||||
|
wrapper.or().in(User::getPhone, phones);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).stream()
|
||||||
|
.peek(userService::decryptPhone)
|
||||||
|
.forEach(user -> {
|
||||||
|
if (StringUtils.isNotBlank(user.getAccount())) {
|
||||||
|
existingByAccount.put(user.getAccount(), user);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(user.getPhone())) {
|
||||||
|
existingByPhone.put(user.getPhone(), user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Map<String, Long> userMap = new HashMap<>();
|
||||||
|
Set<Long> existsUserIds = new HashSet<>();
|
||||||
|
users.forEach(user -> {
|
||||||
|
User existingUser = existingByAccount.get(user.getAccount());
|
||||||
|
if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) {
|
||||||
|
existingUser = existingByPhone.get(user.getPhone());
|
||||||
|
}
|
||||||
|
if (existingUser != null) {
|
||||||
|
user.setId(existingUser.getId());
|
||||||
|
user.setPassword(null);
|
||||||
|
existsUserIds.add(existingUser.getId());
|
||||||
|
} else {
|
||||||
|
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||||
|
user.setPostId("-1");
|
||||||
|
user.setPersonCategory(1);
|
||||||
|
user.setDataScopeRange(1);
|
||||||
|
user.setDataLevelRange(1);
|
||||||
|
user.setIncludeNewCustomer(0);
|
||||||
|
userService.encryptPhone(user);
|
||||||
|
}
|
||||||
|
userMap.put(user.getAccount(), user.getId());
|
||||||
|
});
|
||||||
|
|
||||||
|
List<User> addUsers = users.stream()
|
||||||
|
.filter(user -> !existsUserIds.contains(user.getId()))
|
||||||
|
.toList();
|
||||||
|
if (CollectionUtil.isNotEmpty(addUsers)) {
|
||||||
|
ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size());
|
||||||
|
userService.saveBatch(addUsers);
|
||||||
|
}
|
||||||
|
List<User> updateUsers = users.stream()
|
||||||
|
.filter(user -> existsUserIds.contains(user.getId()))
|
||||||
|
.toList();
|
||||||
|
if (CollectionUtil.isNotEmpty(updateUsers)) {
|
||||||
|
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
|
||||||
|
userService.updateBatchById(updateUsers);
|
||||||
|
}
|
||||||
|
ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount);
|
||||||
|
if (userMap.isEmpty()) {
|
||||||
|
return new PersonSyncCount(0, skippedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||||
|
.in(UserDept::getUserId, userMap.values())
|
||||||
|
).stream()
|
||||||
|
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second));
|
||||||
|
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
|
||||||
|
List<UserDept> userDeptList = oaPersons.stream()
|
||||||
|
.filter(oaPerson -> userMap.containsKey(userConvert.resolveAccount(oaPerson)))
|
||||||
|
.map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
userDeptList.forEach(userDept -> {
|
||||||
|
String userDeptKey = getUserDeptKey(userDept);
|
||||||
|
if (userDeptMap.containsKey(userDeptKey)) {
|
||||||
|
userDept.setId(userDeptMap.get(userDeptKey));
|
||||||
|
} else {
|
||||||
|
userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<UserDept> addList = userDeptList.stream()
|
||||||
|
.filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
|
||||||
|
.toList();
|
||||||
|
if (CollectionUtil.isNotEmpty(addList)) {
|
||||||
|
ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size());
|
||||||
|
userDeptService.saveBatch(addList);
|
||||||
|
}
|
||||||
|
List<UserDept> updateList = userDeptList.stream()
|
||||||
|
.filter(userDept -> existsUserDeptIds.contains(userDept.getId()))
|
||||||
|
.toList();
|
||||||
|
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||||
|
ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size());
|
||||||
|
userDeptService.updateBatchById(updateList);
|
||||||
|
}
|
||||||
|
Collection<Long> userIds = userMap.values();
|
||||||
|
List<UserDeptIdsVO> list = userDeptService.queryUserDeptIds(userIds);
|
||||||
|
Set<Long> noRoleUserIds = userService.list(Wrappers.<User>lambdaQuery()
|
||||||
|
.in(User::getId, userIds)
|
||||||
|
.and(wrapper -> wrapper.isNull(User::getRoleId)
|
||||||
|
.or().eq(User::getRoleId, "")
|
||||||
|
.or().eq(User::getRoleId, "-1"))
|
||||||
|
).stream()
|
||||||
|
.map(User::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
String defaultRoleId = getDefaultRoleId();
|
||||||
|
List<User> updateUserParams = list.stream()
|
||||||
|
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
|
||||||
|
.map(userDeptIds -> {
|
||||||
|
User user = new User();
|
||||||
|
user.setId(userDeptIds.getUserId());
|
||||||
|
user.setDeptId(userDeptIds.getDeptIds());
|
||||||
|
user.setDeptCodes(userDeptIds.getDeptCodes());
|
||||||
|
if (noRoleUserIds.contains(userDeptIds.getUserId())) {
|
||||||
|
user.setRoleId(defaultRoleId);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}).toList();
|
||||||
|
if (CollectionUtil.isNotEmpty(updateUserParams)) {
|
||||||
|
userService.updateBatchById(updateUserParams);
|
||||||
|
}
|
||||||
|
return new PersonSyncCount(users.size(), skippedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dept getOrCreateRootCompany() {
|
||||||
|
Dept rootCompany = deptService.getOne(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.eq(Dept::getDeptName, OAConvertConstant.ROOT_COMPANY_NAME)
|
||||||
|
.last("limit 1"), false);
|
||||||
|
if (rootCompany != null) {
|
||||||
|
return rootCompany;
|
||||||
|
}
|
||||||
|
rootCompany = new Dept();
|
||||||
|
rootCompany.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||||
|
rootCompany.setTenantId(resolveTenantId());
|
||||||
|
rootCompany.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||||
|
rootCompany.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||||
|
rootCompany.setDeptName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||||
|
rootCompany.setFullName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||||
|
rootCompany.setShortName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||||
|
rootCompany.setDeptCode("OACROOT");
|
||||||
|
rootCompany.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||||
|
rootCompany.setBelongCompanyCode("OACROOT");
|
||||||
|
rootCompany.setDeptCategory(DeptCategory.COMPANY.getCode());
|
||||||
|
rootCompany.setSort(0);
|
||||||
|
rootCompany.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||||
|
rootCompany.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||||
|
rootCompany.setIsOa(YES);
|
||||||
|
rootCompany.setIsPlatformCompany(0);
|
||||||
|
rootCompany.setSyncTime(new Date());
|
||||||
|
deptService.save(rootCompany);
|
||||||
|
ComposeLogUtil.getLastLog().info("已创建顶级组织:{}", OAConvertConstant.ROOT_COMPANY_NAME);
|
||||||
|
return rootCompany;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId,
|
||||||
|
DeptCategory deptCategory, List<Dept> addList, List<Dept> updateList) {
|
||||||
|
if (existing != null) {
|
||||||
|
Dept updateParam = new Dept();
|
||||||
|
updateParam.setId(existing.getId());
|
||||||
|
updateParam.setDeptName(name);
|
||||||
|
updateParam.setFullName(name);
|
||||||
|
updateParam.setShortName(name);
|
||||||
|
updateParam.setParentId(parent.getId());
|
||||||
|
updateParam.setParentCode(parent.getDeptCode());
|
||||||
|
updateParam.setAncestors(buildAncestors(parent));
|
||||||
|
updateParam.setIsOa(YES);
|
||||||
|
updateParam.setSyncTime(new Date());
|
||||||
|
updateList.add(updateParam);
|
||||||
|
existing.setDeptName(name);
|
||||||
|
existing.setFullName(name);
|
||||||
|
existing.setShortName(name);
|
||||||
|
existing.setParentId(parent.getId());
|
||||||
|
existing.setParentCode(parent.getDeptCode());
|
||||||
|
existing.setAncestors(updateParam.getAncestors());
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
Dept dept = this.buildOrgDept(name, oaCode, parent, tenantId, deptCategory);
|
||||||
|
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||||
|
addList.add(dept);
|
||||||
|
return dept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveOrgs(List<Dept> addList, List<Dept> updateList, DeptCategory deptCategory) {
|
||||||
|
if (CollectionUtil.isNotEmpty(addList)) {
|
||||||
|
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
|
||||||
|
deptService.saveBatch(addList);
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||||
|
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
||||||
|
deptService.updateBatchById(updateList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dept buildOrgDept(String name, String deptCode, Dept parent, String tenantId, DeptCategory deptCategory) {
|
||||||
|
Dept dept = new Dept();
|
||||||
|
dept.setTenantId(tenantId);
|
||||||
|
dept.setParentId(parent.getId());
|
||||||
|
dept.setParentCode(parent.getDeptCode());
|
||||||
|
dept.setAncestors(this.buildAncestors(parent));
|
||||||
|
dept.setDeptName(name);
|
||||||
|
dept.setFullName(name);
|
||||||
|
dept.setShortName(name);
|
||||||
|
dept.setDeptCode(deptCode);
|
||||||
|
dept.setBelongCompanyCode(DeptCategory.COMPANY.equals(deptCategory) ? deptCode : parent.getBelongCompanyCode());
|
||||||
|
dept.setDeptCategory(deptCategory.getCode());
|
||||||
|
dept.setSort(0);
|
||||||
|
dept.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||||
|
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||||
|
dept.setIsOa(YES);
|
||||||
|
dept.setIsPlatformCompany(0);
|
||||||
|
dept.setSyncTime(new Date());
|
||||||
|
return dept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDept buildUserDept(OAPersonResponse oaPerson, Map<String, Long> userMap, OaOrgIndex orgIndex) {
|
||||||
|
Dept department = orgIndex.findDept(oaPerson);
|
||||||
|
if (department == null || department.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
UserDept userDept = userConvert.person2userDept(oaPerson, userMap);
|
||||||
|
if (userDept.getUserId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Dept company = orgIndex.findCompany(oaPerson);
|
||||||
|
userDept.setDeptId(department.getId());
|
||||||
|
userDept.setDeptCode(department.getDeptCode());
|
||||||
|
userDept.setDeptName(department.getDeptName());
|
||||||
|
if (company != null) {
|
||||||
|
userDept.setCompanyCode(company.getDeptCode());
|
||||||
|
userDept.setCompanyName(company.getDeptName());
|
||||||
|
}
|
||||||
|
return userDept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDefaultRoleId() {
|
||||||
|
String defaultRole = ParamCache.getValue(DEFAULT_PARAM_ROLE);
|
||||||
|
if (defaultRole == null) {
|
||||||
|
defaultRole = DEFAULT_ROLE;
|
||||||
|
}
|
||||||
|
List<Role> roleList = roleService.list(Wrappers.<Role>lambdaQuery()
|
||||||
|
.eq(Role::getRoleAlias, defaultRole)
|
||||||
|
);
|
||||||
|
if (CollectionUtil.isEmpty(roleList)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return roleList.get(0).getId().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCompanyKey(OAPersonResponse oaPerson) {
|
||||||
|
if (oaPerson == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||||
|
return oaPerson.getSubcompanyid1().trim();
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||||
|
return "NAME:" + oaPerson.getSubcompanyname().trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveDepartmentKey(OAPersonResponse oaPerson) {
|
||||||
|
if (oaPerson == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||||
|
return oaPerson.getDepartmentid().trim();
|
||||||
|
}
|
||||||
|
String companyKey = resolveCompanyKey(oaPerson);
|
||||||
|
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) {
|
||||||
|
return companyKey + ":" + oaPerson.getDepartmentname().trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCompanyCode(OAPersonResponse oaPerson, String companyKey) {
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||||
|
return OAConvertConstant.COMPANY_OA_PREFIX + oaPerson.getSubcompanyid1().trim();
|
||||||
|
}
|
||||||
|
return OAConvertConstant.COMPANY_OA_PREFIX + "N" + Math.abs(companyKey.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildDepartmentCode(OAPersonResponse oaPerson, String departmentKey) {
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||||
|
return OAConvertConstant.DEPARTMENT_OA_PREFIX + oaPerson.getDepartmentid().trim();
|
||||||
|
}
|
||||||
|
return OAConvertConstant.DEPARTMENT_OA_PREFIX + "N" + Math.abs(departmentKey.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAncestors(Dept parent) {
|
||||||
|
String ancestors = parent.getAncestors();
|
||||||
|
if (StringUtils.isBlank(ancestors)) {
|
||||||
|
ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID);
|
||||||
|
}
|
||||||
|
return ancestors + "," + parent.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTenantId() {
|
||||||
|
String tenantId = AuthUtil.getTenantId();
|
||||||
|
if (StringUtils.isBlank(tenantId)) {
|
||||||
|
return BladeConstant.ADMIN_TENANT_ID;
|
||||||
|
}
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserDeptKey(UserDept userDept) {
|
||||||
|
if (userDept == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return userDept.getUserId() + userDept.getCompanyCode() + userDept.getDeptCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本批人员同步计数
|
||||||
|
*/
|
||||||
|
public static class PersonSyncCount {
|
||||||
|
private final int syncedCount;
|
||||||
|
private final int skippedCount;
|
||||||
|
|
||||||
|
public PersonSyncCount(int syncedCount, int skippedCount) {
|
||||||
|
this.syncedCount = syncedCount;
|
||||||
|
this.skippedCount = skippedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSyncedCount() {
|
||||||
|
return syncedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSkippedCount() {
|
||||||
|
return skippedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OA 组织索引
|
||||||
|
*/
|
||||||
|
public static class OaOrgIndex {
|
||||||
|
private final Map<String, Dept> companyByOaId = new HashMap<>();
|
||||||
|
private final Map<String, Dept> deptByOaId = new HashMap<>();
|
||||||
|
private final Map<String, Dept> deptByCompanyAndName = new HashMap<>();
|
||||||
|
|
||||||
|
private Dept findCompany(OAPersonResponse oaPerson) {
|
||||||
|
if (oaPerson == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||||
|
Dept company = companyByOaId.get(oaPerson.getSubcompanyid1().trim());
|
||||||
|
if (company != null) {
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||||
|
return companyByOaId.get("NAME:" + oaPerson.getSubcompanyname().trim());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dept findDept(OAPersonResponse oaPerson) {
|
||||||
|
if (oaPerson == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||||
|
Dept department = deptByOaId.get(oaPerson.getDepartmentid().trim());
|
||||||
|
if (department != null) {
|
||||||
|
return department;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String companyKey = StringUtils.isNotBlank(oaPerson.getSubcompanyid1())
|
||||||
|
? oaPerson.getSubcompanyid1().trim()
|
||||||
|
: (StringUtils.isNotBlank(oaPerson.getSubcompanyname()) ? "NAME:" + oaPerson.getSubcompanyname().trim() : null);
|
||||||
|
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) {
|
||||||
|
Dept department = deptByCompanyAndName.get(companyKey + "#" + oaPerson.getDepartmentname());
|
||||||
|
if (department != null) {
|
||||||
|
return department;
|
||||||
|
}
|
||||||
|
return deptByOaId.get(companyKey + ":" + oaPerson.getDepartmentname().trim());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.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;
|
||||||
|
import org.springblade.core.tool.api.R;
|
||||||
|
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.pojo.dto.PhoneChangeDTO;
|
||||||
|
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
|
||||||
|
import org.springblade.system.pojo.entity.User;
|
||||||
|
import org.springblade.system.service.IUserPhoneService;
|
||||||
|
import org.springblade.system.service.IUserService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户手机号变更服务实现
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPhoneServiceImpl implements IUserPhoneService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与登录短信一致,对应后台 /resource/sms 的 smsCode
|
||||||
|
*/
|
||||||
|
private static final String SMS_RESOURCE_CODE = "ali_reg";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原手机号已校验凭证(Redis)
|
||||||
|
*/
|
||||||
|
private static final String PHONE_CHANGE_VERIFIED_KEY = "blade: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 ISmsClient smsClient;
|
||||||
|
private final BladeRedis bladeRedis;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R sendCode(String phone) {
|
||||||
|
String normalizedPhone = normalizePhone(phone);
|
||||||
|
Long userId = AuthUtil.getUserId();
|
||||||
|
if (Func.isEmpty(userId)) {
|
||||||
|
throw new ServiceException("请先登录");
|
||||||
|
}
|
||||||
|
User currentUser = requireCurrentUser(userId);
|
||||||
|
String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId());
|
||||||
|
boolean isCurrentPhone = StringUtil.equals(normalizedPhone, Func.toStr(currentUser.getPhone()));
|
||||||
|
if (!isCurrentPhone) {
|
||||||
|
assertPhoneAvailable(tenantId, normalizedPhone, userId);
|
||||||
|
}
|
||||||
|
R result = smsClient.sendValidate(tenantId, SMS_RESOURCE_CODE, normalizedPhone);
|
||||||
|
if (result == null || !result.isSuccess()) {
|
||||||
|
return R.fail(SmsUtil.SEND_FAIL);
|
||||||
|
}
|
||||||
|
return R.data(result.getData(), SmsUtil.SEND_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyOldPhone(PhoneVerifyDTO phoneVerify) {
|
||||||
|
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);
|
||||||
|
String oldPhone = Func.toStr(currentUser.getPhone()).trim();
|
||||||
|
if (StringUtil.isBlank(oldPhone)) {
|
||||||
|
throw new ServiceException("当前账号未绑定手机号");
|
||||||
|
}
|
||||||
|
validateSms(currentUser.getTenantId(), id, code, oldPhone);
|
||||||
|
bladeRedis.setEx(PHONE_CHANGE_VERIFIED_KEY + userId, "1", PHONE_CHANGE_VERIFIED_TTL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean changePhone(PhoneChangeDTO phoneChange) {
|
||||||
|
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("请先完成原手机号验证");
|
||||||
|
}
|
||||||
|
String id = Func.toStr(phoneChange.getId()).trim();
|
||||||
|
String code = Func.toStr(phoneChange.getCode()).trim();
|
||||||
|
String newPhone = normalizePhone(phoneChange.getNewPhone());
|
||||||
|
if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) {
|
||||||
|
throw new ServiceException("请先获取并填写验证码");
|
||||||
|
}
|
||||||
|
User currentUser = requireCurrentUser(userId);
|
||||||
|
String oldPhone = Func.toStr(currentUser.getPhone()).trim();
|
||||||
|
if (StringUtil.equals(newPhone, oldPhone)) {
|
||||||
|
throw new ServiceException("新手机号不可与当前手机号相同");
|
||||||
|
}
|
||||||
|
String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId());
|
||||||
|
assertPhoneAvailable(tenantId, newPhone, userId);
|
||||||
|
validateSms(tenantId, id, code, newPhone);
|
||||||
|
|
||||||
|
User updateUser = new User();
|
||||||
|
updateUser.setId(userId);
|
||||||
|
updateUser.setPhone(newPhone);
|
||||||
|
// 账号若等于原手机号,同步更新,保证短信登录可用
|
||||||
|
if (StringUtil.isNotBlank(oldPhone) && StringUtil.equals(oldPhone, Func.toStr(currentUser.getAccount()))) {
|
||||||
|
assertAccountAvailable(tenantId, newPhone, userId);
|
||||||
|
updateUser.setAccount(newPhone);
|
||||||
|
}
|
||||||
|
boolean updated = userService.updateById(updateUser);
|
||||||
|
if (!updated) {
|
||||||
|
throw new ServiceException("手机号修改失败");
|
||||||
|
}
|
||||||
|
bladeRedis.del(PHONE_CHANGE_VERIFIED_KEY + userId);
|
||||||
|
CacheUtil.clear(USER_CACHE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private User requireCurrentUser(Long userId) {
|
||||||
|
User user = userService.getById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new ServiceException("用户不存在");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSms(String tenantId, String id, String value, String phone) {
|
||||||
|
R result = smsClient.validateMessage(tenantId, SMS_RESOURCE_CODE, id, value, phone);
|
||||||
|
if (result == null || !result.isSuccess()) {
|
||||||
|
throw new ServiceException(SmsUtil.VALIDATE_FAIL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPhoneAvailable(String tenantId, String phone, Long excludeUserId) {
|
||||||
|
Long phoneCount = userService.count(
|
||||||
|
Wrappers.<User>lambdaQuery()
|
||||||
|
.eq(User::getTenantId, tenantId)
|
||||||
|
.eq(User::getPhone, phone)
|
||||||
|
.ne(User::getId, excludeUserId)
|
||||||
|
);
|
||||||
|
if (phoneCount != null && phoneCount > 0L) {
|
||||||
|
throw new ServiceException(StringUtil.format("当前手机 [{}] 已存在!", phone));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertAccountAvailable(String tenantId, String account, Long excludeUserId) {
|
||||||
|
Long accountCount = userService.count(
|
||||||
|
Wrappers.<User>lambdaQuery()
|
||||||
|
.eq(User::getTenantId, tenantId)
|
||||||
|
.eq(User::getAccount, account)
|
||||||
|
.ne(User::getId, excludeUserId)
|
||||||
|
);
|
||||||
|
if (accountCount != null && accountCount > 0L) {
|
||||||
|
throw new ServiceException(StringUtil.format("当前用户 [{}] 已存在!", account));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizePhone(String phone) {
|
||||||
|
String normalizedPhone = Func.toStr(phone).trim();
|
||||||
|
if (!MOBILE_PATTERN.matcher(normalizedPhone).matches()) {
|
||||||
|
throw new ServiceException("手机号格式不正确");
|
||||||
|
}
|
||||||
|
return normalizedPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+40
@@ -529,6 +529,46 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) {
|
||||||
|
if (Func.isBlank(tenantId) || Func.isEmpty(userId) || Func.isBlank(openid)) {
|
||||||
|
throw new ServiceException("绑定微信 openid 参数不完整");
|
||||||
|
}
|
||||||
|
String source = "WECHAT_MINI";
|
||||||
|
UserOauth byOpenId = userOauthService.getOne(Wrappers.<UserOauth>lambdaQuery()
|
||||||
|
.eq(UserOauth::getTenantId, tenantId)
|
||||||
|
.eq(UserOauth::getSource, source)
|
||||||
|
.eq(UserOauth::getUuid, openid)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (byOpenId != null) {
|
||||||
|
byOpenId.setUserId(userId);
|
||||||
|
if (Func.isNotBlank(phone)) {
|
||||||
|
byOpenId.setUsername(phone);
|
||||||
|
}
|
||||||
|
return userOauthService.updateById(byOpenId);
|
||||||
|
}
|
||||||
|
UserOauth byUser = userOauthService.getOne(Wrappers.<UserOauth>lambdaQuery()
|
||||||
|
.eq(UserOauth::getTenantId, tenantId)
|
||||||
|
.eq(UserOauth::getSource, source)
|
||||||
|
.eq(UserOauth::getUserId, userId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (byUser != null) {
|
||||||
|
byUser.setUuid(openid);
|
||||||
|
if (Func.isNotBlank(phone)) {
|
||||||
|
byUser.setUsername(phone);
|
||||||
|
}
|
||||||
|
return userOauthService.updateById(byUser);
|
||||||
|
}
|
||||||
|
UserOauth oauth = new UserOauth();
|
||||||
|
oauth.setTenantId(tenantId);
|
||||||
|
oauth.setUserId(userId);
|
||||||
|
oauth.setUuid(openid);
|
||||||
|
oauth.setUsername(Func.toStr(phone, ""));
|
||||||
|
oauth.setSource(source);
|
||||||
|
return userOauthService.save(oauth);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public boolean grant(String userIds, String roleIds) {
|
public boolean grant(String userIds, String roleIds) {
|
||||||
|
|||||||
+4
-8
@@ -30,7 +30,6 @@ import lombok.AllArgsConstructor;
|
|||||||
import org.springblade.core.boot.ctrl.BladeController;
|
import org.springblade.core.boot.ctrl.BladeController;
|
||||||
import org.springblade.core.mp.support.Condition;
|
import org.springblade.core.mp.support.Condition;
|
||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.core.secure.annotation.PreAuth;
|
|
||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||||
@@ -48,7 +47,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
* <p>
|
* <p>
|
||||||
* 对外路径:{@code /api/blade-transport/exception-disposal/**}
|
* 对外路径:{@code /api/blade-transport/exception-disposal/**}
|
||||||
* 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。
|
* 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。
|
||||||
* 司机上报(submit)/ 列表 / 详情仅需登录态;跟进与完成保留菜单鉴权。
|
* 列表 / 详情 / 上报 / 跟进 / 完成均仅需登录态(小程序调度端与司机端共用)。
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -80,27 +79,24 @@ public class ExceptionDisposalController extends BladeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/follow")
|
@PostMapping("/follow")
|
||||||
@PreAuth(menu = "exception_disposal")
|
|
||||||
@ApiOperationSupport(order = 4)
|
@ApiOperationSupport(order = 4)
|
||||||
@Operation(summary = "异常跟进")
|
@Operation(summary = "异常跟进", description = "调度端跟进;仅需登录态")
|
||||||
public R follow(@RequestBody ExceptionDisposalFollowRequest request) {
|
public R follow(@RequestBody ExceptionDisposalFollowRequest request) {
|
||||||
exceptionDisposalService.follow(request);
|
exceptionDisposalService.follow(request);
|
||||||
return R.success("跟进成功");
|
return R.success("跟进成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/complete")
|
@PostMapping("/complete")
|
||||||
@PreAuth(menu = "exception_disposal")
|
|
||||||
@ApiOperationSupport(order = 5)
|
@ApiOperationSupport(order = 5)
|
||||||
@Operation(summary = "完成异常")
|
@Operation(summary = "完成异常", description = "调度端结案;仅需登录态")
|
||||||
public R complete(@RequestBody ExceptionDisposalFollowRequest request) {
|
public R complete(@RequestBody ExceptionDisposalFollowRequest request) {
|
||||||
exceptionDisposalService.complete(request.getId());
|
exceptionDisposalService.complete(request.getId());
|
||||||
return R.success("完成成功");
|
return R.success("完成成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/batch-complete")
|
@PostMapping("/batch-complete")
|
||||||
@PreAuth(menu = "exception_disposal")
|
|
||||||
@ApiOperationSupport(order = 6)
|
@ApiOperationSupport(order = 6)
|
||||||
@Operation(summary = "批量完成异常")
|
@Operation(summary = "批量完成异常", description = "调度端批量结案;仅需登录态")
|
||||||
public R batchComplete(@RequestParam String ids) {
|
public R batchComplete(@RequestParam String ids) {
|
||||||
exceptionDisposalService.batchComplete(ids);
|
exceptionDisposalService.batchComplete(ids);
|
||||||
return R.success("批量完成成功");
|
return R.success("批量完成成功");
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springblade.core.boot.ctrl.BladeController;
|
||||||
|
import org.springblade.core.tool.api.R;
|
||||||
|
import org.springblade.transport.pojo.entity.Waybill;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||||
|
import org.springblade.transport.service.IManageWaybillService;
|
||||||
|
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.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端运单(小程序管理端)
|
||||||
|
* <p>
|
||||||
|
* 对外完整路径:{@code /api/blade-transport/waybill/manage/**}
|
||||||
|
* (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /waybill/manage/**})。
|
||||||
|
* 同时兼容未去前缀直连({@code /blade-transport/waybill/manage/**})。
|
||||||
|
* 仅需登录态,不挂管理端菜单鉴权。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RequestMapping({"/waybill/manage", "/blade-transport/waybill/manage"})
|
||||||
|
@Tag(name = "调度端运单", description = "小程序调度端首页统计与运单列表")
|
||||||
|
public class ManageWaybillController extends BladeController {
|
||||||
|
|
||||||
|
private final IManageWaybillService manageWaybillService;
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
@ApiOperationSupport(order = 1)
|
||||||
|
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;租户内不过滤组织(小程序调度账号组织常与运单不一致);在途异常=异常处置状态≠已完成")
|
||||||
|
public R<AdminHomeStatsVO> stats() {
|
||||||
|
return R.data(manageWaybillService.stats());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/home")
|
||||||
|
@ApiOperationSupport(order = 2)
|
||||||
|
@Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名")
|
||||||
|
public R<AdminHomeVO> home() {
|
||||||
|
return R.data(manageWaybillService.home());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ApiOperationSupport(order = 3)
|
||||||
|
@Operation(summary = "运单分页列表", description = "当前组织运单;status:0待接单/1运输中/2已完成;exception:exception/normal;transportType:common/load")
|
||||||
|
public R<IPage<AdminWaybillCardVO>> list(
|
||||||
|
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||||
|
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
|
||||||
|
@Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword,
|
||||||
|
@Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
|
||||||
|
@Parameter(description = "异常:exception有异常/normal无异常") @RequestParam(required = false) String exception,
|
||||||
|
@Parameter(description = "运输组织:common普通/load配载") @RequestParam(required = false) String transportType,
|
||||||
|
@Parameter(description = "创建日起 YYYY-MM-DD") @RequestParam(required = false) String startDate,
|
||||||
|
@Parameter(description = "创建日止 YYYY-MM-DD") @RequestParam(required = false) String endDate) {
|
||||||
|
return R.data(manageWaybillService.pageList(
|
||||||
|
current, size, keyword, status, exception, transportType, startDate, endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/detail")
|
||||||
|
@ApiOperationSupport(order = 4)
|
||||||
|
@Operation(summary = "运单详情", description = "调度端查看运单详情(含 punchNodes / enrouteRecords),不校验司机归属与组织;字段对齐小程序 pages/waybill/detail")
|
||||||
|
public R<AdminWaybillDetailVO> detail(
|
||||||
|
@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
|
||||||
|
return R.data(manageWaybillService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pending")
|
||||||
|
@ApiOperationSupport(order = 5)
|
||||||
|
@Operation(summary = "待处理运单", description = "待接单/运输中;needReassign=true 仅司机已拒单")
|
||||||
|
public R<IPage<AdminWaybillCardVO>> pending(
|
||||||
|
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||||
|
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
|
||||||
|
@Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword,
|
||||||
|
@Parameter(description = "是否需重新派单") @RequestParam(required = false) Boolean needReassign) {
|
||||||
|
return R.data(manageWaybillService.pendingList(current, size, keyword, needReassign));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/reassign")
|
||||||
|
@ApiOperationSupport(order = 6)
|
||||||
|
@Operation(summary = "重新派单", description = "小程序调度端:跳过组织校验,仅需登录态;传入运单ID及新司机、手机号、车牌")
|
||||||
|
public R reassign(@RequestBody Waybill waybill) {
|
||||||
|
return R.status(manageWaybillService.reassign(
|
||||||
|
waybill.getId(),
|
||||||
|
waybill.getDriverId(),
|
||||||
|
waybill.getDriverName(),
|
||||||
|
waybill.getDriverPhone(),
|
||||||
|
waybill.getVehicleNo()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/driver-search")
|
||||||
|
@ApiOperationSupport(order = 7)
|
||||||
|
@Operation(summary = "搜索司机", description = "按姓名/手机号模糊搜索,供重新派单选用")
|
||||||
|
public R<List<AdminDriverOptionVO>> driverSearch(
|
||||||
|
@Parameter(description = "关键字") @RequestParam(required = false) String keyword) {
|
||||||
|
return R.data(manageWaybillService.searchDrivers(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/vehicle-search")
|
||||||
|
@ApiOperationSupport(order = 8)
|
||||||
|
@Operation(summary = "搜索车牌", description = "按车牌模糊搜索(来自司机绑定车牌)")
|
||||||
|
public R<List<AdminVehicleOptionVO>> vehicleSearch(
|
||||||
|
@Parameter(description = "关键字") @RequestParam(required = false) String keyword) {
|
||||||
|
return R.data(manageWaybillService.searchVehicles(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+6
@@ -68,6 +68,12 @@ public interface IDriverWaybillService {
|
|||||||
*/
|
*/
|
||||||
DriverWaybillCardVO detail(Long id);
|
DriverWaybillCardVO detail(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。
|
||||||
|
* 供调度端 manage/detail 复用。
|
||||||
|
*/
|
||||||
|
DriverWaybillCardVO detailPunchSnapshot(Long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。
|
* 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。
|
||||||
*/
|
*/
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端(小程序管理端)运单首页服务
|
||||||
|
*/
|
||||||
|
public interface IManageWaybillService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运单状态统计:运输中 / 待接单 / 在途异常 / 已完成
|
||||||
|
*/
|
||||||
|
AdminHomeStatsVO stats();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页聚合:统计 + 角标 + 待处理事项(异常处置≠已完成)+ 用户名
|
||||||
|
*/
|
||||||
|
AdminHomeVO home();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端运单分页列表
|
||||||
|
*
|
||||||
|
* @param current 页码
|
||||||
|
* @param size 每页条数
|
||||||
|
* @param keyword 运单号/司机/车牌
|
||||||
|
* @param status 0待接单/1运输中/2已完成,空=全部
|
||||||
|
* @param exception exception有异常 / normal无异常 / 空=全部
|
||||||
|
* @param transportType common普通 / load配载 / 空=全部
|
||||||
|
* @param startDate 创建日起 YYYY-MM-DD
|
||||||
|
* @param endDate 创建日止 YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
IPage<AdminWaybillCardVO> pageList(Integer current, Integer size, String keyword, String status,
|
||||||
|
String exception, String transportType, String startDate, String endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端运单详情(不校验司机归属)
|
||||||
|
*/
|
||||||
|
AdminWaybillDetailVO detail(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待处理运单(待接单 / 运输中;可筛需重新派单)
|
||||||
|
*/
|
||||||
|
IPage<AdminWaybillCardVO> pendingList(Integer current, Integer size, String keyword, Boolean needReassign);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新派单:跳过管理端部门校验,仅需登录态(司机、手机号、车牌)
|
||||||
|
*/
|
||||||
|
boolean reassign(Long id, Long driverId, String driverName, String driverPhone, String vehicleNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索司机(姓名/手机号)
|
||||||
|
*/
|
||||||
|
List<AdminDriverOptionVO> searchDrivers(String keyword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索车牌(来自司机绑定车牌)
|
||||||
|
*/
|
||||||
|
List<AdminVehicleOptionVO> searchVehicles(String keyword);
|
||||||
|
|
||||||
|
}
|
||||||
+6
@@ -60,6 +60,12 @@ public interface IWaybillService extends BaseService<Waybill> {
|
|||||||
boolean maintainMileage(WaybillMileageRequest request);
|
boolean maintainMileage(WaybillMileageRequest request);
|
||||||
boolean cancel(Long id);
|
boolean cancel(Long id);
|
||||||
boolean reassign(Waybill waybill);
|
boolean reassign(Waybill waybill);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序调度端重新派单:跳过管理端部门校验,其余逻辑与 {@link #reassign(Waybill)} 一致。
|
||||||
|
*/
|
||||||
|
boolean reassignWithoutDeptCheck(Waybill waybill);
|
||||||
|
|
||||||
boolean complete(Long id);
|
boolean complete(Long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+72
-2
@@ -225,6 +225,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
return toCard(normalizeAcceptStatus(waybill), true);
|
return toCard(normalizeAcceptStatus(waybill), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DriverWaybillCardVO detailPunchSnapshot(Long id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new ServiceException("运单ID不能为空");
|
||||||
|
}
|
||||||
|
Waybill waybill = waybillService.getById(id);
|
||||||
|
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||||
|
throw new ServiceException("运单不存在");
|
||||||
|
}
|
||||||
|
return toCard(normalizeAcceptStatus(waybill), true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public DriverEnrouteRecordVO submitEnroute(EnrouteSubmitDTO dto) {
|
public DriverEnrouteRecordVO submitEnroute(EnrouteSubmitDTO dto) {
|
||||||
@@ -610,6 +622,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
card.setAcceptStatus(waybill.getDriverAcceptStatus());
|
card.setAcceptStatus(waybill.getDriverAcceptStatus());
|
||||||
card.setRejectReason(waybill.getDriverRejectReason());
|
card.setRejectReason(waybill.getDriverRejectReason());
|
||||||
|
|
||||||
|
// 详情页字段(列表也可带上,体积很小)
|
||||||
|
String cargoName = Func.toStr(waybill.getCargoName(), "");
|
||||||
|
String weightText = card.getWeight();
|
||||||
|
card.setCargoName(cargoName);
|
||||||
|
card.setPickupAddress(card.getFromAddress());
|
||||||
|
card.setUnloadAddress(card.getToAddress());
|
||||||
|
card.setCargoQuantity(weightText);
|
||||||
|
card.setTotalWeight(weightText);
|
||||||
|
card.setTransportType(toTransportTypeLabel(waybill.getTransportType()));
|
||||||
|
card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime()));
|
||||||
|
card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime()));
|
||||||
|
card.setRemark(Func.toStr(waybill.getRemark(), ""));
|
||||||
|
|
||||||
if (withEnrouteRecords) {
|
if (withEnrouteRecords) {
|
||||||
Date lastPunchAt = findLastPunchTime(waybill.getId());
|
Date lastPunchAt = findLastPunchTime(waybill.getId());
|
||||||
WaybillProcessSupport.TransitCheckinDecision transit = WaybillProcessSupport.evaluateTransitCheckin(
|
WaybillProcessSupport.TransitCheckinDecision transit = WaybillProcessSupport.evaluateTransitCheckin(
|
||||||
@@ -623,6 +648,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
card.setTransitTimeEnd(transit.timeEnd());
|
card.setTransitTimeEnd(transit.timeEnd());
|
||||||
card.setEnrouteRecords(listEnrouteRecords(waybill.getId()));
|
card.setEnrouteRecords(listEnrouteRecords(waybill.getId()));
|
||||||
card.setPunchNodes(buildPunchNodes(waybill, transit, processJson));
|
card.setPunchNodes(buildPunchNodes(waybill, transit, processJson));
|
||||||
|
card.setRoutePoints(buildSimpleRoutePoints(waybill));
|
||||||
|
card.setProcessJson(processJson);
|
||||||
} else {
|
} else {
|
||||||
// 列表/首页:只解析过程配置是否启用在途打卡,不做频次/时段与落库查询
|
// 列表/首页:只解析过程配置是否启用在途打卡,不做频次/时段与落库查询
|
||||||
boolean punchEnabled = WaybillProcessSupport.isTransitPunchEnabled(processJson);
|
boolean punchEnabled = WaybillProcessSupport.isTransitPunchEnabled(processJson);
|
||||||
@@ -634,18 +661,61 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<DriverWaybillCardVO.DriverRoutePointVO> buildSimpleRoutePoints(Waybill waybill) {
|
||||||
|
DriverWaybillCardVO.DriverRoutePointVO load = new DriverWaybillCardVO.DriverRoutePointVO();
|
||||||
|
load.setName(Func.toStr(waybill.getDepartureName(), "装货点"));
|
||||||
|
load.setAddress(Func.toStr(waybill.getDepartureAddress(), load.getName()));
|
||||||
|
load.setStatus("pending");
|
||||||
|
DriverWaybillCardVO.DriverRoutePointVO unload = new DriverWaybillCardVO.DriverRoutePointVO();
|
||||||
|
unload.setName(Func.toStr(waybill.getArrivalName(), "卸货点"));
|
||||||
|
unload.setAddress(Func.toStr(waybill.getArrivalAddress(), unload.getName()));
|
||||||
|
unload.setStatus("pending");
|
||||||
|
return List.of(load, unload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toTransportTypeLabel(String transportType) {
|
||||||
|
if (Func.isBlank(transportType)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String t = transportType.trim().toLowerCase();
|
||||||
|
return switch (t) {
|
||||||
|
case "road", "gl" -> "公路运输";
|
||||||
|
case "railway", "rail" -> "铁路运输";
|
||||||
|
case "river", "water", "waterway" -> "水路运输";
|
||||||
|
case "air", "aviation" -> "航空运输";
|
||||||
|
default -> transportType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatLocalDateYmd(LocalDate date) {
|
||||||
|
if (date == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态获取项目启用中的过程配置节点 JSON;无则回退运单快照 processJson。
|
* 优先用项目启用中的过程配置;若动态配置无打卡节点,回退运单快照 processJson,
|
||||||
|
* 避免项目配置改坏后司机端打卡页空白。
|
||||||
*/
|
*/
|
||||||
private String resolveProcessJson(Waybill waybill) {
|
private String resolveProcessJson(Waybill waybill) {
|
||||||
if (waybill == null) {
|
if (waybill == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
String snapshot = waybill.getProcessJson();
|
||||||
String live = loadLiveProcessConfigJson(waybill.getProjectId());
|
String live = loadLiveProcessConfigJson(waybill.getProjectId());
|
||||||
if (Func.isNotEmpty(live)) {
|
if (Func.isNotEmpty(live)) {
|
||||||
|
if (!WaybillProcessSupport.listDriverPunchNodes(live).isEmpty()) {
|
||||||
return live;
|
return live;
|
||||||
}
|
}
|
||||||
return waybill.getProcessJson();
|
// 动态配置存在但无可打卡节点:仍回退快照
|
||||||
|
if (Func.isNotEmpty(snapshot)
|
||||||
|
&& !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
return live;
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String loadLiveProcessConfigJson(Long projectId) {
|
private String loadLiveProcessConfigJson(Long projectId) {
|
||||||
|
|||||||
+642
@@ -0,0 +1,642 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
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.transport.pojo.entity.Driver;
|
||||||
|
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||||
|
import org.springblade.transport.pojo.entity.RiskDisposal;
|
||||||
|
import org.springblade.transport.pojo.entity.Waybill;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeBadgesVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminTodoItemVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||||
|
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||||
|
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
|
||||||
|
import org.springblade.transport.service.IDriverService;
|
||||||
|
import org.springblade.transport.service.IDriverWaybillService;
|
||||||
|
import org.springblade.transport.service.IExceptionDisposalService;
|
||||||
|
import org.springblade.transport.service.IManageWaybillService;
|
||||||
|
import org.springblade.transport.service.IRiskDisposalService;
|
||||||
|
import org.springblade.transport.service.IWaybillService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度端:运单状态统计 + 异常/风险角标 + 待处理事项 + 运单列表
|
||||||
|
* <p>
|
||||||
|
* 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 dept 过滤,
|
||||||
|
* 仅依赖租户隔离,口径接近后台 {@code /waybill-manage/list?allDept=1}。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||||
|
|
||||||
|
private static final String STATUS_PENDING = "pending";
|
||||||
|
private static final String STATUS_RUNNING = "running";
|
||||||
|
private static final String STATUS_COMPLETED = "completed";
|
||||||
|
private static final String STATUS_CANCELLED = "cancelled";
|
||||||
|
private static final String ACCEPT_REJECTED = "rejected";
|
||||||
|
|
||||||
|
private static final String DISPOSAL_PENDING = "pending";
|
||||||
|
private static final String DISPOSAL_PROCESSING = "processing";
|
||||||
|
private static final String RISK_PENDING = "pending";
|
||||||
|
|
||||||
|
private static final String EXCEPTION_YES = "exception";
|
||||||
|
private static final String EXCEPTION_NO = "normal";
|
||||||
|
private static final String TRANSPORT_COMMON = "common";
|
||||||
|
private static final String TRANSPORT_LOAD = "load";
|
||||||
|
|
||||||
|
private static final int FEED_LIMIT = 20;
|
||||||
|
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||||
|
private static final int MAX_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_MD = DateTimeFormatter.ofPattern("MM-dd");
|
||||||
|
private static final DateTimeFormatter DATE_YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private final IWaybillService waybillService;
|
||||||
|
private final IDriverService driverService;
|
||||||
|
private final IExceptionDisposalService exceptionDisposalService;
|
||||||
|
private final IRiskDisposalService riskDisposalService;
|
||||||
|
private final IDriverWaybillService driverWaybillService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdminHomeStatsVO stats() {
|
||||||
|
AdminHomeStatsVO vo = new AdminHomeStatsVO();
|
||||||
|
vo.setPendingAccept(countWaybillByStatus(STATUS_PENDING));
|
||||||
|
vo.setTransporting(countWaybillByStatus(STATUS_RUNNING));
|
||||||
|
vo.setCompleted(countWaybillByStatus(STATUS_COMPLETED));
|
||||||
|
vo.setException(countIncompleteExceptions());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdminHomeVO home() {
|
||||||
|
AdminHomeVO home = new AdminHomeVO();
|
||||||
|
home.setUserName(resolveUserName());
|
||||||
|
home.setStats(stats());
|
||||||
|
|
||||||
|
AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||||
|
badges.setException(home.getStats().getException());
|
||||||
|
badges.setRisk(countPendingRisks());
|
||||||
|
home.setBadges(badges);
|
||||||
|
home.setFeed(buildExceptionFeed());
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<AdminWaybillCardVO> pageList(Integer current, Integer size, String keyword, String status,
|
||||||
|
String exception, String transportType, String startDate, String endDate) {
|
||||||
|
int pageNo = current == null || current < 1 ? 1 : current;
|
||||||
|
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
|
||||||
|
|
||||||
|
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
|
||||||
|
if (EXCEPTION_YES.equals(exception) && exceptionWaybillIds.isEmpty()) {
|
||||||
|
Page<AdminWaybillCardVO> emptyVo = new Page<>(pageNo, pageSize, 0);
|
||||||
|
emptyVo.setRecords(List.of());
|
||||||
|
return emptyVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<Waybill> wrapper = scopedWaybillQuery();
|
||||||
|
applyStatusFilter(wrapper, status);
|
||||||
|
applyExceptionFilter(wrapper, exception, exceptionWaybillIds);
|
||||||
|
applyTransportTypeFilter(wrapper, transportType);
|
||||||
|
applyKeywordFilter(wrapper, keyword);
|
||||||
|
applyCreateTimeFilter(wrapper, startDate, endDate);
|
||||||
|
wrapper.orderByDesc(Waybill::getCreateTime);
|
||||||
|
|
||||||
|
IPage<Waybill> entityPage = waybillService.page(new Page<>(pageNo, pageSize), wrapper);
|
||||||
|
List<Long> pageIds = entityPage.getRecords().stream()
|
||||||
|
.map(Waybill::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
Set<Long> pageExceptionIds = pageIds.isEmpty()
|
||||||
|
? Collections.emptySet()
|
||||||
|
: exceptionWaybillIds.stream().filter(pageIds::contains).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Page<AdminWaybillCardVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||||
|
voPage.setRecords(entityPage.getRecords().stream()
|
||||||
|
.map(w -> toCard(w, pageExceptionIds.contains(w.getId())))
|
||||||
|
.toList());
|
||||||
|
return voPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdminWaybillDetailVO detail(Long id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new org.springblade.core.log.exception.ServiceException("运单ID不能为空");
|
||||||
|
}
|
||||||
|
Waybill waybill = waybillService.getById(id);
|
||||||
|
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||||
|
throw new org.springblade.core.log.exception.ServiceException("运单不存在");
|
||||||
|
}
|
||||||
|
boolean hasException = false;
|
||||||
|
Long exceptionId = null;
|
||||||
|
ExceptionDisposal latest = exceptionDisposalService.getOne(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||||
|
.eq(ExceptionDisposal::getWaybillId, id)
|
||||||
|
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||||
|
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||||
|
.orderByDesc(ExceptionDisposal::getCreateTime)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (latest != null) {
|
||||||
|
hasException = true;
|
||||||
|
exceptionId = latest.getId();
|
||||||
|
}
|
||||||
|
return toDetail(waybill, hasException, exceptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<AdminWaybillCardVO> pendingList(Integer current, Integer size, String keyword, Boolean needReassign) {
|
||||||
|
int pageNo = current == null || current < 1 ? 1 : current;
|
||||||
|
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<Waybill> wrapper = scopedWaybillQuery()
|
||||||
|
.in(Waybill::getBusinessStatus, STATUS_PENDING, STATUS_RUNNING);
|
||||||
|
if (Boolean.TRUE.equals(needReassign)) {
|
||||||
|
wrapper.eq(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED);
|
||||||
|
} else if (Boolean.FALSE.equals(needReassign)) {
|
||||||
|
wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus)
|
||||||
|
.or().ne(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED));
|
||||||
|
}
|
||||||
|
applyKeywordFilter(wrapper, keyword);
|
||||||
|
wrapper.orderByDesc(Waybill::getUpdateTime).orderByDesc(Waybill::getCreateTime);
|
||||||
|
|
||||||
|
IPage<Waybill> entityPage = waybillService.page(new Page<>(pageNo, pageSize), wrapper);
|
||||||
|
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
|
||||||
|
List<Long> pageIds = entityPage.getRecords().stream()
|
||||||
|
.map(Waybill::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
Set<Long> pageExceptionIds = pageIds.isEmpty()
|
||||||
|
? Collections.emptySet()
|
||||||
|
: exceptionWaybillIds.stream().filter(pageIds::contains).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Page<AdminWaybillCardVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||||
|
voPage.setRecords(entityPage.getRecords().stream()
|
||||||
|
.map(w -> toCard(w, pageExceptionIds.contains(w.getId())))
|
||||||
|
.toList());
|
||||||
|
return voPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminWaybillDetailVO toDetail(Waybill waybill, boolean hasException, Long exceptionId) {
|
||||||
|
AdminWaybillDetailVO detail = new AdminWaybillDetailVO();
|
||||||
|
detail.setId(waybill.getId());
|
||||||
|
detail.setWaybillNo(waybill.getWaybillNo());
|
||||||
|
detail.setStatus(toAppStatus(waybill.getBusinessStatus()));
|
||||||
|
|
||||||
|
String mode = Func.toStr(waybill.getTransportType(), "");
|
||||||
|
detail.setTransportMode(mode);
|
||||||
|
detail.setTransportType(toTransportTypeLabel(mode));
|
||||||
|
detail.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON);
|
||||||
|
|
||||||
|
detail.setFromName(formatPlaceName(waybill.getDepartureName(), mode));
|
||||||
|
detail.setToName(formatPlaceName(waybill.getArrivalName(), mode));
|
||||||
|
String fromAddr = Func.toStr(waybill.getDepartureAddress(), Func.toStr(waybill.getDepartureName(), ""));
|
||||||
|
String toAddr = Func.toStr(waybill.getArrivalAddress(), Func.toStr(waybill.getArrivalName(), ""));
|
||||||
|
detail.setFromAddress(fromAddr);
|
||||||
|
detail.setToAddress(toAddr);
|
||||||
|
detail.setPickupAddress(fromAddr);
|
||||||
|
detail.setUnloadAddress(toAddr);
|
||||||
|
|
||||||
|
String cargo = Func.toStr(waybill.getCargoName(), "");
|
||||||
|
String weight = formatWeight(waybill.getQuantity(), waybill.getQuantityUnit());
|
||||||
|
detail.setCargoName(cargo);
|
||||||
|
detail.setCargoQuantity(weight);
|
||||||
|
detail.setWeight(weight);
|
||||||
|
detail.setTotalWeight(weight);
|
||||||
|
|
||||||
|
detail.setPlanShipTime(formatLocalDate(waybill.getEstimatedStartTime()));
|
||||||
|
detail.setPlanFinishTime(formatLocalDate(waybill.getEstimatedEndTime()));
|
||||||
|
detail.setCarrierName(Func.toStr(waybill.getCarrierName(), ""));
|
||||||
|
detail.setDriverName(Func.toStr(waybill.getDriverName(), ""));
|
||||||
|
detail.setDriverPhone(Func.toStr(waybill.getDriverPhone(), ""));
|
||||||
|
detail.setVehicleNo(Func.toStr(waybill.getVehicleNo(), ""));
|
||||||
|
detail.setRemark(Func.toStr(waybill.getRemark(), ""));
|
||||||
|
detail.setHasException(hasException);
|
||||||
|
detail.setExceptionId(exceptionId);
|
||||||
|
detail.setNeedReassign(ACCEPT_REJECTED.equals(waybill.getDriverAcceptStatus()));
|
||||||
|
detail.setAcceptStatus(Func.toStr(waybill.getDriverAcceptStatus(), ""));
|
||||||
|
detail.setRejectReason(Func.toStr(waybill.getDriverRejectReason(), ""));
|
||||||
|
detail.setDriverId(waybill.getDriverId());
|
||||||
|
|
||||||
|
AdminWaybillDetailVO.AdminRoutePointVO load = new AdminWaybillDetailVO.AdminRoutePointVO();
|
||||||
|
load.setName(Func.toStr(waybill.getDepartureName(), "装货点"));
|
||||||
|
load.setAddress(fromAddr);
|
||||||
|
load.setStatus("pending");
|
||||||
|
AdminWaybillDetailVO.AdminRoutePointVO unload = new AdminWaybillDetailVO.AdminRoutePointVO();
|
||||||
|
unload.setName(Func.toStr(waybill.getArrivalName(), "卸货点"));
|
||||||
|
unload.setAddress(toAddr);
|
||||||
|
unload.setStatus("pending");
|
||||||
|
detail.setRoutePoints(List.of(load, unload));
|
||||||
|
|
||||||
|
// 复用司机端打卡组装:过程节点 + 途打卡记录(调度端只读展示)
|
||||||
|
DriverWaybillCardVO punch = driverWaybillService.detailPunchSnapshot(waybill.getId());
|
||||||
|
if (punch != null) {
|
||||||
|
detail.setPunchNodes(punch.getPunchNodes());
|
||||||
|
detail.setEnrouteRecords(punch.getEnrouteRecords());
|
||||||
|
if (punch.getRoutePoints() != null && !punch.getRoutePoints().isEmpty()) {
|
||||||
|
detail.setRoutePoints(punch.getRoutePoints().stream().map(p -> {
|
||||||
|
AdminWaybillDetailVO.AdminRoutePointVO rp = new AdminWaybillDetailVO.AdminRoutePointVO();
|
||||||
|
rp.setName(p.getName());
|
||||||
|
rp.setAddress(p.getAddress());
|
||||||
|
rp.setStatus(p.getStatus());
|
||||||
|
return rp;
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean reassign(Long id, Long driverId, String driverName, String driverPhone, String vehicleNo) {
|
||||||
|
Waybill request = new Waybill();
|
||||||
|
request.setId(id);
|
||||||
|
request.setDriverId(driverId);
|
||||||
|
request.setDriverName(driverName);
|
||||||
|
request.setDriverPhone(driverPhone);
|
||||||
|
request.setVehicleNo(vehicleNo);
|
||||||
|
return waybillService.reassignWithoutDeptCheck(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AdminDriverOptionVO> searchDrivers(String keyword) {
|
||||||
|
String key = Func.toStr(keyword, "").trim();
|
||||||
|
LambdaQueryWrapper<Driver> wrapper = Wrappers.<Driver>lambdaQuery()
|
||||||
|
.eq(Driver::getIsDeleted, 0)
|
||||||
|
.orderByDesc(Driver::getUpdateTime)
|
||||||
|
.last("LIMIT 20");
|
||||||
|
if (Func.isNotBlank(key)) {
|
||||||
|
wrapper.and(w -> w.like(Driver::getDriverName, key).or().like(Driver::getMobile, key));
|
||||||
|
}
|
||||||
|
return driverService.list(wrapper).stream().map(d -> {
|
||||||
|
AdminDriverOptionVO vo = new AdminDriverOptionVO();
|
||||||
|
vo.setId(d.getId());
|
||||||
|
vo.setName(Func.toStr(d.getDriverName(), ""));
|
||||||
|
vo.setPhone(Func.toStr(d.getMobile(), ""));
|
||||||
|
vo.setVehicleNo(Func.toStr(d.getDrivingVehicle(), ""));
|
||||||
|
return vo;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AdminVehicleOptionVO> searchVehicles(String keyword) {
|
||||||
|
String key = Func.toStr(keyword, "").trim();
|
||||||
|
LambdaQueryWrapper<Driver> wrapper = Wrappers.<Driver>lambdaQuery()
|
||||||
|
.eq(Driver::getIsDeleted, 0)
|
||||||
|
.isNotNull(Driver::getDrivingVehicle)
|
||||||
|
.ne(Driver::getDrivingVehicle, "")
|
||||||
|
.orderByDesc(Driver::getUpdateTime)
|
||||||
|
.last("LIMIT 30");
|
||||||
|
if (Func.isNotBlank(key)) {
|
||||||
|
wrapper.like(Driver::getDrivingVehicle, key);
|
||||||
|
}
|
||||||
|
java.util.LinkedHashMap<String, AdminVehicleOptionVO> map = new java.util.LinkedHashMap<>();
|
||||||
|
for (Driver d : driverService.list(wrapper)) {
|
||||||
|
String plate = Func.toStr(d.getDrivingVehicle(), "").trim();
|
||||||
|
if (Func.isBlank(plate) || map.containsKey(plate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AdminVehicleOptionVO vo = new AdminVehicleOptionVO();
|
||||||
|
vo.setVehicleNo(plate);
|
||||||
|
vo.setDriverName(Func.toStr(d.getDriverName(), ""));
|
||||||
|
map.put(plate, vo);
|
||||||
|
}
|
||||||
|
return new java.util.ArrayList<>(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 运输方式字典值 → 展示文案 */
|
||||||
|
private String toTransportTypeLabel(String transportType) {
|
||||||
|
if (Func.isBlank(transportType)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String t = transportType.trim().toLowerCase();
|
||||||
|
return switch (t) {
|
||||||
|
case "road", "gl" -> "公路运输";
|
||||||
|
case "railway", "rail" -> "铁路运输";
|
||||||
|
case "river", "water", "waterway" -> "水路运输";
|
||||||
|
case "air", "aviation" -> "航空运输";
|
||||||
|
default -> transportType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private long countWaybillByStatus(String status) {
|
||||||
|
return waybillService.count(Wrappers.<Waybill>lambdaQuery()
|
||||||
|
.eq(Waybill::getBusinessStatus, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序调度端不做组织过滤。
|
||||||
|
* 「小程序管理」等账号 JWT/档案 dept 常与运单业务组织不一致,按 dept 过滤会导致统计全 0;
|
||||||
|
* 与后台 allDept=1 一致,仅依赖租户隔离(MyBatis-Plus TenantLine)。
|
||||||
|
*/
|
||||||
|
private LambdaQueryWrapper<Waybill> scopedWaybillQuery() {
|
||||||
|
return Wrappers.<Waybill>lambdaQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyStatusFilter(LambdaQueryWrapper<Waybill> wrapper, String status) {
|
||||||
|
String businessStatus = toBusinessStatus(status);
|
||||||
|
if (Func.isNotBlank(businessStatus)) {
|
||||||
|
wrapper.eq(Waybill::getBusinessStatus, businessStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyExceptionFilter(LambdaQueryWrapper<Waybill> wrapper, String exception, Set<Long> exceptionWaybillIds) {
|
||||||
|
if (EXCEPTION_YES.equals(exception)) {
|
||||||
|
wrapper.in(Waybill::getId, exceptionWaybillIds);
|
||||||
|
} else if (EXCEPTION_NO.equals(exception) && !exceptionWaybillIds.isEmpty()) {
|
||||||
|
wrapper.notIn(Waybill::getId, exceptionWaybillIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyTransportTypeFilter(LambdaQueryWrapper<Waybill> wrapper, String transportType) {
|
||||||
|
if (TRANSPORT_LOAD.equals(transportType)) {
|
||||||
|
wrapper.isNotNull(Waybill::getLoadingNo).ne(Waybill::getLoadingNo, "");
|
||||||
|
} else if (TRANSPORT_COMMON.equals(transportType)) {
|
||||||
|
wrapper.and(w -> w.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyKeywordFilter(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
|
||||||
|
if (Func.isBlank(keyword)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = keyword.trim();
|
||||||
|
wrapper.and(w -> w.like(Waybill::getWaybillNo, key)
|
||||||
|
.or().like(Waybill::getDriverName, key)
|
||||||
|
.or().like(Waybill::getVehicleNo, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCreateTimeFilter(LambdaQueryWrapper<Waybill> wrapper, String startDate, String endDate) {
|
||||||
|
if (Func.isNotBlank(startDate)) {
|
||||||
|
Date start = DateUtil.parse(startDate.trim() + " 00:00:00", DateUtil.PATTERN_DATETIME);
|
||||||
|
if (start != null) {
|
||||||
|
wrapper.ge(Waybill::getCreateTime, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Func.isNotBlank(endDate)) {
|
||||||
|
Date end = DateUtil.parse(endDate.trim() + " 23:59:59", DateUtil.PATTERN_DATETIME);
|
||||||
|
if (end != null) {
|
||||||
|
wrapper.le(Waybill::getCreateTime, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小程序 status → 后端 businessStatus */
|
||||||
|
private String toBusinessStatus(String status) {
|
||||||
|
if (Func.isBlank(status)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch (status.trim()) {
|
||||||
|
case "0", STATUS_PENDING -> STATUS_PENDING;
|
||||||
|
case "1", STATUS_RUNNING, "transporting", "doing" -> STATUS_RUNNING;
|
||||||
|
case "2", STATUS_COMPLETED, "done" -> STATUS_COMPLETED;
|
||||||
|
case "3", STATUS_CANCELLED -> STATUS_CANCELLED;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Long> loadIncompleteExceptionWaybillIds() {
|
||||||
|
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||||
|
.select(ExceptionDisposal::getWaybillId)
|
||||||
|
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||||
|
.isNotNull(ExceptionDisposal::getWaybillId));
|
||||||
|
Set<Long> ids = new HashSet<>();
|
||||||
|
for (ExceptionDisposal item : list) {
|
||||||
|
if (item.getWaybillId() != null) {
|
||||||
|
ids.add(item.getWaybillId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) {
|
||||||
|
AdminWaybillCardVO card = new AdminWaybillCardVO();
|
||||||
|
card.setId(waybill.getId());
|
||||||
|
card.setWaybillNo(waybill.getWaybillNo());
|
||||||
|
String mode = Func.toStr(waybill.getTransportType(), "");
|
||||||
|
card.setTransportMode(mode);
|
||||||
|
card.setFromName(formatPlaceName(waybill.getDepartureName(), mode));
|
||||||
|
card.setToName(formatPlaceName(waybill.getArrivalName(), mode));
|
||||||
|
card.setCargo(Func.toStr(waybill.getCargoName(), ""));
|
||||||
|
card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()));
|
||||||
|
card.setPlanTime(formatLocalDate(waybill.getEstimatedStartTime()));
|
||||||
|
card.setPlanTimeEnd(formatLocalDate(waybill.getEstimatedEndTime()));
|
||||||
|
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
|
||||||
|
card.setCarrierName(Func.toStr(waybill.getCarrierName(), ""));
|
||||||
|
card.setDriverName(Func.toStr(waybill.getDriverName(), ""));
|
||||||
|
card.setVehicleNo(Func.toStr(waybill.getVehicleNo(), ""));
|
||||||
|
card.setHasException(hasException);
|
||||||
|
card.setTransportType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON);
|
||||||
|
card.setCreateTime(formatDateTime(waybill.getCreateTime()));
|
||||||
|
card.setNeedReassign(ACCEPT_REJECTED.equals(waybill.getDriverAcceptStatus()));
|
||||||
|
card.setBuyerPaid(false);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公路运输:起/终仅展示市县(去掉省/自治区);其它运输方式原样返回。
|
||||||
|
*/
|
||||||
|
private String formatPlaceName(String name, String transportType) {
|
||||||
|
String raw = Func.toStr(name, "").trim();
|
||||||
|
if (Func.isBlank(raw) || !isRoadTransport(transportType)) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return toCityCounty(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRoadTransport(String transportType) {
|
||||||
|
if (Func.isBlank(transportType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String t = transportType.trim().toLowerCase();
|
||||||
|
return t.contains("road") || transportType.contains("公路") || transportType.contains("道路") || "gl".equals(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 去掉省级前缀,保留「市 + 区/县/旗」 */
|
||||||
|
private String toCityCounty(String name) {
|
||||||
|
String s = name.replaceFirst("^.+?(省|自治区|特别行政区)", "");
|
||||||
|
if (Func.isBlank(s)) {
|
||||||
|
s = name;
|
||||||
|
}
|
||||||
|
java.util.regex.Matcher city = java.util.regex.Pattern
|
||||||
|
.compile("^(.+?市)(.+?(?:区|县|旗|市))?")
|
||||||
|
.matcher(s);
|
||||||
|
if (city.find()) {
|
||||||
|
return Func.toStr(city.group(1), "") + Func.toStr(city.group(2), "");
|
||||||
|
}
|
||||||
|
java.util.regex.Matcher prefecture = java.util.regex.Pattern
|
||||||
|
.compile("^(.+?(?:州|盟|地区))(.+?(?:区|县|旗|市))?")
|
||||||
|
.matcher(s);
|
||||||
|
if (prefecture.find()) {
|
||||||
|
return Func.toStr(prefecture.group(1), "") + Func.toStr(prefecture.group(2), "");
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer toAppStatus(String businessStatus) {
|
||||||
|
if (Func.isBlank(businessStatus)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch (businessStatus) {
|
||||||
|
case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0;
|
||||||
|
case STATUS_RUNNING -> 1;
|
||||||
|
case STATUS_COMPLETED -> 2;
|
||||||
|
case STATUS_CANCELLED -> 3;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatWeight(BigDecimal quantity, String unit) {
|
||||||
|
if (quantity == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String qty = quantity.stripTrailingZeros().toPlainString();
|
||||||
|
return Func.isBlank(unit) ? qty : qty + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatLocalDate(LocalDate date) {
|
||||||
|
if (date == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return date.format(DATE_YMD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDateTime(Date date) {
|
||||||
|
if (date == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return DateUtil.format(date, DateUtil.PATTERN_DATETIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long countIncompleteExceptions() {
|
||||||
|
return exceptionDisposalService.count(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||||
|
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long countPendingRisks() {
|
||||||
|
return riskDisposalService.count(Wrappers.<RiskDisposal>lambdaQuery()
|
||||||
|
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AdminTodoItemVO> buildExceptionFeed() {
|
||||||
|
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||||
|
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||||
|
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||||
|
.last("LIMIT " + FEED_LIMIT));
|
||||||
|
return list.stream().map(this::toTodoItem).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminTodoItemVO toTodoItem(ExceptionDisposal disposal) {
|
||||||
|
AdminTodoItemVO item = new AdminTodoItemVO();
|
||||||
|
item.setId(disposal.getId());
|
||||||
|
item.setType("exception");
|
||||||
|
item.setTitle("异常待处置");
|
||||||
|
item.setTimeAgo(formatTimeAgo(disposal.getReportTime() != null
|
||||||
|
? disposal.getReportTime()
|
||||||
|
: toLocalDateTime(disposal.getCreateTime())));
|
||||||
|
item.setDesc(buildExceptionDesc(disposal));
|
||||||
|
item.setWaybillNo(Func.toStr(disposal.getWaybillNo(), ""));
|
||||||
|
item.setActionLabel("立即处置");
|
||||||
|
String status = Func.toStr(disposal.getDisposalStatus(), DISPOSAL_PENDING);
|
||||||
|
item.setTargetUrl("/subpackages/admin/exception?status=" + status);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildExceptionDesc(ExceptionDisposal disposal) {
|
||||||
|
String reporter = Func.toStr(disposal.getReporterName(), "司机");
|
||||||
|
String type = Func.toStr(disposal.getExceptionType(), "异常");
|
||||||
|
String reason = Func.isNotBlank(disposal.getExceptionReason())
|
||||||
|
? disposal.getExceptionReason()
|
||||||
|
: Func.toStr(disposal.getReportDescription(), "");
|
||||||
|
if (Func.isBlank(reason)) {
|
||||||
|
return reporter + "上报" + type;
|
||||||
|
}
|
||||||
|
String text = reporter + "上报" + type + ":" + reason.trim();
|
||||||
|
return text.length() > 80 ? text.substring(0, 80) + "…" : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveUserName() {
|
||||||
|
String realName = UserCache.getUserRealName(AuthUtil.getUserId());
|
||||||
|
if (Func.isNotBlank(realName)) {
|
||||||
|
return realName;
|
||||||
|
}
|
||||||
|
return Func.toStr(AuthUtil.getUserName(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTimeAgo(LocalDateTime time) {
|
||||||
|
if (time == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Duration duration = Duration.between(time, LocalDateTime.now());
|
||||||
|
if (duration.isNegative()) {
|
||||||
|
duration = Duration.ZERO;
|
||||||
|
}
|
||||||
|
long minutes = duration.toMinutes();
|
||||||
|
if (minutes < 1) {
|
||||||
|
return "刚刚";
|
||||||
|
}
|
||||||
|
if (minutes < 60) {
|
||||||
|
return minutes + "分钟";
|
||||||
|
}
|
||||||
|
long hours = duration.toHours();
|
||||||
|
if (hours < 24) {
|
||||||
|
return hours + "小时";
|
||||||
|
}
|
||||||
|
long days = duration.toDays();
|
||||||
|
if (days < 30) {
|
||||||
|
return days + "天";
|
||||||
|
}
|
||||||
|
return time.format(DATE_MD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime toLocalDateTime(Date date) {
|
||||||
|
if (date == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+11
-1
@@ -819,10 +819,20 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public boolean reassign(Waybill request) {
|
public boolean reassign(Waybill request) {
|
||||||
|
return doReassign(request, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean reassignWithoutDeptCheck(Waybill request) {
|
||||||
|
return doReassign(request, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean doReassign(Waybill request, boolean checkDept) {
|
||||||
if (request == null || Func.isEmpty(request.getId())) {
|
if (request == null || Func.isEmpty(request.getId())) {
|
||||||
throw new ServiceException("运单ID不能为空");
|
throw new ServiceException("运单ID不能为空");
|
||||||
}
|
}
|
||||||
Waybill waybill = loadEditable(request.getId(), true);
|
Waybill waybill = loadEditable(request.getId(), checkDept);
|
||||||
assertNotLoaded(waybill);
|
assertNotLoaded(waybill);
|
||||||
if (!"pending".equals(waybill.getBusinessStatus()) && !"running".equals(waybill.getBusinessStatus())) {
|
if (!"pending".equals(waybill.getBusinessStatus()) && !"running".equals(waybill.getBusinessStatus())) {
|
||||||
throw new ServiceException("仅待执行/进行中运单允许重新派单");
|
throw new ServiceException("仅待执行/进行中运单允许重新派单");
|
||||||
|
|||||||
+21
-2
@@ -1,9 +1,13 @@
|
|||||||
package org.springblade.thirdparty.oa.config;
|
package org.springblade.thirdparty.oa.config;
|
||||||
|
|
||||||
|
import feign.Request;
|
||||||
import feign.RequestInterceptor;
|
import feign.RequestInterceptor;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author bfhuange
|
* @author bfhuange
|
||||||
* @since 2024/12/18
|
* @since 2024/12/18
|
||||||
@@ -16,8 +20,23 @@ public class OAFeignClientConfig {
|
|||||||
public RequestInterceptor requestInterceptor() {
|
public RequestInterceptor requestInterceptor() {
|
||||||
return template -> {
|
return template -> {
|
||||||
// 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor
|
// 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor
|
||||||
String authorization=oaProperties.getAuthorization();
|
template.header("Authorization", normalizeAuthorization(oaProperties.getAuthorization()));
|
||||||
template.header("Authorization",authorization);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Request.Options options() {
|
||||||
|
return new Request.Options(10, TimeUnit.SECONDS, 120, TimeUnit.SECONDS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeAuthorization(String authorization) {
|
||||||
|
if (StringUtil.isBlank(authorization)) {
|
||||||
|
return authorization;
|
||||||
|
}
|
||||||
|
if (StringUtil.startsWithIgnoreCase(authorization, "Basic ")
|
||||||
|
|| StringUtil.startsWithIgnoreCase(authorization, "Bearer ")) {
|
||||||
|
return authorization;
|
||||||
|
}
|
||||||
|
return "Basic " + authorization;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -30,4 +30,8 @@ public class OAConvertConstant {
|
|||||||
* 根公司父id
|
* 根公司父id
|
||||||
*/
|
*/
|
||||||
public static final Long ROOT_PARENT_ID = 0L;
|
public static final Long ROOT_PARENT_ID = 0L;
|
||||||
|
/**
|
||||||
|
* 同步人员时挂载的顶级组织名称
|
||||||
|
*/
|
||||||
|
public static final String ROOT_COMPANY_NAME = "桂物物流集团";
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -42,6 +42,6 @@ public interface IOAClient {
|
|||||||
* @param param
|
* @param param
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@PostMapping("${thirdParty.oa.queryPersonPageUrl:/api/hrm/resful/getHrmUserInfoWithPage}")
|
@PostMapping("${thirdParty.oa.queryPersonPageUrl:/gwzh/OA/OA_GET_USER_LIST}")
|
||||||
OAResponse<OAPersonResponse> queryPersonPage(@RequestBody OASearch<OAPersonSearch> param);
|
OAResponse<OAPersonResponse> queryPersonPage(@RequestBody OASearch<OAPersonSearch> param);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-third-party-api</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>blade-wechat-api</artifactId>
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<description>微信小程序:code2session / 手机号 / openid</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-core-tool</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序第三方能力自动配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ComponentScan("org.springblade.thirdparty.wechat")
|
||||||
|
@EnableConfigurationProperties(WechatMiniProperties.class)
|
||||||
|
public class ThirdPartyWechatAutoConfiguration {
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序配置(Nacos:thirdParty.wechat.mini)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ConfigurationProperties(prefix = "third-party.wechat.mini")
|
||||||
|
public class WechatMiniProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 AppId
|
||||||
|
*/
|
||||||
|
private String appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 AppSecret
|
||||||
|
*/
|
||||||
|
private String appSecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信 API 根地址
|
||||||
|
*/
|
||||||
|
private String apiBase = "https://api.weixin.qq.com";
|
||||||
|
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序常量
|
||||||
|
*/
|
||||||
|
public interface WechatMiniConstant {
|
||||||
|
|
||||||
|
/** blade_user_oauth.source */
|
||||||
|
String SOURCE = "WECHAT_MINI";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth2 grant_type(对齐 BladeX OAuth2GranterConstant.WECHAT_APPLET)
|
||||||
|
*/
|
||||||
|
String GRANT_TYPE = "wechat_applet";
|
||||||
|
|
||||||
|
String JSCODE2SESSION_PATH = "/sns/jscode2session";
|
||||||
|
String ACCESS_TOKEN_PATH = "/cgi-bin/token";
|
||||||
|
String GET_PHONE_NUMBER_PATH = "/wxa/business/getuserphonenumber";
|
||||||
|
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序调用异常
|
||||||
|
*/
|
||||||
|
public class WechatMiniException extends RuntimeException {
|
||||||
|
|
||||||
|
public WechatMiniException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WechatMiniException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.pojo.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信手机号结果
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatPhoneVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 不带区号的手机号 */
|
||||||
|
private String phoneNumber;
|
||||||
|
|
||||||
|
/** 带区号手机号 */
|
||||||
|
private String purePhoneNumber;
|
||||||
|
|
||||||
|
private String countryCode;
|
||||||
|
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.pojo.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jscode2session 结果
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatSessionVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private String openid;
|
||||||
|
private String sessionKey;
|
||||||
|
private String unionid;
|
||||||
|
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.service;
|
||||||
|
|
||||||
|
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
|
||||||
|
import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序能力:openid / 手机号
|
||||||
|
*/
|
||||||
|
public interface IWechatMiniService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wx.login code → openid / session_key
|
||||||
|
*/
|
||||||
|
WechatSessionVO code2Session(String loginCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getPhoneNumber 返回的 code → 手机号
|
||||||
|
*/
|
||||||
|
WechatPhoneVO getPhoneNumber(String phoneCode);
|
||||||
|
|
||||||
|
}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
package org.springblade.thirdparty.wechat.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.http.HttpRequest;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springblade.thirdparty.wechat.config.WechatMiniProperties;
|
||||||
|
import org.springblade.thirdparty.wechat.constant.WechatMiniConstant;
|
||||||
|
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
|
||||||
|
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
|
||||||
|
import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO;
|
||||||
|
import org.springblade.thirdparty.wechat.service.IWechatMiniService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序:code2session / getuserphonenumber
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WechatMiniServiceImpl implements IWechatMiniService {
|
||||||
|
|
||||||
|
/** access_token 提前 200 秒刷新 */
|
||||||
|
private static final long TOKEN_REFRESH_AHEAD_MS = 200_000L;
|
||||||
|
|
||||||
|
private final WechatMiniProperties properties;
|
||||||
|
|
||||||
|
private volatile String cachedAccessToken;
|
||||||
|
private volatile long accessTokenExpireAt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WechatSessionVO code2Session(String loginCode) {
|
||||||
|
assertConfigured();
|
||||||
|
if (StringUtil.isBlank(loginCode)) {
|
||||||
|
throw new WechatMiniException("微信登录 code 不能为空");
|
||||||
|
}
|
||||||
|
String url = properties.getApiBase() + WechatMiniConstant.JSCODE2SESSION_PATH
|
||||||
|
+ "?appid=" + properties.getAppId()
|
||||||
|
+ "&secret=" + properties.getAppSecret()
|
||||||
|
+ "&js_code=" + loginCode
|
||||||
|
+ "&grant_type=authorization_code";
|
||||||
|
String body = HttpUtil.get(url, 8000);
|
||||||
|
JSONObject json = parseJson(body, "code2session");
|
||||||
|
assertWxOk(json, "获取 openid 失败");
|
||||||
|
String openid = json.getStr("openid");
|
||||||
|
if (StringUtil.isBlank(openid)) {
|
||||||
|
throw new WechatMiniException("微信未返回 openid");
|
||||||
|
}
|
||||||
|
WechatSessionVO vo = new WechatSessionVO();
|
||||||
|
vo.setOpenid(openid);
|
||||||
|
vo.setSessionKey(json.getStr("session_key"));
|
||||||
|
vo.setUnionid(json.getStr("unionid"));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WechatPhoneVO getPhoneNumber(String phoneCode) {
|
||||||
|
assertConfigured();
|
||||||
|
if (StringUtil.isBlank(phoneCode)) {
|
||||||
|
throw new WechatMiniException("微信手机号 code 不能为空");
|
||||||
|
}
|
||||||
|
String accessToken = getAccessToken();
|
||||||
|
String url = properties.getApiBase() + WechatMiniConstant.GET_PHONE_NUMBER_PATH
|
||||||
|
+ "?access_token=" + accessToken;
|
||||||
|
Map<String, Object> payload = new HashMap<>(2);
|
||||||
|
payload.put("code", phoneCode);
|
||||||
|
String body = HttpRequest.post(url)
|
||||||
|
.body(JSONUtil.toJsonStr(payload))
|
||||||
|
.timeout(8000)
|
||||||
|
.execute()
|
||||||
|
.body();
|
||||||
|
JSONObject json = parseJson(body, "getuserphonenumber");
|
||||||
|
assertWxOk(json, "获取手机号失败");
|
||||||
|
JSONObject phoneInfo = json.getJSONObject("phone_info");
|
||||||
|
if (phoneInfo == null) {
|
||||||
|
throw new WechatMiniException("微信未返回手机号信息");
|
||||||
|
}
|
||||||
|
WechatPhoneVO vo = new WechatPhoneVO();
|
||||||
|
vo.setPhoneNumber(phoneInfo.getStr("phoneNumber"));
|
||||||
|
vo.setPurePhoneNumber(phoneInfo.getStr("purePhoneNumber"));
|
||||||
|
vo.setCountryCode(phoneInfo.getStr("countryCode"));
|
||||||
|
if (StringUtil.isBlank(vo.getPurePhoneNumber()) && StringUtil.isBlank(vo.getPhoneNumber())) {
|
||||||
|
throw new WechatMiniException("微信未返回有效手机号");
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAccessToken() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (StringUtil.isNotBlank(cachedAccessToken) && now < accessTokenExpireAt) {
|
||||||
|
return cachedAccessToken;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
now = System.currentTimeMillis();
|
||||||
|
if (StringUtil.isNotBlank(cachedAccessToken) && now < accessTokenExpireAt) {
|
||||||
|
return cachedAccessToken;
|
||||||
|
}
|
||||||
|
String url = properties.getApiBase() + WechatMiniConstant.ACCESS_TOKEN_PATH
|
||||||
|
+ "?grant_type=client_credential"
|
||||||
|
+ "&appid=" + properties.getAppId()
|
||||||
|
+ "&secret=" + properties.getAppSecret();
|
||||||
|
String body = HttpUtil.get(url, 8000);
|
||||||
|
JSONObject json = parseJson(body, "getAccessToken");
|
||||||
|
assertWxOk(json, "获取 access_token 失败");
|
||||||
|
String token = json.getStr("access_token");
|
||||||
|
Integer expiresIn = json.getInt("expires_in", 7200);
|
||||||
|
if (StringUtil.isBlank(token)) {
|
||||||
|
throw new WechatMiniException("微信未返回 access_token");
|
||||||
|
}
|
||||||
|
cachedAccessToken = token;
|
||||||
|
accessTokenExpireAt = System.currentTimeMillis() + Math.max(60, expiresIn) * 1000L - TOKEN_REFRESH_AHEAD_MS;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertConfigured() {
|
||||||
|
if (StringUtil.isBlank(properties.getAppId()) || StringUtil.isBlank(properties.getAppSecret())) {
|
||||||
|
throw new WechatMiniException("未配置微信小程序 appId/appSecret(Nacos: thirdParty.wechat.mini)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject parseJson(String body, String action) {
|
||||||
|
if (StringUtil.isBlank(body)) {
|
||||||
|
throw new WechatMiniException("微信接口无响应: " + action);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSONUtil.parseObj(body);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析微信响应失败 action={} body={}", action, body, e);
|
||||||
|
throw new WechatMiniException("解析微信响应失败: " + action, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertWxOk(JSONObject json, String fallbackMsg) {
|
||||||
|
Integer errcode = json.getInt("errcode");
|
||||||
|
if (errcode != null && errcode != 0) {
|
||||||
|
String errmsg = json.getStr("errmsg", fallbackMsg);
|
||||||
|
throw new WechatMiniException(fallbackMsg + ":" + errmsg + "(" + errcode + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.springblade.thirdparty.wechat.config.ThirdPartyWechatAutoConfiguration
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<module>blade-wps-api</module>
|
<module>blade-wps-api</module>
|
||||||
<module>blade-ocr-api</module>
|
<module>blade-ocr-api</module>
|
||||||
<module>blade-track-api</module>
|
<module>blade-track-api</module>
|
||||||
|
<module>blade-wechat-api</module>
|
||||||
</modules>
|
</modules>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<description>BladeX 第三方API集合</description>
|
<description>BladeX 第三方API集合</description>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ thirdParty:
|
|||||||
oa:
|
oa:
|
||||||
# OA开放接口地址
|
# OA开放接口地址
|
||||||
baseUrl: http://127.0.0.1:8080
|
baseUrl: http://127.0.0.1:8080
|
||||||
|
queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST
|
||||||
track:
|
track:
|
||||||
# 轨迹开放接口地址
|
# 轨迹开放接口地址
|
||||||
baseUrl: http://127.0.0.1:8080
|
baseUrl: http://127.0.0.1:8080
|
||||||
@@ -120,6 +121,11 @@ thirdParty:
|
|||||||
downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download}
|
downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download}
|
||||||
queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list}
|
queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list}
|
||||||
queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list}
|
queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list}
|
||||||
|
wechat:
|
||||||
|
mini:
|
||||||
|
# 微信小程序(手机号一键登录 / openid)
|
||||||
|
appId: ${WECHAT_MINI_APP_ID:}
|
||||||
|
appSecret: ${WECHAT_MINI_APP_SECRET:}
|
||||||
|
|
||||||
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
||||||
baidu:
|
baidu:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ thirdParty:
|
|||||||
oa:
|
oa:
|
||||||
# OA开放接口地址
|
# OA开放接口地址
|
||||||
baseUrl: http://127.0.0.1:8080
|
baseUrl: http://127.0.0.1:8080
|
||||||
|
queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST
|
||||||
track:
|
track:
|
||||||
# 轨迹开放接口地址
|
# 轨迹开放接口地址
|
||||||
baseUrl: http://127.0.0.1:8080
|
baseUrl: http://127.0.0.1:8080
|
||||||
@@ -92,6 +93,10 @@ thirdParty:
|
|||||||
downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download}
|
downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download}
|
||||||
queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list}
|
queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list}
|
||||||
queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list}
|
queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list}
|
||||||
|
wechat:
|
||||||
|
mini:
|
||||||
|
appId: ${WECHAT_MINI_APP_ID:}
|
||||||
|
appSecret: ${WECHAT_MINI_APP_SECRET:}
|
||||||
|
|
||||||
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
||||||
baidu:
|
baidu:
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ blade:
|
|||||||
#接口放行
|
#接口放行
|
||||||
skip-url:
|
skip-url:
|
||||||
- /test/**
|
- /test/**
|
||||||
|
# 退出登录:允许无令牌/令牌失效时也能调用(服务端对无用户直接返回成功)
|
||||||
|
- /oauth/logout/**
|
||||||
|
- /blade-auth/oauth/logout/**
|
||||||
#授权认证配置
|
#授权认证配置
|
||||||
auth:
|
auth:
|
||||||
- method: ALL
|
- method: ALL
|
||||||
|
|||||||
@@ -27,6 +27,28 @@
|
|||||||
"filters": [],
|
"filters": [],
|
||||||
"uri": "lb://blade-transport"
|
"uri": "lb://blade-transport"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "blade-user-alias-route",
|
||||||
|
"order": 0,
|
||||||
|
"predicates": [
|
||||||
|
{
|
||||||
|
"name": "Path",
|
||||||
|
"args": {
|
||||||
|
"pattern": "/blade-user/**"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"name": "RewritePath",
|
||||||
|
"args": {
|
||||||
|
"regexp": "/blade-user/(?<segment>.*)",
|
||||||
|
"replacement": "/user/$\\{segment}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uri": "lb://blade-system"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "example-route",
|
"id": "example-route",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ thirdParty:
|
|||||||
oa:
|
oa:
|
||||||
# OA开放接口地址
|
# OA开放接口地址
|
||||||
baseUrl: ${OA_BASE_URL:http://127.0.0.1:8080}
|
baseUrl: ${OA_BASE_URL:http://127.0.0.1:8080}
|
||||||
|
queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST}
|
||||||
track:
|
track:
|
||||||
# 轨迹开放接口地址
|
# 轨迹开放接口地址
|
||||||
baseUrl: ${TRACK_BASE_URL:http://127.0.0.1:8080}
|
baseUrl: ${TRACK_BASE_URL:http://127.0.0.1:8080}
|
||||||
mk:
|
mk:
|
||||||
# MK开放接口地址
|
# MK开放接口地址
|
||||||
baseUrl: ${MK_BASE_URL:http://127.0.0.1:8080}
|
baseUrl: ${MK_BASE_URL:http://127.0.0.1:8080}
|
||||||
|
wechat:
|
||||||
|
mini:
|
||||||
|
# 微信小程序 AppId / AppSecret(Nacos 配置,勿提交真实 secret)
|
||||||
|
appId: ${WECHAT_MINI_APP_ID:}
|
||||||
|
appSecret: ${WECHAT_MINI_APP_SECRET:}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- 小程序调度端入口权限码 mp_auth
|
||||||
|
-- user-info 的 permission 来自按钮叶子 code;需把本菜单授权给调度/管理员角色
|
||||||
|
-- 执行后:后台「角色管理 → 权限配置」勾选「小程序调度端」,或按需 INSERT blade_role_menu
|
||||||
|
|
||||||
|
-- 父级菜单(小程序)
|
||||||
|
INSERT IGNORE INTO `blade_menu`
|
||||||
|
(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`)
|
||||||
|
VALUES
|
||||||
|
(2091800000000000001, 0, 'miniprogram', '小程序', 'menu', '/miniprogram', 'iconfont iconicon_work', 98, 1, 0, 1, '', '小程序相关权限', 0);
|
||||||
|
|
||||||
|
-- 按钮权限:mp_auth(叶子节点,会被 permissionCodes 收集)
|
||||||
|
INSERT IGNORE INTO `blade_menu`
|
||||||
|
(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`)
|
||||||
|
VALUES
|
||||||
|
(2091800000000000002, 2091800000000000001, 'mp_auth', '小程序调度端', 'mp_auth', '', '', 1, 2, 0, 1, NULL, '登录后进入调度端首页', 0);
|
||||||
|
|
||||||
|
-- 示例:给管理员角色(role_id 按环境实际调整,默认管理员 1123598816738675201)
|
||||||
|
-- INSERT IGNORE INTO `blade_role_menu` (`id`, `menu_id`, `role_id`)
|
||||||
|
-- VALUES
|
||||||
|
-- (2091800000000000101, 2091800000000000001, 1123598816738675201),
|
||||||
|
-- (2091800000000000102, 2091800000000000002, 1123598816738675201);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- 为小程序客户端授权类型补充微信小程序登录(wechat_applet)
|
||||||
|
-- 执行后建议清客户端缓存 / 重启 blade-auth
|
||||||
|
|
||||||
|
UPDATE blade_client
|
||||||
|
SET authorized_grant_types = CONCAT(authorized_grant_types, ',wechat_applet')
|
||||||
|
WHERE client_id IN ('saber3', 'saber', 'sword', 'rider')
|
||||||
|
AND FIND_IN_SET('wechat_applet', REPLACE(authorized_grant_types, ' ', '')) = 0
|
||||||
|
AND is_deleted = 0;
|
||||||
+1379
File diff suppressed because it is too large
Load Diff
+1368
File diff suppressed because it is too large
Load Diff
@@ -149,6 +149,11 @@
|
|||||||
<artifactId>blade-track-api</artifactId>
|
<artifactId>blade-track-api</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-wechat-api</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
|
|||||||
Reference in New Issue
Block a user