Selaa lähdekoodia

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

yuanrf 1 viikko sitten
vanhempi
commit
4a023f8f76

+ 17 - 17
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -1243,15 +1243,13 @@ namespace OASystem.API.Controllers
                     PageSize = 999999,
                     FieldWeights = new Dictionary<string, int>
                     {
-                        { "TeamName", 10 },
-                        //{ "ClientUnit", 8 },
-                        //{ "ClientName", 6 }
+                        { "TeamName", 10 }
                     },
                     Filters = new List<SearchFilter>()
                     {
                         new(){Field = "IsDel",Operator="eq",Value="0" }
                     },
-                    OrderBy = "TeamName",
+                    OrderBy = "VisitDate",
                     ReturnFields = new List<string>() { "TeamName", "ClientUnit", "ClientName" }
                 };
 
@@ -1277,7 +1275,8 @@ namespace OASystem.API.Controllers
                         {
                             item.Data.TeamName,
                             item.Data.ClientUnit,
-                            item.Data.ClientName
+                            item.Data.ClientName,
+                            //item.Data.VisitDate
                         });
                     }
                     
@@ -2428,14 +2427,19 @@ namespace OASystem.API.Controllers
         [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
         public async Task<IActionResult> GetGroupNameAndVisaNationality(GroupNameDto dto)
         {
-            var groupData = await _groupRepository.GetGroupNameAndVisaNationality(dto);
-            if (groupData.Code != 0)
+            string sql = string.Format(@"Select Id,TeamName GroupName From  Grp_DelegationInfo 
+                                 Where TeamName != '' And IsDel = 0
+                                 Order By VisitDate Desc");
+            var groupNameList = await _sqlSugar.SqlQueryable<GroupNameView>(sql).ToListAsync();
+
+            var data = new
             {
-                return Ok(JsonView(false, groupData.Msg));
-            }
+                groupNameData = groupNameList,
+                visaNationalityData = _visaTypeInit.ToList()
+            };
 
 
-            return Ok(JsonView(groupData.Data));
+            return Ok(JsonView(data));
         }
 
         /// <summary>
@@ -6162,13 +6166,9 @@ FROM
         [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
         public async Task<IActionResult> GetCrmByGroupId(ClientByGroupIdDto dto)
         {
-            var groupData = await _groupRepository.GetCrmByGroupId(dto);
-            if (groupData.Code != 0)
-            {
-                return Ok(JsonView(false, groupData.Msg));
-            }
+            var data = await _groupRepository.GetCrmByGroupId(dto);
 
-            return Ok(JsonView(groupData.Data));
+            return Ok(JsonView(true, data.Code != 0 ? "暂无信息" : "操作成功", data.Data));
         }
 
 
@@ -31388,7 +31388,7 @@ WHERE
         public async Task<IActionResult> GroupProcessSetActualDone(GroupProcessSetActualDoneDto dto)
         {
             //验证签证信息 
-            var res = await _processOverviewRep.SetActualDoneAsync(dto.NodeId, dto.ActualDone, dto.CurrUserId);
+            var res = await _processOverviewRep.SetActualDoneAsync(dto);
 
             if (res.Code == StatusCodes.Status200OK)
             {

+ 119 - 3
OASystem/OASystem.Api/Controllers/SearchController.cs

@@ -1,10 +1,16 @@
-using Microsoft.AspNetCore.Http;
+using EyeSoft.Collections.Generic;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using NPOI.SS.Formula.Functions;
 using OASystem.API.OAMethodLib.DeepSeekAPI;
 using OASystem.API.OAMethodLib.GenericSearch;
+using OASystem.Domain.AesEncryption;
+using OASystem.Domain.Entities.Customer;
 using OASystem.Domain.Entities.Financial;
 using OASystem.Domain.Entities.Groups;
+using OASystem.Domain.ViewModels.Search;
 using OASystem.Infrastructure.Repositories.System;
+using static iTextSharp.text.pdf.AcroFields;
 
 namespace OASystem.API.Controllers
 {
@@ -15,16 +21,22 @@ namespace OASystem.API.Controllers
     [ApiController]
     public class SearchController : ControllerBase
     {
+        private readonly SqlSugarClient _sqlSugar;
         private readonly DynamicSearchService<Grp_DelegationInfo> _groupSearchService;
+        private readonly DynamicSearchService<NewClientDataView> _clientSearchService;
         public SearchController(
-           DynamicSearchService<Grp_DelegationInfo> groupSearchService
+            SqlSugarClient sqlSugar,
+           DynamicSearchService<Grp_DelegationInfo> groupSearchService,
+           DynamicSearchService<NewClientDataView> clientSearchService
            )
         {
+            _sqlSugar = sqlSugar;
             _groupSearchService = groupSearchService;
+            _clientSearchService = clientSearchService;
         }
 
         /// <summary>
-        ///  接团信息  单字段(团组名称)关键字输入提示
+        ///  接团信息  关键字输入提示(单字段)
         /// </summary>
         /// <param name="keyword">关键字</param>
         /// <returns></returns>
@@ -88,5 +100,109 @@ namespace OASystem.API.Controllers
 
         }
 
+        /// <summary>
+        /// 客户资料  关键字输入提示(多字段)
+        /// </summary>
+        /// <param name="userId">关键字</param>
+        /// <param name="keyword">关键字</param>
+        /// <returns></returns>
+        [HttpGet("group/{userId}/{keyword}")]
+        [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
+        public async Task<IActionResult> ClientKeywordSearch(int userId,string keyword)
+        {
+            try
+            {
+                // 验证请求参数
+                if (string.IsNullOrEmpty(keyword))
+                {
+                    return Ok(JsonView(true, $"暂无数据!"));
+                }
+
+                //查询有权限的数据
+                var clientIds = await _sqlSugar
+                    .Queryable<Crm_ClientDataAndUser>()
+                    .Where(x => x.IsDel == 0 && x.usersId == userId)
+                    .Select(x => x.NewClientDataId)
+                    .ToListAsync();
+                if (clientIds == null || clientIds.Count < 1)
+                {
+                    return Ok(JsonView(true, $"暂无数据!"));
+                }
+
+                var data = _sqlSugar.Queryable<Crm_NewClientData>()
+                    .Where(x => x.IsDel == 0 && clientIds.Contains(x.Id))
+                    .OrderByDescending(x => x.CreateTime)
+                    .Select(x => new NewClientDataView (){
+                        Id = x.Id,
+                        Client = x.Client,
+                        Contact = x.Contact,
+                        Job = x.Job,
+                        Telephone = x.Telephone,
+                        Location = x.Location,
+                    })
+                    .ToList();
+                int index = 0;
+                data.ForEach(x =>
+                {
+                    index++;
+                    x.RowNumber = index;
+                    x.Client = AesEncryptionHelper.Decrypt(x.Client);
+                    x.Contact = AesEncryptionHelper.Decrypt(x.Contact);
+                    x.Job = AesEncryptionHelper.Decrypt(x.Job);
+                    x.Telephone = AesEncryptionHelper.Decrypt(x.Telephone);
+                    x.Location = AesEncryptionHelper.Decrypt(x.Location);
+                });
+
+
+                var searchRequest = new DynamicSearchRequest
+                {
+                    Keyword = keyword,
+                    RequireAllSingleChars = true,
+                    PageIndex = 1,
+                    PageSize = 999999,
+                    FieldWeights = new Dictionary<string, int>
+                    {
+                        { "Client", 10 },
+                        { "Contact", 8 },
+                        { "Location", 6 }
+                    },
+                    OrderBy = "CreateTime"
+                };
+
+                // 验证字段配置
+                var validation = _clientSearchService.ValidateFieldConfig(
+                    searchRequest.FieldWeights,
+                    searchRequest.ReturnFields);
+
+                if (!validation.IsValid)
+                {
+                    return Ok(JsonView(true, $"暂无数据!{validation.Message}"));
+                }
+
+                var result = _clientSearchService.SearchDataSource(searchRequest,data);
+
+                if (result.Success)
+                {
+                    var view = result.Items.Select(x => x.Data).ToList();
+
+                    int resetIndex = 0;
+                    view.ForEach(x => {
+                        resetIndex++;
+                        x.RowNumber = resetIndex;
+                    });
+
+                    return Ok(JsonView(true, result.Message, view, view.Count));
+                }
+
+                return Ok(JsonView(true,"暂无数据"));
+            }
+            catch (Exception ex)
+            {
+                return Ok(JsonView(true, $"搜索服务暂时不可用!"));
+            }
+
+        }
+
+
     }
 }

+ 1 - 0
OASystem/OASystem.Api/OAMethodLib/GenericSearch/DynamicSearchRequest.cs

@@ -50,6 +50,7 @@
         /// 是否要求所有单字必须完全出现(新增参数)
         /// </summary>
         public bool RequireAllSingleChars { get; set; } = false;
+
     }
 
     /// <summary>

+ 49 - 48
OASystem/OASystem.Api/OAMethodLib/GenericSearch/DynamicSearchService.cs

@@ -1,7 +1,4 @@
-using SqlSugar;
-using System.Diagnostics;
-using System.DirectoryServices;
-using System.Linq;
+using System.Diagnostics;
 using System.Linq.Expressions;
 
 namespace OASystem.API.OAMethodLib.GenericSearch
@@ -81,6 +78,54 @@ namespace OASystem.API.OAMethodLib.GenericSearch
             }
         }
 
+        /// <summary>
+        /// 执行动态搜索(应用层统计匹配度)
+        /// 基于数据源
+        /// </summary>
+        /// <param name="request">搜索请求参数</param>
+        /// <param name="dataSource">数据源</param>
+        /// <returns>包含搜索结果和匹配度信息的结果对象</returns>
+        public SearchResult<T> SearchDataSource(DynamicSearchRequest request, List<T> dataSource)
+        {
+            var resultView = new SearchResult<T>() { Success = false, Message = "异常错误" };
+
+            var stopwatch = Stopwatch.StartNew();
+            var searchId = Guid.NewGuid().ToString("N")[..8];
+
+            if (dataSource == null) return new SearchResult<T>();
+
+            try
+            {
+                List<T> data = dataSource;
+                int totalCount = dataSource.Count;
+
+                // 在应用层计算匹配度
+                var scoredItems = CalculateMatchScore(data, request);
+
+                stopwatch.Stop();
+
+                return new SearchResult<T>
+                {
+                    Message = $"搜索成功!耗时:{stopwatch.ElapsedMilliseconds}ms",
+                    Items = scoredItems,
+                    TotalCount = totalCount,
+                    Keyword = request.Keyword,
+                    FieldWeights = request.FieldWeights,
+                    ReturnFields = request.ReturnFields,
+                    PageIndex = request.PageIndex,
+                    PageSize = request.PageSize,
+                    ResponseTime = stopwatch.ElapsedMilliseconds,
+                    SearchId = searchId
+                };
+            }
+            catch (Exception ex)
+            {
+                stopwatch.Stop();
+                resultView.Message = string.Format("【{SearchId}】动态搜索失败: {ErrorMessage}", searchId, ex.Message);
+                return resultView;
+            }
+        }
+
         /// <summary>
         /// 轻量级搜索 - 只返回指定字段,提升性能(应用层统计匹配度)
         /// </summary>
@@ -367,50 +412,6 @@ namespace OASystem.API.OAMethodLib.GenericSearch
                     whereConditions.Add($"({string.Join(" OR ", keywordConditions)})");
                 }
                 #endregion
