1、新增小程序相关接口

2、调整OA
This commit is contained in:
2026-09-18 16:31:37 +08:00
parent 0fa0eae43c
commit 01993779a7
71 changed files with 6463 additions and 213 deletions
+4
View File
@@ -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";
} }
@@ -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,参数:loginCodewx.login)、phoneCodegetPhoneNumber)。
* 流程:换 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;
}
}
@@ -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/**");
@@ -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);
} }
@@ -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("获取数据失败");
}
} }
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<>();
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
} }
@@ -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_oauthsource=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);
} }
@@ -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;
}
@@ -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;
}
+4
View File
@@ -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>
@@ -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));
}
/** /**
* 管理员修改密码 * 管理员修改密码
*/ */
@@ -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));
}
}
@@ -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));
}
} }
@@ -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);
/** /**
* 树形结构 * 树形结构
* *
@@ -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);
} }
@@ -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);
}
@@ -188,6 +188,11 @@ public interface IUserService extends BaseService<User> {
*/ */
UserInfo userInfo(UserOauth userOauth); UserInfo userInfo(UserOauth userOauth);
/**
* 绑定微信小程序 openid 到已有用户(blade_user_oauthsource=WECHAT_MINI
*/
boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone);
/** /**
* 根据租户与账号获取用户 * 根据租户与账号获取用户
* *
@@ -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());
@@ -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();
}
} }
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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) {
@@ -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("批量完成成功");
@@ -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已完成;exceptionexception/normaltransportTypecommon/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));
}
}
@@ -68,6 +68,12 @@ public interface IDriverWaybillService {
*/ */
DriverWaybillCardVO detail(Long id); DriverWaybillCardVO detail(Long id);
/**
* 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。
* 供调度端 manage/detail 复用。
*/
DriverWaybillCardVO detailPunchSnapshot(Long id);
/** /**
* 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。 * 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。
*/ */
@@ -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);
}
@@ -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);
/** /**
@@ -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) {
@@ -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();
}
}
@@ -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("仅待执行/进行中运单允许重新派单");
@@ -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;
}
} }
@@ -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 = "桂物物流集团";
} }
@@ -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>
@@ -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 {
}
@@ -0,0 +1,28 @@
package org.springblade.thirdparty.wechat.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 微信小程序配置(NacosthirdParty.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";
}
@@ -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";
}
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
@@ -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/appSecretNacos: 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 + ")");
}
}
}
@@ -0,0 +1 @@
org.springblade.thirdparty.wechat.config.ThirdPartyWechatAutoConfiguration
+1
View File
@@ -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>
+6
View File
@@ -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:
+5
View File
@@ -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:
+3
View File
@@ -213,6 +213,9 @@ blade:
#接口放行 #接口放行
skip-url: skip-url:
- /test/** - /test/**
# 退出登录:允许无令牌/令牌失效时也能调用(服务端对无用户直接返回成功)
- /oauth/logout/**
- /blade-auth/oauth/logout/**
#授权认证配置 #授权认证配置
auth: auth:
- method: ALL - method: ALL
+22
View File
@@ -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,
+6
View File
@@ -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 / AppSecretNacos 配置,勿提交真实 secret
appId: ${WECHAT_MINI_APP_ID:}
appSecret: ${WECHAT_MINI_APP_SECRET:}
+21
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1368
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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>