using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using NCC.Extend.Entitys.lq_employee_store_assignment; using NCC.Extend.Entitys.lq_user_profile_audit; using NCC.Extend.Entitys.lq_user_profile_version; using NCC.Extend.Models; using NCC.System.Entitys.Permission; using Newtonsoft.Json; using SqlSugar; namespace NCC.Extend { /// /// 按统计月末时点还原员工主档(门店、岗位、组织等),用于跨月调动后仍按当月归属算薪。 /// public static class LqSalaryUserSnapshotHelper { private static readonly JsonSerializerSettings VersionSnapshotJsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include, DateFormatString = "yyyy-MM-dd HH:mm:ss", DateTimeZoneHandling = DateTimeZoneHandling.Local }; private static readonly HashSet SkipVersionOverlayProps = new HashSet(StringComparer.OrdinalIgnoreCase) { nameof(UserEntity.Id), nameof(UserEntity.Password), nameof(UserEntity.Secretkey), }; private static readonly HashSet SalaryRelevantAuditFields = new HashSet(StringComparer.OrdinalIgnoreCase) { nameof(UserEntity.Mdid), nameof(UserEntity.PositionId), nameof(UserEntity.Gw), nameof(UserEntity.Zw), nameof(UserEntity.OrganizeId), nameof(UserEntity.Gwfl), nameof(UserEntity.RoleId), }; /// /// 当月有考勤打卡记录或有门店归属切段与当月相交的员工,用于扩大「当前岗位已变」仍可能参与当月工资核算的用户集合。 /// public static async Task> GetSalaryCandidateUserIdsAsync( ISqlSugarClient db, DateTime startDate, DateTime endDate, int year, int month) { var attIds = await AttendanceWorkdaysFromRecordsHelper.GetUserIdsWithRecordsInMonthAsync(db, year, month); var assignIds = await db.Queryable() .Where(a => a.IsEffective == 1 && a.StartDate <= endDate.Date && (a.EndDate == null || a.EndDate >= startDate.Date)) .Select(a => a.EmployeeId) .ToListAsync(); return attIds.Union(assignIds).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); } /// /// 将用户主档还原到统计月末:优先用 R-004 全量时点版本覆盖;否则按 R-001 回滚「次月 0 点起」的字段审计;最后按 R-002 门店切段覆盖 (切段优先)。 /// public static async Task ApplySalaryUserSnapshotsAsync( ISqlSugarClient db, IReadOnlyList users, DateTime statisticsMonthEnd) { if (users == null || users.Count == 0) { return; } var ids = users.Select(u => u.Id).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); if (ids.Count == 0) { return; } var asOf = statisticsMonthEnd; var versionRows = await db.Queryable() .Where(v => ids.Contains(v.TargetUserId) && v.ValidFrom <= asOf && (v.ValidTo == null || v.ValidTo > asOf)) .ToListAsync(); var versionByUser = versionRows .GroupBy(v => v.TargetUserId) .ToDictionary( g => g.Key, g => g.OrderByDescending(x => x.VersionNo).First()); var nextMonthStart = statisticsMonthEnd.Date.AddDays(1); var audits = await db.Queryable() .Where(a => ids.Contains(a.TargetUserId) && a.OperateTime >= nextMonthStart) .ToListAsync(); var auditsByUser = audits .GroupBy(a => a.TargetUserId) .ToDictionary( g => g.Key, g => g.OrderByDescending(x => x.OperateTime).ThenByDescending(x => x.Id).ToList()); var asOfDate = statisticsMonthEnd.Date; var segments = await db.Queryable() .Where(a => ids.Contains(a.EmployeeId) && a.IsEffective == 1 && a.StartDate <= asOfDate && (a.EndDate == null || a.EndDate >= asOfDate)) .ToListAsync(); var storeByUser = segments .GroupBy(s => s.EmployeeId) .ToDictionary( g => g.Key, g => g.OrderByDescending(x => x.StartDate).First().StoreId); foreach (var user in users) { if (string.IsNullOrEmpty(user?.Id)) { continue; } var usedFullVersion = false; if (versionByUser.TryGetValue(user.Id, out var ver) && !string.IsNullOrWhiteSpace(ver.FullSnapshotJson)) { try { var dto = JsonConvert.DeserializeObject( ver.FullSnapshotJson, VersionSnapshotJsonSettings); if (dto?.User != null && dto.SchemaVersion >= LqUserProfileSnapshotConstants.FullUserSchemaVersion) { OverlayUserFromVersionSnapshot(user, dto.User); usedFullVersion = true; } } catch { // 解析失败则回退审计回滚 } } if (!usedFullVersion && auditsByUser.TryGetValue(user.Id, out var list)) { ApplyAuditRollback(user, list); } if (storeByUser.TryGetValue(user.Id, out var sid) && !string.IsNullOrEmpty(sid)) { user.Mdid = sid; } } } private static void OverlayUserFromVersionSnapshot(UserEntity target, UserEntity source) { if (target == null || source == null) { return; } foreach (var prop in typeof(UserEntity).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanRead || !prop.CanWrite || SkipVersionOverlayProps.Contains(prop.Name)) { continue; } prop.SetValue(target, prop.GetValue(source)); } } private static void ApplyAuditRollback(UserEntity user, List auditsDesc) { foreach (var a in auditsDesc) { if (string.IsNullOrEmpty(a.FieldKey) || !SalaryRelevantAuditFields.Contains(a.FieldKey)) { continue; } var prop = typeof(UserEntity).GetProperty( a.FieldKey, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (prop?.CanWrite != true) { continue; } if (prop.PropertyType == typeof(string)) { prop.SetValue(user, string.IsNullOrEmpty(a.OldValue) ? null : a.OldValue); } } } } }