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;
|
2025-09-30 08:59:35 +08:00
|
|
|
|
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-30 08:59:35 +08:00
|
|
|
|
|
2025-09-26 13:35:45 +08:00
|
|
|
|
private const string UserListIdUrl = "https://oapi.dingtalk.com/topapi/user/listid";
|
2025-09-30 08:59:35 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//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
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//Logger.Info("正在获取部门结构...");
|
2025-09-26 13:35:45 +08:00
|
|
|
|
var departments = await GetSubDepartmentsAsync();
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//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>();
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//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));
|
2025-09-30 08:59:35 +08:00
|
|
|
|
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();
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//Logger.Info($"共获取到 {userIdList.Count} 个用户");
|
2025-09-25 14:37:10 +08:00
|
|
|
|
|
2025-09-26 13:35:45 +08:00
|
|
|
|
// 分批处理用户(每批100人)
|
2025-09-30 08:59:35 +08:00
|
|
|
|
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));
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//Logger.Info($"正在处理用户批次 {i / batchSize + 1}/{(userIdList.Count - 1) / batchSize + 1}");
|
2025-09-26 13:35:45 +08:00
|
|
|
|
|
2025-09-30 08:59:35 +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
|
|
|
|
|
2025-09-30 08:59:35 +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
|
2025-09-30 08:59:35 +08:00
|
|
|
|
{
|
|
|
|
|
|
var data = await HttpUtil.SendPostRequest<DingTalkResponse<EmployeeQuery>>(UserListIdUrl, queryParams, JsonConvert.SerializeObject(payload));
|
|
|
|
|
|
if (data.errcode == 0)
|
2025-09-25 14:37:10 +08:00
|
|
|
|
{
|
2025-09-30 08:59:35 +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
|
|
|
|
|
2025-09-30 08:59:35 +08:00
|
|
|
|
var data = await HttpUtil.SendPostRequest<DingTalkResponse<List<DingTalkDepartment>>>(DepartmentSubUrl, queryParams, JsonConvert.SerializeObject(payload));
|
2025-09-26 13:35:45 +08:00
|
|
|
|
|
2025-09-30 08:59:35 +08:00
|
|
|
|
if (data.errcode == 0)
|
2025-09-25 14:37:10 +08:00
|
|
|
|
{
|
2025-09-30 08:59:35 +08:00
|
|
|
|
foreach (var dept in data.result)
|
2025-09-25 14:37:10 +08:00
|
|
|
|
{
|
2025-09-30 08:59:35 +08:00
|
|
|
|
departments.Add(dept);
|
|
|
|
|
|
|
|
|
|
|
|
await Task.Delay(100); // 间隔时间,避免请求过快
|
2025-09-26 13:35:45 +08:00
|
|
|
|
// 递归获取子部门
|
2025-09-30 08:59:35 +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-30 08:59:35 +08:00
|
|
|
|
|
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" }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-30 08:59:35 +08:00
|
|
|
|
//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
|
|
|
|
{
|
2025-09-30 08:59:35 +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
|
|
|
|
|
2025-09-30 08:59:35 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using (var client = new HttpClient())
|
2025-09-26 13:35:45 +08:00
|
|
|
|
{
|
2025-09-30 08:59:35 +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();
|
2025-09-30 08:59:35 +08:00
|
|
|
|
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
|
|
|
|
}
|
2025-09-30 08:59:35 +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
|
|
|
|
{
|
2025-09-30 08:59:35 +08:00
|
|
|
|
Logger.Error($"{data.errcode},{data.errmsg}");
|
|
|
|
|
|
throw new Exception($"{data.errcode},{data.errmsg}");
|
2025-09-26 13:35:45 +08:00
|
|
|
|
}
|
2025-09-30 08:59:35 +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
|
|
|
|
}
|
2025-09-30 08:59:35 +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
|
|
|
|
}
|
|
|
|
|
|
}
|