Merge remote-tracking branch 'origin/master' into feature/payment-apply-bfi-sync

# Conflicts:
#	blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java
#	blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java
#	blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java
#	blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
#	blade-service/blade-transport/pom.xml
#	blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java
#	blade-service/blade-transport/src/main/resources/application-dev.yml
#	blade-third-party-api/pom.xml
#	doc/nacos/blade-dev.yaml
#	doc/nacos/blade-prod.yaml
#	doc/nacos/third-party-api.yaml
#	pom.xml
This commit is contained in:
刘泉佳
2026-09-24 03:15:30 +08:00
235 changed files with 15516 additions and 2569 deletions
@@ -44,6 +44,10 @@ public class OpenApiApplication {
public static void main(String[] args) {
BladeApplication.disableNacosLaunchConfig();
// 当前处理人刷新依赖 RedisLockClient;Nacos 全局 blade.lock.enabled=false 时仍需为本服务开启
if (System.getProperty("blade.lock.enabled") == null) {
System.setProperty("blade.lock.enabled", "true");
}
BladeApplication.run(AppConstant.APPLICATION_OPENAPI_NAME, OpenApiApplication.class, args);
}
@@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.Hidden;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tool.api.FR;
import org.springblade.openapi.mk.api.IApi4MK;
import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO;
@@ -49,6 +51,7 @@ public class Api4MK implements IApi4MK {
}
@Override
@PreAuth(AuthConstant.PERMIT_ALL)
public FR<Boolean> processCommonCallback(Api4MKProcessApprovalDTO param) {
log.info("mk流程通用回调 操作名称:{} 参数:{}", ProcessOperationType.getOperationName(param.getOperation()), JSON.toJSONString(param));
callback(param, ProcessHandler::approve);
@@ -10,6 +10,7 @@ spring:
- nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true
- nacos:blade-openapi-dynamictp.yaml?group=DEFAULT_GROUP&refreshEnabled=true
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true
- optional:classpath:openapi-lock.yaml
cloud:
nacos:
username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}}
@@ -0,0 +1,9 @@
# openapi 当前处理人刷新依赖 Redisson 分布式锁。
# 该文件必须在 nacos blade-*.yaml 之后导入,用于覆盖全局 blade.lock.enabled=false。
# 连接信息与业务 Redis 保持一致,避免 Redisson 因缺少密码出现 NOAUTH。
blade:
lock:
enabled: true
address: redis://${spring.data.redis.host:127.0.0.1}:${spring.data.redis.port:6379}
password: ${spring.data.redis.password:}
database: ${spring.data.redis.database:0}
+8
View File
@@ -45,6 +45,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-user-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-resource-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-process-api</artifactId>
@@ -58,6 +62,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-mk-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-transport-api</artifactId>
</dependency>
<!-- 其他依赖 -->
<dependency>
@@ -17,6 +17,7 @@ import org.springblade.process.pojo.dto.ApprovalDTO;
import org.springblade.process.pojo.vo.ApprovalVO;
import org.springblade.process.pojo.vo.ProcessApprovedRecordVO;
import org.springblade.process.service.IBusinessProcessService;
import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -52,29 +53,42 @@ public class BusinessProcessController extends BladeController {
return R.data(pages);
}
@GetMapping("/isEditView")
@PostMapping("/processSubmit")
@ApiOperationSupport(order = 2)
@Operation(summary = "提交MK审核流", description = "调用mk processSubmit,传入templateCode/submitIdentity/formInstanceId")
public R<String> processSubmit(@Validated @RequestBody MKProcessCreateDTO param) {
return R.data(businessProcessService.processSubmit(param));
}
@PostMapping("/processDelete")
@ApiOperationSupport(order = 7)
public R<Boolean> processDelete(@RequestBody MKProcessCreateDTO param) {
return R.data(businessProcessService.processDelete(param == null ? null : param.getFormInstanceId()));
}
@GetMapping("/isEditView")
@ApiOperationSupport(order = 3)
@Operation(summary = "是否编辑页", description = "传入业务id")
public R<Boolean> isEditView(@Valid @NotBlank(message = "业务id不能为空") String bizId) {
return R.data(businessProcessService.isEditView(bizId));
}
@GetMapping("/getMKApprovalUrl")
@ApiOperationSupport(order = 3)
@ApiOperationSupport(order = 4)
@Operation(summary = "获取mk审批页链接", description = "传入业务id或流程实例id")
public R<String> getMKApprovalUrl(String bizId, String processInstanceId) {
return R.data(businessProcessService.getMKApprovalUrl(bizId, processInstanceId));
}
@GetMapping("/getApprovedRecords")
@ApiOperationSupport(order = 4)
@ApiOperationSupport(order = 5)
@Operation(summary = "查询审批记录", description = "传入业务id或流程实例id")
public R<List<ProcessApprovedRecordVO>> getApprovedRecords(String bizId, String processInstanceId) {
return R.data(businessProcessService.queryApprovedRecords(bizId, processInstanceId));
}
@GetMapping("/downloadFile")
@ApiOperationSupport(order = 5)
@ApiOperationSupport(order = 6)
@Operation(summary = "下载附件", description = "传入附件id")
public void downloadFile(HttpServletResponse response, @Valid @NotBlank(message = "附件id不能为空") String fileId) {
businessProcessService.downloadFile(response, fileId);
@@ -3,6 +3,8 @@ package org.springblade.process.feign;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tool.api.FR;
import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO;
import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO;
@@ -13,8 +15,10 @@ import org.springblade.process.pojo.vo.ProcessApprovedRecordVO;
import org.springblade.process.pojo.vo.ProcessTodoVO;
import org.springblade.process.service.IBusinessProcessService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@@ -74,4 +78,20 @@ public class BusinessProcessClient implements IBusinessProcessClient {
public FR<List<ProcessApprovedRecordVO>> queryApprovedRecordsNoAttachments(String bizId, String processInstanceId) {
return FR.data(businessProcessService.queryApprovedRecordsNoAttachments(bizId, processInstanceId));
}
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping(GET_CURRENT_NODES)
@Override
public FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam(value = "loginName", required = false) String loginName) {
return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName));
}
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping(GET_PROCESS_INFO)
@Override
public FR<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam(value = "loginName", required = false) String loginName) {
return FR.data(businessProcessService.getProcessInfo(processInstanceId, loginName));
}
}
@@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springblade.process.pojo.dto.*;
import org.springblade.process.pojo.entity.BusinessProcess;
import org.springblade.process.pojo.vo.*;
import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO;
import java.util.List;
@@ -25,6 +26,40 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
*/
BusinessProcessVO submitBusinessProcess(BusinessProcessSubmitDTO<?> param);
/**
* 直接调用 MK processSubmit 提交流程
*
* @param param MK 流程创建参数
* @return 流程实例 id
*/
String processSubmit(MKProcessCreateDTO param);
/**
* 按业务表单实例 id 删除 MK 流程(不删除本地业务流程记录,供驳回后重新提交使用)
*
* @param formInstanceId 业务表单实例 id
* @return 是否成功
*/
boolean processDelete(String formInstanceId);
/**
* 获取流程当前节点详情
*
* @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
* @return 当前节点列表
*/
List<?> getCurrentNodes(String processInstanceId, String loginName);
/**
* 获取流程实例详情
*
* @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
* @return 流程实例详情
*/
Object getProcessInfo(String processInstanceId, String loginName);
/**
* 修改业务流程状态
* @param param
@@ -15,6 +15,7 @@ import org.springblade.process.pojo.enums.ApproveStatusEnum;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.log.utils.AssertUtils;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.convert.ApprovalConvert;
import org.springblade.process.convert.BusinessProcessConvert;
@@ -24,6 +25,8 @@ import org.springblade.process.pojo.entity.BusinessProcess;
import org.springblade.process.pojo.enums.TodoStatus;
import org.springblade.process.pojo.vo.*;
import org.springblade.process.service.IBusinessProcessService;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.service.IUserService;
import org.springblade.thirdparty.mk.config.MKProperties;
import org.springblade.thirdparty.mk.constant.MKConstant;
import org.springblade.thirdparty.mk.constant.MKDoc;
@@ -39,12 +42,17 @@ import org.springblade.thirdparty.mk.pojo.dto.approval.MKProcessDTO;
import org.springblade.thirdparty.mk.pojo.dto.sort.*;
import org.springblade.thirdparty.mk.pojo.vo.*;
import org.springblade.thirdparty.mk.service.IMKService;
import org.springblade.transport.feign.ICustomerArchiveClient;
import org.springblade.transport.feign.IMkProcessClient;
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 业务流程关联表 服务实现类
@@ -61,6 +69,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
private final IMKService mkService;
private final MKProperties mkProperties;
private final ApprovalConvert approvalConvert;
private final IUserService userService;
private final ICustomerArchiveClient customerArchiveClient;
private final IMkProcessClient mkProcessClient;
@Transactional(rollbackFor = Exception.class)
@Override
@@ -114,6 +125,259 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
return businessProcessVO;
}
@Transactional(rollbackFor = Exception.class)
@Override
public String processSubmit(MKProcessCreateDTO param) {
AssertUtils.notNull(param, "提交流程参数不能为空");
AssertUtils.notBlank(param.getFormInstanceId(), "表单实例id不能为空");
AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空");
// 前端拿到的手机号可能经 @Sensitive 脱敏(如 137****8880),这里从库取真实手机号覆盖
String loginName = resolveCurrentUserPhone();
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
param.setSubmitIdentity(loginName);
param.setLoginName(loginName);
String bizType = param.getBizType();
MKProcessCreateDTO mkParam = new MKProcessCreateDTO();
mkParam.setFormInstanceId(param.getFormInstanceId());
mkParam.setSubject(param.getSubject());
mkParam.setSubmitIdentity(loginName);
mkParam.setLoginName(loginName);
mkParam.setTemplateCode(param.getTemplateCode());
mkParam.setFormValues(param.getFormValues());
mkParam.setTempVarData(param.getTempVarData());
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(mkParam));
String processInstanceId = mkService.processSubmit(mkParam);
AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id");
Long bizId;
try {
bizId = Long.valueOf(param.getFormInstanceId());
} catch (NumberFormatException e) {
throw new ServiceException("表单实例id格式不正确");
}
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
.eq(BusinessProcess::getBizId, bizId)
);
if (businessProcess == null) {
businessProcess = new BusinessProcess();
businessProcess.setBizId(bizId);
businessProcess.setProcessType(param.getTemplateCode());
businessProcess.setSubject(param.getSubject());
businessProcess.setPromoterId(AuthUtil.getUserId());
businessProcess.setPromoterName(AuthUtil.getNickName());
businessProcess.setPromoterLoginName(loginName);
businessProcess.setSubmitTime(new Date());
} else {
businessProcess.setProcessType(param.getTemplateCode());
if (StringUtil.isNotBlank(param.getSubject())) {
businessProcess.setSubject(param.getSubject());
}
businessProcess.setPromoterLoginName(loginName);
if (businessProcess.getSubmitTime() == null) {
businessProcess.setSubmitTime(new Date());
}
}
businessProcess.setProcessInstanceId(processInstanceId);
businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue());
this.saveOrUpdate(businessProcess);
this.getCurrentNodes(processInstanceId, loginName);
Object processInfo = this.getProcessInfo(processInstanceId, loginName);
this.syncBizFromProcessInfo(bizType, param.getFormInstanceId(), processInfo);
return processInstanceId;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean processDelete(String formInstanceId) {
AssertUtils.notBlank(formInstanceId, "表单实例id不能为空");
Long bizId;
try {
bizId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
throw new ServiceException("表单实例id格式不正确");
}
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
.eq(BusinessProcess::getBizId, bizId)
);
if (businessProcess == null || StringUtil.isBlank(businessProcess.getProcessInstanceId())) {
log.warn("客商驳回后删除流程跳过,未找到流程实例 formInstanceId={}", formInstanceId);
return true;
}
String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone();
}
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
log.info("调用mk processDelete processInstanceId={} loginName={} formInstanceId={}",
businessProcess.getProcessInstanceId(), loginName, formInstanceId);
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
if (!deleted) {
throw new ServiceException("删除MK流程失败");
}
return true;
}
@Override
public List<?> getCurrentNodes(String processInstanceId, String loginName) {
if (StringUtils.isBlank(processInstanceId)) {
log.warn("查询当前节点失败,processInstanceId为空");
return Collections.emptyList();
}
String resolvedLoginName = loginName;
if (StringUtils.isBlank(resolvedLoginName)) {
BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId);
resolvedLoginName = resolvePromoterLoginName(businessProcess, null);
}
if (StringUtils.isBlank(resolvedLoginName)) {
log.warn("查询当前节点失败,loginName为空 processInstanceId={}", processInstanceId);
return Collections.emptyList();
}
try {
List<MKNodeVO> currentNodes = mkService.getCurrentNodes(processInstanceId, resolvedLoginName);
log.info("获取流程当前节点详情 processInstanceId={} loginName={} result={}",
processInstanceId, resolvedLoginName, JSON.toJSONString(currentNodes));
return currentNodes == null ? Collections.emptyList() : currentNodes;
} catch (Exception e) {
log.error("查询当前节点异常 processInstanceId={} loginName={}", processInstanceId, resolvedLoginName, e);
return Collections.emptyList();
}
}
@Override
public Object getProcessInfo(String processInstanceId, String loginName) {
if (StringUtils.isBlank(processInstanceId)) {
log.warn("查询流程实例详情失败,processInstanceId为空");
return null;
}
String resolvedLoginName = loginName;
if (StringUtils.isBlank(resolvedLoginName)) {
BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId);
resolvedLoginName = resolvePromoterLoginName(businessProcess, null);
}
if (StringUtils.isBlank(resolvedLoginName)) {
log.warn("查询流程实例详情失败,loginName为空 processInstanceId={}", processInstanceId);
return null;
}
try {
Object processInfo = mkService.getProcessInfo(processInstanceId, resolvedLoginName);
log.info("获取流程实例详情 processInstanceId={} loginName={} result={}",
processInstanceId, resolvedLoginName, JSON.toJSONString(processInfo));
return processInfo;
} catch (Exception e) {
log.error("查询流程实例详情异常 processInstanceId={} loginName={}", processInstanceId, resolvedLoginName, e);
return null;
}
}
private void syncBizFromProcessInfo(String bizType, String formInstanceId, Object processInfo) {
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
log.warn("同步业务当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
return;
}
Long bizId;
try {
bizId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
log.warn("同步业务当前节点失败,表单实例id不是数字:{}", formInstanceId);
return;
}
String resolvedBizType = StringUtils.isBlank(bizType) ? "customer-archive" : bizType;
if ("customer-archive".equals(resolvedBizType)) {
this.syncCustomerArchiveFromProcessInfo(formInstanceId, processInfo);
return;
}
MkProcessSyncDTO param = new MkProcessSyncDTO();
param.setBizType(resolvedBizType);
param.setId(bizId);
param.setAction("sync");
param.setProcessInfo(processInfo);
try {
FR<Boolean> result = mkProcessClient.apply(param);
log.info("同步业务当前节点完成 bizType={} bizId={} result={}",
resolvedBizType, bizId, JSON.toJSONString(result));
} catch (Exception e) {
log.error("同步业务当前节点异常 bizType={} bizId={}", resolvedBizType, bizId, e);
}
}
private void syncCustomerArchiveFromProcessInfo(String formInstanceId, Object processInfo) {
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
log.warn("同步客商当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
return;
}
Long customerId;
try {
customerId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
log.warn("同步客商当前节点失败,表单实例id不是数字:{}", formInstanceId);
return;
}
CustomerProcessNodeSyncDTO param = new CustomerProcessNodeSyncDTO();
param.setId(customerId);
param.setProcessInfo(processInfo);
param.setApprovalStatus("reviewing");
try {
FR<Boolean> result = customerArchiveClient.syncProcessNode(param);
log.info("同步客商当前节点完成 customerId={} result={}", customerId, JSON.toJSONString(result));
} catch (Exception e) {
log.error("同步客商当前节点异常 customerId={}", customerId, e);
}
}
private String readMkNodeName(Object node) {
if (node instanceof MKNodeVO vo) {
return vo.getNodeName();
}
if (node instanceof Map<?, ?> map) {
Object value = map.get("nodeName");
return value == null ? null : String.valueOf(value);
}
return null;
}
private Stream<String> readMkHandlerNames(Object node) {
List<MKNodeHandlerVO> handlers = null;
if (node instanceof MKNodeVO vo) {
handlers = vo.getNodeHandlers();
} else if (node instanceof Map<?, ?> map && map.get("nodeHandlers") instanceof List<?> list) {
return list.stream().map(item -> {
if (item instanceof MKNodeHandlerVO handler) {
return handler.getHandlerName();
}
if (item instanceof Map<?, ?> handlerMap) {
Object value = handlerMap.get("handlerName");
return value == null ? null : String.valueOf(value);
}
return null;
});
}
if (CollectionUtil.isEmpty(handlers)) {
return Stream.empty();
}
return handlers.stream().map(MKNodeHandlerVO::getHandlerName);
}
/**
* 从当前登录用户实体读取真实手机号(绕过接口返回脱敏)
*/
private String resolveCurrentUserPhone() {
Long userId = AuthUtil.getUserId();
if (userId != null) {
User user = userService.getById(userId);
if (user != null && StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
return user.getPhone().trim();
}
if (user != null && StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
return user.getAccount().trim();
}
}
String account = AuthUtil.getUserAccount();
if (StringUtil.isNotBlank(account) && account.matches("^1\\d{10}$")) {
return account.trim();
}
return null;
}
@Transactional(rollbackFor = Exception.class)
@Override
public String updateBusinessProcessStatus(BusinessProcessUpdateDTO param) {
@@ -28,13 +28,16 @@ package org.springblade.system;
import org.springblade.core.cloud.client.BladeCloudApplication;
import org.springblade.core.launch.BladeApplication;
import org.springblade.core.launch.constant.AppConstant;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springblade.system.props.IamSyncProperties;
/**
* 系统模块启动器
* @author Chill
*/
@ComponentScan(basePackages = {"org.springblade.system", "org.springblade.process"})
@EnableConfigurationProperties(IamSyncProperties.class)
@BladeCloudApplication
public class SystemApplication {
@@ -44,4 +47,3 @@ public class SystemApplication {
}
}
@@ -50,8 +50,10 @@ import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.enums.DictEnum;
import org.springblade.system.pojo.vo.DeptVO;
import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.service.IDeptService;
import org.springblade.system.service.IOASyncService;
import org.springblade.system.wrapper.DeptWrapper;
import org.springframework.web.bind.annotation.*;
@@ -73,6 +75,7 @@ import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
public class DeptController extends BladeController {
private final IDeptService deptService;
private final IOASyncService oaSyncService;
/**
* 详情
@@ -159,12 +162,63 @@ public class DeptController extends BladeController {
return R.fail("操作失败");
}
/**
* 同步IAM组织。
*/
@IsAdmin
@PostMapping("/sync-iam-organizations")
@ApiOperationSupport(order = 7)
@Operation(summary = "同步IAM组织")
public R<Integer> syncIamOrganizations() {
return R.data(deptService.syncIamOrganizations());
}
/**
* 清除非顶级组织,供自动同步前确认后调用
*/
@IsAdmin
@PostMapping("/clear-non-top")
@ApiOperationSupport(order = 8)
@Operation(summary = "清除非顶级组织")
public R<Integer> clearNonTopDepts() {
int clearedCount = deptService.clearNonTopDepts();
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
return R.data(clearedCount);
}
/**
* 从OA按页同步公司
*/
@IsAdmin
@PostMapping("/sync-oa-company")
@ApiOperationSupport(order = 8)
@Operation(summary = "同步OA公司")
public R<OaOrgSyncPageVO> syncOaCompany(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
return R.data(oaSyncService.syncCompanyPage(current, size));
}
/**
* 从OA按页同步部门(需先完成公司同步)
*/
@IsAdmin
@PostMapping("/sync-oa-department")
@ApiOperationSupport(order = 9)
@Operation(summary = "同步OA部门")
public R<OaOrgSyncPageVO> syncOaDepartment(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
return R.data(oaSyncService.syncDepartmentPage(current, size));
}
/**
* 删除
*/
@IsAdmin
@PostMapping("/remove")
@ApiOperationSupport(order = 7)
@ApiOperationSupport(order = 10)
@Operation(summary = "删除", description = "传入ids")
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
CacheUtil.clear(SYS_CACHE);
@@ -177,7 +231,7 @@ public class DeptController extends BladeController {
*/
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/select")
@ApiOperationSupport(order = 8)
@ApiOperationSupport(order = 11)
@Operation(summary = "下拉数据源", description = "传入id集合")
public R<List<Dept>> select(Long userId, String deptId) {
if (Func.isNotEmpty(userId)) {
@@ -194,7 +248,7 @@ public class DeptController extends BladeController {
*/
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/platform-company-select")
@ApiOperationSupport(order = 9)
@ApiOperationSupport(order = 12)
@Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表")
public R<List<Dept>> platformCompanySelect() {
return R.data(deptService.listPlatformCompany());
@@ -205,7 +259,7 @@ public class DeptController extends BladeController {
*/
@IsAdmin
@GetMapping("/dept-leader-info")
@ApiOperationSupport(order = 10)
@ApiOperationSupport(order = 13)
@Operation(summary = "获取部门的主管信息", description = "传入deptId")
public R<List<UserVO>> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) {
List<UserVO> list = deptService.deptLeaderInfo(deptId);
@@ -1,263 +1,276 @@
/**
* 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.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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 jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.excel.PortTerminalImporter;
import org.springblade.system.pojo.entity.PortTerminal;
import org.springblade.system.pojo.vo.PortTerminalVO;
import org.springblade.system.service.IPortTerminalService;
import org.springblade.system.wrapper.PortTerminalWrapper;
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 org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 港口码头主数据 控制器
*
* @author Chill
*/
@NonDS
@RestController
@AllArgsConstructor
@PreAuth(menu = "port_terminal")
@RequestMapping("/port-terminal")
@Tag(name = "港口码头主数据", description = "港口码头主数据")
public class PortTerminalController extends BladeController {
private static final int DEFAULT_CURRENT = 1;
private static final int DEFAULT_SIZE = 10;
private static final int MAX_SIZE = 100;
private static final String SOURCE_INITIAL = "初始化录入";
private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
private static final String SOURCE_INITIAL_OLD = "初始导入";
private static final String SOURCE_MANUAL = "手动录入";
private static final String SOURCE_MANUAL_OLD = "手工导入";
private final IPortTerminalService portTerminalService;
/**
* 详情
*/
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "详情", description = "传入portTerminal")
public R<PortTerminalVO> detail(PortTerminal portTerminal) {
if (Func.isEmpty(portTerminal.getId())) {
return R.fail("主键不能为空");
}
PortTerminal detail = portTerminalService.getById(portTerminal.getId());
if (Func.isEmpty(detail)) {
return R.fail("港口码头不存在");
}
detail.setDataSource(normalizeDataSource(detail.getDataSource()));
return R.data(PortTerminalWrapper.build().entityVO(detail));
}
/**
* 分页
*/
@GetMapping("/list")
@ApiOperationSupport(order = 2)
@Operation(summary = "分页", description = "传入portTerminal")
public R<IPage<PortTerminalVO>> list(PortTerminalVO portTerminal, Query query) {
IPage<PortTerminalVO> pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
return R.data(pages);
}
/**
* 新增或修改
*/
@PostMapping("/submit")
@ApiOperationSupport(order = 3)
@Operation(summary = "新增或修改", description = "传入portTerminal")
public R submit(@Valid @RequestBody PortTerminal portTerminal) {
return R.status(portTerminalService.submit(portTerminal));
}
/**
* 删除
*/
@PostMapping("/remove")
@ApiOperationSupport(order = 4)
@Operation(summary = "逻辑删除", description = "传入ids")
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
if (Func.isEmpty(ids)) {
return R.fail("主键不能为空");
}
return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
}
/**
* 启用或停用
*/
@PostMapping("/status")
@ApiOperationSupport(order = 5)
@Operation(summary = "启用或停用", description = "传入id和status")
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
@Parameter(description = "状态", required = true) @RequestParam Integer status) {
return R.status(portTerminalService.changeStatus(id, status));
}
/**
* 上级港口下拉数据源
*/
@GetMapping("/port-select")
@ApiOperationSupport(order = 6)
@Operation(summary = "上级港口下拉数据源")
public R<List<PortTerminal>> portSelect() {
List<PortTerminal> ports = portTerminalService.selectEnabledPorts();
ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource())));
return R.data(ports);
}
/**
* 导入港口码头主数据
*/
@PostMapping("/import-port-terminal")
@ApiOperationSupport(order = 7)
@Operation(summary = "导入港口码头主数据", description = "传入excel")
public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
List<PortTerminalExcel> failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
return null;
}
return R.success("操作成功");
}
/**
* 导出港口码头主数据
*/
@GetMapping("/export-port-terminal")
@ApiOperationSupport(order = 8)
@Operation(summary = "导出港口码头主数据")
public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map<String, Object> portTerminal, HttpServletResponse response) {
Object ids = portTerminal.remove("ids");
Object dataSource = portTerminal.remove("dataSource");
portTerminal.remove("Blade-Auth");
portTerminal.remove("Authorization");
portTerminal.remove("access_token");
normalizeRegionCodeCondition(portTerminal);
QueryWrapper<PortTerminal> queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
applyDataSourceCondition(queryWrapper, dataSource);
if (Func.isNotEmpty(ids)) {
queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
}
List<PortTerminalExportExcel> list = portTerminalService.exportPortTerminal(queryWrapper);
ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
}
/**
* 导出模板
*/
@GetMapping("/export-template")
@ApiOperationSupport(order = 9)
@Operation(summary = "导出模板")
public void exportTemplate(HttpServletResponse response) {
List<PortTerminalExcel> list = new ArrayList<>();
ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
}
private Query normalizeQuery(Query query) {
if (query == null) {
query = new Query();
}
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
query.setCurrent(DEFAULT_CURRENT);
}
if (query.getSize() == null || query.getSize() <= 0) {
query.setSize(DEFAULT_SIZE);
}
if (query.getSize() > MAX_SIZE) {
query.setSize(MAX_SIZE);
}
return query;
}
private void normalizeRegionCodeCondition(Map<String, Object> params) {
Object regionCode = params.remove("regionCode");
if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
params.put("districtCode", regionCode);
}
}
private void applyDataSourceCondition(QueryWrapper<PortTerminal> queryWrapper, Object dataSource) {
String value = Func.toStrWithEmpty(dataSource, "");
if (Func.isEmpty(value)) {
return;
}
if (SOURCE_INITIAL.equals(value)) {
queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD);
} else if (SOURCE_MANUAL.equals(value)) {
queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD);
} else {
queryWrapper.eq("data_source", value);
}
}
private String normalizeDataSource(String dataSource) {
if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) {
return SOURCE_INITIAL;
}
return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource;
}
}
/**
* 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.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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 jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.system.excel.ImportFailureException;
import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.excel.PortTerminalImporter;
import org.springblade.system.pojo.entity.PortTerminal;
import org.springblade.system.pojo.vo.PortTerminalVO;
import org.springblade.system.service.IPortTerminalService;
import org.springblade.system.wrapper.PortTerminalWrapper;
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 org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 港口码头主数据 控制器
*
* @author Chill
*/
@NonDS
@RestController
@AllArgsConstructor
@PreAuth(menu = "port_terminal")
@RequestMapping("/port-terminal")
@Tag(name = "港口码头主数据", description = "港口码头主数据")
public class PortTerminalController extends BladeController {
private static final int DEFAULT_CURRENT = 1;
private static final int DEFAULT_SIZE = 10;
private static final int MAX_SIZE = 100;
private static final String SOURCE_INITIAL = "初始化录入";
private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
private static final String SOURCE_INITIAL_OLD = "初始导入";
private static final String SOURCE_MANUAL = "手动录入";
private static final String SOURCE_MANUAL_OLD = "手工导入";
private final IPortTerminalService portTerminalService;
/**
* 详情
*/
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "详情", description = "传入portTerminal")
public R<PortTerminalVO> detail(PortTerminal portTerminal) {
if (Func.isEmpty(portTerminal.getId())) {
return R.fail("主键不能为空");
}
PortTerminal detail = portTerminalService.getById(portTerminal.getId());
if (Func.isEmpty(detail)) {
return R.fail("港口码头不存在");
}
detail.setDataSource(normalizeDataSource(detail.getDataSource()));
return R.data(PortTerminalWrapper.build().entityVO(detail));
}
/**
* 分页
*/
@GetMapping("/list")
@ApiOperationSupport(order = 2)
@Operation(summary = "分页", description = "传入portTerminal")
public R<IPage<PortTerminalVO>> list(PortTerminalVO portTerminal, Query query) {
IPage<PortTerminalVO> pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
return R.data(pages);
}
/**
* 新增或修改
*/
@PostMapping("/submit")
@ApiOperationSupport(order = 3)
@Operation(summary = "新增或修改", description = "传入portTerminal")
public R submit(@Valid @RequestBody PortTerminal portTerminal) {
return R.status(portTerminalService.submit(portTerminal));
}
/**
* 删除
*/
@PostMapping("/remove")
@ApiOperationSupport(order = 4)
@Operation(summary = "逻辑删除", description = "传入ids")
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
if (Func.isEmpty(ids)) {
return R.fail("主键不能为空");
}
return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
}
/**
* 启用或停用
*/
@PostMapping("/status")
@ApiOperationSupport(order = 5)
@Operation(summary = "启用或停用", description = "传入id和status")
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
@Parameter(description = "状态", required = true) @RequestParam Integer status) {
return R.status(portTerminalService.changeStatus(id, status));
}
/**
* 上级港口下拉数据源
*/
@GetMapping("/port-select")
@ApiOperationSupport(order = 6)
@Operation(summary = "上级港口下拉数据源")
public R<List<PortTerminal>> portSelect() {
List<PortTerminal> ports = portTerminalService.selectEnabledPorts();
ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource())));
return R.data(ports);
}
/**
* 导入港口码头主数据
*/
@PostMapping("/import-port-terminal")
@ApiOperationSupport(order = 7)
@Operation(summary = "导入港口码头主数据", description = "传入excel")
public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
} catch (ImportFailureException exception) {
// 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
exportFailure(response, exception.getFailureList());
return null;
}
return R.success("操作成功");
}
/**
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
* <p>
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
*/
private void exportFailure(HttpServletResponse response, List<?> failureList) {
ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
}
/**
* 导出港口码头主数据
*/
@GetMapping("/export-port-terminal")
@ApiOperationSupport(order = 8)
@Operation(summary = "导出港口码头主数据")
public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map<String, Object> portTerminal, HttpServletResponse response) {
Object ids = portTerminal.remove("ids");
Object dataSource = portTerminal.remove("dataSource");
portTerminal.remove("Blade-Auth");
portTerminal.remove("Authorization");
portTerminal.remove("access_token");
normalizeRegionCodeCondition(portTerminal);
QueryWrapper<PortTerminal> queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
applyDataSourceCondition(queryWrapper, dataSource);
if (Func.isNotEmpty(ids)) {
queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
}
List<PortTerminalExportExcel> list = portTerminalService.exportPortTerminal(queryWrapper);
ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
}
/**
* 导出模板
*/
@GetMapping("/export-template")
@ApiOperationSupport(order = 9)
@Operation(summary = "导出模板")
public void exportTemplate(HttpServletResponse response) {
List<PortTerminalExcel> list = new ArrayList<>();
ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
}
private Query normalizeQuery(Query query) {
if (query == null) {
query = new Query();
}
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
query.setCurrent(DEFAULT_CURRENT);
}
if (query.getSize() == null || query.getSize() <= 0) {
query.setSize(DEFAULT_SIZE);
}
if (query.getSize() > MAX_SIZE) {
query.setSize(MAX_SIZE);
}
return query;
}
private void normalizeRegionCodeCondition(Map<String, Object> params) {
Object regionCode = params.remove("regionCode");
if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
params.put("districtCode", regionCode);
}
}
private void applyDataSourceCondition(QueryWrapper<PortTerminal> queryWrapper, Object dataSource) {
String value = Func.toStrWithEmpty(dataSource, "");
if (Func.isEmpty(value)) {
return;
}
if (SOURCE_INITIAL.equals(value)) {
queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD);
} else if (SOURCE_MANUAL.equals(value)) {
queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD);
} else {
queryWrapper.eq("data_source", value);
}
}
private String normalizeDataSource(String dataSource) {
if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) {
return SOURCE_INITIAL;
}
return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource;
}
}
@@ -54,7 +54,9 @@ import org.springblade.core.tool.utils.StringPool;
import org.springblade.system.excel.UserExcel;
import org.springblade.system.excel.UserImporter;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.service.IOASyncService;
import org.springblade.system.service.IUserService;
import org.springblade.system.wrapper.UserWrapper;
import org.springframework.web.bind.annotation.*;
@@ -77,6 +79,7 @@ import java.util.Map;
public class UserController {
private final IUserService userService;
private final IOASyncService oaSyncService;
/**
* 查询单条
@@ -157,6 +160,19 @@ public class UserController {
return R.status(userService.submit(user));
}
/**
* 从OA按页同步人员,并按公司/部门生成组织后绑定到三级部门。
*/
@IsAdmin
@PostMapping("/sync-iam-accounts")
@ApiOperationSupport(order = 6)
@Operation(summary = "同步OA人员")
public R<OaPersonSyncPageVO> syncIamAccounts(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
return R.data(oaSyncService.syncPersonFromUserList(current, size));
}
/**
* 修改
*/
@@ -216,6 +232,20 @@ public class UserController {
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));
}
}
@@ -80,6 +80,9 @@ public interface DeptConvert {
}
// 公司所属公司编码就是他自己的部门编码
dept.setBelongCompanyCode(dept.getDeptCode());
// OA主键与上级OA主键,分批同步结束后用于重建层级
dept.setOaId(company.getId());
dept.setOaSupSubComId(company.getSupsubcomid());
// 显示顺序处理
dept.setSort(OAUtils.parseInt(company.getShoworder()));
// 部门类型,公司
@@ -100,6 +103,13 @@ public interface DeptConvert {
Dept dept = this.baseConvert(oaDept);
// 部门编码添加前缀
dept.setDeptCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + oaDept.getId());
// OA主键;上级为根部门时记录所属公司OA主键,否则记录上级部门OA主键
dept.setOaId(oaDept.getId());
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
dept.setOaSupSubComId(oaDept.getSubcompanyid1());
} else {
dept.setOaSupSubComId(oaDept.getSupdepid());
}
// 部门所属公司编码就是他所属公司编码加前缀
dept.setBelongCompanyCode(OAConvertConstant.COMPANY_OA_PREFIX + dept.getBelongCompanyCode());
String parentCode = dept.getParentCode();
@@ -64,11 +64,11 @@ public interface UserConvert {
@Mapping(target = "password", ignore = true)
@Mapping(target = "birthday", ignore = true)
@Mapping(target = "sex", ignore = true)
@Mapping(target = "account", ignore = true)
@Mapping(source = "workcode", target = "code")
@Mapping(source = "lastname", target = "name")
@Mapping(source = "lastname", target = "realName")
@Mapping(source = "mobile", target = "phone")
@Mapping(source = "mobile", target = "account")
@Mapping(source = "email", target = "email")
User baseConvert(OAPersonResponse person);
@@ -86,7 +86,7 @@ public interface UserConvert {
*/
default User person2user(OAPersonResponse person, String defaultPassword) {
User user = baseConvert(person);
user.setAccount(resolveAccount(person));
// 密码
user.setPassword(defaultPassword);
// 性别
@@ -102,6 +102,28 @@ public interface UserConvert {
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人员转本系统用户部门
* @param person
@@ -120,7 +142,7 @@ public interface UserConvert {
// 排序
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
// 用户id
userDept.setUserId(userMap.get(person.getMobile()));
userDept.setUserId(userMap.get(resolveAccount(person)));
userDept.setSyncTime(new Date());
return userDept;
}
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 币种汇率 Excel
@@ -62,8 +61,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("汇率")
private BigDecimal exchangeRate;
@ExcelProperty("生效日期")
private LocalDate effectiveDate;
@ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String effectiveDate;
@ExcelProperty("状态")
private String statusName;
@@ -71,8 +70,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("来源")
private String dataSource;
@ExcelProperty("失效日期")
private LocalDate expiryDate;
@ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String expiryDate;
@ExcelProperty("备注")
private String remark;
@@ -0,0 +1,60 @@
/**
* 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.excel;
import org.springblade.core.log.exception.ServiceException;
import java.io.Serial;
import java.util.List;
/**
* 导入失败异常,携带失败明细用于导出原表并标注错误。
* <p>
* 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
* 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
*
* @author Chill
*/
public class ImportFailureException extends ServiceException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 失败明细,包含原表全部数据,错误行已标注错误原因。
*/
private final transient List<?> failureList;
public ImportFailureException(List<?> failureList) {
super("导入失败,已回滚全部数据");
this.failureList = failureList;
}
public List<?> getFailureList() {
return failureList;
}
}
@@ -34,7 +34,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* UserExcel
@@ -102,7 +101,7 @@ public class UserExcel implements Serializable {
private String postName;
@ColumnWidth(20)
@ExcelProperty("生日")
private Date birthday;
@ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String birthday;
}
@@ -232,6 +232,12 @@ public class SysClient implements ISysClient {
.orderByAsc(CargoType::getCargoCode)));
}
@Override
@GetMapping(PERMISSIONS)
public R<List<String>> getPermissions(String roleId) {
return R.data(menuService.permissionCodes(roleId));
}
@Override
@GetMapping(INVOICE_ITEMS)
public R<List<InvoiceItem>> getInvoiceItems() {
@@ -133,4 +133,10 @@ public class UserClient implements IUserClient {
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));
}
}
@@ -13,6 +13,7 @@
<result column="update_time" property="updateTime"/>
<result column="status" property="status"/>
<result column="is_deleted" property="isDeleted"/>
<result column="unit_code" property="unitCode"/>
<result column="unit_name" property="unitName"/>
<result column="dimension" property="dimension"/>
<result column="remark" property="remark"/>
@@ -30,6 +31,7 @@
mmu.update_time,
mmu.status,
mmu.is_deleted,
mmu.unit_code,
mmu.unit_name,
mmu.dimension,
mmu.remark
@@ -39,6 +41,10 @@
LEFT JOIN blade_user uu ON uu.id = mmu.update_user
WHERE
mmu.is_deleted = 0
<if test="measurementUnit.unitCode != null and measurementUnit.unitCode != ''">
<bind name="unitCodeLike" value="'%' + measurementUnit.unitCode + '%'"/>
AND mmu.unit_code LIKE #{unitCodeLike}
</if>
<if test="measurementUnit.unitName != null and measurementUnit.unitName != ''">
<bind name="unitNameLike" value="'%' + measurementUnit.unitName + '%'"/>
AND mmu.unit_name LIKE #{unitNameLike}
@@ -95,7 +95,7 @@
</select>
<select id="exportUser" resultType="org.springblade.system.excel.UserExcel">
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, DATE_FORMAT(birthday, '%Y-%m-%d') AS birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
</select>
<select id="selectCustomerOptions" resultType="java.util.HashMap">
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* IAM账号同步配置。
*
* @author Chill
*/
@Data
@ConfigurationProperties(prefix = "iam.sync")
public class IamSyncProperties {
/** IAM增量账号接口地址。 */
private String accountListUrl;
/** IAM组织接口地址。 */
private String orgListUrl;
/** IAM接口Authorization请求头。 */
private String authorization;
/** IAM接口Auth请求头。 */
private String profileAuthorization;
/** 单页请求数量。 */
private int pageSize = 50;
}
@@ -146,6 +146,13 @@ public interface IDeptService extends IService<Dept> {
*/
boolean removeDept(String ids);
/**
* 删除非顶级组织,顶级组织(父级为 0)保留
*
* @return 删除数量
*/
int clearNonTopDepts();
/**
* 新增或修改部门
* <p>
@@ -156,6 +163,13 @@ public interface IDeptService extends IService<Dept> {
*/
boolean submit(Dept dept);
/**
* 从IAM同步管理租户组织。
* @return 同步处理的组织数量
*/
int syncIamOrganizations();
/**
* 按名称与父级查询部门列表(限定当前会话租户)
*
@@ -180,4 +194,16 @@ public interface IDeptService extends IService<Dept> {
*/
void updateAncestors(Date startTime);
/**
* 按 OA 主键与上级 OA 主键重建组织、部门父子关系,并刷新祖级列表
*/
void rebuildOaHierarchy();
/**
* 删除 OA 同步后无法挂到上级的组织(上级 OA 主键在本地不存在),并级联删除其子级
*
* @return 删除数量
*/
int removeOaOrphansWithoutParent();
}
@@ -77,6 +77,14 @@ public interface IMenuService extends IService<Menu> {
*/
List<MenuVO> buttons(String roleId);
/**
* 权限标识集合(按钮编号,与前端 GetButtons 叶子 code 一致)
*
* @param roleId 角色id
* @return 权限标识
*/
List<String> permissionCodes(String roleId);
/**
* 树形结构
*
@@ -1,5 +1,8 @@
package org.springblade.system.service;
import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
/**
* oa同步接口
* @author bfhuange
@@ -18,4 +21,38 @@ public interface IOASyncService {
* @param syncAll 是否同步所有
*/
void syncPersonAndPushMK(boolean syncAll);
/**
* 从 OA 人员接口全量同步组织与人员,不推送 MK
*
* @return 处理的人员数量
*/
int syncPersonFromUserList();
/**
* 按页从 OA 人员接口同步组织与人员
*
* @param current 当前页,从 1 开始
* @param size 每页条数
* @return 本页同步结果
*/
OaPersonSyncPageVO syncPersonFromUserList(int current, int size);
/**
* 按页从 OA 公司接口同步公司
*
* @param current 当前页,从 1 开始
* @param size 每页条数
* @return 本页同步结果
*/
OaOrgSyncPageVO syncCompanyPage(int current, int size);
/**
* 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表
*
* @param current 当前页,从 1 开始
* @param size 每页条数
* @return 本页同步结果
*/
OaOrgSyncPageVO syncDepartmentPage(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);
}
@@ -78,6 +78,13 @@ public interface IUserService extends BaseService<User> {
*/
boolean submit(User user);
/**
* 从IAM同步管理租户账号。
*
* @return 同步处理的账号数量
*/
int syncIamAccounts();
/**
* 修改用户(租户守卫校验用户归属,含账号 / 手机查重)
*
@@ -181,6 +188,11 @@ public interface IUserService extends BaseService<User> {
*/
UserInfo userInfo(UserOauth userOauth);
/**
* 绑定微信小程序 openid 到已有用户(blade_user_oauth,source=WECHAT_MINI)
*/
boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone);
/**
* 根据租户与账号获取用户
*
@@ -281,7 +293,7 @@ public interface IUserService extends BaseService<User> {
boolean registerUser(User user);
/**
* 新建IAM统一身份认证用户(按可信租户落库)
* 新建或补齐IAM统一身份认证用户(按可信租户落库,默认分配角色 1123598816738675203)
*
* @param user 用户实体
* @return 是否成功
@@ -125,6 +125,8 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
CurrencyExcel excel = data.get(index);
try {
Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class));
currency.setEffectiveDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEffectiveDate(), "生效日期"));
currency.setExpiryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpiryDate(), "失效日期"));
currency.setDataSource(SOURCE_BATCH);
currency.setStatus(STATUS_ENABLED);
prepare(currency, SOURCE_BATCH);
@@ -29,8 +29,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.constant.DataStatusEnum;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.secure.utils.AuthUtil;
@@ -42,16 +46,26 @@ import org.springblade.system.cache.SysCache;
import org.springblade.system.mapper.DeptMapper;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.enums.DeptCategory;
import org.springblade.system.pojo.vo.DeptVO;
import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.props.IamSyncProperties;
import org.springblade.system.service.IDeptService;
import org.springblade.system.service.IUserService;
import org.springblade.system.wrapper.DeptWrapper;
import org.springblade.system.wrapper.UserWrapper;
import org.springblade.thirdparty.oa.constant.OAConstant;
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -59,6 +73,7 @@ import java.util.stream.Collectors;
import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT;
import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT_PARENT;
import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
/**
* 服务实现类
@@ -72,8 +87,13 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
private static final String TENANT_ID = "tenantId";
private static final String PARENT_ID = "parentId";
private static final String IAM_SYNC_TENANT_ID = "000000";
private static final Long IAM_SYNC_PARENT_ID = 1123598813738675201L;
private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
private final IUserService userService;
private final IamSyncProperties iamSyncProperties;
private final ObjectMapper objectMapper;
@Override
public Dept getDetail(Dept dept) {
@@ -214,6 +234,21 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return removeByIds(idList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int clearNonTopDepts() {
Long nonTopCount = this.count(Wrappers.<Dept>lambdaQuery()
.isNotNull(Dept::getParentId)
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
if (nonTopCount == null || nonTopCount == 0L) {
return 0;
}
this.remove(Wrappers.<Dept>lambdaQuery()
.isNotNull(Dept::getParentId)
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
return nonTopCount.intValue();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(Dept dept) {
@@ -255,9 +290,190 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return saveOrUpdate(dept);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int syncIamOrganizations() {
int pageNumber = 1;
int fetchedCount = 0;
int syncedCount = 0;
int totalCount = -1;
int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50;
while (true) {
JsonNode dataNode = requestIamOrgPage(pageNumber, pageSize);
JsonNode orgList = dataNode.path("list");
if (!orgList.isArray() || orgList.isEmpty()) {
break;
}
if (dataNode.has("total")) {
totalCount = dataNode.path("total").asInt(totalCount);
}
for (JsonNode orgNode : orgList) {
if (syncIamOrganization(orgNode)) {
syncedCount++;
}
}
fetchedCount += orgList.size();
int responsePage = dataNode.path("page").asInt(pageNumber);
int responseSize = dataNode.path("size").asInt(pageSize);
if ((totalCount >= 0 && fetchedCount >= totalCount)
|| orgList.size() < pageSize
|| (totalCount >= 0 && responsePage * responseSize >= totalCount)) {
break;
}
pageNumber = responsePage + 1;
}
log.info("IAM组织同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount);
return syncedCount;
}
private JsonNode requestIamOrgPage(int pageNumber, int pageSize) {
try {
Map<String, String> requestBody = new LinkedHashMap<>();
requestBody.put("size", String.valueOf(pageSize));
requestBody.put("page", String.valueOf(pageNumber));
HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getOrgListUrl()))
.timeout(Duration.ofSeconds(20))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization()))
.header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization()))
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new ServiceException(StringUtil.format("IAM组织接口调用失败,HTTP状态码:{}", response.statusCode()));
}
JsonNode responseNode = objectMapper.readTree(response.body());
if (!"0".equals(responseNode.path("code").asText())) {
throw new ServiceException(StringUtil.format("IAM组织接口调用失败:{}", responseNode.path("msg").asText()));
}
JsonNode dataNode = responseNode.path("data");
if (!dataNode.isObject()) {
throw new ServiceException("IAM组织接口返回数据格式错误");
}
return dataNode;
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
log.error("调用IAM组织接口被中断,page={}", pageNumber, exception);
throw new ServiceException("调用IAM组织接口被中断");
} catch (IOException | IllegalArgumentException exception) {
log.error("调用IAM组织接口失败,page={}", pageNumber, exception);
throw new ServiceException("调用IAM组织接口失败");
}
}
private boolean syncIamOrganization(JsonNode orgNode) {
String orgCode = readIamText(orgNode, "orgCode", "org_code", "organizationCode", "organization_code",
"code", "app_org__org_code", "app_org__org_no", "app_org__organization_code", "app_org__code");
String orgId = readIamText(orgNode, "orgId", "org_id", "id", "app_org__id", "app_org__org_id");
if (StringUtil.isBlank(orgCode)) {
orgCode = orgId;
}
if (StringUtil.isBlank(orgCode)) {
log.warn("IAM组织缺少组织编码,跳过同步");
return false;
}
if (orgCode.length() > 30) {
log.warn("IAM组织编码超过30个字符,跳过同步,orgCode={}", orgCode);
return false;
}
String orgName = readIamText(orgNode, "name", "orgName", "org_name", "organizationName", "organization_name",
"fullName", "app_org__name", "app_org__org_name", "app_org__org_full_name", "app_org__organization_name");
if (StringUtil.isBlank(orgName)) {
log.warn("IAM组织缺少组织名称,跳过同步,orgCode={}", orgCode);
return false;
}
Integer status = readIamInt(orgNode, "status", "org_status", "app_org__status", "app_org__org_status") == 1
? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode();
Dept dept = getOne(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getTenantId, IAM_SYNC_TENANT_ID)
.eq(Dept::getDeptCode, orgCode), false);
if (dept == null) {
dept = new Dept();
dept.setTenantId(IAM_SYNC_TENANT_ID);
dept.setParentId(IAM_SYNC_PARENT_ID);
dept.setAncestors(resolveIamParentAncestors());
dept.setDeptCode(orgCode);
dept.setDeptName(orgName);
dept.setFullName(orgName);
dept.setShortName(orgName);
dept.setDeptCategory(1);
dept.setSort(0);
dept.setStatus(status);
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
dept.setIsOa(1);
dept.setIsPlatformCompany(0);
dept.setSyncTime(new Date());
boolean saved = save(dept);
if (saved) {
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
}
return saved;
}
boolean changed = !Objects.equals(dept.getDeptName(), orgName) || !Objects.equals(dept.getFullName(), orgName)
|| !Objects.equals(dept.getShortName(), orgName) || !Objects.equals(dept.getStatus(), status)
|| !Objects.equals(dept.getIsOa(), 1);
if (!changed) {
return true;
}
dept.setDeptName(orgName);
dept.setFullName(orgName);
dept.setShortName(orgName);
dept.setStatus(status);
dept.setIsOa(1);
dept.setSyncTime(new Date());
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
return updateById(dept);
}
private String resolveIamParentAncestors() {
Dept parent = getById(IAM_SYNC_PARENT_ID);
String ancestors = parent == null ? String.valueOf(BladeConstant.TOP_PARENT_ID) : parent.getAncestors();
if (StringUtil.isBlank(ancestors)) {
ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID);
}
return ancestors + StringPool.COMMA + IAM_SYNC_PARENT_ID;
}
private String readIamText(JsonNode node, String... fieldNames) {
JsonNode valueNode = findIamNode(node, fieldNames);
return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim();
}
private int readIamInt(JsonNode node, String... fieldNames) {
JsonNode valueNode = findIamNode(node, fieldNames);
return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0);
}
private JsonNode findIamNode(JsonNode node, String... fieldNames) {
for (String fieldName : fieldNames) {
JsonNode valueNode = node.get(fieldName);
if (valueNode != null && !valueNode.isNull()) {
return valueNode;
}
}
return null;
}
private String normalizeAuthorizationHeader(String value) {
if (StringUtil.isBlank(value)) {
return StringPool.EMPTY;
}
if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) {
return value;
}
return "Basic " + value;
}
private void validateDeptCategory(Dept dept, Dept parent) {
// 顶级组织不要求选择上级组织
if (parent == null) {
throw new ServiceException("请选择上级组织");
if (Integer.valueOf(6).equals(dept.getDeptCategory()) && Func.isEmpty(dept.getCarrierCustomerId())) {
throw new ServiceException("请选择承运商");
}
return;
}
List<Integer> allowedCategories;
if (BladeConstant.TOP_PARENT_ID.equals(parent.getParentId())) {
@@ -288,13 +504,24 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (deptCode.length() > 30) {
throw new ServiceException("组织编码不能超过30个字符");
}
if (parent == null || StringUtil.isBlank(parent.getDeptCode())) {
// 顶级组织不依赖上级编码
if (parent == null) {
this.ensureDeptCodeUnique(dept, deptCode);
dept.setDeptCode(deptCode);
return;
}
if (StringUtil.isBlank(parent.getDeptCode())) {
throw new ServiceException("请选择已配置组织编码的上级组织");
}
String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
if (!deptCode.matches(pattern)) {
throw new ServiceException("组织编码格式应为:上级编码-分段数字");
}
this.ensureDeptCodeUnique(dept, deptCode);
dept.setDeptCode(deptCode);
}
private void ensureDeptCodeUnique(Dept dept, String deptCode) {
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery().eq(Dept::getDeptCode, deptCode);
if (Func.isNotEmpty(dept.getId())) {
queryWrapper.ne(Dept::getId, dept.getId());
@@ -302,7 +529,6 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (baseMapper.selectCount(queryWrapper) > 0) {
throw new ServiceException("组织编码已存在");
}
dept.setDeptCode(deptCode);
}
@Override
@@ -345,24 +571,243 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return UserWrapper.build().listVO(userList);
}
/**
* 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
*/
private void applyTenantScope(QueryWrapper<Dept> queryWrapper) {
if (!AuthUtil.isAdministrator()) {
queryWrapper.lambda().eq(Dept::getTenantId, AuthUtil.getTenantId());
}
}
@Transactional(rollbackFor = Exception.class)
@Override
public void updateAncestors(Date startTime) {
// 1. 查询需要更新祖级列表的部门
List<Dept> list = this.list(Wrappers.<Dept>lambdaQuery()
// 祖级列表为空
this.refreshAncestors(false, startTime);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void rebuildOaHierarchy() {
List<Dept> oaDeptList = this.list(Wrappers.<Dept>lambdaQuery()
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, ""));
if (CollectionUtil.isEmpty(oaDeptList)) {
log.info("没有记录OA主键的组织,跳过层级重建");
return;
}
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
Map<Long, Long> desiredParentIdMap = new LinkedHashMap<>();
Map<Long, String> desiredParentCodeMap = new HashMap<>();
for (Dept dept : oaDeptList) {
OaParentLink parentLink = this.resolveOaParentLink(dept, companyByOaId, departmentByOaId);
if (parentLink == null) {
continue;
}
desiredParentIdMap.put(dept.getId(), parentLink.parentId());
desiredParentCodeMap.put(dept.getId(), parentLink.parentCode());
}
Set<Long> cyclicIds = this.findCyclicDeptIds(desiredParentIdMap);
if (CollectionUtil.isNotEmpty(cyclicIds)) {
log.warn("检测到OA组织循环引用,跳过层级重建 ids={}", cyclicIds);
cyclicIds.forEach(cyclicId -> {
desiredParentIdMap.remove(cyclicId);
desiredParentCodeMap.remove(cyclicId);
});
}
List<Dept> updateList = new ArrayList<>();
for (Dept dept : oaDeptList) {
Long parentId = desiredParentIdMap.get(dept.getId());
if (parentId == null) {
continue;
}
String parentCode = desiredParentCodeMap.get(dept.getId());
if (Objects.equals(parentId, dept.getParentId()) && StringUtil.equals(parentCode, dept.getParentCode())) {
continue;
}
Dept updateParam = new Dept();
updateParam.setId(dept.getId());
updateParam.setParentId(parentId);
updateParam.setParentCode(parentCode);
updateList.add(updateParam);
}
if (CollectionUtil.isNotEmpty(updateList)) {
this.updateBatchById(updateList);
log.info("按OA主键重建组织层级,更新父子关系{}条", updateList.size());
} else {
log.info("OA组织层级已是最新,无需调整父子关系");
}
this.refreshAncestors(true, null);
}
@Transactional(rollbackFor = Exception.class)
@Override
public int removeOaOrphansWithoutParent() {
List<Dept> oaDeptList = this.list(Wrappers.<Dept>lambdaQuery()
.isNotNull(Dept::getOaId)
.ne(Dept::getOaId, ""));
if (CollectionUtil.isEmpty(oaDeptList)) {
return 0;
}
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
Set<Long> deleteIds = new HashSet<>();
for (Dept dept : oaDeptList) {
String parentOaId = dept.getOaSupSubComId();
// 上级为空或为 0:视为本地锚点/根级,不作为孤儿删除
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
continue;
}
boolean parentExists;
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
parentExists = companyByOaId.containsKey(parentOaId);
} else {
parentExists = departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
}
if (!parentExists) {
deleteIds.add(dept.getId());
}
}
if (CollectionUtil.isEmpty(deleteIds)) {
log.info("没有需要清理的OA孤儿组织");
return 0;
}
// 按 OA 上级关系级联:上级被删则子级一并删除
boolean oaCascadeChanged = true;
while (oaCascadeChanged) {
oaCascadeChanged = false;
for (Dept dept : oaDeptList) {
if (deleteIds.contains(dept.getId())) {
continue;
}
String parentOaId = dept.getOaSupSubComId();
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
continue;
}
Dept parent = DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())
? companyByOaId.get(parentOaId)
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
if (parent != null && deleteIds.contains(parent.getId())) {
deleteIds.add(dept.getId());
oaCascadeChanged = true;
}
}
}
// 按本地 parent_id 再级联一轮,清掉挂在已删节点下的子树
List<Dept> allDeptList = this.list();
boolean parentIdCascadeChanged = true;
while (parentIdCascadeChanged) {
parentIdCascadeChanged = false;
for (Dept dept : allDeptList) {
if (deleteIds.contains(dept.getId())) {
continue;
}
if (dept.getParentId() != null && deleteIds.contains(dept.getParentId())) {
deleteIds.add(dept.getId());
parentIdCascadeChanged = true;
}
}
}
this.removeByIds(deleteIds);
log.info("删除OA层级无法挂载的组织{}条", deleteIds.size());
return deleteIds.size();
}
/**
* 按机构类型建立 OA 主键索引
*/
private Map<String, Dept> indexByOaId(List<Dept> oaDeptList, Integer deptCategory) {
Map<String, Dept> oaIdMap = new HashMap<>();
for (Dept dept : oaDeptList) {
if (!Objects.equals(deptCategory, dept.getDeptCategory()) || StringUtil.isBlank(dept.getOaId())) {
continue;
}
Dept previous = oaIdMap.put(dept.getOaId(), dept);
if (previous != null) {
log.warn("OA主键重复,层级重建使用后一条 category={} oaId={} id={} previousId={}",
dept.getDeptCategory(), dept.getOaId(), dept.getId(), previous.getId());
}
}
return oaIdMap;
}
/**
* 根据 OA 上级主键解析本系统父级
*/
private OaParentLink resolveOaParentLink(Dept dept, Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
String parentOaId = dept.getOaSupSubComId();
boolean rootParent = StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId);
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
if (rootParent) {
return new OaParentLink(OAConvertConstant.ROOT_PARENT_ID, OAConstant.ROOT_COMPANY_ID);
}
Dept parent = companyByOaId.get(parentOaId);
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
log.warn("未找到上级公司,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
dept.getOaId(), parentOaId, dept.getId());
return null;
}
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, OAConvertConstant.COMPANY_OA_PREFIX, parentOaId));
}
if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
return null;
}
if (rootParent) {
log.warn("部门缺少上级OA主键,跳过层级重建 oaId={} deptId={}", dept.getOaId(), dept.getId());
return null;
}
boolean parentIsDepartment = StringUtil.isNotBlank(dept.getParentCode())
&& dept.getParentCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
boolean parentIsCompany = StringUtil.isNotBlank(dept.getParentCode())
&& dept.getParentCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX);
Dept parent;
if (parentIsDepartment) {
parent = departmentByOaId.get(parentOaId);
} else if (parentIsCompany) {
parent = companyByOaId.get(parentOaId);
} else {
parent = departmentByOaId.get(parentOaId);
if (parent == null) {
parent = companyByOaId.get(parentOaId);
}
}
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
log.warn("未找到上级组织,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
dept.getOaId(), parentOaId, dept.getId());
return null;
}
String prefix = DeptCategory.DEPT.getCode().equals(parent.getDeptCategory())
? OAConvertConstant.DEPARTMENT_OA_PREFIX : OAConvertConstant.COMPANY_OA_PREFIX;
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, prefix, parentOaId));
}
private String resolveParentCode(Dept parent, String prefix, String parentOaId) {
if (StringUtil.isNotBlank(parent.getDeptCode())) {
return parent.getDeptCode();
}
return prefix + parentOaId;
}
/**
* 找出父子关系成环的部门
*/
private Set<Long> findCyclicDeptIds(Map<Long, Long> parentIdMap) {
Set<Long> cyclicIds = new HashSet<>();
for (Long deptId : parentIdMap.keySet()) {
Set<Long> visiting = new HashSet<>();
Long currentId = deptId;
while (currentId != null && !OAConvertConstant.ROOT_PARENT_ID.equals(currentId)) {
if (!visiting.add(currentId)) {
cyclicIds.addAll(visiting);
break;
}
currentId = parentIdMap.get(currentId);
}
}
return cyclicIds;
}
/**
* 刷新祖级列表
*
* @param refreshAll 是否刷新全部部门
* @param startTime 增量起始时间,仅 refreshAll 为 false 时生效
*/
private void refreshAncestors(boolean refreshAll, Date startTime) {
List<Dept> list = refreshAll ? this.list() : this.list(Wrappers.<Dept>lambdaQuery()
.isNull(Dept::getAncestors)
// 开始时间不为空,查询同步时间大于开始时间或修改时间大于开始时间的数据
.or(startTime != null, wrapper -> wrapper.ge(Dept::getSyncTime, startTime))
.or(startTime != null, wrapper -> wrapper.ge(Dept::getUpdateTime, startTime))
);
@@ -370,29 +815,19 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
log.info("需要更新祖级列表的部门为空");
return;
}
// 所有公司部门列表
List<DeptVO> allList = this.list().stream()
List<DeptVO> allList = (refreshAll ? list : this.list()).stream()
.map(dept -> BeanUtil.copyProperties(dept, DeptVO.class))
.toList();
// id对应的部门map
Map<Long, DeptVO> idDeptMap = allList.stream()
.collect(Collectors.toMap(DeptVO::getId, Function.identity(), (a, b) -> b));
// 获取根节点列表
List<DeptVO> rootList = getRootList(allList, idDeptMap);
// 计算祖级列表
setAncestors(rootList);
// 3. 更新祖级列表
List<Dept> updateParams = list.stream()
.filter(dept -> {
DeptVO vo = idDeptMap.get(dept.getId());
if (vo == null) {
if (vo == null || vo.getAncestors() == null) {
return false;
}
if (vo.getAncestors() == null) {
return false;
}
// 祖级列表和数据库的不一样的才需要更新
return !StringUtil.equals(vo.getAncestors(), dept.getAncestors());
})
.map(dept -> {
@@ -407,6 +842,21 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
}
}
/**
* OA 上级链接
*/
private record OaParentLink(Long parentId, String parentCode) {
}
/**
* 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
*/
private void applyTenantScope(QueryWrapper<Dept> queryWrapper) {
if (!AuthUtil.isAdministrator()) {
queryWrapper.lambda().eq(Dept::getTenantId, AuthUtil.getTenantId());
}
}
/**
* 获取根节点列表
* @param allList
@@ -46,6 +46,7 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
private static final int STATUS_ENABLED = 1;
private static final int STATUS_DISABLED = 2;
private static final int UNIT_CODE_MAX_LENGTH = 50;
private static final int UNIT_NAME_MAX_LENGTH = 50;
private static final int DIMENSION_MAX_LENGTH = 20;
private static final int REMARK_MAX_LENGTH = 200;
@@ -85,6 +86,7 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
}
private void prepare(MeasurementUnit measurementUnit) {
measurementUnit.setUnitCode(trimToEmpty(measurementUnit.getUnitCode()));
measurementUnit.setUnitName(trimToEmpty(measurementUnit.getUnitName()));
measurementUnit.setDimension(trimToEmpty(measurementUnit.getDimension()));
measurementUnit.setRemark(trimToNull(measurementUnit.getRemark()));
@@ -94,6 +96,12 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
}
private void validate(MeasurementUnit measurementUnit) {
if (Func.isEmpty(measurementUnit.getUnitCode())) {
throw new ServiceException("计量单位编码不能为空");
}
if (measurementUnit.getUnitCode().length() > UNIT_CODE_MAX_LENGTH) {
throw new ServiceException("计量单位编码不能超过50字");
}
if (Func.isEmpty(measurementUnit.getUnitName())) {
throw new ServiceException("计量单位不能为空");
}
@@ -115,9 +123,22 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
&& !Objects.equals(measurementUnit.getStatus(), STATUS_DISABLED)) {
throw new ServiceException("启停状态不正确");
}
validateUniqueUnitCode(measurementUnit);
validateUniqueUnitName(measurementUnit);
}
private void validateUniqueUnitCode(MeasurementUnit measurementUnit) {
LambdaQueryWrapper<MeasurementUnit> queryWrapper = Wrappers.<MeasurementUnit>lambdaQuery()
.eq(MeasurementUnit::getUnitCode, measurementUnit.getUnitCode())
.eq(MeasurementUnit::getIsDeleted, 0);
if (Func.isNotEmpty(measurementUnit.getId())) {
queryWrapper.ne(MeasurementUnit::getId, measurementUnit.getId());
}
if (count(queryWrapper) > 0L) {
throw new ServiceException("该计量单位编码已存在");
}
}
private void validateUniqueUnitName(MeasurementUnit measurementUnit) {
LambdaQueryWrapper<MeasurementUnit> queryWrapper = Wrappers.<MeasurementUnit>lambdaQuery()
.eq(MeasurementUnit::getUnitName, measurementUnit.getUnitName())
@@ -164,6 +164,35 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IM
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
public List<TreeNode> tree() {
return ForestNodeMerger.merge(baseMapper.tree());
@@ -11,16 +11,15 @@ import org.springblade.common.constant.DictTypeEnum;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
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.ParamCache;
import org.springblade.system.convert.DeptConvert;
import org.springblade.system.convert.UserConvert;
import org.springblade.system.log.ComposeLogUtil;
import org.springblade.system.pojo.entity.*;
import org.springblade.system.pojo.enums.DataSync;
import org.springblade.system.pojo.enums.DeptCategory;
import org.springblade.system.pojo.vo.UserDeptIdsVO;
import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
import org.springblade.system.service.*;
import org.springblade.system.util.DataSyncRecordUtils;
import org.springblade.thirdparty.oa.constant.OAConstant;
@@ -29,6 +28,8 @@ import org.springblade.thirdparty.oa.feign.IOAClient;
import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse;
import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse;
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.OADepartmentSearch;
import org.springblade.thirdparty.oa.pojo.search.OAPersonSearch;
@@ -38,6 +39,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -54,20 +56,14 @@ import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
@RequiredArgsConstructor
@Service
public class OASyncServiceImpl implements IOASyncService {
/**
* 用户表部门id最大长度
*/
private static final int MAX_DEPT_ID_LENGTH = 2000;
private final IOAClient oaClient;
private final DeptConvert deptConvert;
private final IDeptService deptService;
private final IUserService userService;
private final UserConvert userConvert;
private final IUserDeptService userDeptService;
private final IRoleService roleService;
private final IMKPushService mkPushService;
private final IDataSyncRecordService dataSyncRecordService;
private final OaUserListSyncHelper oaUserListSyncHelper;
@Transactional(rollbackFor = Exception.class)
@Override
@@ -78,10 +74,18 @@ public class OASyncServiceImpl implements IOASyncService {
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
// 1. 同步公司
this.syncCompany(queryStartTime);
// 公司分批入库后,重建层级并清理无法挂载的公司
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
// 2. 同步部门
this.syncDept(queryStartTime);
// 2. 更新部门祖级列表
// 3. 分批写入完成后,按 OA 主键重建组织、部门层级
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
// 4. 更新部门祖级列表
deptService.updateAncestors(queryStartTime);
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
}, syncAll);
// 2. 推送mk
mkPushService.pushOrgAndRecord(startTime);
@@ -104,6 +108,53 @@ 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();
}
}
@Transactional(rollbackFor = Exception.class)
@Override
public OaOrgSyncPageVO syncCompanyPage(int current, int size) {
try {
ComposeLogUtil.addLog(log);
return this.syncCompanyFromOaPage(current, size);
} finally {
ComposeLogUtil.removeLastLog();
}
}
@Transactional(rollbackFor = Exception.class)
@Override
public OaOrgSyncPageVO syncDepartmentPage(int current, int size) {
try {
ComposeLogUtil.addLog(log);
return this.syncDepartmentFromOaPage(current, size);
} finally {
ComposeLogUtil.removeLastLog();
}
}
/**
* 同步并记录
*
@@ -146,20 +197,15 @@ public class OASyncServiceImpl implements IOASyncService {
// 未处理的数据
List<Dept> notHandleList = new ArrayList<>();
// 1. 设置查询参数
OACompanySearch companySearch = new OACompanySearch();
companySearch.setCurPage(1);
if (startTime != null) {
// 开始时间不为空,设置修改时间参数
companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
}
OACompanySearch companySearch = buildCompanySearch(startTime);
// 2. 分页查询并处理数据
OAUtils.pageSyncHandler(companySearch, param -> oaClient.queryCompanyPage(new OASearch<>(param)), response -> {
ComposeLogUtil.getLastLog().error("调用OA接口查询公司信息失败 {}", JSON.toJSONString(response));
return new ServiceException("调用OA接口查询公司信息失败");
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
// 处理数据
List<Dept> deptList = handleCompany(list);
notHandleList.addAll(deptList);
OrgSyncCount syncCount = handleCompany(list, false);
notHandleList.addAll(syncCount.getNotHandledList());
});
// 3. 未处理的数据
if (CollectionUtil.isNotEmpty(notHandleList)) {
@@ -175,7 +221,7 @@ public class OASyncServiceImpl implements IOASyncService {
* @param startTime 查询开始时间
*/
private void syncDept(Date startTime) {
String subCompanyIds = getSubCompanyIds();
String subCompanyIds = getOaSubCompanyIds(false);
if (StringUtils.isEmpty(subCompanyIds)) {
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
return;
@@ -183,21 +229,15 @@ public class OASyncServiceImpl implements IOASyncService {
// 未处理的数据
List<Dept> notHandleList = new ArrayList<>();
// 1. 设置查询参数
OADepartmentSearch departmentSearch = new OADepartmentSearch();
departmentSearch.setCurPage(1);
departmentSearch.setSubcompanyid1(subCompanyIds);
if (startTime != null) {
// 开始时间不为空,设置修改时间参数
departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
}
OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, subCompanyIds);
// 2. 分页查询并处理数据
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(response));
return new ServiceException("调用OA接口查询部门信息失败");
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
// 处理数据
List<Dept> deptList = handleDept(list);
notHandleList.addAll(deptList);
OrgSyncCount syncCount = handleDept(list, false);
notHandleList.addAll(syncCount.getNotHandledList());
});
// 3. 未处理的数据
if (CollectionUtil.isNotEmpty(notHandleList)) {
@@ -213,242 +253,325 @@ public class OASyncServiceImpl implements IOASyncService {
* @param startTime 查询开始时间
*/
private void syncPerson(Date startTime) {
String subCompanyIds = getSubCompanyIds();
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);
this.syncPersonFromOa(startTime);
}
/**
* 处理oa人员
* @param oaPersons
* 从 OA 人员列表同步组织与人员
*
* @param startTime 增量查询开始时间,为空则全量
* @return 处理的人员数量
*/
private void handlePerson(List<OAPersonResponse> oaPersons) {
if (CollectionUtil.isEmpty(oaPersons)) {
// 数据为空,直接返回
return;
}
// 根公司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());
}
});
private int syncPersonFromOa(Date startTime) {
OAPersonSearch personSearch = buildPersonSearch(startTime);
List<OAPersonResponse> oaPersons = new ArrayList<>();
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();
}
// 不存在的新增
List<User> addUsers = users.stream()
.filter(user -> !existsUserIds.contains(user.getId()))
.toList();
if (CollectionUtil.isNotEmpty(addUsers)) {
ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size());
userService.saveBatch(addUsers);
/**
* 按页从 OA 人员列表同步组织与人员
*
* @param current 当前页
* @param size 每页条数
* @return 本页同步结果
*/
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : 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接口查询人员信息失败");
}
// 存在的修改
List<User> updateUsers = users.stream()
.filter(user -> existsUserIds.contains(user.getId()))
.toList();
if (CollectionUtil.isNotEmpty(updateUsers)) {
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
userService.updateBatchById(updateUsers);
}
// 没有手机号的数据 = 手机号为空的数量
long noPhoneNum = oaPersons.stream()
.map(OAPersonResponse::getMobile)
.filter(StringUtils::isBlank)
.count();
ComposeLogUtil.getLastLog().info("没有手机号的数据:{}", noPhoneNum);
OAResponseData<OAPersonResponse> responseData = oaResponse.getData();
List<OAPersonResponse> oaPersons = responseData.getDataList() == null
? Collections.emptyList() : responseData.getDataList();
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
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);
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;
}
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
.in(UserDept::getUserId, userMap.values())
).stream()
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (a, b) -> b));
// 数据库存在的所有用户部门id
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
/**
* 按页从 OA 公司接口同步公司
*
* @param current 当前页
* @param size 每页条数
* @return 本页同步结果
*/
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
OACompanySearch companySearch = buildCompanySearch(null);
companySearch.setCurPage(pageNo);
companySearch.setPageSize(pageSize);
OAResponse<OACompanyResponse> oaResponse = oaClient.queryCompanyPage(new OASearch<>(companySearch));
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<OACompanyResponse> responseData = oaResponse.getData();
List<OACompanyResponse> oaCompanies = responseData.getDataList() == null
? Collections.emptyList() : responseData.getDataList();
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
OrgSyncCount syncCount = handleCompany(oaCompanies, true);
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
}
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount);
if (Boolean.TRUE.equals(pageVO.getFinished())) {
// 公司全部分批入库后:重建层级,并删除上级不存在的数据
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
}
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
ComposeLogUtil.getLastLog().info("OA公司分页同步完成 {}/{},成功{},跳过{}",
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
return pageVO;
}
List<UserDept> userDeptList = oaPersons.stream()
// 只要包含用户手机号的
.filter(oaPerson -> userMap.containsKey(oaPerson.getMobile()))
.map(oaPerson -> userConvert.person2userDept(oaPerson, userMap))
.toList();
userDeptList.forEach(userDept -> {
String userDeptKey = getUserDeptKey(userDept);
if (userDeptMap.containsKey(userDeptKey)) {
// 根据key获取用户部门id
userDept.setId(userDeptMap.get(userDeptKey));
} else {
// 没有就生成一个id
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);
/**
* 按页从 OA 部门接口同步部门
*
* @param current 当前页
* @param size 每页条数
* @return 本页同步结果
*/
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
String subCompanyIds = getOaSubCompanyIds(true);
if (StringUtils.isEmpty(subCompanyIds)) {
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
emptyPageVO.setFinished(true);
return emptyPageVO;
}
// 存在的修改
List<UserDept> updateList = userDeptList.stream()
.filter(userDept -> existsUserDeptIds.contains(userDept.getId()))
.toList();
if (CollectionUtil.isNotEmpty(updateList)) {
ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size());
userDeptService.updateBatchById(updateList);
OADepartmentSearch departmentSearch = buildDepartmentSearch(null, subCompanyIds);
departmentSearch.setCurPage(pageNo);
departmentSearch.setPageSize(pageSize);
OAResponse<OADepartmentResponse> oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch));
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(oaResponse));
throw new ServiceException("调用OA接口查询部门信息失败");
}
// 回写部门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);
OAResponseData<OADepartmentResponse> responseData = oaResponse.getData();
List<OADepartmentResponse> oaDepartments = responseData.getDataList() == null
? Collections.emptyList() : responseData.getDataList();
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
OrgSyncCount syncCount = handleDept(oaDepartments, true);
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
}
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
if (Boolean.TRUE.equals(pageVO.getFinished())) {
// 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
deptService.updateAncestors(null);
}
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 {}/{},成功{},跳过{}",
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
return pageVO;
}
/**
* 组装组织分页同步结果
*/
private OaOrgSyncPageVO buildOrgSyncPageVO(String stage, int pageNo, int pageSize, long totalSize,
int fetchedCount, OrgSyncCount syncCount) {
OaOrgSyncPageVO pageVO = new OaOrgSyncPageVO();
pageVO.setStage(stage);
pageVO.setCurrent(pageNo);
pageVO.setSize(pageSize);
pageVO.setTotal(totalSize);
pageVO.setFetchedCount(fetchedCount);
pageVO.setSyncedCount(syncCount.getSyncedCount());
pageVO.setSkippedCount(syncCount.getSkippedCount());
boolean finished = fetchedCount == 0
|| fetchedCount < pageSize
|| (long) pageNo * pageSize >= totalSize;
pageVO.setFinished(finished);
return pageVO;
}
/**
* 组装 OA 公司分页查询参数
*
* @param startTime 增量查询开始时间
* @return 查询参数
*/
private OACompanySearch buildCompanySearch(Date startTime) {
OACompanySearch companySearch = new OACompanySearch();
companySearch.setCurPage(1);
companySearch.setPageSize(20);
if (startTime != null) {
companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
}
return companySearch;
}
/**
* 组装 OA 部门分页查询参数
*
* @param startTime 增量查询开始时间
* @param subCompanyIds 子公司 id 列表
* @return 查询参数
*/
private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) {
OADepartmentSearch departmentSearch = new OADepartmentSearch();
departmentSearch.setCurPage(1);
departmentSearch.setPageSize(20);
departmentSearch.setSubcompanyid1(subCompanyIds);
if (startTime != null) {
departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
}
return departmentSearch;
}
/**
* 组装 OA 人员分页查询参数
*
* @param startTime 增量查询开始时间
* @return 查询参数
*/
private OAPersonSearch buildPersonSearch(Date startTime) {
OAPersonSearch personSearch = new OAPersonSearch();
personSearch.setCurPage(1);
personSearch.setPageSize(20);
personSearch.setCreated("");
personSearch.setWorkcode("");
personSearch.setSubcompanyid1("");
personSearch.setDepartmentid("");
personSearch.setJobtitleid("");
personSearch.setId("");
personSearch.setLoginid("");
personSearch.setIsadaccount("");
personSearch.setModified(startTime == null ? "" : DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
return personSearch;
}
/**
* 处理oa公司
* @param oaCompanies
* @return 未处理的数据
* @param oaCompanies OA公司列表
* @param syncAll true=自动同步时不过滤字典白名单;false=仅同步配置的公司名称
* @return 同步统计
*/
private List<Dept> handleCompany(List<OACompanyResponse> oaCompanies) {
private OrgSyncCount handleCompany(List<OACompanyResponse> oaCompanies, boolean syncAll) {
if (CollectionUtil.isEmpty(oaCompanies)) {
// 数据为空,直接返回
return Collections.emptyList();
return OrgSyncCount.empty();
}
// 获取需要的公司名称
Set<String> companyNames = getCompanyNames();
Set<String> companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
// 转换数据
List<Dept> allParam = oaCompanies.stream()
// 筛选只要需要的公司
.filter(company -> companyNames.contains(company.getSubcompanyname()))
// 强制跳过上级公司为 0 的数据
.filter(company -> !OAConstant.ROOT_COMPANY_ID.equals(company.getSupsubcomid()))
// 非全量同步时,筛选只要配置的公司
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
.map(deptConvert::company2dept)
.toList();
int filteredSkipCount = oaCompanies.size() - allParam.size();
if (CollectionUtil.isEmpty(allParam)) {
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
// 查询数据库的部门,转换成map
Map<String, Long> deptMap = getAllCompanyDeptMap();
// 处理部门
return handleDept(allParam, deptMap, DeptCategory.COMPANY);
List<Dept> notHandledList = handleDept(allParam, deptMap, DeptCategory.COMPANY);
int syncedCount = allParam.size() - notHandledList.size();
int skippedCount = filteredSkipCount + notHandledList.size();
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
}
/**
* 处理oa部门
* @param oaDepts
* @return 未处理的数据
* @param oaDepts OA部门列表
* @param syncAll true=自动同步时不过滤根公司部门白名单;false=沿用原过滤规则
* @return 同步统计
*/
private List<Dept> handleDept(List<OADepartmentResponse> oaDepts) {
private OrgSyncCount handleDept(List<OADepartmentResponse> oaDepts, boolean syncAll) {
if (CollectionUtil.isEmpty(oaDepts)) {
// 数据为空,直接返回
return Collections.emptyList();
return OrgSyncCount.empty();
}
// 查询所有公司的编码和id的map
Map<String, Long> companyDeptMap = getAllCompanyDeptMap();
// 根公司id
String rootCompanyId = getRootCompanyId();
if (rootCompanyId == null) {
return Collections.emptyList();
if (!syncAll && rootCompanyId == null) {
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
}
// 根公司下要同步的部门名称
Set<String> rootCompanyDeptNames = getRootCompanyDeptNames();
Set<String> rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
// 转换数据
List<Dept> allParam = oaDepts.stream()
// 部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
.filter(oaDept -> !rootCompanyId.equals(oaDept.getSubcompanyid1()) || (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
// 全量同步时不过滤;否则:部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
.filter(oaDept -> syncAll
|| rootCompanyId == null
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
.map(dept -> deptConvert.dept2dept(dept, companyDeptMap))
.toList();
int filteredSkipCount = oaDepts.size() - allParam.size();
if (CollectionUtil.isEmpty(allParam)) {
return Collections.emptyList();
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
Set<String> deptCodes = allParam.stream()
.map(Dept::getDeptCode)
.collect(Collectors.toSet());
// 查询数据库的部门,转换成map
Map<String, Long> deptMap = deptService.list(Wrappers.<Dept>lambdaQuery()
Set<String> deptCodes = new HashSet<>();
allParam.forEach(dept -> {
if (StringUtils.isNotEmpty(dept.getDeptCode())) {
deptCodes.add(dept.getDeptCode());
}
if (StringUtils.isNotEmpty(dept.getParentCode())) {
deptCodes.add(dept.getParentCode());
}
});
// 查询数据库的部门,转换成map。包含本页及上级编码,便于命中已入库的上级
Map<String, Long> deptMap = deptCodes.isEmpty() ? new HashMap<>() : deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
.in(Dept::getDeptCode, deptCodes)
).stream()
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
.collect(Collectors.toMap(Dept::getDeptCode, Dept::getId, (a, b) -> b));
// 公司编码也放入 map,根部门挂到公司时才能解析父级
companyDeptMap.forEach(deptMap::putIfAbsent);
// 处理部门
return this.handleDept(allParam, deptMap, DeptCategory.DEPT);
List<Dept> notHandledList = this.handleDept(allParam, deptMap, DeptCategory.DEPT);
int syncedCount = allParam.size() - notHandledList.size();
int skippedCount = filteredSkipCount + notHandledList.size();
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
}
/**
@@ -471,14 +594,30 @@ public class OASyncServiceImpl implements IOASyncService {
}
});
// 重新把没有父id的数据设置一下父id
allParam.stream()
.filter(dept -> dept.getParentId() == null && deptMap.containsKey(dept.getDeptCode()))
.forEach(dept -> dept.setParentId(deptMap.get(dept.getParentCode())));
// 重新把没有父id的数据设置一下父id;上级尚未入库时,新增记录先挂到根节点,全部分批完成后重建层级并清理无法挂载的数据
int unresolvedParentCount = 0;
for (Dept dept : allParam) {
if (dept.getParentId() != null) {
continue;
}
Long parentId = deptMap.get(dept.getParentCode());
if (parentId != null) {
dept.setParentId(parentId);
continue;
}
unresolvedParentCount++;
if (!existsDeptIds.contains(dept.getId())) {
dept.setParentId(OAConvertConstant.ROOT_PARENT_ID);
}
}
if (unresolvedParentCount > 0) {
ComposeLogUtil.getLastLog().warn("同步{}时有{}条上级尚未入库,已先保存,分批结束后重建层级并清理无法挂载数据",
deptCategory.getName(), unresolvedParentCount);
}
// 父id不为空,保存数据
// 父id不为空才新增;已存在的记录即使父id未解析也更新(空父id不会覆盖原值)
List<Dept> list = allParam.stream()
.filter(dept -> dept.getParentId() != null)
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
.toList();
// 不存在的新增
List<Dept> addList = list.stream()
@@ -496,31 +635,86 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
deptService.updateBatchById(updateList);
}
// 返回未处理的
// 返回仍未保存的数据
return allParam.stream()
.filter(dept -> dept.getParentId() == null)
.filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
.toList();
}
/**
* 获取oa查询参数,子公司id参数
* @return
* 组织同步统计
*/
private String getSubCompanyIds() {
Set<String> companyNames = this.getCompanyNames();
if (companyNames.isEmpty()) {
private static class OrgSyncCount {
private final int syncedCount;
private final int skippedCount;
private final List<Dept> notHandledList;
private OrgSyncCount(int syncedCount, int skippedCount, List<Dept> notHandledList) {
this.syncedCount = syncedCount;
this.skippedCount = skippedCount;
this.notHandledList = notHandledList == null ? Collections.emptyList() : notHandledList;
}
private static OrgSyncCount empty() {
return new OrgSyncCount(0, 0, Collections.emptyList());
}
private int getSyncedCount() {
return syncedCount;
}
private int getSkippedCount() {
return skippedCount;
}
private List<Dept> getNotHandledList() {
return notHandledList;
}
}
/**
* 获取 OA 部门查询使用的子公司 id
*
* @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司
* @return 逗号分隔的 OA 公司 id
*/
private String getOaSubCompanyIds(boolean syncAll) {
List<Dept> companyList;
if (syncAll) {
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
);
} else {
Set<String> companyNames = this.getCompanyNames();
if (companyNames.isEmpty()) {
return null;
}
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
.in(Dept::getDeptName, companyNames)
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
);
}
return companyList.stream()
.map(this::resolveOaCompanyId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.joining(","));
}
/**
* 解析本地公司对应的 OA 公司 id
*/
private String resolveOaCompanyId(Dept dept) {
if (dept == null) {
return null;
}
return deptService.list(Wrappers.<Dept>lambdaQuery()
// 根据名称查询公司
.in(Dept::getDeptName, companyNames)
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
).stream()
// 只要部门编码不为空,且包含了oa公司前缀的
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().contains(OAConvertConstant.COMPANY_OA_PREFIX))
// 去掉oa公司前缀
.map(dept -> dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, ""))
.collect(Collectors.joining(","));
if (StringUtils.isNotBlank(dept.getOaId())) {
return dept.getOaId();
}
if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX)) {
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
}
return null;
}
/**
@@ -634,16 +828,4 @@ public class OASyncServiceImpl implements IOASyncService {
// 部门编码去掉前缀,就是oa的id
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;
}
}
@@ -31,10 +31,15 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.constant.DataStatusEnum;
import org.bouncycastle.util.encoders.Hex;
import org.springblade.common.constant.ParamConstant;
import org.springblade.common.constant.TenantConstant;
import org.springblade.common.utils.PasswordRuleUtil;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
@@ -63,6 +68,7 @@ import org.springblade.system.pojo.entity.*;
import org.springblade.system.pojo.enums.DictEnum;
import org.springblade.system.pojo.enums.UserType;
import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.props.IamSyncProperties;
import org.springblade.system.service.IRoleService;
import org.springblade.system.service.IUserDeptService;
import org.springblade.system.service.IUserOauthService;
@@ -71,7 +77,11 @@ import org.springblade.system.wrapper.UserWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -79,6 +89,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import static org.springblade.common.constant.ParamConstant.DEFAULT_PARAM_PASSWORD;
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
@@ -91,11 +103,14 @@ import static org.springblade.core.tenant.TenantGuard.EntityType.USER;
*/
@Service
@AllArgsConstructor
@Slf4j
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implements IUserService {
private static final String GUEST_NAME = "guest";
private static final String PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
private static final int RANDOM_PASSWORD_LENGTH = 8;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final String IAM_DEFAULT_DICT_CODE = "iam_default";
private static final String IAM_DEFAULT_ROLE_NAME = "默认角色";
private static final String IAM_DEFAULT_DEPT_NAME = "默认部门";
private static final String IAM_SYNC_TENANT_ID = "000000";
private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
private final IUserDeptService userDeptService;
private final UserDataScopeMapper userDataScopeMapper;
@@ -105,6 +120,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
private final BladeTenantProperties tenantProperties;
private final OAuth2Properties properties;
private final IamSyncProperties iamSyncProperties;
private final ObjectMapper objectMapper;
@Override
@@ -132,6 +149,163 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
return saveUser(user);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int syncIamAccounts() {
int pageNumber = 1;
int fetchedCount = 0;
int syncedCount = 0;
int totalCount = -1;
int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50;
while (true) {
JsonNode dataNode = requestIamAccountPage(pageNumber, pageSize);
JsonNode accountList = dataNode.path("list");
if (!accountList.isArray() || accountList.isEmpty()) {
break;
}
if (dataNode.has("total")) {
totalCount = dataNode.path("total").asInt(totalCount);
}
for (JsonNode accountNode : accountList) {
if (syncIamAccount(accountNode)) {
syncedCount++;
}
}
fetchedCount += accountList.size();
int responsePage = dataNode.path("page").asInt(pageNumber);
int responseSize = dataNode.path("size").asInt(pageSize);
if ((totalCount >= 0 && fetchedCount >= totalCount)
|| accountList.size() < pageSize
|| (totalCount >= 0 && responsePage * responseSize >= totalCount)) {
break;
}
pageNumber = responsePage + 1;
}
log.info("IAM账号同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount);
return syncedCount;
}
private JsonNode requestIamAccountPage(int pageNumber, int pageSize) {
try {
Map<String, String> requestBody = new LinkedHashMap<>();
requestBody.put("size", String.valueOf(pageSize));
requestBody.put("page", String.valueOf(pageNumber));
HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getAccountListUrl()))
.timeout(Duration.ofSeconds(20))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization()))
.header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization()))
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new ServiceException(StringUtil.format("IAM账号接口调用失败,HTTP状态码:{}", response.statusCode()));
}
JsonNode responseNode = objectMapper.readTree(response.body());
if (!"0".equals(responseNode.path("code").asText())) {
throw new ServiceException(StringUtil.format("IAM账号接口调用失败:{}", responseNode.path("msg").asText()));
}
JsonNode dataNode = responseNode.path("data");
if (!dataNode.isObject()) {
throw new ServiceException("IAM账号接口返回数据格式错误");
}
return dataNode;
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
log.error("调用IAM账号接口被中断,page={}", pageNumber, exception);
throw new ServiceException("调用IAM账号接口被中断");
} catch (IOException | IllegalArgumentException exception) {
log.error("调用IAM账号接口失败,page={}", pageNumber, exception);
throw new ServiceException("调用IAM账号接口失败");
}
}
private boolean syncIamAccount(JsonNode accountNode) {
String account = readIamText(accountNode, "accountNo", "account_no", "app_account__account_no");
if (StringUtil.isBlank(account)) {
log.warn("IAM账号缺少accountNo,跳过同步");
return false;
}
String name = readIamText(accountNode, "name", "app_account__name");
if (StringUtil.isBlank(name)) {
name = readIamText(accountNode, "accountName", "account_name", "app_account__account_name");
}
if (StringUtil.isBlank(name)) {
name = account;
}
Integer status = readIamInt(accountNode, "status", "app_account__status") == 1
? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode();
User user = userByAccount(IAM_SYNC_TENANT_ID, account);
if (user == null) {
user = new User();
user.setTenantId(IAM_SYNC_TENANT_ID);
user.setAccount(account);
user.setName(name);
user.setRealName(name);
user.setRoleId(resolveIamDefaultRoleId());
user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
user.setPostId(StringPool.MINUS_ONE);
user.setUserType(UserType.WEB.getCategory());
user.setStatus(status);
user.setIsOa(1);
user.setSyncTime(new Date());
applyUserDefaults(user);
return saveUser(user);
}
boolean changed = !Objects.equals(user.getName(), name) || !Objects.equals(user.getRealName(), name)
|| !Objects.equals(user.getStatus(), status) || !Objects.equals(user.getIsOa(), 1);
if (isMissingIamRole(user.getRoleId())) {
user.setRoleId(resolveIamDefaultRoleId());
changed = true;
}
if (isMissingIamAssignment(user.getDeptId())) {
user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
changed = true;
}
if (!changed) {
return true;
}
user.setName(name);
user.setRealName(name);
user.setStatus(status);
user.setIsOa(1);
user.setSyncTime(new Date());
CacheUtil.clear(USER_CACHE);
return updateById(user);
}
private String readIamText(JsonNode node, String... fieldNames) {
JsonNode valueNode = findIamNode(node, fieldNames);
return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim();
}
private int readIamInt(JsonNode node, String... fieldNames) {
JsonNode valueNode = findIamNode(node, fieldNames);
return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0);
}
private JsonNode findIamNode(JsonNode node, String... fieldNames) {
for (String fieldName : fieldNames) {
JsonNode valueNode = node.get(fieldName);
if (valueNode != null && !valueNode.isNull()) {
return valueNode;
}
}
return null;
}
private String normalizeAuthorizationHeader(String value) {
if (StringUtil.isBlank(value)) {
return StringPool.EMPTY;
}
if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) {
return value;
}
return "Basic " + value;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(User user) {
@@ -352,6 +526,46 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
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
@Transactional(rollbackFor = Exception.class)
public boolean grant(String userIds, String roleIds) {
@@ -371,7 +585,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
List<User> userList = TenantGuard.verifyBatch(this, idList, USER);
Map<String, String> passwordMap = new LinkedHashMap<>();
for (User user : userList) {
String password = randomPassword();
String password = PasswordRuleUtil.generate();
User updateUser = new User();
updateUser.setPassword(DigestUtil.encrypt(password));
updateUser.setUpdateTime(DateUtil.now());
@@ -387,6 +601,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
if (!Objects.equals(password, password2)) {
throw new ServiceException("两次输入密码不一致!");
}
validatePlainPassword(password);
User user = TenantGuard.verify(this, userId, USER);
User updateUser = new User();
updateUser.setPassword(DigestUtil.encrypt(password));
@@ -455,7 +670,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
if (user.getId() != null) {
this.updateUser(user);
} else {
user.setPassword(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
// 空密码交由 saveUser 按规则填充(初始密码不合规时自动生成)
user.setPassword(null);
this.submit(user);
}
});
@@ -472,6 +688,9 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
*/
private User buildImportUser(UserExcel userExcel, String tenantId) {
User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class));
// 宽容解析生日文本(2026-8-2 等写法),User.birthday 为 java.util.Date 需转换
java.time.LocalDate birthday = org.springblade.common.excel.LenientDateParser.parseDate(userExcel.getBirthday(), "生日");
user.setBirthday(birthday == null ? null : java.sql.Date.valueOf(birthday));
user.setTenantId(tenantId);
user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
@@ -563,8 +782,31 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
if (user.getUserType() == null) {
user.setUserType(UserType.WEB.getCategory());
}
if (StringUtil.isBlank(user.getRoleId())) {
user.setRoleId(StringPool.MINUS_ONE);
String defaultRoleId = resolveIamDefaultRoleId();
User existingUser = userByAccount(user.getTenantId(), user.getAccount());
if (existingUser != null) {
boolean changed = false;
if (isMissingIamRole(existingUser.getRoleId())) {
existingUser.setRoleId(defaultRoleId);
changed = true;
}
if (isMissingIamAssignment(existingUser.getDeptId())) {
existingUser.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
changed = true;
}
if (existingUser.getIsOa() == null || existingUser.getIsOa() != 1) {
existingUser.setIsOa(1);
existingUser.setSyncTime(new Date());
changed = true;
}
if (!changed) {
return true;
}
CacheUtil.clear(USER_CACHE);
return this.updateById(existingUser);
}
if (isMissingIamRole(user.getRoleId())) {
user.setRoleId(defaultRoleId);
}
if (StringUtil.isBlank(user.getDeptId())) {
user.setDeptId(StringPool.MINUS_ONE);
@@ -579,6 +821,26 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
return saveUser(user);
}
private boolean isMissingIamRole(String roleId) {
return StringUtil.isBlank(roleId) || StringPool.MINUS_ONE.equals(roleId);
}
private String resolveIamDefaultRoleId() {
return resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME);
}
private String resolveIamDefaultId(String dictValue) {
String dictKey = DictCache.getKey(IAM_DEFAULT_DICT_CODE, dictValue);
if (StringUtil.isBlank(dictKey) || StringPool.MINUS_ONE.equals(dictKey)) {
throw new ServiceException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue));
}
return dictKey;
}
private boolean isMissingIamAssignment(String value) {
return StringUtil.isBlank(value) || StringPool.MINUS_ONE.equals(value);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updatePlatform(Long userId, Integer userType, String userExt) {
@@ -801,8 +1063,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
}
}
if (Func.isEmpty(user.getPassword())) {
user.setPassword(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
String initPassword = ParamCache.getValue(DEFAULT_PARAM_PASSWORD);
user.setPassword(PasswordRuleUtil.isValid(initPassword) ? initPassword : PasswordRuleUtil.generate());
}
validatePlainPassword(user.getPassword());
user.setPassword(DigestUtil.encrypt(user.getPassword()));
Long userCount = baseMapper.selectCount(Wrappers.<User>query().lambda().eq(User::getTenantId, tenantId).eq(User::getAccount, user.getAccount()));
if (userCount > 0L && Func.isEmpty(user.getId())) {
@@ -884,12 +1148,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
// }
}
private String randomPassword() {
StringBuilder password = new StringBuilder(RANDOM_PASSWORD_LENGTH);
for (int index = 0; index < RANDOM_PASSWORD_LENGTH; index++) {
password.append(PASSWORD_CHARS.charAt(SECURE_RANDOM.nextInt(PASSWORD_CHARS.length())));
private void validatePlainPassword(String password) {
if (!PasswordRuleUtil.isValid(password)) {
throw new ServiceException(PasswordRuleUtil.RULE_MESSAGE);
}
return password.toString();
}
}
@@ -0,0 +1,6 @@
# 本机多网卡时 Nacos 易误选 169.254.*(链路本地),导致网关 Host is down
spring:
cloud:
nacos:
discovery:
ip: ${NACOS_DISCOVERY_IP:127.0.0.1}
@@ -23,3 +23,21 @@ spring:
url: ${blade.datasource.${spring.profiles.active}.url}
username: ${blade.datasource.${spring.profiles.active}.username}
password: ${blade.datasource.${spring.profiles.active}.password}
# IAM账号同步
iam:
sync:
account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST}
org-list-url: ${IAM_SSO_ORG_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ORG_LIST}
authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==}
page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50}
# OA组织/人员同步走同一 gwzh 网关,仅需 Authorization(与可用 curl 一致)
thirdParty:
oa:
baseUrl: ${OA_BASE_URL:http://172.16.204.83:38000}
queryCompanyPageUrl: ${OA_QUERY_COMPANY_PAGE_URL:/gwzh/OA/OA_GET_COMPANY_LIST}
queryDepartmentPageUrl: ${OA_QUERY_DEPARTMENT_PAGE_URL:/gwzh/OA/OA_GET_DEPARTMENT_LIST}
queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST}
authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}
+8
View File
@@ -31,6 +31,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-transport-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-lbs-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-user-api</artifactId>
@@ -39,6 +43,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-system-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-process-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-bfi-api</artifactId>
@@ -6,14 +6,20 @@ package org.springblade.transport.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 凭证图片 MinIO 客户端配置。
* <p>
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
*/
@Configuration
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
public class VoucherMinioConfig {
@Bean
@@ -0,0 +1,102 @@
/**
* 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.alibaba.fastjson2.JSON;
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 lombok.extern.slf4j.Slf4j;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.impl.CustomerArchivePublicProcessService;
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.Map;
/**
* 客商档案公开查看 控制器
*
* @author Chill
*/
@Slf4j
@RestController
@AllArgsConstructor
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@RequestMapping("/customer-archive/public")
@Tag(name = "客商档案公开查看", description = "客商档案公开查看")
public class CustomerArchivePublicController {
private final ICustomerArchiveService customerArchiveService;
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
/**
* 公开详情
*/
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "传入id,无需登录")
public R<CustomerArchiveVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(customerArchiveService.publicDetail(id));
}
/**
* 公开变更记录分页
*/
@GetMapping("/change-record/list")
@ApiOperationSupport(order = 2)
@Operation(summary = "公开变更记录分页", description = "传入客商ID,无需登录")
public R<IPage<CustomerChangeRecordVO>> changeRecordList(
@Parameter(description = "客商ID", required = true) @RequestParam Long customerId, Query query) {
return R.data(customerArchiveService.publicChangeRecordPage(Condition.getPage(query), customerId));
}
/**
* 公开接收流程页 postMessage 数据
*/
@PostMapping("/process-message")
@ApiOperationSupport(order = 3)
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步客商")
public R processMessage(@RequestBody Map<String, Object> body) {
log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body));
customerArchivePublicProcessService.handleProcessMessage(body);
return R.success("ok");
}
}
@@ -30,7 +30,6 @@ import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
import org.springblade.transport.pojo.entity.ExceptionDisposal;
@@ -48,7 +47,7 @@ import org.springframework.web.bind.annotation.RestController;
* <p>
* 对外路径:{@code /api/blade-transport/exception-disposal/**}
* 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。
* 司机上报(submit)/ 列表 / 详情仅需登录态;跟进与完成保留菜单鉴权。
* 列表 / 详情 / 上报 / 跟进 / 完成均仅需登录态(小程序调度端与司机端共用)。
*/
@RestController
@AllArgsConstructor
@@ -80,27 +79,24 @@ public class ExceptionDisposalController extends BladeController {
}
@PostMapping("/follow")
@PreAuth(menu = "exception_disposal")
@ApiOperationSupport(order = 4)
@Operation(summary = "异常跟进")
@Operation(summary = "异常跟进", description = "调度端跟进;仅需登录态")
public R follow(@RequestBody ExceptionDisposalFollowRequest request) {
exceptionDisposalService.follow(request);
return R.success("跟进成功");
}
@PostMapping("/complete")
@PreAuth(menu = "exception_disposal")
@ApiOperationSupport(order = 5)
@Operation(summary = "完成异常")
@Operation(summary = "完成异常", description = "调度端结案;仅需登录态")
public R complete(@RequestBody ExceptionDisposalFollowRequest request) {
exceptionDisposalService.complete(request.getId());
return R.success("完成成功");
}
@PostMapping("/batch-complete")
@PreAuth(menu = "exception_disposal")
@ApiOperationSupport(order = 6)
@Operation(summary = "批量完成异常")
@Operation(summary = "批量完成异常", description = "调度端批量结案;仅需登录态")
public R batchComplete(@RequestParam String ids) {
exceptionDisposalService.batchComplete(ids);
return R.success("批量完成成功");
@@ -119,22 +119,29 @@ public class LoadingManageController extends BladeController {
return R.status(loadingManageService.changeRoute(loadingManage));
}
@PostMapping("/cancel")
@PostMapping("/start")
@ApiOperationSupport(order = 10)
@Operation(summary = "改为进行中", description = "传入id")
public R start(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(loadingManageService.start(id));
}
@PostMapping("/cancel")
@ApiOperationSupport(order = 11)
@Operation(summary = "取消", description = "传入id")
public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(loadingManageService.cancel(id));
}
@PostMapping("/complete")
@ApiOperationSupport(order = 11)
@ApiOperationSupport(order = 12)
@Operation(summary = "完成", description = "传入id")
public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(loadingManageService.complete(id));
}
@PostMapping("/batch-complete")
@ApiOperationSupport(order = 12)
@ApiOperationSupport(order = 13)
@Operation(summary = "批量完成", description = "传入ids")
public R<BusinessRemoveResultVO> batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
return R.data(loadingManageService.batchComplete(ids));
@@ -0,0 +1,143 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
import org.springblade.transport.pojo.vo.AdminHomeVO;
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
import org.springblade.transport.service.IManageWaybillService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 调度端运单(小程序管理端)
* <p>
* 对外完整路径:{@code /api/blade-transport/waybill/manage/**}
* (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /waybill/manage/**})。
* 同时兼容未去前缀直连({@code /blade-transport/waybill/manage/**})。
* 仅需登录态,不挂管理端菜单鉴权。
*/
@RestController
@AllArgsConstructor
@RequestMapping({"/waybill/manage", "/blade-transport/waybill/manage"})
@Tag(name = "调度端运单", description = "小程序调度端首页统计与运单列表")
public class ManageWaybillController extends BladeController {
private final IManageWaybillService manageWaybillService;
@GetMapping("/stats")
@ApiOperationSupport(order = 1)
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;租户内不过滤组织(小程序调度账号组织常与运单不一致);在途异常=异常处置状态≠已完成")
public R<AdminHomeStatsVO> stats() {
return R.data(manageWaybillService.stats());
}
@GetMapping("/home")
@ApiOperationSupport(order = 2)
@Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名")
public R<AdminHomeVO> home() {
return R.data(manageWaybillService.home());
}
@GetMapping("/list")
@ApiOperationSupport(order = 3)
@Operation(summary = "运单分页列表", description = "当前组织运单;status:0待接单/1运输中/2已完成;exception:exception/normal;transportType:common/load")
public R<IPage<AdminWaybillCardVO>> list(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword,
@Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
@Parameter(description = "异常:exception有异常/normal无异常") @RequestParam(required = false) String exception,
@Parameter(description = "运输组织:common普通/load配载") @RequestParam(required = false) String transportType,
@Parameter(description = "创建日起 YYYY-MM-DD") @RequestParam(required = false) String startDate,
@Parameter(description = "创建日止 YYYY-MM-DD") @RequestParam(required = false) String endDate) {
return R.data(manageWaybillService.pageList(
current, size, keyword, status, exception, transportType, startDate, endDate));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 4)
@Operation(summary = "运单详情", description = "调度端查看运单详情(含 punchNodes / enrouteRecords),不校验司机归属与组织;字段对齐小程序 pages/waybill/detail")
public R<AdminWaybillDetailVO> detail(
@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
return R.data(manageWaybillService.detail(id));
}
@GetMapping("/pending")
@ApiOperationSupport(order = 5)
@Operation(summary = "待处理运单", description = "待接单/运输中;needReassign=true 仅司机已拒单")
public R<IPage<AdminWaybillCardVO>> pending(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword,
@Parameter(description = "是否需重新派单") @RequestParam(required = false) Boolean needReassign) {
return R.data(manageWaybillService.pendingList(current, size, keyword, needReassign));
}
@PostMapping("/reassign")
@ApiOperationSupport(order = 6)
@Operation(summary = "重新派单", description = "小程序调度端:跳过组织校验,仅需登录态;传入运单ID及新司机、手机号、车牌")
public R reassign(@RequestBody Waybill waybill) {
return R.status(manageWaybillService.reassign(
waybill.getId(),
waybill.getDriverId(),
waybill.getDriverName(),
waybill.getDriverPhone(),
waybill.getVehicleNo()));
}
@GetMapping("/driver-search")
@ApiOperationSupport(order = 7)
@Operation(summary = "搜索司机", description = "按姓名/手机号模糊搜索,供重新派单选用")
public R<List<AdminDriverOptionVO>> driverSearch(
@Parameter(description = "关键字") @RequestParam(required = false) String keyword) {
return R.data(manageWaybillService.searchDrivers(keyword));
}
@GetMapping("/vehicle-search")
@ApiOperationSupport(order = 8)
@Operation(summary = "搜索车牌", description = "按车牌模糊搜索(来自司机绑定车牌)")
public R<List<AdminVehicleOptionVO>> vehicleSearch(
@Parameter(description = "关键字") @RequestParam(required = false) String keyword) {
return R.data(manageWaybillService.searchVehicles(keyword));
}
}
@@ -0,0 +1,55 @@
package org.springblade.transport.controller;
import com.alibaba.fastjson2.JSON;
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 lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.R;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.Map;
/**
* MK 业务流程公开查看
*/
@Slf4j
@RestController
@AllArgsConstructor
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@RequestMapping("/mk-process/public")
@Tag(name = "MK业务流程公开查看", description = "MK业务流程公开查看")
public class MkProcessPublicController {
private final MkProcessMessageService mkProcessMessageService;
@GetMapping("/{bizType}/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "无需登录")
public R detail(@PathVariable String bizType,
@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
}
@PostMapping("/{bizType}/process-message")
@ApiOperationSupport(order = 2)
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步业务")
public R processMessage(@PathVariable String bizType, @RequestBody Map<String, Object> body) {
log.info("公开页收到流程消息 bizType={} body={}", bizType, JSON.toJSONString(body));
mkProcessMessageService.handleProcessMessage(bizType, body);
return R.success("ok");
}
}
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.service.IProcessConfigService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -83,20 +84,34 @@ public class ProcessConfigController extends BladeController {
private final VoucherFileMapper voucherFileMapper;
private final VoucherImageMapper voucherImageMapper;
private final VoucherManageMapper voucherManageMapper;
private final MinioClient minioClient;
private final ObjectProvider<MinioClient> minioClientProvider;
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper,
MinioClient minioClient) {
ObjectProvider<MinioClient> minioClientProvider) {
this.processConfigService = processConfigService;
this.waybillMapper = waybillMapper;
this.voucherFileMapper = voucherFileMapper;
this.voucherImageMapper = voucherImageMapper;
this.voucherManageMapper = voucherManageMapper;
this.minioClient = minioClient;
this.minioClientProvider = minioClientProvider;
}
/**
* 获取 MinIO 客户端。
* <p>
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
* 此时凭证预览地址无法生成,抛出明确提示而非启动即失败。
*/
private MinioClient minioClient() {
MinioClient minioClient = minioClientProvider.getIfAvailable();
if (minioClient == null) {
throw new IllegalStateException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
}
return minioClient;
}
@GetMapping("/detail")
@@ -147,7 +162,7 @@ public class ProcessConfigController extends BladeController {
result.put("waybillNo", image.getWaybillNo());
result.put("objectKey", image.getObjectKey());
try {
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
.expiry(1, TimeUnit.HOURS).build()));
} catch (Exception e) {
@@ -270,7 +285,7 @@ public class ProcessConfigController extends BladeController {
result.put("objectKey", image.objectKey());
result.put("matched", image.matched());
try {
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.objectKey())
.expiry(1, TimeUnit.HOURS).build()));
} catch (Exception exception) {
@@ -105,7 +105,7 @@ public class ProjectApplyController extends BladeController {
@ApiOperationSupport(order = 4)
@Operation(summary = "新增或修改", description = "传入projectApply")
public R submit(@RequestBody ProjectApply projectApply) {
return R.status(projectApplyService.submit(projectApply));
return projectApplyService.submit(projectApply) ? R.data(projectApply) : R.fail("保存失败");
}
@PostMapping("/submit-approval")
@@ -53,6 +53,8 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillLocateVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.pojo.vo.WaybillVO;
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
@@ -109,6 +111,23 @@ public class WaybillController extends BladeController {
return R.data(waybillService.listPunchRecords(waybillId));
}
@PostMapping("/locate")
@ApiOperationSupport(order = 1)
@Operation(summary = "车辆实时定位", description = "按运单绑定车牌调用 LBS_LOCATE")
public R<WaybillLocateVO> locate(@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
return R.data(waybillService.locateVehicle(id));
}
@PostMapping("/track")
@ApiOperationSupport(order = 1)
@Operation(summary = "车辆历史轨迹", description = "按运单绑定车牌 + 日期区间调用 LBS_TRACK")
public R<WaybillTrackVO> track(
@Parameter(description = "运单ID", required = true) @RequestParam Long id,
@Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate,
@Parameter(description = "结束日期 YYYY-MM-DD", required = true) @RequestParam String endDate) {
return R.data(waybillService.trackVehicle(id, startDate, endDate));
}
@GetMapping("/list")
@ApiOperationSupport(order = 2)
@Operation(summary = "分页", description = "传入waybill")
@@ -194,7 +213,7 @@ public class WaybillController extends BladeController {
@ApiOperationSupport(order = 9)
@Operation(summary = "新增或修改", description = "传入waybill")
public R submit(@RequestBody Waybill waybill) {
return R.status(waybillService.submit(waybill));
return waybillService.submit(waybill) ? R.data(waybill) : R.fail("保存失败");
}
@PostMapping("/save-draft")
@@ -36,7 +36,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 事故记录 Excel
@@ -60,8 +59,8 @@ public class AccidentRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号")
private String vehicleNo;
@ExcelProperty("*事故发生日期")
private LocalDate accidentDate;
@ExcelProperty(value = "*事故发生日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String accidentDate;
@ExcelProperty("事故发生地点")
private String accidentLocation;
@@ -37,7 +37,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 事故记录导出 Excel
@@ -83,14 +83,14 @@ public class AccidentRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 年检记录 Excel
@@ -59,11 +58,11 @@ public class AnnualInspectionRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号")
private String vehicleNo;
@ExcelProperty("*检测评定日期")
private LocalDate inspectionAssessmentDate;
@ExcelProperty(value = "*检测评定日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String inspectionAssessmentDate;
@ExcelProperty("*有效期截止日")
private LocalDate validUntilDate;
@ExcelProperty(value = "*有效期截止日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String validUntilDate;
@ExcelProperty("*车辆技术等级")
private String vehicleTechnicalLevel;
@@ -36,7 +36,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 年检记录导出 Excel
@@ -83,14 +83,14 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -1,96 +1,99 @@
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 常用地址导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(18)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CommonAddressExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelIgnore
private Long id;
@ExcelProperty("地址名称")
private String addressName;
@ExcelProperty("地址编号")
private String addressCode;
@ExcelProperty("类型")
private String addressType;
@ExcelProperty("站点编码")
private String siteCodeDisplay;
@ExcelProperty("详细地址")
private String detailAddress;
@ExcelProperty("经度")
private BigDecimal longitude;
@ExcelProperty("纬度")
private BigDecimal latitude;
@ExcelProperty("行政区划")
private String regionName;
@ExcelProperty("联系人")
private String contactName;
@ExcelProperty("联系方式")
private String contactPhone;
@ExcelProperty("组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("更新时间")
private Date updateTime;
@ExcelProperty("创建时间")
private Date createTime;
}
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 常用地址导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(18)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CommonAddressExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelIgnore
private Long id;
@ExcelProperty("地址名称")
private String addressName;
@ExcelProperty("地址编号")
private String addressCode;
@ExcelProperty("类型")
private String addressType;
@ExcelProperty("站点编码")
private String siteCodeDisplay;
@ExcelProperty("详细地址")
private String detailAddress;
@ExcelProperty("经度")
private BigDecimal longitude;
@ExcelProperty("纬度")
private BigDecimal latitude;
@ExcelProperty("行政区划")
private String regionName;
@ExcelProperty("联系人")
private String contactName;
@ExcelProperty("联系方式")
private String contactPhone;
@ExcelProperty("组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -1,101 +1,104 @@
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 常用货物导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(24)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CommonCargoExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("货物名称")
private String cargoName;
@ExcelProperty("货物编号后缀")
private String cargoCodeSuffix;
@ExcelProperty("一级货物类型")
private String firstCargoTypeName;
@ExcelProperty("二级货物类型")
private String secondCargoTypeName;
@ExcelProperty("二级货物类型编码")
private String secondCargoTypeCode;
@ExcelProperty("包装")
private String packageType;
@ExcelProperty("品牌")
private String brand;
@ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
private String model;
@ExcelProperty("单价")
@NumberFormat("0.00")
private BigDecimal cargoValue;
@ExcelProperty("计价单位")
private String priceUnit;
@ExcelProperty("说明")
private String descriptionOne;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("组织")
private String deptName;
@ExcelProperty("更新时间")
private Date updateTime;
@ExcelProperty("创建时间")
private Date createTime;
}
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 常用货物导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(24)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CommonCargoExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("货物名称")
private String cargoName;
@ExcelProperty("货物编号后缀")
private String cargoCodeSuffix;
@ExcelProperty("一级货物类型")
private String firstCargoTypeName;
@ExcelProperty("二级货物类型")
private String secondCargoTypeName;
@ExcelProperty("二级货物类型编码")
private String secondCargoTypeCode;
@ExcelProperty("包装")
private String packageType;
@ExcelProperty("品牌")
private String brand;
@ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
private String model;
@ExcelProperty("单价")
@NumberFormat("0.00")
private BigDecimal cargoValue;
@ExcelProperty("计价单位")
private String priceUnit;
@ExcelProperty("说明")
private String descriptionOne;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("组织")
private String deptName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -1,82 +1,85 @@
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 常用线路导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CommonRouteExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("线路编号")
private String routeCode;
@ExcelProperty("线路名称")
private String routeName;
@ExcelProperty("发货地")
private String departureName;
@ExcelProperty("发货地址")
private String departureAddress;
@ExcelProperty("发货联系人")
private String departureContact;
@ExcelProperty("发货联系方式")
private String departurePhone;
@ExcelProperty("收货地")
private String arrivalName;
@ExcelProperty("收货地址")
private String arrivalAddress;
@ExcelProperty("收货联系人")
private String arrivalContact;
@ExcelProperty("收货联系方式")
private String arrivalPhone;
@ExcelProperty("所属组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
private Date updateTime;
@ExcelProperty("创建时间")
private Date createTime;
}
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 常用线路导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CommonRouteExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("线路编号")
private String routeCode;
@ExcelProperty("线路名称")
private String routeName;
@ExcelProperty("发货地")
private String departureName;
@ExcelProperty("发货地址")
private String departureAddress;
@ExcelProperty("发货联系人")
private String departureContact;
@ExcelProperty("发货联系方式")
private String departurePhone;
@ExcelProperty("收货地")
private String arrivalName;
@ExcelProperty("收货地址")
private String arrivalAddress;
@ExcelProperty("收货联系人")
private String arrivalContact;
@ExcelProperty("收货联系方式")
private String arrivalPhone;
@ExcelProperty("所属组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -1,118 +1,121 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 合同管理 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class ContractManageExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("合同编号")
private String contractNo;
@ExcelProperty("合同名称")
private String contractName;
@ExcelProperty("所属项目")
private String projectName;
@ExcelProperty("所属组织")
private String organizationName;
@ExcelProperty("合同类别")
private String contractCategory;
@ExcelProperty("签约类型")
private String signType;
@ExcelProperty("甲方")
private String partyA;
@ExcelProperty("乙方")
private String partyB;
@ExcelProperty("开始日期")
private LocalDate startDate;
@ExcelProperty("结束日期")
private LocalDate endDate;
@ExcelProperty("临时效力起")
private LocalDate temporaryStartDate;
@ExcelProperty("临时效力止")
private LocalDate temporaryEndDate;
@ExcelProperty("经办人")
private String handlerUserName;
@ExcelProperty("合同阶段")
private String contractStage;
@ExcelProperty("审核状态")
private String approvalStatus;
@ExcelProperty("归档状态")
private String archiveStatus;
@ExcelProperty("结算币种")
private String settlementCurrency;
@ExcelProperty("结算方式")
private String settlementMode;
@ExcelProperty("开票周期(天)")
private Integer invoiceCycle;
@ExcelProperty("一式份数")
private Integer copyCount;
@ExcelProperty("回款账期(天)")
private Integer paymentDays;
@ExcelProperty("合同金额")
@NumberFormat("0.00")
private BigDecimal contractAmount;
@ExcelProperty("是否范本")
private Integer templateFlag;
@ExcelProperty("原件合同编号")
private String originalContractNo;
@ExcelProperty("是否电子章")
private Integer electronicSealFlag;
@ExcelProperty("当前节点")
private String currentNode;
@ExcelProperty("当前处理人")
private String currentProcessor;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@ExcelProperty("更新时间")
private LocalDateTime updateTime;
}
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
* 合同管理 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class ContractManageExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("合同编号")
private String contractNo;
@ExcelProperty("合同名称")
private String contractName;
@ExcelProperty("所属项目")
private String projectName;
@ExcelProperty("所属组织")
private String organizationName;
@ExcelProperty("合同类别")
private String contractCategory;
@ExcelProperty("签约类型")
private String signType;
@ExcelProperty("甲方")
private String partyA;
@ExcelProperty("乙方")
private String partyB;
@ExcelProperty("开始日期")
private LocalDate startDate;
@ExcelProperty("结束日期")
private LocalDate endDate;
@ExcelProperty("临时效力起")
private LocalDate temporaryStartDate;
@ExcelProperty("临时效力止")
private LocalDate temporaryEndDate;
@ExcelProperty("经办人")
private String handlerUserName;
@ExcelProperty("合同阶段")
private String contractStage;
@ExcelProperty("审核状态")
private String approvalStatus;
@ExcelProperty("归档状态")
private String archiveStatus;
@ExcelProperty("结算币种")
private String settlementCurrency;
@ExcelProperty("结算方式")
private String settlementMode;
@ExcelProperty("开票周期(天)")
private Integer invoiceCycle;
@ExcelProperty("一式份数")
private Integer copyCount;
@ExcelProperty("回款账期(天)")
private Integer paymentDays;
@ExcelProperty("合同金额")
@NumberFormat("0.00")
private BigDecimal contractAmount;
@ExcelProperty("是否范本")
private Integer templateFlag;
@ExcelProperty("原件合同编号")
private String originalContractNo;
@ExcelProperty("是否电子章")
private Integer electronicSealFlag;
@ExcelProperty("当前节点")
private String currentNode;
@ExcelProperty("当前处理人")
private String currentProcessor;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -58,6 +58,9 @@ public class CustomerArchiveExcel implements Serializable {
private String fullName;
@ExcelProperty("客商类型")
private String customerKind;
@ExcelProperty("客商分类")
private String customerType;
@ExcelProperty("客商性质")
@@ -7,12 +7,10 @@ package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 设备台账 Excel
@@ -29,7 +27,7 @@ public class EquipmentLedgerExcel implements Serializable {
@ExcelProperty("设备品牌") private String equipmentBrand;
@ExcelProperty("设备类型") private String equipmentType;
@ExcelProperty("规格型号") private String specificationModel;
@ExcelProperty("出厂日期") @DateTimeFormat("yyyy-MM-dd") private LocalDate factoryDate;
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate;
@ExcelProperty("备注") private String remark;
@ExcelIgnore private String errorMessage;
}
@@ -11,6 +11,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* ETC记录导出 Excel
@@ -50,14 +51,14 @@ public class EtcRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 保险记录 Excel
@@ -68,11 +67,11 @@ public class InsuranceRecordExcel implements Serializable {
@ExcelProperty("*保单号")
private String policyNo;
@ExcelProperty("*开始日期")
private LocalDate startDate;
@ExcelProperty(value = "*开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String startDate;
@ExcelProperty("*结束日期")
private LocalDate endDate;
@ExcelProperty(value = "*结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String endDate;
@ExcelProperty("保额")
private BigDecimal insuredAmount;
@@ -83,8 +82,8 @@ public class InsuranceRecordExcel implements Serializable {
@ExcelProperty("发票号")
private String invoiceNo;
@ExcelProperty("开票日期")
private LocalDate invoiceDate;
@ExcelProperty(value = "开票日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String invoiceDate;
@ExcelProperty("备注")
private String remark;
@@ -36,7 +36,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 保险记录导出 Excel
@@ -83,14 +83,14 @@ public class InsuranceRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -33,22 +33,20 @@ import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import cn.idev.excel.util.DateUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/**
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
* <p>
* 文本解析委托公共宽容解析器(支持 2026-8-2、2026/8/2 等写法,口径见根工作区
* docs/import-spec.md);无法识别的文本按既有行为抛出转换异常,
* 文案带原值,与「日期格式无法识别」口径一致。
*
* @author Chill
*/
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_TIME_MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
@Override
public Class<?> supportJavaTypeKey() {
@@ -71,16 +69,14 @@ public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime
if (value == null || value.trim().isEmpty()) {
return null;
}
String normalizedValue = value.trim();
try {
return LocalDateTime.parse(normalizedValue, DATE_TIME_FORMATTER);
} catch (DateTimeParseException ignored) {
try {
return LocalDateTime.parse(normalizedValue, DATE_TIME_MINUTE_FORMATTER);
} catch (DateTimeParseException ignoredMinute) {
return LocalDate.parse(normalizedValue, DATE_FORMATTER).atStartOfDay();
}
LocalDateTime parsed = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
if (parsed != null) {
return parsed;
}
// 无法识别的文本:与既有行为一致抛出转换异常(ExcelDataConvertException 是 RuntimeException,
// 会带上本文案一路抛到 Controller,由全局异常处理返回给前端提示)。
throw new cn.idev.excel.exception.ExcelDataConvertException(-1, -1, cellData, contentProperty,
value.trim() + " 日期格式无法识别");
}
@Override
@@ -11,7 +11,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 车辆保养记录导出 Excel
@@ -26,12 +26,12 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
@ExcelProperty(value = "创建时间", index = 12)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty(value = "更新人", index = 13)
private String updateUserName;
@ExcelProperty(value = "更新时间", index = 14)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
}
@@ -38,6 +38,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 车辆维修记录 Excel
@@ -95,16 +96,16 @@ public class MaintenanceRecordExcel implements Serializable {
@NumberFormat("0.00")
private BigDecimal mileage;
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("里程单位")
private String mileageUnit;
@@ -25,7 +25,6 @@
*/
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -42,7 +41,4 @@ public class MaintenanceRecordExportExcel extends MaintenanceRecordExcel {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "导出失败原因", index = 17)
private String errorMessage;
}
@@ -36,7 +36,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 里程记录导出 Excel
@@ -72,14 +72,14 @@ public class MileageRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -11,6 +11,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 油电记录导出 Excel
@@ -61,14 +62,14 @@ public class OilElectricRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -16,7 +16,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 其他费用记录 Excel
@@ -40,8 +39,8 @@ public class OtherExpenseRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号")
private String vehicleNo;
@ExcelProperty("*费用日期")
private LocalDate expenseDate;
@ExcelProperty(value = "*费用日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String expenseDate;
@ExcelProperty("*费用类型")
private String expenseType;
@@ -11,7 +11,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 其他费用记录导出 Excel
@@ -43,14 +43,14 @@ public class OtherExpenseRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -1,76 +1,79 @@
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 过程配置导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class ProcessConfigExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("配置编号")
private String configCode;
@ExcelProperty("配置名称")
private String configName;
@ExcelProperty("项目ID集合")
private String projectIds;
@ExcelProperty("项目")
private String projectNames;
@ExcelProperty("包含过程节点")
private String includedNodes;
@ExcelProperty("默认后台完成运输天数")
private Integer defaultFinishDays;
@ExcelProperty("状态")
private Integer status;
@ExcelProperty("所属组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
private Date updateTime;
@ExcelProperty("创建时间")
private Date createTime;
}
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 过程配置导出 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class ProcessConfigExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("配置编号")
private String configCode;
@ExcelProperty("配置名称")
private String configName;
@ExcelProperty("项目ID集合")
private String projectIds;
@ExcelProperty("项目")
private String projectNames;
@ExcelProperty("包含过程节点")
private String includedNodes;
@ExcelProperty("默认后台完成运输天数")
private Integer defaultFinishDays;
@ExcelProperty("状态")
private Integer status;
@ExcelProperty("所属组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -23,6 +23,7 @@
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -33,6 +34,7 @@ import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 项目立项 Excel
@@ -123,8 +125,10 @@ public class ProjectApplyExcel implements Serializable {
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间")
private LocalDateTime updateTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -23,6 +23,7 @@
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -30,7 +31,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 发货模板 Excel
@@ -59,8 +60,10 @@ public class ShippingTemplateExcel implements Serializable {
@ExcelProperty("备注")
private String remark;
@ExcelProperty("更新时间")
private LocalDateTime updateTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -23,6 +23,7 @@
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -32,7 +33,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 临时额度申请 Excel
@@ -81,8 +82,10 @@ public class TemporaryCreditLimitExcel implements Serializable {
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间")
private LocalDateTime updateTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -38,6 +38,10 @@ import java.time.format.DateTimeFormatter;
/**
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
* <p>
* 数值日期序列号转 ISO 文本,文本原样透传;文本的宽容解析由服务层委托
* {@link org.springblade.common.excel.LenientDateParser} 完成(支持 2026-8-2 等写法,
* 口径见根工作区 docs/import-spec.md)。
*
* @author Chill
*/
@@ -37,7 +37,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 换胎记录导出 Excel
@@ -76,14 +76,14 @@ public class TireReplacementRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -28,7 +28,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 变更记录导出 Excel
@@ -57,14 +57,14 @@ public class TransportChangeRecordExportExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("备注")
private String remark;
@@ -23,6 +23,7 @@
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -31,7 +32,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 运输计划 Excel
@@ -92,8 +93,10 @@ public class TransportPlanExcel implements Serializable {
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间")
private LocalDateTime updateTime;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -37,6 +37,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 违章记录 Excel
@@ -95,14 +96,14 @@ public class ViolationRecordExcel implements Serializable {
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Date updateTime;
@ExcelIgnore
private String errorMessage;
@@ -60,8 +60,11 @@ public class ViolationRecordImportExcel implements Serializable {
@ExcelProperty("*驾驶人")
private String driverName;
@ExcelProperty("*类型/事项")
private String violationTypeOrItem;
@ExcelProperty("*类型")
private String violationType;
@ExcelProperty("*事项")
private String violationItem;
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@@ -1,163 +1,163 @@
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
* 运单管理 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class WaybillExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("运单号")
private String waybillNo;
@ExcelProperty("项目")
private String projectName;
@ExcelProperty("客户合同")
private String contractName;
@ExcelProperty("客户名称")
private String customerName;
@ExcelProperty("运输类型")
private String transportType;
@ExcelProperty("货物名称")
private String cargoName;
@ExcelProperty("货物类型")
private String cargoType;
@ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
private String model;
@ExcelProperty("数量")
private BigDecimal quantity;
@ExcelProperty("数量单位")
private String quantityUnit;
@ExcelProperty("发货地")
private String departureName;
@ExcelProperty("发货地址")
private String departureAddress;
@ExcelProperty("发货联系人")
private String departureContact;
@ExcelProperty("发货联系方式")
private String departurePhone;
@ExcelProperty("收货地")
private String arrivalName;
@ExcelProperty("收货地址")
private String arrivalAddress;
@ExcelProperty("收货联系人")
private String arrivalContact;
@ExcelProperty("收货联系方式")
private String arrivalPhone;
@ExcelProperty("任务录入模式")
private String taskEntryMode;
@ExcelProperty("承运类型")
private String carrierType;
@ExcelProperty("承运商名称")
private String carrierName;
@ExcelProperty("司机姓名")
private String driverName;
@ExcelProperty("司机手机号")
private String driverPhone;
@ExcelProperty("车/船/航班/班列号")
private String vehicleNo;
@ExcelProperty("挂车车牌号")
private String trailerVehicleNo;
@ExcelProperty("押运人")
private String escortName;
@ExcelProperty("押运人手机号")
private String escortPhone;
@ExcelProperty("里程(km)")
private BigDecimal mileage;
@ExcelProperty("预计发货日期")
private LocalDate estimatedStartTime;
@ExcelProperty("预计完成日期")
private LocalDate estimatedEndTime;
@ExcelProperty("单价")
private BigDecimal unitPrice;
@ExcelProperty("计价单位")
private String priceUnit;
@ExcelProperty("其他费用合计")
private BigDecimal otherFeeTotal;
@ExcelProperty("任务备注")
private String taskRemark;
@ExcelProperty("原始单号")
private String originalNo;
@ExcelProperty("业务状态")
private String businessStatus;
@ExcelProperty("数据来源")
private String dataSource;
@ExcelProperty("开始日期")
private LocalDate startDate;
@ExcelProperty("结束日期")
private LocalDate endDate;
@ExcelProperty("计划名称")
private String planName;
@ExcelProperty("多联总单")
private String masterNo;
@ExcelProperty("配载单号")
private String loadingNo;
@ExcelProperty("运单批次号")
private String batchNo;
@ExcelProperty("关联单号")
private String relationNo;
@ExcelProperty("当前过程节点")
private String currentProcessNode;
@ExcelProperty("所属组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelIgnore
private String errorMessage;
}
/**
* 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.excel;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
* 运单管理 Excel
*
* @author Chill
*/
@Data
@ColumnWidth(22)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class WaybillExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("运单号")
private String waybillNo;
@ExcelProperty("项目")
private String projectName;
@ExcelProperty("客户合同")
private String contractName;
@ExcelProperty("客户名称")
private String customerName;
@ExcelProperty("运输类型")
private String transportType;
@ExcelProperty("货物名称")
private String cargoName;
@ExcelProperty("货物类型")
private String cargoType;
@ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
private String model;
@ExcelProperty("数量")
private BigDecimal quantity;
@ExcelProperty("数量单位")
private String quantityUnit;
@ExcelProperty("发货地")
private String departureName;
@ExcelProperty("发货地址")
private String departureAddress;
@ExcelProperty("发货联系人")
private String departureContact;
@ExcelProperty("发货联系方式")
private String departurePhone;
@ExcelProperty("收货地")
private String arrivalName;
@ExcelProperty("收货地址")
private String arrivalAddress;
@ExcelProperty("收货联系人")
private String arrivalContact;
@ExcelProperty("收货联系方式")
private String arrivalPhone;
@ExcelProperty("任务录入模式")
private String taskEntryMode;
@ExcelProperty("承运类型")
private String carrierType;
@ExcelProperty("承运商名称")
private String carrierName;
@ExcelProperty("司机姓名")
private String driverName;
@ExcelProperty("司机手机号")
private String driverPhone;
@ExcelProperty("车/船/航班/班列号")
private String vehicleNo;
@ExcelProperty("挂车车牌号")
private String trailerVehicleNo;
@ExcelProperty("押运人")
private String escortName;
@ExcelProperty("押运人手机号")
private String escortPhone;
@ExcelProperty("里程(km)")
private BigDecimal mileage;
@ExcelProperty(value = "预计发货日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String estimatedStartTime;
@ExcelProperty(value = "预计完成日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String estimatedEndTime;
@ExcelProperty("单价")
private BigDecimal unitPrice;
@ExcelProperty("计价单位")
private String priceUnit;
@ExcelProperty("其他费用合计")
private BigDecimal otherFeeTotal;
@ExcelProperty("任务备注")
private String taskRemark;
@ExcelProperty("原始单号")
private String originalNo;
@ExcelProperty("业务状态")
private String businessStatus;
@ExcelProperty("数据来源")
private String dataSource;
@ExcelProperty(value = "开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String startDate;
@ExcelProperty(value = "结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private String endDate;
@ExcelProperty("计划名称")
private String planName;
@ExcelProperty("多联总单")
private String masterNo;
@ExcelProperty("配载单号")
private String loadingNo;
@ExcelProperty("运单批次号")
private String batchNo;
@ExcelProperty("关联单号")
private String relationNo;
@ExcelProperty("当前过程节点")
private String currentProcessNode;
@ExcelProperty("所属组织")
private String deptName;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新人")
private String updateUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelIgnore
private String errorMessage;
}
@@ -0,0 +1,39 @@
package org.springblade.transport.feign;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 客商档案 Feign实现
*/
@Hidden
@RestController
@AllArgsConstructor
public class CustomerArchiveClient implements ICustomerArchiveClient {
private final ICustomerArchiveService customerArchiveService;
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@PostMapping(SYNC_PROCESS_NODE)
@Override
public FR<Boolean> syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param) {
if (param == null) {
return FR.data(false);
}
if (param.getProcessInfo() != null) {
return FR.data(customerArchiveService.syncProcessNodeFromProcessInfo(param.getId(), param.getProcessInfo()));
}
return FR.data(customerArchiveService.syncProcessNode(
param.getId(), param.getCurrentNode(), param.getCurrentProcessor(), param.getApprovalStatus()));
}
}
@@ -0,0 +1,42 @@
package org.springblade.transport.feign;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.mk.IMkProcessBizHandler;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* MK 业务流程同步 Feign 实现
*/
@Hidden
@RestController
@AllArgsConstructor
public class MkProcessClient implements IMkProcessClient {
private final MkProcessMessageService mkProcessMessageService;
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@PostMapping(APPLY)
@Override
public FR<Boolean> apply(@RequestBody MkProcessSyncDTO param) {
if (param == null || param.getId() == null) {
return FR.data(false);
}
IMkProcessBizHandler handler = mkProcessMessageService.handler(param.getBizType());
String action = param.getAction() == null ? "sync" : param.getAction();
return switch (action) {
case "approve" -> FR.data(handler.approveFromProcess(param.getId(), param.getProcessorName()));
case "reject" -> FR.data(handler.rejectFromProcess(param.getId(), param.getProcessorName()));
default -> FR.data(handler.syncFromProcessInfo(param.getId(), param.getProcessInfo()));
};
}
}
@@ -15,6 +15,7 @@
<result column="customer_code" property="customerCode"/>
<result column="short_name" property="shortName"/>
<result column="full_name" property="fullName"/>
<result column="customer_kind" property="customerKind"/>
<result column="customer_nature" property="customerNature"/>
<result column="guangxi_top100" property="guangxiTop100"/>
<result column="unified_credit_code" property="unifiedCreditCode"/>
@@ -46,6 +47,7 @@
<result column="current_node" property="currentNode"/>
<result column="current_processor" property="currentProcessor"/>
<result column="approved_time" property="approvedTime"/>
<result column="review_status" property="reviewStatus"/>
<result column="kingdee_status" property="kingdeeStatus"/>
<result column="kingdee_fail_reason" property="kingdeeFailReason"/>
<result column="kingdee_sync_time" property="kingdeeSyncTime"/>
@@ -65,6 +67,7 @@
customer_code,
short_name,
full_name,
customer_kind,
customer_nature,
guangxi_top100,
unified_credit_code,
@@ -99,6 +102,15 @@
kingdee_status,
kingdee_fail_reason,
kingdee_sync_time
approved_time,
(
SELECT s.review_status
FROM blade_customer_credit_score s
WHERE s.customer_id = blade_customer_archive.id
AND (s.is_deleted = 0 OR s.is_deleted IS NULL)
ORDER BY s.score_date DESC, s.update_time DESC, s.id DESC
LIMIT 1
) AS review_status
FROM blade_customer_archive
WHERE is_deleted = 0
<if test="userId != null">
@@ -125,6 +137,9 @@
<bind name="creditCodeLike" value="'%' + customer.unifiedCreditCode + '%'"/>
AND unified_credit_code LIKE #{creditCodeLike}
</if>
<if test="customer.customerKind != null and customer.customerKind != ''">
AND customer_kind = #{customer.customerKind}
</if>
<if test="customer.customerNature != null and customer.customerNature != ''">
AND customer_nature = #{customer.customerNature}
</if>
@@ -22,9 +22,15 @@
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springblade.transport.pojo.entity.ProjectApply;
import java.util.List;
import java.util.Map;
/**
* 项目立项 Mapper 接口
*
@@ -32,4 +38,26 @@ import org.springblade.transport.pojo.entity.ProjectApply;
*/
public interface ProjectApplyMapper extends BaseMapper<ProjectApply> {
/**
* 按租户读取业务字典项,供未登录公开页回显下拉文案
*/
@InterceptorIgnore(tenantLine = "true")
@Select("""
SELECT dict_key AS dictKey, dict_value AS dictValue
FROM blade_dict_biz
WHERE is_deleted = 0 AND parent_id <> 0 AND tenant_id = #{tenantId} AND code = #{code}
""")
List<Map<String, Object>> selectDictBizItems(@Param("tenantId") String tenantId, @Param("code") String code);
/**
* 读取系统字典项,供未登录公开页回显下拉文案
*/
@InterceptorIgnore(tenantLine = "true")
@Select("""
SELECT dict_key AS dictKey, dict_value AS dictValue
FROM blade_dict
WHERE is_deleted = 0 AND parent_id <> 0 AND code = #{code}
""")
List<Map<String, Object>> selectDictItems(@Param("code") String code);
}
@@ -28,8 +28,8 @@
d.is_deleted
FROM blade_transport_vehicle_dispatch d
LEFT JOIN blade_transport_vehicle v
ON v.plate_no = d.plate_no
AND v.tenant_id = d.tenant_id
ON v.plate_no COLLATE utf8mb4_general_ci = d.plate_no
AND v.tenant_id COLLATE utf8mb4_general_ci = d.tenant_id
AND v.is_deleted = 0
WHERE d.is_deleted = 0
<if test="dispatch.applicationNo != null and dispatch.applicationNo != ''">
@@ -0,0 +1,17 @@
package org.springblade.transport.mk;
/**
* MK 业务流程处理器
*/
public interface IMkProcessBizHandler {
String bizType();
Object publicDetail(Long id);
boolean syncFromProcessInfo(Long id, Object processInfo);
boolean approveFromProcess(Long id, String processorName);
boolean rejectFromProcess(Long id, String processorName);
}
@@ -0,0 +1,439 @@
package org.springblade.transport.mk;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 各业务 MK 流程处理器
*/
public final class MkProcessBizHandlers {
private MkProcessBizHandlers() {
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class CustomerHandler implements IMkProcessBizHandler {
private final ICustomerArchiveService service;
@Override
public String bizType() {
return "customer-archive";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
return service.syncProcessNodeFromProcessInfo(id, processInfo);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
return service.approveFromProcess(id, processorName);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
return service.rejectFromProcess(id, processorName);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ProjectHandler implements IMkProcessBizHandler {
private final IProjectApplyService service;
@Override
public String bizType() {
return "project-apply";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ProjectApply entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
entity.setApprovalStatus("reviewing");
}
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ProjectApply entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())
|| "change_approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setEffectiveType("formal");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ProjectApply entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|| "change_rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ContractHandler implements IMkProcessBizHandler {
private final IContractManageService service;
@Override
public String bizType() {
return "contract-manage";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ContractManage entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
entity.setApprovalStatus("reviewing");
}
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ContractManage entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())
|| "change_approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ContractManage entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|| "change_rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class WaybillHandler implements IMkProcessBizHandler {
private final IWaybillService service;
@Override
public String bizType() {
return "waybill-manage";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
if (Func.isEmpty(node)) {
return false;
}
entity.setCurrentProcessNode(node);
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核通过");
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核不通过");
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class PreSettlementHandler implements IMkProcessBizHandler {
private final IPreSettlementService service;
@Override
public String bizType() {
return "pre-settlement";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
PreSettlement entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
PreSettlement entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
PreSettlement entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class FormalSettlementHandler implements IMkProcessBizHandler {
private final IFormalSettlementService service;
@Override
public String bizType() {
return "formal-settlement";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
FormalSettlement entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
FormalSettlement entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
FormalSettlement entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class PaymentHandler implements IMkProcessBizHandler {
private final IPaymentApplicationService service;
@Override
public String bizType() {
return "payment-application";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
PaymentApplication entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
PaymentApplication entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
PaymentApplication entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
}
@@ -0,0 +1,191 @@
package org.springblade.transport.mk;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 公开页流程消息:当前节点立即查询,流程实例详情延迟 5 秒查询后同步业务状态。
*/
@Slf4j
@Service
@TenantIgnore
@RequiredArgsConstructor
public class MkProcessMessageService {
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
private final IBusinessProcessClient businessProcessClient;
private final List<IMkProcessBizHandler> handlers;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
Thread thread = new Thread(runnable, "mk-process-info-" + THREAD_INDEX.incrementAndGet());
thread.setDaemon(true);
return thread;
});
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
public IMkProcessBizHandler handler(String bizType) {
Map<String, IMkProcessBizHandler> mapping = handlers.stream()
.collect(Collectors.toMap(IMkProcessBizHandler::bizType, Function.identity(), (a, b) -> a));
IMkProcessBizHandler handler = mapping.get(bizType);
if (handler == null) {
throw new IllegalArgumentException("不支持的MK业务类型:" + bizType);
}
return handler;
}
public void handleProcessMessage(String bizType, Map<String, Object> body) {
IMkProcessBizHandler handler = handler(bizType);
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("公开页流程消息未找到 processId,跳过查询当前节点 bizType={}", bizType);
return;
}
String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId");
queryCurrentNodes(bizType, processId, loginName);
scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues);
}
private void queryCurrentNodes(String bizType, String processId, String loginName) {
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
Object nodeData = result == null ? null : result.getData();
log.info("公开页流程消息当前节点详情 bizType={} processId={} loginName={} result={}",
bizType, processId, loginName, JSON.toJSONString(nodeData));
} catch (Exception e) {
log.error("公开页查询当前节点失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
}
}
private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
String loginName, String bizIdText, Map<String, Object> formValues) {
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId);
scheduler.schedule(
() -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy),
PROCESS_INFO_DELAY_SECONDS,
TimeUnit.SECONDS
);
}
private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
String loginName, String bizIdText, Map<String, Object> formValues) {
try {
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}",
bizType, processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(bizIdText)) {
try {
handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo);
} catch (NumberFormatException e) {
log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText);
}
}
} catch (Exception e) {
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
}
}
private static boolean isProcessFinished(Object processInfo) {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
String processId, Map<String, Object> formValues, boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
if (StringUtil.isBlank(bizIdText)) {
log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId);
return;
}
try {
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
Long bizId = Long.valueOf(bizIdText);
if (approved) {
handler.approveFromProcess(bizId, processorName);
} else {
handler.rejectFromProcess(bizId, processorName);
}
} catch (NumberFormatException e) {
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
}
}
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
}
@@ -0,0 +1,76 @@
package org.springblade.transport.mk;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 解析 MK 流程实例详情中的当前节点、当前处理人
*/
public final class MkProcessNodeHelper {
private MkProcessNodeHelper() {
}
public static String currentProcessor(Object processInfo) {
return joinDistinct(extractTexts(asMap(processInfo).get("currentHandlers"), "fdName", "name"), "、");
}
public static String currentNode(Object processInfo) {
Map<String, Object> taskInfo = asMap(asMap(processInfo).get("fdTaskInfo"));
return joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "、");
}
public static String text(String value, String fallback) {
return Func.isEmpty(value) ? fallback : value.trim();
}
public static Map<String, Object> asMap(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> result = new LinkedHashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
if (value == null) {
return Map.of();
}
try {
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
return parsed == null ? Map.of() : parsed;
} catch (Exception ignored) {
return Map.of();
}
}
private static List<String> extractTexts(Object listObj, String... keys) {
List<String> result = new ArrayList<>();
if (!(listObj instanceof Collection<?> collection) || keys == null) {
return result;
}
for (Object item : collection) {
Map<String, Object> map = asMap(item);
for (String key : keys) {
String text = Func.toStr(map.get(key), "").trim();
if (Func.isNotEmpty(text)) {
result.add(text);
break;
}
}
}
return result;
}
private static String joinDistinct(List<String> values, String delimiter) {
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
}
}
@@ -40,6 +40,14 @@ public interface IContractManageService extends BaseService<ContractManage> {
IPage<ContractManageVO> selectContractManagePage(IPage<ContractManage> page, ContractManageVO contractManage);
ContractManageVO detail(Long id);
/**
* 公开详情,不校验当前用户所属组织
*
* @param id 主键
* @return 合同详情
*/
ContractManageVO publicDetail(Long id);
boolean saveDraft(ContractManage contractManage);
boolean submit(ContractManage contractManage);
boolean toTemporary(Long id);
@@ -66,6 +66,23 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
*/
CustomerArchiveVO detail(Long id);
/**
* 公开查看详情(不校验登录态与数据权限)
*
* @param id 主键
* @return 客商档案详情
*/
CustomerArchiveVO publicDetail(Long id);
/**
* 公开查看变更记录分页(不校验登录态与数据权限)
*
* @param page 分页参数
* @param customerId 客商ID
* @return 变更记录分页
*/
IPage<CustomerChangeRecordVO> publicChangeRecordPage(IPage<CustomerChangeRecordVO> page, Long customerId);
/**
* 新增或修改客商档案
* <p>仅当 {@code customer.recordChange = true}(前端点「提交」)时写入变更记录;「保存」不落变更记录。</p>
@@ -83,6 +100,53 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
*/
boolean submitApproval(Long id);
/**
* 按 MK 当前节点同步客商当前节点、当前处理人和审批状态
*
* @param id 客商ID
* @param currentNode 当前节点
* @param currentProcessor 当前处理人
* @param approvalStatus 审批状态,可为空,为空时保持为审核中
* @return 是否成功
*/
boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus);
/**
* 按 MK getCurrentNodes 返回结果同步客商当前节点
*
* @param id 客商ID
* @param currentNodes MK 当前节点详情
* @return 是否成功
*/
boolean syncProcessNodeFromMk(Long id, Object currentNodes);
/**
* 按 MK 流程实例详情回写当前节点、当前处理人
*
* @param id 客商ID
* @param processInfo MK getProcessInfo 返回数据
* @return 是否成功
*/
boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo);
/**
* MK 流程结束(fdProcessStatus=30)时将客商置为审核通过
*
* @param id 客商ID
* @param processorName 当前处理人,可为空
* @return 是否成功
*/
boolean approveFromProcess(Long id, String processorName);
/**
* MK 流程状态为 20 时将客商置为审核驳回
*
* @param id 客商ID
* @param processorName 当前处理人,可为空
* @return 是否成功
*/
boolean rejectFromProcess(Long id, String processorName);
/**
* 撤回审批并恢复草稿状态
*
@@ -68,6 +68,12 @@ public interface IDriverWaybillService {
*/
DriverWaybillCardVO detail(Long id);
/**
* 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。
* 供调度端 manage/detail 复用。
*/
DriverWaybillCardVO detailPunchSnapshot(Long id);
/**
* 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。
*/
@@ -50,6 +50,8 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
boolean changeRoute(LoadingManage loadingManage);
boolean start(Long id);
boolean cancel(Long id);
boolean complete(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);
}
@@ -40,6 +40,14 @@ public interface IProjectApplyService extends BaseService<ProjectApply> {
IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply);
ProjectApplyVO detail(Long id);
/**
* 公开详情,不校验当前用户所属组织
*
* @param id 主键
* @return 项目立项详情
*/
ProjectApplyVO publicDetail(Long id);
Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply);
Map<String, Object> changeRecordDetail(Long id, Integer recordIndex);
boolean saveDraft(ProjectApply projectApply);
@@ -37,6 +37,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO;
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Collection;
@@ -83,6 +84,12 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
void generateForCompletedWaybills(List<Long> waybillIds);
/**
* 运单维护里程后,同步未挂结算单的应收应付明细里程;
* 对按里程、按吨·公里计费的费用行按新里程重新计算。
*/
void syncMileageFromWaybill(Long waybillId, BigDecimal mileage);
/**
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
@@ -28,6 +28,8 @@ import org.springblade.transport.excel.WaybillExcel;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.pojo.vo.WaybillLocateVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.pojo.vo.WaybillVO;
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
@@ -44,11 +46,37 @@ public interface IWaybillService extends BaseService<Waybill> {
IPage<WaybillVO> selectWaybillPage(IPage<Waybill> page, WaybillVO waybill);
WaybillVO detail(Long id);
/**
* 公开详情,不校验当前用户所属组织
*
* @param id 主键
* @return 运单详情
*/
WaybillVO publicDetail(Long id);
/**
* 管理端:运单打卡记录 + 司机上传凭证图(label=节点-凭证类型)
*/
WaybillPunchRecordsVO listPunchRecords(Long waybillId);
/**
* 运单车辆实时定位(按运单绑定车牌调用 LBS)
*
* @param id 运单ID
* @return 定位结果
*/
WaybillLocateVO locateVehicle(Long id);
/**
* 运单历史轨迹回放(按运单绑定车牌 + 日期区间调用 LBS)
*
* @param id 运单ID
* @param startDate 开始日期 YYYY-MM-DD
* @param endDate 结束日期 YYYY-MM-DD
* @return 轨迹结果
*/
WaybillTrackVO trackVehicle(Long id, String startDate, String endDate);
Waybill syncDriverAcceptState(Waybill waybill);
boolean submit(Waybill waybill);
boolean saveDraft(Waybill waybill);
@@ -60,6 +88,12 @@ public interface IWaybillService extends BaseService<Waybill> {
boolean maintainMileage(WaybillMileageRequest request);
boolean cancel(Long id);
boolean reassign(Waybill waybill);
/**
* 小程序调度端重新派单:跳过管理端部门校验,其余逻辑与 {@link #reassign(Waybill)} 一致。
*/
boolean reassignWithoutDeptCheck(Waybill waybill);
boolean complete(Long id);
/**
@@ -94,6 +94,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
AccidentRecordExcel excel = data.get(index);
try {
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
accidentRecord.setAccidentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getAccidentDate(), "事故发生日期"));
prepare(accidentRecord);
List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
if (Func.isNotEmpty(validationErrors)) {

Some files were not shown because too many files have changed in this diff Show More