using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using NCC.Common.Const; using NCC.Common.Core.Manager; using NCC.Dependency; using NCC.Extend.Entitys.lq_user_profile_audit; using NCC.Extend.Interfaces.UserProfileAudit; using Newtonsoft.Json; using NCC.System.Entitys.Permission; using SqlSugar; using UAParser; using Yitter.IdGenerator; namespace NCC.Extend { /// /// 用户主档字段级审计写入实现(R-001) /// public class UserProfileAuditAppenderService : IUserProfileAuditAppender, ITransient { private static readonly JsonSerializerSettings CloneSettings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Include }; private static readonly HashSet ExcludeProperties = new HashSet(StringComparer.OrdinalIgnoreCase) { nameof(UserEntity.Id), nameof(UserEntity.Password), nameof(UserEntity.Secretkey), nameof(UserEntity.QuickQuery), nameof(UserEntity.PropertyJson), nameof(UserEntity.CommonMenu), nameof(UserEntity.PortalId), nameof(UserEntity.ExtensionLong), nameof(UserEntity.ExtensionLong1), nameof(UserEntity.ExtensionStr), nameof(UserEntity.OpenId), nameof(UserEntity.FirstLogTime), nameof(UserEntity.FirstLogIP), nameof(UserEntity.PrevLogTime), nameof(UserEntity.PrevLogIP), nameof(UserEntity.LastLogTime), nameof(UserEntity.LastLogIP), nameof(UserEntity.LogSuccessCount), nameof(UserEntity.LogErrorCount), nameof(UserEntity.CreatorTime), nameof(UserEntity.CreatorUserId), nameof(UserEntity.LastModifyTime), nameof(UserEntity.LastModifyUserId), nameof(UserEntity.DeleteMark), nameof(UserEntity.DeleteTime), nameof(UserEntity.DeleteUserId), nameof(UserEntity.ChangePasswordDate), nameof(UserEntity.IsAdministrator) }; private static readonly Dictionary FieldLabels = new Dictionary(StringComparer.OrdinalIgnoreCase) { [nameof(UserEntity.Account)] = "账号", [nameof(UserEntity.RealName)] = "姓名", [nameof(UserEntity.NickName)] = "昵称", [nameof(UserEntity.HeadIcon)] = "头像", [nameof(UserEntity.Gender)] = "性别", [nameof(UserEntity.Birthday)] = "生日", [nameof(UserEntity.MobilePhone)] = "手机", [nameof(UserEntity.TelePhone)] = "电话", [nameof(UserEntity.Landline)] = "固定电话", [nameof(UserEntity.Email)] = "邮箱", [nameof(UserEntity.Nation)] = "民族", [nameof(UserEntity.NativePlace)] = "籍贯", [nameof(UserEntity.EntryDate)] = "入职日期", [nameof(UserEntity.CertificatesType)] = "证件类型", [nameof(UserEntity.CertificatesNumber)] = "证件号码", [nameof(UserEntity.Education)] = "文化程度", [nameof(UserEntity.UrgentContacts)] = "紧急联系人", [nameof(UserEntity.UrgentTelePhone)] = "紧急电话", [nameof(UserEntity.PostalAddress)] = "通讯地址", [nameof(UserEntity.Signature)] = "自我介绍", [nameof(UserEntity.Language)] = "系统语言", [nameof(UserEntity.Theme)] = "系统样式", [nameof(UserEntity.Description)] = "描述", [nameof(UserEntity.SortCode)] = "排序码", [nameof(UserEntity.ManagerId)] = "主管", [nameof(UserEntity.OrganizeId)] = "组织", [nameof(UserEntity.PositionId)] = "岗位", [nameof(UserEntity.RoleId)] = "角色", [nameof(UserEntity.EnabledMark)] = "启用状态", [nameof(UserEntity.Mdid)] = "门店", [nameof(UserEntity.Zw)] = "职位", [nameof(UserEntity.Fyft)] = "费用分摊", [nameof(UserEntity.Gwfl)] = "岗位分类", [nameof(UserEntity.Gw)] = "岗位", [nameof(UserEntity.PunchAllowedStoreIds)] = "可打卡门店", [nameof(UserEntity.AttendanceRestGroupId)] = "应休分组", [nameof(UserEntity.IsOnJob)] = "是否在职", [nameof(UserEntity.LeaveDate)] = "离职日期" }; private readonly ISqlSugarClient _db; public UserProfileAuditAppenderService(ISqlSugarClient db) { _db = db; } /// public async Task AppendProfileDiffAsync( UserEntity before, UserEntity after, string action, string source, string batchId = null, string remark = null) { if (after == null || string.IsNullOrWhiteSpace(after.Id)) { return; } var (opId, opName, ip, ua) = ResolveOperatorContext(); var time = DateTime.Now; var rows = new List(); foreach (var prop in typeof(UserEntity).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanRead || ExcludeProperties.Contains(prop.Name)) { continue; } var oldVal = before == null ? null : prop.GetValue(before); var newVal = prop.GetValue(after); if (before == null && IsEffectivelyEmpty(newVal)) { continue; } if (ValuesEqual(oldVal, newVal)) { continue; } var label = FieldLabels.TryGetValue(prop.Name, out var lb) ? lb : prop.Name; rows.Add(new LqUserProfileAuditEntity { Id = YitIdHelper.NextId().ToString(), TargetUserId = after.Id, OperatorUserId = opId, OperatorUserName = opName, OperateTime = time, Action = action, BatchId = batchId, FieldKey = prop.Name, FieldLabel = label, OldValue = FormatValue(prop.Name, oldVal), NewValue = FormatValue(prop.Name, newVal), Source = source, ClientIp = ip, UserAgent = ua, Remark = remark }); } if (rows.Count == 0) { return; } await _db.Insertable(rows).ExecuteCommandAsync(); } /// public async Task AppendPasswordAuditAsync(string targetUserId, string action, string source, string remark = null) { if (string.IsNullOrWhiteSpace(targetUserId)) { return; } var (opId, opName, ip, ua) = ResolveOperatorContext(); await _db.Insertable(new LqUserProfileAuditEntity { Id = YitIdHelper.NextId().ToString(), TargetUserId = targetUserId.Trim(), OperatorUserId = opId, OperatorUserName = opName, OperateTime = DateTime.Now, Action = action, FieldKey = "Password", FieldLabel = "密码", OldValue = "[已变更]", NewValue = "[已变更]", Source = source, ClientIp = ip, UserAgent = ua, Remark = remark }).ExecuteCommandAsync(); } /// /// 深拷贝用户实体供事务前保存快照(避免同一引用被后续修改污染)。 /// public static UserEntity CloneUser(UserEntity src) { if (src == null) { return null; } return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(src, CloneSettings), CloneSettings); } private static bool IsEffectivelyEmpty(object v) { if (v == null) { return true; } if (v is string s) { return string.IsNullOrWhiteSpace(s); } return false; } private static bool ValuesEqual(object a, object b) { if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } if (a is DateTime dt1 && b is DateTime dt2) { return dt1 == dt2; } return Equals(a, b); } private static string FormatValue(string propName, object value) { if (value == null) { return string.Empty; } if (value is DateTime dt) { return dt.ToString("yyyy-MM-dd HH:mm:ss"); } var s = Convert.ToString(value); if (string.IsNullOrEmpty(s)) { return string.Empty; } if (propName.Equals(nameof(UserEntity.MobilePhone), StringComparison.OrdinalIgnoreCase) || propName.Equals(nameof(UserEntity.UrgentTelePhone), StringComparison.OrdinalIgnoreCase) || propName.Equals(nameof(UserEntity.TelePhone), StringComparison.OrdinalIgnoreCase)) { return MaskPhone(s); } if (propName.Equals(nameof(UserEntity.CertificatesNumber), StringComparison.OrdinalIgnoreCase)) { return MaskIdCard(s); } return s; } private static string MaskPhone(string s) { if (string.IsNullOrEmpty(s) || s.Length < 7) { return "[已变更]"; } return s.Substring(0, 3) + "****" + s.Substring(s.Length - 4); } private static string MaskIdCard(string s) { if (string.IsNullOrEmpty(s) || s.Length < 8) { return "[已变更]"; } return s.Substring(0, 4) + "********" + s.Substring(s.Length - 4); } /// /// 从当前请求 JWT 解析操作人,避免注入 IUserManager 与 UserRelationService 形成循环依赖。 /// private static (string opId, string opName, string ip, string ua) ResolveOperatorContext() { var ctx = App.HttpContext; string uid = null; var userName = "系统"; if (ctx?.User?.Identity?.IsAuthenticated == true) { uid = ctx.User.FindFirst(ClaimConst.CLAINM_USERID)?.Value?.Trim(); userName = ctx.User.FindFirst(ClaimConst.CLAINM_REALNAME)?.Value?.Trim(); if (string.IsNullOrWhiteSpace(userName)) { userName = ctx.User.FindFirst(ClaimConst.CLAINM_ACCOUNT)?.Value?.Trim(); } if (string.IsNullOrWhiteSpace(userName)) { userName = "系统"; } } string ip = null; string ua = null; if (ctx != null) { ip = ctx.Connection?.RemoteIpAddress?.ToString(); ua = ctx.Request?.Headers["User-Agent"].ToString(); if (ua != null && ua.Length > 500) { ua = ua.Substring(0, 500); } try { var client = Parser.GetDefault().Parse(ctx.Request.Headers["User-Agent"]); if (!string.IsNullOrEmpty(client?.String)) { ua = client.String.Length > 500 ? client.String.Substring(0, 500) : client.String; } } catch { // ignore } } return (uid, userName, ip, ua); } } }