优化钉钉与YS系统的部门与员工同步逻辑

- 重构 `DingTalkService`,添加部门和员工相关API支持。
- 更新 `DingTalkResponse` 和 `DingTalkEmployeeRsp`,支持可空类型。
- 添加 `CacheYSDepartments` 和 `CacheDingTalkDepartments` 方法,用于缓存部门数据。
- 新增 `SyncYSERPDeptToDingTalk` 方法,实现YS部门同步到钉钉。
- 优化 `GetAllYSEmployees` 和 `GetAllDingTalkEmployees`,支持缓存。
- 添加 `UpdateEmpInfo` 和 `SyncYSEmpToDingTalk` 方法,支持员工信息更新与同步。
- 新增 `HR_DingTalkDept` 和 `HR_YSDept` 类,存储部门信息。
- 添加 `YSERPEmployeeInfo` 和 `YSERPPagedResponse` 类,支持YS员工数据结构。
- 引入 `HttpUtil` 工具类,封装HTTP请求逻辑。
- 添加相关控制器API,支持部门和员工数据的缓存与同步。
This commit is contained in:
Ling 2025-09-30 08:59:35 +08:00
parent f04f48ddd4
commit 9a9a41e9fa
26 changed files with 1314 additions and 160 deletions

View File

@ -34,7 +34,7 @@ namespace VOL.DingTalk.Models.Biz
public class EmployeeQuery {
public List<string> data_list { get; set; }
public List<string> userid_list { get; set; }
public long? next_cursor { get; set; }
}

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VOL.DingTalk.Models.Biz
{
/// <summary>
/// 20250929 属性文档参考 https://open.dingtalk.com/document/orgapp/update-dedicated-accounts-information
/// </summary>
public class DingTalkEmployeeUpdate
{
/// <summary>
/// 用户ID
/// </summary>
public string userid { get; set; }
/// <summary>
/// 扩展属性长度最大2000个字符。json对象 格式 "{\"爱好\":\"旅游\",\"年龄\":\"24\"}"
/// </summary>
public string extension { get; set; }
/// <summary>
/// 钉钉企业账号的登录名。
/// </summary>
public string? loginId { get; set; }
/// <summary>
/// 直属主管的userId。
/// </summary>
public string? manager_userid { get; set; }
/// <summary>
/// 通讯录语言,取值。
/// </summary>
public string? language { get; set; }
/// <summary>
/// 分机号长度最大50个字符。
/// </summary>
public string? telephone { get; set; }
/// <summary>
/// 是否号码隐藏:
/// </summary>
public string? hide_mobile { get; set; }
/// <summary>
/// 入职时间UNIX时间戳单位毫秒。
/// </summary>
public string? hired_date { get; set; }
/// <summary>
/// 职位长度最大200个字符。
/// </summary>
public string? title { get; set; }
/// <summary>
/// 企业账号员工的企业邮箱类型。
/// </summary>
public string? org_email_type { get; set; }
/// <summary>
/// 企业账号手机号。
/// </summary>
public string? avatarMediaId { get; set; }
/// <summary>
/// 员工在对应的部门中的职位。
/// </summary>
public string dept_title_list { get; set; }
/// <summary>
/// 办公地点长度最大100个字符。
/// </summary>
public string? work_place { get; set; }
/// <summary>
/// 员工在对应的部门中的排序。
/// </summary>
public string? dept_order_list { get; set; }
/// <summary>
/// 是否开启高管模式默认值false。
/// </summary>
public string? senior_mode { get; set; }
/// <summary>
/// 备注长度最大2000个字符。
/// </summary>
public string? remark { get; set; }
/// <summary>
/// 企业邮箱
/// </summary>
public string? org_email { get; set; }
/// <summary>
/// 用户名称长度最大80个字符。
/// </summary>
public string? name { get; set; }
/// <summary>
/// 企业账号的昵称。
/// </summary>
public string? nickname { get; set; }
/// <summary>
/// 企业账号手机号。
/// </summary>
public string? exclusive_mobile { get; set; }
/// <summary>
/// 强制更新的字段,支持清空指定的字段,多个字段之间使用逗号分隔。目前支持字段: manager_userid、org_email。
/// </summary>
public string? force_update_fields { get; set; }
/// <summary>
/// 所属部门ID列表。
/// </summary>
public string? dept_id_list { get; set; }
/// <summary>
/// 工号
/// </summary>
public string? job_number { get; set; }
/// <summary>
/// 邮箱
/// </summary>
public string? email { get; set; }
}
}

View File

