ProcessOverviewRepository.cs 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. using AutoMapper;
  2. using Newtonsoft.Json;
  3. using OASystem.Domain;
  4. using OASystem.Domain.Dtos.Groups;
  5. using OASystem.Domain.Entities.Groups;
  6. using OASystem.Domain.Entities.Resource;
  7. using System.Reflection;
  8. namespace OASystem.Infrastructure.Repositories.Groups
  9. {
  10. /// <summary>
  11. /// 团组流程总览表仓储
  12. /// </summary>
  13. public class ProcessOverviewRepository : BaseRepository<Grp_ProcessOverview, Grp_ProcessOverview>
  14. {
  15. private readonly IMapper _mapper;
  16. private readonly DelegationInfoRepository _groupRep;
  17. public ProcessOverviewRepository(SqlSugarClient sqlSugar, IMapper mapper, DelegationInfoRepository groupRep) : base(sqlSugar)
  18. {
  19. _mapper = mapper;
  20. _groupRep = groupRep;
  21. }
  22. /// <summary>
  23. /// 基础数据初始化-团组流程
  24. /// </summary>
  25. /// <param name="groupId"></param>
  26. /// <param name="currUserId"></param>
  27. /// <returns></returns>
  28. public async Task<List<Grp_ProcessOverview>> ProcessDataInitAsync(int groupId, int currUserId, List<string> visaCountries)
  29. {
  30. var processs = new List<Grp_ProcessOverview>();
  31. //团组验证
  32. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  33. if (groupInfo == null) return processs;
  34. // 检查是否已存在流程
  35. var existingProcesses = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  36. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  37. .ToListAsync();
  38. if (existingProcesses.Any()) return processs;
  39. #region 商邀报批流程
  40. var custInfo = await _sqlSugar.Queryable<Grp_TourClientList>()
  41. .Where(c => c.DiId == groupId && c.IsDel == 0)
  42. .OrderByDescending(c => c.CreateTime)
  43. .FirstAsync();
  44. string oaNode2Tips = "客户提供完整名单后,2周内取得邀请函(翻译件)。";
  45. if (custInfo != null)
  46. {
  47. oaNode2Tips = $"请于{custInfo.CreateTime.AddDays(14):yyyy年MM月dd日}内完成该项工作(客户提供完整名单后,2周内取得邀请函(翻译件))";
  48. }
  49. var oaNode4Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(按进度实际公务活动落实情况,出发前5日落实公务)";
  50. var oaNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组结束前完成)";
  51. processs.Add(
  52. Grp_ProcessOverview.Create(groupId, 1, GroupProcessType.Invitation, ProcessStatus.InProgress, currUserId,
  53. new List<Grp_ProcessNode>()
  54. {
  55. Grp_ProcessNode.Create(1, "报批基础资料准备","更新报批行程和请示,提供其他报批所需材料,4个工作日内完成。",ProcessStatus.InProgress, true,false,false,false,currUserId),
  56. Grp_ProcessNode.Create(2, "报批邀请函资料准备",oaNode2Tips, ProcessStatus.InProgress, false,false,false,false,currUserId),
  57. Grp_ProcessNode.Create(3, "获得批件","提供完整的报批全套资源。",ProcessStatus.InProgress, false,false,false,false, currUserId ),
  58. Grp_ProcessNode.Create(4, "对接公务",oaNode4Tips,ProcessStatus.InProgress, false,false,false,false, currUserId),
  59. Grp_ProcessNode.Create(5, "参与翻译对接","",ProcessStatus.InProgress, false,false,false,true, currUserId),
  60. Grp_ProcessNode.Create(6, "商邀文案配合","",ProcessStatus.InProgress, false,false,false,false, currUserId),
  61. Grp_ProcessNode.Create(7, "票据上传(相关票据)",oaNode7Tips,ProcessStatus.InProgress, false,false,true,false, currUserId),
  62. }));
  63. #endregion
  64. #region 签证流程
  65. //单独处理签证流程节点
  66. var visaNodes = new List<Grp_ProcessNode>();
  67. if (visaCountries != null && visaCountries.Count > 0)
  68. {
  69. var visaDefualtNodes = new List<VisaProcessNode>();
  70. for (int i = 1; i < visaCountries.Count + 1; i++)
  71. {
  72. visaDefualtNodes.Add(VisaProcessNode.Info(i, visaCountries[i - 1].ToString()));
  73. }
  74. var visaNode2Tips = $"请于{groupInfo.VisitDate:yyyy年MM月dd日}内完成该项工作(按进度实际签证办理落实情况,团组出发前上传票据。)";
  75. visaNodes.Add(Grp_ProcessNode.Create(1, "签证信息", "", ProcessStatus.InProgress, true, false, false, false, currUserId, JsonConvert.SerializeObject(visaDefualtNodes)));
  76. visaNodes.Add(Grp_ProcessNode.Create(2, "票据上传(明细表、费用票据、保单及超支费用账单)", visaNode2Tips, ProcessStatus.InProgress, false, false, true, false, currUserId));
  77. }
  78. processs.Add(Grp_ProcessOverview.Create(groupId, 2, GroupProcessType.Visa, ProcessStatus.UnStarted, currUserId, visaNodes));
  79. #endregion
  80. #region 机票流程
  81. string airNode1Tips = "建团后打勾确认出团的时候开始24小时内。";
  82. if (groupInfo.Step == 1 || groupInfo.Step == 2)
  83. {
  84. if (groupInfo.StepOperationTime.HasValue)
  85. {
  86. airNode1Tips = $"请于{groupInfo.StepOperationTime.Value.AddDays(1):yyyy年MM月dd日}内完成该项工作(建团后打勾确认出团的时候开始24小时内)";
  87. }
  88. }
  89. var airNode3Tips = $"完成机票采购确认(含预算核对、出票确认等)";
  90. var airNode5Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5日)";
  91. var airNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组归国后5个工作日内)";
  92. var airNode8Tips = $"1. 票据上传(机票报销蓝联、行程单及机票说明) \r\n 2. 请于{groupInfo.VisitEndDate.AddDays(10):yyyy年MM月dd日}内完成该项工作(团组归国后10个工作日内) *按机票报价*0.999折扣出具机票报销蓝联、行程单及机票说明";
  93. processs.Add(
  94. Grp_ProcessOverview.Create(groupId, 3, GroupProcessType.AirTicket, ProcessStatus.InProgress, currUserId,
  95. new List<Grp_ProcessNode>()
  96. {
  97. Grp_ProcessNode.Create(1, "初步拟定航程方案及价格", airNode1Tips, ProcessStatus.InProgress, true,false,false,false, currUserId ),
  98. Grp_ProcessNode.Create(2, "机票占位、续位", "", ProcessStatus.UnStarted, false,false,false,false,currUserId ),
  99. Grp_ProcessNode.Create(3, "完成机票采购确认", airNode3Tips, ProcessStatus.UnStarted,false,false,false,false, currUserId),
  100. Grp_ProcessNode.Create(4, "进行出票操作并核查信息", "", ProcessStatus.UnStarted, false,false,false,false, currUserId),
  101. Grp_ProcessNode.Create(5, "机票已出", airNode5Tips, ProcessStatus.UnStarted, false,false,false,false, currUserId),
  102. Grp_ProcessNode.Create(6, "完成机票选座", "", ProcessStatus.UnStarted, false,false,false,false,currUserId),
  103. Grp_ProcessNode.Create(7, "票据上传(机票超支费用账单)", airNode7Tips, ProcessStatus.UnStarted, false,false,true,false, currUserId),
  104. Grp_ProcessNode.Create(8, "票据上传", airNode8Tips, ProcessStatus.UnStarted, false,false,true,false, currUserId)
  105. }
  106. )
  107. );
  108. #endregion
  109. #region 酒店流程
  110. string hotelNode1Tips = "1. 筛选并按照预算标准,对目标酒店进行询价、比价、谈价 \r\n2. 建团后打勾确认出团的时候开始2个工作日。";
  111. if (groupInfo.Step == 1 || groupInfo.Step == 2)
  112. {
  113. if (groupInfo.StepOperationTime.HasValue)
  114. {
  115. hotelNode1Tips = $"请于{groupInfo.StepOperationTime.Value.AddDays(2):yyyy年MM月dd日}内完成该项工作(建团后打勾确认出团的时候开始2个工作日)";
  116. }
  117. }
  118. var hotelNode4Tips = $"1.行前再次确认酒店订单、付款状态及入住安排 \r\n 2.请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5天)";
  119. var hotelNode5Tips = $"1.行程结束后整理酒店发票(含超支费用发票)与结算 \r\n 2.请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组结束后5天内)";
  120. processs.Add(
  121. Grp_ProcessOverview.Create(groupId, 4, GroupProcessType.Hotel, ProcessStatus.InProgress, currUserId,
  122. new List<Grp_ProcessNode>()
  123. {
  124. Grp_ProcessNode.Create(1, "按照预算,询价、比价、谈价", hotelNode1Tips, ProcessStatus.InProgress, true, false, false, false, currUserId),
  125. Grp_ProcessNode.Create(2, "获取酒店确认函与入住名单核对", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  126. Grp_ProcessNode.Create(3, "预订酒店并录入OA", "", ProcessStatus.UnStarted,false, false, false,false,currUserId ),
  127. Grp_ProcessNode.Create(4, "行前再次确认酒店相关情况", hotelNode4Tips,ProcessStatus.UnStarted, false, false, false,false,currUserId ),
  128. Grp_ProcessNode.Create(5, "行程结束后整理酒店发票与结算", hotelNode5Tips, ProcessStatus.UnStarted, false, false, true,false, currUserId ),
  129. }
  130. )
  131. );
  132. #endregion
  133. #region 地接流程
  134. var airTripCodeInfo = await _sqlSugar.Queryable<Air_TicketBlackCode>()
  135. .Where(x => x.IsDel == 0 && x.DiId == groupId)
  136. .OrderByDescending(x => x.CreateTime)
  137. .FirstAsync();
  138. string opNode1Tips = $"机票行程代码最后一段录入后1个工作日内。";
  139. if (airTripCodeInfo != null)
  140. {
  141. opNode1Tips = $"请于{airTripCodeInfo.CreateTime.AddDays(1):yyyy年MM月dd日}内完成该项工作(机票行程代码最后一段录入后1个工作日内)";
  142. }
  143. string opNode2Tips = $"1.联系并询价地接、餐厅、用车、景点等供应商 \r\n 2. 请于{groupInfo.CreateTime.AddDays(7):yyyy年MM月dd日}内完成该项工作(建团完成后7个工作日内)";
  144. string opNode3Tips = $"请于{groupInfo.CreateTime.AddDays(10):yyyy年MM月dd日}内完成该项工作(上一步往后3个工作日内)";
  145. string opNode4Tips = $"请于{groupInfo.CreateTime.AddDays(12):yyyy年MM月dd日}内完成该项工作(上一步往后2个工作日内)";
  146. var backListInfo = await _sqlSugar.Queryable<Grp_InvertedList>().Where(x => x.DiId == groupId && x.IsDel == 0).FirstAsync();
  147. string opNode5Tips = $"1.制定最终《行程单》及《出行手册》 \r\n2. 倒推表里开行前会 -3天。";
  148. if (backListInfo != null) {
  149. if (DateTime.TryParse(backListInfo.PreTripMeetingDt,out DateTime dateTime))
  150. {
  151. opNode5Tips = $"请于{dateTime.AddDays(-3):yyyy年MM月dd日}内完成该项工作(倒推表里开行前会 -3天)";
  152. }
  153. }
  154. string opNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组归国后5个工作日内) *上传最终报批行程,确定城市间交通最终版报价分配;地接账单(清楚标注超时及其他项超支费用)、地接交通费用原始票据、城市间交通明细表;";
  155. processs.Add(
  156. Grp_ProcessOverview.Create(groupId, 5, GroupProcessType.LocalGuide, ProcessStatus.InProgress, currUserId,
  157. new List<Grp_ProcessNode>()
  158. {
  159. Grp_ProcessNode.Create(1,"根据机票方案出框架行程", opNode1Tips,ProcessStatus.InProgress, true, false, false,false,currUserId ),
  160. Grp_ProcessNode.Create(2,"联系并询价地接相关的供应商", opNode2Tips,ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  161. Grp_ProcessNode.Create(3,"提交供应商报价及比价表", opNode3Tips, ProcessStatus.UnStarted, false, false, false, false,currUserId),
  162. Grp_ProcessNode.Create(4,"执行采购流程", opNode4Tips, ProcessStatus.UnStarted, false, false, false,false, currUserId),
  163. Grp_ProcessNode.Create(5,"制定最终行程单及出行手册", opNode5Tips, ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  164. Grp_ProcessNode.Create(6,"送机", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  165. Grp_ProcessNode.Create(7,"最终版报批行程、票据上传", opNode7Tips, ProcessStatus.UnStarted, false, false, true, false,currUserId )
  166. }
  167. )
  168. );
  169. #endregion
  170. #region 费用结算流程
  171. var feeNode3Tips = $"1.整理统计团组超支费用、三公报销资料给到各单位 \r\n 2. 请于{groupInfo.VisitEndDate.AddDays(12):yyyy年MM月dd日}内完成该项工作(团组归国后12个工作日内)";
  172. processs.Add(
  173. Grp_ProcessOverview.Create(groupId, 6, GroupProcessType.FeeSettle, ProcessStatus.InProgress, currUserId,
  174. new List<Grp_ProcessNode>()
  175. {
  176. Grp_ProcessNode.Create(1, "城市间交通报批金额核定", "团组报批前", ProcessStatus.InProgress, true, true, false,false,currUserId ),
  177. Grp_ProcessNode.Create(2, "团组全程各段机票打票金额的核定", "团组报批后、订票前", ProcessStatus.UnStarted, false, false, false,false,currUserId ),
  178. Grp_ProcessNode.Create(3, "整理统计相关财务资料给到各单位", feeNode3Tips, ProcessStatus.UnStarted, false, false, false,false,currUserId ),
  179. Grp_ProcessNode.Create(4, "费用结算完毕", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  180. }
  181. )
  182. );
  183. #endregion
  184. return processs;
  185. }
  186. /// <summary>
  187. /// 团组流程初始化
  188. /// </summary>
  189. /// <param name="request">创建流程请求参数</param>
  190. /// <returns>创建的流程信息</returns>
  191. public async Task<Result> ProcessInitAsync(int groupId, int currUserId)
  192. {
  193. //团组验证
  194. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  195. if (groupInfo == null)
  196. {
  197. return new Result { Code = 400, Msg = "团组不存在" };
  198. }
  199. // 检查是否已存在流程
  200. var existingProcesses = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  201. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  202. .ToListAsync();
  203. if (existingProcesses.Any())
  204. {
  205. return new Result { Code = 400, Msg = "该团组的流程已存在" };
  206. }
  207. //处理签证国家
  208. var visaCountries = _groupRep.GroupSplitCountry(groupInfo.VisitCountry);
  209. // 定义默认的流程节点
  210. var processs = await ProcessDataInitAsync(groupId, currUserId, visaCountries);
  211. _sqlSugar.BeginTran();
  212. foreach (var item in processs)
  213. {
  214. var processId = await _sqlSugar.Insertable(item).ExecuteReturnIdentityAsync();
  215. if (processId < 1)
  216. {
  217. _sqlSugar.RollbackTran();
  218. return new Result { Code = 400, Msg = "团组流程进度总览表添加失败!" };
  219. }
  220. item.Id = processId;
  221. // 记录流程日志
  222. await LogProcessOpAsync(null, item, "Create", currUserId);
  223. var nodes = item.Nodes.Select((nodeDto, index) => new Grp_ProcessNode
  224. {
  225. ProcessId = processId,
  226. NodeName = nodeDto.NodeName,
  227. NodeOrder = nodeDto.NodeOrder,
  228. OverallStatus = nodeDto.OverallStatus,
  229. NodeDescTips = nodeDto.NodeDescTips,
  230. //Country = nodeDto.Country,
  231. IsCurrent = nodeDto.IsCurrent,
  232. IsAssist = nodeDto.IsAssist,
  233. IsPart = nodeDto.IsPart,
  234. IsFileUp = nodeDto.IsFileUp,
  235. Remark = nodeDto.Remark
  236. }).ToList();
  237. var nodeIds = await _sqlSugar.Insertable(nodes).ExecuteCommandAsync();
  238. if (nodeIds < 1)
  239. {
  240. _sqlSugar.RollbackTran();
  241. return new Result { Code = 400, Msg = "团组流程进度流程节点添加失败!" };
  242. }
  243. //设置节点ID
  244. nodes = await _sqlSugar.Queryable<Grp_ProcessNode>().Where(x => x.IsDel == 0 && x.ProcessId == processId).ToListAsync();
  245. //记录节点日志
  246. foreach (var node in nodes)
  247. {
  248. await LogNodeOpAsync(null, node, "Create", currUserId);
  249. }
  250. }
  251. _sqlSugar.CommitTran();
  252. return new Result { Code = 200, Msg = "添加成功!" }; ;
  253. }
  254. /// <summary>
  255. /// 获取团组的所有流程及流程详情
  256. /// </summary>
  257. /// <param name="request">创建流程请求参数</param>
  258. /// <returns>创建的流程信息</returns>
  259. public async Task<Result> ProcessesDetailsAsync(int groupId)
  260. {
  261. //团组验证
  262. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  263. if (groupInfo == null)
  264. {
  265. return new Result { Code = 400, Msg = "团组不存在" };
  266. }
  267. // 检查是否已存在流程
  268. var existingProcesses = await _sqlSugar.Queryable<Grp_ProcessOverview>().Where(p => p.IsDel == 0 && p.GroupId == groupId).ToListAsync();
  269. if (!existingProcesses.Any())
  270. {
  271. //新建团组流程
  272. var res = await ProcessInitAsync(groupId, 4);
  273. if (res.Code != 200)
  274. {
  275. return res;
  276. }
  277. }
  278. var users = await _sqlSugar.Queryable<Sys_Users>().Select(x => new {x.Id,x.CnName }).ToListAsync();
  279. var processData = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  280. .Where(p => p.GroupId == groupId && p.IsDel == 0)
  281. .Mapper(p => p.Nodes, p => p.Nodes.First().ProcessId)
  282. .ToListAsync();
  283. // 预先构建用户字典,提升查询性能
  284. var userDict = users.ToDictionary(u => u.Id, u => u.CnName);
  285. var processes = processData.Select(p =>
  286. {
  287. var orderedNodes = p.Nodes.OrderBy(n => n.NodeOrder).ToList();
  288. var totalNodes = orderedNodes.Count;
  289. return new
  290. {
  291. p.Id,
  292. p.GroupId,
  293. p.ProcessType,
  294. ProcessName = p.ProcessType.GetEnumDescription(),
  295. Nodes = orderedNodes.Select((n, index) =>
  296. {
  297. var isLastNode = index == totalNodes - 1;
  298. var isSecondLastNode = index == totalNodes - 2;
  299. var isFifthStep = index == 4;
  300. // 计算按钮状态
  301. bool isEnaAssistBtn = p.ProcessType == GroupProcessType.FeeSettle && n.NodeOrder == 1;
  302. // 文件上传按钮启用规则
  303. bool isEnaFileUpBtn = false;
  304. // 是否参与按钮启用
  305. bool isEnaPartBtn = false;
  306. // 规则1:商邀流程第五步启用参与按钮
  307. if (p.ProcessType == GroupProcessType.Invitation && isFifthStep)
  308. {
  309. isEnaPartBtn = true;
  310. }
  311. // 规则2:机票流程倒数第二步启用上传按钮
  312. else if (p.ProcessType == GroupProcessType.AirTicket && isSecondLastNode)
  313. {
  314. isEnaFileUpBtn = true;
  315. }
  316. // 规则3:默认流程节点最后一步启用上传按钮
  317. else if (isLastNode && p.ProcessType != GroupProcessType.FeeSettle)
  318. {
  319. isEnaFileUpBtn = true;
  320. }
  321. // 处理签证子节点
  322. List<VisaProcessNode> visaSubNodes = new();
  323. if (p.ProcessType == GroupProcessType.Visa && n.NodeOrder == 1)
  324. {
  325. visaSubNodes = JsonConvert.DeserializeObject<List<VisaProcessNode>>(n.Remark ?? "[]")
  326. ?? new List<VisaProcessNode>();
  327. }
  328. // 获取操作人姓名(使用字典提升性能)
  329. string operatorName = "-";
  330. if (n.Operator.HasValue && userDict.TryGetValue(n.Operator.Value, out var name))
  331. {
  332. operatorName = name;
  333. }
  334. return new
  335. {
  336. n.Id,
  337. n.ProcessId,
  338. n.NodeOrder,
  339. n.NodeName,
  340. n.OverallStatus,
  341. StatusText = n.OverallStatus.GetEnumDescription(),
  342. Operator = operatorName,
  343. OpeateTime = n.OperationTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-",
  344. ActualDone = n.ActualDone?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
  345. n.NodeDescTips,
  346. isEnaAssistBtn, // 是否启用财务流程首节点协助按钮
  347. n.IsAssist, // 财务流程首节点 存储值
  348. isEnaFileUpBtn, // 是否启用上传文件按钮
  349. n.IsFileUp, // 票据上传节点 存储值
  350. isEnaPartBtn, // 是否启用参与按钮
  351. n.IsPart, // 参与按钮 存储值
  352. visaSubNodes // 签证节点类型使用
  353. };
  354. }).ToList()
  355. };
  356. }).ToList();
  357. return new Result { Code = 200, Data = processes, Msg = "查询成功!" };
  358. }
  359. /// <summary>
  360. /// 更新节点状态
  361. /// </summary>
  362. /// <param name="nodeId">节点ID</param>
  363. /// <param name="currUserId">当前用户ID</param>
  364. /// <param name="processStatus">流程状态,默认为已完成</param>
  365. /// <returns>操作结果</returns>
  366. public async Task<Result> UpdateNodeStatusAsync(int nodeId, int currUserId, ProcessStatus processStatus = ProcessStatus.Completed)
  367. {
  368. try
  369. {
  370. // 使用事务确保数据一致性
  371. var result = await _sqlSugar.Ado.UseTranAsync(async () =>
  372. {
  373. // 1. 获取并验证节点
  374. var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
  375. .FirstAsync(n => n.Id == nodeId && n.IsDel == 0) ?? throw new BusinessException("当前节点不存在或已被删除。");
  376. // 2. 获取流程信息,检查ProcessType
  377. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  378. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0) ?? throw new BusinessException("关联的流程不存在。");
  379. // 3. 节点操作验证
  380. ValidateNodeOperation(node, processStatus);
  381. // 4. 存储更新前的值
  382. var before = new Grp_ProcessNode()
  383. {
  384. Id = node.Id,
  385. ProcessId = node.ProcessId,
  386. NodeName = node.NodeName,
  387. NodeOrder = node.NodeOrder,
  388. OverallStatus = node.OverallStatus,
  389. Operator = node.Operator,
  390. OperationTime = node.OperationTime,
  391. IsCurrent = node.IsCurrent,
  392. };
  393. // 5. 更新节点状态
  394. node.OverallStatus = processStatus;
  395. node.Operator = currUserId;
  396. node.OperationTime = DateTime.Now;
  397. var updateCount = await _sqlSugar.Updateable(node)
  398. .UpdateColumns(n => new
  399. {
  400. n.OverallStatus,
  401. n.Operator,
  402. n.OperationTime
  403. })
  404. .ExecuteCommandAsync();
  405. if (updateCount == 0)
  406. {
  407. throw new BusinessException("节点状态更新失败。");
  408. }
  409. // 6. 记录节点日志
  410. await LogNodeOpAsync(before, node, "Update", currUserId);
  411. // 7. 如果是完成当前节点,处理流程流转
  412. // 当前节点或者流程类型为商邀可进入状态流转
  413. if (processStatus == ProcessStatus.Completed && (node.IsCurrent || process.ProcessType == GroupProcessType.Invitation))
  414. {
  415. await ProcessCurrentNodeCompletionAsync(node, currUserId);
  416. }
  417. return new Result { Code = StatusCodes.Status200OK, Msg = "操作成功。" };
  418. });
  419. return result.IsSuccess ? result.Data : new Result
  420. {
  421. Code = StatusCodes.Status500InternalServerError,
  422. Msg = result.ErrorMessage
  423. };
  424. }
  425. catch (BusinessException ex)
  426. {
  427. // 业务异常
  428. return new Result { Code = StatusCodes.Status400BadRequest, Msg = ex.Message };
  429. }
  430. catch (Exception ex)
  431. {
  432. // 系统异常
  433. return new Result { Code = StatusCodes.Status500InternalServerError, Msg = "系统错误,请稍后重试" };
  434. }
  435. }
  436. /// <summary>
  437. /// 验证节点操作权限
  438. /// </summary>
  439. /// <param name="node">流程节点</param>
  440. /// <param name="targetStatus">目标状态</param>
  441. private static void ValidateNodeOperation(Grp_ProcessNode node, ProcessStatus targetStatus)
  442. {
  443. // 验证节点是否已完成
  444. if (node.OverallStatus == ProcessStatus.Completed)
  445. {
  446. throw new BusinessException("当前节点已完成,不可重复操作。");
  447. }
  448. // 验证状态流转是否合法(可选)
  449. //if (targetStatus != ProcessStatus.Completed)
  450. //{
  451. // throw new BusinessException("未开始或者进行中的节点只能重新完成,不可进行其他操作。");
  452. //}
  453. // 验证是否尝试将已完成节点改为其他状态
  454. if (node.OverallStatus == ProcessStatus.Completed && targetStatus != ProcessStatus.Completed)
  455. {
  456. throw new BusinessException("已完成节点不可修改状态。");
  457. }
  458. }
  459. /// <summary>
  460. /// 处理当前节点完成后的流程流转
  461. /// </summary>
  462. private async Task ProcessCurrentNodeCompletionAsync(Grp_ProcessNode currentNode, int currUserId)
  463. {
  464. // 1. 获取流程信息
  465. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  466. .FirstAsync(p => p.Id == currentNode.ProcessId && p.IsDel == 0);
  467. if (process == null)
  468. {
  469. throw new BusinessException("关联的流程不存在。");
  470. }
  471. var processBefore = new Grp_ProcessOverview()
  472. {
  473. Id = process.Id,
  474. GroupId = process.GroupId,
  475. ProcessOrder = process.ProcessOrder,
  476. ProcessType = process.ProcessType,
  477. OverallStatus = process.OverallStatus,
  478. StartTime = process.StartTime,
  479. EndTime = process.EndTime,
  480. UpdatedUserId = process.UpdatedUserId,
  481. UpdatedTime = process.UpdatedTime
  482. };
  483. // 2. 取消当前节点的当前状态
  484. var before = new Grp_ProcessNode()
  485. {
  486. Id = currentNode.Id,
  487. ProcessId = currentNode.ProcessId,
  488. NodeName = currentNode.NodeName,
  489. NodeOrder = currentNode.NodeOrder,
  490. OverallStatus = currentNode.OverallStatus,
  491. Operator = currentNode.Operator,
  492. OperationTime = currentNode.OperationTime,
  493. IsCurrent = currentNode.IsCurrent,
  494. };
  495. currentNode.IsCurrent = false;
  496. await _sqlSugar.Updateable(currentNode)
  497. .UpdateColumns(n => new { n.IsCurrent })
  498. .ExecuteCommandAsync();
  499. // 2.1 记录节点日志 取消当前节点状态
  500. await LogNodeOpAsync(before, currentNode, "Update", currUserId);
  501. // 3. 查找并激活下一个节点 商邀节点单独处理
  502. if (process.ProcessType == GroupProcessType.Invitation)
  503. {
  504. var invitaNodeStatus = await _sqlSugar.Queryable<Grp_ProcessNode>()
  505. .Where(x => x.IsDel == 0 && x.ProcessId == currentNode.ProcessId)
  506. .ToListAsync();
  507. int completedCount = invitaNodeStatus.Count(n => n.OverallStatus == ProcessStatus.Completed);
  508. int nodeCount = invitaNodeStatus.Count;
  509. if (completedCount == nodeCount) //全部子节点完成,该流程完成
  510. {
  511. process.OverallStatus = ProcessStatus.Completed;
  512. process.EndTime = DateTime.Now;
  513. }
  514. }
  515. else
  516. {
  517. var nextNode = await _sqlSugar.Queryable<Grp_ProcessNode>()
  518. .Where(n => n.ProcessId == currentNode.ProcessId
  519. && n.NodeOrder == currentNode.NodeOrder + 1
  520. && n.IsDel == 0)
  521. .FirstAsync();
  522. if (nextNode != null)
  523. {
  524. var nextNodeBefore = new Grp_ProcessNode()
  525. {
  526. Id = nextNode.Id,
  527. ProcessId = nextNode.ProcessId,
  528. NodeName = nextNode.NodeName,
  529. NodeOrder = nextNode.NodeOrder,
  530. OverallStatus = nextNode.OverallStatus,
  531. Operator = nextNode.Operator,
  532. OperationTime = nextNode.OperationTime,
  533. IsCurrent = nextNode.IsCurrent,
  534. };
  535. // 激活下一个节点
  536. nextNode.IsCurrent = true;
  537. nextNode.OverallStatus = ProcessStatus.InProgress;
  538. //nextNode.Operator = currUserId;
  539. //nextNode.OperationTime = DateTime.Now;
  540. var updateCount = await _sqlSugar.Updateable(nextNode)
  541. .UpdateColumns(n => new
  542. {
  543. n.IsCurrent,
  544. n.OverallStatus,
  545. n.Operator,
  546. n.OperationTime
  547. })
  548. .ExecuteCommandAsync();
  549. if (updateCount == 0)
  550. {
  551. throw new BusinessException("激活下一节点失败");
  552. }
  553. // 1.1 记录节点日志 激活下一节点当前节点状态
  554. await LogNodeOpAsync(nextNodeBefore, nextNode, "Start", currUserId);
  555. // 更新流程状态为进行中
  556. process.OverallStatus = ProcessStatus.InProgress;
  557. }
  558. else
  559. {
  560. // 下一节点不存在,整个流程完成
  561. process.OverallStatus = ProcessStatus.Completed;
  562. process.EndTime = DateTime.Now;
  563. }
  564. }
  565. // 4. 更新流程信息
  566. process.UpdatedUserId = currUserId;
  567. process.UpdatedTime = DateTime.Now;
  568. var processUpdateCount = await _sqlSugar.Updateable(process)
  569. .UpdateColumns(p => new
  570. {
  571. p.OverallStatus,
  572. p.EndTime,
  573. p.UpdatedUserId,
  574. p.UpdatedTime
  575. })
  576. .ExecuteCommandAsync();
  577. if (processUpdateCount == 0)
  578. {
  579. throw new BusinessException("流程状态更新失败。");
  580. }
  581. //记录流程日志
  582. await LogProcessOpAsync(processBefore, process, "Update", currUserId);
  583. }
  584. /// <summary>
  585. /// 更新签证节点信息及状态
  586. /// </summary>
  587. /// <param name="dto">签证节点更新数据传输对象</param>
  588. /// <returns>操作结果</returns>
  589. public async Task<Result> UpdateVisaNodeDetailsAsync(GroupProcessUpdateVisaNodeDetailsDto dto)
  590. {
  591. // 1. 获取并验证节点和流程
  592. var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
  593. .FirstAsync(n => n.Id == dto.NodeId && n.IsDel == 0)
  594. ?? throw new BusinessException("当前节点不存在或已被删除。");
  595. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  596. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0)
  597. ?? throw new BusinessException("当前流程不存在或已被删除。");
  598. if (process.ProcessType != GroupProcessType.Visa)
  599. {
  600. throw new BusinessException("当前流程节点不为签证流程,不可编辑。");
  601. }
  602. // 2. 检查签证子节点 字段信息是否全部填写
  603. var allSubNodesCompleted = dto.VisaSubNodes?.All(subNode => EntityExtensions.IsCompleted(subNode)) ?? false;
  604. // 2.1 存储更新前流程及节点信息
  605. var nodeBefore = new Grp_ProcessNode()
  606. {
  607. Id = node.Id,
  608. ProcessId = node.ProcessId,
  609. NodeName = node.NodeName,
  610. NodeOrder = node.NodeOrder,
  611. OverallStatus = node.OverallStatus,
  612. Operator = node.Operator,
  613. OperationTime = node.OperationTime,
  614. IsCurrent = node.IsCurrent,
  615. };
  616. var processBefore = new Grp_ProcessOverview()
  617. {
  618. Id = process.Id,
  619. GroupId = process.GroupId,
  620. ProcessOrder = process.ProcessOrder,
  621. ProcessType = process.ProcessType,
  622. OverallStatus = process.OverallStatus,
  623. StartTime = process.StartTime,
  624. EndTime = process.EndTime,
  625. UpdatedUserId = process.UpdatedUserId,
  626. UpdatedTime = process.UpdatedTime
  627. };
  628. // 3. 更新节点信息
  629. node.Remark = JsonConvert.SerializeObject(dto.VisaSubNodes);
  630. node.Operator = dto.CurrUserId;
  631. node.OperationTime = DateTime.Now;
  632. if (allSubNodesCompleted)
  633. {
  634. node.OverallStatus = ProcessStatus.Completed;
  635. process.OverallStatus = ProcessStatus.Completed;
  636. process.EndTime = DateTime.Now;
  637. process.UpdatedUserId = dto.CurrUserId;
  638. process.UpdatedTime = DateTime.Now;
  639. // 更新流程状态
  640. await _sqlSugar.Updateable(process)
  641. .UpdateColumns(p => new
  642. {
  643. p.OverallStatus,
  644. p.EndTime,
  645. p.UpdatedUserId,
  646. p.UpdatedTime
  647. })
  648. .ExecuteCommandAsync();
  649. //记录流程日志
  650. await LogProcessOpAsync(processBefore, process, "Update", dto.CurrUserId);
  651. }
  652. // 4. 保存节点更新
  653. await _sqlSugar.Updateable(node)
  654. .UpdateColumns(n => new
  655. {
  656. n.Remark,
  657. n.Operator,
  658. n.OperationTime,
  659. n.OverallStatus
  660. })
  661. .ExecuteCommandAsync();
  662. //记录节点日志
  663. await LogNodeOpAsync(nodeBefore, node, "Update", dto.CurrUserId);
  664. return new Result { Code = 200, Msg = "节点信息更新成功。" };
  665. }
  666. /// <summary>
  667. /// 更新节点信息及状态
  668. /// </summary>
  669. /// <param name="dto">签证节点更新数据传输对象</param>
  670. /// <returns>操作结果</returns>
  671. public async Task<Result> SetActualDoneAsync(GroupProcessSetActualDoneDto dto )
  672. {
  673. int nodeId = dto.NodeId;
  674. var isDtNul = DateTime.TryParse(dto.ActualDone,out DateTime actualDone);
  675. int currUserId = dto.CurrUserId;
  676. bool isAssist = dto.IsAssist;
  677. bool isFileUp = dto.IsFileUp;
  678. bool isPart = dto.IsPart;
  679. // 1. 获取并验证节点和流程
  680. var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
  681. .FirstAsync(n => n.Id == nodeId && n.IsDel == 0)
  682. ?? throw new BusinessException("当前节点不存在或已被删除。");
  683. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  684. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0)
  685. ?? throw new BusinessException("当前流程不存在或已被删除。");
  686. // 2.1 存储更新前流程及节点信息
  687. var nodeBefore = new Grp_ProcessNode()
  688. {
  689. Id = node.Id,
  690. ProcessId = node.ProcessId,
  691. NodeName = node.NodeName,
  692. NodeOrder = node.NodeOrder,
  693. OverallStatus = node.OverallStatus,
  694. Operator = node.Operator,
  695. OperationTime = node.OperationTime,
  696. IsCurrent = node.IsCurrent
  697. };
  698. if (isDtNul)
  699. {
  700. node.ActualDone = actualDone;
  701. }
  702. node.IsAssist = isAssist;
  703. node.IsFileUp = isFileUp;
  704. node.IsPart = isPart;
  705. // 3. 保存节点更新
  706. await _sqlSugar.Updateable(node)
  707. .UpdateColumns(n => new
  708. {
  709. ActualDone = isDtNul ? node.ActualDone : null,
  710. n.IsAssist,
  711. n.IsFileUp,
  712. n.IsPart,
  713. })
  714. .ExecuteCommandAsync();
  715. //记录节点日志
  716. await LogNodeOpAsync(nodeBefore, node, "Update", currUserId);
  717. return new Result { Code = 200, Msg = "实际操作时间设置成功。" };
  718. }
  719. #region 操作日志
  720. /// <summary>
  721. /// 记录流程操作日志
  722. /// </summary>
  723. /// <param name="before">操作前</param>
  724. /// <param name="after">操作后</param>
  725. /// <param name="opType">操作类型(Create - 创建、Update - 更新、Complete - 完成)</param>
  726. /// <param name="operId">操作人ID</param>
  727. /// <returns>异步任务</returns>
  728. public async Task LogProcessOpAsync(Grp_ProcessOverview before, Grp_ProcessOverview after,string opType, int operId)
  729. {
  730. var chgDetails = GetProcessChgDetails(before, after);
  731. var log = new Grp_ProcessLog
  732. {
  733. ProcessId = after?.Id ?? before?.Id,
  734. GroupId = after?.GroupId ?? before?.GroupId ?? 0,
  735. OpType = opType,
  736. OpDesc = GenerateProcessOpDesc(opType, before, after, chgDetails),
  737. BeforeData = before != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  738. AfterData = after != null ? JsonConvert.SerializeObject(after, GetJsonSettings()) : null,
  739. ChgFields = string.Join(",", chgDetails.Select(x => x.FieldName)),
  740. CreateUserId = operId
  741. };
  742. await _sqlSugar.Insertable(log).ExecuteCommandAsync();
  743. }
  744. /// <summary>
  745. /// 记录节点操作日志
  746. /// </summary>
  747. /// <param name="before">操作前</param>
  748. /// <param name="after">操作后</param>
  749. /// <param name="opType">操作类型(Create - 创建、Update - 更新、Start - 启动、Complete - 完成)</param>
  750. /// <param name="operId">操作人ID</param>
  751. /// <returns>异步任务</returns>
  752. public async Task LogNodeOpAsync(Grp_ProcessNode before, Grp_ProcessNode after,string opType, int operId)
  753. {
  754. var chgDetails = GetNodeChgDetails(before, after);
  755. var log = new Grp_ProcessLog
  756. {
  757. NodeId = after?.Id ?? before?.Id,
  758. ProcessId = after?.ProcessId ?? before?.ProcessId,
  759. GroupId = 0, // 通过流程ID关联获取
  760. OpType = opType,
  761. OpDesc = GenerateNodeOpDesc(opType, before, after, chgDetails),
  762. BeforeData = before != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  763. AfterData = after != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  764. ChgFields = string.Join(",", chgDetails.Select(x => x.FieldName)),
  765. CreateUserId = operId
  766. };
  767. await _sqlSugar.Insertable(log).ExecuteCommandAsync();
  768. }
  769. /// <summary>
  770. /// 获取流程变更详情
  771. /// </summary>
  772. /// <param name="before">变更前</param>
  773. /// <param name="after">变更后</param>
  774. /// <returns>变更详情</returns>
  775. private List<FieldChgDetail> GetProcessChgDetails(Grp_ProcessOverview before, Grp_ProcessOverview after)
  776. {
  777. var chgDetails = new List<FieldChgDetail>();
  778. if (before == null || after == null) return chgDetails;
  779. var props = typeof(Grp_ProcessOverview).GetProperties(BindingFlags.Public | BindingFlags.Instance)
  780. .Where(p => p.CanRead && p.CanWrite && !IsExclField(p.Name));
  781. foreach (var prop in props)
  782. {
  783. var beforeVal = prop.GetValue(before);
  784. var afterVal = prop.GetValue(after);
  785. if (!Equals(beforeVal, afterVal))
  786. {
  787. chgDetails.Add(new FieldChgDetail
  788. {
  789. FieldName = prop.Name,
  790. BeforeValue = FormatVal(beforeVal),
  791. AfterValue = FormatVal(afterVal)
  792. });
  793. }
  794. }
  795. return chgDetails;
  796. }
  797. /// <summary>
  798. /// 获取节点变更详情
  799. /// </summary>
  800. /// <param name="before">变更前</param>
  801. /// <param name="after">变更后</param>
  802. /// <returns>变更详情</returns>
  803. private List<FieldChgDetail> GetNodeChgDetails(Grp_ProcessNode before, Grp_ProcessNode after)
  804. {
  805. var chgDetails = new List<FieldChgDetail>();
  806. if (before == null || after == null) return chgDetails;
  807. var props = typeof(Grp_ProcessNode).GetProperties(BindingFlags.Public | BindingFlags.Instance)
  808. .Where(p => p.CanRead && p.CanWrite && !IsExclField(p.Name));
  809. foreach (var prop in props)
  810. {
  811. var beforeVal = prop.GetValue(before);
  812. var afterVal = prop.GetValue(after);
  813. if (!Equals(beforeVal, afterVal))
  814. {
  815. chgDetails.Add(new FieldChgDetail
  816. {
  817. FieldName = prop.Name,
  818. BeforeValue = FormatVal(beforeVal),
  819. AfterValue = FormatVal(afterVal)
  820. });
  821. }
  822. }
  823. return chgDetails;
  824. }
  825. /// <summary>
  826. /// 生成流程操作描述
  827. /// </summary>
  828. /// <param name="opType">操作类型</param>
  829. /// <param name="before">操作前</param>
  830. /// <param name="after">操作后</param>
  831. /// <param name="chgDetails">变更详情</param>
  832. /// <returns>操作描述</returns>
  833. private string GenerateProcessOpDesc(string opType, Grp_ProcessOverview before,
  834. Grp_ProcessOverview after, List<FieldChgDetail> chgDetails)
  835. {
  836. var processType = after?.ProcessType ?? before?.ProcessType;
  837. var processName = GetProcessTypeName(processType);
  838. if (!chgDetails.Any())
  839. {
  840. return opType switch
  841. {
  842. "Create" => $"创建流程:{processName}",
  843. "Update" => $"更新流程:{processName} - 无变更",
  844. //"Start" => $"启动流程:{processName}",
  845. "Complete" => $"完成流程:{processName}",
  846. //"Delete" => $"删除流程:{processName}",
  847. _ => $"{opType}:{processName}"
  848. };
  849. }
  850. var chgDesc = string.Join("; ", chgDetails.Select(x =>
  851. $"{GetFieldDisplayName(x.FieldName)} ({x.BeforeValue} -> {x.AfterValue})"));
  852. return $"{GetOpTypeDisplay(opType)}:{processName} - {chgDesc}";
  853. }
  854. /// <summary>
  855. /// 获取JSON序列化设置
  856. /// </summary>
  857. /// <returns>JSON设置</returns>
  858. private static JsonSerializerSettings GetJsonSettings()
  859. {
  860. return new JsonSerializerSettings
  861. {
  862. ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
  863. NullValueHandling = NullValueHandling.Ignore,
  864. DateFormatString = "yyyy-MM-dd HH:mm:ss",
  865. Formatting = Formatting.None
  866. };
  867. }
  868. /// <summary>
  869. /// 生成节点操作描述
  870. /// </summary>
  871. /// <param name="opType">操作类型</param>
  872. /// <param name="before">操作前</param>
  873. /// <param name="after">操作后</param>
  874. /// <param name="chgDetails">变更详情</param>
  875. /// <returns>操作描述</returns>
  876. private string GenerateNodeOpDesc(string opType, Grp_ProcessNode before,
  877. Grp_ProcessNode after, List<FieldChgDetail> chgDetails)
  878. {
  879. var nodeName = after?.NodeName ?? before?.NodeName;
  880. if (!chgDetails.Any())
  881. {
  882. return opType switch
  883. {
  884. "Create" => $"创建节点:{nodeName}",
  885. "Update" => $"更新节点:{nodeName} - 无变更",
  886. "Start" => $"启动节点:{nodeName}",
  887. "Complete" => $"完成节点:{nodeName}",
  888. //"Delete" => $"删除节点:{nodeName}",
  889. _ => $"{opType}:{nodeName}"
  890. };
  891. }
  892. var chgDesc = string.Join("; ", chgDetails.Select(x =>
  893. $"{GetFieldDisplayName(x.FieldName)} ({x.BeforeValue} -> {x.AfterValue})"));
  894. return $"{GetOpTypeDisplay(opType)}:{nodeName} - {chgDesc}";
  895. }
  896. /// <summary>
  897. /// 获取流程类型名称
  898. /// </summary>
  899. /// <param name="processType">流程类型</param>
  900. /// <returns>流程名称</returns>
  901. private static string GetProcessTypeName(GroupProcessType? processType)
  902. {
  903. return processType switch
  904. {
  905. GroupProcessType.Invitation => "商邀报批",
  906. GroupProcessType.Visa => "签证",
  907. GroupProcessType.AirTicket => "机票",
  908. GroupProcessType.Hotel => "酒店",
  909. GroupProcessType.LocalGuide => "地接",
  910. GroupProcessType.FeeSettle => "费用结算",
  911. _ => "未知流程"
  912. };
  913. }
  914. /// <summary>
  915. /// 获取操作类型显示
  916. /// </summary>
  917. /// <param name="opType">操作类型</param>
  918. /// <returns>显示名称</returns>
  919. private static string GetOpTypeDisplay(string opType)
  920. {
  921. return opType switch
  922. {
  923. "Create" => "创建",
  924. "Update" => "更新",
  925. "Start" => "启动",
  926. "Complete" => "完成",
  927. "Delete" => "删除",
  928. "StatusChg" => "状态变更",
  929. _ => opType
  930. };
  931. }
  932. /// <summary>
  933. /// 获取字段显示名称
  934. /// </summary>
  935. /// <param name="fieldName">字段名</param>
  936. /// <returns>显示名称</returns>
  937. private string GetFieldDisplayName(string fieldName)
  938. {
  939. return fieldName switch
  940. {
  941. "OverallStatus" => "状态",
  942. "ProcessOrder" => "流程顺序",
  943. "StartTime" => "开始时间",
  944. "EndTime" => "结束时间",
  945. "NodeOrder" => "节点顺序",
  946. "NodeName" => "节点名称",
  947. "IsCurrent" => "当前节点",
  948. "Operator" => "操作人",
  949. "OperationTime" => "操作时间",
  950. _ => fieldName
  951. };
  952. }
  953. /// <summary>
  954. /// 格式化值显示
  955. /// </summary>
  956. /// <param name="value">值</param>
  957. /// <returns>格式化值</returns>
  958. private string FormatVal(object value)
  959. {
  960. if (value == null) return "空";
  961. if (value is ProcessStatus status)
  962. {
  963. return status switch
  964. {
  965. ProcessStatus.UnStarted => "未开始",
  966. ProcessStatus.InProgress => "进行中",
  967. ProcessStatus.Completed => "已完成",
  968. _ => status.ToString()
  969. };
  970. }
  971. if (value is bool boolVal) return boolVal ? "是" : "否";
  972. if (value is DateTime dateVal) return dateVal.ToString("yyyy-MM-dd HH:mm");
  973. var strVal = value.ToString();
  974. return string.IsNullOrEmpty(strVal) ? "空" : strVal;
  975. }
  976. /// <summary>
  977. /// 检查是否排除字段
  978. /// </summary>
  979. /// <param name="fieldName">字段名</param>
  980. /// <returns>是否排除</returns>
  981. private bool IsExclField(string fieldName)
  982. {
  983. var exclFields = new List<string>
  984. {
  985. "Id", "CreateTime", "CreateUserId", "UpdatedTime", "UpdatedUserId",
  986. "Nodes", "Process" // 导航属性
  987. };
  988. return exclFields.Contains(fieldName);
  989. }
  990. /// <summary>
  991. /// 获取流程日志
  992. /// </summary>
  993. /// <param name="processId">流程ID</param>
  994. /// <returns>日志列表</returns>
  995. public async Task<List<Grp_ProcessLog>> GetProcessLogsAsync(int processId)
  996. {
  997. return await _sqlSugar.Queryable<Grp_ProcessLog>()
  998. .Where(x => x.ProcessId == processId)
  999. .OrderByDescending(x => x.CreateTime)
  1000. .ToListAsync();
  1001. }
  1002. /// <summary>
  1003. /// 获取团组流程日志
  1004. /// </summary>
  1005. /// <param name="groupId">团组ID</param>
  1006. /// <returns>日志列表</returns>
  1007. public async Task<List<Grp_ProcessLog>> GetGroupLogsAsync(int groupId)
  1008. {
  1009. return await _sqlSugar.Queryable<Grp_ProcessLog>()
  1010. .Where(x => x.GroupId == groupId)
  1011. .OrderByDescending(x => x.CreateTime)
  1012. .ToListAsync();
  1013. }
  1014. #endregion
  1015. #region 节点按钮策略
  1016. // 定义按钮状态计算策略接口
  1017. public interface IButtonStateStrategy
  1018. {
  1019. (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index);
  1020. }
  1021. // 通用策略
  1022. public class DefaultButtonStateStrategy : IButtonStateStrategy
  1023. {
  1024. public (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index)
  1025. {
  1026. var totalNodes = process.Nodes.Count();
  1027. var isLastNode = index == totalNodes - 1;
  1028. return (false, isLastNode); // 默认只有最后一步启用文件上传
  1029. }
  1030. }
  1031. // 财务流程策略
  1032. public class FeeSettleButtonStateStrategy : IButtonStateStrategy
  1033. {
  1034. public (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index)
  1035. {
  1036. var totalNodes = process.Nodes.Count();
  1037. var isLastNode = index == totalNodes - 1;
  1038. bool isEnaAssistBtn = node.NodeOrder == 1; // 首节点启用协助
  1039. bool isEnaFileUpBtn = isLastNode; // 最后一步启用文件上传
  1040. return (isEnaAssistBtn, isEnaFileUpBtn);
  1041. }
  1042. }
  1043. // 机票流程策略
  1044. public class FlightButtonStateStrategy : IButtonStateStrategy
  1045. {
  1046. public (bool IsEnaAssistBtn, bool IsEnaFileUpBtn) Calculate(Grp_ProcessOverview process, Grp_ProcessNode node, int index)
  1047. {
  1048. var totalNodes = process.Nodes.Count();
  1049. var isLastNode = index == totalNodes - 1;
  1050. var isSecondLastNode = index == totalNodes - 2;
  1051. bool isEnaAssistBtn = false;
  1052. bool isEnaFileUpBtn = isLastNode || isSecondLastNode; // 倒数两步都启用文件上传
  1053. return (isEnaAssistBtn, isEnaFileUpBtn);
  1054. }
  1055. }
  1056. // 策略工厂
  1057. public static class ButtonStateStrategyFactory
  1058. {
  1059. private static readonly Dictionary<GroupProcessType, IButtonStateStrategy> _strategies = new()
  1060. {
  1061. [GroupProcessType.Invitation] = new DefaultButtonStateStrategy(),
  1062. [GroupProcessType.Visa] = new DefaultButtonStateStrategy(),
  1063. [GroupProcessType.AirTicket] = new FlightButtonStateStrategy(),
  1064. [GroupProcessType.Hotel] = new DefaultButtonStateStrategy(),
  1065. [GroupProcessType.LocalGuide] = new DefaultButtonStateStrategy(),
  1066. [GroupProcessType.FeeSettle] = new FeeSettleButtonStateStrategy()
  1067. };
  1068. public static IButtonStateStrategy GetStrategy(GroupProcessType processType)
  1069. {
  1070. return _strategies.TryGetValue(processType, out var strategy)
  1071. ? strategy
  1072. : new DefaultButtonStateStrategy();
  1073. }
  1074. }
  1075. #endregion
  1076. }
  1077. }