diff --git a/VOL.DingTalk/Models/Biz/DingTalkEmployeeRsp.cs b/VOL.DingTalk/Models/Biz/DingTalkEmployeeRsp.cs index b55a825..8da6d08 100644 --- a/VOL.DingTalk/Models/Biz/DingTalkEmployeeRsp.cs +++ b/VOL.DingTalk/Models/Biz/DingTalkEmployeeRsp.cs @@ -34,7 +34,7 @@ namespace VOL.DingTalk.Models.Biz public class EmployeeQuery { - public List data_list { get; set; } + public List userid_list { get; set; } public long? next_cursor { get; set; } } diff --git a/VOL.DingTalk/Models/Biz/DingTalkEmployeeUpdate.cs b/VOL.DingTalk/Models/Biz/DingTalkEmployeeUpdate.cs new file mode 100644 index 0000000..4f0bbd2 --- /dev/null +++ b/VOL.DingTalk/Models/Biz/DingTalkEmployeeUpdate.cs @@ -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 +{ + /// + /// 20250929 属性文档参考 https://open.dingtalk.com/document/orgapp/update-dedicated-accounts-information + /// + public class DingTalkEmployeeUpdate + { + /// + /// 用户ID + /// + public string userid { get; set; } + /// + /// 扩展属性,长度最大2000个字符。json对象 格式 "{\"爱好\":\"旅游\",\"年龄\":\"24\"}" + /// + public string extension { get; set; } + /// + /// 钉钉企业账号的登录名。 + /// + public string? loginId { get; set; } + /// + /// 直属主管的userId。 + /// + public string? manager_userid { get; set; } + /// + /// 通讯录语言,取值。 + /// + public string? language { get; set; } + /// + /// 分机号,长度最大50个字符。 + /// + public string? telephone { get; set; } + /// + /// 是否号码隐藏: + /// + public string? hide_mobile { get; set; } + /// + /// 入职时间,UNIX时间戳,单位毫秒。 + /// + public string? hired_date { get; set; } + /// + /// 职位,长度最大200个字符。 + /// + public string? title { get; set; } + /// + /// 企业账号员工的企业邮箱类型。 + /// + public string? org_email_type { get; set; } + /// + /// 企业账号手机号。 + /// + public string? avatarMediaId { get; set; } + /// + /// 员工在对应的部门中的职位。 + /// + public string dept_title_list { get; set; } + /// + /// 办公地点,长度最大100个字符。 + /// + public string? work_place { get; set; } + /// + /// 员工在对应的部门中的排序。 + /// + public string? dept_order_list { get; set; } + /// + /// 是否开启高管模式,默认值false。 + /// + public string? senior_mode { get; set; } + /// + /// 备注,长度最大2000个字符。 + /// + public string? remark { get; set; } + /// + /// 企业邮箱 + /// + public string? org_email { get; set; } + /// + /// 用户名称,长度最大80个字符。 + /// + public string? name { get; set; } + /// + /// 企业账号的昵称。 + /// + public string? nickname { get; set; } + /// + /// 企业账号手机号。 + /// + public string? exclusive_mobile { get; set; } + /// + /// 强制更新的字段,支持清空指定的字段,多个字段之间使用逗号分隔。目前支持字段: manager_userid、org_email。 + /// + public string? force_update_fields { get; set; } + /// + /// 所属部门ID列表。 + /// + public string? dept_id_list { get; set; } + /// + /// 工号 + /// + public string? job_number { get; set; } + /// + /// 邮箱 + /// + public string? email { get; set; } + + + } +} diff --git a/VOL.DingTalk/Models/Biz/DingTalkResponse.cs b/VOL.DingTalk/Models/Biz/DingTalkResponse.cs index e1d11ef..2cdb3d3 100644 --- a/VOL.DingTalk/Models/Biz/DingTalkResponse.cs +++ b/VOL.DingTalk/Models/Biz/DingTalkResponse.cs @@ -8,10 +8,10 @@ namespace VOL.DingTalk.Models.Biz { internal class DingTalkResponse { - 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; } } diff --git a/VOL.DingTalk/Models/DingTalkConfig.cs b/VOL.DingTalk/Models/DingTalkConfig.cs index cdd5f5f..f54b0d0 100644 --- a/VOL.DingTalk/Models/DingTalkConfig.cs +++ b/VOL.DingTalk/Models/DingTalkConfig.cs @@ -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 fields = [ - "sys00-name", // 姓名 "sys00-email", // 邮箱 "sys00-mobile", // 手机号 diff --git a/VOL.DingTalk/Services/Biz/DingTalkService.cs b/VOL.DingTalk/Services/Biz/DingTalkService.cs index 958630a..0c4bc5a 100644 --- a/VOL.DingTalk/Services/Biz/DingTalkService.cs +++ b/VOL.DingTalk/Services/Biz/DingTalkService.cs @@ -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 _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(); } - Logger.Info("正在获取部门结构..."); + //Logger.Info("正在获取部门结构..."); var departments = await GetSubDepartmentsAsync(); - Logger.Info($"共获取到 {departments.Count} 个部门"); + //Logger.Info($"共获取到 {departments.Count} 个部门"); HashSet allUserIds = new HashSet(); - 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 userIdList = allUserIds.ToList(); - Logger.Info($"共获取到 {userIdList.Count} 个用户"); + //Logger.Info($"共获取到 {userIdList.Count} 个用户"); // 分批处理用户(每批100人) - var allEmployees = new List(); + var allEmployees = new List(); 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 { { "access_token", accessToken } }; try - { - using (var client = new HttpClient()) + { + var data = await HttpUtil.SendPostRequest>(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>(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(); + 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>(responseContent); + var data = await HttpUtil.SendPostRequest>>(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>(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>(responseContent); + // return data["access_token"]; + // } + // catch (HttpRequestException ex) + // { + // Logger.Error($"获取access_token失败: {ex.Message}"); + // return null; + // } + //} + try + { + var data = await HttpUtil.SendPostRequest>(_tokenUrl, new Dictionary(), 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(); + throw new Exception("Token 失效"); + } + + var departments = new List(); + var payload = new Dictionary { + {"name", dept.name } , + {"parent_id", dept.parent_id } , + }; + var queryParams = new Dictionary { { "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>>(responseContent); + + if(data.errcode != 0) + { + Logger.Error($"{data.errcode},{data.errmsg}"); + throw new Exception($"{data.errcode},{data.errmsg}"); + } + + } + }catch(Exception) + { + throw; + } + } + + + public async Task 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 { { "access_token", accessToken } }; + + try + { + + var data = await HttpUtil.SendPostRequest>(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>(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; } } } \ No newline at end of file diff --git a/VOL.DingTalk/Util.cs b/VOL.DingTalk/Util.cs new file mode 100644 index 0000000..2a175f7 --- /dev/null +++ b/VOL.DingTalk/Util.cs @@ -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 SendPostRequest(string url,Dictionary 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(responseContent); + return data!; + } + + } + } + } +} diff --git a/VOL.Entity/DomainModels/DeptShip/HR_DeptShip.cs b/VOL.Entity/DomainModels/DeptShip/HR_DeptShip.cs index b409485..ea4b048 100644 --- a/VOL.Entity/DomainModels/DeptShip/HR_DeptShip.cs +++ b/VOL.Entity/DomainModels/DeptShip/HR_DeptShip.cs @@ -29,9 +29,9 @@ namespace VOL.Entity.DomainModels public int ShipId { get; set; } /// - ///YSERP部门ID + ///YSERP部门 /// - [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; } /// - ///钉钉部门ID + ///钉钉部门 /// - [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; } + /// + ///根节点 + /// + [Display(Name ="根节点")] + [Column(TypeName="bool")] + [Editable(true)] + public bool? IsRoot { get; set; } + } } \ No newline at end of file diff --git a/VOL.Entity/DomainModels/DeptShip/SystemDept/HR_DingTalkDept.cs b/VOL.Entity/DomainModels/DeptShip/SystemDept/HR_DingTalkDept.cs new file mode 100644 index 0000000..7bba1d6 --- /dev/null +++ b/VOL.Entity/DomainModels/DeptShip/SystemDept/HR_DingTalkDept.cs @@ -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; } + } +} diff --git a/VOL.Entity/DomainModels/DeptShip/SystemDept/HR_YSDept.cs b/VOL.Entity/DomainModels/DeptShip/SystemDept/HR_YSDept.cs new file mode 100644 index 0000000..9677d77 --- /dev/null +++ b/VOL.Entity/DomainModels/DeptShip/SystemDept/HR_YSDept.cs @@ -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; } + } +} diff --git a/VOL.Entity/DomainModels/EmployeeSync/HR_EmployeeSync.cs b/VOL.Entity/DomainModels/EmployeeSync/HR_EmployeeSync.cs index fe6cc0f..fcd53b5 100644 --- a/VOL.Entity/DomainModels/EmployeeSync/HR_EmployeeSync.cs +++ b/VOL.Entity/DomainModels/EmployeeSync/HR_EmployeeSync.cs @@ -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 { /// @@ -91,6 +90,15 @@ namespace VOL.Entity.DomainModels [Editable(true)] public string EmpLastSyncInfo { get; set; } + /// + ///员工钉钉部门ID + /// + [Display(Name ="员工钉钉部门ID")] + [MaxLength(50)] + [Column(TypeName="nvarchar(50)")] + [Editable(true)] + public string EmpDingTalkDeptID { get; set; } + } } \ No newline at end of file diff --git a/VOL.HR/IServices/DeptShip/Partial/IHR_DeptShipService.cs b/VOL.HR/IServices/DeptShip/Partial/IHR_DeptShipService.cs index afb65b0..baf61d6 100644 --- a/VOL.HR/IServices/DeptShip/Partial/IHR_DeptShipService.cs +++ b/VOL.HR/IServices/DeptShip/Partial/IHR_DeptShipService.cs @@ -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> GetYSERPDepartments(); + + + + Task> GetDingTalkDepartments(); } } diff --git a/VOL.HR/IServices/DeptSync/Partial/IHR_DeptSyncService.cs b/VOL.HR/IServices/DeptSync/Partial/IHR_DeptSyncService.cs index 0b13c3c..b798f06 100644 --- a/VOL.HR/IServices/DeptSync/Partial/IHR_DeptSyncService.cs +++ b/VOL.HR/IServices/DeptSync/Partial/IHR_DeptSyncService.cs @@ -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); } - } +} diff --git a/VOL.HR/IServices/EmployeeSync/Partial/IHR_EmployeeSyncService.cs b/VOL.HR/IServices/EmployeeSync/Partial/IHR_EmployeeSyncService.cs index 47ac72f..a5e07ea 100644 --- a/VOL.HR/IServices/EmployeeSync/Partial/IHR_EmployeeSyncService.cs +++ b/VOL.HR/IServices/EmployeeSync/Partial/IHR_EmployeeSyncService.cs @@ -18,5 +18,11 @@ namespace VOL.HR.IServices Task GenEmpSystemShip(); + + + Task UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo); + + + Task SyncYSEmpToDingTalk(string ysEmpId); } - } +} diff --git a/VOL.HR/Services/DeptShip/Partial/HR_DeptShipService.cs b/VOL.HR/Services/DeptShip/Partial/HR_DeptShipService.cs index 03f6ef8..2a09829 100644 --- a/VOL.HR/Services/DeptShip/Partial/HR_DeptShipService.cs +++ b/VOL.HR/Services/DeptShip/Partial/HR_DeptShipService.cs @@ -63,5 +63,23 @@ namespace VOL.HR.Services return depts; } } - } + + public Task> GetDingTalkDepartments() + { + + if (_cacheService.Exists("DINGTALK_DEPT_CACHE")) + { + return Task.FromResult(_cacheService.Get>("DINGTALK_DEPT_CACHE")); + } + else + { + var depts = _dingTalkService.GetSubDepartmentsAsync(); + if (depts != null) + { + _cacheService.Add("DINGTALK_DEPT_CACHE", JsonConvert.SerializeObject(depts.Result), 600); + } + return depts; + } + } + } } diff --git a/VOL.HR/Services/DeptSync/Partial/HR_DeptSyncService.cs b/VOL.HR/Services/DeptSync/Partial/HR_DeptSyncService.cs index 7133f30..15a094d 100644 --- a/VOL.HR/Services/DeptSync/Partial/HR_DeptSyncService.cs +++ b/VOL.HR/Services/DeptSync/Partial/HR_DeptSyncService.cs @@ -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(); + try + { + db.BeginTran(); + // 通过缓存直接反序列化转换对象 + var deptsData = _cacheService.Get>("YS_DEPT_CACHE"); + + // 清空再插入 + db.DbMaintenance.TruncateTable(); + db.Fastest().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(); + + db.BeginTran(); + // 通过缓存直接反序列化转换对象 + var deptsData = _cacheService.Get>("DINGTALK_DEPT_CACHE"); + + // 清空再插入 + db.DbMaintenance.TruncateTable(); + db.Fastest().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; + } + } + } + } + + + /// + /// 将扁平的 YSERP 部门列表转换为树形结构 + /// 规则: + /// - parentid 为 null/empty/"0" 或者父节点在列表中不存在时,视为根节点 + /// - 避免将节点添加为自己的子节点 + /// - 会初始化每个节点的 subDepts 列表并按 displayorder 排序(然后按 name 作为次级排序) + /// + /// 扁平部门列表 + /// 树形结构的根节点列表 + private List BuildYSDeptTree(List flatList) + { + if (flatList == null || flatList.Count == 0) + return new List(); + + // 初始化 subDepts,防止 null 引发 NRE + foreach (var d in flatList) + { + if (d.subDepts == null) + d.subDepts = new List(); + 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(); + + 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(); + parent.subDepts.Add(node); + } + else + { + // 找不到父节点视为根 + roots.Add(node); + } + } + } + + // 递归排序子节点(按 displayorder,再按 name) + void SortRecursively(List 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; + } + + } } diff --git a/VOL.HR/Services/EmployeeSync/Partial/HR_EmployeeSyncService.cs b/VOL.HR/Services/EmployeeSync/Partial/HR_EmployeeSyncService.cs index 7741158..5431d6c 100644 --- a/VOL.HR/Services/EmployeeSync/Partial/HR_EmployeeSyncService.cs +++ b/VOL.HR/Services/EmployeeSync/Partial/HR_EmployeeSyncService.cs @@ -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> GetAllYSEmployees() + public async Task> GetAllYSEmployees() { if (_cacheService.Exists("YS_EMP_CACHE")) { - return Task.FromResult(_cacheService.Get>("YS_EMP_CACHE")); + return _cacheService.Get>("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> GetAllDingTalkEmployees() + public async Task> GetAllDingTalkEmployees() { if (_cacheService.Exists("DingTalk_EMP_CACHE")) { - return Task.FromResult(_cacheService.Get>("DingTalk_EMP_CACHE")); + return _cacheService.Get>("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 dingTalkEmployees = []; List 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(); + db.DbMaintenance.TruncateTable(); + //db.Deleteable(); db.Fastest().BulkCopy(newList); db.CommitTran(); } @@ -145,6 +169,50 @@ namespace VOL.HR.Services } return Task.CompletedTask; } + + public async Task UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo) + { + return await _dingTalkService.UpdateEmpInfo(updateInfo); + //return Task.FromResult(true); + } + + + public async Task 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; + } } } diff --git a/VOL.WebApi/Controllers/HR/Partial/HR_DeptShipController.cs b/VOL.WebApi/Controllers/HR/Partial/HR_DeptShipController.cs index b92b458..788f1f1 100644 --- a/VOL.WebApi/Controllers/HR/Partial/HR_DeptShipController.cs +++ b/VOL.WebApi/Controllers/HR/Partial/HR_DeptShipController.cs @@ -31,7 +31,7 @@ namespace VOL.HR.Controllers } /// - /// table加载数据后刷新当前table数据的字典项(适用字典数据量比较大的情况) + /// /// /// /// diff --git a/VOL.WebApi/Controllers/HR/Partial/HR_DeptSyncController.cs b/VOL.WebApi/Controllers/HR/Partial/HR_DeptSyncController.cs index f4f4335..1688d95 100644 --- a/VOL.WebApi/Controllers/HR/Partial/HR_DeptSyncController.cs +++ b/VOL.WebApi/Controllers/HR/Partial/HR_DeptSyncController.cs @@ -29,5 +29,43 @@ namespace VOL.HR.Controllers _service = service; _httpContextAccessor = httpContextAccessor; } + + /// + /// + /// + /// + /// + [HttpPost, Route("cacheYSERPDept")] + public async Task GetYSERPDepartments() + { + await Service.CacheYSDepartments(); + return Json(new { }); + } + + /// + /// + /// + /// + /// + [HttpPost, Route("cacheDingTalkDept")] + public async Task CacheDingTalkDept() + { + await Service.CacheDingTalkDepartments(); + return Json(new { }); + } + + // + /// + /// + /// + /// + [HttpPost, Route("syncYSERPDeptToDingTalk")] + public async Task SyncYSERPDeptToDingTalk() + { + await Service.SyncYSERPDeptToDingTalk(); + return Json(new { }); + } + + } } diff --git a/VOL.WebApi/Controllers/HR/Partial/HR_EmployeeSyncController.cs b/VOL.WebApi/Controllers/HR/Partial/HR_EmployeeSyncController.cs index 8d545f6..3ce93b0 100644 --- a/VOL.WebApi/Controllers/HR/Partial/HR_EmployeeSyncController.cs +++ b/VOL.WebApi/Controllers/HR/Partial/HR_EmployeeSyncController.cs @@ -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 /// - /// + /// /// /// /// - [HttpPost, Route("/api/HR_YSEmployees/getPageData")] - public async Task GetYSAllEmployees() + [HttpPost, Route("GenEmpSystemShip")] + public async Task GenEmpSystemShip() { - var emps = await Service.GetAllYSEmployees(); - return Json(emps); + await Service.GenEmpSystemShip(); + return Json(new { }); } /// @@ -56,16 +56,40 @@ namespace VOL.HR.Controllers return Json(emps); } + /// + /// + /// + /// + /// + [HttpPost, Route("/api/HR_YSEmployees/getPageData")] + public async Task GetYSAllEmployees() + { + var emps = await Service.GetAllYSEmployees(); + return Json(emps); + } + + /// /// /// /// /// - [HttpPost, Route("GenEmpSystemShip")] - public async Task GenEmpSystemShip() + [HttpPost, Route("UpdateEmpInfo")] + public async Task UpdateEmpInfo(DingTalkEmployeeUpdate updateInfo) { - await Service.GenEmpSystemShip(); + await Service.UpdateEmpInfo(updateInfo); return Json(new { }); } + /// + /// + /// + /// + /// + [HttpPost, Route("SyncYSEmpToDingTalk")] + public async Task SyncYSEmpToDingTalk([FromBody] string ysEmpId) + { + var result = await Service.SyncYSEmpToDingTalk(ysEmpId); + return Json(new { success=result }); + } } } diff --git a/VOL.WebApi/Download/20250928/Template/跨系统部门关联20250928092853.xlsx b/VOL.WebApi/Download/20250928/Template/跨系统部门关联20250928092853.xlsx new file mode 100644 index 0000000..6ee7050 Binary files /dev/null and b/VOL.WebApi/Download/20250928/Template/跨系统部门关联20250928092853.xlsx differ diff --git a/VOL.WebApi/Download/ExcelExport/20250928/跨系统部门关联20250928094653.xlsx b/VOL.WebApi/Download/ExcelExport/20250928/跨系统部门关联20250928094653.xlsx new file mode 100644 index 0000000..61cf923 Binary files /dev/null and b/VOL.WebApi/Download/ExcelExport/20250928/跨系统部门关联20250928094653.xlsx differ diff --git a/VOL.WebApi/Download/Logger/Queue/WriteError/20250929.txt b/VOL.WebApi/Download/Logger/Queue/WriteError/20250929.txt new file mode 100644 index 0000000..a94b9a5 --- /dev/null +++ b/VOL.WebApi/Download/Logger/Queue/WriteError/20250929.txt @@ -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 diff --git a/VOL.YSErp/Models/Biz/YSERPDepartment.cs b/VOL.YSErp/Models/Biz/YSERPDepartment.cs index 2b42a76..ecfaf36 100644 --- a/VOL.YSErp/Models/Biz/YSERPDepartment.cs +++ b/VOL.YSErp/Models/Biz/YSERPDepartment.cs @@ -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 subDepts { get; set; } = []; } } diff --git a/VOL.YSErp/Models/Biz/YSERPEmployeeInfo.cs b/VOL.YSErp/Models/Biz/YSERPEmployeeInfo.cs new file mode 100644 index 0000000..cce7aec --- /dev/null +++ b/VOL.YSErp/Models/Biz/YSERPEmployeeInfo.cs @@ -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 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; } + } + + } +} diff --git a/VOL.YSErp/Models/Biz/YSERPResponse.cs b/VOL.YSErp/Models/Biz/YSERPPagedResponse.cs similarity index 78% rename from VOL.YSErp/Models/Biz/YSERPResponse.cs rename to VOL.YSErp/Models/Biz/YSERPPagedResponse.cs index fd989dd..dad0e88 100644 --- a/VOL.YSErp/Models/Biz/YSERPResponse.cs +++ b/VOL.YSErp/Models/Biz/YSERPPagedResponse.cs @@ -6,7 +6,21 @@ using System.Threading.Tasks; namespace VOL.YSErp.Models.Biz { + + public class YSERPResponse + { + + 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 { public string code { get; set; } @@ -27,6 +41,8 @@ namespace VOL.YSErp.Models.Biz public bool haveNextPage { get; set; } public List recordList { get; set; } } + + public class YSERPListResponse { diff --git a/VOL.YSErp/Services/Biz/YSERPService.cs b/VOL.YSErp/Services/Biz/YSERPService.cs index 624f1b8..00a6c3f 100644 --- a/VOL.YSErp/Services/Biz/YSERPService.cs +++ b/VOL.YSErp/Services/Biz/YSERPService.cs @@ -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>(content); + var data = JsonSerializer.Deserialize>(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> 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 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>(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()