LqUserProfileVersionQueryService.cs 6.48 KB
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using NCC.Common.Enum;
using NCC.Common.Core.Manager;
using NCC.Common.Filter;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.lq_user_profile_version;
using NCC.Extend.Interfaces.UserProfileVersion;
using NCC.Extend.Models;
using NCC.FriendlyException;
using NCC.System.Entitys.Permission;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using SqlSugar;

namespace NCC.Extend
{
    /// <summary>
    /// 用户主档时点全量版本查询与还原(R-004)。
    /// </summary>
    [ApiDescriptionSettings(Tag = "绿纤用户主档版本", Name = "LqUserProfileVersion", Order = 201)]
    [Route("api/Extend/LqUserProfileVersion")]
    public class LqUserProfileVersionQueryService : IDynamicApiController, ITransient
    {
        private readonly ISqlSugarClient _db;
        private readonly IUserManager _userManager;
        private readonly IUserProfileVersionAppender _userProfileVersionAppender;

        public LqUserProfileVersionQueryService(
            ISqlSugarClient db,
            IUserManager userManager,
            IUserProfileVersionAppender userProfileVersionAppender)
        {
            _db = db;
            _userManager = userManager;
            _userProfileVersionAppender = userProfileVersionAppender;
        }

        /// <summary>
        /// 按用户分页查询全量主档版本(新到旧);权限与字段审计列表一致。
        /// </summary>
        [HttpGet("logs")]
        public async Task<dynamic> GetVersionLogs([FromQuery] string targetUserId, [FromQuery] PageInputBase page)
        {
            if (string.IsNullOrWhiteSpace(targetUserId))
            {
                throw NCCException.Oh("targetUserId 不能为空");
            }

            page ??= new PageInputBase();
            var userInfo = await _userManager.GetUserInfo();
            var target = await _db.Queryable<UserEntity>()
                .Where(x => x.Id == targetUserId && x.DeleteMark == null)
                .FirstAsync();
            if (target == null)
            {
                throw NCCException.Oh("目标用户不存在");
            }

            if (!userInfo.isAdministrator
                && !userInfo.dataScope.Any(it => it.organizeId == target.OrganizeId && it.Edit == true))
            {
                throw NCCException.Oh(ErrorCode.D1013);
            }

            var q = _db.Queryable<LqUserProfileVersionEntity>()
                .Where(x => x.TargetUserId == targetUserId)
                .OrderBy(x => x.VersionNo, OrderByType.Desc);

            var data = await q.ToPagedListAsync(page.currentPage, page.pageSize);
            return PageResult<LqUserProfileVersionEntity>.SqlSugarPageResult(data);
        }

        /// <summary>
        /// 将 BASE_USER 还原为指定版本快照(密码/密钥保留当前库内值);成功后追加新版本记录。
        /// </summary>
        [HttpPost("restore")]
        public async Task RestoreToVersion([FromBody] LqUserProfileVersionRestoreInput input)
        {
            if (string.IsNullOrWhiteSpace(input?.VersionId))
            {
                throw NCCException.Oh("versionId 不能为空");
            }

            var userInfo = await _userManager.GetUserInfo();

            var verRows = await _db.Queryable<LqUserProfileVersionEntity>()
                .Where(x => x.Id == input.VersionId)
                .Take(1)
                .ToListAsync();
            var ver = verRows.FirstOrDefault();
            if (ver == null)
            {
                throw NCCException.Oh("版本不存在");
            }

            LqUserProfileFullSnapshotDto dto;
            try
            {
                dto = JsonConvert.DeserializeObject<LqUserProfileFullSnapshotDto>(ver.FullSnapshotJson);
            }
            catch
            {
                throw NCCException.Oh("快照 JSON 解析失败");
            }

            if (dto == null
                || dto.SchemaVersion < LqUserProfileSnapshotConstants.FullUserSchemaVersion
                || dto.User == null)
            {
                throw NCCException.Oh("该版本快照格式过旧,无法一键还原,请人工对照 JSON 修改主档");
            }

            var currentRows = await _db.Queryable<UserEntity>()
                .Where(x => x.Id == ver.TargetUserId)
                .Take(1)
                .ToListAsync();
            var current = currentRows.FirstOrDefault();
            if (current == null)
            {
                throw NCCException.Oh("用户不存在");
            }

            if (!userInfo.isAdministrator
                && !userInfo.dataScope.Any(it => it.organizeId == current.OrganizeId && it.Edit == true))
            {
                throw NCCException.Oh(ErrorCode.D1013);
            }

            CopyUserSnapshotOntoRow(current, dto.User);

            try
            {
                _db.Ado.BeginTran();
                var ok = await _db.Updateable(current).ExecuteCommandAsync();
                if (ok <= 0)
                {
                    throw NCCException.Oh("还原写入失败");
                }

                await _userProfileVersionAppender.AfterUserProfilePersistedAsync(ver.TargetUserId, "RESTORE_FROM_VERSION", true);
                _db.Ado.CommitTran();
            }
            catch
            {
                _db.Ado.RollbackTran();
                throw;
            }
        }

        /// <summary>
        /// 将快照中的业务字段覆盖到当前行,保留 Id、Password、Secretkey。
        /// </summary>
        internal static void CopyUserSnapshotOntoRow(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)
                {
                    continue;
                }

                var n = prop.Name;
                if (string.Equals(n, nameof(UserEntity.Id), StringComparison.OrdinalIgnoreCase)
                    || string.Equals(n, nameof(UserEntity.Password), StringComparison.OrdinalIgnoreCase)
                    || string.Equals(n, nameof(UserEntity.Secretkey), StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                prop.SetValue(target, prop.GetValue(source));
            }
        }
    }
}