list = deptService.deptLeaderInfo(deptId);
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java
index 3979b3ca..0639b52d 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java
@@ -1,263 +1,276 @@
-/**
- * BladeX Commercial License Agreement
- * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
- *
- * Use of this software is governed by the Commercial License Agreement
- * obtained after purchasing a license from BladeX.
- *
- * 1. This software is for development use only under a valid license
- * from BladeX.
- *
- * 2. Redistribution of this software's source code to any third party
- * without a commercial license is strictly prohibited.
- *
- * 3. Licensees may copyright their own code but cannot use segments
- * from this software for such purposes. Copyright of this software
- * remains with BladeX.
- *
- * Using this software signifies agreement to this License, and the software
- * must not be used for illegal purposes.
- *
- * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
- * not liable for any claims arising from secondary or illegal development.
- *
- * Author: Chill Zhuang (bladejava@qq.com)
- */
-package org.springblade.system.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 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> list(PortTerminalVO portTerminal, Query query) {
- IPage 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> portSelect() {
- List 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 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 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 queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
- applyDataSourceCondition(queryWrapper, dataSource);
- if (Func.isNotEmpty(ids)) {
- queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
- }
- List 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 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 params) {
- Object regionCode = params.remove("regionCode");
- if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
- params.put("districtCode", regionCode);
- }
- }
-
- private void applyDataSourceCondition(QueryWrapper 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.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.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 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> list(PortTerminalVO portTerminal, Query query) {
+ IPage 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> portSelect() {
+ List 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("操作成功");
+ }
+
+ /**
+ * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
+ *
+ * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
+ */
+ 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 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 queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
+ applyDataSourceCondition(queryWrapper, dataSource);
+ if (Func.isNotEmpty(ids)) {
+ queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
+ }
+ List 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 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 params) {
+ Object regionCode = params.remove("regionCode");
+ if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
+ params.put("districtCode", regionCode);
+ }
+ }
+
+ private void applyDataSourceCondition(QueryWrapper 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;
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java
index 328fd2af..ab5e7820 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java
@@ -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 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);
}
+ /**
+ * 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密)
+ *
+ * 对外路径:/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));
+ }
+
/**
* 管理员修改密码
*/
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java
new file mode 100644
index 00000000..ecf7b715
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java
@@ -0,0 +1,93 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.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;
+
+/**
+ * 用户手机号变更(小程序「修改手机号」)
+ *
+ * 对外路径:/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;
+
+ /**
+ * 发送短信验证码(需登录)
+ *
+ * 当前手机号、未占用的新手机号均可发送;新号若已被其他账号占用则拒绝。
+ */
+ @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));
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java
index 3199b7ef..d46b3c82 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java
@@ -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();
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java
index 924daee0..28f7004b 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java
@@ -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;
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java
index 941b7eb3..b59573d5 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java
@@ -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;
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java
new file mode 100644
index 00000000..0277ec01
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java
@@ -0,0 +1,60 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.excel;
+
+import org.springblade.core.log.exception.ServiceException;
+
+import java.io.Serial;
+import java.util.List;
+
+/**
+ * 导入失败异常,携带失败明细用于导出原表并标注错误。
+ *
+ * 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
+ * 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
+ *
+ * @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;
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java
index 1dd71acc..306e144f 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java
@@ -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;
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
index b227c4a8..07e04660 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
@@ -232,6 +232,12 @@ public class SysClient implements ISysClient {
.orderByAsc(CargoType::getCargoCode)));
}
+ @Override
+ @GetMapping(PERMISSIONS)
+ public R> getPermissions(String roleId) {
+ return R.data(menuService.permissionCodes(roleId));
+ }
+
@Override
@GetMapping(INVOICE_ITEMS)
public R> getInvoiceItems() {
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java
index 12b97fd0..f25da255 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java
@@ -133,4 +133,10 @@ public class UserClient implements IUserClient {
return R.data(service.remove(Wrappers.query().lambda().in(User::getTenantId, Func.toStrList(tenantIds))));
}
+ @Override
+ @PostMapping(BIND_WX_MINI_OPENID)
+ public R bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) {
+ return R.data(service.bindWxMiniOpenId(tenantId, userId, openid, phone));
+ }
+
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml
index 387f1a27..10fbb903 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml
@@ -13,6 +13,7 @@
+
@@ -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
+
+
+ AND mmu.unit_code LIKE #{unitCodeLike}
+
AND mmu.unit_name LIKE #{unitNameLike}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml
index 6a4dd476..70dd2e9a 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml
@@ -95,7 +95,7 @@
- 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}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java
new file mode 100644
index 00000000..2cbc9a3c
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java
@@ -0,0 +1,55 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.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;
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java
index 48e51020..cde043fe 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java
@@ -146,6 +146,13 @@ public interface IDeptService extends IService {
*/
boolean removeDept(String ids);
+ /**
+ * 删除非顶级组织,顶级组织(父级为 0)保留
+ *
+ * @return 删除数量
+ */
+ int clearNonTopDepts();
+
/**
* 新增或修改部门
*
@@ -156,6 +163,13 @@ public interface IDeptService extends IService {
*/
boolean submit(Dept dept);
+ /**
+ * 从IAM同步管理租户组织。
+
+ * @return 同步处理的组织数量
+ */
+ int syncIamOrganizations();
+
/**
* 按名称与父级查询部门列表(限定当前会话租户)
*
@@ -180,4 +194,16 @@ public interface IDeptService extends IService {
*/
void updateAncestors(Date startTime);
+ /**
+ * 按 OA 主键与上级 OA 主键重建组织、部门父子关系,并刷新祖级列表
+ */
+ void rebuildOaHierarchy();
+
+ /**
+ * 删除 OA 同步后无法挂到上级的组织(上级 OA 主键在本地不存在),并级联删除其子级
+ *
+ * @return 删除数量
+ */
+ int removeOaOrphansWithoutParent();
+
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java
index 6b18066c..2d7c77f3 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java
@@ -77,6 +77,14 @@ public interface IMenuService extends IService {
*/
List buttons(String roleId);
+ /**
+ * 权限标识集合(按钮编号,与前端 GetButtons 叶子 code 一致)
+ *
+ * @param roleId 角色id
+ * @return 权限标识
+ */
+ List permissionCodes(String roleId);
+
/**
* 树形结构
*
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java
index 1c7c1d9a..3e11ad4b 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java
@@ -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);
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java
new file mode 100644
index 00000000..75653ac3
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java
@@ -0,0 +1,63 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.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);
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java
index e1ff5ee5..7c3db61f 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java
@@ -78,6 +78,13 @@ public interface IUserService extends BaseService {
*/
boolean submit(User user);
+ /**
+ * 从IAM同步管理租户账号。
+ *
+ * @return 同步处理的账号数量
+ */
+ int syncIamAccounts();
+
/**
* 修改用户(租户守卫校验用户归属,含账号 / 手机查重)
*
@@ -181,6 +188,11 @@ public interface IUserService extends BaseService {
*/
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 {
boolean registerUser(User user);
/**
- * 新建IAM统一身份认证用户(按可信租户落库)
+ * 新建或补齐IAM统一身份认证用户(按可信租户落库,默认分配角色 1123598816738675203)
*
* @param user 用户实体
* @return 是否成功
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java
index 9e329b39..dea6d264 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java
@@ -125,6 +125,8 @@ public class CurrencyServiceImpl extends BaseServiceImpl 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 implements ID
return removeByIds(idList);
}
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int clearNonTopDepts() {
+ Long nonTopCount = this.count(Wrappers.lambdaQuery()
+ .isNotNull(Dept::getParentId)
+ .ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
+ if (nonTopCount == null || nonTopCount == 0L) {
+ return 0;
+ }
+ this.remove(Wrappers.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 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 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 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.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 allowedCategories;
if (BladeConstant.TOP_PARENT_ID.equals(parent.getParentId())) {
@@ -288,13 +504,24 @@ public class DeptServiceImpl extends ServiceImpl 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 queryWrapper = Wrappers.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 implements ID
if (baseMapper.selectCount(queryWrapper) > 0) {
throw new ServiceException("组织编码已存在");
}
- dept.setDeptCode(deptCode);
}
@Override
@@ -345,24 +571,243 @@ public class DeptServiceImpl extends ServiceImpl implements ID
return UserWrapper.build().listVO(userList);
}
- /**
- * 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
- */
- private void applyTenantScope(QueryWrapper queryWrapper) {
- if (!AuthUtil.isAdministrator()) {
- queryWrapper.lambda().eq(Dept::getTenantId, AuthUtil.getTenantId());
- }
- }
-
-
@Transactional(rollbackFor = Exception.class)
@Override
public void updateAncestors(Date startTime) {
- // 1. 查询需要更新祖级列表的部门
- List list = this.list(Wrappers.lambdaQuery()
- // 祖级列表为空
+ this.refreshAncestors(false, startTime);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public void rebuildOaHierarchy() {
+ List oaDeptList = this.list(Wrappers.lambdaQuery()
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, ""));
+ if (CollectionUtil.isEmpty(oaDeptList)) {
+ log.info("没有记录OA主键的组织,跳过层级重建");
+ return;
+ }
+ Map companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
+ Map departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
+ Map desiredParentIdMap = new LinkedHashMap<>();
+ Map 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 cyclicIds = this.findCyclicDeptIds(desiredParentIdMap);
+ if (CollectionUtil.isNotEmpty(cyclicIds)) {
+ log.warn("检测到OA组织循环引用,跳过层级重建 ids={}", cyclicIds);
+ cyclicIds.forEach(cyclicId -> {
+ desiredParentIdMap.remove(cyclicId);
+ desiredParentCodeMap.remove(cyclicId);
+ });
+ }
+ List 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 oaDeptList = this.list(Wrappers.lambdaQuery()
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, ""));
+ if (CollectionUtil.isEmpty(oaDeptList)) {
+ return 0;
+ }
+ Map companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
+ Map departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
+ Set 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 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 indexByOaId(List oaDeptList, Integer deptCategory) {
+ Map 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 companyByOaId, Map 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 findCyclicDeptIds(Map parentIdMap) {
+ Set cyclicIds = new HashSet<>();
+ for (Long deptId : parentIdMap.keySet()) {
+ Set 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 list = refreshAll ? this.list() : this.list(Wrappers.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 implements ID
log.info("需要更新祖级列表的部门为空");
return;
}
- // 所有公司部门列表
- List allList = this.list().stream()
+ List allList = (refreshAll ? list : this.list()).stream()
.map(dept -> BeanUtil.copyProperties(dept, DeptVO.class))
.toList();
- // id对应的部门map
Map idDeptMap = allList.stream()
.collect(Collectors.toMap(DeptVO::getId, Function.identity(), (a, b) -> b));
- // 获取根节点列表
List rootList = getRootList(allList, idDeptMap);
- // 计算祖级列表
setAncestors(rootList);
-
- // 3. 更新祖级列表
List 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 implements ID
}
}
+ /**
+ * OA 上级链接
+ */
+ private record OaParentLink(Long parentId, String parentCode) {
+ }
+
+ /**
+ * 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
+ */
+ private void applyTenantScope(QueryWrapper queryWrapper) {
+ if (!AuthUtil.isAdministrator()) {
+ queryWrapper.lambda().eq(Dept::getTenantId, AuthUtil.getTenantId());
+ }
+ }
+
/**
* 获取根节点列表
* @param allList
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java
index 615bb673..df18f4f4 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java
@@ -46,6 +46,7 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl 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 queryWrapper = Wrappers.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 queryWrapper = Wrappers.lambdaQuery()
.eq(MeasurementUnit::getUnitName, measurementUnit.getUnitName())
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java
index e04feb2e..7b0ced47 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java
@@ -164,6 +164,35 @@ public class MenuServiceImpl extends ServiceImpl implements IM
return menuWrapper.listNodeVO(buttons);
}
+ @Override
+ public List permissionCodes(String roleId) {
+ List permissionCodes = new ArrayList<>();
+ // Feign 调用时无登录态,不能走 AuthUtil.isAdministrator() 分支;按 roleId 取按钮权限
+ List 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 menuList, List permissionCodes) {
+ if (menuList == null || menuList.isEmpty()) {
+ return;
+ }
+ for (MenuVO menu : menuList) {
+ List children = menu.getChildren();
+ if (children != null && !children.isEmpty()) {
+ collectLeafPermissionCodes(children, permissionCodes);
+ } else if (StringUtil.isNotBlank(menu.getCode())) {
+ permissionCodes.add(menu.getCode());
+ }
+ }
+ }
+
@Override
public List tree() {
return ForestNodeMerger.merge(baseMapper.tree());
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java
index 38e7eb97..7bd93f98 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java
@@ -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 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 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 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 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 oaPersons) {
- if (CollectionUtil.isEmpty(oaPersons)) {
- // 数据为空,直接返回
- return;
- }
- // 根公司id
- String rootCompanyId = getRootCompanyId();
- if (rootCompanyId == null) {
- return;
- }
- // 根公司下要同步的部门id
- Set rootCompanyDeptIds = getRootCompanyDeptIds(rootCompanyId);
- oaPersons = oaPersons.stream()
- // 公司不是根公司,或者部门在根公司下要同步的部门列表中
- .filter(oaPerson -> !rootCompanyId.equals(oaPerson.getSubcompanyid1()) || rootCompanyDeptIds.contains(oaPerson.getDepartmentid()))
- .toList();
- // 根据手机号转成set
- TreeSet oaPersonSet = CollectionUtil.toTreeSet(oaPersons, Comparator.comparing(OAPersonResponse::getMobile));
- // 默认密码
- String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
- // 转换数据
- List users = oaPersonSet.stream()
- // 只需要手机不为空的
- .filter(person -> StringUtils.isNotBlank(person.getMobile()))
- .map(person -> userConvert.person2user(person, defaultPassword))
- .toList();
- List phones = users.stream()
- .map(User::getPhone)
- .filter(StringUtils::isNotBlank)
- .distinct()
- .toList();
- // 查询所有用户
- Map userMap = userService.list(Wrappers.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 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 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 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 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 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 responseData = oaResponse.getData();
+ List 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 userDeptMap = userDeptService.list(Wrappers.lambdaQuery()
- .in(UserDept::getUserId, userMap.values())
- ).stream()
- .collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (a, b) -> b));
- // 数据库存在的所有用户部门id
- Set 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 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 responseData = oaResponse.getData();
+ List 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 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 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 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 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 userIds = userMap.values();
- List list = userDeptService.queryUserDeptIds(userIds);
- // 查询没有角色的用户id
- Set noRoleUserIds = userService.list(Wrappers.lambdaQuery()
- .in(User::getId, userIds)
- .isNull(User::getRoleId)
- ).stream()
- .map(User::getId)
- .collect(Collectors.toSet());
- // 获取默认角色id
- String defaultRoleId = getDefaultRoleId();
- List 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 responseData = oaResponse.getData();
+ List 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 handleCompany(List oaCompanies) {
+ private OrgSyncCount handleCompany(List oaCompanies, boolean syncAll) {
if (CollectionUtil.isEmpty(oaCompanies)) {
- // 数据为空,直接返回
- return Collections.emptyList();
+ return OrgSyncCount.empty();
}
// 获取需要的公司名称
- Set companyNames = getCompanyNames();
+ Set companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
// 转换数据
List 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 deptMap = getAllCompanyDeptMap();
// 处理部门
- return handleDept(allParam, deptMap, DeptCategory.COMPANY);
+ List 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 handleDept(List oaDepts) {
+ private OrgSyncCount handleDept(List oaDepts, boolean syncAll) {
if (CollectionUtil.isEmpty(oaDepts)) {
- // 数据为空,直接返回
- return Collections.emptyList();
+ return OrgSyncCount.empty();
}
// 查询所有公司的编码和id的map
Map 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 rootCompanyDeptNames = getRootCompanyDeptNames();
+ Set rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
// 转换数据
List 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 deptCodes = allParam.stream()
- .map(Dept::getDeptCode)
- .collect(Collectors.toSet());
- // 查询数据库的部门,转换成map
- Map deptMap = deptService.list(Wrappers.lambdaQuery()
+ Set 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 deptMap = deptCodes.isEmpty() ? new HashMap<>() : deptService.list(Wrappers.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 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 list = allParam.stream()
- .filter(dept -> dept.getParentId() != null)
+ .filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
.toList();
// 不存在的新增
List 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 companyNames = this.getCompanyNames();
- if (companyNames.isEmpty()) {
+ private static class OrgSyncCount {
+ private final int syncedCount;
+ private final int skippedCount;
+ private final List notHandledList;
+
+ private OrgSyncCount(int syncedCount, int skippedCount, List 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 getNotHandledList() {
+ return notHandledList;
+ }
+ }
+
+ /**
+ * 获取 OA 部门查询使用的子公司 id
+ *
+ * @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司
+ * @return 逗号分隔的 OA 公司 id
+ */
+ private String getOaSubCompanyIds(boolean syncAll) {
+ List companyList;
+ if (syncAll) {
+ companyList = deptService.list(Wrappers.lambdaQuery()
+ .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ );
+ } else {
+ Set companyNames = this.getCompanyNames();
+ if (companyNames.isEmpty()) {
+ return null;
+ }
+ companyList = deptService.list(Wrappers.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.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();
- }
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java
new file mode 100644
index 00000000..6a8a1884
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java
@@ -0,0 +1,609 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.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 oaPersons) {
+ OaOrgIndex orgIndex = new OaOrgIndex();
+ if (CollectionUtil.isEmpty(oaPersons)) {
+ return orgIndex;
+ }
+ Date orgSyncStart = new Date();
+ Dept rootCompany = this.getOrCreateRootCompany();
+ String tenantId = resolveTenantId();
+ Map companyPersonMap = new LinkedHashMap<>();
+ Map 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 existingCompanyByCode = new HashMap<>();
+ Map existingCompanyByName = new HashMap<>();
+ deptService.list(Wrappers.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 addCompanies = new ArrayList<>();
+ List 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 existingDeptByCode = new HashMap<>();
+ Map existingDeptByParentAndName = new HashMap<>();
+ List companyIds = orgIndex.companyByOaId.values().stream()
+ .map(Dept::getId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (CollectionUtil.isNotEmpty(companyIds)) {
+ deptService.list(Wrappers.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 addDepartments = new ArrayList<>();
+ List 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 oaPersons, OaOrgIndex orgIndex) {
+ if (CollectionUtil.isEmpty(oaPersons)) {
+ return new PersonSyncCount(0, 0);
+ }
+ Map 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 users = uniquePersonMap.values().stream()
+ .map(person -> {
+ User user = userConvert.person2user(person, defaultPassword);
+ user.setTenantId(tenantId);
+ return user;
+ })
+ .toList();
+ List accounts = users.stream()
+ .map(User::getAccount)
+ .filter(StringUtils::isNotBlank)
+ .distinct()
+ .toList();
+ List phones = users.stream()
+ .map(User::getPhone)
+ .filter(StringUtils::isNotBlank)
+ .distinct()
+ .toList();
+ Map existingByAccount = new HashMap<>();
+ Map existingByPhone = new HashMap<>();
+ userService.list(Wrappers.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 userMap = new HashMap<>();
+ Set 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 addUsers = users.stream()
+ .filter(user -> !existsUserIds.contains(user.getId()))
+ .toList();
+ if (CollectionUtil.isNotEmpty(addUsers)) {
+ ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size());
+ userService.saveBatch(addUsers);
+ }
+ List 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 userDeptMap = userDeptService.list(Wrappers.lambdaQuery()
+ .in(UserDept::getUserId, userMap.values())
+ ).stream()
+ .collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second));
+ Set existsUserDeptIds = new HashSet<>(userDeptMap.values());
+ List 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 addList = userDeptList.stream()
+ .filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
+ .toList();
+ if (CollectionUtil.isNotEmpty(addList)) {
+ ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size());
+ userDeptService.saveBatch(addList);
+ }
+ List updateList = userDeptList.stream()
+ .filter(userDept -> existsUserDeptIds.contains(userDept.getId()))
+ .toList();
+ if (CollectionUtil.isNotEmpty(updateList)) {
+ ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size());
+ userDeptService.updateBatchById(updateList);
+ }
+ Collection userIds = userMap.values();
+ List list = userDeptService.queryUserDeptIds(userIds);
+ Set noRoleUserIds = userService.list(Wrappers.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 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.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 addList, List 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 addList, List 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 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 roleList = roleService.list(Wrappers.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 companyByOaId = new HashMap<>();
+ private final Map deptByOaId = new HashMap<>();
+ private final Map 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;
+ }
+ }
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java
index fe3e2c40..892f4d62 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java
@@ -1,694 +1,640 @@
-/**
- * BladeX Commercial License Agreement
- * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
- *
- * Use of this software is governed by the Commercial License Agreement
- * obtained after purchasing a license from BladeX.
- *
- * 1. This software is for development use only under a valid license
- * from BladeX.
- *
- * 2. Redistribution of this software's source code to any third party
- * without a commercial license is strictly prohibited.
- *
- * 3. Licensees may copyright their own code but cannot use segments
- * from this software for such purposes. Copyright of this software
- * remains with BladeX.
- *
- * Using this software signifies agreement to this License, and the software
- * must not be used for illegal purposes.
- *
- * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
- * not liable for any claims arising from secondary or illegal development.
- *
- * Author: Chill Zhuang (bladejava@qq.com)
- */
-package org.springblade.system.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import lombok.AllArgsConstructor;
-import org.springblade.core.log.exception.ServiceException;
-import org.springblade.core.mp.base.BaseServiceImpl;
-import org.springblade.core.tool.utils.BeanUtil;
-import org.springblade.core.tool.utils.Func;
-import org.springblade.system.cache.UserCache;
-import org.springblade.system.excel.PortTerminalExcel;
-import org.springblade.system.excel.PortTerminalExportExcel;
-import org.springblade.system.mapper.PortTerminalMapper;
-import org.springblade.system.pojo.entity.PortTerminal;
-import org.springblade.system.pojo.entity.Region;
-import org.springblade.system.pojo.vo.PortTerminalVO;
-import org.springblade.system.service.IPortTerminalService;
-import org.springblade.system.service.IRegionService;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.transaction.interceptor.TransactionAspectSupport;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.util.regex.Pattern;
-
-/**
- * 港口码头主数据 服务实现类
- *
- * @author Chill
- */
-@Service
-@AllArgsConstructor
-public class PortTerminalServiceImpl extends BaseServiceImpl implements IPortTerminalService {
-
- private static final String CATEGORY_PORT = "港口";
- private static final String CATEGORY_TERMINAL = "码头";
- 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_BATCH = "批量导入";
- private static final String SOURCE_MANUAL = "手动录入";
- private static final String SOURCE_MANUAL_OLD = "手工导入";
- private static final int STATUS_ENABLED = 1;
- private static final int STATUS_DISABLED = 2;
- private static final int CODE_MAX_LENGTH = 30;
- private static final int NAME_MAX_LENGTH = 100;
- private static final int REGION_MAX_LENGTH = 50;
- private static final int REGION_CODE_MAX_LENGTH = 32;
- private static final int DETAIL_ADDRESS_MAX_LENGTH = 255;
- private static final int REMARK_MAX_LENGTH = 200;
- private static final BigDecimal MIN_LONGITUDE = new BigDecimal("-180");
- private static final BigDecimal MAX_LONGITUDE = new BigDecimal("180");
- private static final BigDecimal MIN_LATITUDE = new BigDecimal("-90");
- private static final BigDecimal MAX_LATITUDE = new BigDecimal("90");
- private static final Pattern PORT_CODE_PATTERN = Pattern.compile("^[A-Z]{5}$");
- private static final Pattern TERMINAL_CODE_PATTERN = Pattern.compile("^[A-Z]{5}-[A-Z0-9]+$");
-
- private final IRegionService regionService;
-
- @Override
- public IPage selectPortTerminalPage(IPage