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);
}
}
}
}
}