@ -8,10 +8,10 @@ namespace VOL.DingTalk.Models.Biz
{
internal class DingTalkResponse<T>
{
public int errcode { get; set; }
public string errmsg { get; set; }
public T result { get; set; }
public string request_id { get; set; }
public int? errcode { get; set; }
public string? errmsg { get; set; }
public T? result { get; set; }
public string? request_id { get; set; }
public bool? success { get; set; }
}

View File

@ -16,13 +16,19 @@ namespace VOL.DingTalk.Models
*/
public class DingTalkConfig
{
#if DEBUG
public string CorpId = "dingec656045526ce9c0ee0f45d8e4f7c288";
public string AppKey = "dingtl9hb00ktdkguzrh";
public string AppSecret = "NkWagc8VEhdZyjEPkK0oYbi_ZzqVMHLelmFWdGswBjh2zGgSt0UWLj1W58xNHwfv";
public long AgentId = 3973173455;
#else
// 钉钉测试企业
public string CorpId = "ding994fa2867db3cb44ee0f45d8e4f7c288";
public string AppKey = "dingokdmazowevvpxolh";
public string AppSecret = "sYCVrttZZC-K_sIrPw1m20j41Pcbbxt2fC5joFQGOh7xjUxD8DT2I_KKZ4S2zje8";
public long AgentId = 4018728780;
#endif
public List<string> fields = [
"sys00-name", // 姓名
"sys00-email", // 邮箱
"sys00-mobile", // 手机号

View File

@ -6,6 +6,7 @@ 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
{
@ -14,7 +15,11 @@ namespace VOL.DingTalk.Services.Biz
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 =
[
@ -120,43 +125,46 @@ namespace VOL.DingTalk.Services.Biz
{
DateTime startTime = DateTime.Now;
Logger.Info("正在获取钉钉访问凭证...");
//Logger.Info("正在获取钉钉访问凭证...");
var accessToken = await GetValidTokenAsync();
if (accessToken == null)
{
return new List<DingTalkEmployee>();
}
Logger.Info("正在获取部门结构...");
//Logger.Info("正在获取部门结构...");
var departments = await GetSubDepartmentsAsync();
Logger.Info($"共获取到 {departments.Count} 个部门");
//Logger.Info($"共获取到 {departments.Count} 个部门");
HashSet<string> allUserIds = new HashSet<string>();
Logger.Info("正在获取部门用户列表...");
//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} 个用户");
if(userIds != null)
{
allUserIds.UnionWith(userIds);
}
//Logger.Info($"部门 {dept.name} 有 {userIds.Count} 个用户");
}
List<string> userIdList = allUserIds.ToList();
Logger.Info($"共获取到 {userIdList.Count} 个用户");
//Logger.Info($"共获取到 {userIdList.Count} 个用户");
// 分批处理用户每批100人
var allEmployees = new List<DingTalkEmployee>();
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}");
//Logger.Info($"正在处理用户批次 {i / batchSize + 1}/{(userIdList.Count - 1) / batchSize + 1}");
var rosterData = await GetRosterInfoAsync(batch);
var rosterData = await GetRosterInfoAsync(batch);
allEmployees.AddRange([.. rosterData.Select(DingTalkEmployee.TranFrom)]);
}
Logger.Info($"成功获取 {allEmployees.Count} 名员工信息");
Logger.Info($"钉钉数据获取完成,耗时: {DateTime.Now.Subtract(startTime).TotalSeconds:.2f}秒");
//Logger.Info($"成功获取 {allEmployees.Count} 名员工信息");
//Logger.Info($"钉钉数据获取完成,耗时: {DateTime.Now.Subtract(startTime).TotalSeconds:.2f}秒");
return allEmployees;
}
@ -173,25 +181,15 @@ namespace VOL.DingTalk.Services.Biz
var queryParams = new Dictionary<string, string> { { "access_token", accessToken } };
try
{
using (var client = new HttpClient())
{
var data = await HttpUtil.SendPostRequest<DingTalkResponse<EmployeeQuery>>(UserListIdUrl, queryParams, JsonConvert.SerializeObject(payload));
if (data.errcode == 0)
{
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>();
return data.result.userid_list;
}
else
{
return null;
}
}
catch (HttpRequestException ex)
@ -266,48 +264,22 @@ namespace VOL.DingTalk.Services.Biz
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);
var data = await HttpUtil.SendPostRequest<DingTalkResponse<List<DingTalkDepartment>>>(DepartmentSubUrl, queryParams, JsonConvert.SerializeObject(payload));
if (Convert.ToInt32(data["errcode"]) == 0)
if (data.errcode == 0)
{
var result = (Newtonsoft.Json.Linq.JArray)data["result"];
foreach (Newtonsoft.Json.Linq.JObject dept in result)
foreach (var dept in data.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);
departments.Add(dept);
//await Task.Delay(1500); // 间隔1.5秒,避免请求过快
await Task.Delay(100); // 间隔时间,避免请求过快
// 递归获取子部门
departments.AddRange(await GetSubDepartmentsAsync(deptId));
departments.AddRange(await GetSubDepartmentsAsync(dept.dept_id));
}
}
return departments;
}
}
catch (HttpRequestException ex)
{
@ -513,26 +485,135 @@ namespace VOL.DingTalk.Services.Biz
{ "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");
//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 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;
}
}
}

36
VOL.DingTalk/Util.cs Normal file
View File

@ -0,0 +1,36 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VOL.DingTalk.Models.Biz;
namespace VOL.DingTalk
{
public class Util
{
public class HttpUtil
{
public static async Task<T> SendPostRequest<T>(string url,Dictionary<string,string> queryData,string bodyData)
{
using (var client = new HttpClient())
{
//var jsonPayload = JsonConvert.SerializeObject(payload);
var content = new StringContent(bodyData, Encoding.UTF8, "application/json");
var uriBuilder = new UriBuilder(url);
if(queryData != null && queryData.Count > 0)
uriBuilder.Query = string.Join("&", queryData.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<T>(responseContent);
return data!;
}
}
}
}
}

View File

@ -29,9 +29,9 @@ namespace VOL.Entity.DomainModels
public int ShipId { get; set; }
/// <summary>
///YSERP部门ID
///YSERP部门
/// </summary>
[Display(Name ="YSERP部门ID")]
[Display(Name ="YSERP部门")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
@ -43,13 +43,12 @@ namespace VOL.Entity.DomainModels
[Display(Name ="YS部门名称")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string YSDeptName { get; set; }
/// <summary>
///钉钉部门ID
///钉钉部门
/// </summary>
[Display(Name ="钉钉部门ID")]
[Display(Name ="钉钉部门")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
@ -61,9 +60,16 @@ namespace VOL.Entity.DomainModels
[Display(Name ="钉钉部门名称")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string DingTalkDeptName { get; set; }
/// <summary>
///根节点
/// </summary>
[Display(Name ="根节点")]
[Column(TypeName="bool")]
[Editable(true)]
public bool? IsRoot { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VOL.Entity.DomainModels.DeptShip.SystemDept
{
public class HR_DingTalkDept
{
[SugarColumn(IsPrimaryKey = true)]
[Key]
public int dept_id { get; set; }
public bool auto_add_user { get; set; }
public bool create_dept_group { get; set; }
public string name { get; set; }
public int parent_id { get; set; }
public string ext { get; set; }
}
}

View File

@ -0,0 +1,32 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
namespace VOL.Entity.DomainModels.DeptShip.SystemDept
{
public class HR_YSDept
{
[SugarColumn(IsPrimaryKey = true)]
[Key]
public string id { get; set; }
public int orgtype { get; set; }
public string code { get; set; }
public string sysid { get; set; }
public int displayorder { get; set; }
public string parentid { get; set; }
public int dr { get; set; }
public string parentCode { get; set; }
public string parentorgid { get; set; }
public int enable { get; set; }
public int is_biz_unit { get; set; }
public string tenantid { get; set; }
public string name { get; set; }
public string pubts { get; set; }
}
}

View File

@ -15,7 +15,6 @@ using VOL.Entity.SystemModels;
namespace VOL.Entity.DomainModels
{
[Entity(TableCnName = "员工信息同步",TableName = "HR_EmployeeSync")]
[SugarTable("HR_EmployeeSync")]
public partial class HR_EmployeeSync:BaseEntity
{
/// <summary>
@ -91,6 +90,15 @@ namespace VOL.Entity.DomainModels
[Editable(true)]
public string EmpLastSyncInfo { get; set; }
/// <summary>
///员工钉钉部门ID
/// </summary>
[Display(Name ="员工钉钉部门ID")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string EmpDingTalkDeptID { get; set; }
}
}

View File

@ -6,10 +6,15 @@ using VOL.Entity.DomainModels;
using VOL.Core.Utilities;
using System.Linq.Expressions;
using VOL.YSErp.Models.Biz;
using VOL.DingTalk.Models.Biz;
namespace VOL.HR.IServices
{
public partial interface IHR_DeptShipService
{
Task<List<YSERPDepartment>> GetYSERPDepartments();
Task<List<DingTalkDepartment>> GetDingTalkDepartments();
}
}

View File

@ -5,9 +5,18 @@ using VOL.Core.BaseProvider;
using VOL.Entity.DomainModels;
using VOL.Core.Utilities;
using System.Linq.Expressions;
using VOL.DingTalk.Models.Biz;
namespace VOL.HR.IServices
{
public partial interface IHR_DeptSyncService
{
Task CacheYSDepartments();
Task CacheDingTalkDepartments();
Task SyncYSERPDeptToDingTalk();
Task CreateDingTalkDept(DingTalkDepartment dept);
}
}
}

View File

@ -18,5 +18,11 @@ namespace VOL.HR.IServices
Task GenEmpSystemShip();
Task<bool> UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo);
Task<bool> SyncYSEmpToDingTalk(string ysEmpId);
}
}
}

View File

@ -63,5 +63,23 @@ namespace VOL.HR.Services
return depts;
}
}
}
public Task<List<DingTalkDepartment>> GetDingTalkDepartments()
{
if (_cacheService.Exists("DINGTALK_DEPT_CACHE"))
{
return Task.FromResult(_cacheService.Get<List<DingTalkDepartment>>("DINGTALK_DEPT_CACHE"));
}
else
{
var depts = _dingTalkService.GetSubDepartmentsAsync();
if (depts != null)
{
_cacheService.Add("DINGTALK_DEPT_CACHE", JsonConvert.SerializeObject(depts.Result), 600);
}
return depts;
}
}
}
}

