Browse Source

Merge branch 'develop' of http://132.232.92.186:3000/XinXiBu/OA2023 into develop

yuanrf 1 day ago
parent
commit
9d4a83a3c5

+ 5 - 3
OASystem/EntitySync/Program.cs

@@ -1,4 +1,5 @@
-using OASystem.Domain.Entities.Resource;
+using OASystem.Domain.Entities.Groups;
+using OASystem.Domain.Entities.Resource;
 using SqlSugar;
 
 var db = new SqlSugarClient(new ConnectionConfig()
@@ -170,7 +171,8 @@ db.CodeFirst.SetStringDefaultLength(50).BackupTable().InitTables(new Type[]
     //typeof(Sys_AuditTemplateNodeUser),//审核模板节点人员表 
     //typeof(Fin_ApplicationLinkGoods),//日服申请单物品关联表 
     //typeof(Grp_GamesBudgetMaster‌),//世运会成本预算明细 
-    typeof(Res_VisaFeeStandard),//签证费用标准 
-    typeof(Res_VisaFeeStandardDetails),//签证费用标准详情 
+    //typeof(Res_VisaFeeStandard),//签证费用标准 
+    //typeof(Res_VisaFeeStandardDetails),//签证费用标准详情 
+    typeof(Grp_VisaProcessSteps),//签证流程管控步骤
 });
 Console.WriteLine("数据库结构同步完成!");

+ 240 - 5
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -114,6 +114,7 @@ namespace OASystem.API.Controllers
         private readonly RestaurantRepository _restaurantRep;
         private readonly EnterExitCostQuoteRepository _enterExitCostQuoteRep;
         private readonly GroupOrderPreInfoRepository _grpOrderPreInfoRep;
+        private readonly VisaProcessRepository _visaProcessRep;
 
         private readonly IDeepSeekService _deepSeekService;
 
@@ -163,6 +164,7 @@ namespace OASystem.API.Controllers
         /// <param name="restaurantRep"></param>
         /// <param name="enterExitCostQuoteRep"></param>
         /// <param name="grpOrderPreInfoRep"></param>
+        /// <param name="visaProcessRep"></param>
         public GroupsController(
             ILogger<GroupsController> logger,
             ITextFileLogger eec_textLogger,
@@ -207,6 +209,7 @@ namespace OASystem.API.Controllers
             RestaurantRepository restaurantRep,
             EnterExitCostQuoteRepository enterExitCostQuoteRep,
             GroupOrderPreInfoRepository grpOrderPreInfoRep,
+            VisaProcessRepository visaProcessRep,
             IDeepSeekService deepSeekService
             )
         {
@@ -269,6 +272,7 @@ namespace OASystem.API.Controllers
             _enterExitCostQuoteRep = enterExitCostQuoteRep;
             _grpOrderPreInfoRep = grpOrderPreInfoRep;
             _deepSeekService = deepSeekService;
+            _visaProcessRep = visaProcessRep;
         }
 
         #region 流程管控
@@ -1229,6 +1233,9 @@ namespace OASystem.API.Controllers
                     //默认创建倒推表
                     await _invertedListRep._Create(dto.UserId, diId);
 
+                    //默认创建签证流程
+                    await _visaProcessRep.Create(dto.UserId, diId);
+
                 }
                 else if (dto.Status == 2)
                 {
@@ -1246,8 +1253,6 @@ namespace OASystem.API.Controllers
                 }
                 #endregion
 
-
-
                 #endregion
 
                 #region 团组操作默认添加/修改收款账单
@@ -1300,9 +1305,6 @@ namespace OASystem.API.Controllers
 
                 #endregion
 
-
-
-
                 if (diId > 0)
                 {
                     await AppNoticeLibrary.SendUserMsg_GroupTimeInfo_ToVisaUser(diId, startTime.ToString("yyyy-MM-dd"), endTime.ToString("yyyy-MM-dd"));
@@ -2819,6 +2821,9 @@ FROM
 
                     _groupTextLogger.LogInformation("团组:【{teamName}({groupId})】 操作人:【{operationName}】 操作时间:【{operationTime}】{logContent}", teamName, diid, operationName, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), groupStepLabel);
 
+
+                    //设置 签证流程初始值
+                    await _visaProcessRep.UpdateBatchTop4Step(dto.diid, dto.currUserId);
                 }
             }
             catch (Exception ex)
@@ -27231,7 +27236,237 @@ ORDER BY
 
         #region 团组签证流程
 
