瀏覽代碼

签证流程步骤记录日志 -> 建表、建实体内、逻辑代码编码编写

Lyyyi 1 天之前
父節點
當前提交
7a59a85db7

+ 3 - 2
OASystem/EntitySync/Program.cs

@@ -173,7 +173,8 @@ db.CodeFirst.SetStringDefaultLength(50).BackupTable().InitTables(new Type[]
     //typeof(Grp_GamesBudgetMaster‌),//世运会成本预算明细 
     //typeof(Res_VisaFeeStandard),//签证费用标准 
     //typeof(Res_VisaFeeStandardDetails),//签证费用标准详情 
-    typeof(Grp_ProcessOverview),//团组流程总览表
-    typeof(Grp_ProcessNode),//流程节点
+    //typeof(Grp_ProcessOverview),//团组流程总览表
+    //typeof(Grp_ProcessNode),//流程节点
+    typeof(Grp_VisaProcessSteps_Log),//流程节点
 });
 Console.WriteLine("数据库结构同步完成!");

+ 53 - 15
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -4,10 +4,12 @@ using Aspose.Words.Drawing;
 using Aspose.Words.Tables;
 using DiffMatchPatch;
 using Dm.util;
+using Humanizer;
 using Microsoft.AspNetCore.SignalR;
 using NPOI.SS.Formula.Functions;
 using NPOI.SS.UserModel;
 using NPOI.SS.Util;
+using NPOI.Util;
 using NPOI.XSSF.UserModel;
 using OASystem.API.Middlewares;
 using OASystem.API.OAMethodLib;
@@ -33,6 +35,7 @@ using OASystem.Domain.ViewModels.CRM;
 using OASystem.Domain.ViewModels.Financial;
 using OASystem.Domain.ViewModels.Groups;
 using OASystem.Domain.ViewModels.OCR;
+using OASystem.Domain.ViewModels.SmallFun;
 using OASystem.Infrastructure.Repositories.CRM;
 using OASystem.Infrastructure.Repositories.Financial;
 using OASystem.Infrastructure.Repositories.Groups;
@@ -28403,6 +28406,8 @@ ORDER BY
             var stepValid = await _sqlSugar.Queryable<Grp_VisaProcessSteps>().Where(x => x.Id == dto.StepId && x.IsDel == 0).FirstAsync();
             if (stepValid == null) return Ok(JsonView(false, "签证流程ID无效。"));
 
+            var before = stepValid;
+
             var userValid = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.CurrUserId && x.IsDel == 0).AnyAsync();
             if (!userValid) return Ok(JsonView(false, "用户ID无效。"));
 
@@ -28461,25 +28466,20 @@ ORDER BY
             }
 
             //数据库存储
-            var stepInfo = new Grp_VisaProcessSteps
-            {
-                Id = dto.StepId,
-                TypedFileNameValue = filePaths,
-                LastUpdateUserId = dto.CurrUserId
-            };
+            stepValid.TypedFileNameValue = filePaths;
+            stepValid.LastUpdateUserId = dto.CurrUserId;
+            stepValid.LastUpdateTime = DateTime.Now;
 
-            var res = await _sqlSugar.Updateable<Grp_VisaProcessSteps>()
-                .SetColumns(s => new Grp_VisaProcessSteps
-                {
-                    AttachUrl = stepInfo.AttachUrl,
-                    LastUpdateUserId = dto.CurrUserId,
-                    LastUpdateTime = DateTime.Now
-                })
+            var res = await _sqlSugar.Updateable(stepValid)
+                .UpdateColumns(it => new { it.TypedFileNameValue, it.LastUpdateUserId, it.LastUpdateTime })
                 .Where(s => s.Id == dto.StepId && s.IsDel == 0)
                 .ExecuteCommandAsync();
 
             if (res < 1) Ok(JsonView(false, "文件上传失败。"));
 
+            //记录上传文件
+            await _visaProcessRep.LogOperationAsync(before, stepValid, "Upload", dto.CurrUserId);
+
             return Ok(JsonView(true));
         }
 
@@ -28488,8 +28488,11 @@ ORDER BY
         /// </summary>
         /// <returns></returns>
         [HttpGet]