View File

@ -6,17 +6,17 @@
*使UserContext.Current操作
*HR_DeptSyncService对增ServiceFunFilter
*/
using VOL.Core.BaseProvider;
using VOL.Core.Extensions.AutofacManager;
using VOL.Entity.DomainModels;
using System.Linq;
using VOL.Core.Utilities;
using System.Linq.Expressions;
using VOL.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using VOL.Core.DBManager;
using VOL.Core.Extensions;
using VOL.DingTalk.Models.Biz;
using VOL.DingTalk.Services.Biz;
using VOL.Entity.DomainModels.DeptShip.SystemDept;
using VOL.HR.IRepositories;
using VOL.YSErp.Models.Biz;
using VOL.YSErp.Services.Biz;
namespace VOL.HR.Services
{
@ -24,18 +24,222 @@ namespace VOL.HR.Services
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHR_DeptSyncRepository _repository;//访问数据库
private readonly IHR_DeptShipRepository _shipRepository;//访问数据库
private readonly DingTalkService _dingTalkService;
private readonly YSERPService _ysService;
[ActivatorUtilitiesConstructor]
private Core.CacheManager.ICacheService _cacheService;
[ActivatorUtilitiesConstructor]
public HR_DeptSyncService(
IHR_DeptSyncRepository dbRepository,
IHttpContextAccessor httpContextAccessor
IHR_DeptShipRepository shipDbRepository,
IHttpContextAccessor httpContextAccessor,
Core.CacheManager.ICacheService cacheService
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
_shipRepository = shipDbRepository;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
_cacheService = cacheService;
_dingTalkService = new DingTalkService(new DingTalk.Models.SystemToken(), new DingTalk.Models.DingTalkConfig());
_ysService = new YSERPService(new YSErp.Models.SystemToken(), new YSErp.Models.YSConfig());
}
}
public async Task CacheYSDepartments()
{
var ysDepts = await _ysService.GetAllDepartmentsAsync();
if(ysDepts != null)
{
_cacheService.Remove("YS_DEPT_CACHE");
_cacheService.Add("YS_DEPT_CACHE", JsonConvert.SerializeObject(ysDepts), 60 * 60);
var db = DBServerProvider.SqlSugarClient;
// 自动建表
db.CodeFirst.InitTables<HR_YSDept>();
try
{
db.BeginTran();
// 通过缓存直接反序列化转换对象
var deptsData = _cacheService.Get<List<HR_YSDept>>("YS_DEPT_CACHE");
// 清空再插入
db.DbMaintenance.TruncateTable<HR_YSDept>();
db.Fastest<HR_YSDept>().BulkCopy(deptsData);
db.CommitTran();
}
catch (Exception ex)
{
db.RollbackTran();
throw;
}
}
}
public async Task CacheDingTalkDepartments()
{
var depts = await _dingTalkService.GetSubDepartmentsAsync();
if (depts != null)
{
_cacheService.Remove("DINGTALK_DEPT_CACHE");
_cacheService.Add("DINGTALK_DEPT_CACHE", JsonConvert.SerializeObject(depts), 60 * 60);
var db = DBServerProvider.SqlSugarClient;
try
{
// 自动建表
db.CodeFirst.InitTables<HR_DingTalkDept>();
db.BeginTran();
// 通过缓存直接反序列化转换对象
var deptsData = _cacheService.Get<List<HR_DingTalkDept>>("DINGTALK_DEPT_CACHE");
// 清空再插入
db.DbMaintenance.TruncateTable<HR_DingTalkDept>();
db.Fastest<HR_DingTalkDept>().BulkCopy(deptsData);
db.CommitTran();
}
catch (Exception ex)
{
db.RollbackTran();
throw;
}
}
}
public async Task CreateDingTalkDept(DingTalkDepartment dept)
{
}
public async Task SyncYSERPDeptToDingTalk()
{
var ysDepts = await _ysService.GetAllDepartmentsAsync();
//var depts = await _dingTalkService.GetSubDepartmentsAsync();
//var deptShip = _shipRepository.Find(i => i.IsRoot ?? false);
if(ysDepts.Count != 0)
{
var rootDept = _shipRepository.Find(i => i.IsRoot ?? false).FirstOrDefault();
BuildYSDeptTree(ysDepts).Where(i => i.id == rootDept.YSDeptId).ToList().ForEach(CreateOrUpdateDingTalkDept);
}
}
public void CreateOrUpdateDingTalkDept(YSERPDepartment ysDept)
{
var deptShip = _shipRepository.Find(i => i.YSDeptId == ysDept.id).FirstOrDefault();
if (deptShip != null && (!deptShip.IsRoot ?? true))
{
}
else
{
if (string.IsNullOrEmpty(ysDept.parentid))
{
var rootDept = _shipRepository.Find(i => i.IsRoot ?? false).FirstOrDefault();
var pId = int.Parse(rootDept?.DingTalkDeptId ?? "1");
try
{
_dingTalkService.CreateDingTalkDept(new DingTalkDepartment
{
parent_id = pId,
name = ysDept.name
});
}
catch (Exception)
{
throw;
}
}
}
}
/// <summary>
/// 将扁平的 YSERP 部门列表转换为树形结构
/// 规则:
/// - parentid 为 null/empty/"0" 或者父节点在列表中不存在时,视为根节点
/// - 避免将节点添加为自己的子节点
/// - 会初始化每个节点的 subDepts 列表并按 displayorder 排序(然后按 name 作为次级排序)
/// </summary>
/// <param name="flatList">扁平部门列表</param>
/// <returns>树形结构的根节点列表</returns>
private List<YSERPDepartment> BuildYSDeptTree(List<YSERPDepartment> flatList)
{
if (flatList == null || flatList.Count == 0)
return new List<YSERPDepartment>();
// 初始化 subDepts防止 null 引发 NRE
foreach (var d in flatList)
{
if (d.subDepts == null)
d.subDepts = new List<YSERPDepartment>();
else
d.subDepts.Clear();
}
// 建立 id -> node 的字典(只包含有 id 的节点)
var dict = flatList
.Where(d => !string.IsNullOrEmpty(d.id))
.GroupBy(d => d.id) // 防止重复 id 导致异常,取第一个
.ToDictionary(g => g.Key, g => g.First());
var roots = new List<YSERPDepartment>();
foreach (var node in flatList)
{
// 视为根的条件parentid 为空、等于 "0"、或父节点不存在、或者 parentid 与自身 id 相同(防止自引用)
if (string.IsNullOrEmpty(node.parentid) || node.parentid == "0" ||
!dict.ContainsKey(node.parentid) || node.parentid == node.id)
{
roots.Add(node);
}
else
{
// 将当前节点添加到父节点的子集合
if (dict.TryGetValue(node.parentid, out var parent))
{
if (parent.subDepts == null)
parent.subDepts = new List<YSERPDepartment>();
parent.subDepts.Add(node);
}
else
{
// 找不到父节点视为根
roots.Add(node);
}
}
}
// 递归排序子节点(按 displayorder再按 name
void SortRecursively(List<YSERPDepartment> list)
{
if (list == null || list.Count == 0) return;
list.Sort((a, b) =>
{
var c = a.displayorder.CompareTo(b.displayorder);
if (c == 0) return string.Compare(a.name, b.name, StringComparison.Ordinal);
return c;
});
foreach (var item in list)
{
SortRecursively(item.subDepts);
}
}
SortRecursively(roots);
return roots;
}
}
}

