using System.Text; using Dlhis.Basic.Api; using Dlhis.Basic.Api.Requests; using Dlhis.Canteen.Tickets.Services; using Dlhis.Canteen.Tickets.Services.Implements; using Dlhis.Canteen.Voucher.Requests; using Dlhis.Canteen.Voucher.Response; using Dlhis.Constants; using Dlhis.Entity.Basic.Entity; 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; using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions; using Kurisu.AspNetCore.Utils.Extensions; using SqlSugar; namespace Dlhis.Canteen.Voucher.Services.Implements; [Service("BirthdayVoucherService")] public class BirthdayVoucherService : BaseVoucherService, IVoucherService { private readonly IFactorySettingApi _factorySettingApi; private readonly IDingTalkApi _dingTalkApi; private readonly ILogger _logger; /// public BirthdayVoucherService(IServiceProvider serviceProvider) : base(serviceProvider) { _factorySettingApi = serviceProvider.GetService(); _dingTalkApi = serviceProvider.GetService(); _logger = serviceProvider.GetService>(); } /// public override async Task CreateAsync(Dictionary pairs) { var p = GetDateRange(pairs); var remark = pairs != null && pairs.TryGetValue("remark", out var r) ? r?.ToString() : string.Empty; var userCodes = pairs != null && pairs.TryGetValue("userCodes", out var c) ? c as List : new List(); //忽略tenant操作 await DbContext.IgnoreAsync(async () => { 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(); try { var years = GetBirthdayYears(gainfo.Birthday.Value, p.begin, p.end); if (gainfo.EntryDate.HasValue) { // 仅保留在候选发放年份中,入职日期严格早于该年生日的年份 years = years.Where(y => { var r = gainfo.EntryDate.Value.Date < SafeDateForYear(y, gainfo.Birthday.Value); if (!r) { events.Add(new UserBusinessEventEntity { UserCode = gainfo.Code, EventType = "GenerateBirthdayVoucher", Content = $"入职时间为{gainfo.EntryDate.Value.ToString("yyyy-MM-dd")},不发放日期{SafeDateForYear(y, gainfo.Birthday.Value).ToString("yyyy-MM-dd")}的生日券" }); } return r; }).ToList(); } foreach (var year in years) { await GenerateVoucherAsync(gainfo, departmentInfos, year, remark); } await DbContext.Client.Ado.CommitTranAsync(); } 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); } }); } private async Task GenerateVoucherAsync(GainfoEntity gainfo, List departments, int year, string remark) { //生日券金额固定100元 const decimal amount = 100; var dayStr = year.ToString();//按年区分,避免生日日期变更重复发放 //由数据类型+工号+生日组成唯一标识 var no = GenerateUniqueCode(VoucherEnumType.Birthday.ToString() + gainfo.Code + dayStr); if (!await DbContext.Queryable() .Where(x => x.No == no) .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 = claimDate, OriginalBalance = amount, Balance = amount, Description = dayStr + "生日券", Remark = remark, CreatedBy = 1, CreateTime = DateTime.Now, ModifiedBy = 1, ModifiedTime = DateTime.Now, Version = 0 }); var department = departments.FirstOrDefault(x => x.No == gainfo.DepartmentNo); await DbContext.InsertAsync(new VoucherUserInfoEntity { VoucherNo = no, UserCode = gainfo.Code, CostId = gainfo.CostId, Werks = gainfo.Werks, Factory = gainfo.Factory, PositionNo = gainfo.PositionNo, DepartmentNo = gainfo.DepartmentNo, DepartmentNoFirst = department?.DepartmentNoFirst, DepartmentNoSecond = department?.DepartmentNoSecond, CreatedBy = 1, CreateTime = DateTime.Now, ModifiedBy = 1, ModifiedTime = DateTime.Now }); try { await _dingTalkApi.SendMessageUrlAsync(new DingTalkMessageUrlRequest { MessageUrl = "dingtalk://dingtalkclient/page/link?url=https%3A%2F%2Fiam.jinkosolar.com%2Fsso%2Foidc1%2FdAuthorize%3Fclient_id%3DAPP104%26response_type%3Dcode%26redirect_uri%3Dhttps%3a%2f%2fface.jinkosolar.com%2fh5%2f%3fto%3dmyBirthdayCoupon", PicUrl = "@lALOACZwe2Rk", Title = "请查收你的生日消费券", Text = gainfo.Name + "生日快乐!特意为你准备了一张生日消费券,点击下方链接去查看,快去超市选购心仪好物吧!", UserCode = gainfo.Code }); } catch (Exception ex) { _logger.LogError(ex, "发送生日券消息失败,工号:{UserCode},错误原因:{Error}", gainfo.Code, ex.Message); } } } /// /// 返回在指定年份内与原生日相匹配的安全日期。 /// - 对 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 static (DateTime begin, DateTime end) GetDateRange(Dictionary pairs) { var begin = DateTime.Today; var end = begin; if (pairs != null) { if (pairs.TryGetValue("begin", out var b)) { if (b is DateTime bb) { begin = bb; } } if (pairs.TryGetValue("end", out var e)) { if (e is DateTime ee) { end = ee; } } } return (begin, end); } private async Task<(List, List)> GetGainfosAsync(List userCodes, DateTime begin, DateTime end) { var factries = await _factorySettingApi.GetEnableBirthdayVoucherFactoriesAsync(); var isToday = begin.Date == end.Date; var includeLeapDayBirthday = isToday && begin.Month == 2 && begin.Day == 28 && !DateTime.IsLeapYear(begin.Year); var beginMonth = begin.Month; var endMonth = end.Month; var overYear = endMonth < beginMonth; var excludePositionNos = new List { "41001471", "43000181", "43000452", "43000459", "49200047", "49400001", "49400002", "49990004" }; 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 => x.Type == GainfoUserTypeEnumType.Normal || x.Type == GainfoUserTypeEnumType.SecurityWhite) .Where(x => x.Birthday.HasValue) //.Where(x => !string.IsNullOrEmpty(x.CostId)) // 正常情况:匹配当天生日 // 注意:LW 劳务派遣工号统一在下方返回前过滤,不能在此处 SQL 过滤,否则留痕事件无法写入 .WhereIF(isToday && !includeLeapDayBirthday, x => SqlFunc.DateValue(x.Birthday.Value, DateType.Month) == begin.Month && SqlFunc.DateValue(x.Birthday.Value, DateType.Day) == begin.Day) // 特殊情况:非闰年的2月28日,同时匹配当天生日和2月29日生日 .WhereIF(isToday && includeLeapDayBirthday, x => (SqlFunc.DateValue(x.Birthday.Value, DateType.Month) == begin.Month && SqlFunc.DateValue(x.Birthday.Value, DateType.Day) == begin.Day) || (SqlFunc.DateValue(x.Birthday.Value, DateType.Month) == 2 && SqlFunc.DateValue(x.Birthday.Value, DateType.Day) == 29)) .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 = "成本中心为空" }); } else if (gainfo.Code.StartsWith("LW", StringComparison.OrdinalIgnoreCase)) { events.Add(new UserBusinessEventEntity { UserCode = gainfo.Code, EventType = "GenerateBirthdayVoucher", Content = "LW工号劳务不发放生日券" }); } } return (gainfos .Where(x => x.DepartmentNo.IsPresent()) .Where(x => !excludePositionNos.Contains(x.PositionNo)) .Where(x => x.CostId.IsPresent()) .Where(x => !x.Code.StartsWith("LW", StringComparison.OrdinalIgnoreCase))//排除劳务派遣工号(LW 开头) .ToList(), events); } private static string GenerateUniqueCode(string data) { return QrCodeConstants.Birthday + data.ToArray().Select(x => { var b = Encoding.ASCII.GetBytes(x.ToString()); int i = b[0]; return Convert.ToString(i); }).Join(string.Empty); } /// /// 计算给定生日在区间 [begin, end] 内出现的所有年份(可能为0、1或多个)。 /// - 对于每个候选年份构造安全的日期(处理 2/29 在非闰年的情况,退到当年最大天数)。 /// - 若区间内找不到匹配年份,则返回包含 begin.Year 的单元素列表作为兜底行为,保持向后兼容。 /// private static List GetBirthdayYears(DateTime birthday, DateTime begin, DateTime end) { var years = new List(); // 扩展范围到 begin.Year-1 .. end.Year+1 以覆盖跨年边界的极端情况 var startYear = begin.Year - 1; var endYear = end.Year + 1; for (var y = startYear; y <= endYear; y++) { var candidate = SafeDate(y, birthday); if (candidate >= begin && candidate <= end) { years.Add(candidate.Year); } } // 去重并排序 years = years.Distinct().OrderBy(x => x).ToList(); return years; static DateTime SafeDate(int year, DateTime bday) { var day = Math.Min(bday.Day, DateTime.DaysInMonth(year, bday.Month)); return new DateTime(year, bday.Month, day); } } public override async Task> GetVoucherBalancePageAsync(VoucherBalancePageRequest request) { return await DbContext.Queryable() .LeftJoin((x, bPlace) => x.BusinessPlaceCode == bPlace.Code) .LeftJoin((x, bPlace, department) => x.DepartmentNo == department.No) .WhereIF(request.Factories.IsPresent(), x => request.Factories.Contains(x.TenantId)) .WhereIF(request.UserCode.IsPresent(), x => x.UserCode == request.UserCode) .WhereIF(request.DepartmentNo.IsPresent(), x => x.DepartmentNo == request.DepartmentNo) .WhereIF(request.SearchDateTimePeriod.HasValue(), x => x.CostDate >= request.SearchDateTimePeriod.Start && x.CostDate <= request.SearchDateTimePeriod.End) //成本日期 .Where(x => x.Status == BalanceRecordStatusEnumType.Valid && x.Type == BalanceRecordTypeEnumType.Out) //有效,消费数据 .Where(x => x.Source == BalanceRecordSourceEnumType.Birthday) .Select((x, bPlace, department) => new VoucherBalancePageResponse { No = x.No, UserNo = x.UserNo, Name = x.Name, UserCode = x.UserCode, DepartmentNo = x.DepartmentNo, DepartmentName = department.Name, CostDate = x.CostDate, Remark = x.Remark, Source = x.Source, Type = x.Type, Amount = SqlFunc.Abs(x.Amount), BusinessPlaceName = bPlace.Name, ShiftType = x.ShiftType, DeviceInfo = x.DeviceUniqueNo, CostId = x.CostId, OriginTenantId = x.OriginTenantId, Status = x.Status, CostDateTime = x.CostDateTime, Factory = x.TenantId, Werks = x.Werks }) .ToPageListAsync(request); } }