feat: 添加钉钉组织架构同步功能,同步用户部门信息但保留手动设置的部门负责人

This commit is contained in:
Your Name 2026-01-09 10:48:32 +08:00
parent 841154677f
commit dcb86dc9f2
3 changed files with 221 additions and 15 deletions

View File

@ -435,7 +435,19 @@ public class DingTalkController {
@Operation(summary = "同步钉钉用户")
@PostMapping("/sync-users")
public Result<Integer> syncUsers() {
return Result.success(dingTalkService.syncUsers());
return Result.success(0); // 废弃,使用 sync-organization 代替
}
@Operation(summary = "同步钉钉组织架构", description = "同步用户部门信息,但保留管理员手动设置的部门负责人")
@PostMapping("/sync-organization")
public Result<Map<String, Object>> syncOrganization() {
try {
Map<String, Object> result = dingTalkService.syncOrganization();
return Result.success(result);
} catch (Exception e) {
log.error("同步钉钉组织架构失败", e);
return Result.error("同步失败: " + e.getMessage());
}
}
@Operation(summary = "检查功能状态")

View File

@ -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<String, Object> 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<String> errors = new ArrayList<>();
return 0;
try {
String accessToken = getAccessToken(config);
if (accessToken == null) {
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取钉钉访问令牌失败");
}
// 获取系统中所有有钉钉ID的用户
List<User> users = userMapper.selectList(
new LambdaQueryWrapper<User>()
.isNotNull(User::getDingUserId)
.ne(User::getDingUserId, "")
.eq(User::getStatus, 1)
);
log.info("找到 {} 个钉钉用户需要同步", users.size());
for (User user : users) {
try {
// 获取用户在钉钉中的部门信息
Map<String, Object> dingUserInfo = getDingUserDetail(accessToken, user.getDingUserId());
if (dingUserInfo != null) {
// 获取用户的主部门ID
Object deptIdListObj = dingUserInfo.get("dept_id_list");
if (deptIdListObj instanceof List && !((List<?>) deptIdListObj).isEmpty()) {
List<Integer> deptIdList = (List<Integer>) deptIdListObj;
Long dingDeptId = deptIdList.get(0).longValue(); // 取第一个作为主部门
// 查找系统中对应的部门
Department sysDept = departmentMapper.selectOne(
new LambdaQueryWrapper<Department>()
.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<String, Object> 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<String, Object> 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<String, Object> getDingUserDetail(String accessToken, String dingUserId) {
try {
String url = String.format("%s/topapi/v2/user/get?access_token=%s", DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("userid", dingUserId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> 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<String, Object>) 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<String, Object> getDingDepartmentDetail(String accessToken, Long deptId) {
try {
String url = String.format("%s/topapi/v2/department/get?access_token=%s", DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("dept_id", deptId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> 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<String, Object>) body.get("result");
}
}
} catch (Exception e) {
log.error("获取钉钉部门详情异常: deptId={}", deptId, e);
}
return null;
}
/**

View File

@ -471,10 +471,16 @@
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">部门管理</h2>
<a-button type="primary" @click="showDepartmentModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加部门
</a-button>
<div style="display: flex; gap: 12px;">
<a-button @click="handleSyncOrganization" :loading="syncingOrganization">
<template #icon><Icon name="refresh" size="sm" /></template>
同步钉钉组织
</a-button>
<a-button type="primary" @click="showDepartmentModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加部门
</a-button>
</div>
</div>
<a-alert
@ -2315,7 +2321,7 @@
<script setup lang="ts">
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 {