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

538 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string> _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<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<DingTalkEmployee>> GetAllEmployeesAsync()
{
DateTime startTime = DateTime.Now;
Logger.Info("正在获取钉钉访问凭证...");
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<DingTalkEmployee>();
}
Logger.Info("正在获取部门结构...");
var departments = await GetSubDepartmentsAsync();
Logger.Info($"共获取到 {departments.Count} 个部门");
HashSet<string> allUserIds = new HashSet<string>();
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<string> userIdList = allUserIds.ToList();
Logger.Info($"共获取到 {userIdList.Count} 个用户");
// 分批处理用户每批100人
var allEmployees = new List<DingTalkEmployee>();
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<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.Error($"获取部门用户失败: {ex.Message}");
return new List<string>();
}
}
public async Task<List<DingTalkEmployeeRsp>> GetRosterInfoAsync(List<string> userIds)
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<DingTalkEmployeeRsp>();
}
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", _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<Dictionary<string, object>>(responseContent);
//return ((Newtonsoft.Json.Linq.JArray)data["result"]).ToObject<List<DingTalkEmployeeRsp>>();
var rspResult = JsonConvert.DeserializeObject<DingTalkResponse<List<DingTalkEmployeeRsp>>>(responseContent);
return rspResult.result;
}
}
catch (HttpRequestException ex)
{
Logger.Error($"获取花名册失败: {ex.Message}");
return new List<DingTalkEmployeeRsp>();
}
}
public async Task<List<DingTalkDepartment>> GetSubDepartmentsAsync(long parentId = 1)
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<DingTalkDepartment>();
}
var departments = new List<DingTalkDepartment>();
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 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<DingTalkDepartment>();
}
}
public async Task<string> 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<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", _config.AgentId },
{
"param", new Dictionary<string, object>
{
{ "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<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.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<string> GetAccessTokenAsync()
{
var payload = new Dictionary<string, string>
{
{ "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<Dictionary<string, string>>(responseContent);
return data["access_token"];
}
catch (HttpRequestException ex)
{
Logger.Error($"获取access_token失败: {ex.Message}");
return null;
}
}
}
}
}