-
-                #region or 构建
-                //var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
-                //var keywordConditions = new List<string>();
-
-                //// 统一处理所有搜索词
-                //var allSearchTerms = new List<string>();
-
-                //// 添加符号分割的片段(去除特殊字符)
-                //allSearchTerms.AddRange(searchAnalysis.SymbolSegments
-                //    .Select(segment => Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", ""))
-                //    .Where(segment => !string.IsNullOrEmpty(segment)));
-
-                //// 添加单字(排除重复)
-                //foreach (var singleChar in searchAnalysis.SingleChars)
-                //{
-                //    var charStr = singleChar.ToString();
-                //    // 只有当单字不在任何符号片段中时才添加
-                //    if (!allSearchTerms.Any(term => term.Contains(charStr)))
-                //    {
-                //        allSearchTerms.Add(charStr);
-                //    }
-                //}
-
-                //// 处理每个搜索词
-                //foreach (var term in allSearchTerms.Distinct())
-                //{
-                //    var fieldConditions = validFields.Select(field =>
-                //    {
-                //        var paramName = $"@term{parameters.Count}";
-                //        parameters.Add(new SugarParameter(paramName, $"%{term}%"));
-                //        return $"{field} LIKE {paramName}";
-                //    });
-                //    keywordConditions.Add($"({string.Join(" OR ", fieldConditions)})");
-                //}
-
-                //// 所有搜索条件使用 AND 连接
-                //if (keywordConditions.Any())
-                //{
-                //    whereConditions.Add($"({string.Join(" AND ", keywordConditions)})");
-                //}
-
-
-                #endregion
             }
 
             // 构建过滤条件

+ 21 - 2
OASystem/OASystem.Domain/Dtos/Groups/VisaProcessDtos.cs

