From dcb86dc9f29ff8f5906e55769492ac8b18f71fb1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 9 Jan 2026 10:48:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=92=89=E9=92=89?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E6=9E=B6=E6=9E=84=E5=90=8C=E6=AD=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=90=8C=E6=AD=A5=E7=94=A8=E6=88=B7=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E4=BF=A1=E6=81=AF=E4=BD=86=E4=BF=9D=E7=95=99=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E8=AE=BE=E7=BD=AE=E7=9A=84=E9=83=A8=E9=97=A8=E8=B4=9F?= =?UTF-8?q?=E8=B4=A3=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DingTalkController.java | 14 +- .../designhub/service/DingTalkService.java | 172 +++++++++++++++++- frontend/src/views/Settings.vue | 50 ++++- 3 files changed, 221 insertions(+), 15 deletions(-) diff --git a/backend/src/main/java/com/designhub/controller/DingTalkController.java b/backend/src/main/java/com/designhub/controller/DingTalkController.java index 0001922..3c38d90 100644 --- a/backend/src/main/java/com/designhub/controller/DingTalkController.java +++ b/backend/src/main/java/com/designhub/controller/DingTalkController.java @@ -435,7 +435,19 @@ public class DingTalkController { @Operation(summary = "同步钉钉用户") @PostMapping("/sync-users") public Result syncUsers() { - return Result.success(dingTalkService.syncUsers()); + return Result.success(0); // 废弃,使用 sync-organization 代替 + } + + @Operation(summary = "同步钉钉组织架构", description = "同步用户部门信息,但保留管理员手动设置的部门负责人") + @PostMapping("/sync-organization") + public Result> syncOrganization() { + try { + Map result = dingTalkService.syncOrganization(); + return Result.success(result); + } catch (Exception e) { + log.error("同步钉钉组织架构失败", e); + return Result.error("同步失败: " + e.getMessage()); + } } @Operation(summary = "检查功能状态") diff --git a/backend/src/main/java/com/designhub/service/DingTalkService.java b/backend/src/main/java/com/designhub/service/DingTalkService.java index e4632ec..703e49a 100644 --- a/backend/src/main/java/com/designhub/service/DingTalkService.java +++ b/backend/src/main/java/com/designhub/service/DingTalkService.java @@ -4,8 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.designhub.common.ResultCode; import com.designhub.entity.DingTalkConfig; import com.designhub.exception.BusinessException; +import com.designhub.entity.Department; +import com.designhub.entity.User; import com.designhub.mapper.DepartmentMapper; import com.designhub.mapper.DingTalkConfigMapper; +import com.designhub.mapper.UserMapper; import org.springframework.util.StringUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,6 +38,7 @@ public class DingTalkService { private final ObjectMapper objectMapper; private final RestTemplate restTemplate = new RestTemplate(); private final DepartmentMapper departmentMapper; + private final UserMapper userMapper; private final SystemConfigService systemConfigService; private static final String DINGTALK_API_BASE = "https://oapi.dingtalk.com"; @@ -931,21 +935,171 @@ public class DingTalkService { } /** - * 同步钉钉用户 + * 同步钉钉组织架构(用户部门信息) + * 注意:只同步用户的部门信息,不覆盖管理员手动设置的部门负责人 */ - public int syncUsers() { + @Transactional + public Map syncOrganization() { DingTalkConfig config = getConfig(); - if (config == null) { - throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉未配置"); + if (config == null || !isConfigured()) { + throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉未配置或未启用"); } - // 获取用户列表并同步到系统 - // 这里需要根据实际需求实现用户同步逻辑 - log.info("开始同步钉钉用户..."); + log.info("开始同步钉钉组织架构..."); - // TODO: 实现用户同步逻辑 + int updatedUsers = 0; + int skippedUsers = 0; + int errorUsers = 0; + List errors = new ArrayList<>(); - return 0; + try { + String accessToken = getAccessToken(config); + if (accessToken == null) { + throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取钉钉访问令牌失败"); + } + + // 获取系统中所有有钉钉ID的用户 + List users = userMapper.selectList( + new LambdaQueryWrapper() + .isNotNull(User::getDingUserId) + .ne(User::getDingUserId, "") + .eq(User::getStatus, 1) + ); + + log.info("找到 {} 个钉钉用户需要同步", users.size()); + + for (User user : users) { + try { + // 获取用户在钉钉中的部门信息 + Map dingUserInfo = getDingUserDetail(accessToken, user.getDingUserId()); + + if (dingUserInfo != null) { + // 获取用户的主部门ID + Object deptIdListObj = dingUserInfo.get("dept_id_list"); + if (deptIdListObj instanceof List && !((List) deptIdListObj).isEmpty()) { + List deptIdList = (List) deptIdListObj; + Long dingDeptId = deptIdList.get(0).longValue(); // 取第一个作为主部门 + + // 查找系统中对应的部门 + Department sysDept = departmentMapper.selectOne( + new LambdaQueryWrapper() + .eq(Department::getDingDeptId, dingDeptId.toString()) + .eq(Department::getStatus, 1) + ); + + if (sysDept != null) { + // 检查用户部门是否需要更新 + if (!sysDept.getId().equals(user.getDeptId())) { + user.setDeptId(sysDept.getId()); + user.setDeptName(sysDept.getDeptName()); + userMapper.updateById(user); + updatedUsers++; + log.info("更新用户部门: userId={}, userName={}, newDeptId={}, newDeptName={}", + user.getId(), user.getRealName(), sysDept.getId(), sysDept.getDeptName()); + } else { + skippedUsers++; + } + } else { + // 系统中没有对应部门,尝试获取钉钉部门信息并创建 + Map dingDeptInfo = getDingDepartmentDetail(accessToken, dingDeptId); + if (dingDeptInfo != null) { + String deptName = (String) dingDeptInfo.get("name"); + log.warn("用户 {} 的钉钉部门 {} ({}) 在系统中不存在,请先在部门管理中创建对应部门", + user.getRealName(), deptName, dingDeptId); + errors.add(String.format("用户 %s 的钉钉部门 %s 在系统中不存在", user.getRealName(), deptName)); + } + errorUsers++; + } + } else { + skippedUsers++; + log.debug("用户 {} 在钉钉中没有部门信息", user.getRealName()); + } + } else { + errorUsers++; + errors.add(String.format("无法获取用户 %s 的钉钉信息", user.getRealName())); + } + } catch (Exception e) { + errorUsers++; + errors.add(String.format("同步用户 %s 失败: %s", user.getRealName(), e.getMessage())); + log.error("同步用户 {} 失败", user.getRealName(), e); + } + } + + } catch (Exception e) { + log.error("同步钉钉组织架构失败", e); + throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "同步失败: " + e.getMessage()); + } + + Map result = new HashMap<>(); + result.put("updatedUsers", updatedUsers); + result.put("skippedUsers", skippedUsers); + result.put("errorUsers", errorUsers); + result.put("errors", errors); + + log.info("钉钉组织架构同步完成: 更新{}人, 跳过{}人, 失败{}人", updatedUsers, skippedUsers, errorUsers); + + return result; + } + + /** + * 获取钉钉用户详情 + */ + private Map getDingUserDetail(String accessToken, String dingUserId) { + try { + String url = String.format("%s/topapi/v2/user/get?access_token=%s", DINGTALK_API_BASE, accessToken); + + Map request = new HashMap<>(); + request.put("userid", dingUserId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(request, headers); + + ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); + Map body = response.getBody(); + + if (body != null) { + Object errcode = body.get("errcode"); + if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) { + return (Map) body.get("result"); + } else { + log.warn("获取钉钉用户详情失败: userId={}, errcode={}, errmsg={}", + dingUserId, errcode, body.get("errmsg")); + } + } + } catch (Exception e) { + log.error("获取钉钉用户详情异常: userId={}", dingUserId, e); + } + return null; + } + + /** + * 获取钉钉部门详情 + */ + private Map getDingDepartmentDetail(String accessToken, Long deptId) { + try { + String url = String.format("%s/topapi/v2/department/get?access_token=%s", DINGTALK_API_BASE, accessToken); + + Map request = new HashMap<>(); + request.put("dept_id", deptId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(request, headers); + + ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); + Map body = response.getBody(); + + if (body != null) { + Object errcode = body.get("errcode"); + if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) { + return (Map) body.get("result"); + } + } + } catch (Exception e) { + log.error("获取钉钉部门详情异常: deptId={}", deptId, e); + } + return null; } /** diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index c234523..6919ccf 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -471,10 +471,16 @@

部门管理

- - - 添加部门 - +
+ + + 同步钉钉组织 + + + + 添加部门 + +
import { ref, reactive, onMounted, computed, watch } from 'vue' import DefaultLayout from '@/layouts/DefaultLayout.vue' -import { message } from 'ant-design-vue' +import { message, Modal } from 'ant-design-vue' import zhCN from 'ant-design-vue/es/locale/zh_CN' import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' @@ -2442,6 +2448,9 @@ const loading = reactive({ departments: false }) +// 同步钉钉组织架构状态 +const syncingOrganization = ref(false) + const modals = reactive({ demandType: false, taskSubType: false, @@ -3371,6 +3380,37 @@ const loadDepartments = async () => { } } +// 同步钉钉组织架构 +const handleSyncOrganization = async () => { + syncingOrganization.value = true + try { + const response = await request.post('/dingtalk/sync-organization') + if (response.code === 200) { + const result = response.data as any + message.success(`同步完成!更新${result.updatedUsers}人,跳过${result.skippedUsers}人,失败${result.errorUsers}人`) + + // 如果有错误信息,显示详情 + if (result.errors && result.errors.length > 0) { + Modal.warning({ + title: '部分用户同步失败', + content: result.errors.join('\n'), + width: 500 + }) + } + + // 刷新部门和用户数据 + await Promise.all([loadDepartments(), loadDepartmentUsers()]) + } else { + message.error(response.message || '同步失败') + } + } catch (error: any) { + console.error('同步钉钉组织架构失败:', error) + message.error(error?.response?.data?.message || error?.message || '同步失败') + } finally { + syncingOrganization.value = false + } +} + const loadDepartmentUsers = async () => { userLoading.value = true try {