using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Globalization; using System.Net.Http.Headers; using System.Text; namespace VOL.HR.Services.DingTalk { public class DingTalkService { private readonly string _corpId; private readonly string _appKey; private readonly string _appSecret; private readonly string _agentId; private string _accessToken; private DateTime _tokenExpiry; private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1); private readonly ILogger _logger; // 钉钉API地址 private readonly string _tokenUrl; private const string DepartmentSubUrl = "https://oapi.dingtalk.com/topapi/v2/department/listsub"; private const string UserListIdUrl = "https://oapi.dingtalk.com/topapi/user/listid"; private const string RosterUrl = "https://api.dingtalk.com/v1.0/hrm/rosters/lists/query"; private const string UpdateEmployeeUrl = "https://oapi.dingtalk.com/topapi/smartwork/hrm/employee/v2/update"; // 需要获取的花名册字段代码 private readonly List _fieldCodes = new List { "sys00-name", // 姓名 "sys00-email", // 邮箱 "sys00-mobile", // 手机号 "sys02-certNo", // 证件号码 "sys02-birthTime", // 出生日期 "sys02-sexType", // 性别 "sys00-dept", // 部门 "sys00-jobNumber" // 工号 }; public DingTalkService(string corpId, string appKey, string appSecret, string agentId, ILogger logger) { _corpId = corpId; _appKey = appKey; _appSecret = appSecret; _agentId = agentId; _tokenUrl = $"https://api.dingtalk.com/v1.0/oauth2/{_corpId}/token"; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } private async Task GetAccessTokenAsync() { var payload = new Dictionary { { "client_id", _appKey }, { "client_secret", _appSecret }, { "grant_type", "client_credentials" } }; using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var jsonPayload = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); try { var response = await client.PostAsync(_tokenUrl, content); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject>(responseContent); return data["access_token"]; } catch (HttpRequestException ex) { _logger.LogError($"获取access_token失败: {ex.Message}"); return null; } } } public async Task GetValidTokenAsync() { await _tokenLock.WaitAsync(); try { // 如果token不存在或已过期,重新获取 if (string.IsNullOrEmpty(_accessToken) || DateTime.Now >= _tokenExpiry) { _logger.LogInformation("钉钉access_token已过期,重新获取..."); var token = await GetAccessTokenAsync(); if (token != null) { _accessToken = token; // 钉钉token有效期为7200秒,提前300秒刷新 _tokenExpiry = DateTime.Now + TimeSpan.FromSeconds(6900); } else { _logger.LogError("无法获取有效的钉钉access_token"); return null; } } return _accessToken; } finally { _tokenLock.Release(); } } public async Task>> GetSubDepartmentsAsync(long parentId = 1) { var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return new List>(); } var departments = new List>(); var payload = new Dictionary { { "dept_id", parentId } }; var queryParams = new Dictionary { { "access_token", accessToken } }; try { using (var client = new HttpClient()) { var jsonPayload = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var uriBuilder = new UriBuilder(DepartmentSubUrl); uriBuilder.Query = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); var response = await client.PostAsync(uriBuilder.Uri, content); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject>(responseContent); if (Convert.ToInt32(data["errcode"]) == 0) { var result = (Newtonsoft.Json.Linq.JArray)data["result"]; foreach (Newtonsoft.Json.Linq.JObject dept in result) { long deptId = (long)dept["dept_id"]; var department = new Dictionary { { "id", deptId }, { "name", dept["name"].ToString() }, { "parent_id", (long)dept["parent_id"] } }; departments.Add(department); // 递归获取子部门 departments.AddRange(await GetSubDepartmentsAsync(deptId)); } } return departments; } } catch (HttpRequestException ex) { _logger.LogError($"获取部门列表失败: {ex.Message}"); return new List>(); } } public async Task> GetDeptUserIdsAsync(long deptId) { var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return new List(); } var payload = new Dictionary { { "dept_id", deptId } }; var queryParams = new Dictionary { { "access_token", accessToken } }; try { using (var client = new HttpClient()) { var jsonPayload = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var uriBuilder = new UriBuilder(UserListIdUrl); uriBuilder.Query = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); var response = await client.PostAsync(uriBuilder.Uri, content); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject>(responseContent); if (Convert.ToInt32(data["errcode"]) == 0) { var result = (Newtonsoft.Json.Linq.JObject)data["result"]; return ((Newtonsoft.Json.Linq.JArray)result["userid_list"]).Select(x => x.ToString()).ToList(); } return new List(); } } catch (HttpRequestException ex) { _logger.LogError($"获取部门用户失败: {ex.Message}"); return new List(); } } public async Task>> GetRosterInfoAsync(List userIds) { var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return new List>(); } var headers = new Dictionary { { "x-acs-dingtalk-access-token", accessToken }, { "Content-Type", "application/json" } }; var payload = new Dictionary { { "userIdList", userIds }, { "fieldFilterList", _fieldCodes }, { "appAgentId", _agentId }, { "text2SelectConvert", true } }; try { using (var client = new HttpClient()) { foreach (var header in headers) { client.DefaultRequestHeaders.Add(header.Key, header.Value); } var jsonPayload = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var response = await client.PostAsync(RosterUrl, content); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject>(responseContent); return ((Newtonsoft.Json.Linq.JArray)data["result"]).ToObject>>(); } } catch (HttpRequestException ex) { _logger.LogError($"获取花名册失败: {ex.Message}"); return new List>(); } } public List> ExtractEmployeeData(List> rosterData) { var employees = new List>(); foreach (var userData in rosterData) { var emp = new Dictionary { { "userid", userData["userId"].ToString() } }; var fieldDataList = (Newtonsoft.Json.Linq.JArray)userData["fieldDataList"]; foreach (Newtonsoft.Json.Linq.JObject field in fieldDataList) { string fieldCode = field["fieldCode"].ToString(); string fieldValue = ""; var fieldValueList = (Newtonsoft.Json.Linq.JArray)field["fieldValueList"]; if (fieldValueList != null && fieldValueList.Count > 0) { // 处理部门信息 if (fieldCode == "sys00-dept") { // 部门可能有多个,取第一个 fieldValue = field["fieldValueList"][0]["label"].ToString(); } else { fieldValue = field["fieldValueList"][0]["label"].ToString(); } } // 映射字段到中文名 switch (fieldCode) { case "sys00-name": emp["姓名"] = fieldValue; break; case "sys00-email": emp["邮箱"] = fieldValue; break; case "sys00-dept": emp["部门"] = fieldValue; break; case "sys00-mobile": emp["手机号"] = fieldValue; break; case "sys02-certNo": emp["证件号码"] = fieldValue; break; case "sys02-birthTime": emp["出生日期"] = fieldValue; break; case "sys02-sexType": emp["性别"] = fieldValue; break; case "sys00-jobNumber": emp["工号"] = fieldValue; break; } } employees.Add(emp); } return employees; } public async Task<(List>, List>)> GetAllEmployeesAsync() { DateTime startTime = DateTime.Now; _logger.LogInformation("正在获取钉钉访问凭证..."); var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return (new List>(), new List>()); } _logger.LogInformation("正在获取部门结构..."); var departments = await GetSubDepartmentsAsync(); _logger.LogInformation($"共获取到 {departments.Count} 个部门"); HashSet allUserIds = new HashSet(); _logger.LogInformation("正在获取部门用户列表..."); foreach (var dept in departments) { var userIds = await GetDeptUserIdsAsync(Convert.ToInt64(dept["id"])); allUserIds.UnionWith(userIds); _logger.LogInformation($"部门 {dept["name"]} 有 {userIds.Count} 个用户"); } List userIdList = allUserIds.ToList(); _logger.LogInformation($"共获取到 {userIdList.Count} 个用户"); // 分批处理用户(每批100人) List> allEmployees = new List>(); int batchSize = 100; for (int i = 0; i < userIdList.Count; i += batchSize) { var batch = userIdList.GetRange(i, Math.Min(batchSize, userIdList.Count - i)); _logger.LogInformation($"正在处理用户批次 {i / batchSize + 1}/{(userIdList.Count - 1) / batchSize + 1}"); var rosterData = await GetRosterInfoAsync(batch); var employees = ExtractEmployeeData(rosterData); allEmployees.AddRange(employees); } _logger.LogInformation($"成功获取 {allEmployees.Count} 名员工信息"); _logger.LogInformation($"钉钉数据获取完成,耗时: {DateTime.Now.Subtract(startTime).TotalSeconds:.2f}秒"); return (allEmployees, departments); } public async Task UpdateEmployeeAsync(string userId, Dictionary> fieldUpdates) { var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return false; } // 构建更新参数 - 根据API文档调整结构 var groups = new List>(); foreach (var groupId in fieldUpdates.Keys) { var fields = fieldUpdates[groupId]; var fieldList = new List>(); foreach (var fieldCode in fields.Keys) { string value = fields[fieldCode]; // 处理特殊字段类型 object processedValue = value; // 日期字段需要特殊处理 if (fieldCode == "sys02-birthTime" && !string.IsNullOrEmpty(value)) { // 确保日期格式正确 processedValue = FormatDate(value); } // 性别字段需要转换 if (fieldCode == "sys02-sexType") { processedValue = ConvertGenderForDingTalk(value); } fieldList.Add(new Dictionary { { "field_code", fieldCode }, { "value", processedValue } }); } groups.Add(new Dictionary { { "group_id", groupId }, { "sections", new List> { new Dictionary { { "section", fieldList } } } } }); } var queryParams = new Dictionary { { "access_token", accessToken } }; var payload = new Dictionary { { "agentid", _agentId }, { "param", new Dictionary { { "userid", userId }, { "groups", groups } } } }; try { _logger.LogInformation($"钉钉更新请求: {JsonConvert.SerializeObject(payload, Formatting.None)}"); using (var client = new HttpClient()) { var uriBuilder = new UriBuilder(UpdateEmployeeUrl); uriBuilder.Query = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); var jsonPayload = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var response = await client.PostAsync(uriBuilder.Uri, content); _logger.LogInformation($"钉钉更新响应状态: {response.StatusCode}"); _logger.LogInformation($"钉钉更新响应内容: {await response.Content.ReadAsStringAsync()}"); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject>(responseContent); // 根据钉钉API文档检查响应 if (Convert.ToInt32(data.GetValueOrDefault("errcode", -1)) == 0 && Convert.ToBoolean(data.GetValueOrDefault("result", false)) && Convert.ToBoolean(data.GetValueOrDefault("success", false))) { _logger.LogInformation($"员工 {userId} 更新成功"); return true; } else { string errorMsg = data.GetValueOrDefault("errmsg", "未知错误")?.ToString(); _logger.LogError($"员工更新失败: {errorMsg}"); return false; } } } catch (HttpRequestException ex) { _logger.LogError($"更新员工信息失败: {ex.Message}"); return false; } } private string FormatDate(string dateStr) { try { // 尝试解析各种日期格式 if (!string.IsNullOrEmpty(dateStr)) { // 移除时间部分(如果有) if (dateStr.Contains(" ")) { dateStr = dateStr.Split(" ")[0]; } // 尝试解析常见日期格式 foreach (string fmt in new string[] { "yyyy-MM-dd", "yyyy/MM/dd", "yyyy.MM.dd", "yyyy年MM月dd日" }) { if (DateTime.TryParseExact(dateStr, fmt, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateObj)) { return dateObj.ToString("yyyy-MM-dd"); } } } return dateStr; } catch { return dateStr; } } private string ConvertGenderForDingTalk(string gender) { if (gender == "男") { return "1"; } else if (gender == "女") { return "2"; } else { return gender; } } } }