using Newtonsoft.Json; using System.Globalization; using System.Linq; using System.Net.Http.Headers; using System.Text; using VOL.Core.Services; using VOL.DingTalk.Models; using VOL.DingTalk.Models.Biz; namespace VOL.DingTalk.Services.Biz { public class DingTalkService { private const string DepartmentSubUrl = "https://oapi.dingtalk.com/topapi/v2/department/listsub"; 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 const string UserListIdUrl = "https://oapi.dingtalk.com/topapi/user/listid"; // 需要获取的花名册字段代码 private readonly List _fieldCodes = [ "sys00-name", // 姓名 "sys00-email", // 邮箱 "sys00-mobile", // 手机号 "sys02-certNo", // 证件号码 "sys02-birthTime", // 出生日期 "sys02-sexType", // 性别 "sys00-dept", // 部门 "sys00-jobNumber", // 工号 "sys00-reportManager", // 直接主管 "sys00-position" // 职位 ]; private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1); // 钉钉API地址 private readonly string _tokenUrl; private DingTalkConfig _config; private SystemToken _token; public DingTalkService(SystemToken token, DingTalkConfig config) { _token = token; _config = config; //Init(); _tokenUrl = $"https://api.dingtalk.com/v1.0/oauth2/{_config.CorpId}/token"; } 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> GetAllEmployeesAsync() { DateTime startTime = DateTime.Now; Logger.Info("正在获取钉钉访问凭证..."); var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return new List(); } Logger.Info("正在获取部门结构..."); var departments = await GetSubDepartmentsAsync(); Logger.Info($"共获取到 {departments.Count} 个部门"); HashSet allUserIds = new HashSet(); Logger.Info("正在获取部门用户列表..."); foreach (var dept in departments) { var userIds = await GetDeptUserIdsAsync(Convert.ToInt64(dept.dept_id)); allUserIds.UnionWith(userIds); Logger.Info($"部门 {dept.name} 有 {userIds.Count} 个用户"); } List userIdList = allUserIds.ToList(); Logger.Info($"共获取到 {userIdList.Count} 个用户"); // 分批处理用户(每批100人) var 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.Info($"正在处理用户批次 {i / batchSize + 1}/{(userIdList.Count - 1) / batchSize + 1}"); var rosterData = await GetRosterInfoAsync(batch); allEmployees.AddRange([.. rosterData.Select(DingTalkEmployee.TranFrom)]); } Logger.Info($"成功获取 {allEmployees.Count} 名员工信息"); Logger.Info($"钉钉数据获取完成,耗时: {DateTime.Now.Subtract(startTime).TotalSeconds:.2f}秒"); return allEmployees; } 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.Error($"获取部门用户失败: {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", _config.AgentId }, { "text2SelectConvert", true } }; try { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Clear(); 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>(); var rspResult = JsonConvert.DeserializeObject>>(responseContent); return rspResult.result; } } catch (HttpRequestException ex) { Logger.Error($"获取花名册失败: {ex.Message}"); return new List(); } } 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 DingTalkDepartment //{ // { "id", deptId }, // { "name", dept["name"].ToString() }, // { "parent_id", (long)dept["parent_id"] } //}; var department = new DingTalkDepartment { dept_id = (int)dept["dept_id"], name = dept["name"].ToString(), parent_id = (int)dept["parent_id"], auto_add_user = (bool)dept["auto_add_user"], create_dept_group = (bool)dept["create_dept_group"], ext = dept["ext"]?.ToString() }; departments.Add(department); //await Task.Delay(1500); // 间隔1.5秒,避免请求过快 // 递归获取子部门 departments.AddRange(await GetSubDepartmentsAsync(deptId)); } } return departments; } } catch (HttpRequestException ex) { Logger.Error($"获取部门列表失败: {ex.Message}"); return new List(); } } public async Task GetValidTokenAsync() { await _tokenLock.WaitAsync(); try { // 如果token不存在或已过期,重新获取 if (_token.IsTokenExpiry()) { Logger.Info("钉钉access_token已过期,重新获取..."); var token = await GetAccessTokenAsync(); if (token != null) { _token.Token = token; // 钉钉token有效期为7200秒,提前300秒刷新 _token.TokenExpiry = 6900; } else { Logger.Error("无法获取有效的钉钉access_token"); return null; } } return _token.Token; } finally { _tokenLock.Release(); } } 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", _config.AgentId }, { "param", new Dictionary { { "userid", userId }, { "groups", groups } } } }; try { Logger.Info($"钉钉更新请求: {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.Info($"钉钉更新响应状态: {response.StatusCode}"); Logger.Info($"钉钉更新响应内容: {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.Info($"员工 {userId} 更新成功"); return true; } else { string errorMsg = data.GetValueOrDefault("errmsg", "未知错误")?.ToString(); Logger.Error($"员工更新失败: {errorMsg}"); return false; } } } catch (HttpRequestException ex) { Logger.Error($"更新员工信息失败: {ex.Message}"); return false; } } private string ConvertGenderForDingTalk(string gender) { if (gender == "男") { return "1"; } else if (gender == "女") { return "2"; } else { return gender; } } 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 async Task GetAccessTokenAsync() { var payload = new Dictionary { { "client_id", _config.AppKey }, { "client_secret", _config.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.Error($"获取access_token失败: {ex.Message}"); return null; } } } } }