@@ -58,8 +58,8 @@ namespace OASystem.Domain.Dtos.Groups
 
     public class GroupProcessUpdateNodeStatusDto
     {
-
         public int NodeId { get; set; }
+
         public int CurrUserId { get; set; }
     }
 
@@ -78,10 +78,29 @@ namespace OASystem.Domain.Dtos.Groups
         /// </summary>
         public int NodeId { get; set; }
 
+        /// <summary>
+        /// 是否协助 (财务流程首节点使用)
+        /// </summary>
+        public bool IsAssist { get; set; } = false;
+
+        /// <summary>
+        /// 是否有文件 (签证、机票、酒店、地接 流程结尾节点使用)
+        /// </summary>
+        public bool IsFileUp { get; set; } = false;
+
+        /// <summary>
+        /// 是否参与 (商邀第五步节点使用)
+        /// </summary>
+        public bool IsPart { get; set; } = false;
+
         /// <summary>
         /// 实际完成时间
+        /// 情况1:正常使用
+        /// 情况2:IsAssist == true,设置值
+        /// 情况3:IsPart == true,设置值
+        /// 情况4:IsFileUp == true,设置值
         /// </summary>
-        public DateTime ActualDone { get; set; }
+        public string ActualDone { get; set; }
 
         /// <summary>
         /// 当前用户Id  

+ 39 - 8
OASystem/OASystem.Domain/Entities/Groups/Grp_ProcessOverview.cs

@@ -145,20 +145,48 @@ namespace OASystem.Domain.Entities.Groups
         [DefaultValue(false)]
         public bool IsCurrent { get; set; }
 
+        /// <summary>
+        /// 是否协助
+        /// (财务流程首节点使用)
+        /// </summary>
+        [SugarColumn(ColumnName = "IsAssist", ColumnDescription = "是否协助(财务流程首节点使用)", ColumnDataType = "bit")]
+        [DefaultValue(false)]
+        public bool IsAssist { get; set; }
+
+        /// <summary>
+        /// 是否上传文件
+        /// (签证、机票、酒店、地接 流程结尾节点使用)
+        /// </summary>
+        [SugarColumn(ColumnName = "IsFileUp", ColumnDescription = "是否上传文件(签证、机票、酒店、地接 流程结尾节点使用)", ColumnDataType = "bit")]
+        [DefaultValue(false)]
+        public bool IsFileUp { get; set; }
+
+        /// <summary>
+        /// 是否参与
+        /// (商邀 第五步使用)
+        /// </summary>
+        [SugarColumn(ColumnName = "IsPart", ColumnDescription = "是否参与(商邀 第五步使用)", ColumnDataType = "bit")]
+        [DefaultValue(false)]
+        public bool IsPart { get; set; }
+
+
         public Grp_ProcessNode() { }
 
         /// <summary>
         /// Create
         /// </summary>
-        /// <param name="nodeOrder"></param>
-        /// <param name="nodeName"></param>
-        /// <param name="nodeDescTips"></param>
-        /// <param name="status"></param>
-        /// <param name="isCurrent"></param>
-        /// <param name="currUserId"></param>
-        /// <param name="remark"></param>
+        /// <param name="nodeOrder">排序</param>
+        /// <param name="nodeName">名称</param>
+        /// <param name="nodeDescTips">描述提示</param>
+        /// <param name="status">状态</param>
+        /// <param name="isCurrent">是否是当前节点</param>
+        /// <param name="isAssist">是否协助</param>
+        /// <param name="isFileUp">是否上传文件</param>
+        /// <param name="isPart">是否参与</param>
+        /// <param name="currUserId">当前用户Id</param>
+        /// <param name="remark">备注</param>
         /// <returns></returns>
