feat: 添加钉钉组织架构同步功能,同步用户部门信息但保留手动设置的部门负责人
This commit is contained in:
parent
841154677f
commit
dcb86dc9f2
@ -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 = "检查功能状态")
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user