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