View File

@ -29,6 +29,7 @@ using VOL.Entity.DomainModels;
using VOL.HR.IRepositories;
using VOL.YSErp.Models.Biz;
using VOL.YSErp.Services.Biz;
using static VOL.YSErp.Models.Biz.YSERPEmployeeInfo;
namespace VOL.HR.Services
{
@ -36,6 +37,7 @@ namespace VOL.HR.Services
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHR_EmployeeSyncRepository _repository;//访问数据库
private readonly IHR_DeptShipRepository _deptShipRepo;
private readonly DingTalkService _dingTalkService;
private readonly YSERPService _ysService;
@ -44,6 +46,7 @@ namespace VOL.HR.Services
[ActivatorUtilitiesConstructor]
public HR_EmployeeSyncService(
IHR_EmployeeSyncRepository dbRepository,
IHR_DeptShipRepository deptShipRepo,
IHttpContextAccessor httpContextAccessor,
ICacheService cacheService
)
@ -51,6 +54,7 @@ namespace VOL.HR.Services
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
_deptShipRepo = deptShipRepo;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
@ -59,34 +63,34 @@ namespace VOL.HR.Services
_dingTalkService = new DingTalkService(new DingTalk.Models.SystemToken(),new DingTalk.Models.DingTalkConfig());
_ysService = new YSERPService(new YSErp.Models.SystemToken(), new YSErp.Models.YSConfig());
}
public Task<List<YSERPEmployee>> GetAllYSEmployees()
public async Task<List<YSERPEmployee>> GetAllYSEmployees()
{
if (_cacheService.Exists("YS_EMP_CACHE"))
{
return Task.FromResult(_cacheService.Get<List<YSERPEmployee>>("YS_EMP_CACHE"));
return _cacheService.Get<List<YSERPEmployee>>("YS_EMP_CACHE");
}
else
{
var emps = _ysService.GetAllEmployeesAsync();
if(emps != null)
var emps = await _ysService.GetAllEmployeesAsync();
if(emps != null && emps.Count > 0)
{
_cacheService.Add("YS_EMP_CACHE", JsonConvert.SerializeObject(emps.Result), 600);
_cacheService.Add("YS_EMP_CACHE", JsonConvert.SerializeObject(emps), 600);
}
return emps;
}
}
public Task<List<DingTalkEmployee>> GetAllDingTalkEmployees()
public async Task<List<DingTalkEmployee>> GetAllDingTalkEmployees()
{
if (_cacheService.Exists("DingTalk_EMP_CACHE"))
{
return Task.FromResult(_cacheService.Get<List<DingTalkEmployee>>("DingTalk_EMP_CACHE"));
return _cacheService.Get<List<DingTalkEmployee>>("DingTalk_EMP_CACHE");
}
else
{
var emps = _dingTalkService.GetAllEmployeesAsync();
if(emps != null)
var emps = await _dingTalkService.GetAllEmployeesAsync();
if(emps != null && emps.Count > 0)
{
_cacheService.Add("DingTalk_EMP_CACHE", JsonConvert.SerializeObject(emps.Result), 600);
_cacheService.Add("DingTalk_EMP_CACHE", JsonConvert.SerializeObject(emps), 600);
}
return emps;
}
@ -97,7 +101,7 @@ namespace VOL.HR.Services
List<DingTalkEmployee> dingTalkEmployees = [];
List<YSERPEmployee> ysERPEmployees = [];
var ysTask = GetAllYSEmployees().ContinueWith(x =>
var ysTask = GetAllYSEmployees().ContinueWith(x =>
{
ysERPEmployees = x.Result;
});
@ -114,7 +118,25 @@ namespace VOL.HR.Services
var dingTalkInfo = dingTalkEmployees?.FirstOrDefault(e => e.Mobile == emp.mobile);
if (dingTalkInfo != null)
{
//_repository.AddRange()
if(!newList.Any(i => i.EmpYSID == emp.id))
{
//_repository.AddRange()
newList.Add(new HR_EmployeeSync
{
EmpYSID = emp.id,
EmpYSMobile = emp.mobile,
EmpYSDeptID = emp.dept_id,
EmpJobNumber = emp.code,
EmpDingDingID = dingTalkInfo.UserId,
EmpDingDingMobile = dingTalkInfo.Mobile,
EmpDingTalkDeptID = dingTalkInfo.Dept,
EmpLastSyncInfo = "Init generate for system.",
});
}
}
else
{
newList.Add(new HR_EmployeeSync
{
EmpYSID = emp.id,
@ -122,8 +144,9 @@ namespace VOL.HR.Services
EmpYSDeptID = emp.dept_id,
EmpJobNumber = emp.code,
EmpDingDingID = dingTalkInfo.UserId,
EmpDingDingMobile = dingTalkInfo.Mobile,
EmpDingDingID = "",
EmpDingDingMobile = "",
EmpDingTalkDeptID = "",
EmpLastSyncInfo = "Init generate for system.",
});
}
@ -134,7 +157,8 @@ namespace VOL.HR.Services
{
db.BeginTran();
// 清空重新生成
db.Deleteable<HR_EmployeeSync>();
db.DbMaintenance.TruncateTable<HR_EmployeeSync>();
//db.Deleteable<HR_EmployeeSync>();
db.Fastest<HR_EmployeeSync>().BulkCopy(newList);
db.CommitTran();
}
@ -145,6 +169,50 @@ namespace VOL.HR.Services
}
return Task.CompletedTask;
}
public async Task<bool> UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo)
{
return await _dingTalkService.UpdateEmpInfo(updateInfo);
//return Task.FromResult(true);
}
public async Task<bool> SyncYSEmpToDingTalk(string ysEmpId)
{
var ysEmpInfo = await _ysService.GetEmpInfoById(ysEmpId);
if(ysEmpInfo == null)
{
Core.Services.Logger.Error($"未在YS中找到用户信息{ysEmpId}");
return false;
}
// 获取对应的部门关联关系
var dingDeptId = _deptShipRepo.Find(i => i.YSDeptId == ysEmpInfo.deptId).FirstOrDefault().DingTalkDeptId;
// 获取直系主管在钉钉的ID 只取主职
var director = ysEmpInfo.staffJob.FirstOrDefault(it => !it.endFlag).director;
var reportManagerId = "";
if (!string.IsNullOrEmpty(director))
{
reportManagerId = _repository.Find(i => i.EmpYSID == director)?.FirstOrDefault()?.EmpDingDingID;
}
// 获取要同步的用户对应的钉钉的ID
var empDingTalkId = _repository.Find(i => i.EmpYSID == ysEmpId).FirstOrDefault().EmpDingDingID;
//TODO: 增加配置表,通过反射自动获取匹配信息
var dingTalkInfo = new DingTalkEmployeeUpdate
{
userid = empDingTalkId,
dept_id_list = dingDeptId,
manager_userid = string.IsNullOrEmpty(reportManagerId) ? null : reportManagerId,
email = ysEmpInfo.email,
job_number = ysEmpInfo.code,
exclusive_mobile = ysEmpInfo.mobile
};
var updated = await _dingTalkService.UpdateEmpInfo(dingTalkInfo);
return updated;
}
}
}