-        public static Grp_ProcessNode Create(int nodeOrder, string nodeName, string nodeDescTips,ProcessStatus status, bool isCurrent, int currUserId, string remark=null)
+        public static Grp_ProcessNode Create(int nodeOrder, string nodeName, string nodeDescTips,ProcessStatus status, bool isCurrent, bool isAssist, bool isFileUp,bool isPart, int currUserId, string remark=null)
         {
             return new Grp_ProcessNode
             {
@@ -167,6 +195,9 @@ namespace OASystem.Domain.Entities.Groups
                 NodeDescTips = nodeDescTips,
                 OverallStatus = status,
                 IsCurrent = isCurrent,
+                IsAssist = isAssist,
+                IsPart = isPart,
+                IsFileUp = isFileUp,
                 CreateUserId = currUserId,
                 Remark = remark
             };

+ 1 - 1
OASystem/OASystem.Domain/ViewModels/Groups/DelegationInfoView.cs

@@ -632,7 +632,7 @@ namespace OASystem.Domain.ViewModels.Groups
         /// <summary>
         /// 客户名称
         /// </summary>
-        public string clientName { get; set; }
+        public string ClientName { get; set; }
 
         /// <summary>
         /// 客户手机号

+ 19 - 0
OASystem/OASystem.Domain/ViewModels/Search/NewClientDataView.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OASystem.Domain.ViewModels.Search
+{
+    public class NewClientDataView
+    {
+        public int RowNumber { get; set; }
+        public int Id { get; set; }
+        public string Client { get; set; }
+        public string Contact { get; set; }
+        public string Job { get; set; }
+        public string Telephone { get; set; }
+        public string Location { get; set; }
+    }
+}

+ 67 - 19
OASystem/OASystem.Infrastructure/Repositories/Groups/DelegationInfoRepository.cs

@@ -258,19 +258,30 @@ namespace OASystem.Infrastructure.Repositories.Groups
         /// 团组国家分割字符
         /// 返回 国家数组
         /// </summary>
-        /// <param name="visitCountry"></param>
+        /// <param name="countryString"></param>
         /// <returns></returns>
-        public List<string> GroupSplitCountry(string visitCountry)
+        public List<string> GroupSplitCountry(string countryString)
         {
-            var countrys = new List<string>();
+            if (string.IsNullOrWhiteSpace(countryString))
+                return new List<string>();
 
-            if (!string.IsNullOrEmpty(visitCountry))
+            // 预先定义分隔符
+            var separators = new[]
             {
-                if (visitCountry.Contains("、")) countrys = visitCountry.Split("、").ToList();
-                else if (visitCountry.Contains("|")) countrys = visitCountry.Split("|").ToList();
-                else countrys.Add(visitCountry);
-            }
-            return countrys;
+                ' ', '\t', '\n', '\r',         // 空白字符
+                '、', '|', ',', ',', ';', ';', '/', '\\', // 分隔符
+                '(', ')', '[', ']', '{', '}', '<', '>',    // 括号
+                '【', '】', '(', ')', '《', '》',          // 中文括号
+                '\'', '"', '`',                             // 引号
+                '-', '_', '+', '=', '~', '!', '@', '#', '$', '%', '^', '&', '*',
+                '·', '。', '?', '!', ':', ';', ',', '、', '~', '—',  // 中文标点
+                '€', '£', '¥', '¢'                           // 货币符号
+            };
+
+            return countryString.Trim()
+                               .Split(separators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+                               .Distinct()  // 去重
+                               .ToList();
         }
 
         /// <summary>
@@ -1233,7 +1244,7 @@ namespace OASystem.Infrastructure.Repositories.Groups
             {
                 string sql = string.Format(@"Select Id,TeamName GroupName From  Grp_DelegationInfo 
                                              Where TeamName != '' And IsDel = 0
-                                             Order By Id Desc");
+                                             Order By VisitDate Desc");
                 var _groupNameList = await _sqlSugar.SqlQueryable<GroupNameView>(sql).ToListAsync();
 
                 string visaSql = string.Format(@"Select Id,Name From Sys_SetData Where STid = 41 And IsDel = 0");
@@ -1266,25 +1277,62 @@ namespace OASystem.Infrastructure.Repositories.Groups
             var result = new Result() { Code = -2 };
             if (dto.PortType == 1 || dto.PortType == 2 || dto.PortType == 3)
             {
-                string sql = string.Format(@"Select gdjc.GrpDCId grpId,cdc.Id,cdc.LastName+cdc.FirstName clientName,cdc.Tel,ccc.CertNo CerdNo 
-                                             From  Grp_DelegationJoinCustomer gdjc
-                                             Inner join Crm_DeleClient cdc On gdjc.CrmDCId =  cdc.Id
-                                             Left Join Crm_CustomerCert ccc On ccc.SdId = 773 And  cdc.Id = ccc.DcId 
-                                             Where gdjc.GrpDCId = {0}", dto.GroupId);
 
+                var clientInfos = await _sqlSugar.Queryable<Grp_TourClientList>().Where(x => x.IsDel == 0 && x.DiId == dto.GroupId).ToListAsync();
+                var clientIds = clientInfos.Select(x => x.ClientId).ToList();
 
-                var clientList = await _sqlSugar.SqlQueryable<CrmByGroupIdView>(sql).ToListAsync();
+                var visaClientInfos = await _sqlSugar.Queryable<Crm_DeleClient>().Where(x => x.IsDel == 0 && clientIds.Contains(x.Id))
+                    .Select(x => new Crm_DeleClient
+                    {
+                        DiId = dto.GroupId,
+                        Id = x.Id,
+                        FirstName = x.FirstName,
+                        LastName = x.LastName,
+                        Tel = x.Tel
+                    })
+                    .ToListAsync();
+
+                var clientCertInfos = await _sqlSugar.Queryable<Crm_CustomerCert>().Where(x => x.IsDel == 0 && x.SdId == 773 && clientIds.Contains(x.DcId))
+                    .Select(x => new Crm_CustomerCert
+                    {
+                        DcId = x.DcId,
+                        CertNo = x.CertNo
+                    })
+                    .ToListAsync();
 
-                if (clientList.Count > 0)
+                //解密
+                foreach (var item in visaClientInfos)
                 {
-                    result.Code = 0;
+                    EncryptionProcessor.DecryptProperties(item);
+                }
+
+                //信息整合
+                var view = new List<CrmByGroupIdView>();
+                foreach (var item in visaClientInfos)
+                {
+                    var idNo = AesEncryptionHelper.Decrypt(clientCertInfos.FirstOrDefault(x => x.DcId == item.Id)?.CertNo ?? "") ?? "-";
+
+                    if (string.IsNullOrEmpty(idNo)) idNo = "-";
+
+                    view.Add(new() {
+                        GrpId = dto.GroupId,
+                        ClientName = item.LastName + item.FirstName,
+                        Tel = item.Tel ?? "-",
+                        CerdNo = idNo
+                    });
+                }
+
+                if (view.Count > 0)
+                {
+
                     result.Msg = "成功!";
-                    result.Data = clientList;
+                    result.Data = view;
                 }
                 else
                 {
                     result.Msg = "暂无数据!";
                 }
+                result.Code = 0;
             }
 
             return result;

+ 233 - 85
OASystem/OASystem.Infrastructure/Repositories/Groups/ProcessOverviewRepository.cs

@@ -21,7 +21,6 @@ namespace OASystem.Infrastructure.Repositories.Groups
             _groupRep = groupRep;
         }
 
-
         /// <summary>
         /// 基础数据初始化-团组流程
         /// </summary>
@@ -50,20 +49,22 @@ namespace OASystem.Infrastructure.Repositories.Groups
             string oaNode2Tips = "客户提供完整名单后,2周内取得邀请函(翻译件)。";
             if (custInfo != null)
             {
-                oaNode2Tips = $"请于{custInfo.CreateTime.AddDays(14):yyyy年MM月dd日}内完成该项工作(客户提供完整名单后,2周内取得邀请函(翻译件))";
+                oaNode2Tips = $"请于{custInfo.CreateTime.AddDays(14):yyyy年MM月dd日}内完成该项工作(客户提供完整名单后,2周内取得邀请函(翻译件))";
             }
 
-            var oaNode4Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(按进度实际公务活动落实情况,出发前5日落实公务。)";
+            var oaNode4Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(按进度实际公务活动落实情况,出发前5日落实公务)";
+            var oaNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组结束前完成)";
             processs.Add(
                 Grp_ProcessOverview.Create(groupId, 1, GroupProcessType.Invitation, ProcessStatus.InProgress, currUserId,
                 new List<Grp_ProcessNode>()
                     {
-                        Grp_ProcessNode.Create(1, "报批基础资料准备","更新报批行程和请示,提供其他报批所需材料,4个工作日内完成。",ProcessStatus.InProgress, true,currUserId),
-                        Grp_ProcessNode.Create(2, "报批邀请函资料准备",oaNode2Tips, ProcessStatus.InProgress, false,currUserId),
-                        Grp_ProcessNode.Create(3, "获得批件","提供完整的报批全套资源。",ProcessStatus.InProgress, false, currUserId ),
-                        Grp_ProcessNode.Create(4, "对接公务",oaNode4Tips,ProcessStatus.InProgress, false, currUserId),
-                        Grp_ProcessNode.Create(5, "参与翻译对接","",ProcessStatus.InProgress, false, currUserId),
-                        Grp_ProcessNode.Create(6, "商邀文案配合","",ProcessStatus.InProgress, false, currUserId),
+                        Grp_ProcessNode.Create(1, "报批基础资料准备","更新报批行程和请示,提供其他报批所需材料,4个工作日内完成。",ProcessStatus.InProgress, true,false,false,false,currUserId),
+                        Grp_ProcessNode.Create(2, "报批邀请函资料准备",oaNode2Tips, ProcessStatus.InProgress, false,false,false,false,currUserId),
+                        Grp_ProcessNode.Create(3, "获得批件","提供完整的报批全套资源。",ProcessStatus.InProgress, false,false,false,false, currUserId ),
+                        Grp_ProcessNode.Create(4, "对接公务",oaNode4Tips,ProcessStatus.InProgress, false,false,false,false, currUserId),
+                        Grp_ProcessNode.Create(5, "参与翻译对接","",ProcessStatus.InProgress, false,false,false,true, currUserId),
+                        Grp_ProcessNode.Create(6, "商邀文案配合","",ProcessStatus.InProgress, false,false,false,false, currUserId),
+                        Grp_ProcessNode.Create(7, "票据上传(相关票据)",oaNode7Tips,ProcessStatus.InProgress, false,false,true,false, currUserId),
                     }));
             #endregion
 
@@ -80,10 +81,12 @@ namespace OASystem.Infrastructure.Repositories.Groups
                     visaDefualtNodes.Add(VisaProcessNode.Info(i, visaCountries[i - 1].ToString()));
                 }
 
-                visaNodes.Add(Grp_ProcessNode.Create(1, "签证", "", ProcessStatus.InProgress, true, currUserId, JsonConvert.SerializeObject(visaDefualtNodes)));
+                var visaNode2Tips = $"请于{groupInfo.VisitDate:yyyy年MM月dd日}内完成该项工作(按进度实际签证办理落实情况,团组出发前上传票据。)";
+                visaNodes.Add(Grp_ProcessNode.Create(1, "签证信息", "", ProcessStatus.InProgress, true, false, false, false, currUserId, JsonConvert.SerializeObject(visaDefualtNodes)));
+                visaNodes.Add(Grp_ProcessNode.Create(2, "票据上传(明细表、费用票据、保单及超支费用账单)", visaNode2Tips, ProcessStatus.InProgress, false, false, true, false, currUserId));
             }
 
