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