View File

@ -31,7 +31,7 @@ namespace VOL.HR.Controllers
}
/// <summary>
/// table加载数据后刷新当前table数据的字典项(适用字典数据量比较大的情况)
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>

View File

@ -29,5 +29,43 @@ namespace VOL.HR.Controllers
_service = service;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("cacheYSERPDept")]
public async Task<IActionResult> GetYSERPDepartments()
{
await Service.CacheYSDepartments();
return Json(new { });
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("cacheDingTalkDept")]
public async Task<IActionResult> CacheDingTalkDept()
{
await Service.CacheDingTalkDepartments();
return Json(new { });
}
// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("syncYSERPDeptToDingTalk")]
public async Task<IActionResult> SyncYSERPDeptToDingTalk()
{
await Service.SyncYSERPDeptToDingTalk();
return Json(new { });
}
}
}

View File

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using VOL.Core.Filters;
using VOL.DingTalk.Models.Biz;
using VOL.Entity.DomainModels;
using VOL.HR.IServices;
@ -17,9 +18,8 @@ namespace VOL.HR.Controllers
{
public partial class HR_EmployeeSyncController
{
private readonly IHR_EmployeeSyncService _service;//访问业务代码
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHR_EmployeeSyncService _service;//访问业务代码
[ActivatorUtilitiesConstructor]
public HR_EmployeeSyncController(
IHR_EmployeeSyncService service,
@ -33,15 +33,15 @@ namespace VOL.HR.Controllers
/// <summary>
///
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("/api/HR_YSEmployees/getPageData")]
public async Task<IActionResult> GetYSAllEmployees()
[HttpPost, Route("GenEmpSystemShip")]
public async Task<IActionResult> GenEmpSystemShip()
{
var emps = await Service.GetAllYSEmployees();
return Json(emps);
await Service.GenEmpSystemShip();
return Json(new { });
}
/// <summary>
@ -56,16 +56,40 @@ namespace VOL.HR.Controllers
return Json(emps);
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("/api/HR_YSEmployees/getPageData")]
public async Task<IActionResult> GetYSAllEmployees()
{
var emps = await Service.GetAllYSEmployees();
return Json(emps);
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("GenEmpSystemShip")]
public async Task<IActionResult> GenEmpSystemShip()
[HttpPost, Route("UpdateEmpInfo")]
public async Task<IActionResult> UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo)
{
await Service.GenEmpSystemShip();
await Service.UpdateEmpInfo(updateInfo);
return Json(new { });
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost, Route("SyncYSEmpToDingTalk")]
public async Task<IActionResult> SyncYSEmpToDingTalk([FromBody] string ysEmpId)
{
var result = await Service.SyncYSEmpToDingTalk(ysEmpId);
return Json(new { success=result });
}
}
}

View File

