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; using static VOL.DingTalk.Util; 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 const string CreateDept = "https://oapi.dingtalk.com/topapi/v2/department/create"; private const string UpdateEmpInfoUrl = "https://oapi.dingtalk.com/topapi/v2/user/update"; // 需要获取的花名册字段代码 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)); if(userIds != null) { 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 { var data = await HttpUtil.SendPostRequest>(UserListIdUrl, queryParams, JsonConvert.SerializeObject(payload)); if (data.errcode == 0) { return data.result.userid_list; } else { return null; } } 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 { var data = await HttpUtil.SendPostRequest>>(DepartmentSubUrl, queryParams, JsonConvert.SerializeObject(payload)); if (data.errcode == 0) { foreach (var dept in data.result) { departments.Add(dept); await Task.Delay(100); // 间隔时间,避免请求过快 // 递归获取子部门 departments.AddRange(await GetSubDepartmentsAsync(dept.dept_id)); } } 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; // } //} try { var data = await HttpUtil.SendPostRequest>(_tokenUrl, new Dictionary(), JsonConvert.SerializeObject(payload)); return data["access_token"]; } catch (HttpRequestException ex) { Logger.Error($"获取access_token失败: {ex.Message}"); return null; } } public void CreateDingTalkDept(DingTalkDepartment dept) { var accessToken = GetValidTokenAsync().Result; if (accessToken == null) { //return new List(); throw new Exception("Token 失效"); } var departments = new List(); var payload = new Dictionary { {"name", dept.name } , {"parent_id", dept.parent_id } , }; 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(CreateDept); uriBuilder.Query = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); var response = client.PostAsync(uriBuilder.Uri, content).Result; response.EnsureSuccessStatusCode(); var responseContent = response.Content.ReadAsStringAsync().Result; var data = JsonConvert.DeserializeObject>>(responseContent); if(data.errcode != 0) { Logger.Error($"{data.errcode},{data.errmsg}"); throw new Exception($"{data.errcode},{data.errmsg}"); } } }catch(Exception) { throw; } } public async Task UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo) { if(updateInfo == null || string.IsNullOrEmpty( updateInfo.userid)) { throw new Exception("参数错误"); } var accessToken = await GetValidTokenAsync(); if (accessToken == null) { return false; } var queryParams = new Dictionary { { "access_token", accessToken } }; try { var data = await HttpUtil.SendPostRequest>(UpdateEmpInfoUrl, queryParams, JsonConvert.SerializeObject(updateInfo, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); if(data.errcode != 0) { Logger.Error($"{data.errcode},{data.errmsg}"); throw new Exception($"{data.errcode},{data.errmsg}"); } //using (var client = new HttpClient()) //{ // var jsonPayload = JsonConvert.SerializeObject(updateInfo, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); // var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); // var uriBuilder = new UriBuilder(UpdateEmpInfoUrl); // 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(data.errcode != 0) // { // Logger.Error($"{data.errcode},{data.errmsg}"); // throw new Exception($"{data.errcode},{data.errmsg}"); // } //} } catch (HttpRequestException ex) { Logger.Error($"获取部门列表失败: {ex.Message}"); throw new Exception($"获取部门列表失败: {ex.Message}"); } return true; } } }