From d2f49723d6bdb78283cc7dee8c3176c0657f2bce Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Sun, 11 Jan 2026 10:14:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(upms):=20=E9=87=8D=E6=9E=84=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=99=A8=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=92=89=E9=92=89=E5=92=8C=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1Excel=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化钉钉部门同步逻辑,提取常量到 ConnectConstants - 新增企业微信部门及用户Excel导入功能 - 实现钉钉部门和用户Excel导入功能 - 移除API同步功能,统一使用导入方式 Co-Authored-By: Claude Opus 4.5 --- .../admin/api/constant/ConnectConstants.java | 73 ++ .../admin/api/vo/DingTalkDeptExcelVO.java | 35 + .../pigx/admin/api/vo/DingUserExcelVo.java | 11 +- .../pigx/admin/api/vo/WeChatDeptExcelVO.java | 36 + .../pigx/admin/api/vo/WeChatUserExcelVO.java | 85 ++ .../admin/controller/ConnectController.java | 68 +- .../pigx/admin/service/ConnectService.java | 44 +- .../service/impl/ConnectServiceImpl.java | 754 +++++++++++------- .../src/main/resources/file/cp.xlsx | Bin 0 -> 10910 bytes .../src/main/resources/file/dingtalk.xlsx | Bin 0 -> 13663 bytes 10 files changed, 776 insertions(+), 330 deletions(-) create mode 100644 pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/constant/ConnectConstants.java create mode 100644 pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingTalkDeptExcelVO.java create mode 100644 pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatDeptExcelVO.java create mode 100644 pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatUserExcelVO.java create mode 100644 pigx-upms/pigx-upms-biz/src/main/resources/file/cp.xlsx create mode 100644 pigx-upms/pigx-upms-biz/src/main/resources/file/dingtalk.xlsx diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/constant/ConnectConstants.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/constant/ConnectConstants.java new file mode 100644 index 000000000..5f17ba134 --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/constant/ConnectConstants.java @@ -0,0 +1,73 @@ +package com.pig4cloud.pigx.admin.api.constant; + +/** + * 第三方连接常量类 + *

+ * 用于定义第三方平台(如钉钉)连接相关的字段名称和常量值 + * + * @author lengleng + * @date 2026/01/10 + */ +public interface ConnectConstants { + + /** + * JSON字段名: 手机号 + */ + String FIELD_MOBILE = "mobile"; + + /** + * JSON字段名: 头像 + */ + String FIELD_AVATAR = "avatar"; + + /** + * JSON字段名: 昵称 + */ + String FIELD_NICKNAME = "nickname"; + + /** + * JSON字段名: 姓名 + */ + String FIELD_NAME = "name"; + + /** + * JSON字段名: 邮箱 + */ + String FIELD_EMAIL = "email"; + + /** + * JSON字段名: 部门ID + */ + String FIELD_DEPT_ID = "dept_id"; + + /** + * JSON字段名: 父部门ID + */ + String FIELD_PARENT_ID = "parent_id"; + + /** + * JSON字段名: 部门顺序列表 + */ + String FIELD_DEPT_ORDER_LIST = "dept_order_list"; + + /** + * JSON字段名: 用户ID列表 + */ + String FIELD_USERID_LIST = "userid_list"; + + /** + * JSON字段名: 返回结果 + */ + String FIELD_RESULT = "result"; + + /** + * 语言代码: 简体中文 + */ + String LANGUAGE_ZH_CN = "zh_CN"; + + /** + * 钉钉根部门ID + */ + Long ROOT_DEPT_ID = 1L; + +} diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingTalkDeptExcelVO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingTalkDeptExcelVO.java new file mode 100644 index 000000000..d8b4facae --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingTalkDeptExcelVO.java @@ -0,0 +1,35 @@ +package com.pig4cloud.pigx.admin.api.vo; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import com.pig4cloud.pigx.common.excel.annotation.ExcelLine; +import lombok.Data; + +import java.io.Serializable; + +/** + * 钉钉部门导入VO + * 用于解析钉钉导出的Excel中的部门数据 + * + * @author lengleng + * @date 2026-01-10 + */ +@Data +public class DingTalkDeptExcelVO implements Serializable { + + /** + * 导入时回显行号(用于错误定位) + */ + @ExcelLine + @ExcelIgnore + private Long lineNum; + + /** + * 部门路径 + * 格式: "公司/部门A/子部门B" (用"/"表示层级关系) + * 同一行可能包含多个部门路径,用";"分隔 + */ + @ExcelProperty("部门") + private String deptPath; + +} diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingUserExcelVo.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingUserExcelVo.java index f7a34e0cd..b0f205768 100644 --- a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingUserExcelVo.java +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/DingUserExcelVo.java @@ -39,17 +39,24 @@ public class DingUserExcelVo implements Serializable { private String name; /** - * 部门名称 + * 部门路径 + * 格式: "公司/部门A/子部门B" (用"/"表示层级关系) + * 支持多部门: "部门1;部门2" (用英文分号分隔,第一个为主部门) */ @NotBlank(message = "部门不能为空") @ExcelProperty("部门") - private String deptName; + private String deptPath; + /** + * 邮箱 + */ @ExcelProperty("邮箱") private String email; /** * 锁定标记 + * 0: 正常(激活) + * 9: 锁定(未激活) */ @ExcelProperty("激活状态,0:是,9:否") private String lockFlag; diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatDeptExcelVO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatDeptExcelVO.java new file mode 100644 index 000000000..bfe21e4fc --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatDeptExcelVO.java @@ -0,0 +1,36 @@ +package com.pig4cloud.pigx.admin.api.vo; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import com.pig4cloud.pigx.common.excel.annotation.ExcelLine; +import lombok.Data; + +import java.io.Serializable; + +/** + * 企业微信部门导入VO + * 用于解析企业微信导出的通讯录Excel中的部门数据 + * + * @author lengleng + * @date 2026-01-10 + */ +@Data +public class WeChatDeptExcelVO implements Serializable { + + /** + * 导入时回显行号(用于错误定位) + */ + @ExcelLine + @ExcelIgnore + private Long lineNum; + + /** + * 部门路径 + * 格式: "腾讯公司/微信事业群/广州研发部" (用"/"表示层级关系) + * 同一行可能包含多个部门路径,用";"分隔,如: "部门路径1;部门路径2" + * 企业微信Excel中对应"部门"列(第5列,索引4) + */ + @ExcelProperty(value = "部门", index = 4) + private String deptPath; + +} diff --git a/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatUserExcelVO.java b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatUserExcelVO.java new file mode 100644 index 000000000..40e153b85 --- /dev/null +++ b/pigx-upms/pigx-upms-api/src/main/java/com/pig4cloud/pigx/admin/api/vo/WeChatUserExcelVO.java @@ -0,0 +1,85 @@ +package com.pig4cloud.pigx.admin.api.vo; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import com.pig4cloud.pigx.common.excel.annotation.ExcelLine; +import lombok.Data; + +import java.io.Serializable; + +/** + * 企业微信用户导入VO + * 用于解析企业微信导出的通讯录Excel中的用户数据 + * + * @author lengleng + * @date 2026-01-10 + */ +@Data +public class WeChatUserExcelVO implements Serializable { + + /** + * 导入时回显行号(用于错误定位) + */ + @ExcelLine + @ExcelIgnore + private Long lineNum; + + /** + * 姓名 + */ + @ExcelProperty(value = "姓名", index = 0) + private String name; + + /** + * 账号 (userid) + * 企业微信中的唯一标识 + */ + @ExcelProperty(value = "账号", index = 1) + private String account; + + /** + * 别名 + */ + @ExcelProperty(value = "别名", index = 2) + private String alias; + + /** + * 职务 + */ + @ExcelProperty(value = "职务", index = 3) + private String position; + + /** + * 部门路径 + * 格式: "腾讯公司/微信事业群/广州研发部" + * 支持多部门: "部门1;部门2" (用英文分号分隔,第一个为主部门) + * 实际Excel列:第5列(索引4) + */ + @ExcelProperty(value = "部门", index = 4) + private String deptPath; + + /** + * 性别 (男/女) + */ + @ExcelProperty(value = "性别", index = 5) + private String gender; + + /** + * 手机号 (关键字段,用于判重) + */ + @ExcelProperty(value = "手机", index = 6) + private String phone; + + /** + * 座机 + */ + @ExcelProperty(value = "座机", index = 7) + private String telephone; + + /** + * 邮箱 + */ + @ExcelProperty(value = "邮箱", index = 8) + private String email; + +} diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/ConnectController.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/ConnectController.java index 3cf57aaf4..70252abe0 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/ConnectController.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/controller/ConnectController.java @@ -1,18 +1,24 @@ package com.pig4cloud.pigx.admin.controller; -import com.pig4cloud.pigx.admin.api.entity.SysDept; +import com.pig4cloud.pigx.admin.api.vo.DingTalkDeptExcelVO; +import com.pig4cloud.pigx.admin.api.vo.DingUserExcelVo; +import com.pig4cloud.pigx.admin.api.vo.WeChatDeptExcelVO; +import com.pig4cloud.pigx.admin.api.vo.WeChatUserExcelVO; import com.pig4cloud.pigx.admin.service.ConnectService; -import com.pig4cloud.pigx.admin.service.SysDeptService; import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.common.excel.annotation.RequestExcel; import com.pig4cloud.pigx.common.security.annotation.HasPermission; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** * 钉钉、微信 互联 * @@ -28,50 +34,56 @@ public class ConnectController { private final ConnectService connectService; - private final SysDeptService deptService; - /** - * 同步钉钉用户信息 - * @return 操作结果 + * 导入企业微信部门 + * + * @param excelVOList Excel数据列表(通过@RequestExcel自动解析) + * @param bindingResult 校验结果 + * @return 导入结果 */ - @PostMapping("/sync/ding/user") + @PostMapping("/import/wecom/dept") @HasPermission("sys_connect_sync") - public R syncUser() { - for (SysDept sysDept : deptService.list()) { - connectService.syncDingUser(sysDept.getDeptId()); - } - return R.ok(); + public R importWeComDept(@RequestExcel(headRowNumber = 9) List excelVOList, BindingResult bindingResult) { + return connectService.importWeChatDept(excelVOList, bindingResult); } /** - * 同步钉钉部门信息 - * @return 同步操作结果 - * @see R + * 导入企业微信用户 + * + * @param excelVOList Excel数据列表(通过@RequestExcel自动解析) + * @param bindingResult 校验结果 + * @return 导入结果 */ - @PostMapping("/sync/ding/dept") + @PostMapping("/import/wecom/user") @HasPermission("sys_connect_sync") - public R syncDept() { - return R.ok(connectService.syncDingDept()); + public R importWeComUser(@RequestExcel(headRowNumber = 9) List excelVOList, BindingResult bindingResult) { + return connectService.importWeChatUser(excelVOList, bindingResult); } /** - * 同步企业微信用户信息 - * @return 同步操作结果 + * 导入钉钉部门 + * + * @param excelVOList Excel数据列表(通过@RequestExcel自动解析) + * @param bindingResult 校验结果 + * @return 导入结果 */ - @PostMapping("/sync/cp/user") + @PostMapping("/import/ding/dept") @HasPermission("sys_connect_sync") - public R syncCpUser() { - return connectService.syncCpUser(); + public R importDingDept(@RequestExcel(headRowNumber = 3) List excelVOList, BindingResult bindingResult) { + return connectService.importDingTalkDept(excelVOList, bindingResult); } /** - * 同步企业微信部门信息 - * @return 包含同步结果的响应对象 + * 导入钉钉用户 + * + * @param excelVOList Excel数据列表(通过@RequestExcel自动解析) + * @param bindingResult 校验结果 + * @return 导入结果 */ - @PostMapping("/sync/cp/dept") + @PostMapping("/import/ding/user") @HasPermission("sys_connect_sync") - public R syncCpDept() { - return connectService.syncCpDept(); + public R importDingUser(@RequestExcel(headRowNumber = 3) List excelVOList, BindingResult bindingResult) { + return connectService.importDingTalkUser(excelVOList, bindingResult); } } diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/ConnectService.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/ConnectService.java index 5a72198a4..9f6213e71 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/ConnectService.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/ConnectService.java @@ -1,7 +1,13 @@ package com.pig4cloud.pigx.admin.service; +import com.pig4cloud.pigx.admin.api.vo.DingTalkDeptExcelVO; +import com.pig4cloud.pigx.admin.api.vo.DingUserExcelVo; +import com.pig4cloud.pigx.admin.api.vo.WeChatDeptExcelVO; +import com.pig4cloud.pigx.admin.api.vo.WeChatUserExcelVO; import com.pig4cloud.pigx.common.core.util.R; -import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.springframework.validation.BindingResult; + +import java.util.List; /** * @author lengleng @@ -12,27 +18,39 @@ import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; public interface ConnectService { /** - * 同步钉钉部门 + * 导入企业微信部门 + * + * @param excelVOList Excel数据列表 + * @param bindingResult 校验结果 + * @return 导入结果 */ - Boolean syncDingDept(); + R importWeChatDept(List excelVOList, BindingResult bindingResult); /** - * 同步钉钉用户 + * 导入企业微信用户 + * + * @param excelVOList Excel数据列表 + * @param bindingResult 校验结果 + * @return 导入结果 */ - R syncDingUser(Long deptId); + R importWeChatUser(List excelVOList, BindingResult bindingResult); /** - * 同步企微部门 - * @return + * 导入钉钉部门 + * + * @param excelVOList Excel数据列表 + * @param bindingResult 校验结果 + * @return 导入结果 */ - R syncCpDept(); + R importDingTalkDept(List excelVOList, BindingResult bindingResult); /** - * 同步企微用户 - * @return + * 导入钉钉用户 + * + * @param excelVOList Excel数据列表 + * @param bindingResult 校验结果 + * @return 导入结果 */ - R syncCpUser(); - - WxCpDefaultConfigImpl getCpConfig(); + R importDingTalkUser(List excelVOList, BindingResult bindingResult); } diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/ConnectServiceImpl.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/ConnectServiceImpl.java index dcb067e23..2bb24e217 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/ConnectServiceImpl.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/ConnectServiceImpl.java @@ -1,57 +1,37 @@ package com.pig4cloud.pigx.admin.service.impl; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.dingtalk.api.DefaultDingTalkClient; -import com.dingtalk.api.DingTalkClient; -import com.dingtalk.api.request.OapiGettokenRequest; -import com.dingtalk.api.request.OapiUserListidRequest; -import com.dingtalk.api.request.OapiV2DepartmentListsubRequest; -import com.dingtalk.api.request.OapiV2UserGetRequest; -import com.dingtalk.api.response.OapiGettokenResponse; -import com.dingtalk.api.response.OapiUserListidResponse; -import com.dingtalk.api.response.OapiV2DepartmentListsubResponse; -import com.dingtalk.api.response.OapiV2UserGetResponse; -import com.pig4cloud.pigx.admin.api.constant.UpmsErrorCodes; import com.pig4cloud.pigx.admin.api.dto.UserDTO; import com.pig4cloud.pigx.admin.api.entity.SysDept; -import com.pig4cloud.pigx.admin.api.entity.SysSocialDetails; import com.pig4cloud.pigx.admin.api.entity.SysUser; -import com.pig4cloud.pigx.admin.mapper.SysSocialDetailsMapper; import com.pig4cloud.pigx.admin.service.ConnectService; import com.pig4cloud.pigx.admin.service.SysDeptService; import com.pig4cloud.pigx.admin.service.SysUserService; -import com.pig4cloud.pigx.common.core.constant.SecurityConstants; -import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum; -import com.pig4cloud.pigx.common.core.util.MsgUtils; import com.pig4cloud.pigx.common.core.util.R; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.cp.api.WxCpService; -import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; -import me.chanjar.weixin.cp.bean.WxCpDepart; -import me.chanjar.weixin.cp.bean.WxCpUser; -import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; -import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; +import java.util.ArrayList; + +import com.pig4cloud.pigx.admin.api.vo.DingTalkDeptExcelVO; +import com.pig4cloud.pigx.admin.api.vo.DingUserExcelVo; +import com.pig4cloud.pigx.admin.api.vo.WeChatDeptExcelVO; +import com.pig4cloud.pigx.admin.api.vo.WeChatUserExcelVO; +import com.pig4cloud.pigx.common.excel.vo.ErrorMessage; +import org.springframework.validation.BindingResult; + /** - * 互联平台实现 + * 互联平台服务实现类 *

- * 钉钉: https://open-dev.dingtalk.com/apiExplorer + * 提供企业微信和钉钉组织架构导入功能,包括部门导入和用户导入 * * @author lengleng * @date 2022/4/22 @@ -62,306 +42,506 @@ import java.util.List; @RequiredArgsConstructor public class ConnectServiceImpl implements ConnectService { - private final SysSocialDetailsMapper sysSocialDetailsMapper; - + /** + * 部门服务 + */ private final SysDeptService sysDeptService; + /** + * 用户服务 + */ private final SysUserService sysUserService; - // JSON field name constants for DingTalk/WeChat API - private static final String FIELD_MOBILE = "mobile"; - - private static final String FIELD_AVATAR = "avatar"; - - private static final String FIELD_NICKNAME = "nickname"; - - private static final String FIELD_NAME = "name"; - - private static final String FIELD_EMAIL = "email"; - - private static final String FIELD_DEPT_ID = "dept_id"; - - private static final String FIELD_PARENT_ID = "parent_id"; - - private static final String FIELD_DEPT_ORDER_LIST = "dept_order_list"; - - private static final String FIELD_USERID_LIST = "userid_list"; - - private static final String FIELD_RESULT = "result"; - - // WeChat Enterprise config field name constants - private static final String FIELD_AGENT_ID = "agentId"; - - private static final String FIELD_TOKEN = "token"; - - private static final String FIELD_AES_KEY = "aesKey"; - - // Language code constant - private static final String LANGUAGE_ZH_CN = "zh_CN"; /** - * 同步钉钉部门 + * 导入企业微信部门 + *

+ * 解析企业微信导出的通讯录Excel,支持两种格式: + * 1. 单部门路径: "腾讯公司/微信事业群/广州研发部" + * 2. 多部门路径: "部门A;部门B" (用分号分隔) + *

+ * 处理逻辑: + * - 解析部门路径,从顶级部门开始逐级创建 + * - 如果部门已存在(相同父部门下同名),则更新部门信息(覆盖模式) + * - 自动维护部门的 parentId 关系 * - * @return Boolean + * @param excelVOList Excel数据列表 + * @param bindingResult 字段级别校验结果 + * @return 导入结果 */ @Override @Transactional(rollbackFor = Exception.class) - public Boolean syncDingDept() { - OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest(); - req.setDeptId(1L); - req.setLanguage(LANGUAGE_ZH_CN); + public R importWeChatDept(List excelVOList, BindingResult bindingResult) { + List errorMessageList = (List) bindingResult.getTarget(); - // 数据库中现有的部门 - List deptList = sysDeptService.list(Wrappers.emptyWrapper()); + // 加载现有部门缓存 + Map deptKeyMap = sysDeptService.list(Wrappers.emptyWrapper()).stream() + .collect(Collectors.toMap( + dept -> buildDeptKey(dept.getParentId(), dept.getName()), + dept -> dept, + (v1, v2) -> v1 + )); - // 查询根部门下的所有部门 - List insertDept = queryChildDept(req, getDingAccessToken(), deptList); + int successCount = 0; - // 去重 - List syncDept = insertDept.stream().distinct().toList(); - - // 保存部门以及部门关系 - if (CollectionUtils.isNotEmpty(syncDept)) { - syncDept.forEach(sysDeptService::save); - } - - return Boolean.TRUE; - } - - /** - * 同步部门下的钉钉用户 - * - * @param deptId 部门ID - * @return R - */ - @SneakyThrows - @Override - @Transactional(rollbackFor = Exception.class) - public R syncDingUser(Long deptId) { - // 数据库中现有用户 - List users = sysUserService.list(Wrappers.emptyWrapper()); - - String token = getDingAccessToken(); - - // 查询部门下的用户id列表 - DingTalkClient client = new DefaultDingTalkClient(SecurityConstants.DING_DEPT_USERIDS_URL); - OapiUserListidRequest req = new OapiUserListidRequest(); - req.setDeptId(deptId); - OapiUserListidResponse rsp = client.execute(req, token); - - JSONObject jsonObject = JSONUtil.parseObj(rsp.getBody()); - JSONObject result = jsonObject.getJSONObject(FIELD_RESULT); - - if (result == null) { - return R.ok(); - } - List useridList = result.get(FIELD_USERID_LIST, List.class); - - // 查询用户详情 - for (String userid : useridList) { - DingTalkClient userClient = new DefaultDingTalkClient(SecurityConstants.DING_USER_INFO_URL); - OapiV2UserGetRequest userGetRequest = new OapiV2UserGetRequest(); - userGetRequest.setUserid(userid); - OapiV2UserGetResponse userGetResponse = userClient.execute(userGetRequest, token); - JSONObject userResult = JSONUtil.parseObj(userGetResponse.getBody()); - JSONObject userObj = JSONUtil.parseObj(userResult.get(FIELD_RESULT)); - - boolean exist = users.stream() - .filter(user -> StrUtil.isNotBlank(user.getPhone())) - .anyMatch(user -> user.getPhone().equals(userObj.getStr(FIELD_MOBILE))); - - if (exist || StrUtil.isBlank(userObj.getStr(FIELD_MOBILE))) { - log.info("用户已存在或手机号不合法 {}", userid); + for (WeChatDeptExcelVO excel : excelVOList) { + if (StrUtil.isBlank(excel.getDeptPath())) { + errorMessageList.add(new ErrorMessage(excel.getLineNum(), Set.of("部门路径不能为空"))); continue; } - // 用户部门信息 - JSONArray deptOrders = userObj.getJSONArray(FIELD_DEPT_ORDER_LIST); - JSONObject deptInfo = (JSONObject) deptOrders.get(0); - - // 用户信息 - UserDTO userDTO = new UserDTO(); - userDTO.setDeptId(deptInfo.getLong(FIELD_DEPT_ID)); - userDTO.setAvatar(userObj.getStr(FIELD_AVATAR)); - userDTO.setUsername(userObj.getStr(FIELD_MOBILE)); - userDTO.setPhone(userObj.getStr(FIELD_MOBILE)); - userDTO.setNickname(userObj.getStr(FIELD_NICKNAME)); - userDTO.setName(userObj.getStr(FIELD_NAME)); - userDTO.setEmail(userObj.getStr(FIELD_EMAIL)); - // 初始化密码为 手机号 - userDTO.setPassword(userObj.getStr(FIELD_MOBILE)); - userDTO.setPasswordModifyTime(LocalDateTime.now()); - sysUserService.saveUser(userDTO); + for (String deptPath : excel.getDeptPath().split(";")) { + deptPath = deptPath.trim(); + if (StrUtil.isNotBlank(deptPath)) { + successCount += createDeptHierarchy(deptPath, deptKeyMap); + } + } } - return R.ok(); + if (CollUtil.isNotEmpty(errorMessageList)) { + return R.failed(errorMessageList); + } + return R.ok(null, String.format("部门导入成功! 新增: %d 个", successCount)); } /** - * 同步企微部门 - * - * @return R + * 构建部门唯一键 (parentId + name) */ - @Override - @Transactional(rollbackFor = Exception.class) - public R syncCpDept() { - WxCpService wxCpService = new WxCpServiceImpl(); - wxCpService.setWxCpConfigStorage(getCpConfig()); - - List departList; - try { - departList = wxCpService.getDepartmentService().list(null); - } catch (WxErrorException e) { - log.error("获取企业微信部门列表失败", e); - return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_CONNECT_CP_DEPT_SYNC_ERROR)); - } - - if (CollUtil.isEmpty(departList)) { - return R.ok(); - } - - // 数据库中现有的部门 - List deptList = sysDeptService.list(Wrappers.emptyWrapper()); - departList.stream() - .filter(depart -> deptList.stream().noneMatch(sysDept -> depart.getName().equals(sysDept.getName()))) - .map(dept -> { - SysDept sysDept = new SysDept(); - sysDept.setName(dept.getName()); - sysDept.setDeptId(dept.getId()); - sysDept.setParentId(dept.getParentId()); - sysDept.setSortOrder(dept.getOrder().intValue()); - return sysDept; - }) - .forEach(sysDeptService::save); - return R.ok(); + private String buildDeptKey(Long parentId, String deptName) { + return parentId + StrUtil.COLON + deptName; } /** - * 同步企微用户 + * 标准化手机号 (去除区号前缀) + * 支持格式: +86-17034642119, +86 17034642119, 86-17034642119, 8617034642119 * - * @return R + * @param phone 原始手机号 + * @return 纯手机号 (11位) */ - @Override - public R syncCpUser() { - WxCpService wxCpService = new WxCpServiceImpl(); - wxCpService.setWxCpConfigStorage(getCpConfig()); - - List cpUserList; - try { - cpUserList = wxCpService.getUserService().listByDepartment(1L, true, 0); - } catch (WxErrorException e) { - log.error("获取企业微信用户列表失败", e); - return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_CONNECT_CP_USER_SYNC_ERROR)); + private String normalizePhone(String phone) { + if (StrUtil.isBlank(phone)) { + return phone; } - - if (CollUtil.isEmpty(cpUserList)) { - return R.ok(); + // 移除所有非数字字符,然后取后11位 + String digits = phone.replaceAll("\\D", ""); + if (digits.length() > 11) { + return digits.substring(digits.length() - 11); } + return digits; + } - // 系统原有用户 - List userList = sysUserService.list(Wrappers.emptyWrapper()); + /** + * 创建部门层级 + * + * @param deptPath 部门路径 "公司/部门/子部门" + * @param deptKeyMap 部门缓存 (会被更新) + * @return 新创建的部门数量 + */ + private int createDeptHierarchy(String deptPath, Map deptKeyMap) { + int created = 0; + String[] deptNames = deptPath.split(StrUtil.SLASH); + Long parentId = 0L; - for (WxCpUser cpUser : cpUserList) { - boolean exist = userList.stream() - .filter(user -> StrUtil.isNotBlank(user.getPhone())) - .anyMatch(user -> user.getPhone().equals(cpUser.getMobile())); - if (exist || StrUtil.isBlank(cpUser.getMobile())) { - log.info("用户已存在或手机号不合法跳过 {}", cpUser); + for (String rawDeptName : deptNames) { + String deptName = rawDeptName.trim(); + if (StrUtil.isBlank(deptName)) { continue; } - UserDTO user = new UserDTO(); - user.setUsername(cpUser.getMobile()); - user.setName(cpUser.getName()); - user.setDeptId(cpUser.getDepartIds()[0]); - user.setEmail(cpUser.getEmail()); - user.setPhone(cpUser.getMobile()); - user.setAvatar(cpUser.getAvatar()); - // 初始化密码为 手机号 - user.setPassword(cpUser.getMobile()); - sysUserService.saveUser(user); + String deptKey = buildDeptKey(parentId, deptName); + SysDept existingDept = deptKeyMap.get(deptKey); + + if (existingDept != null) { + parentId = existingDept.getDeptId(); + } else { + SysDept newDept = new SysDept(); + newDept.setName(deptName); + newDept.setParentId(parentId); + newDept.setSortOrder(1000); + sysDeptService.save(newDept); + + deptKeyMap.put(deptKey, newDept); + parentId = newDept.getDeptId(); + created++; + } } - - return R.ok(); - } - - private String getDingAccessToken() { - SysSocialDetails dingTalk = sysSocialDetailsMapper.selectOne(Wrappers.lambdaQuery() - .eq(SysSocialDetails::getType, LoginTypeEnum.DINGTALK.getType())); - - DingTalkClient client = new DefaultDingTalkClient(SecurityConstants.DING_OLD_GET_TOKEN); - OapiGettokenRequest request = new OapiGettokenRequest(); - request.setAppkey(dingTalk.getAppId()); - request.setAppsecret(dingTalk.getAppSecret()); - request.setHttpMethod(HttpMethod.GET.name()); - try { - OapiGettokenResponse response = client.execute(request); - return response.getAccessToken(); - } catch (Exception e) { - log.error("调用钉钉异常", e); - } - return null; - } - - public WxCpDefaultConfigImpl getCpConfig() { - SysSocialDetails cp = sysSocialDetailsMapper.selectOne(Wrappers.lambdaQuery() - .eq(SysSocialDetails::getType, LoginTypeEnum.WEIXIN_CP.getType())); - - WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl(); - config.setCorpId(cp.getAppId()); // 设置微信企业号的appid - config.setCorpSecret(cp.getAppSecret()); // 设置微信企业号的app - // corpSecret - JSONObject ext = JSONUtil.parseObj(cp.getExt()); - config.setAgentId(ext.getInt(FIELD_AGENT_ID)); // 设置微信企业号应用ID - config.setToken(ext.getStr(FIELD_TOKEN)); // 设置微信企业号应用的token - config.setAesKey(ext.getStr(FIELD_AES_KEY)); // 设置微信企业号应用的EncodingAESKey - - return config; + return created; } /** - * 查询当前部门下的所有部门 + * 导入企业微信用户 + *

+ * 解析企业微信导出的通讯录 Excel 中的用户数据 + *

+ * 处理逻辑: + * - 通过手机号判断用户是否已存在 + * - 已存在用户: 更新用户信息(覆盖模式) + * - 新用户: 创建用户账号 + * - 部门不存在: 分配到根部门(parentId=0) + * - 初始密码: 手机号 + * + * @param excelVOList Excel 数据列表 + * @param bindingResult 字段级别校验结果 + * @return 导入结果 */ - @SneakyThrows - private List queryChildDept(OapiV2DepartmentListsubRequest req, String token, List deptList) { - List sysDepts = new LinkedList<>(); + @Override + @Transactional(rollbackFor = Exception.class) + public R importWeChatUser(List excelVOList, BindingResult bindingResult) { + // 1. 获取字段级别校验失败的数据 + List errorMessageList = (List) bindingResult.getTarget(); - // 调用Api查询 - DingTalkClient client = new DefaultDingTalkClient(SecurityConstants.DING_OLD_DEPT_URL); - OapiV2DepartmentListsubResponse rsp = client.execute(req, token); + // 2. 加载现有数据 + List existingUsers = sysUserService.list(Wrappers.emptyWrapper()); + Map phoneUserMap = existingUsers.stream() + .filter(user -> StrUtil.isNotBlank(user.getPhone())) + .collect(Collectors.toMap(SysUser::getPhone, user -> user, (v1, v2) -> v1)); - JSONObject result = JSONUtil.parseObj(rsp.getBody()); - List resultList = result.get(FIELD_RESULT, List.class); + // 构建部门缓存 (使用精确匹配: parentId:name) + List allDepts = sysDeptService.list(Wrappers.emptyWrapper()); + Map deptKeyMap = allDepts.stream() + .collect(Collectors.toMap( + dept -> buildDeptKey(dept.getParentId(), dept.getName()), + dept -> dept, + (v1, v2) -> v1 + )); + // 同时保留按名称查找的备用映射 (兼容简单场景) + Map deptNameMap = allDepts.stream() + .collect(Collectors.toMap(SysDept::getName, dept -> dept, (v1, v2) -> v1)); - if (CollUtil.isEmpty(resultList)) { - return sysDepts; + // 3. 统计计数器 + int successCount = 0; + int updateCount = 0; + + // 4. 遍历Excel中的每一行 + for (WeChatUserExcelVO excel : excelVOList) { + Set errorMsg = new HashSet<>(); + + // 5. 基础字段校验 + String phone = excel.getPhone(); + if (StrUtil.isBlank(phone)) { + errorMsg.add("手机号不能为空"); + } + + if (StrUtil.isBlank(excel.getName())) { + errorMsg.add("姓名不能为空"); + } + + if (!errorMsg.isEmpty()) { + errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMsg)); + continue; + } + + // 6. 解析部门信息(支持多部门,使用层级精确匹配) + List deptIds = resolveDeptIdsWithHierarchy(excel.getDeptPath(), deptKeyMap, deptNameMap, excel.getName()); + Long primaryDeptId = deptIds.isEmpty() ? 0L : deptIds.get(0); + + // 7. 计算昵称 (优先使用别名,否则使用姓名) + String nickname = StrUtil.isNotBlank(excel.getAlias()) ? excel.getAlias() : excel.getName(); + + // 8. 判断用户是否已存在(通过手机号) + SysUser existingUser = phoneUserMap.get(phone); + + if (existingUser != null) { + // 用户已存在,更新用户信息(覆盖模式) + existingUser.setName(excel.getName()); + existingUser.setNickname(nickname); + existingUser.setEmail(excel.getEmail()); + existingUser.setDeptId(primaryDeptId); + existingUser.setWxCpUserid(excel.getAccount()); + + sysUserService.updateById(existingUser); + updateCount++; + } else { + // 新用户,创建账号 + UserDTO userDTO = new UserDTO(); + userDTO.setUsername(phone); + userDTO.setPhone(phone); + userDTO.setName(excel.getName()); + userDTO.setNickname(nickname); + userDTO.setEmail(excel.getEmail()); + userDTO.setDeptIds(deptIds); // 设置多部门 + userDTO.setPassword(phone); + userDTO.setPasswordModifyTime(LocalDateTime.now()); + userDTO.setWxCpUserid(excel.getAccount()); + + sysUserService.saveUser(userDTO); + successCount++; + } } - // 过滤出数据库存在的部门 - List depts = resultList.stream() - .filter(dept -> deptList.stream().noneMatch(sysDept -> sysDept.getName().equals(dept.get(FIELD_NAME)))) - .map(dept -> { - SysDept sysDept = new SysDept(); - sysDept.setName(Convert.toStr(dept.get(FIELD_NAME))); - sysDept.setDeptId(Convert.toLong(dept.get(FIELD_DEPT_ID))); - sysDept.setParentId(Convert.toLong(dept.get(FIELD_PARENT_ID))); - return sysDept; - }) - .toList(); - - // 递归查询下一级 - if (CollectionUtils.isNotEmpty(depts)) { - sysDepts.addAll(depts); - depts.forEach(dept -> { - OapiV2DepartmentListsubRequest request = new OapiV2DepartmentListsubRequest(); - request.setDeptId(dept.getDeptId()); - request.setLanguage(LANGUAGE_ZH_CN); - List childDept = queryChildDept(request, token, deptList); - sysDepts.addAll(childDept); - }); + // 9. 返回结果 + if (CollUtil.isNotEmpty(errorMessageList)) { + return R.failed(errorMessageList); } - return sysDepts; + return R.ok(null, String.format("用户导入成功! 新增: %d 个, 更新: %d 个", successCount, updateCount)); + } + + /** + * 解析部门路径,使用层级精确匹配 + * + * @param deptPath 部门路径 (支持多部门用";"分隔) + * @param deptKeyMap 精确匹配缓存 (parentId:name -> SysDept) + * @param deptNameMap 名称匹配缓存 (备用) + * @param userName 用户名 (用于日志) + * @return 部门ID列表,第一个为主部门 + */ + private List resolveDeptIdsWithHierarchy(String deptPath, + Map deptKeyMap, + Map deptNameMap, + String userName) { + + List deptIds = new ArrayList<>(); + + if (StrUtil.isBlank(deptPath)) { + deptIds.add(0L); + return deptIds; + } + + for (String path : deptPath.split(";")) { + String trimmedPath = path.trim(); + if (StrUtil.isBlank(trimmedPath)) { + continue; + } + + // 使用统一的部门路径解析 (支持降级匹配) + Long deptId = resolveDeptPath(trimmedPath, deptKeyMap, deptNameMap); + + if (deptId != null) { + deptIds.add(deptId); + } else { + log.warn("部门不存在: {}, 用户: {}", trimmedPath, userName); + } + } + + if (deptIds.isEmpty()) { + log.warn("用户没有有效部门,将分配到根部门, 用户: {}", userName); + deptIds.add(0L); + } + + return deptIds; + } + + /** + * 解析部门路径,返回最终部门ID + * + * @param deptPath 部门路径 "公司/部门A/子部门B" + * @param deptKeyMap 精确匹配缓存 (parentId:name -> SysDept) + * @param deptNameMap 名称匹配缓存 (可选,用于降级匹配,传null则不降级) + * @return 部门ID,未找到返回 null + */ + private Long resolveDeptPath(String deptPath, + Map deptKeyMap, + Map deptNameMap) { + String[] deptNames = deptPath.split(StrUtil.SLASH); + Long parentId = 0L; + SysDept targetDept = null; + + for (String rawName : deptNames) { + String deptName = rawName.trim(); + if (StrUtil.isBlank(deptName)) { + continue; + } + + String deptKey = buildDeptKey(parentId, deptName); + targetDept = deptKeyMap.get(deptKey); + + if (targetDept == null && deptNameMap != null) { + // 降级匹配: 仅按名称 + targetDept = deptNameMap.get(deptName); + } + + if (targetDept == null) { + return null; + } + parentId = targetDept.getDeptId(); + } + + return targetDept != null ? targetDept.getDeptId() : null; + } + + /** + * 导入钉钉部门 + *

+ * 解析钉钉导出的 Excel 中的部门数据 + *

+ * 处理逻辑: + * - 解析部门路径 "公司/部门A/子部门B" (支持多部门用";"分隔) + * - 从顶级部门开始逐级创建 + * - 使用 parentId + name 作为唯一键,支持同名部门 + * - 部门已存在则复用,不存在则创建 + * - 自动维护 parentId 层级关系 + * + * @param excelVOList Excel 数据列表 + * @param bindingResult 字段级别校验结果 + * @return 导入结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R importDingTalkDept(List excelVOList, BindingResult bindingResult) { + List errorMessageList = (List) bindingResult.getTarget(); + + // 加载现有部门缓存 + Map deptKeyMap = sysDeptService.list(Wrappers.emptyWrapper()).stream() + .collect(Collectors.toMap( + dept -> buildDeptKey(dept.getParentId(), dept.getName()), + dept -> dept, + (v1, v2) -> v1 + )); + + int successCount = 0; + + for (DingTalkDeptExcelVO excel : excelVOList) { + if (StrUtil.isBlank(excel.getDeptPath())) { + errorMessageList.add(new ErrorMessage(excel.getLineNum(), Set.of("部门路径不能为空"))); + continue; + } + + for (String deptPath : excel.getDeptPath().split(";")) { + deptPath = deptPath.trim(); + if (StrUtil.isNotBlank(deptPath)) { + successCount += createDeptHierarchy(deptPath, deptKeyMap); + } + } + } + + if (CollUtil.isNotEmpty(errorMessageList)) { + return R.failed(errorMessageList); + } + return R.ok(null, String.format("部门导入成功! 新增: %d 个", successCount)); + } + + /** + * 导入钉钉用户 + *

+ * 解析钉钉导出的 Excel 中的用户数据 + *

+ * 处理逻辑: + * - 通过手机号判断用户是否已存在 + * - 已存在用户: 更新用户信息(覆盖模式) + * - 新用户: 创建用户账号,用户名=手机号 + * - 部门不存在: 分配到根部门(parentId=0) + * - 初始密码: 手机号 + * + * @param excelVOList Excel 数据列表 + * @param bindingResult 字段级别校验结果 + * @return 导入结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R importDingTalkUser(List excelVOList, BindingResult bindingResult) { + // 1. 获取字段级别校验失败的数据 + List errorMessageList = (List) bindingResult.getTarget(); + + // 2. 加载现有数据 + List existingUsers = sysUserService.list(Wrappers.emptyWrapper()); + Map phoneUserMap = existingUsers.stream() + .filter(user -> StrUtil.isNotBlank(user.getPhone())) + .collect(Collectors.toMap(SysUser::getPhone, user -> user, (v1, v2) -> v1)); + + // 构建部门缓存 (使用精确匹配: parentId:name) + List allDepts = sysDeptService.list(Wrappers.emptyWrapper()); + Map deptKeyMap = allDepts.stream() + .collect(Collectors.toMap( + dept -> buildDeptKey(dept.getParentId(), dept.getName()), + dept -> dept, + (v1, v2) -> v1 + )); + // 同时保留按名称查找的备用映射 (兼容简单场景) + Map deptNameMap = allDepts.stream() + .collect(Collectors.toMap(SysDept::getName, dept -> dept, (v1, v2) -> v1)); + + // 3. 统计计数器 + int successCount = 0; + int updateCount = 0; + + // 4. 遍历Excel中的每一行 + for (DingUserExcelVo excel : excelVOList) { + Set errorMsg = new HashSet<>(); + + // 5. 基础字段校验 (标准化手机号,去除区号前缀) + String phone = normalizePhone(excel.getPhone()); + if (StrUtil.isBlank(phone)) { + errorMsg.add("手机号不能为空"); + } + + if (StrUtil.isBlank(excel.getName())) { + errorMsg.add("姓名不能为空"); + } + + if (CollUtil.isNotEmpty(errorMsg)) { + errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMsg)); + continue; + } + + // 6. 解析部门信息 + List deptIds = new ArrayList<>(); + if (StrUtil.isNotBlank(excel.getDeptPath())) { + // 支持多部门: "部门路径1;部门路径2" + String[] deptPaths = excel.getDeptPath().split(";"); + + for (String deptPath : deptPaths) { + deptPath = deptPath.trim(); + if (StrUtil.isBlank(deptPath)) { + continue; + } + + // 解析部门层级路径 + Long deptId = resolveDeptPath(deptPath, deptKeyMap, deptNameMap); + if (deptId != null) { + deptIds.add(deptId); + } + } + } + + // 如果没有找到部门,分配到根部门 + if (CollUtil.isEmpty(deptIds)) { + deptIds.add(0L); + } + + // 7. 计算昵称 (钉钉没有别名字段,直接使用姓名) + String nickname = excel.getName(); + + // 8. 判断用户是否存在 (通过手机号) + SysUser existingUser = phoneUserMap.get(phone); + + if (existingUser != null) { + // 用户已存在,更新用户信息 (参考企业微信实现,使用 UserDTO 统一处理) + UserDTO userDTO = new UserDTO(); + userDTO.setUserId(existingUser.getUserId()); + userDTO.setName(excel.getName()); + userDTO.setUsername(phone); + userDTO.setNickname(nickname); + userDTO.setEmail(excel.getEmail()); + userDTO.setDeptIds(deptIds); // 设置多部门,由 updateUser 自动处理部门关联 + userDTO.setLockFlag(StrUtil.isNotBlank(excel.getLockFlag()) ? excel.getLockFlag() : "0"); + + sysUserService.updateUser(userDTO); + updateCount++; + } else { + // 新用户,创建账号 (参考企业微信实现) + UserDTO userDTO = new UserDTO(); + userDTO.setUsername(phone); // 用户名=手机号 + userDTO.setPhone(phone); + userDTO.setName(excel.getName()); + userDTO.setNickname(nickname); + userDTO.setEmail(excel.getEmail()); + userDTO.setDeptIds(deptIds); // 设置多部门 + userDTO.setPassword(phone); // 初始密码=手机号 + userDTO.setPasswordModifyTime(LocalDateTime.now()); + userDTO.setLockFlag(StrUtil.isNotBlank(excel.getLockFlag()) ? excel.getLockFlag() : "0"); + + sysUserService.saveUser(userDTO); + successCount++; + } + } + + // 8. 返回结果 + if (CollUtil.isNotEmpty(errorMessageList)) { + return R.failed(errorMessageList); + } + + return R.ok(null, String.format("用户导入成功! 新增: %d 个, 更新: %d 个", successCount, updateCount)); } } diff --git a/pigx-upms/pigx-upms-biz/src/main/resources/file/cp.xlsx b/pigx-upms/pigx-upms-biz/src/main/resources/file/cp.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7c08aee12a89bcaac2126cfe676f5c9a1ef987c0 GIT binary patch literal 10910 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 Y#FCeS{INWEVL76JK)=ZOQT#ajA15&g^8f$< literal 0 HcmV?d00001 diff --git a/pigx-upms/pigx-upms-biz/src/main/resources/file/dingtalk.xlsx b/pigx-upms/pigx-upms-biz/src/main/resources/file/dingtalk.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..89880e37faea8ac4fb1badaa0f6828498c92491f GIT binary patch literal 13663 zcmaJ|Wk4NEvc}!ro!}bWg1ZEFcXxMp4+JN8aCdii4+M87xIajCZ|=?8-Tu*M&YY_1 z?yqO6&QyK#Q34nQ3gE2;OKwmPfwy~%C_$r3-dII97SkSEWA}1gK z01prV0K(tJbZu;CT`VoqV=JY*=~0B7NuE1@tTdG(@F#2*A@S2Q3+S?rNtjbq(mWv6 zT6h3q151FDun52F83#BpUL%2!>*;aIJe~F29rUQ8gqHJ#|CSN}_W&&3L7r+_ExLF2 znCmPk#%s!`ssaqDDewikvb|s48rNk8L9~RPFiS#^)rk5l=_kg^9Lwx@b#3v89Bum4 zUA4|iMP*V9gPhK_m`{X!MKP@D4wjbgG&-jB*rtGGZbz zW8*Z()jNuT`x6B-e2{V*4PX94v5EQfABd!%8F{}aO^CbE!1nEM%)v#{YVgvnWa6)A zoz$m_jAgI3F;`(9n|TrhNfCs7*U_A?6V(4~Q&#CiKPs#9yz9AhMCu`nCj}o{F z{s2#UYMJ7M0%>U|kK>n3Z>Vog*l4Dj8bqRgwMtj|O|dSRdnY5FZdW5NDq3;h)0ahv z=DnaFf-8Y54OHX!gwy*Ak%9M#Vx2>3{6W$>6Kw765{=m{a}{&7K$g(-8N^}&jYAWi zYC;Swj{vCeeeFfbPRukJ4x`+nQ8)1|;Mt(ML6mipbrRRn_KGt?Xhl2SbMI#i!4Yr- zAW=ol=^-JW&($Up?k@NXVA2$IWriG^dT^^iKZLhFn1rn?FiB1HXu^EXT&v&S$SXcT zulkM({;9xEzRY(~Z8y5?7_jBaEMudyK~r}yLbJ*86beD6z=PX6gIa0HG|byPY5|o^ zpU;IBi5tCvvi#hjDLIP^DyIiNRHh47%!Dgj_jZNH)4n~@1yA8KzzeZ(08h^+65tKg z80SXocA;n3f5QIs<&sM9750!<*pdGMyQ95>wdEV|wQ=g^{`4q5Z@|ahBw^&tW+$4d zVYn|WG_}EnR-!T?A~z>gzO-G|4IlPtbwTc)rJs_JCAT`2RwMgceUkLu)mwuYRGb}< zg33^mzyK6Ul~Xb-&K2#{GNB(XE025NyV9eNoQM(aHJ_d`GGlhg ztDM*MSI}hDpzrOM7WIedBizVgDe`|JgG+OnPQRn6l6%xCm@oFUcC6llS><#zPN4*~ zrY6ULwgDIZWnEt#+rt~g;Wl>~$`Jnz^#AO8fL}p( zvG~L5G$Vkh?Y!t z^MC$cH*o@}VVsh5C1c2$H;n4Cg}3ap)WMx(rG^(1lNKDCO`fr^IexKAL3<-?%z4Jg zu3mLfoo1+$SDaue?o^+`<=$I`5Lg=MgeHCs;H#-5lZi5g3Ct1%gSzmsily(^e9YgX zGsI@6PUn2Q&y|=6&a=CF&i|_bG(zOg_{AeBE5E7|YIb<(#rkDuvEK_S?oFdCp;Cd= zd-&A2NLrOSnbbMbVOf~~Zs{Ml)Muz|Mx0saAK3*Wy*4b4K-;}N9nvFI2yt7Fc_^&ZhWN|miZ1vg~diOlMMtL!?`Pu)6hxl+nt^B^= zg$8oY(dHM7d*1Mi-aB^(#U6LcNgmc3UOdn3Q)E>uQ&qL?EWGd}wa>;iY%WXMun=49 zG?6h=jx2b71>O882HE3iH7aUi6@W_cOA6(lxuAZnGnyYclOBvZO7BAx#hZl}w^%I1 zpmR#Xpb>%4a%TE$gd_qASq?VcNr>;Qco7?NNT>B+@7>rlpFVt-Zgp5)&J@nN2mbFD z&Ff|HcF`Ev={lQQ8UGox%1zt+p+EruFvI`=u>X*Bva}HVrNuz|dVHKF8=b>~(7&pD z;LO`LPZI8nz|Su|TlX-hWOh2!ACE4%q22+JM(v*YYy`7xSyTs=C(7kgYY^JWf;Ud* zAMEY)(l+@1u&Y>YfQZP!5IG#ZQc}=1VxS*Bde}#$R!^vlQS~gO<2u`@Zu@16nXg(CVbYfzzO~1M_tdDGI#bYeA*Y^wy}^7_l|S6231ygJ6Ay-w6@fYE51w6`SV>7sJGW=hy7vUnbFo6lXd9$=Ec$c7#{%#SLuQz-kS_p~3`8Mkv!n*e8 z*UxQ`&BPxMo$u^yDj=NKo`7$iwQw#~zw_Wdd9i*7- zz@HyOvutG;ZuUiO46M2~IgIAufhs}@j23uaa?K5RZq#O90F4!h1`U-^8RSMT zvyH?-qY?5{Ur|D}XMmbH{RzLBB|#I*dJaGnB@Lrq=6FRo?2Ev2H!X5_ z#3EY!qR)iAVvquR&@^MfCi$46_;{d*?GR$|YRJ+hexK#52zUI(FGG!d!J@2t7{{-M zoc!IEBE@VmI*dS7%2&P!Y+*Y@cy*^rw)4&VTFT*oRj2~aQL&{FLcwtecdbjkTlcZ1 zOr@htMd^=_!%3lh!m$Vm>TZB5%+byBiAw47{jnWcUOC`Z4mYx1% z1eVKsYbOA;K^LM|gv}+TDrwBMWz;ka3yJT2HgEfKh1|)aRy_PUsh>j3q1Je8k88F5 z?W3-Y_LyELsZ0X$#j<7YRW(lYrc#FMGI13O71@TNdIpd!c9u*!W7HAjk`2*zXD`WAc=KpGDv^QIgw}(DPoW9;BjCWr^wq;6Vm96 zSfXN)lu4CI5H--8CQzlc_L&ZBdROLpu$c%FB?}Q_rePl!5Y0=Oezr!eATn?C3t8NL zX5XmH8$R?V=R_bU@&=xqgBTa78p=+pqV?ZK%t}57>!bL1I za$v?r{a=BD<7Uu%;R&fj3Qfyk;9bNjPV!fJzVk8b-SSZ*zxp5DS}>Tkm2Z2IpR;aY zThVM*ML;Zj?C}}sLlS#@6CsKfH*`!so3upqWM$1GwKGB3u!dB#=*@{ld<${Kw_OqT zI6iDJAx87;X^6a?6S)oEoEg+mCrBr5tmNe|{_!1%9VZO#vKwe0<$4ZiU*UQTX#dl7 z0q9iWF&%WS{Bqvw(TOKMl0(shj_+${^41x=cF_7&Z7&rm88C3OmR3UVms1L3uA^66*uy4V4^-+ zQ`(y>La9J^(71NWRC3v7{R4zFdfFr>sq32~XRwV;c2~Omt@);9sswQ81Bb_rTI+F2 zAnT)bx$TPiS8wQYzQ&h(EHC;!eiWJ(uYldA)oS1AV4 zeFs=gOQ!nf@Y%!|$Q*EKmu6Y~nmVXm2^0kZRZX)nNQv!yHHit}K5&4KykR6_-0b6L zAfO-HH3bKgkWXU-D;1`{;6_C3-hCvP3z~_@fM6Rm=x@&;TtJj4FV}v&z3+X0dKF$! z5S@|9bH9CgzBw)OVYiLC8~BrO{(A9g`|ZP$Oly0F=gq;OvQ-QHdHc)dqIJ91K3Erz z8l&6Y)%1sq51x0UyWY=dJ%Fp5CKg>HB%NK&p)xI;)V{U*9SbApN$ttKOme(B{91(YtNc|(M8$CdH48k{<&R}anDZ?RWCW*o) zgLfmZH~!uNvom_0lG#HwOig@)hLgUjsbis*Xr7lGGqKKoo)+al3Mkloj%s3AKi?ilG;B_iBjidQBe@T0nXXyKt z?ET5WHuFfyJ?_{(h;At(!BL_X840(1%LV4juv7o)W>2FloKlY)^2rDCNp2Z}t*)kO9uKL!lAVC29^pE7H+hYGH9AZhi1EMr|&OxJf!tJ?K+ zJ=~K6##4SbuZwLvr-dM2VkOj-G>TtviZP)9Q;fNM z!t&c8$Wp#mQmMhErlqzf=atcvbr|JC1RA-eJx}eI!#CBzCz& z70s}|4JViA3p^r3+y$J^jxXlpC)vRx73)_Xrkm!KFC|P-w}b{+PbZxRQ+1sm;F~%o z*&GA`pOS25$RW*;fxZpZJje%&kOFI+O%8Sr zrf61bxGdIwVZm{nMK1Qep#9=|8bw*n=~BHpm~fT~q~UO8SN~a}o(bAfxJ-$gqOCe~ zwR$^g@@5c_mWa%p!;Bn4jG4rE)ftpcM@*#@&4_QU`js8;zor+6VHB-uG-!sZFA}Fs zcdi9;d{`(|qJfb@+ZD$(ed-Rpg9gU_0C2w7*}ROIK!gqf`|Sz*EoA&o$x^am%~-w0 zg06rHs*bD)|B|V9a=215$Bkmc5gmK6cr5 z2N*TggW0Pf+P^|WSpfloMHD7Vux=g)fmK>U9q^)e&E4$2tvz6WO;9F5JA~S@ia-8< z^A5k9nzrE?k~(wqF1m+*7R61q&F!5)IV;lxbp;!8Q|l!cLcmUW-ga*?y%+6=tDp=zaHAdm zaUkEb4L|3u-R51*v}E`u&1wlczjcb69?kkgA)Dt@bYE=|zx~09$4dj36v|VII%6e; zC&%L|<%X;D)tdoFbHc{~??cnu*!RcwFHoVaXO(r@J6i9{ryM_Vb|O#IP?)3vYVgsWIidf%s|+gLXHL!gvuiVBsY zPk!=SDcW1aFl42$-(pb9y!0B@7YEIm&3~qfRTZVn1&&Q~R3H(dH_J;(3oOQzsZJ-B z62p4pX{#^_#D$@Cf0*}_KI6GM!6rVBg}W%Z%FK5#McB5l^dd?rv|6mq|5RaWndP;U zB^WNl@4NlMtDCo1M$%?MUN_y&QYu!3-Ks`kX_YxPgwZk%{%fqD{0t?%Ykx?4PGo+D zd!&|5AqBf6w2?A$ZC+cqe5$$B`&B_XsYTvlw|YVnqi2P2X1ZL>@xU5mG`pGjp!FFk zrB6ECjDb+7Hpc}dv&xmwa!Q2@LIm)fY`M~*TNqwCTP@cHqwD1qsoAnH8C9kWNkW5P zMW`XZk9-mhcRa_Lg#RR~6Ak35QG_PAUE}$rDX&1#rE!>pPV743wBe?UJNBV-w}@g% z@Z?8mgMHj8)UK<g%=ZYev}evJL%29dmn@;`Z3Kgf^@fFmob{Q@vc38WodYC@bCgWGOf~>!=x-DhGB=KFmd) zrAGMJR4#-mOuWI*Tn8ZlWGU$4l#0Wwf?b!6bWo#$l(_bho~s@Zwvd@;nool|bhVNO zuFOvG3pt(;6nSM&Bj$KWa=2gQbdzHiqffV;Wnvf%D-!qg4DKWrfbBG!Z?F7Z*JRzu zrh1_7Q$Mx5j=h%(TA6GvcNBq!S-(x)wV$tfj%KwMTpPP=mWzd?ure}k zcRAc14VLf~@?`I5^f(=Xdh{No)1Qv9rkS-icG9O11=FEASEVUvwGX2~JMkd_I!uR? zIn?`)1j+ecRs{|MZ@~+g$#KGwgg4F|)gTh>#Lv@0(e)%NWjDs<&PBDk@uw9xk8;`H z)iR{Y8yCnHM8T3c6&+w3ONOyIy7BMqMt)Gq%au27XDN_gl|Lujr-H5~UfCys(skuJ zC{O1AOf@e=fB9I@4)9O^yxqPs3i4V`zy<*TfdBnGtY>X){?j$fcfRGWTZzx%6HiOy zYgE4#?H4Vw=;a-oDT<>@4@kX>tPRCoq^01;xeaJNc7zvQX0zKeR_S>yn@ORA@Ut&o zJnk%B7;*7@H>PldmSgDTws5A2@Chc;^Eh1pT>RyIhp6cgaS9Y1o)XzrabwA$MVnQiVGF1}u2- z7-y6}9)Das zS6enIVtH6)7+l-+j0k6lIOWYk1TevtlTdyRW2>PtR$C>;G-8r?Ic2(EK+urdXfxHTBY#BTewuPNt>s ze+6_0R|~_Rk^H;narBZX=1Wx1i%y;&d7+L_x*P-xg^YtF~ zep6@obcMZyt}LP~pulM{9B0aHV9hmN4Mz?47s>b&uMO?BWXz=tNiL_BC8>?pvKf*w zuG4nc9nUJS?JuX~_0-k|&Hj;oZjBlrOZfVazxlUDb~#%o^$PQ0-j(vzZ#Mv&=h&D; zLMgMf-zQwfdhe;s-_Q&z`}FG6$S&MGt*aF>9UQ4Y&Fxij)_|Q!jbGi<;Pbe-=S@yj zt9?f#Jm4fP)hJ82r*Z$NgEM!ZPAz#)bK1YDTe*21OtWgW6m^U*rv;j~s_JfUz2;V0 z2V8{bB0PY((x-BB9JL#@oZ*eBlCZ2bexkwvJF7NWZME3L`I_VOq(4dZ^v-oyKJQp1 z-3^t>=a$14I2#Qg5i{H0H78+$0vTB}K+t+0h}<)tFzgXdFe}DE?b~&FT+0fTuNvf?pF9X>c5Du9;nN|^ z(r<`@K}c$aTYbt^YOe1r=K$TwiK?>uFx5A;u`?_6qn`evDByx<6gf>yv!2OxAepa~ zb9W-|OO>9|h?}LZu0TF$LK;K~H5{3(?~amZfk#<_hEJH_7qRE`0txwzdagt1S&|m7 zrU0)}w>Djg^X$oh#5E0bzM={>$MZzcbq?jV;RrQy>MGOnsuP`w)oeQAJ7|G?jMt;-{{q0*X1Kax8wahYOn7y;||t zuKpgPc0$!+TXp_)*<&`5*LD)3>m@^^LuM?n`eIc~$j^zy=EdT#eX;5ib87Q&Ea<>j zI+?kXx=IrqutC2KH(#6R9`ZJ+e2k&bhe5hbGio`L^p{~x9(Rf{)(Tj{QozhZ;X!80 z&rl)d%EggIW+qKddngD4+HuLVU1r@8TjeApqR--8k`Geh<)gKz!I*>EVo9-&2?335 zYbS zbS10=ovR4yuazRHhf%W^x;Xob?hjkD&cOx>`}aK5qn2gP$8#B+ChTW*C~^Vi0Z2;Z zx(s$XYp30MdTjGpFmm z^VFx!3Jb=L3Jbg^%+eVDvjlZF0p?W)Z5ocDXAMPjI(3Uhm>%3 z*uj-sR6WS6$%nY|Y#Zf2XJYnqfF5ktXCHHnGiqNAXKav`yXAwol=C?zHn>YU^7pC4 zO%5ipUZ;?lKkD9b8?L+o^E3wt-$>kD!mawc;v0>o4*p?pv|@I@&fnD|Va{d`Cxsw0 z%pj8&^w~7Ej`#6G=_|Om!>GMy@#gd~y&?VSOwklB3_8D}$@(rh6~1(yyBTeOykpaA z@#eiY^)1Kl~;`c6+6AU4y$=V4z& zV#W!O#;%BDr?~(YAGH8Ffdr5uPb1qQc5?`1Lg9d5mk>lx8`#HQuLf1>SXR^1Fk;>9 zUprzw>~EXAnh$QCyjlk?ly({UBa=>Sm%KnNJH-6X(!C<_wWFuy+e{P2`6(j8)ouwt z$N1Vsxp*z%l^DKuDCgq84~K@9GLW=B8y8=i__n1~@xaqV1b0=3u|gKy&pP7!d0#NnAIqXF6JYj=+AG>1F3}**ln%qkP)#&Ltt^hMksivX!2|{xq*GbJC6uMFRVQOh;L0 zL=NY})9@DSsg$?3_kHIoKgSdbF^AKPtMK%G@Qn=5y?`j+E-+%~%u~P2>(cX`GeV}; z_z|efD0{(?3wULN1UXEDK*V#<5CBtvh!Ba3m@EJhQD8#^UA|5=fC_$OB0xItl-BCi zc-dIr(h7i63;>U?BdtBe5Iylm6GKD<6p+RooXph5&1h{F6%uED$gS4{~eQVn|Y{C`0NV z>X@*gl~CBSV7Uzok)0!j9jQtvqq$HIwFZV0R>|u*Bi+rc0I4^Fh>aDqN?q^XMzR5h ztEC-{LMrE#RB;zLkrL3E&P$T&fifo6Q~IpOGD(#d8KR;2%jz1w#V3?Tq;h~BpmG4J z5#F%HpJN#n%Z+rRZk#d{i(4`DMX7u$Uooa-r<}qfFI10bS%d$SiH zKBRgt8JLHz_K9~9I)?y8eceaS1|7F_|57-*!wqZvj1kFTF1v6ct=eY7Z0AxC1y59( z)UAV%12X6-vy5EG)1@BwXiltAmK0Hf-~*Q)*)w>_~32g#F(xB@h3Kcqw&B*$j(lk+PXgCAY)s=Ii>})D zo|*75?_5mJv8yACg0ONcj40`=jO2x6BS#I;fi(w2&QUUGWv&e+y}c?~CAfXLj+=X6 zsy?H#cjC1fdx9tJ84}jgR*!p2JrZ#gRbX$WA0Kj*4YE47-z8WpZIO5qf0RV_rVPm!uMIkGC;iTY-ukg9I#8wn0&RFvho zc@V}MA|x&aYY|6Q-RInwU}lz7SaG=Nq|rZP5?vC-xhKBABa-&TVELU^Kqly#XcmLa z4?<+}g4}ovv4HNm48w1U)2-jk)W^t?L<*#LkUYz`Bi6~Fy08QaEXC8pVDNIN!S3Lc zV#=d*1YydD7ODwDkF4e+(>3L(ScaA&v$ZOVgmRi%f!XsKTdVc43`>_iz&pgeE_9B0 zXUDIbfedpDDpq-X;!ld&SnSs3!CMH%j<8euaC^of636kNtVWqi}%nY^qu;hBJ8wDIin&j zI?xdvNf~(hBuTzZyhDytJ#K5|lck<%0*_9<+euyZFlfg9PN2n*AJyXLDC@^-J>b+P zd*$A9*u5nS~12 zKj;(tv$R@$v>N&$+?R>>QHrZ~g)+pw8O)X~u;tF$y6A9hjXOC9ghBST{>6g(v-s$Px zuFlv=^_pQ(jsVi^!xs|9{+_KwR~}|f!in|z z`I;U6CH7Xo|91}JU+7Aib<-mR-UP1jjkG1EC(4O$S%?!Z&2jt0^d*MUK#pGa*O%K6 z+7jeqTZ34Gwcd=1E`1+o3!#&o0WbeTDUPrwRndBi;=xqzEJF}dTki&ehbk#rQEY8p zXU%3Z_IcncR?z7@w}YYIm|~C(hZ&VHdpw;bSJpnUFkUv*|Ktr_+59~48RHB+Yt-CU zPk>2D2C|n^+;4TPw@4$qAsKXnq8p|Iz#+l~aH^e|w2Z_9!{;}}eVpkNZT3S;nsU^` z^Exc%T{d3CT%~%6c09%AFtaHztAw}%gg-(zRfyP~qv(Ge_0P8dcPRgrLH`@O;Xk2j ziZ9kX{)%S9D=Crq7pnhH<2Tdc7rZfpX21+Eyr+TB{*&Bul5km{TCKC0}|K!#v{PnObs5_baD4j}R(JQl{hRCTAvxQ|t* z0=O{&{xa6ge#*g4`(ouO+3I-nU(E#sGp(Zvcw!05ngIS+$_nys_7=x3Gk z51G;|_o-#n5nl17WiQR9Ya6TIS^t@`Xz>Dz@_xnQ__Yr4Mw?X^u(on|rDtj@xY`)n zYrf^VNoZl5=?R=91gf2vS%#VPcGbjK#--sihrTk`<3W z%PgFoO8=5BEr+@jVk8Y*f-DePSkMjj{YhY2IObWu)@i7OG4VoDpPD}`xITFgfB%-H zRA#+&qHy(=R!Z~mq9|< z1NF?6X()mIh0|j-r>W_^t?y*=$wjFH)UT!7?#r z7ol%Q6A7d;&B|mv8(baRgk4VkSwCN$p?{PB1VZ^)ANob)|8vJ)Hw^#~fZ(;~q5Y5h z_shV4ChflY@?Y)kod841-=6)SgZ}B;-*kQr8v2*c&zt3c4ff{jezmv3-rC>ZRru$4 zf4;5oCj4tWvcIeS>)nMvA^MYo|64oe^-}+9(0>#1|5X2zO8#5@jUDw{{V!tq|624L zE&5k`TeQb(D*4rs|1X7qqEt!sH_!j)ME?vCZ{z$zY2oi`|1V(tY3EOt_HR4szvlZ7 z&i4PB{2QP5S9@D+Z*TQ_%n6;+d??*FAIMqa{t%*|G9a=uT_KB_5Yg(^v}ir$prd6V%gu-e)EF1n5r(hz5WLp#B;FPw3wC{ttvv Bx@!Oc literal 0 HcmV?d00001