From d7a34c6e1038e3de13b295453d4d227b2368355f Mon Sep 17 00:00:00 2001 From: liguoyang Date: Tue, 6 Jan 2026 10:35:27 +0800 Subject: [PATCH] =?UTF-8?q?fix=20=E7=94=9F=E6=97=A5=E5=88=B8=E5=8F=91?= =?UTF-8?q?=E6=94=BE=E7=BB=86=E8=8A=82=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserBusinessEventController.cs | 44 +++++ .../UserBusinessEventPageRequest.cs | 13 ++ .../UserBusinessEventResponse.cs | 24 +++ .../Services/Implements/JinkoApiService.cs | 166 +++++++++--------- .../Implements/BirthdayVoucherService.cs | 65 ++++++- .../Common/Entities/BusinessEventEntity.cs | 26 +++ .../Jobs/Visitor/TipAppointmentExpireJob.cs | 2 +- 7 files changed, 247 insertions(+), 93 deletions(-) create mode 100644 Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventController.cs create mode 100644 Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventPageRequest.cs create mode 100644 Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventResponse.cs create mode 100644 Src/Dlhis.Entity/Common/Entities/BusinessEventEntity.cs diff --git a/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventController.cs b/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventController.cs new file mode 100644 index 00000000..8338d227 --- /dev/null +++ b/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventController.cs @@ -0,0 +1,44 @@ +using Dlhis.Entity.Basic.Entity; +using Dlhis.Entity.Common.Entities; +using Kurisu.AspNetCore.DataAccess.Dto; +using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions; +using Kurisu.AspNetCore.DataAccess.SqlSugar.Services; +using Kurisu.AspNetCore.Utils.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Dlhis.Basic.Common.BusinessEvents; + + +[Authorize] +[Route("api/[controller]")] +[ApiController] +public class UserBusinessEventController : ControllerBase +{ + private readonly IDbContext _dbContext; + + public UserBusinessEventController(IDbContext dbContext) + { + _dbContext = dbContext; + } + + /// + /// 获取用户业务事件分页数据 + /// + /// + [HttpGet("page")] + public async Task> GetUserBusinessEventPageAsync([FromQuery] UserBusinessEventPageRequest request) + { + return await _dbContext.Queryable() + .LeftJoin((x, ga) => x.UserCode == ga.Code) + .WhereIF(request.Search.IsPresent(), (x, ga) => x.UserCode == request.Search || ga.Name.Contains(request.Search)) + .Select((x, ga) => new UserBusinessEventResponse + { + UserCode = x.UserCode, + EventType = x.EventType, + UserName = ga.Name, + Content = x.Content + }) + .ToPageListAsync(request); + } +} diff --git a/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventPageRequest.cs b/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventPageRequest.cs new file mode 100644 index 00000000..37e4ea10 --- /dev/null +++ b/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventPageRequest.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; +using Kurisu.AspNetCore.DataAccess.Dto; + +namespace Dlhis.Basic.Common.BusinessEvents; + +public class UserBusinessEventPageRequest : PageDto +{ + /// + /// 事件类型 + /// + [Required(ErrorMessage = "事件类型不能为空")] + public string EventType { get; set; } +} diff --git a/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventResponse.cs b/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventResponse.cs new file mode 100644 index 00000000..a1b3fbca --- /dev/null +++ b/Src/Dlhis.Basic/Common/BusinessEvents/UserBusinessEventResponse.cs @@ -0,0 +1,24 @@ +namespace Dlhis.Basic.Common.BusinessEvents; + +public class UserBusinessEventResponse +{ + /// + /// 工号 + /// + public string UserCode { get; set; } + + /// + /// 事件类型 + /// + public string EventType { get; set; } + + /// + /// 用户名称 + /// + public string UserName { get; set; } + + /// + /// 事件内容 + /// + public string Content { get; set; } +} diff --git a/Src/Dlhis.Basic/Common/Message/Services/Implements/JinkoApiService.cs b/Src/Dlhis.Basic/Common/Message/Services/Implements/JinkoApiService.cs index e6c28216..8edab138 100644 --- a/Src/Dlhis.Basic/Common/Message/Services/Implements/JinkoApiService.cs +++ b/Src/Dlhis.Basic/Common/Message/Services/Implements/JinkoApiService.cs @@ -23,6 +23,89 @@ public class JinkoApiService : IJinkoApiService _serviceProvider = serviceProvider; } + + public async Task SendWorkMessageAsync(DingTalkWorkMessageRequest request) + { + request.Content.IsEmpty().ThrowIfTrue("发送内容不能为空"); + + var users = await GetJinkoOaInfoByWorkCodes(request.WorkCodes); + var userIds = users.Select(x => x.Id).Join(","); + (userIds.Length > 100).ThrowIfTrue("超出消息接受人数,length > 100"); + + var body = new + { + agent_id = _agentId, + userid_list = userIds, + msg = new + { + msgtype = "text", + text = new + { + content = request.Content + } + } + }.ToJson(); + + await CommonRequestAsync(body); + } + + public async Task SendWorkMessageByWorkGridAsync(DingTalkWorkMessageRequest request) + { + await SendWorkMessageAsync(request); + + using var scope = _serviceProvider.CreateScope(); + var ol = scope.ServiceProvider.GetService(); + await ol.LogAsync(OperateLogType.WorkGrid, $"dingtalk消息推送:{request.ToJson()}"); + } + + public async Task SendMessageUrlAsync(DingTalkMessageUrlRequest request) + { + request.Title.IsEmpty().ThrowIfTrue("发送标题不能为空"); + request.Text.IsEmpty().ThrowIfTrue("发送内容不能为空"); + request.UserCode.IsEmpty().ThrowIfTrue("发送用户不能为空"); + request.MessageUrl.IsEmpty().ThrowIfTrue("消息链接不能为空"); + request.PicUrl.IsEmpty().ThrowIfTrue("图片链接不能为空"); + + + var users = await GetJinkoOaInfoByWorkCodes(new List { request.UserCode }); + var userIds = users.Select(x => x.Id).Join(","); + var body = new + { + agent_id = _agentId, + userid_list = userIds, + msg = new + { + msgtype = "link", + link = new + { + messageUrl = request.MessageUrl, + picUrl = request.PicUrl, + title = request.Title, + text = request.Text + } + } + }.ToJson(); + + await CommonRequestAsync(body); + } + + private async Task CommonRequestAsync(string body) + { +#if !DEBUG + var token = await GetTokenAsync(); + var httpClient = _httpClientFactory.CreateClient("jinko.httpclient"); + var url = $"/topapi/message/corpconversation/asyncsend_v2?access_token={token}"; + var content = new StringContent(body, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync(url, content); + var jsonResponse = await response.Content.ReadAsStringAsync(); + + var jObject = JObject.Parse(jsonResponse); + (jObject.Value("errcode") == "0").ThrowIfFalse(jObject.Value("errmsg")); +#endif + } + + private async Task GetTokenAsync() { var token = string.Empty; @@ -49,51 +132,6 @@ public class JinkoApiService : IJinkoApiService return token; } - - public async Task SendWorkMessageAsync(DingTalkWorkMessageRequest request) - { - request.Content.IsEmpty().ThrowIfTrue("发送内容不能为空"); - var token = await GetTokenAsync(); - - var users = await GetJinkoOaInfoByWorkCodes(request.WorkCodes); - var userIds = users.Select(x => x.Id).Join(","); - (userIds.Length > 100).ThrowIfTrue("超出消息接受人数,length > 100"); - - var body = new - { - agent_id = _agentId, - userid_list = userIds, - msg = new - { - msgtype = "text", - text = new - { - content = request.Content - } - } - }.ToJson(); - - var httpClient = _httpClientFactory.CreateClient("jinko.httpclient"); - var url = $"/topapi/message/corpconversation/asyncsend_v2?access_token={token}"; - var content = new StringContent(body, Encoding.UTF8, "application/json"); - - var response = await httpClient.PostAsync(url, content); - var jsonResponse = await response.Content.ReadAsStringAsync(); - - var jObject = JObject.Parse(jsonResponse); - (jObject.Value("errcode") == "0").ThrowIfFalse(jObject.Value("errmsg")); - } - - public async Task SendWorkMessageByWorkGridAsync(DingTalkWorkMessageRequest request) - { - await SendWorkMessageAsync(request); - - using var scope = _serviceProvider.CreateScope(); - var ol = scope.ServiceProvider.GetService(); - await ol.LogAsync(OperateLogType.WorkGrid, $"dingtalk消息推送:{request.ToJson()}"); - } - - private async Task> GetJinkoOaInfoByWorkCodes(List codes) { var httpClient = _httpClientFactory.CreateClient("jinko.oa"); @@ -115,42 +153,4 @@ public class JinkoApiService : IJinkoApiService return oaInfo.Result.Data; } - public async Task SendMessageUrlAsync(DingTalkMessageUrlRequest request) - { - request.Title.IsEmpty().ThrowIfTrue("发送标题不能为空"); - request.Text.IsEmpty().ThrowIfTrue("发送内容不能为空"); - request.UserCode.IsEmpty().ThrowIfTrue("发送用户不能为空"); - request.MessageUrl.IsEmpty().ThrowIfTrue("消息链接不能为空"); - request.PicUrl.IsEmpty().ThrowIfTrue("图片链接不能为空"); - var token = await GetTokenAsync(); - - var users = await GetJinkoOaInfoByWorkCodes(new List { request.UserCode }); - var userIds = users.Select(x => x.Id).Join(","); - var body = new - { - agent_id = _agentId, - userid_list = userIds, - msg = new - { - msgtype = "link", - link = new - { - messageUrl = request.MessageUrl, - picUrl = request.PicUrl, - title = request.Title, - text = request.Text - } - } - }.ToJson(); - - var httpClient = _httpClientFactory.CreateClient("jinko.httpclient"); - var url = $"/topapi/message/corpconversation/asyncsend_v2?access_token={token}"; - var content = new StringContent(body, Encoding.UTF8, "application/json"); - - var response = await httpClient.PostAsync(url, content); - var jsonResponse = await response.Content.ReadAsStringAsync(); - - var jObject = JObject.Parse(jsonResponse); - (jObject.Value("errcode") == "0").ThrowIfFalse(jObject.Value("errmsg")); - } } \ No newline at end of file diff --git a/Src/Dlhis.Canteen/Voucher/Services/Implements/BirthdayVoucherService.cs b/Src/Dlhis.Canteen/Voucher/Services/Implements/BirthdayVoucherService.cs index d6a84c5e..799ef2cc 100644 --- a/Src/Dlhis.Canteen/Voucher/Services/Implements/BirthdayVoucherService.cs +++ b/Src/Dlhis.Canteen/Voucher/Services/Implements/BirthdayVoucherService.cs @@ -11,6 +11,7 @@ using Dlhis.Entity.Basic.Enums; using Dlhis.Entity.Canteen.Entities; using Dlhis.Entity.Canteen.Entities.Voucher; using Dlhis.Entity.Canteen.Enums; +using Dlhis.Entity.Common.Entities; using Kurisu.AspNetCore.CustomClass; using Kurisu.AspNetCore.DataAccess.Dto; using Kurisu.AspNetCore.DataAccess.Entity; @@ -45,12 +46,13 @@ public class BirthdayVoucherService : BaseVoucherService, IVoucherService //忽略tenant操作 await DbContext.IgnoreAsync(async () => { - var gainfos = await GetGainfosAsync(userCodes, p.begin, p.end); + var (gainfos, events) = await GetGainfosAsync(userCodes, p.begin, p.end); var departmentNos = gainfos.Select(g => g.DepartmentNo).Distinct().ToList(); var departmentInfos = await DbContext.Queryable() .Where(x => departmentNos.Contains(x.No)) .ToListAsync(); + foreach (var gainfo in gainfos.Where(x => x.Birthday.HasValue)) { await DbContext.Client.Ado.BeginTranAsync(); @@ -64,11 +66,17 @@ public class BirthdayVoucherService : BaseVoucherService, IVoucherService await DbContext.Client.Ado.CommitTranAsync(); } - catch (Exception) + catch (Exception ex) { await DbContext.Client.Ado.RollbackTranAsync(); + events.Add(new UserBusinessEventEntity { UserCode = gainfo.Code, EventType = "GenerateBirthdayVoucher", Content = $"出现异常:{ex.Message}" }); } } + + if (events.Any()) + { + await DbContext.InsertAsync(events); + } }); } @@ -86,11 +94,16 @@ public class BirthdayVoucherService : BaseVoucherService, IVoucherService .Where(x => x.Type == VoucherEnumType.Birthday) .AnyAsync()) { + + // 使用安全日期来处理 2/29 情况:非闰年会退到当年该月最大天数(即 2/28) + var claimDate = SafeDateForYear(year, gainfo.Birthday.Value); + + await DbContext.InsertAsync(new VoucherEntity { No = no, Type = VoucherEnumType.Birthday, - ClaimDateTime = new DateTime(year, gainfo.Birthday.Value.Month, gainfo.Birthday.Value.Day), + ClaimDateTime = claimDate, OriginalBalance = amount, Balance = amount, @@ -144,8 +157,17 @@ public class BirthdayVoucherService : BaseVoucherService, IVoucherService } } + /// + /// 返回在指定年份内与原生日相匹配的安全日期。 + /// - 对 2/29 在非闰年情况下,会退到当年该月最大天数(即 2/28)。 + /// + private static DateTime SafeDateForYear(int year, DateTime birthday) + { + var day = Math.Min(birthday.Day, DateTime.DaysInMonth(year, birthday.Month)); + return new DateTime(year, birthday.Month, day); + } - private (DateTime begin, DateTime end) GetDateRange(Dictionary pairs) + private static (DateTime begin, DateTime end) GetDateRange(Dictionary pairs) { var begin = DateTime.Today; var end = begin; @@ -172,7 +194,7 @@ public class BirthdayVoucherService : BaseVoucherService, IVoucherService return (begin, end); } - private async Task> GetGainfosAsync(List userCodes, DateTime begin, DateTime end) + private async Task<(List, List)> GetGainfosAsync(List userCodes, DateTime begin, DateTime end) { var factries = await _factorySettingApi.GetEnableBirthdayVoucherFactoriesAsync(); @@ -195,17 +217,42 @@ public class BirthdayVoucherService : BaseVoucherService, IVoucherService "49990004" }; - return await DbContext.Queryable() + var gainfos = await DbContext.Queryable() .WhereIF(userCodes.IsPresent(), x => userCodes.Contains(x.Code)) - .Where(x => !SqlFunc.IsNullOrEmpty(x.DepartmentNo)) - .Where(x => !excludePositionNos.Contains(x.PositionNo))//排除特殊岗位 + //.Where(x => !SqlFunc.IsNullOrEmpty(x.DepartmentNo)) + //.Where(x => !excludePositionNos.Contains(x.PositionNo))//排除特殊岗位 .Where(x => x.Type == GainfoUserTypeEnumType.Normal || x.Type == GainfoUserTypeEnumType.SecurityWhite) - .Where(x => x.Birthday.HasValue && !string.IsNullOrEmpty(x.CostId)) + .Where(x => x.Birthday.HasValue) + //.Where(x => !string.IsNullOrEmpty(x.CostId)) .WhereIF(isToday, x => SqlFunc.DateValue(x.Birthday.Value, DateType.Month) == begin.Month && SqlFunc.DateValue(x.Birthday.Value, DateType.Day) == begin.Day) .WhereIF(!isToday && !overYear, x => SqlFunc.DateValue(x.Birthday.Value, DateType.Month) >= beginMonth && SqlFunc.DateValue(x.Birthday.Value, DateType.Month) <= endMonth) .WhereIF(!isToday && overYear, x => SqlFunc.DateValue(x.Birthday.Value, DateType.Month) >= beginMonth || SqlFunc.DateValue(x.Birthday.Value, DateType.Month) <= endMonth) .Where(x => factries.Contains(x.Factory)) .ToListAsync(); + + //UserBusinessEventEntity + var events = new List(); + foreach (var gainfo in gainfos) + { + if (gainfo.DepartmentNo.IsEmpty()) + { + events.Add(new UserBusinessEventEntity { UserCode = gainfo.Code, EventType = "GenerateBirthdayVoucher", Content = "部门编号为空" }); + } + else if (excludePositionNos.Contains(gainfo.PositionNo)) + { + events.Add(new UserBusinessEventEntity { UserCode = gainfo.Code, EventType = "GenerateBirthdayVoucher", Content = $"排除岗位编码{gainfo.PositionNo}" }); + } + else if (gainfo.CostId.IsEmpty()) + { + events.Add(new UserBusinessEventEntity { UserCode = gainfo.Code, EventType = "GenerateBirthdayVoucher", Content = "成本中心为空" }); + } + } + + return (gainfos + .Where(x => x.DepartmentNo.IsPresent()) + .Where(x => !excludePositionNos.Contains(x.PositionNo)) + .Where(x => x.CostId.IsPresent()) + .ToList(), events); } private static string GenerateUniqueCode(string data) diff --git a/Src/Dlhis.Entity/Common/Entities/BusinessEventEntity.cs b/Src/Dlhis.Entity/Common/Entities/BusinessEventEntity.cs new file mode 100644 index 00000000..45a2369c --- /dev/null +++ b/Src/Dlhis.Entity/Common/Entities/BusinessEventEntity.cs @@ -0,0 +1,26 @@ +using Kurisu.AspNetCore.DataAccess.SqlSugar; +using SqlSugar; + +namespace Dlhis.Entity.Common.Entities; + +[SugarTable(TableName = "UserBusinessEvents", TableDescription = "用户业务事件记录")] +public class UserBusinessEventEntity : SugarBaseEntity +{ + /// + /// 工号 + /// + [SugarColumn(IsNullable = false, ColumnDescription = "人员工号")] + public string UserCode { get; set; } + + /// + /// 事件类型 + /// + [SugarColumn(IsNullable = false, ColumnDescription = "事件类型")] + public string EventType { get; set; } + + /// + /// 发生内容 + /// + [SugarColumn(IsNullable = false, ColumnDescription = "发生内容")] + public string Content { get; set; } +} diff --git a/Src/Dlhis.ScheduledTask/Jobs/Visitor/TipAppointmentExpireJob.cs b/Src/Dlhis.ScheduledTask/Jobs/Visitor/TipAppointmentExpireJob.cs index cf2c1def..93d11c34 100644 --- a/Src/Dlhis.ScheduledTask/Jobs/Visitor/TipAppointmentExpireJob.cs +++ b/Src/Dlhis.ScheduledTask/Jobs/Visitor/TipAppointmentExpireJob.cs @@ -10,7 +10,7 @@ namespace Dlhis.ScheduledTask.Jobs.Visitor; /// /// 每天10点提醒访客预约到期 /// -[RecurringJob(nameof(TipAppointmentExpireJob), "0 0 10 * * ?", Name = "提醒到访人访客将要到期", RunOnDevelopment = true)] +[RecurringJob(nameof(TipAppointmentExpireJob), "0 0 10 * * ?", Name = "提醒到访人访客将要到期")] public class TipAppointmentExpireJob : BaseJob { private readonly IDingTalkApi _dingTalkApi;