mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 12:39:05 +08:00
feat(upms): 重构连接器导入功能,支持钉钉和企业微信Excel导入
- 优化钉钉部门同步逻辑,提取常量到 ConnectConstants - 新增企业微信部门及用户Excel导入功能 - 实现钉钉部门和用户Excel导入功能 - 移除API同步功能,统一使用导入方式 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31ad1fbeb8
commit
d2f49723d6
10 files changed
+776
-330
No files matched your search
+73
@@ -0,0 +1,73 @@
|
||||
package com.pig4cloud.pigx.admin.api.constant;
|
||||
|
||||
/**
|
||||
* 第三方连接常量类
|
||||
* <p>
|
||||
* 用于定义第三方平台(如钉钉)连接相关的字段名称和常量值
|
||||
*
|
||||
* @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;
|
||||
|
||||
}
|
||||
+35
@@ -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;
|
||||
|
||||
}
|
||||
+9
-2
@@ -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;
|
||||
|
||||
+36
@@ -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;
|
||||
|
||||
}
|
||||
+85
@@ -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;
|
||||
|
||||
}
|
||||
+40
-28
@@ -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<WeChatDeptExcelVO> 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<WeChatUserExcelVO> 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<DingTalkDeptExcelVO> 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<DingUserExcelVo> excelVOList, BindingResult bindingResult) {
|
||||
return connectService.importDingTalkUser(excelVOList, bindingResult);
|
||||
}
|
||||
|
||||
}
|
||||
+31
-13
@@ -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<WeChatDeptExcelVO> excelVOList, BindingResult bindingResult);
|
||||
|
||||
/**
|
||||
* 同步钉钉用户
|
||||
* 导入企业微信用户
|
||||
*
|
||||
* @param excelVOList Excel数据列表
|
||||
* @param bindingResult 校验结果
|
||||
* @return 导入结果
|
||||
*/
|
||||
R syncDingUser(Long deptId);
|
||||
R importWeChatUser(List<WeChatUserExcelVO> excelVOList, BindingResult bindingResult);
|
||||
|
||||
/**
|
||||
* 同步企微部门
|
||||
* @return
|
||||
* 导入钉钉部门
|
||||
*
|
||||
* @param excelVOList Excel数据列表
|
||||
* @param bindingResult 校验结果
|
||||
* @return 导入结果
|
||||
*/
|
||||
R<Boolean> syncCpDept();
|
||||
R importDingTalkDept(List<DingTalkDeptExcelVO> excelVOList, BindingResult bindingResult);
|
||||
|
||||
/**
|
||||
* 同步企微用户
|
||||
* @return
|
||||
* 导入钉钉用户
|
||||
*
|
||||
* @param excelVOList Excel数据列表
|
||||
* @param bindingResult 校验结果
|
||||
* @return 导入结果
|
||||
*/
|
||||
R<Boolean> syncCpUser();
|
||||
|
||||
WxCpDefaultConfigImpl getCpConfig();
|
||||
R importDingTalkUser(List<DingUserExcelVo> excelVOList, BindingResult bindingResult);
|
||||
|
||||
}
|
||||
+467
-287
@@ -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;
|
||||
|
||||
|
||||
/**
|
||||
* 互联平台实现
|
||||
* 互联平台服务实现类
|
||||
* <p>
|
||||
* 钉钉: 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";
|
||||
|
||||
/**
|
||||
* 同步钉钉部门
|
||||
* 导入企业微信部门
|
||||
* <p>
|
||||
* 解析企业微信导出的通讯录Excel,支持两种格式:
|
||||
* 1. 单部门路径: "腾讯公司/微信事业群/广州研发部"
|
||||
* 2. 多部门路径: "部门A;部门B" (用分号分隔)
|
||||
* <p>
|
||||
* 处理逻辑:
|
||||
* - 解析部门路径,从顶级部门开始逐级创建
|
||||
* - 如果部门已存在(相同父部门下同名),则更新部门信息(覆盖模式)
|
||||
* - 自动维护部门的 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<WeChatDeptExcelVO> excelVOList, BindingResult bindingResult) {
|
||||
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
|
||||
|
||||
// 数据库中现有的部门
|
||||
List<SysDept> deptList = sysDeptService.list(Wrappers.emptyWrapper());
|
||||
// 加载现有部门缓存
|
||||
Map<String, SysDept> deptKeyMap = sysDeptService.list(Wrappers.emptyWrapper()).stream()
|
||||
.collect(Collectors.toMap(
|
||||
dept -> buildDeptKey(dept.getParentId(), dept.getName()),
|
||||
dept -> dept,
|
||||
(v1, v2) -> v1
|
||||
));
|
||||
|
||||
// 查询根部门下的所有部门
|
||||
List<SysDept> insertDept = queryChildDept(req, getDingAccessToken(), deptList);
|
||||
int successCount = 0;
|
||||
|
||||
// 去重
|
||||
List<SysDept> 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<SysUser> 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<String> 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<Boolean> syncCpDept() {
|
||||
WxCpService wxCpService = new WxCpServiceImpl();
|
||||
wxCpService.setWxCpConfigStorage(getCpConfig());
|
||||
|
||||
List<WxCpDepart> 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<SysDept> 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<Boolean> syncCpUser() {
|
||||
WxCpService wxCpService = new WxCpServiceImpl();
|
||||
wxCpService.setWxCpConfigStorage(getCpConfig());
|
||||
|
||||
List<WxCpUser> 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<SysUser> userList = sysUserService.list(Wrappers.emptyWrapper());
|
||||
/**
|
||||
* 创建部门层级
|
||||
*
|
||||
* @param deptPath 部门路径 "公司/部门/子部门"
|
||||
* @param deptKeyMap 部门缓存 (会被更新)
|
||||
* @return 新创建的部门数量
|
||||
*/
|
||||
private int createDeptHierarchy(String deptPath, Map<String, SysDept> 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.<SysSocialDetails>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.<SysSocialDetails>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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前部门下的所有部门
|
||||
* 导入企业微信用户
|
||||
* <p>
|
||||
* 解析企业微信导出的通讯录 Excel 中的用户数据
|
||||
* <p>
|
||||
* 处理逻辑:
|
||||
* - 通过手机号判断用户是否已存在
|
||||
* - 已存在用户: 更新用户信息(覆盖模式)
|
||||
* - 新用户: 创建用户账号
|
||||
* - 部门不存在: 分配到根部门(parentId=0)
|
||||
* - 初始密码: 手机号
|
||||
*
|
||||
* @param excelVOList Excel 数据列表
|
||||
* @param bindingResult 字段级别校验结果
|
||||
* @return 导入结果
|
||||
*/
|
||||
@SneakyThrows
|
||||
private List<SysDept> queryChildDept(OapiV2DepartmentListsubRequest req, String token, List<SysDept> deptList) {
|
||||
List<SysDept> sysDepts = new LinkedList<>();
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R importWeChatUser(List<WeChatUserExcelVO> excelVOList, BindingResult bindingResult) {
|
||||
// 1. 获取字段级别校验失败的数据
|
||||
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
|
||||
|
||||
// 调用Api查询
|
||||
DingTalkClient client = new DefaultDingTalkClient(SecurityConstants.DING_OLD_DEPT_URL);
|
||||
OapiV2DepartmentListsubResponse rsp = client.execute(req, token);
|
||||
// 2. 加载现有数据
|
||||
List<SysUser> existingUsers = sysUserService.list(Wrappers.emptyWrapper());
|
||||
Map<String, SysUser> 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<JSONObject> resultList = result.get(FIELD_RESULT, List.class);
|
||||
// 构建部门缓存 (使用精确匹配: parentId:name)
|
||||
List<SysDept> allDepts = sysDeptService.list(Wrappers.emptyWrapper());
|
||||
Map<String, SysDept> deptKeyMap = allDepts.stream()
|
||||
.collect(Collectors.toMap(
|
||||
dept -> buildDeptKey(dept.getParentId(), dept.getName()),
|
||||
dept -> dept,
|
||||
(v1, v2) -> v1
|
||||
));
|
||||
// 同时保留按名称查找的备用映射 (兼容简单场景)
|
||||
Map<String, SysDept> 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<String> 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<Long> 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<SysDept> 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<SysDept> 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<Long> resolveDeptIdsWithHierarchy(String deptPath,
|
||||
Map<String, SysDept> deptKeyMap,
|
||||
Map<String, SysDept> deptNameMap,
|
||||
String userName) {
|
||||
|
||||
List<Long> 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<String, SysDept> deptKeyMap,
|
||||
Map<String, SysDept> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入钉钉部门
|
||||
* <p>
|
||||
* 解析钉钉导出的 Excel 中的部门数据
|
||||
* <p>
|
||||
* 处理逻辑:
|
||||
* - 解析部门路径 "公司/部门A/子部门B" (支持多部门用";"分隔)
|
||||
* - 从顶级部门开始逐级创建
|
||||
* - 使用 parentId + name 作为唯一键,支持同名部门
|
||||
* - 部门已存在则复用,不存在则创建
|
||||
* - 自动维护 parentId 层级关系
|
||||
*
|
||||
* @param excelVOList Excel 数据列表
|
||||
* @param bindingResult 字段级别校验结果
|
||||
* @return 导入结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R importDingTalkDept(List<DingTalkDeptExcelVO> excelVOList, BindingResult bindingResult) {
|
||||
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
|
||||
|
||||
// 加载现有部门缓存
|
||||
Map<String, SysDept> 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入钉钉用户
|
||||
* <p>
|
||||
* 解析钉钉导出的 Excel 中的用户数据
|
||||
* <p>
|
||||
* 处理逻辑:
|
||||
* - 通过手机号判断用户是否已存在
|
||||
* - 已存在用户: 更新用户信息(覆盖模式)
|
||||
* - 新用户: 创建用户账号,用户名=手机号
|
||||
* - 部门不存在: 分配到根部门(parentId=0)
|
||||
* - 初始密码: 手机号
|
||||
*
|
||||
* @param excelVOList Excel 数据列表
|
||||
* @param bindingResult 字段级别校验结果
|
||||
* @return 导入结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R importDingTalkUser(List<DingUserExcelVo> excelVOList, BindingResult bindingResult) {
|
||||
// 1. 获取字段级别校验失败的数据
|
||||
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
|
||||
|
||||
// 2. 加载现有数据
|
||||
List<SysUser> existingUsers = sysUserService.list(Wrappers.emptyWrapper());
|
||||
Map<String, SysUser> phoneUserMap = existingUsers.stream()
|
||||
.filter(user -> StrUtil.isNotBlank(user.getPhone()))
|
||||
.collect(Collectors.toMap(SysUser::getPhone, user -> user, (v1, v2) -> v1));
|
||||
|
||||
// 构建部门缓存 (使用精确匹配: parentId:name)
|
||||
List<SysDept> allDepts = sysDeptService.list(Wrappers.emptyWrapper());
|
||||
Map<String, SysDept> deptKeyMap = allDepts.stream()
|
||||
.collect(Collectors.toMap(
|
||||
dept -> buildDeptKey(dept.getParentId(), dept.getName()),
|
||||
dept -> dept,
|
||||
(v1, v2) -> v1
|
||||
));
|
||||
// 同时保留按名称查找的备用映射 (兼容简单场景)
|
||||
Map<String, SysDept> 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<String> 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<Long> 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));
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user