+        /// <summary>
+        /// 团组签证流程 - 团组列表(分页)
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessGroupNames(VisaProcessGroupNamesDto dto)
+        {
+            int currUserId = dto.CurrUserId <= 0 ? 1 : dto.CurrUserId;
+            string groupName = dto.GroupName.Trim();
+            int pageIndex = dto.PageIndex <= 0 ? 1 : dto.PageIndex;
+            int pageSize = dto.PageSize <= 0 ? 10 : dto.PageSize;
+            int portType = dto.PortType;
+
+            RefAsync<int> total = 0;
+
+            var query = _sqlSugar.Queryable<Grp_GroupsTaskAssignment>()
+                .InnerJoin<Grp_DelegationInfo>((x, y) => x.DIId == y.Id)
+                .Where((x, y) => x.IsDel == 0 && x.CTId == 80 && x.UId == currUserId);
+
+            //构建单字模糊查询
+            if (!string.IsNullOrEmpty(groupName))
+            {
+                var characters = groupName.ToCharArray();
+                foreach (var character in characters)
+                {
+                    var charString = character.ToString();
+                    query = query.Where((x, y) =>
+                        SqlFunc.Contains(y.TeamName, charString));
+                }
+            }
+
+            var groups = await query
+                .GroupBy((x,y) => y.Id)
+                .OrderByDescending((x, y) => y.VisitDate)
+                .Select((x, y) => new 
+                {
+                    y.Id,
+                    GroupName = y.TeamName
+                })
+                .ToPageListAsync(pageIndex, pageSize, total);
+
+            var msg = "SUCCESS";
+            if (total <= 0) msg = "暂无可操作的团组,请联系经理分配团组!";
+
+            return Ok(JsonView(true, msg, groups,total));
+        }
+
+        /// <summary>
+        /// 团组签证流程 - 详情
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessInfoByGroupId(VisaProcessInfoByGroupIdDto dto)
+        {
+            var res = await _visaProcessRep.Info(dto);
+            if (res.Code != 200) return Ok(JsonView(false, res.Msg));
+
+            return Ok(JsonView(true, res.Msg, res.Data));
+        }
+
+        /// <summary>
+        /// 团组签证流程 - 流程创建测试
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessAdd(int groupId,int currUserId)
+        {
+            return Ok(await _visaProcessRep.Create(currUserId, groupId));
+        }
+
+        /// <summary>
+        /// 团组签证流程 - 流程Top4Init 测试
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessTop4StepInit(int groupId, int currUserId)
+        {
+            return Ok(await _visaProcessRep.UpdateBatchTop4Step(groupId, currUserId));
+        }
+
+
+        /// <summary>
+        /// 团组签证流程 - 设置状态为已完成
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessSetCompleted(int id, int currUserId)
+        {
+            //参数验证
+            var stepValid = await _sqlSugar.Queryable<Grp_VisaProcessSteps>().Where(x => x.Id == id && x.IsDel == 0).AnyAsync();
+            if (!stepValid) return Ok(JsonView(false,"签证流程ID无效。"));
+
+            var userValid = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == currUserId && x.IsDel == 0).AnyAsync();
+            if (!userValid) return Ok(JsonView(false, "用户ID无效。"));
+
+            var res = await _visaProcessRep.SetCompleted(id, currUserId);
+            if (res.Code != 200) return Ok(JsonView(false, res.Msg));
+
+            return Ok(JsonView(true, res.Msg));
+        }
+
+        /// <summary>
+        /// 团组签证流程 - 修改各步骤内容
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessSetContent(VisaProcessSetContent dto)
+        {
+            //参数验证
+            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 userValid = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.CurrUserId && x.IsDel == 0).AnyAsync();
+            if (!userValid) return Ok(JsonView(false, "用户ID无效。"));
+
+            if (dto.StepContents == null && dto.StepContents.Count < 1)
+            {
+                return Ok(JsonView(false, "步骤内容无效。"));
+            }
+
+            //各步骤值处理
+            object stepVal;
+            if (stepValid.Step == 7) stepVal = dto.StepContents;
+            else stepVal = dto.StepContents[0].ToString();
+
+            var stepInfo = new Grp_VisaProcessSteps
+                {
+                    Id = dto.StepId,
+                    DataType = stepValid.DataType,
+                    TypedValue = stepVal,
+                    LastUpdateUserId = dto.CurrUserId
+                };
+
+            var res = await _visaProcessRep.Update(stepInfo);
+
+            if (res.Code != 200) return Ok(JsonView(false, res.Msg));
 
