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;