-        public async Task<IActionResult> VisaProcessDownload(int groupId)
+        public async Task<IActionResult> VisaProcessDownload(int groupId,int currUserId)
         {
+            var userValid = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == currUserId && x.IsDel == 0).AnyAsync();
+            if (!userValid) return Ok(JsonView(false, "用户ID无效。"));
+
             //验证根目录
             string rootRrl = AppSettingsHelper.Get("OfficeBaseUrl");
             string rootPath = AppSettingsHelper.Get("OfficeTempBasePath");
@@ -28647,7 +28650,7 @@ WHERE
             ZipFile.CreateFromDirectory(sourceDirectory, zipFilePath, CompressionLevel.Fastest, false);
 
             // 等待并验证
-            await Task.Delay(300); // 给系统时间完成文件操作
+            await Task.Delay(100); // 给系统时间完成文件操作
 
             if (System.IO.File.Exists(zipFilePath))
             {
@@ -28655,12 +28658,47 @@ WHERE
 
                 var url = $"{rootRrl}GrpFile/VisaProcessFiles/{groupName}/{dirName}.zip";
 
+                //记录下载日志
+                await _visaProcessRep.LogOperationAsync(null, null, "Download", currUserId);
+
                 return Ok(JsonView(url));
             }
 
             return Ok(JsonView(false));
         }
 
+        /// <summary>
+        /// 团组签证流程 - 根据步骤记录ID获取该步骤的所有操作日志
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet]
+        public async Task<IActionResult> VisaProcessLogByStepId(int stepId)
+        {
+            //参数验证
+            var stepValid = await _sqlSugar.Queryable<Grp_VisaProcessSteps>().Where(x => x.Id == stepId && x.IsDel == 0).FirstAsync();
+            if (stepValid == null) return Ok(JsonView(false, "签证流程ID无效。"));
+
+            var logs = await _visaProcessRep.GetStepLogsAsync(stepId);
+
+            return Ok(JsonView(logs));
+        }
+
+        /// <summary>
+        /// 团组签证流程 - 根据团组ID获取该团组下所有步骤的操作日志
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet]
+        public async Task<IActionResult> VisaProcessLogByGroupId(int groupId)
+        {
+            //参数验证
+            var stepValid = await _sqlSugar.Queryable<Grp_VisaProcessSteps>().Where(x => x.GroupId == groupId && x.IsDel == 0).FirstAsync();
+            if (stepValid == null) return Ok(JsonView(false, "团组ID无效。"));
+
+            var logs = await _visaProcessRep.GetGroupStepLogsAsync(groupId);
+
+            return Ok(JsonView(logs));
+        }
+
         #endregion
 
         #region 团组总览进程

+ 110 - 1
OASystem/OASystem.Domain/Entities/Groups/Grp_VisaProcessSteps.cs

@@ -221,7 +221,6 @@ namespace OASystem.Domain.Entities.Groups
 
         #endregion
 
-
         public Grp_VisaProcessSteps() { }
 
         /// <summary>
@@ -244,4 +243,114 @@ namespace OASystem.Domain.Entities.Groups
             };
         }
     }