@ -0,0 +1,11 @@
Got timeout reading communication packets at MySqlConnector.Core.ResultSet.ReadResultSetHeaderAsync(IOBehavior ioBehavior) in /_/src/MySqlConnector/Core/ResultSet.cs:line 43
at MySqlConnector.MySqlDataReader.ActivateResultSet(CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlDataReader.cs:line 130
at MySqlConnector.MySqlDataReader.CreateAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary`2 cachedProcedures, IMySqlCommand command, CommandBehavior behavior, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlDataReader.cs:line 468
at MySqlConnector.Core.CommandExecutor.ExecuteReaderAsync(IReadOnlyList`1 commands, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/CommandExecutor.cs:line 56
at MySqlConnector.MySqlCommand.ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlCommand.cs:line 296
at MySqlConnector.MySqlBulkLoader.LoadAsync(IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlBulkLoader.cs:line 213
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
at VOL.Core.Services.Logger.Start() in D:\Code\Laservall-Data-Center\vol.api.sqlsugar\VOL.Core\Services\Logger.cs:line 183System.Private.CoreLib

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace VOL.YSErp.Models.Biz
@ -41,5 +42,7 @@ namespace VOL.YSErp.Models.Biz
public string id { get; set; }
public string pubts { get; set; }
[JsonIgnore]
public List<YSERPDepartment> subDepts { get; set; } = [];
}
}

View File

