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

619 lines
24 KiB
C#
Raw Normal View History

2025-09-26 13:35:45 +08:00
using Newtonsoft.Json;
2025-09-25 14:37:10 +08:00
using System.Globalization;
2025-09-26 13:35:45 +08:00
using System.Linq;
2025-09-25 14:37:10 +08:00
using System.Net.Http.Headers;
using System.Text;
2025-09-26 13:35:45 +08:00
using VOL.Core.Services;
using VOL.DingTalk.Models;
using VOL.DingTalk.Models.Biz;
using static VOL.DingTalk.Util;
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
namespace VOL.DingTalk.Services.Biz
2025-09-25 14:37:10 +08:00
{
public class DingTalkService
{
2025-09-26 13:35:45 +08:00
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";
2025-09-26 13:35:45 +08:00
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";
2025-09-26 13:35:45 +08:00
// 需要获取的花名册字段代码
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" // 职位
];
2025-09-25 14:37:10 +08:00
private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1);
// 钉钉API地址
private readonly string _tokenUrl;
2025-09-26 13:35:45 +08:00
private DingTalkConfig _config;
private SystemToken _token;
public DingTalkService(SystemToken token, DingTalkConfig config)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
_token = token;
_config = config;
//Init();
_tokenUrl = $"https://api.dingtalk.com/v1.0/oauth2/{_config.CorpId}/token";
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
public List<Dictionary<string, string>> ExtractEmployeeData(List<Dictionary<string, object>> rosterData)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
var employees = new List<Dictionary<string, string>>();
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
foreach (var userData in rosterData)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
var emp = new Dictionary<string, string> { { "userid", userData["userId"].ToString() } };
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
var fieldDataList = (Newtonsoft.Json.Linq.JArray)userData["fieldDataList"];
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
foreach (Newtonsoft.Json.Linq.JObject field in fieldDataList)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
string fieldCode = field["fieldCode"].ToString();
string fieldValue = "";
var fieldValueList = (Newtonsoft.Json.Linq.JArray)field["fieldValueList"];
if (fieldValueList != null && fieldValueList.Count > 0)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
// 处理部门信息
if (fieldCode == "sys00-dept")
{
// 部门可能有多个,取第一个
fieldValue = field["fieldValueList"][0]["label"].ToString();
}
else
{
fieldValue = field["fieldValueList"][0]["label"].ToString();
}
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
// 映射字段到中文名
switch (fieldCode)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
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;
2025-09-25 14:37:10 +08:00
}
}
2025-09-26 13:35:45 +08:00
employees.Add(emp);
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
return employees;
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
public async Task<List<DingTalkEmployee>> GetAllEmployeesAsync()
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
DateTime startTime = DateTime.Now;
//Logger.Info("正在获取钉钉访问凭证...");
2025-09-25 14:37:10 +08:00
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
2025-09-26 13:35:45 +08:00
return new List<DingTalkEmployee>();
2025-09-25 14:37:10 +08:00
}
//Logger.Info("正在获取部门结构...");
2025-09-26 13:35:45 +08:00
var departments = await GetSubDepartmentsAsync();
//Logger.Info($"共获取到 {departments.Count} 个部门");
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
HashSet<string> allUserIds = new HashSet<string>();
//Logger.Info("正在获取部门用户列表...");
2025-09-26 13:35:45 +08:00
foreach (var dept in departments)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
var userIds = await GetDeptUserIdsAsync(Convert.ToInt64(dept.dept_id));
if(userIds != null)
{
allUserIds.UnionWith(userIds);
}
//Logger.Info($"部门 {dept.name} 有 {userIds.Count} 个用户");
2025-09-26 13:35:45 +08:00
}
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
List<string> userIdList = allUserIds.ToList();
//Logger.Info($"共获取到 {userIdList.Count} 个用户");
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
// 分批处理用户每批100人
var allEmployees = new List<DingTalkEmployee>();
2025-09-26 13:35:45 +08:00
int batchSize = 100;
for (int i = 0; i < userIdList.Count; i += batchSize)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
var batch = userIdList.GetRange(i, Math.Min(batchSize, userIdList.Count - i));
//Logger.Info($"正在处理用户批次 {i / batchSize + 1}/{(userIdList.Count - 1) / batchSize + 1}");
2025-09-26 13:35:45 +08:00
var rosterData = await GetRosterInfoAsync(batch);
2025-09-26 13:35:45 +08:00
allEmployees.AddRange([.. rosterData.Select(DingTalkEmployee.TranFrom)]);
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
//Logger.Info($"成功获取 {allEmployees.Count} 名员工信息");
//Logger.Info($"钉钉数据获取完成,耗时: {DateTime.Now.Subtract(startTime).TotalSeconds:.2f}秒");
2025-09-26 13:35:45 +08:00
return allEmployees;
2025-09-25 14:37:10 +08:00
}
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)
2025-09-25 14:37:10 +08:00
{
return data.result.userid_list;
}
else
{
return null;
2025-09-25 14:37:10 +08:00
}
}
catch (HttpRequestException ex)
{
2025-09-26 13:35:45 +08:00
Logger.Error($"获取部门用户失败: {ex.Message}");
2025-09-25 14:37:10 +08:00
return new List<string>();
}
}
2025-09-26 13:35:45 +08:00
public async Task<List<DingTalkEmployeeRsp>> GetRosterInfoAsync(List<string> userIds)
2025-09-25 14:37:10 +08:00
{
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
2025-09-26 13:35:45 +08:00
return new List<DingTalkEmployeeRsp>();
2025-09-25 14:37:10 +08:00
}
var headers = new Dictionary<string, string>
{
{ "x-acs-dingtalk-access-token", accessToken },
2025-09-26 13:35:45 +08:00
//{ "Content-Type", "application/json" }
2025-09-25 14:37:10 +08:00
};
var payload = new Dictionary<string, object>
{
{ "userIdList", userIds },
{ "fieldFilterList", _fieldCodes },
2025-09-26 13:35:45 +08:00
{ "appAgentId", _config.AgentId },
2025-09-25 14:37:10 +08:00
{ "text2SelectConvert", true }
};
try
{
using (var client = new HttpClient())
{
2025-09-26 13:35:45 +08:00
client.DefaultRequestHeaders.Clear();
2025-09-25 14:37:10 +08:00
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();
2025-09-26 13:35:45 +08:00
//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;
2025-09-25 14:37:10 +08:00
}
}
catch (HttpRequestException ex)
{
2025-09-26 13:35:45 +08:00
Logger.Error($"获取花名册失败: {ex.Message}");
return new List<DingTalkEmployeeRsp>();
2025-09-25 14:37:10 +08:00
}
}
2025-09-26 13:35:45 +08:00
public async Task<List<DingTalkDepartment>> GetSubDepartmentsAsync(long parentId = 1)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
return new List<DingTalkDepartment>();
}
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
var departments = new List<DingTalkDepartment>();
var payload = new Dictionary<string, object> { { "dept_id", parentId } };
var queryParams = new Dictionary<string, string> { { "access_token", accessToken } };
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
try
{
2025-09-25 14:37:10 +08:00
var data = await HttpUtil.SendPostRequest<DingTalkResponse<List<DingTalkDepartment>>>(DepartmentSubUrl, queryParams, JsonConvert.SerializeObject(payload));
2025-09-26 13:35:45 +08:00
if (data.errcode == 0)
2025-09-25 14:37:10 +08:00
{
foreach (var dept in data.result)
2025-09-25 14:37:10 +08:00
{
departments.Add(dept);
await Task.Delay(100); // 间隔时间,避免请求过快
2025-09-26 13:35:45 +08:00
// 递归获取子部门
departments.AddRange(await GetSubDepartmentsAsync(dept.dept_id));
2025-09-26 13:35:45 +08:00
}
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
return departments;
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
catch (HttpRequestException ex)
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
Logger.Error($"获取部门列表失败: {ex.Message}");
return new List<DingTalkDepartment>();
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
}
2025-09-25 14:37:10 +08:00
2025-09-26 13:35:45 +08:00
public async Task<string> GetValidTokenAsync()
{
await _tokenLock.WaitAsync();
try
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
// 如果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;
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
finally
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
_tokenLock.Release();
2025-09-25 14:37:10 +08:00
}
}
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>
{
2025-09-26 13:35:45 +08:00
{ "agentid", _config.AgentId },
2025-09-25 14:37:10 +08:00
{
"param", new Dictionary<string, object>
{
{ "userid", userId },
{ "groups", groups }
}
}
};
try
{
2025-09-26 13:35:45 +08:00
Logger.Info($"钉钉更新请求: {JsonConvert.SerializeObject(payload, Formatting.None)}");
2025-09-25 14:37:10 +08:00
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);
2025-09-26 13:35:45 +08:00
Logger.Info($"钉钉更新响应状态: {response.StatusCode}");
Logger.Info($"钉钉更新响应内容: {await response.Content.ReadAsStringAsync()}");
2025-09-25 14:37:10 +08:00
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)))
{
2025-09-26 13:35:45 +08:00
Logger.Info($"员工 {userId} 更新成功");
2025-09-25 14:37:10 +08:00
return true;
}
else
{
string errorMsg = data.GetValueOrDefault("errmsg", "未知错误")?.ToString();
2025-09-26 13:35:45 +08:00
Logger.Error($"员工更新失败: {errorMsg}");
2025-09-25 14:37:10 +08:00
return false;
}
}
}
catch (HttpRequestException ex)
{
2025-09-26 13:35:45 +08:00
Logger.Error($"更新员工信息失败: {ex.Message}");
2025-09-25 14:37:10 +08:00
return false;
}
}
2025-09-26 13:35:45 +08:00
private string ConvertGenderForDingTalk(string gender)
{
if (gender == "男")
{
return "1";
}
else if (gender == "女")
{
return "2";
}
else
{
return gender;
}
}
2025-09-25 14:37:10 +08:00
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;
}
}
2025-09-26 13:35:45 +08:00
private async Task<string> GetAccessTokenAsync()
2025-09-25 14:37:10 +08:00
{
2025-09-26 13:35:45 +08:00
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)
2025-09-25 14:37:10 +08:00
{
//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 } };
2025-09-26 13:35:45 +08:00
try
{
using (var client = new HttpClient())
2025-09-26 13:35:45 +08:00
{
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;
2025-09-26 13:35:45 +08:00
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}");
}
2025-09-26 13:35:45 +08:00
}
}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)
2025-09-26 13:35:45 +08:00
{
Logger.Error($"{data.errcode},{data.errmsg}");
throw new Exception($"{data.errcode},{data.errmsg}");
2025-09-26 13:35:45 +08:00
}
//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}");
// }
//}
2025-09-25 14:37:10 +08:00
}
catch (HttpRequestException ex)
{
Logger.Error($"获取部门列表失败: {ex.Message}");
throw new Exception($"获取部门列表失败: {ex.Message}");
}
return true;
2025-09-25 14:37:10 +08:00
}
2025-09-26 13:35:45 +08:00
}
}