LqSalaryUserSnapshotHelper.cs 7.92 KB
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
{
    /// <summary>
    /// 按统计月末时点还原员工主档(门店、岗位、组织等),用于跨月调动后仍按当月归属算薪。
    /// </summary>
    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<string> SkipVersionOverlayProps =
            new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                nameof(UserEntity.Id),
                nameof(UserEntity.Password),
                nameof(UserEntity.Secretkey),
            };

        private static readonly HashSet<string> SalaryRelevantAuditFields =
            new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                nameof(UserEntity.Mdid),
                nameof(UserEntity.PositionId),
                nameof(UserEntity.Gw),
                nameof(UserEntity.Zw),
                nameof(UserEntity.OrganizeId),
                nameof(UserEntity.Gwfl),
                nameof(UserEntity.RoleId),
            };

        /// <summary>
        /// 当月有考勤打卡记录或有门店归属切段与当月相交的员工,用于扩大「当前岗位已变」仍可能参与当月工资核算的用户集合。
        /// </summary>
        public static async Task<List<string>> GetSalaryCandidateUserIdsAsync(
            ISqlSugarClient db,
            DateTime startDate,
            DateTime endDate,
            int year,
            int month)
        {
            var attIds = await AttendanceWorkdaysFromRecordsHelper.GetUserIdsWithRecordsInMonthAsync(db, year, month);

            var assignIds = await db.Queryable<LqEmployeeStoreAssignmentEntity>()
                .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();
        }

        /// <summary>
        /// 将用户主档还原到统计月末:优先用 R-004 全量时点版本覆盖;否则按 R-001 回滚「次月 0 点起」的字段审计;最后按 R-002 门店切段覆盖 <see cref="UserEntity.Mdid"/>(切段优先)。
        /// </summary>
        public static async Task ApplySalaryUserSnapshotsAsync(
            ISqlSugarClient db,
            IReadOnlyList<UserEntity> 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<LqUserProfileVersionEntity>()
                .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<LqUserProfileAuditEntity>()
                .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<LqEmployeeStoreAssignmentEntity>()
                .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<LqUserProfileFullSnapshotDto>(
                            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<LqUserProfileAuditEntity> 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);
                }
            }
        }
    }
}