+            return Ok(JsonView(true, res.Msg));
+        }
+
+        /// <summary>
+        /// 团组签证流程 - 上传文件
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> VisaProcessUploadFiles([FromForm] VisaProcessUploadFilesDto dto)
+        {
+            //参数验证
+            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 userValid = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.CurrUserId && x.IsDel == 0).AnyAsync();
+            if (!userValid) return Ok(JsonView(false, "用户ID无效。"));
+
+            //验证文件是否存在
+            if (dto.Files == null || dto.Files.Count < 1) return Ok(JsonView(false, "请选择文件。"));
+
+
+            //验证该步骤是否可以上传文件
+            var uploadSteps = new List<int>() { 
+                5, //实际出签时间
+                6, //是否需要开出境证明
+                7, //是否需要办理电子入境卡手续
+            };
+
+            if (!uploadSteps.Contains(stepValid.Step))
+            {
+                return Ok(JsonView(false, $"该步骤不可上传附件。可上传附件步骤({string.Join("、", uploadSteps)})"));
+            }
+
+            //验证根目录
+            string rootRrl = AppSettingsHelper.Get("OfficeBaseUrl");
+            string rootPath = AppSettingsHelper.Get("OfficeTempBasePath");
+            if (!System.IO.Directory.Exists(rootPath))
+            {
+                System.IO.Directory.CreateDirectory(rootPath);
+            }
+
+            //验证签证流程上传文件夹 //按照团组名称存储文件夹 GrpFile/VisaProcessFiles/
+            var groupName = await _sqlSugar.Queryable<Grp_DelegationInfo>()
+                .Where(x => x.IsDel == 0 && x.Id == stepValid.GroupId)
+                .Select(x => $"{x.TeamName}({x.Id})")
+                .FirstAsync();
+
+            //创建团组文件夹
+            var groupPath = rootPath + "GrpFile/VisaProcessFiles/" + groupName;
+
+            if (!System.IO.Directory.Exists(groupPath))
+            {
+                System.IO.Directory.CreateDirectory(groupPath);
+            }
+
+            List<string> filePaths = new List<string>();
+            //保存文件
+            foreach (var file in dto.Files)
+            {
+                // 生成存储文件名
+                var fileName = CommonFun.GenerateStoredFileName($"Step{stepValid.Step}.{file.FileName.Split(".")[1]}");
+                var filePath = Path.Combine(groupPath, fileName);
+
+                // 保存文件到磁盘
+                using (var stream = new FileStream(filePath, FileMode.Create))
+                {
+                    await file.CopyToAsync(stream);
+                }
+
+                filePaths.Add($"GrpFile/VisaProcessFiles/{groupName}/{fileName}");
+            }
+
+            //数据库存储
+            var stepInfo = new Grp_VisaProcessSteps
+            {
+                Id = dto.StepId,
+                TypedFileNameValue = filePaths,
+                LastUpdateUserId = dto.CurrUserId
+            };
+
+            var res = await _sqlSugar.Updateable<Grp_VisaProcessSteps>()
+                .SetColumns(s => new Grp_VisaProcessSteps
+                {
+                    AttachUrl = stepInfo.AttachUrl,
+                    LastUpdateUserId = dto.CurrUserId,
+                    LastUpdateTime = DateTime.Now
+                })
+                .Where(s => s.Id == dto.StepId && s.IsDel == 0)
+                .ExecuteCommandAsync();
+
+            if (res < 1) Ok(JsonView(false,"文件上传失败。"));
+
+            return Ok(JsonView(true));
+        }
 
         #endregion
 

+ 2 - 5
OASystem/OASystem.Api/Program.cs

@@ -19,7 +19,6 @@ using Quartz.Impl;
 using Quartz.Spi;
 using QuzrtzJob.Factory;
 using Serilog.Events;
-using System.Diagnostics;
 using System.IO.Compression;
 
 Console.Title = $"FMGJ OASystem Server";
