ProcessOverviewRepository.cs 54 KB

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