-            processs.Add(Grp_ProcessOverview.Create(groupId, 2, GroupProcessType.Visa, ProcessStatus.InProgress, currUserId, visaNodes));
+            processs.Add(Grp_ProcessOverview.Create(groupId, 2, GroupProcessType.Visa, ProcessStatus.UnStarted, currUserId, visaNodes));
             #endregion
 
             #region 机票流程
@@ -95,18 +98,24 @@ namespace OASystem.Infrastructure.Repositories.Groups
                     airNode1Tips = $"请于{groupInfo.StepOperationTime.Value.AddDays(1):yyyy年MM月dd日}内完成该项工作(建团后打勾确认出团的时候开始24小时内)";
                 }
             }
-            
+
+            var airNode3Tips = $"完成机票采购确认(含预算核对、出票确认等)";
             var airNode5Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5日)";
+            var airNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组归国后5个工作日内)";
+            var airNode8Tips = $"1. 票据上传(机票报销蓝联、行程单及机票说明) \r\n 2. 请于{groupInfo.VisitEndDate.AddDays(10):yyyy年MM月dd日}内完成该项工作(团组归国后10个工作日内) *按机票报价*0.999折扣出具机票报销蓝联、行程单及机票说明";
+
             processs.Add(
                 Grp_ProcessOverview.Create(groupId, 3, GroupProcessType.AirTicket, ProcessStatus.InProgress, currUserId,
                 new List<Grp_ProcessNode>()
                     {
-                        Grp_ProcessNode.Create(1, "初步拟定航程方案及价格",   airNode1Tips, ProcessStatus.InProgress, true,currUserId ),
-                        Grp_ProcessNode.Create(2, "机票占位、续位",   "", ProcessStatus.UnStarted, false,currUserId ),
-                        Grp_ProcessNode.Create(3, "完成机票采购确认(含预算核对、出票确认等)",  "", ProcessStatus.UnStarted,false,currUserId),
-                        Grp_ProcessNode.Create(4, "进行出票操作并核查信息", "", ProcessStatus.UnStarted, false, currUserId),
-                        Grp_ProcessNode.Create(5, "机票已出",  airNode5Tips, ProcessStatus.UnStarted, false, currUserId),
-                        Grp_ProcessNode.Create(6, "完成机票选座",  "", ProcessStatus.UnStarted, false,currUserId)
+                        Grp_ProcessNode.Create(1, "初步拟定航程方案及价格",   airNode1Tips, ProcessStatus.InProgress, true,false,false,false, currUserId ),
+                        Grp_ProcessNode.Create(2, "机票占位、续位",   "", ProcessStatus.UnStarted, false,false,false,false,currUserId ),
+                        Grp_ProcessNode.Create(3, "完成机票采购确认",  airNode3Tips, ProcessStatus.UnStarted,false,false,false,false, currUserId),
+                        Grp_ProcessNode.Create(4, "进行出票操作并核查信息", "", ProcessStatus.UnStarted, false,false,false,false, currUserId),
+                        Grp_ProcessNode.Create(5, "机票已出",  airNode5Tips, ProcessStatus.UnStarted, false,false,false,false, currUserId),
+                        Grp_ProcessNode.Create(6, "完成机票选座", "", ProcessStatus.UnStarted, false,false,false,false,currUserId),
+                        Grp_ProcessNode.Create(7, "票据上传(机票超支费用账单)", airNode7Tips, ProcessStatus.UnStarted, false,false,true,false, currUserId),
+                        Grp_ProcessNode.Create(8, "票据上传", airNode8Tips, ProcessStatus.UnStarted, false,false,true,false, currUserId)
                     }
                 )
              );
@@ -114,7 +123,7 @@ namespace OASystem.Infrastructure.Repositories.Groups
 
             #region 酒店流程
 
-            string hotelNode1Tips = "建团后打勾确认出团的时候开始2个工作日。";
+            string hotelNode1Tips = "1. 筛选并按照预算标准,对目标酒店进行询价、比价、谈价 \r\n2. 建团后打勾确认出团的时候开始2个工作日。";
             if (groupInfo.Step == 1 || groupInfo.Step == 2)
             {
                 if (groupInfo.StepOperationTime.HasValue)
@@ -123,18 +132,18 @@ namespace OASystem.Infrastructure.Repositories.Groups
                 }
             }
 
