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 000000000..7c08aee12
Binary files /dev/null and b/pigx-upms/pigx-upms-biz/src/main/resources/file/cp.xlsx differ
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 000000000..89880e37f
Binary files /dev/null and b/pigx-upms/pigx-upms-biz/src/main/resources/file/dingtalk.xlsx differ