Laservall_manager_system/VOL.HR/Services/DingTalk/DingTalkService.cs

526 lines
21 KiB
C#
Raw Normal View History

2025-09-25 14:37:10 +08:00
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<DingTalkService> _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<string> _fieldCodes = new List<string>
{
"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<DingTalkService> 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<string> GetAccessTokenAsync()
{
var payload = new Dictionary<string, string>
{
{ "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<Dictionary<string, string>>(responseContent);
return data["access_token"];
}
catch (HttpRequestException ex)
{
_logger.LogError($"获取access_token失败: {ex.Message}");
return null;
}
}
}
public async Task<string> 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<List<Dictionary<string, object>>> GetSubDepartmentsAsync(long parentId = 1)
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<Dictionary<string, object>>();
}
var departments = new List<Dictionary<string, object>>();
var payload = new Dictionary<string, object> { { "dept_id", parentId } };
var queryParams = new Dictionary<string, string> { { "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<Dictionary<string, object>>(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<string, object>
{
{ "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<Dictionary<string, object>>();
}
}
public async Task<List<string>> GetDeptUserIdsAsync(long deptId)
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<string>();
}
var payload = new Dictionary<string, object> { { "dept_id", deptId } };
var queryParams = new Dictionary<string, string> { { "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<Dictionary<string, object>>(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<string>();
}
}
catch (HttpRequestException ex)
{
_logger.LogError($"获取部门用户失败: {ex.Message}");
return new List<string>();
}
}
public async Task<List<Dictionary<string, object>>> GetRosterInfoAsync(List<string> userIds)
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<Dictionary<string, object>>();
}
var headers = new Dictionary<string, string>
{
{ "x-acs-dingtalk-access-token", accessToken },
{ "Content-Type", "application/json" }
};
var payload = new Dictionary<string, object>
{
{ "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<Dictionary<string, object>>(responseContent);
return ((Newtonsoft.Json.Linq.JArray)data["result"]).ToObject<List<Dictionary<string, object>>>();
}
}
catch (HttpRequestException ex)
{
_logger.LogError($"获取花名册失败: {ex.Message}");
return new List<Dictionary<string, object>>();
}
}
public List<Dictionary<string, string>> ExtractEmployeeData(List<Dictionary<string, object>> rosterData)
{
var employees = new List<Dictionary<string, string>>();
foreach (var userData in rosterData)
{
var emp = new Dictionary<string, string> { { "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<Dictionary<string, string>>, List<Dictionary<string, object>>)> GetAllEmployeesAsync()
{
DateTime startTime = DateTime.Now;
_logger.LogInformation("正在获取钉钉访问凭证...");
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return (new List<Dictionary<string, string>>(), new List<Dictionary<string, object>>());
}
_logger.LogInformation("正在获取部门结构...");
var departments = await GetSubDepartmentsAsync();
_logger.LogInformation($"共获取到 {departments.Count} 个部门");
HashSet<string> allUserIds = new HashSet<string>();
_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<string> userIdList = allUserIds.ToList();
_logger.LogInformation($"共获取到 {userIdList.Count} 个用户");
// 分批处理用户每批100人
List<Dictionary<string, string>> allEmployees = new List<Dictionary<string, string>>();
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<bool> UpdateEmployeeAsync(string userId, Dictionary<string, Dictionary<string, string>> fieldUpdates)
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return false;
}
// 构建更新参数 - 根据API文档调整结构
var groups = new List<Dictionary<string, object>>();
foreach (var groupId in fieldUpdates.Keys)
{
var fields = fieldUpdates[groupId];
var fieldList = new List<Dictionary<string, object>>();
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<string, object>
{
{ "field_code", fieldCode },
{ "value", processedValue }
});
}
groups.Add(new Dictionary<string, object>
{
{ "group_id", groupId },
{
"sections", new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{ "section", fieldList }
}
}
}
});
}
var queryParams = new Dictionary<string, string> { { "access_token", accessToken } };
var payload = new Dictionary<string, object>
{
{ "agentid", _agentId },
{
"param", new Dictionary<string, object>
{
{ "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<Dictionary<string, object>>(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;
}
}
}
}