619 lines
24 KiB
C#
619 lines
24 KiB
C#
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<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));
|
||
if(userIds != null)
|
||
{
|
||
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
|
||
{
|
||
var data = await HttpUtil.SendPostRequest<DingTalkResponse<EmployeeQuery>>(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<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
|
||
{
|
||
|
||
var data = await HttpUtil.SendPostRequest<DingTalkResponse<List<DingTalkDepartment>>>(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<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;
|
||
// }
|
||
//}
|
||
try
|
||
{
|
||
var data = await HttpUtil.SendPostRequest<Dictionary<string, string>>(_tokenUrl, new Dictionary<string, string>(), 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<DingTalkEmployee>();
|
||
throw new Exception("Token 失效");
|
||
}
|
||
|
||
var departments = new List<DingTalkDepartment>();
|
||
var payload = new Dictionary<string, object> {
|
||
{"name", dept.name } ,
|
||
{"parent_id", dept.parent_id } ,
|
||
};
|
||
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(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<DingTalkResponse<Dictionary<string,object>>>(responseContent);
|
||
|
||
if(data.errcode != 0)
|
||
{
|
||
Logger.Error($"{data.errcode},{data.errmsg}");
|
||
throw new Exception($"{data.errcode},{data.errmsg}");
|
||
}
|
||
|
||
}
|
||
}catch(Exception)
|
||
{
|
||
throw;
|
||
}
|
||
}
|
||
|
||
|
||
public async Task<bool> 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<string, string> { { "access_token", accessToken } };
|
||
|
||
try
|
||
{
|
||
|
||
var data = await HttpUtil.SendPostRequest<DingTalkResponse<object>>(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<DingTalkResponse<object>>(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;
|
||
}
|
||
}
|
||
} |