+
+    /// <summary>
+    /// 签证流程步骤操作日志
+    /// </summary>
+    [SugarTable(tableName: "Grp_VisaProcessSteps_Log", TableDescription = "签证流程步骤操作日志")]
+    public class Grp_VisaProcessSteps_Log : EntityBase
+    {
+        /// <summary>
+        /// 步骤记录ID
+        /// </summary>
+        [SugarColumn(ColumnName = "StepId", ColumnDescription = "步骤记录ID", IsNullable = false, ColumnDataType = "int")]
+        public int StepId { get; set; }
+
+        /// <summary>
+        /// 团组Id
+        /// </summary>
+        [SugarColumn(ColumnName = "GroupId", ColumnDescription = "团组Id", IsNullable = true, ColumnDataType = "int")]
+        public int GroupId { get; set; }
+
+        /// <summary>
+        /// 步骤
+        /// </summary>
+        [SugarColumn(ColumnName = "Step", ColumnDescription = "步骤", IsNullable = true, ColumnDataType = "int")]
+        public int Step { get; set; }
+
+        /// <summary>
+        /// 操作类型(Create, Update, Delete, Complete等)
+        /// </summary>
+        [SugarColumn(ColumnName = "OperationType", ColumnDescription = "操作类型", IsNullable = false, ColumnDataType = "varchar(50)")]
+        public string OperationType { get; set; }
+
+        /// <summary>
+        /// 操作描述
+        /// </summary>
+        [SugarColumn(ColumnName = "OperationDescription", ColumnDescription = "操作描述", IsNullable = true, ColumnDataType = "varchar(500)")]
+        public string OperationDescription { get; set; }
+
+        /// <summary>
+        /// 变更前的数据(JSON格式)
+        /// </summary>
+        [SugarColumn(ColumnName = "BeforeData", ColumnDescription = "变更前数据", IsNullable = true, ColumnDataType = "nvarchar(max)")]
+        public string BeforeData { get; set; }
+
+        /// <summary>
+        /// 变更后的数据(JSON格式)
+        /// </summary>
+        [SugarColumn(ColumnName = "AfterData", ColumnDescription = "变更后数据", IsNullable = true, ColumnDataType = "nvarchar(max)")]
+        public string AfterData { get; set; }
+
+        /// <summary>
+        /// 变更的字段列表(逗号分隔)
+        /// </summary>
+        [SugarColumn(ColumnName = "ChangedFields", ColumnDescription = "变更字段", IsNullable = true, ColumnDataType = "varchar(500)")]
+        public string ChangedFields { get; set; }
+
+    }
+
+    public class VisaProcessStepsLogView
+    {
+        /// <summary>
+        /// 步骤记录ID
+        /// </summary>
+        public int StepId { get; set; }
+
+        /// <summary>
+        /// 团组Id
+        /// </summary>
+        public int GroupId { get; set; }
+
+        /// <summary>
+        /// 步骤
+        /// </summary>
+        public int Step { get; set; }
+
+        /// <summary>
+        /// 操作类型(Create, Update, Delete, Complete等)
+        /// </summary>
+        public string OperationType { get; set; }
+
+        /// <summary>
+        /// 操作描述
+        /// </summary>
+        public string OperationDescription { get; set; }
+
+        ///// <summary>
+        ///// 变更前的数据(JSON格式)
+        ///// </summary>
+        //public string BeforeData { get; set; }
+
+        ///// <summary>
+        ///// 变更后的数据(JSON格式)
+        ///// </summary>
+        //public string AfterData { get; set; }
+
+        ///// <summary>
+        ///// 变更的字段列表(逗号分隔)
+        ///// </summary>
+        //public string ChangedFields { get; set; }
+
+        /// <summary>
+        /// 操作人姓名
+        /// </summary>
+        public string Operator { get; set; }
+
+        /// <summary>
+        /// 操作时间
+        /// </summary>
+        public DateTime OperationTime { get; set; }
+    }
+
 }

+ 343 - 15
OASystem/OASystem.Infrastructure/Repositories/Groups/VisaProcessRepository.cs

@@ -1,9 +1,14 @@
 using AutoMapper;
+using NPOI.Util;
 using OASystem.Domain;
 using OASystem.Domain.Dtos.Groups;
+using OASystem.Domain.Dtos.Task;
 using OASystem.Domain.Entities.Groups;
 using OASystem.Domain.ViewModels.Groups;
+using OASystem.Domain.ViewModels.SmallFun;
 using OASystem.Infrastructure.Tools;