-            var hotelNode4Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5天)";
-            var hotelNode5Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组结束后5天内)";
+            var hotelNode4Tips = $"1.行前再次确认酒店订单、付款状态及入住安排 \r\n 2.请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5天)";
+            var hotelNode5Tips = $"1.行程结束后整理酒店发票(含超支费用发票)与结算 \r\n 2.请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组结束后5天内)";
 
             processs.Add(
                 Grp_ProcessOverview.Create(groupId, 4, GroupProcessType.Hotel, ProcessStatus.InProgress, currUserId,
                      new List<Grp_ProcessNode>()
                     {
-                        Grp_ProcessNode.Create(1, "筛选并按照预算标准对目标酒店进行询价、比价、谈价", hotelNode1Tips, ProcessStatus.InProgress, true, currUserId),
-                        Grp_ProcessNode.Create(2, "获取酒店确认函与入住名单核对", "", ProcessStatus.UnStarted, false, currUserId ),
-                        Grp_ProcessNode.Create(3, "预订酒店并录入OA",  "", ProcessStatus.UnStarted,false,currUserId ),
-                        Grp_ProcessNode.Create(4, "行前再次确认酒店订单、付款状态及入住安排",  hotelNode4Tips,ProcessStatus.UnStarted, false,currUserId ),
-                        Grp_ProcessNode.Create(5, "行程结束后整理酒店发票与结算",  hotelNode5Tips, ProcessStatus.UnStarted, false, currUserId ),
+                        Grp_ProcessNode.Create(1, "按照预算,询价、比价、谈价", hotelNode1Tips, ProcessStatus.InProgress, true, false, false, false, currUserId),
+                        Grp_ProcessNode.Create(2, "获取酒店确认函与入住名单核对", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
+                        Grp_ProcessNode.Create(3, "预订酒店并录入OA",  "", ProcessStatus.UnStarted,false, false, false,false,currUserId ),
+                        Grp_ProcessNode.Create(4, "行前再次确认酒店相关情况",  hotelNode4Tips,ProcessStatus.UnStarted, false, false, false,false,currUserId ),
+                        Grp_ProcessNode.Create(5, "行程结束后整理酒店发票与结算",  hotelNode5Tips, ProcessStatus.UnStarted, false, false, true,false, currUserId ),
                     }
                 )
             );
@@ -151,12 +160,13 @@ namespace OASystem.Infrastructure.Repositories.Groups
                 opNode1Tips = $"请于{airTripCodeInfo.CreateTime.AddDays(1):yyyy年MM月dd日}内完成该项工作(机票行程代码最后一段录入后1个工作日内)";
             }
 
-            string opNode2Tips = $"请于{groupInfo.CreateTime.AddDays(7):yyyy年MM月dd日}内完成该项工作(建团完成后7个工作日内)";
+
+            string opNode2Tips = $"1.联系并询价地接、餐厅、用车、景点等供应商 \r\n 2. 请于{groupInfo.CreateTime.AddDays(7):yyyy年MM月dd日}内完成该项工作(建团完成后7个工作日内)";
             string opNode3Tips = $"请于{groupInfo.CreateTime.AddDays(10):yyyy年MM月dd日}内完成该项工作(上一步往后3个工作日内)";
             string opNode4Tips = $"请于{groupInfo.CreateTime.AddDays(12):yyyy年MM月dd日}内完成该项工作(上一步往后2个工作日内)";
 
             var backListInfo = await _sqlSugar.Queryable<Grp_InvertedList>().Where(x => x.DiId == groupId && x.IsDel == 0).FirstAsync();
-            string opNode5Tips = $"倒推表里开行前会 -3天。";
+            string opNode5Tips = $"1.制定最终《行程单》及《出行手册》 \r\n2. 倒推表里开行前会 -3天。";
             if (backListInfo != null) {
 
                 if (DateTime.TryParse(backListInfo.PreTripMeetingDt,out DateTime dateTime))
@@ -164,16 +174,19 @@ namespace OASystem.Infrastructure.Repositories.Groups
                     opNode5Tips = $"请于{dateTime.AddDays(-3):yyyy年MM月dd日}内完成该项工作(倒推表里开行前会 -3天)";
                 }
             }
+
+            string opNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组归国后5个工作日内) *上传最终报批行程,确定城市间交通最终版报价分配;地接账单(清楚标注超时及其他项超支费用)、地接交通费用原始票据、城市间交通明细表;";
             processs.Add(
                 Grp_ProcessOverview.Create(groupId, 5, GroupProcessType.LocalGuide, ProcessStatus.InProgress, currUserId,
                      new List<Grp_ProcessNode>()
                     {
-                        Grp_ProcessNode.Create(1,"根据机票方案出框架行程", opNode1Tips,ProcessStatus.InProgress, true, currUserId ),
-                        Grp_ProcessNode.Create(2,"联系并询价地接、餐厅、用车、景点等供应商", opNode2Tips,ProcessStatus.UnStarted, false, currUserId ),
-                        Grp_ProcessNode.Create(3,"提交供应商报价及比价表", opNode3Tips, ProcessStatus.UnStarted, false, currUserId),
-                        Grp_ProcessNode.Create(4,"执行采购流程", opNode4Tips, ProcessStatus.UnStarted, false, currUserId),
-                        Grp_ProcessNode.Create(5,"制定最终《行程表》及《出行手册》", opNode5Tips, ProcessStatus.UnStarted, false, currUserId ),
-                        Grp_ProcessNode.Create(6,"送机", "", ProcessStatus.UnStarted, false, currUserId ),
+                        Grp_ProcessNode.Create(1,"根据机票方案出框架行程", opNode1Tips,ProcessStatus.InProgress, true, false, false,false,currUserId ),
+                        Grp_ProcessNode.Create(2,"联系并询价地接相关的供应商", opNode2Tips,ProcessStatus.UnStarted, false, false, false,false, currUserId ),
+                        Grp_ProcessNode.Create(3,"提交供应商报价及比价表", opNode3Tips, ProcessStatus.UnStarted, false, false, false, false,currUserId),
+                        Grp_ProcessNode.Create(4,"执行采购流程", opNode4Tips, ProcessStatus.UnStarted, false, false, false,false, currUserId),
+                        Grp_ProcessNode.Create(5,"制定最终行程单及出行手册", opNode5Tips, ProcessStatus.UnStarted, false, false, false,false, currUserId ),
+                        Grp_ProcessNode.Create(6,"送机", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
+                        Grp_ProcessNode.Create(7,"最终版报批行程、票据上传", opNode7Tips, ProcessStatus.UnStarted, false, false, true, false,currUserId )
                     }
                 )
             );
@@ -181,12 +194,15 @@ namespace OASystem.Infrastructure.Repositories.Groups
             #endregion
 
             #region 费用结算流程
+            var feeNode3Tips = $"1.整理统计团组超支费用、三公报销资料给到各单位 \r\n 2. 请于{groupInfo.VisitEndDate.AddDays(12):yyyy年MM月dd日}内完成该项工作(团组归国后12个工作日内)";
             processs.Add(
                 Grp_ProcessOverview.Create(groupId, 6, GroupProcessType.FeeSettle, ProcessStatus.InProgress, currUserId,
                     new List<Grp_ProcessNode>()
                     {
-                        Grp_ProcessNode.Create(1, "费用结算中", "", ProcessStatus.InProgress, true,currUserId ),
-                        Grp_ProcessNode.Create(2, "费用结算完毕", "", ProcessStatus.UnStarted, false,currUserId ),
+                        Grp_ProcessNode.Create(1, "城市间交通报批金额核定", "团组报批前", ProcessStatus.InProgress, true, true, false,false,currUserId ),
+                        Grp_ProcessNode.Create(2, "团组全程各段机票打票金额的核定", "团组报批后、订票前", ProcessStatus.UnStarted, false, false, false,false,currUserId ),
+                        Grp_ProcessNode.Create(3, "整理统计相关财务资料给到各单位", feeNode3Tips, ProcessStatus.UnStarted, false, false, false,false,currUserId ),
+                        Grp_ProcessNode.Create(4, "费用结算完毕", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
                     }
                 )
             );
@@ -235,7 +251,7 @@ namespace OASystem.Infrastructure.Repositories.Groups
                     _sqlSugar.RollbackTran();
                     return new Result { Code = 400, Msg = "团组流程进度总览表添加失败!" };
                 }
-
+                item.Id = processId;
                 // 记录流程日志
                 await LogProcessOpAsync(null, item, "Create", currUserId);
 
@@ -248,6 +264,9 @@ namespace OASystem.Infrastructure.Repositories.Groups
                     NodeDescTips = nodeDto.NodeDescTips,
                     //Country = nodeDto.Country,
                     IsCurrent = nodeDto.IsCurrent,
+                    IsAssist = nodeDto.IsAssist,
+                    IsPart = nodeDto.IsPart,
+                    IsFileUp = nodeDto.IsFileUp,
                     Remark = nodeDto.Remark
                 }).ToList();
 