@ -0,0 +1,417 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VOL.YSErp.Models.Biz
{
public class YSERPEmployeeInfo
{
public string email { get; set; }
public string certTypeName { get; set; }
public string staffDefines__Y40_name { get; set; }
public string majorcorpname { get; set; }
public string staffDefines__J1 { get; set; }
public string nationalityName { get; set; }
public string staffDefines__id { get; set; }
public string staffDefines__A86 { get; set; }
public string staffDefines__A1 { get; set; }
public string staffDefines__A85 { get; set; }
public bool staffDefines__A4 { get; set; }
public string staffDefines__A84 { get; set; }
public string staffDefines__Y38_name { get; set; }
public int enable { get; set; }
public int shopAssisTag { get; set; }
public string staffDefines__Y1_name { get; set; }
public string staffDefines__Y10_name { get; set; }
public string delayedRetDate { get; set; }
public string staffDefines__A06 { get; set; }
public bool staffDefines__A05 { get; set; }
public string characterrprName { get; set; }
public string id { get; set; }
public string staffDefines__Y36_name { get; set; }
public string staffDefines__T70 { get; set; }
public string staffDefines__Y39_name { get; set; }
public Staffpart[] staffPart { get; set; }
public string deptId { get; set; }
public string staffDefines__Y41_name { get; set; }
public string linkAddr { get; set; }
public string permanreside { get; set; }
public string nationality { get; set; }
public Staffprobation[] staffProbation { get; set; }
public string staffDefines__Y11_name { get; set; }
public Staffresume[] staffResume { get; set; }
public string staffDefines__Y2_name { get; set; }
public string shortName { get; set; }
public string staffDefines__J1_name { get; set; }
public string staffDefines__Y51_name { get; set; }
public string staffDefines__Y14 { get; set; }
public string staffDefines__Y13 { get; set; }
public string staffDefines__Y12 { get; set; }
public string staffDefines__Y11 { get; set; }
public string countryCode { get; set; }
public string staffDefines__Y35_name { get; set; }
public int workAgeAdjMon { get; set; }
public string staffDefines__Y10 { get; set; }
public string unitId { get; set; }
public string pubts { get; set; }
public string staffDefines__Y13_name { get; set; }
public string staffDefines__Y6_name { get; set; }
public string staffDefines__Y9_name { get; set; }
public int sex { get; set; }
public string maritalName { get; set; }
public string staffDefines__Y31 { get; set; }
public string photo { get; set; }
public string staffDefines__Y30 { get; set; }
public string staffDefines__T70_name { get; set; }
public string userId { get; set; }
public string staffDefines__Y3_name { get; set; }
public string staffDefines__Y45_name { get; set; }
public Staffjob[] staffJob { get; set; }
public string staffDefines__Y42_name { get; set; }
public string selfEmail { get; set; }
public string politicalCode { get; set; }
public Staffedu[] staffEdu { get; set; }
public string originName { get; set; }
public string country { get; set; }
public string certType { get; set; }
public string staffDefines__Y42 { get; set; }
public string political { get; set; }
public string staffDefines__Y41 { get; set; }
public string staffDefines__Y40 { get; set; }
public string joinWorkDate { get; set; }
public string staffDefines__Y8_name { get; set; }
public string staffDefines__Y39 { get; set; }
public string staffDefines__Y38 { get; set; }
public string idCardReverse { get; set; }
public string nationalityCode { get; set; }
public string staffDefines__Y36 { get; set; }
public string staffDefines__Y35 { get; set; }
public string staffDefines__Y34 { get; set; }
public string attachlist { get; set; }
public Dictionary<string,object> staffDefines { get; set; }
public string staffDefines__Y32 { get; set; }
public int workAgeAdjYea { get; set; }
public string staffDefines__Y32_name { get; set; }
public string originCode { get; set; }
public string psnclId { get; set; }
public string staffDefines__Y7_name { get; set; }
public string staffDefines__Y30_name { get; set; }
public int workAgeAdjDay { get; set; }
public string majorcorpCode { get; set; }
public string staffDefines__Y53 { get; set; }
public string staffDefines__Y52 { get; set; }
public string staffDefines__Y51 { get; set; }
public string staffDefines__Y44_name { get; set; }
public string staffDefines__Y50 { get; set; }
public string certTypeCode { get; set; }
public bool staffDefines__FK003 { get; set; }
public Staffsocialrel[] staffSocialRel { get; set; }
public string certNo { get; set; }
public string permanresideName { get; set; }
public string staffDefines__Y45 { get; set; }
public string staffDefines__A1_name { get; set; }
public string staffDefines__Y44 { get; set; }
public string staffDefines__Y5_name { get; set; }
public string staffDefines__Y14_name { get; set; }
public string staffDefines__Y43 { get; set; }
public string name { get; set; }
public string staffDefines__F1 { get; set; }
public string countryName { get; set; }
public Staffctrt[] staffCtrt { get; set; }
public Stafforgrel[] staffOrgRel { get; set; }
public string code { get; set; }
public string staffDefines__Y43_name { get; set; }
public string staffDefines__YY01 { get; set; }
public string origin { get; set; }
public float workAge { get; set; }
public string majorcorpid { get; set; }
public float staffDefines__T42 { get; set; }
public float staffDefines__T41 { get; set; }
public string staffDefines__Y12_name { get; set; }
public string joinedDate { get; set; }
public string characterrpr { get; set; }
public string permanresideCode { get; set; }
public string staffDefines__Y4_name { get; set; }
public string politicalName { get; set; }
public string staffDefines__Y9 { get; set; }
public Staffbankacct[] staffBankAcct { get; set; }
public int bizManTag { get; set; }
public string mobile { get; set; }
public string staffDefines__Y2 { get; set; }
public string staffDefines__Y1 { get; set; }
public string staffDefines__Y4 { get; set; }
public string staffDefines__Y3 { get; set; }
public string staffDefines__Y6 { get; set; }
public string birthDate { get; set; }
public string staffDefines__Y5 { get; set; }
public string idCardFront { get; set; }
public string staffDefines__A78_name { get; set; }
public string staffDefines__Y8 { get; set; }
public string staffDefines__Y7 { get; set; }
public string marital { get; set; }
public string maritalCode { get; set; }
public string staffDefines__Y31_name { get; set; }
public string staffDefines__Y34_name { get; set; }
public Staffprotocol[] staffProtocol { get; set; }
public string staffDefines__Y50_name { get; set; }
public string staffDefines__A78 { get; set; }
public int age { get; set; }
public string characterrprCode { get; set; }
public string masterOrgKeyField { get; set; }
public string _mddFormulaExecuteFlag { get; set; }
public bool editPermission { get; set; }
public bool mobile_email_validate { get; set; }
public string subOrgId { get; set; }
public class Staffpart
{
public string deptName { get; set; }
public string partTypeCode { get; set; }
public bool isMainJob { get; set; }
public bool endFlag { get; set; }
public string orgVidCode { get; set; }
public int recordNum { get; set; }
public string orgId { get; set; }
public string orgRelId { get; set; }
public string trnsTypeCode { get; set; }
public int assgId { get; set; }
public string id { get; set; }
public string staffFte { get; set; }
public string trnsType { get; set; }
public bool isweaken { get; set; }
public string partType { get; set; }
public bool lastFlag { get; set; }
public int trnsEvent { get; set; }
public string trnsTypeName { get; set; }
public string orgName { get; set; }
public string deptVid { get; set; }
public string partTypeName { get; set; }
public string deptId { get; set; }
public string staffjobCode { get; set; }
public string belongJob { get; set; }
public string orgVid { get; set; }
public string beginDate { get; set; }
public int effectiveNumber { get; set; }
public string deptVidCode { get; set; }
public int showOrder { get; set; }
public string staffjobId { get; set; }
public string staffId { get; set; }
public string orgPath { get; set; }
public string deptPath { get; set; }
}
public class Staffprobation
{
public string beginDate { get; set; }
public string belongJobCode { get; set; }
public string endDate { get; set; }
public bool endFlag { get; set; }
public string id { get; set; }
public int promonth { get; set; }
public string belongJob { get; set; }
public string staffId { get; set; }
public int termUnit { get; set; }
public bool lastFlag { get; set; }
}
public class Staffresume
{
public string workCorp { get; set; }
public string beginDate { get; set; }
public string newPost { get; set; }
public string workDept { get; set; }
public string staffJobId { get; set; }
public string id { get; set; }
public int resumeType { get; set; }
public string pubts { get; set; }
public string staffId { get; set; }
public string workPost { get; set; }
public string endDate { get; set; }
public string workDuty { get; set; }
}
public class Staffjob
{
public string deptName { get; set; }
public string newPostId { get; set; }
public bool isMainJob { get; set; }
public string directorCode { get; set; }
public string addrIdCode { get; set; }
public bool endFlag { get; set; }
public string orgVidCode { get; set; }
public string staffJobDefines__id { get; set; }
public int recordNum { get; set; }
public string addrName { get; set; }
public string orgId { get; set; }
public string orgRelId { get; set; }
public string trnsTypeCode { get; set; }
public string newPostName { get; set; }
public string newPostVidCode { get; set; }
public string staffJobDefines__A01 { get; set; }
public string psnclName { get; set; }
public string directorName { get; set; }
public int enable { get; set; }
public string addrId { get; set; }
public string id { get; set; }
public string psnclId { get; set; }
public string pubts { get; set; }
public string trnsType { get; set; }
public Staffjobdefines staffJobDefines { get; set; }
public string empformName { get; set; }
public bool isweaken { get; set; }
public bool lastFlag { get; set; }
public string psnclIdCode { get; set; }
public int trnsEvent { get; set; }
public string trnsTypeName { get; set; }
public string empform { get; set; }
public string orgName { get; set; }
public string deptVid { get; set; }
public string director { get; set; }
public string deptId { get; set; }
public string staffjobCode { get; set; }
public string orgVid { get; set; }
public string newPostVid { get; set; }
public string beginDate { get; set; }
public int effectiveNumber { get; set; }
public string deptVidCode { get; set; }
public int showOrder { get; set; }
public string empformCode { get; set; }
public string staffjobId { get; set; }
public string staffId { get; set; }
public string orgPath { get; set; }
public string deptPath { get; set; }
}
public class Staffjobdefines
{
public string A01 { get; set; }
public string ytenant { get; set; }
public string id { get; set; }
public int dr { get; set; }
}
public class Staffedu
{
public string beginDate { get; set; }
public string major { get; set; }
public string endDate { get; set; }
public string school { get; set; }
public string id { get; set; }
public string degreeAnnex { get; set; }
public string staffId { get; set; }
public bool isPrefs { get; set; }
public string educationAnnex { get; set; }
}
public class Staffsocialrel
{
public string workCorp { get; set; }
public bool isUrgent { get; set; }
public string relName { get; set; }
public string relationName { get; set; }
public string relationCode { get; set; }
public string id { get; set; }
public string workDuty { get; set; }
public string linkTel { get; set; }
public string staffId { get; set; }
public string relation { get; set; }
}
public class Staffctrt
{
public int continueTime { get; set; }
public string majorCorpCode { get; set; }
public int termMonth { get; set; }
public string belongJobCode { get; set; }
public string endDate { get; set; }
public string majorCorpId { get; set; }
public int termType { get; set; }
public string workAddr { get; set; }
public bool isReceive { get; set; }
public int contType { get; set; }
public string signDate { get; set; }
public string belongJob { get; set; }
public string beginDate { get; set; }
public string majorCorpName { get; set; }
public string contractNum { get; set; }
public string workAddrName { get; set; }
public string id { get; set; }
public int performStatus { get; set; }
public string workAddrCode { get; set; }
public string staffId { get; set; }
public string termUnit { get; set; }
public bool lastFlag { get; set; }
}
public class Stafforgrel
{
public bool isReturn { get; set; }
public string staffCode { get; set; }
public string code { get; set; }
public string orgName { get; set; }
public string employerCode { get; set; }
public string entrySrc { get; set; }
public string employerName { get; set; }
public bool endFlag { get; set; }
public string orgIdCode { get; set; }
public int recordNum { get; set; }
public string orgId { get; set; }
public string beginDate { get; set; }
public float secretaryAge { get; set; }
public string entrySrcName { get; set; }
public string entrySrcCode { get; set; }
public string groupDate { get; set; }
public string employer { get; set; }
public string id { get; set; }
public float groupWorkAge { get; set; }
public string pubts { get; set; }
public string staffId { get; set; }
public string seniorityDate { get; set; }
public bool lastFlag { get; set; }
}
public class Staffbankacct
{
public string bankCode { get; set; }
public string openBank { get; set; }
public string accountName { get; set; }
public int isDefaultCard { get; set; }
public string bankAnnex { get; set; }
public string openBankCode { get; set; }
public string bankName { get; set; }
public string openBankName { get; set; }
public string bank { get; set; }
public string currencyName { get; set; }
public string acctType { get; set; }
public string currency { get; set; }
public string id { get; set; }
public string currencyCode { get; set; }
public string staffId { get; set; }
public string account { get; set; }
}
public class Staffprotocol
{
public string majorCorpCode { get; set; }
public string belongJobCode { get; set; }
public string protocolNum { get; set; }
public string majorCorpId { get; set; }
public bool isReceive { get; set; }
public int contType { get; set; }
public string protocolType { get; set; }
public string signDate { get; set; }
public string belongJob { get; set; }
public string beginDate { get; set; }
public string majorCorpName { get; set; }
public string id { get; set; }
public string protocolTypeName { get; set; }
public int performStatus { get; set; }
public string termUnit { get; set; }
public string staffId { get; set; }
public string protocolTypeCode { get; set; }
}
}
}