@@ -176,7 +175,7 @@ builder.Services.AddScoped(options =>
             ConnectionString = _config.GetConnectionString("OA2023DB"),
             DbType = DbType.SqlServer,
             IsAutoCloseConnection = true,
-            
+
         },
         new()
         {
@@ -224,7 +223,7 @@ builder.Services.AddScoped(options =>
             //获取无参数SQL对性能有影响,特别大的SQL参数多的,调试使用
             //UtilMethods.GetSqlString(DbType.SqlServer, exp.sql, exp.parameters);
 
-            
+
         };
         //修改SQL和参数的值
         db.Aop.OnExecutingChangeSql = (sql, pars) =>
@@ -388,7 +387,6 @@ var eec_TextLogger = new LoggerConfiguration()
 
 #endregion
 
-
 #region 团组步骤操作 专用记录器
 
 // 指定磁盘绝对路径(示例:D盘的AppLogs文件夹)
@@ -413,7 +411,6 @@ var groupStepOP_TextLogger = new LoggerConfiguration()
 
 #endregion
 
-
 // 配置Serilog为Log;
 builder.Host.UseSerilog();
 

+ 60 - 0
OASystem/OASystem.Domain/Dtos/Groups/VisaProcessDtos.cs

@@ -0,0 +1,60 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OASystem.Domain.Dtos.Groups
+{
+    public class VisaProcessDtos
+    {
+    }
+
+    public class VisaProcessGroupNamesDto : DtoBase
+    {
+        /// <summary>
+        /// 当前用户Id
+        /// </summary>
+        public int CurrUserId { get; set; }
+
+        /// <summary>
+        /// 团组名称
+        /// </summary>
+        public string GroupName { get; set; }
+    }
+
+    public class VisaProcessInfoByGroupIdDto : PortDtoBase
+    {
+        public int GroupId { get; set; }
+    }
+
+    public class VisaProcessSetContent: PortDtoBase
+    {
+        /// <summary>
+        /// 当前登录用户Id
+        /// </summary>
+        public int CurrUserId { get; set; }
+
+        /// <summary>
+        /// 签证流程步骤Id
+        /// </summary>
+        public int StepId { get; set; }
+
+        /// <summary>
+        /// 签证流程内容
+        /// 内容多个值 按顺序存储
+        /// </summary>
+        public List<string> StepContents { get; set; }
+
+    }
+
+    public class VisaProcessUploadFilesDto
+    {
+        public int StepId { get; set; }
+
+        public List<IFormFile> Files { get; set; }
+
+        public int CurrUserId { get; set; }
+    }
+}

+ 189 - 14
OASystem/OASystem.Domain/Entities/Groups/Grp_VisaProcessSteps.cs

@@ -1,4 +1,7 @@
-namespace OASystem.Domain.Entities.Groups
+using System.Text.Encodings.Web;
+using System.Text.Json;
+
+namespace OASystem.Domain.Entities.Groups
 {
     /// <summary>
     /// 签证流程管控步骤
@@ -18,16 +21,22 @@
         [SugarColumn(ColumnName = "Step", ColumnDescription = "步骤", IsNullable = true, ColumnDataType = "int")]
         public int Step { get; set; }
 
+        /// <summary>
+        /// 标识数据类型
+        /// </summary>
+        [SugarColumn(ColumnName = "DataType", ColumnDescription = "标识数据类型", IsNullable = true, ColumnDataType = "varchar(50)")]
+        public string DataType { get; set; }
+
         /// <summary>
         /// 存储值(办理日期(2025-01-01))、bool(false,true))
         /// </summary>
-        [SugarColumn(ColumnName = "ProcDate", ColumnDescription = "存储值(办理日期(2025-01-01))、bool(false,true))", IsNullable = true, ColumnDataType = "varchar(50)")]
+        [SugarColumn(ColumnName = "StoreVal", ColumnDescription = "存储值(办理日期(2025-01-01))、bool(false,true))", IsNullable = true, ColumnDataType = "varchar(max)")]
         public string StoreVal { get; set; }
 
         /// <summary>
         /// 附件地址
         /// </summary>
-        [SugarColumn(ColumnName = "ProcDate", ColumnDescription = "附件地址", IsNullable = true, ColumnDataType = "varchar(120)")]
+        [SugarColumn(ColumnName = "AttachUrl", ColumnDescription = "附件地址", IsNullable = true, ColumnDataType = "varchar(max)")]
         public string AttachUrl { get; set; }
 
         /// <summary>
@@ -44,8 +53,173 @@
         /// <summary>
         /// 最后更新时间
         /// </summary>
-        [SugarColumn(ColumnName = "LastUpdateTime", ColumnDescription = "最后更新时间", IsNullable = true, ColumnDataType = "varchar(30)")]
-        public string LastUpdateTime { get; set; }
+        [SugarColumn(ColumnName = "LastUpdateTime", ColumnDescription = "最后更新时间", IsNullable = true, ColumnDataType = "DateTime")]
+        public DateTime LastUpdateTime { get; set; } = DateTime.Now;
+
+        #region 动态获取、设置StoreVal值
+
+       /// <summary>
+       /// Converts a given value to its string representation and determines its storage data type.
+       /// </summary>
+       /// <remarks>If the value is an enumerable of strings or integers, it is serialized as a JSON array.
+       /// For other class types not explicitly handled, the value is serialized as a JSON object. Date and time values
+       /// are formatted using the round-trip format specifier ("O").</remarks>
+       /// <param name="value">The value to convert. Can be null or an instance of a supported type such as string, numeric types, boolean,
+       /// DateTime, DateOnly, TimeOnly, Guid, or enumerable collections of strings or integers.</param>
+       /// <returns>A tuple containing the string representation of the value and a string indicating the data type for storage.
+       /// If the input is null, the Value is null and the DataType is "null".</returns>
+        private static (string? Value, string DataType) ConvertToStorage(object? value)
+        {
+            if (value == null) return (null, "null");
+
+            return value switch
+            {
+                string s => (s, "string"),
+                int i => (i.ToString(), "int"),
+                long l => (l.ToString(), "long"),
+                decimal d => (d.ToString(), "decimal"),
+                double d => (d.ToString(), "double"),
+                float f => (f.ToString(), "float"),
+                bool b => (b.ToString(), "bool"),
+                DateTime dt => (dt.ToString("O"), "datetime"),
+                DateOnly date => (date.ToString("O"), "date"),
+                TimeOnly time => (time.ToString("O"), "time"),
+                Guid guid => (guid.ToString(), "guid"),
+                IEnumerable<string> list => (JsonSerializer.Serialize(list), "string[]"),
+                IEnumerable<int> list => (JsonSerializer.Serialize(list), "int[]"),
+                _ when value.GetType().IsClass => (JsonSerializer.Serialize(value), "object"),
+                _ => (value.ToString(), "string")
+            };
+        }
+
+        /// <summary>
+        /// Converts a string representation of a value from storage to its corresponding .NET type based on the
+        /// specified data type.
+        /// </summary>
+        /// <remarks>If the specified data type is not recognized, the original string value is returned.
+        /// For array and object types, the method attempts to deserialize the string as JSON. If conversion fails for
+        /// numeric, boolean, or date/time types, the method returns null.</remarks>
+        /// <param name="value">The string value to convert. Can be null or empty, in which case the method returns null.</param>
+        /// <param name="dataType">The name of the target data type. Supported values include "string", "int", "long", "decimal", "double",
+        /// "float", "bool", "datetime", "date", "time", "guid", "string[]", "int[]", and "object". The comparison is
+        /// case-insensitive.</param>
+        /// <returns>An object representing the converted value in the specified .NET type, or null if the input is null, empty,
+        /// or cannot be converted.</returns>
+        private static object? ConvertFromStorage(string? value, string dataType)
+        {
+            if (string.IsNullOrEmpty(value)) return null;
+
+            return dataType?.ToLower() switch
+            {
+                "string" => value,
+                "int" => int.TryParse(value, out var result) ? result : null,
+                "long" => long.TryParse(value, out var result) ? result : null,
+                "decimal" => decimal.TryParse(value, out var result) ? result : null,
+                "double" => double.TryParse(value, out var result) ? result : null,
+                "float" => float.TryParse(value, out var result) ? result : null,
+                "bool" => bool.TryParse(value, out var result) ? result : null,
+                "datetime" => DateTime.TryParse(value, out var result) ? result : null,
+                "date" => DateOnly.TryParse(value, out var result) ? result : null,
+                "time" => TimeOnly.TryParse(value, out var result) ? result : null,
+                "guid" => Guid.TryParse(value, out var result) ? result : null,
+                "string[]" => JsonSerializer.Deserialize<string[]>(value),
+                "int[]" => JsonSerializer.Deserialize<int[]>(value),
+                "object" => JsonSerializer.Deserialize<JsonElement>(value),
+                "null" => null,
+                _ => value
+            };
+        }
+
+
+        [SugarColumn(IsIgnore = true)]
+        public List<string> TypedFileNameValue
+        {
+            get
+            {
+                if (!string.IsNullOrEmpty(AttachUrl))
+                {
+                    string rootRrl = "http://132.232.92.186:24/";
+                    var result = JsonSerializer.Deserialize<List<string>>(AttachUrl);
+
+                    for (int i = 0; i < result.Count; i++)
+                    {
+                        result[i] = $"{rootRrl}{result[i]}";
+                    }
+
+                    return result;
+                }
+
+                return new List<string>();
+            }
+            set
+            {
+                if (value == null || value.Count < 1)
+                {
+                    AttachUrl = "";
+                }
+                else
+                {
+
+                    var options = new JsonSerializerOptions
+                    {
+                        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+                        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                        WriteIndented = true
+                    };
+
+                    AttachUrl = JsonSerializer.Serialize(value, options);
+                }
+            }
+        }
+
+        [SugarColumn(IsIgnore = true)]
+        public object? TypedValue
+        {
+            get => ConvertFromStorage(StoreVal, DataType);
+            set
+            {
+                var (storageValue, dataType) = ConvertToStorage(value);
+                StoreVal = storageValue;
+                DataType = dataType;
+            }
+        }
+
+        [SugarColumn(IsIgnore = true)]
+        public string? StringValue
+        {
+            get => DataType == "string" ? StoreVal : null;
+            set => (StoreVal, DataType) = (value, "string");
+        }
+
+        [SugarColumn(IsIgnore = true)]
+        public int? IntValue
+        {
+            get => DataType == "int" && int.TryParse(StoreVal, out var result) ? result : null;
+            set => (StoreVal, DataType) = (value?.ToString(), "int");
+        }
+
+        [SugarColumn(IsIgnore = true)]
+        public decimal? DecimalValue
+        {
+            get => DataType == "decimal" && decimal.TryParse(StoreVal, out var result) ? result : null;
+            set => (StoreVal, DataType) = (value?.ToString(), "decimal");
+        }
+
+        [SugarColumn(IsIgnore = true)]
+        public bool? BooleanValue
+        {
+            get => DataType == "bool" && bool.TryParse(StoreVal, out var result) ? result : null;
+            set => (StoreVal, DataType) = (value?.ToString(), "bool");
+        }
+
+        [SugarColumn(IsIgnore = true)]
+        public DateTime? DateTimeValue
+        {
+            get => DataType == "datetime" && DateTime.TryParse(StoreVal, out var result) ? result : null;
+            set => (StoreVal, DataType) = (value?.ToString("O"), "datetime");
+        }
+
+        #endregion
 
 
         public Grp_VisaProcessSteps() { }
@@ -54,18 +228,19 @@
         /// 流程步骤初始化
         /// </summary>
         /// <param name="groupId"></param>
+        /// <param name="createUserId"></param>
         /// <returns></returns>
-        public static List<Grp_VisaProcessSteps> StepsInit(int groupId)
+        public static List<Grp_VisaProcessSteps> StepsInit(int groupId,int createUserId)
         {
             return new List<Grp_VisaProcessSteps>() {
-                new(){ GroupId = groupId, Step = 1 , }, //第一步:签证启动日(团组信息操作里面打勾确认出团)
-                new(){ GroupId = groupId, Step = 2 , }, //第二步:分配工作(从倒退表里-出批件、护照办理取时间)
-                new(){ GroupId = groupId, Step = 3 , }, //第三步:送外办时间(从倒退表里-送签签证取时间)
-                new(){ GroupId = groupId, Step = 4 , }, //第四步:读取预计出签时间(从签证费用标准-签证时间工作日取时间,送外办时间+签证时间(工作日))
-                new(){ GroupId = groupId, Step = 5 , }, //第五步:实际出签时间(手动填写) 附件
-                new(){ GroupId = groupId, Step = 6 , }, //第六步:是否需要开出境证明 (否,是) 附件
-                new(){ GroupId = groupId, Step = 7 , }, //第七步:是否需要办理电子入境卡手续(否,是) -> 如果是,系统生成附件(个人信息资料+机票航班信息+酒店信息)【在出发前三天内填写,取具体的日期】
-                new(){ GroupId = groupId, Step = 8 , }, //第八步:确认完成操作 (确认完成操作后,不可更改该流程数据)
+                new(){ GroupId = groupId, Step = 1, DataType = "string", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第一步:签证启动日(团组信息操作里面打勾确认出团)
+                new(){ GroupId = groupId, Step = 2, DataType = "string", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第二步:分配工作(从倒退表里-出批件、护照办理取时间)
+                new(){ GroupId = groupId, Step = 3, DataType = "string", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第三步:送外办时间(从倒退表里-送签签证取时间)
+                new(){ GroupId = groupId, Step = 4, DataType = "string", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第四步:读取预计出签时间(从签证费用标准-签证时间工作日取时间,送外办时间+签证时间(工作日))
+                new(){ GroupId = groupId, Step = 5, DataType = "string", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第五步:实际出签时间(手动填写) 附件
+                new(){ GroupId = groupId, Step = 6, DataType = "bool", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第六步:是否需要开出境证明 (否,是) 附件
+                new(){ GroupId = groupId, Step = 7, DataType = "string[]", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第七步:是否需要办理电子入境卡手续(否,是) -> 如果是,系统生成附件(个人信息资料+机票航班信息+酒店信息)【在出发前三天内填写,取具体的日期】
+                new(){ GroupId = groupId, Step = 8, DataType = "string", CreateUserId = createUserId, LastUpdateUserId = createUserId }, //第八步:确认完成操作 (确认完成操作后,不可更改该流程数据)
             };
         }
     }

+ 76 - 0
OASystem/OASystem.Domain/ViewModels/Groups/VisaProcessStepsView.cs

@@ -0,0 +1,76 @@
+using OASystem.Domain.Entities.Groups;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OASystem.Domain.ViewModels.Groups
+{
+    public class VisaProcessStepsView:Grp_VisaProcessSteps
+    {
+    }
+
+    public class VisaProcessStepsInfoBase
+    {
+        public int Id { get; set; }
+        public int GroupId { get; set; }
+        public int Step { get; set; }
+        public string DataType { get; set; }
+        public bool IsCompleted { get; set; }
+        public List<string> AttachUrl { get; set; }
+        public string Remark { get; set; }
+    }
+
+    /// <summary>
+    /// dataType = "string" 的签证流程步骤信息
+    /// </summary>
+    public class VisaProcessStepsInfoByStringView:VisaProcessStepsInfoBase
+    {
+        public string TypedValue { get; set; }
+    }
+
+    /// <summary>
+    /// dataType = "bool" 的签证流程步骤信息
+    /// </summary>
+    public class VisaProcessStepsInfoByBoolView:VisaProcessStepsInfoBase
+    {
+        public bool TypedValue { get; set; }
+    }
+
+    /// <summary>
+    /// dataType = "List<string>" 的签证流程步骤信息
+    /// </summary>
+    public class VisaProcessStepsInfoByListView : VisaProcessStepsInfoBase
+    {
+        public VisaProcessSteps7Content TypedValue { get; set; }
+    }
+
+    public class VisaProcessSteps7Content
+    {
+        /// <summary>
+        /// contnet-1
+        /// 是否需要xxx
+        /// </summary>
+        public bool IsSelected { get; set; }
+
+        /// <summary>
+        /// contnet-2
+        /// 预计日期
+        /// </summary>
+        public string ProjectedDate { get; set; }
+    }
+
+
+    public class VisaProcessStepsInfoView
+    {
+        public VisaProcessStepsInfoByStringView Step1 { get; set; }
+        public VisaProcessStepsInfoByStringView Step2 { get; set; }
+        public VisaProcessStepsInfoByStringView Step3 { get; set; }
+        public VisaProcessStepsInfoByStringView Step4 { get; set; }
+        public VisaProcessStepsInfoByStringView Step5 { get; set; }
+        public VisaProcessStepsInfoByBoolView Step6 { get; set; }
+        public VisaProcessStepsInfoByListView Step7 { get; set; }
+        public VisaProcessStepsInfoByStringView Step8 { get; set; }
+    }
+}

+ 325 - 0
OASystem/OASystem.Infrastructure/Repositories/Groups/VisaProcessRepository.cs

@@ -0,0 +1,325 @@
+using AutoMapper;
+using OASystem.Domain;
+using OASystem.Domain.Dtos.Groups;
+using OASystem.Domain.Entities.Groups;
+using OASystem.Domain.ViewModels.Groups;
+using OASystem.Infrastructure.Tools;
+
+namespace OASystem.Infrastructure.Repositories.Groups
+{
+    /// <summary>
+    /// 签证流程步骤仓储
+    /// </summary>
+    public class VisaProcessRepository : BaseRepository<Grp_VisaProcessSteps, Grp_VisaProcessSteps>
+    {
+
+        private readonly IMapper _mapper;
+
+        public VisaProcessRepository(SqlSugarClient sqlSugar, IMapper mapper)
+            : base(sqlSugar)
+        {
+            _mapper = mapper;
+        }
+
+        /// <summary>
+        /// 创建签证流程步骤
+        /// </summary>
+        /// <param name="groupId"></param>
+        /// <param name="createUderId"></param>
+        /// <returns></returns>
+        public async Task<Result> Create(int createUderId,int groupId)
+        {
+            //团组有效验证
+            var groupIsValid = await _sqlSugar.Queryable<Grp_DelegationInfo>()
+                .Where(g => g.Id == groupId && g.IsDel == 0)
+                .AnyAsync();
+
+            if (!groupIsValid)
+            {
+                return new Result(400, "团组无效,无法创建签证流程步骤。");
+            }
+
+            var existingSteps = await _sqlSugar.Queryable<Grp_VisaProcessSteps>()
+                .Where(s => s.GroupId == groupId && s.IsDel == 0)
+                .AnyAsync();
+
+            if (existingSteps)
+            {
+                return new Result(400, "该团组的签证流程步骤已存在,无法重复创建。");
+            }
+
+            var steps = Grp_VisaProcessSteps.StepsInit(groupId, createUderId);
+
+            var add = await _sqlSugar.Insertable(steps).ExecuteCommandAsync();
+
+            if (add < 1) return new Result(400, "签证流程步骤创建失败。");
+
+            return new Result(200,"Success");
+        }
+
+        /// <summary>
+        /// 签证流程 Info
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="createUderId"></param>
+        /// <returns></returns>
+        public async Task<Result> Info(VisaProcessInfoByGroupIdDto dto)
+        {
+            //团组有效验证
+            //var groupIsValid = await _sqlSugar.Queryable<Grp_DelegationInfo>()
+            //    .Where(g => g.Id == dto.GroupId && g.IsDel == 0)
+            //    .AnyAsync();
+
+            //if (!groupIsValid)
+            //{
+            //    return new Result(400, "团组无效,无法查询签证流程步骤。");
+            //}
+
+            var query = await _sqlSugar.Queryable<Grp_VisaProcessSteps>()
+                .Where(s => s.GroupId == dto.GroupId && s.IsDel == 0)
+                .OrderBy(s => s.Step)
+                .ToListAsync();
+            var infos = query.Select(s => new
+            {
+                s.Id,
+                s.GroupId,
+                s.Step,
+                s.DataType,
+                s.TypedValue,
+                //s.StoreVal,
+                s.IsCompleted,
+                s.AttachUrl,
+                s.TypedFileNameValue,
+                s.Remark
+            }).ToList();
+
+            string msg = "Success";
+            //如果不存在,则返回默认数据
+            if (infos == null || infos.Count < 1)
+            {
+                infos = Grp_VisaProcessSteps.StepsInit(dto.GroupId, 208).Select(s => new {
+                    s.Id,
+                    s.GroupId,
+                    s.Step,
+                    s.DataType,
+                    s.TypedValue,
+                    //s.StoreVal,
+                    s.IsCompleted,
+                    s.AttachUrl,
+                    s.TypedFileNameValue,
+                    s.Remark
+                }).ToList();
+                msg = "签证流程步骤信息不存在,展示默认数据。";
+            }
+
+            //数据按照类型处理
+            var datas = new List<object>();
+            var view = new VisaProcessStepsInfoView();
+            foreach (var item in infos)
+            {
+                if (item.DataType == "string")
+                {
+                    var info = new VisaProcessStepsInfoByStringView
+                    {
+                        Id = item.Id,
+                        GroupId = item.GroupId,
+                        Step = item.Step,
+                        DataType = item.DataType,
+                        TypedValue = item.TypedValue == null ? "" : item.TypedValue.ToString(),
+                        //StoreVal = item.StoreVal,
+                        IsCompleted = item.IsCompleted,
+                        AttachUrl = item.TypedFileNameValue,
+                        Remark = item.Remark
+                    };
+
+                    if (item.Step == 1) view.Step1 = info;
+                    else if (item.Step == 2) view.Step2 = info;
+                    else if (item.Step == 3) view.Step3 = info;
+                    else if (item.Step == 4) view.Step4 = info;
+                    else if (item.Step == 5) view.Step5 = info;
+                    else if (item.Step == 8) view.Step8 = info;
+
+
+                    datas.Add(info); 
+                }
+                else if (item.DataType == "bool")
+                {
+                    bool boolVal = false;
+                    if (item.TypedValue != null)
+                    {
+                        boolVal = (bool)item.TypedValue;
+                    }
+
+                    var info = new VisaProcessStepsInfoByBoolView
+                    {
+                        Id = item.Id,
+                        GroupId = item.GroupId,
+                        Step = item.Step,
+                        DataType = item.DataType,
+                        TypedValue = boolVal,
+                        IsCompleted = item.IsCompleted,
+                        AttachUrl = item.TypedFileNameValue,
+                        Remark = item.Remark
+                    };
+
+                    if (item.Step == 6) view.Step6 = info;
+                    datas.Add(info);
+                }
+                else if (item.DataType == "string[]")
+                {
+                    var listVal = new List<string>();
+                    var contnet = new VisaProcessSteps7Content() { IsSelected = false };
+                    if (item.TypedValue != null)
+                    {
+                        listVal = item.TypedValue switch
+                        {
+                            List<string> stringList => stringList,
+                            IEnumerable<string> stringEnumerable => stringEnumerable.ToList(),
+                            string str => new List<string> { str },
+                            IEnumerable<object> objectEnumerable => objectEnumerable.Select(x => x?.ToString() ?? "").ToList(),
+                            _ => new List<string> { item.TypedValue.ToString() ?? "" }
+                        };
+
+                        if (listVal != null && listVal.Count > 0)
+                        {
+                            contnet.IsSelected = bool.TryParse(CommonFun.GetValueOrDefault(listVal,0), out _);
+                            contnet.ProjectedDate = CommonFun.GetValueOrDefault(listVal, 1);
+                        }
+                    }
+
+                    var info = new VisaProcessStepsInfoByListView
+                    {
+                        Id = item.Id,
+                        GroupId = item.GroupId,
+                        Step = item.Step,
+                        DataType = item.DataType,
+                        TypedValue = contnet,
+                        IsCompleted = item.IsCompleted,
+                        AttachUrl = item.TypedFileNameValue,
+                        Remark = item.Remark
+                    };
+
+                    if (item.Step == 7) view.Step7 = info;
+                    datas.Add(info);
+
+                }
+            }
+
+            return new Result(200, msg, view);
+        }
+
+        /// <summary>
+        /// 修改签证流程步骤
+        /// </summary>
+        /// <param name="groupId"></param>
+        /// <param name="createUderId"></param>
+        /// <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
+                })
+                .Where(s => s.Id == info.Id && s.IsDel == 0)
+                .ExecuteCommandAsync();
+
+            if (update < 1) return new Result(400, "更新失败!");
+
+            return new Result(200, "Success");
+        }
+
+        /// <summary>
+        /// 批量修改top4Steps
+        /// </summary>
+        /// <param name="infos"></param>
+        /// <returns></returns>
+        public async Task<Result> UpdateBatchTop4Step(int groupId,int currUserId)
+        {
+            var steps = new List<int>() { 
+                1, //签证启动日
+                2, //分配工作
+                3, //送外办时间
+                4, //预计出签时间
+            };
+
+            var visaSteps = await _sqlSugar.Queryable<Grp_VisaProcessSteps>()
+                .Where(s => s.GroupId == groupId && s.IsDel == 0 && steps.Contains(s.Step))
+                .ToListAsync();
+
+            if (visaSteps.Count < 1) return new Result(400, "签证流程信息不存在。");
+
+            //倒推表信息
+            var invertedInfo = await _sqlSugar.Queryable<Grp_InvertedList>()
+                .Where(i => i.DiId == groupId && i.IsDel == 0)
+                .FirstAsync();
+            if (invertedInfo == null) return new Result(400, "倒推表信息不存在。");
+
+            //设置操作人和时间
+            foreach (var item in visaSteps)
+            {
+                //默认确认完成
+                item.IsCompleted = true;
+                item.LastUpdateUserId = currUserId;
+                item.LastUpdateTime = DateTime.Now;
+            }
+
+            //step=1 设置启动日值
+            visaSteps.FirstOrDefault(x => x.Step == 1).TypedValue = DateTime.Now.ToString("yyyy-MM-dd");
+
+            //step=2 分配工作
+            visaSteps.FirstOrDefault(x => x.Step == 2).TypedValue = invertedInfo.IssueApprovalDt;
+
+            //step=3 送外办时间
+            visaSteps.FirstOrDefault(x => x.Step == 3).TypedValue = invertedInfo.SendVisaDt;
+
+            //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, "更新失败!");
+
+            return new Result(200, "Success");
+        }
+
+        /// <summary>
+        /// 设置已完成的步骤
+        /// </summary>
+        /// <param name="groupId"></param>
+        /// <returns></returns>
+        public async Task<Result> SetCompleted(int id,int currUserId)
+        {
+            //步骤信息验证
+            var stepInfo = await _sqlSugar.Queryable<Grp_VisaProcessSteps>()
+                .Where(s => s.Id == id && s.IsDel == 0 && s.IsCompleted)
+                .FirstAsync();
+            if (stepInfo != null) return new Result(400, "步骤信息已完成,无法重复设置。");
+
+            var update = await _sqlSugar.Updateable<Grp_VisaProcessSteps>()
+                .SetColumns(s => new Grp_VisaProcessSteps
+                {
+                    IsCompleted = true,
+                    LastUpdateUserId = currUserId,
+                    LastUpdateTime = DateTime.Now
+                })
+                .Where(s => s.Id == id && s.IsDel == 0 && !s.IsCompleted)
+                .ExecuteCommandAsync();
+
+            if (update < 1) return new Result(400, "状态设置失败!");
+
+
+            return new Result(200, "Success");
+        }
+
+    }
+}

+ 29 - 0
OASystem/OASystem.Infrastructure/Tools/CommonFun.cs

@@ -132,6 +132,24 @@ public static class CommonFun
             Directory.CreateDirectory(dir);
     }
 
+    /// <summary>
+    /// 生成唯一存储文件名
+    /// </summary>
+    /// <param name="originalFileName"> 文件原始名称 </param>
+    /// <returns></returns>
+    public static string GenerateStoredFileName(string originalFileName)
+    {
+        var extension = Path.GetExtension(originalFileName);
+        var fileNameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);
+        var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
+        var randomStr = Guid.NewGuid().ToString("N").Substring(0, 8);
+
+        // 清理文件名中的非法字符
+        var safeFileName = Regex.Replace(fileNameWithoutExt, @"[^\w\.-]", "");
+
+        return $"{safeFileName}_{timestamp}_{randomStr}{extension}";
+    }
+
     /// <summary>
     /// 验证文件名称
     /// </summary>
@@ -844,4 +862,15 @@ public static class CommonFun
     }
     #endregion
 
+    /// <summary>
+    /// 安全获取列表指定索引的值
+    /// </summary>
+    public static string? GetValueOrDefault(this List<string> list, int index)
+    {
+        if (list == null)
+            throw new ArgumentNullException(nameof(list));
+
+        return index >= 0 && index < list.Count ? list[index] : null;
+    }
+
 }