@@ -258,6 +277,9 @@ namespace OASystem.Infrastructure.Repositories.Groups
                     return new Result { Code = 400, Msg = "团组流程进度流程节点添加失败!" };
                 }
 
+                //设置节点ID
+                nodes = await _sqlSugar.Queryable<Grp_ProcessNode>().Where(x => x.IsDel == 0 && x.ProcessId == processId).ToListAsync();
+
                 //记录节点日志
                 foreach (var node in nodes)
                 {
@@ -302,40 +324,87 @@ namespace OASystem.Infrastructure.Repositories.Groups
                 .Mapper(p => p.Nodes, p => p.Nodes.First().ProcessId)
                 .ToListAsync();
 
-            var processes = processData.Select(p => new
+            // 预先构建用户字典,提升查询性能
+            var userDict = users.ToDictionary(u => u.Id, u => u.CnName);
+
+            var processes = processData.Select(p =>
             {
-                p.Id,
-                p.GroupId,
-                p.ProcessType,
-                ProcessName = p.ProcessType.GetEnumDescription(),
-                //p.OverallStatus,
-                //StatusText = p.OverallStatus.GetDescription(),
-                Nodes = p.Nodes.Select(n =>
-                {
-                    //单独处理签证板块
-                    var visaSubNodes = new List<VisaProcessNode>();
-                    string remark = string.Empty;
+                var orderedNodes = p.Nodes.OrderBy(n => n.NodeOrder).ToList();
+                var totalNodes = orderedNodes.Count;
 
-                    if (p.ProcessType == GroupProcessType.Visa)
-                    {
-                        visaSubNodes = JsonConvert.DeserializeObject<List<VisaProcessNode>>(n.Remark);
-                    }
-                    return new
+                return new
+                {
+                    p.Id,
+                    p.GroupId,
+                    p.ProcessType,
+                    ProcessName = p.ProcessType.GetEnumDescription(),
+                    Nodes = orderedNodes.Select((n, index) =>
                     {
-                        n.Id,
-                        n.ProcessId,
-                        n.NodeOrder,
-                        n.NodeName,
-                        n.OverallStatus,
-                        StatusText = n.OverallStatus.GetEnumDescription(),
-                        Operator = users.FirstOrDefault(u => u.Id == n.Operator)?.CnName ?? "-",
-                        OpeateTime = n.OperationTime.HasValue ? n.OperationTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : "-",
-                        ActualDone = n.ActualDone?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
-                        n.NodeDescTips,
-                        //节点类型为签证时使用
-                        visaSubNodes
-                    };
-                }).OrderBy(n => n.NodeOrder).ToList()
+                        var isLastNode = index == totalNodes - 1;
+                        var isSecondLastNode = index == totalNodes - 2;
+                        var isFifthStep = index == 4; 
+
+                        // 计算按钮状态
+                        bool isEnaAssistBtn = p.ProcessType == GroupProcessType.FeeSettle && n.NodeOrder == 1;
+
+                        // 文件上传按钮启用规则
+                        bool isEnaFileUpBtn = false;
+                        // 是否参与按钮启用
+                        bool isEnaPartBtn = false;
+
+                        // 规则1:商邀流程第五步启用参与按钮
+                        if (p.ProcessType == GroupProcessType.Invitation && isFifthStep)
+                        {
+                            isEnaPartBtn = true;
+                        }
+                        // 规则2:机票流程倒数第二步启用上传按钮
+                        else if (p.ProcessType == GroupProcessType.AirTicket && isSecondLastNode)
+                        {
+                            isEnaFileUpBtn = true;
+                        }
+                        // 规则3:默认流程节点最后一步启用上传按钮
+                        else if (isLastNode && p.ProcessType != GroupProcessType.FeeSettle)
+                        {
+                            isEnaFileUpBtn = true;
+                        }
+
+                        // 处理签证子节点
+                        List<VisaProcessNode> visaSubNodes = new();
+                        if (p.ProcessType == GroupProcessType.Visa && n.NodeOrder == 1)
+                        {
+                            visaSubNodes = JsonConvert.DeserializeObject<List<VisaProcessNode>>(n.Remark ?? "[]")
+                                ?? new List<VisaProcessNode>();
+                        }
+
+                        // 获取操作人姓名(使用字典提升性能)
+                        string operatorName = "-";
+                        if (n.Operator.HasValue && userDict.TryGetValue(n.Operator.Value, out var name))
+                        {
+                            operatorName = name;
+                        }
+
+                        return new
+                        {
+                            n.Id,
+                            n.ProcessId,
+                            n.NodeOrder,
+                            n.NodeName,
+                            n.OverallStatus,
+                            StatusText = n.OverallStatus.GetEnumDescription(),
+                            Operator = operatorName,
+                            OpeateTime = n.OperationTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-",
+                            ActualDone = n.ActualDone?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
+                            n.NodeDescTips,
+                            isEnaAssistBtn,  // 是否启用财务流程首节点协助按钮
+                            n.IsAssist,      // 财务流程首节点 存储值
+                            isEnaFileUpBtn,  // 是否启用上传文件按钮
+                            n.IsFileUp,      // 票据上传节点 存储值
+                            isEnaPartBtn,    // 是否启用参与按钮
+                            n.IsPart,        // 参与按钮 存储值
+                            visaSubNodes     // 签证节点类型使用
+                        };
+                    }).ToList()
+                };
             }).ToList();
 
             return new Result { Code = 200, Data = processes, Msg = "查询成功!" };
@@ -357,21 +426,11 @@ namespace OASystem.Infrastructure.Repositories.Groups
                 {
                     // 1. 获取并验证节点
                     var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
-                        .FirstAsync(n => n.Id == nodeId && n.IsDel == 0);
-
-                    if (node == null)
-                    {
-                        throw new BusinessException("当前节点不存在或已被删除。");
-                    }
+                        .FirstAsync(n => n.Id == nodeId && n.IsDel == 0) ?? throw new BusinessException("当前节点不存在或已被删除。");
 
                     // 2. 获取流程信息,检查ProcessType
                     var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
-                        .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0);
-
-                    if (process == null)
-                    {
-                        throw new BusinessException("关联的流程不存在。");
-                    }
+                        .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0) ?? throw new BusinessException("关联的流程不存在。");
 
                     // 3. 节点操作验证
                     ValidateNodeOperation(node, processStatus);