View File

@ -6,7 +6,21 @@ using System.Threading.Tasks;
namespace VOL.YSErp.Models.Biz
{
public class YSERPResponse<T>
{
public string code { get; set; }
public string message { get; set; }
public T data { get; set; }
public string? displayCode { get; set; }
public int? level { get; set; }
}
public class YSERPPagedResponse<T>
{
public string code { get; set; }
@ -27,6 +41,8 @@ namespace VOL.YSErp.Models.Biz
public bool haveNextPage { get; set; }
public List<T> recordList { get; set; }
}
public class YSERPListResponse<T>
{

View File

@ -27,7 +27,7 @@ namespace VOL.YSErp.Services.Biz
private readonly string _employeeListUrl;
private readonly string _updateEmployeeUrl;
private readonly string _deptListUrl;
private readonly string _empInfoUrl;
public YSERPService(SystemToken token, YSConfig config)
{
_token = token;
@ -40,6 +40,7 @@ namespace VOL.YSErp.Services.Biz
_employeeListUrl = $"{_apiUrl}/iuap-api-gateway/yonbip/hrcloud/staff/listdetailmdd";
_updateEmployeeUrl = $"{_apiUrl}/iuap-api-gateway/yonbip/hrcloud/batchInsertOrUpdateDetailNew";
_deptListUrl = $"{_apiUrl}/iuap-api-gateway/yonbip/digitalModel/basedoc/dept/list";
_empInfoUrl = $"{_apiUrl}/iuap-api-gateway/yonbip/hrcloud/HRCloud/getStaffDetail";
}
@ -199,7 +200,7 @@ namespace VOL.YSErp.Services.Biz
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<YSERPResponse<YSERPEmployee>>(content);
var data = JsonSerializer.Deserialize<YSERPPagedResponse<YSERPEmployee>>(content);
if (!new[] { "200", "00000" }.Contains(data.code))
{
var errorMsg = data.message;
@ -213,9 +214,10 @@ namespace VOL.YSErp.Services.Biz
{
Logger.Info("没有更多员工记录");
break;
}
}
allEmployees.AddRange(data.data.recordList);
var toAdd = recordList.Where(r => !allEmployees.Any(e => e.id == r.id)).ToList();
allEmployees.AddRange(toAdd);
var haveNext = data.data.haveNextPage;
var pageCount = data.data.pageCount;
@ -224,7 +226,7 @@ namespace VOL.YSErp.Services.Biz
{
break;
}
Logger.Info($"第 {pageIndex}/{pageCount} 页,获取 {recordList.Count} 条员工记录");
//Logger.Info($"第 {pageIndex}/{pageCount} 页,获取 {recordList.Count} 条员工记录");
pageIndex++;
await Task.Delay(500); // 避免请求过快
}
@ -274,9 +276,6 @@ namespace VOL.YSErp.Services.Biz
if (data.TryGetProperty("data", out var resultData))
{
//_logger.LogInformation($"处理总数: {GetPropertyValueOrDefault(resultData, "count", 0)}");
//_logger.LogInformation($"成功数: {GetPropertyValueOrDefault(resultData, "successCount", 0)}");
//_logger.LogInformation($"失败数: {GetPropertyValueOrDefault(resultData, "failCount", 0)}");
if (resultData.TryGetProperty("messages", out var messages))
{
@ -303,8 +302,6 @@ namespace VOL.YSErp.Services.Biz
}
}
public async Task<List<YSERPDepartment>> GetAllDepartmentsAsync()
{
var token = await GetValidTokenAsync();
@ -376,25 +373,57 @@ namespace VOL.YSErp.Services.Biz
//_logger.LogInformation($"共获取 {allEmployees.Count} 条YS员工记录");
return allDepts;
}
private string ConvertGender(string gender)
public async Task<YSERPEmployeeInfo> GetEmpInfoById(string empId)
{
return gender switch
var token = await GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
{
"男" => "1",
"女" => "2",
_ => gender
};
}
return new YSERPEmployeeInfo();
}
try
{
var payload = new
{
id = empId
};
private static string GetPropertyValueOrEmpty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) ? property.GetString() ?? "" : "";
}
var request = new HttpRequestMessage(HttpMethod.Post, $"{_empInfoUrl}?access_token={token}")
{
Content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json")
};
private static int GetPropertyValueOrDefault(JsonElement element, string propertyName, int defaultValue)
{
return element.TryGetProperty(propertyName, out var property) ? property.GetInt32() : defaultValue;
var response = await _httpClient.SendAsync(request);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Logger.Error("认证失败: 无效的API令牌或权限不足");
_accessToken = null;
return new YSERPEmployeeInfo();
}
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<YSERPResponse<YSERPEmployeeInfo>>(content);
if (!new[] { "200", "00000" }.Contains(data.code))
{
var errorMsg = data.message;
Logger.Error($"API业务错误: {errorMsg}");
throw new Exception(errorMsg);
}
return data.data;
}
catch (Exception ex)
{
Logger.Error($"获取部门数据失败,{ex}");
throw;
}
}
public void Dispose()