+using System.Reflection;
+using System.Text.Json.Serialization;
 
 namespace OASystem.Infrastructure.Repositories.Groups
 {
@@ -12,7 +17,6 @@ namespace OASystem.Infrastructure.Repositories.Groups
     /// </summary>
     public class VisaProcessRepository : BaseRepository<Grp_VisaProcessSteps, Grp_VisaProcessSteps>
     {
-
         private readonly IMapper _mapper;
 
         public VisaProcessRepository(SqlSugarClient sqlSugar, IMapper mapper)
@@ -54,6 +58,12 @@ namespace OASystem.Infrastructure.Repositories.Groups
 
             if (add < 1) return new Result(400, "签证流程步骤创建失败。");
 
+            // 记录创建日志
+            foreach (var step in steps)
+            {
+                await LogOperationAsync(null, step, "Create", createUderId);
+            }
+
             return new Result(200,"Success");
         }
 
@@ -222,21 +232,23 @@ namespace OASystem.Infrastructure.Repositories.Groups
         /// <returns></returns>
         public async Task<Result> Update(Grp_VisaProcessSteps info)
         {
-            var update = await _sqlSugar.Updateable<Grp_VisaProcessSteps>()
-                .SetColumns(s => new Grp_VisaProcessSteps
-                {
-                    StoreVal = info.StoreVal,
-                    //AttachUrl = info.AttachUrl,
-                    //IsCompleted = info.IsCompleted,
-                    LastUpdateUserId = info.LastUpdateUserId,
-                    LastUpdateTime = DateTime.Now,
-                    Remark = info.Remark
-                })
+            var step = await _sqlSugar.Queryable<Grp_VisaProcessSteps>().FirstAsync(x => x.Id == info.Id);
+            var before = ManualClone(step);
+
+            step.StoreVal = info.StoreVal;
+            step.LastUpdateUserId = info.LastUpdateUserId;
+            step.LastUpdateTime = DateTime.Now;
+            step.Remark = info.Remark;
+
+            var update = await _sqlSugar.Updateable<Grp_VisaProcessSteps>(step)
                 .Where(s => s.Id == info.Id && s.IsDel == 0)
                 .ExecuteCommandAsync();
 
             if (update < 1) return new Result(400, "更新失败!");
 
+            // 记录更新日志
+            await LogOperationAsync(before, step, "Update", info.LastUpdateUserId);
+
             return new Result(200, "Success");
         }
 
@@ -266,6 +278,12 @@ namespace OASystem.Infrastructure.Repositories.Groups
                 .FirstAsync();
             if (invertedInfo == null) return new Result(400, "倒推表信息不存在。");
 
+            //步骤更改前的值
+            var beforeStep1 = ManualClone(visaSteps.FirstOrDefault(x => x.Step == 1));
+            var beforeStep2 = ManualClone(visaSteps.FirstOrDefault(x => x.Step == 2));
+            var beforeStep3 = ManualClone(visaSteps.FirstOrDefault(x => x.Step == 3));
+            var beforeStep4 = ManualClone(visaSteps.FirstOrDefault(x => x.Step == 4));
+
             //设置操作人和时间
             foreach (var item in visaSteps)
             {
@@ -287,14 +305,24 @@ namespace OASystem.Infrastructure.Repositories.Groups
             //step=4 预计出签时间
             visaSteps.FirstOrDefault(x => x.Step == 4).TypedValue = invertedInfo.IssueVisaDt;
 
-            
-
             var update = await _sqlSugar.Updateable(visaSteps)
                 .UpdateColumns(it => new { it.StoreVal, it.LastUpdateUserId,it.LastUpdateTime })
                 .ExecuteCommandAsync();
 
             if (update < 1) return new Result(400, "更新失败!");
 
+            //更改后的值
+            var afterStep1 = visaSteps.FirstOrDefault(x => x.Step == 1);
+            var afterStep2 = visaSteps.FirstOrDefault(x => x.Step == 2);
+            var afterStep3 = visaSteps.FirstOrDefault(x => x.Step == 3);
+            var afterStep4 = visaSteps.FirstOrDefault(x => x.Step == 4);
+
+            // 记录更新日志
+            await LogOperationAsync(beforeStep1, afterStep1, "Update", currUserId);
+            await LogOperationAsync(beforeStep2, afterStep2, "Update", currUserId);
+            await LogOperationAsync(beforeStep3, afterStep3, "Update", currUserId);
+            await LogOperationAsync(beforeStep4, afterStep4, "Update", currUserId);
+
             return new Result(200, "Success");
         }
 
@@ -309,9 +337,17 @@ namespace OASystem.Infrastructure.Repositories.Groups
         {
             //步骤信息验证
             var stepInfo = await _sqlSugar.Queryable<Grp_VisaProcessSteps>()
-                .Where(s => s.Id == id && s.IsDel == 0 && s.IsCompleted == isCompleted)
+                .Where(s => s.Id == id && s.IsDel == 0)
                 .FirstAsync();
-            if (stepInfo != null) return new Result(400, "步骤信息已完成,无法重复设置。");
+
+            if (stepInfo == null) return new Result(400, "步骤信息不存在,无法设置。");
+
+            if (stepInfo.IsCompleted) return new Result(400, "步骤信息已完成,无法重复设置。");
+
+            var before = ManualClone(stepInfo);
+            stepInfo.IsCompleted = isCompleted;
+            stepInfo.LastUpdateUserId = currUserId;
+            stepInfo.LastUpdateTime = DateTime.Now;
 
             var update = await _sqlSugar.Updateable<Grp_VisaProcessSteps>()
                 .SetColumns(s => new Grp_VisaProcessSteps
@@ -325,9 +361,301 @@ namespace OASystem.Infrastructure.Repositories.Groups
 
             if (update < 1) return new Result(400, "状态设置失败!");
 
+            // 记录更新日志
+            await LogOperationAsync(before, stepInfo, "Update", currUserId);
 
             return new Result(200, "Success");
         }
 
+
+        #region 操作日志
+
+        /// <summary>
+        /// 记录签证流程步骤的操作日志
+        /// </summary>
+        /// <param name="before">操作前的步骤数据实体</param>
+        /// <param name="after">操作后的步骤数据实体</param>
+        /// <param name="opType">操作类型(Create-创建, Update-更新, Delete-删除, Complete-完成, Upload-上传附件, Download-下载文件)</param>
+        /// <param name="operatorId">操作人用户ID</param>
+        /// <returns>表示异步操作的任务</returns>
+        /// <remarks>此方法会自动比较前后数据的差异,生成变更字段列表和操作描述</remarks>
+        public async Task LogOperationAsync(Grp_VisaProcessSteps before, Grp_VisaProcessSteps after,string opType, int operatorId)
+        {
+            // 合并基础排除字段和额外排除字段
+            var allExcludedFields = GetDefaultExcludedFields();
+            var changedFields = GetChangeDetails(before, after, allExcludedFields);
+
+            var log = new Grp_VisaProcessSteps_Log
+            {
+                StepId = after?.Id ?? before?.Id ?? 0,
+                GroupId = after?.GroupId ?? before?.GroupId ?? 0,
+                Step = after?.Step ?? before?.Step ?? 0,
+                OperationType = opType,
+                OperationDescription = GenerateOpDesc(opType, before, after, changedFields),
+                BeforeData = before != null ? JsonSerializer.Serialize(before, new JsonSerializerOptions
+                {
+                    ReferenceHandler = ReferenceHandler.IgnoreCycles,
+                    WriteIndented = false
+                }) : null,
+                AfterData = after != null ? JsonSerializer.Serialize(after, new JsonSerializerOptions
+                {
+                    ReferenceHandler = ReferenceHandler.IgnoreCycles,
+                    WriteIndented = false
+                }) : null,
+                ChangedFields = string.Join(",", changedFields),
+                CreateUserId = operatorId,
+            };
+
+            await _sqlSugar.Insertable(log).ExecuteCommandAsync();
+        }
+
+        /// <summary>
+        /// 获取字段变更详情
+        /// </summary>
+        /// <param name="before">变更前</param>
+        /// <param name="after">变更后</param>
+        /// <param name="exclFields">排除字段</param>
+        /// <returns>变更详情列表</returns>
+        private List<FieldChangeDetail> GetChangeDetails(Grp_VisaProcessSteps before, Grp_VisaProcessSteps after, List<string> exclFields = null)
+        {
+            var changeDetails = new List<FieldChangeDetail>();
+            var defaultExclFields = GetDefaultExcludedFields();
+
+            // 合并排除字段
+            var allExclFields = defaultExclFields;
+            if (exclFields != null && exclFields.Any())
+            {
+                allExclFields = defaultExclFields.Union(exclFields).ToList();
+            }
+
+            if (before == null || after == null) return changeDetails;
+
+            var properties = typeof(Grp_VisaProcessSteps).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+            foreach (var prop in properties)
+            {
+                // 跳过排除的字段
+                if (allExclFields.Contains(prop.Name))
+                {
+                    continue;
+                }
+
+                var beforeValue = prop.GetValue(before);
+                var afterValue = prop.GetValue(after);
+
+                // 处理字符串类型的特殊比较(忽略前后空格)
+                if (prop.PropertyType == typeof(string))
+                {
+                    var beforeString = beforeValue?.ToString()?.Trim();
+                    var afterString = afterValue?.ToString()?.Trim();
+
+                    if (beforeString != afterString)
+                    {
+                        changeDetails.Add(new FieldChangeDetail
+                        {
+                            FieldName = prop.Name,
+                            BeforeValue = FormatValue(beforeString),
+                            AfterValue = FormatValue(afterString)
+                        });
+                    }
+                }
+                else
+                {
+                    // 其他类型使用默认比较
+                    if (!Equals(beforeValue, afterValue))
+                    {
+                        changeDetails.Add(new FieldChangeDetail
+                        {
+                            FieldName = prop.Name,
+                            BeforeValue = FormatValue(beforeValue),
+                            AfterValue = FormatValue(afterValue)
+                        });
+                    }
+                }
+            }
+
+            return changeDetails;
+        }
+
+        /// <summary>
+        /// 格式化值显示
+        /// </summary>
+        /// <param name="value">原始值</param>
+        /// <returns>格式化后的值</returns>
+        private static string FormatValue(object value)
+        {
+            if (value == null) return "null";
+
+            var strValue = value.ToString();
+
+            // 处理空字符串
+            if (string.IsNullOrEmpty(strValue)) return "空";
+
+            // 处理布尔值
+            if (value is bool boolValue) return boolValue ? "是" : "否";
+
+            // 处理日期时间
+            if (value is DateTime dateTimeValue) return dateTimeValue.ToString("yyyy-MM-dd HH:mm");
+
+            // 处理长文本截断
+            if (strValue.Length > 50) return strValue.Substring(0, 47) + "...";
+
+            return strValue;
+        }
+
+        /// <summary>
+        /// 生成操作描述
+        /// </summary>
+        /// <param name="opType">操作类型</param>
+        /// <param name="before">操作前</param>
+        /// <param name="after">操作后</param>
+        /// <param name="chgDetails">变更详情</param>
+        /// <returns>操作描述</returns>
+        private static string GenerateOpDesc(string opType, Grp_VisaProcessSteps before,
+            Grp_VisaProcessSteps after, List<FieldChangeDetail> chgDetails)
+        {
+            if (!chgDetails.Any())
+            {
+                return opType switch
+                {
+                    "Create" => $"创建步骤:团组{after.GroupId}-步骤{after.Step}",
+                    "Update" => $"更新步骤:无变更",
+                    "Complete" => $"完成步骤:团组{after.GroupId}-步骤{after.Step}",
+                    "Uncomplete" => $"取消完成:团组{after.GroupId}-步骤{after.Step}",
+                    "Delete" => $"删除步骤:团组{before.GroupId}-步骤{before.Step}",
+                    "Upload" => $"上传附件:团组{after.GroupId}-步骤{after.Step}",
+                    _ => $"{opType}:团组{after?.GroupId ?? before?.GroupId}-步骤{after?.Step ?? before?.Step}"
+                };
+            }
+
+            var changeDesc = string.Join("; ", chgDetails.Select(x =>
+                $"{x.FieldName} ({x.BeforeValue} -> {x.AfterValue})"));
+
+            return opType switch
+            {
+                "Create" => $"创建步骤:团组{after.GroupId}-步骤{after.Step}",
+                "Update" => $"更新步骤:{changeDesc}",
+                "Complete" => $"完成步骤:团组{after.GroupId}-步骤{after.Step}",
+                "Uncomplete" => $"取消完成:团组{after.GroupId}-步骤{after.Step}",
+                "Delete" => $"删除步骤:团组{before.GroupId}-步骤{before.Step}",
+                "Upload" => $"上传附件:团组{after.GroupId}-步骤{after.Step}",
+                _ => $"{opType}:{changeDesc}"
+            };
+        }
+
+        /// <summary>
+        /// 字段变更详情类
+        /// </summary>
+        private class FieldChangeDetail
+        {
+            public string FieldName { get; set; }
+            public string BeforeValue { get; set; }
+            public string AfterValue { get; set; }
+        }
+
+        /// <summary>
+        /// 获取默认排除的字段列表(包含系统字段和忽略字段)
+        /// </summary>
+        /// <returns>默认排除的字段列表</returns>
+        private static List<string> GetDefaultExcludedFields()
+        {
+            var defaultExcludedFields = new List<string>
+            {
+                nameof(Grp_VisaProcessSteps.Id),
+                nameof(Grp_VisaProcessSteps.CreateTime),
+                nameof(Grp_VisaProcessSteps.CreateUserId),
+                //nameof(Grp_VisaProcessSteps.LastUpdateTime),
+                //nameof(Grp_VisaProcessSteps.LastUpdateUserId),
+
+                // 计算字段(原本标记为 [SugarColumn(IsIgnore = true)] 的字段)
+                "TypedFileNameValue",
+                "TypedValue",
+                "StringValue",
+                "IntValue",
+                "DecimalValue",
+                "BooleanValue",
+                "DateTimeValue"
+            };
+
+            return defaultExcludedFields.Distinct().ToList();
+        }
+
+        /// <summary>
+        /// 手动创建步骤实体的副本
+        /// </summary>
+        /// <param name="source">源步骤实体</param>
+        /// <returns>新的步骤实体副本</returns>
+        private Grp_VisaProcessSteps ManualClone(Grp_VisaProcessSteps source)
+        {
+            if (source == null) return null;
+
+            return new Grp_VisaProcessSteps
+            {
+                Id = source.Id,
+                GroupId = source.GroupId,
+                Step = source.Step,
+                DataType = source.DataType,
+                StoreVal = source.StoreVal,
+                AttachUrl = source.AttachUrl,
+                IsCompleted = source.IsCompleted,
+                LastUpdateUserId = source.LastUpdateUserId,
+                LastUpdateTime = source.LastUpdateTime,
+                CreateUserId = source.CreateUserId,
+                CreateTime = source.CreateTime
+                // 注意:这里不拷贝计算属性(TypedValue 等),因为它们会被重新计算
+            };
+        }
+
+        /// <summary>
+        /// 根据步骤记录ID获取该步骤的所有操作日志
+        /// </summary>
+        /// <param name="stepId">步骤记录的主键ID</param>
+        /// <returns>步骤操作日志列表,按操作时间倒序排列</returns>
+        public async Task<List<VisaProcessStepsLogView>> GetStepLogsAsync(int stepId)
+        {
+            return await _sqlSugar.Queryable<Grp_VisaProcessSteps_Log>()
+                .LeftJoin<Sys_Users>((x, y) => x.CreateUserId == y.Id)
+                .Where((x, y) => x.StepId == stepId)
+                .Select((x, y) => new VisaProcessStepsLogView
+                {
+                    StepId = x.StepId,
+                    GroupId = x.GroupId,
+                    Step = x.Step,
+                    OperationType = x.OperationType,
+                    OperationDescription = x.OperationDescription,
+                    Operator = y.CnName,
+                    OperationTime = x.CreateTime,
+                })
+                .OrderByDescending(x => x.OperationTime)
+                .ToListAsync();
+        }
+
+        /// <summary>
+        /// 根据团组ID获取该团组下所有步骤的操作日志
+        /// </summary>
+        /// <param name="groupId">团组ID</param>
+        /// <returns>团组步骤操作日志列表,按操作时间倒序排列</returns>
+        public async Task<List<VisaProcessStepsLogView>> GetGroupStepLogsAsync(int groupId)
+        {
+            return await _sqlSugar.Queryable<Grp_VisaProcessSteps_Log>()
+                .LeftJoin<Sys_Users>((x, y) => x.CreateUserId == y.Id)
+                .Where((x, y) => x.GroupId == groupId)
+                .Select((x, y) => new VisaProcessStepsLogView
+                {
+                    StepId = x.StepId,
+                    GroupId = x.GroupId,
+                    Step = x.Step,
+                    OperationType = x.OperationType,
+                    OperationDescription = x.OperationDescription,
+                    Operator = y.CnName,
+                    OperationTime = x.CreateTime,
+                })
+                .OrderByDescending(x => x.OperationTime)
+                .ToListAsync();
+        }
+
+
+        #endregion
+
     }
 }