@@ -701,12 +760,19 @@ namespace OASystem.Infrastructure.Repositories.Groups
         }
 
         /// <summary>
-        /// 更新签证节点信息及状态
+        /// 更新节点信息及状态
         /// </summary>
         /// <param name="dto">签证节点更新数据传输对象</param>
         /// <returns>操作结果</returns>
-        public async Task<Result> SetActualDoneAsync(int nodeId, DateTime dt, int currUserId)
+        public async Task<Result> SetActualDoneAsync(GroupProcessSetActualDoneDto dto )
         {
+            int nodeId = dto.NodeId;
+            var isDtNul = DateTime.TryParse(dto.ActualDone,out DateTime actualDone);
+            int currUserId = dto.CurrUserId;
+            bool isAssist = dto.IsAssist;
+            bool isFileUp = dto.IsFileUp;
+            bool isPart = dto.IsPart;
+
             // 1. 获取并验证节点和流程
             var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
                 .FirstAsync(n => n.Id == nodeId && n.IsDel == 0)
@@ -726,16 +792,26 @@ namespace OASystem.Infrastructure.Repositories.Groups
                 OverallStatus = node.OverallStatus,
                 Operator = node.Operator,
                 OperationTime = node.OperationTime,
-                IsCurrent = node.IsCurrent,
+                IsCurrent = node.IsCurrent
             };
 
-            node.ActualDone = dt;
+            if (isDtNul)
+            {
+                node.ActualDone = actualDone;
+            }
+            
+            node.IsAssist = isAssist;
+            node.IsFileUp = isFileUp;
+            node.IsPart = isPart;
 
             // 3. 保存节点更新
             await _sqlSugar.Updateable(node)
                 .UpdateColumns(n => new
                 {
-                    n.ActualDone
+                    ActualDone = isDtNul ? node.ActualDone : null,
+                    n.IsAssist,
+                    n.IsFileUp,
+                    n.IsPart,
                 })
                 .ExecuteCommandAsync();
             //记录节点日志
@@ -1077,5 +1153,77 @@ namespace OASystem.Infrastructure.Repositories.Groups
 
 
         #endregion
+
+        #region 节点按钮策略
+        // 定义按钮状态计算策略接口
+        public interface IButtonStateStrategy
+        {
+            (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index);
+        }
+
+        // 通用策略
+        public class DefaultButtonStateStrategy : IButtonStateStrategy
+        {
+            public (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index)
+            {
+                var totalNodes = process.Nodes.Count();
+                var isLastNode = index == totalNodes - 1;
+
+                return (false, isLastNode); // 默认只有最后一步启用文件上传
+            }
+        }
+
+        // 财务流程策略
+        public class FeeSettleButtonStateStrategy : IButtonStateStrategy
+        {
+            public (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index)
+            {
+                var totalNodes = process.Nodes.Count();
+                var isLastNode = index == totalNodes - 1;
+
+                bool isEnaAssistBtn = node.NodeOrder == 1; // 首节点启用协助
+                bool isEnaFileUpBtn = isLastNode; // 最后一步启用文件上传
+
+                return (isEnaAssistBtn, isEnaFileUpBtn);
+            }
+        }
+
+        // 机票流程策略
+        public class FlightButtonStateStrategy : IButtonStateStrategy
+        {
+            public (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index)
+            {
+                var totalNodes = process.Nodes.Count();
+                var isLastNode = index == totalNodes - 1;
+                var isSecondLastNode = index == totalNodes - 2;
+
+                bool isEnaAssistBtn = false;
+                bool isEnaFileUpBtn = isLastNode || isSecondLastNode; // 倒数两步都启用文件上传
+
+                return (isEnaAssistBtn, isEnaFileUpBtn);
+            }
+        }
+
+        // 策略工厂
+        public static class ButtonStateStrategyFactory
+        {
+            private static readonly Dictionary<GroupProcessType, IButtonStateStrategy> _strategies = new()
+            {
+                [GroupProcessType.Invitation] = new DefaultButtonStateStrategy(),
+                [GroupProcessType.Visa] = new DefaultButtonStateStrategy(),
+                [GroupProcessType.AirTicket] = new FlightButtonStateStrategy(),
+                [GroupProcessType.Hotel] = new DefaultButtonStateStrategy(),
+                [GroupProcessType.LocalGuide] = new DefaultButtonStateStrategy(),
+                [GroupProcessType.FeeSettle] = new FeeSettleButtonStateStrategy()
+            };
+
+            public static IButtonStateStrategy GetStrategy(GroupProcessType processType)
+            {
+                return _strategies.TryGetValue(processType, out var strategy)
+                    ? strategy
+                    : new DefaultButtonStateStrategy();
+            }
+        }
+        #endregion
     }
 }