ProcessOverviewRepository.cs 90 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034
  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 OASystem.Domain.ViewModels.Groups;
  8. using Org.BouncyCastle.Asn1.X500;
  9. using System.Reflection;
  10. using System.Runtime.Intrinsics.Arm;
  11. using System.Text.RegularExpressions;
  12. namespace OASystem.Infrastructure.Repositories.Groups
  13. {
  14. /// <summary>
  15. /// 团组流程总览表仓储
  16. /// </summary>
  17. public class ProcessOverviewRepository : BaseRepository<Grp_ProcessOverview, Grp_ProcessOverview>
  18. {
  19. private readonly IMapper _mapper;
  20. private readonly DelegationInfoRepository _groupRep;
  21. public ProcessOverviewRepository(SqlSugarClient sqlSugar, IMapper mapper, DelegationInfoRepository groupRep) : base(sqlSugar)
  22. {
  23. _mapper = mapper;
  24. _groupRep = groupRep;
  25. }
  26. #region 团组流程
  27. /// <summary>
  28. /// 基础数据初始化-团组流程
  29. /// </summary>
  30. /// <param name="groupId"></param>
  31. /// <param name="currUserId"></param>
  32. /// <returns></returns>
  33. public async Task<List<Grp_ProcessOverview>> ProcessDataInitAsync(int groupId, int currUserId, List<string> visaCountries)
  34. {
  35. var processs = new List<Grp_ProcessOverview>();
  36. //团组验证
  37. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  38. if (groupInfo == null) return processs;
  39. // 检查是否已存在流程
  40. var existingProcesses = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  41. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  42. .ToListAsync();
  43. if (existingProcesses.Any()) return processs;
  44. #region 商邀报批流程
  45. var custInfo = await _sqlSugar.Queryable<Grp_TourClientList>()
  46. .Where(c => c.DiId == groupId && c.IsDel == 0)
  47. .OrderByDescending(c => c.CreateTime)
  48. .FirstAsync();
  49. string oaNode2Tips = "客户提供完整名单后,2周内取得邀请函(翻译件)。";
  50. if (custInfo != null)
  51. {
  52. oaNode2Tips = $"请于{custInfo.CreateTime.AddDays(14):yyyy年MM月dd日}内完成该项工作(客户提供完整名单后,2周内取得邀请函(翻译件))";
  53. }
  54. var oaNode4Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(按进度实际公务活动落实情况,出发前5日落实公务)";
  55. var oaNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组结束前完成)";
  56. processs.Add(
  57. Grp_ProcessOverview.Create(groupId, 1, GroupProcessType.Invitation, ProcessStatus.InProgress, currUserId,
  58. new List<Grp_ProcessNode>()
  59. {
  60. Grp_ProcessNode.Create(1, "报批基础资料准备","更新报批行程和请示,提供其他报批所需材料,4个工作日内完成。",ProcessStatus.InProgress, true,false,false,false,currUserId),
  61. Grp_ProcessNode.Create(2, "报批邀请函资料准备",oaNode2Tips, ProcessStatus.InProgress, false,false,false,false,currUserId),
  62. Grp_ProcessNode.Create(3, "获得批件","提供完整的报批全套资源。",ProcessStatus.InProgress, false,false,false,false, currUserId ),
  63. Grp_ProcessNode.Create(4, "对接公务",oaNode4Tips,ProcessStatus.InProgress, false,false,false,false, currUserId),
  64. Grp_ProcessNode.Create(5, "参与翻译对接","",ProcessStatus.InProgress, false,false,false,true, currUserId),
  65. Grp_ProcessNode.Create(6, "商邀文案配合","",ProcessStatus.InProgress, false,false,false,false, currUserId),
  66. Grp_ProcessNode.Create(7, "票据上传(相关票据)",oaNode7Tips,ProcessStatus.InProgress, false,false,true,false, currUserId),
  67. }));
  68. #endregion
  69. #region 签证流程
  70. //单独处理签证流程节点
  71. var visaNodes = new List<Grp_ProcessNode>();
  72. if (visaCountries != null && visaCountries.Count > 0)
  73. {
  74. var visaDefualtNodes = new List<VisaProcessNode>();
  75. for (int i = 1; i < visaCountries.Count + 1; i++)
  76. {
  77. visaDefualtNodes.Add(VisaProcessNode.Info(i, visaCountries[i - 1].ToString()));
  78. }
  79. var visaNode2Tips = $"请于{groupInfo.VisitDate:yyyy年MM月dd日}内完成该项工作(按进度实际签证办理落实情况,团组出发前上传票据。)";
  80. visaNodes.Add(Grp_ProcessNode.Create(1, "签证信息", "", ProcessStatus.InProgress, true, false, false, false, currUserId, JsonConvert.SerializeObject(visaDefualtNodes)));
  81. visaNodes.Add(Grp_ProcessNode.Create(2, "票据上传(明细表、费用票据、保单及超支费用账单)", visaNode2Tips, ProcessStatus.InProgress, false, false, true, false, currUserId));
  82. }
  83. processs.Add(Grp_ProcessOverview.Create(groupId, 2, GroupProcessType.Visa, ProcessStatus.UnStarted, currUserId, visaNodes));
  84. #endregion
  85. #region 机票流程
  86. string airNode1Tips = "建团后打勾确认出团的时候开始24小时内。";
  87. if (groupInfo.Step == 1 || groupInfo.Step == 2)
  88. {
  89. if (groupInfo.StepOperationTime.HasValue)
  90. {
  91. airNode1Tips = $"请于{groupInfo.StepOperationTime.Value.AddDays(1):yyyy年MM月dd日}内完成该项工作(建团后打勾确认出团的时候开始24小时内)";
  92. }
  93. }
  94. var airNode3Tips = $"完成机票采购确认(含预算核对、出票确认等)";
  95. var airNode5Tips = $"请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5日)";
  96. var airNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组归国后5个工作日内)";
  97. var airNode8Tips = $"1. 票据上传(机票报销蓝联、行程单及机票说明) \r\n 2. 请于{groupInfo.VisitEndDate.AddDays(10):yyyy年MM月dd日}内完成该项工作(团组归国后10个工作日内) *按机票报价*0.999折扣出具机票报销蓝联、行程单及机票说明";
  98. processs.Add(
  99. Grp_ProcessOverview.Create(groupId, 3, GroupProcessType.AirTicket, ProcessStatus.InProgress, currUserId,
  100. new List<Grp_ProcessNode>()
  101. {
  102. Grp_ProcessNode.Create(1, "初步拟定航程方案及价格", airNode1Tips, ProcessStatus.InProgress, true,false,false,false, currUserId ),
  103. Grp_ProcessNode.Create(2, "机票占位、续位", "", ProcessStatus.UnStarted, false,false,false,false,currUserId ),
  104. Grp_ProcessNode.Create(3, "完成机票采购确认", airNode3Tips, ProcessStatus.UnStarted,false,false,false,false, currUserId),
  105. Grp_ProcessNode.Create(4, "进行出票操作并核查信息", "", ProcessStatus.UnStarted, false,false,false,false, currUserId),
  106. Grp_ProcessNode.Create(5, "机票已出", airNode5Tips, ProcessStatus.UnStarted, false,false,false,false, currUserId),
  107. Grp_ProcessNode.Create(6, "完成机票选座", "", ProcessStatus.UnStarted, false,false,false,false,currUserId),
  108. Grp_ProcessNode.Create(7, "票据上传(机票超支费用账单)", airNode7Tips, ProcessStatus.UnStarted, false,false,true,false, currUserId),
  109. Grp_ProcessNode.Create(8, "票据上传", airNode8Tips, ProcessStatus.UnStarted, false,false,true,false, currUserId)
  110. }
  111. )
  112. );
  113. #endregion
  114. #region 酒店流程
  115. string hotelNode1Tips = "1. 筛选并按照预算标准,对目标酒店进行询价、比价、谈价 \r\n2. 建团后打勾确认出团的时候开始2个工作日。";
  116. if (groupInfo.Step == 1 || groupInfo.Step == 2)
  117. {
  118. if (groupInfo.StepOperationTime.HasValue)
  119. {
  120. hotelNode1Tips = $"请于{groupInfo.StepOperationTime.Value.AddDays(2):yyyy年MM月dd日}内完成该项工作(建团后打勾确认出团的时候开始2个工作日)";
  121. }
  122. }
  123. var hotelNode4Tips = $"1.行前再次确认酒店订单、付款状态及入住安排 \r\n 2.请于{groupInfo.VisitDate.AddDays(-5):yyyy年MM月dd日}内完成该项工作(团组出发前5天)";
  124. var hotelNode5Tips = $"1.行程结束后整理酒店发票(含超支费用发票)与结算 \r\n 2.请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组结束后5天内)";
  125. processs.Add(
  126. Grp_ProcessOverview.Create(groupId, 4, GroupProcessType.Hotel, ProcessStatus.InProgress, currUserId,
  127. new List<Grp_ProcessNode>()
  128. {
  129. Grp_ProcessNode.Create(1, "按照预算,询价、比价、谈价", hotelNode1Tips, ProcessStatus.InProgress, true, false, false, false, currUserId),
  130. Grp_ProcessNode.Create(2, "获取酒店确认函与入住名单核对", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  131. Grp_ProcessNode.Create(3, "预订酒店并录入OA", "", ProcessStatus.UnStarted,false, false, false,false,currUserId ),
  132. Grp_ProcessNode.Create(4, "行前再次确认酒店相关情况", hotelNode4Tips,ProcessStatus.UnStarted, false, false, false,false,currUserId ),
  133. Grp_ProcessNode.Create(5, "行程结束后整理酒店发票与结算", hotelNode5Tips, ProcessStatus.UnStarted, false, false, true,false, currUserId ),
  134. }
  135. )
  136. );
  137. #endregion
  138. #region 地接流程
  139. var airTripCodeInfo = await _sqlSugar.Queryable<Air_TicketBlackCode>()
  140. .Where(x => x.IsDel == 0 && x.DiId == groupId)
  141. .OrderByDescending(x => x.CreateTime)
  142. .FirstAsync();
  143. string opNode1Tips = $"机票行程代码最后一段录入后1个工作日内。";
  144. if (airTripCodeInfo != null)
  145. {
  146. opNode1Tips = $"请于{airTripCodeInfo.CreateTime.AddDays(1):yyyy年MM月dd日}内完成该项工作(机票行程代码最后一段录入后1个工作日内)";
  147. }
  148. string opNode2Tips = $"1.联系并询价地接、餐厅、用车、景点等供应商 \r\n 2. 请于{groupInfo.CreateTime.AddDays(7):yyyy年MM月dd日}内完成该项工作(建团完成后7个工作日内)";
  149. string opNode3Tips = $"请于{groupInfo.CreateTime.AddDays(10):yyyy年MM月dd日}内完成该项工作(上一步往后3个工作日内)";
  150. string opNode4Tips = $"请于{groupInfo.CreateTime.AddDays(12):yyyy年MM月dd日}内完成该项工作(上一步往后2个工作日内)";
  151. var backListInfo = await _sqlSugar.Queryable<Grp_InvertedList>().Where(x => x.DiId == groupId && x.IsDel == 0).FirstAsync();
  152. string opNode5Tips = $"1.制定最终《行程单》及《出行手册》 \r\n2. 倒推表里开行前会 -3天。";
  153. if (backListInfo != null)
  154. {
  155. if (DateTime.TryParse(backListInfo.PreTripMeetingDt, out DateTime dateTime))
  156. {
  157. opNode5Tips = $"请于{dateTime.AddDays(-3):yyyy年MM月dd日}内完成该项工作(倒推表里开行前会 -3天)";
  158. }
  159. }
  160. string opNode7Tips = $"请于{groupInfo.VisitEndDate.AddDays(5):yyyy年MM月dd日}内完成该项工作(团组归国后5个工作日内) *上传最终报批行程,确定城市间交通最终版报价分配;地接账单(清楚标注超时及其他项超支费用)、地接交通费用原始票据、城市间交通明细表;";
  161. processs.Add(
  162. Grp_ProcessOverview.Create(groupId, 5, GroupProcessType.LocalGuide, ProcessStatus.InProgress, currUserId,
  163. new List<Grp_ProcessNode>()
  164. {
  165. Grp_ProcessNode.Create(1,"根据机票方案出框架行程", opNode1Tips,ProcessStatus.InProgress, true, false, false,false,currUserId ),
  166. Grp_ProcessNode.Create(2,"联系并询价地接相关的供应商", opNode2Tips,ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  167. Grp_ProcessNode.Create(3,"提交供应商报价及比价表", opNode3Tips, ProcessStatus.UnStarted, false, false, false, false,currUserId),
  168. Grp_ProcessNode.Create(4,"执行采购流程", opNode4Tips, ProcessStatus.UnStarted, false, false, false,false, currUserId),
  169. Grp_ProcessNode.Create(5,"制定最终行程单及出行手册", opNode5Tips, ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  170. Grp_ProcessNode.Create(6,"送机", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  171. Grp_ProcessNode.Create(7,"最终版报批行程、票据上传", opNode7Tips, ProcessStatus.UnStarted, false, false, true, false,currUserId )
  172. }
  173. )
  174. );
  175. #endregion
  176. #region 费用结算流程
  177. var feeNode3Tips = $"1.整理统计团组超支费用、三公报销资料给到各单位 \r\n 2. 请于{groupInfo.VisitEndDate.AddDays(12):yyyy年MM月dd日}内完成该项工作(团组归国后12个工作日内)";
  178. processs.Add(
  179. Grp_ProcessOverview.Create(groupId, 6, GroupProcessType.FeeSettle, ProcessStatus.InProgress, currUserId,
  180. new List<Grp_ProcessNode>()
  181. {
  182. Grp_ProcessNode.Create(1, "城市间交通报批金额核定", "团组报批前", ProcessStatus.InProgress, true, true, false,false,currUserId ),
  183. Grp_ProcessNode.Create(2, "团组全程各段机票打票金额的核定", "团组报批后、订票前", ProcessStatus.UnStarted, false, false, false,false,currUserId ),
  184. Grp_ProcessNode.Create(3, "整理统计相关财务资料给到各单位", feeNode3Tips, ProcessStatus.UnStarted, false, false, false,false,currUserId ),
  185. Grp_ProcessNode.Create(4, "费用结算完毕", "", ProcessStatus.UnStarted, false, false, false,false, currUserId ),
  186. }
  187. )
  188. );
  189. #endregion
  190. return processs;
  191. }
  192. /// <summary>
  193. /// 团组流程初始化
  194. /// </summary>
  195. /// <param name="request">创建流程请求参数</param>
  196. /// <returns>创建的流程信息</returns>
  197. public async Task<Result> ProcessInitAsync(int groupId, int currUserId)
  198. {
  199. //团组验证
  200. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  201. if (groupInfo == null)
  202. {
  203. return new Result { Code = 400, Msg = "团组不存在" };
  204. }
  205. // 检查是否已存在流程
  206. var existingProcesses = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  207. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  208. .ToListAsync();
  209. if (existingProcesses.Any())
  210. {
  211. return new Result { Code = 400, Msg = "该团组的流程已存在" };
  212. }
  213. //处理签证国家
  214. var visaCountries = _groupRep.GroupSplitCountry(groupInfo.VisitCountry);
  215. // 定义默认的流程节点
  216. var processs = await ProcessDataInitAsync(groupId, currUserId, visaCountries);
  217. _sqlSugar.BeginTran();
  218. foreach (var item in processs)
  219. {
  220. var processId = await _sqlSugar.Insertable(item).ExecuteReturnIdentityAsync();
  221. if (processId < 1)
  222. {
  223. _sqlSugar.RollbackTran();
  224. return new Result { Code = 400, Msg = "团组流程进度总览表添加失败!" };
  225. }
  226. item.Id = processId;
  227. // 记录流程日志
  228. await LogProcessOpAsync(null, item, "Create", currUserId);
  229. var nodes = item.Nodes.Select((nodeDto, index) => new Grp_ProcessNode
  230. {
  231. ProcessId = processId,
  232. NodeName = nodeDto.NodeName,
  233. NodeOrder = nodeDto.NodeOrder,
  234. OverallStatus = nodeDto.OverallStatus,
  235. NodeDescTips = nodeDto.NodeDescTips,
  236. //Country = nodeDto.Country,
  237. IsCurrent = nodeDto.IsCurrent,
  238. IsAssist = nodeDto.IsAssist,
  239. IsPart = nodeDto.IsPart,
  240. IsFileUp = nodeDto.IsFileUp,
  241. Remark = nodeDto.Remark
  242. }).ToList();
  243. var nodeIds = await _sqlSugar.Insertable(nodes).ExecuteCommandAsync();
  244. if (nodeIds < 1)
  245. {
  246. _sqlSugar.RollbackTran();
  247. return new Result { Code = 400, Msg = "团组流程进度流程节点添加失败!" };
  248. }
  249. //设置节点ID
  250. nodes = await _sqlSugar.Queryable<Grp_ProcessNode>().Where(x => x.IsDel == 0 && x.ProcessId == processId).ToListAsync();
  251. //记录节点日志
  252. foreach (var node in nodes)
  253. {
  254. await LogNodeOpAsync(null, node, "Create", currUserId);
  255. }
  256. }
  257. _sqlSugar.CommitTran();
  258. return new Result { Code = 200, Msg = "添加成功!" }; ;
  259. }
  260. /// <summary>
  261. /// 获取团组的所有流程及节点详情
  262. /// </summary>
  263. /// <param name="request">创建流程请求参数</param>
  264. /// <returns>创建的流程信息</returns>
  265. public async Task<Result> ProcessesDetailsAsync(int groupId)
  266. {
  267. //团组验证
  268. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  269. if (groupInfo == null)
  270. {
  271. return new Result { Code = 400, Msg = "团组不存在" };
  272. }
  273. // 检查是否已存在流程
  274. var existingProcesses = await _sqlSugar.Queryable<Grp_ProcessOverview>().Where(p => p.IsDel == 0 && p.GroupId == groupId).ToListAsync();
  275. if (!existingProcesses.Any())
  276. {
  277. //新建团组流程
  278. var res = await ProcessInitAsync(groupId, 4);
  279. if (res.Code != 200)
  280. {
  281. return res;
  282. }
  283. }
  284. var users = await _sqlSugar.Queryable<Sys_Users>().Select(x => new { x.Id, x.CnName }).ToListAsync();
  285. var processData = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  286. .Where(p => p.GroupId == groupId && p.IsDel == 0)
  287. .Mapper(p => p.Nodes, p => p.Nodes.First().ProcessId)
  288. .ToListAsync();
  289. // 预先构建用户字典,提升查询性能
  290. var userDict = users.ToDictionary(u => u.Id, u => u.CnName);
  291. var processes = processData.Select(p =>
  292. {
  293. var orderedNodes = p.Nodes.OrderBy(n => n.NodeOrder).ToList();
  294. var totalNodes = orderedNodes.Count;
  295. return new
  296. {
  297. p.Id,
  298. p.GroupId,
  299. p.ProcessType,
  300. ProcessName = p.ProcessType.GetEnumDescription(),
  301. Nodes = orderedNodes.Select((n, index) =>
  302. {
  303. var isLastNode = index == totalNodes - 1;
  304. var isSecondLastNode = index == totalNodes - 2;
  305. var isFifthStep = index == 4;
  306. // 计算按钮状态
  307. bool isEnaAssistBtn = p.ProcessType == GroupProcessType.FeeSettle && n.NodeOrder == 1;
  308. // 文件上传按钮启用规则
  309. bool isEnaFileUpBtn = false;
  310. // 是否参与按钮启用
  311. bool isEnaPartBtn = false;
  312. // 规则1:商邀流程第五步启用参与按钮
  313. if (p.ProcessType == GroupProcessType.Invitation && isFifthStep)
  314. {
  315. isEnaPartBtn = true;
  316. }
  317. // 规则2:机票流程倒数第二步启用上传按钮
  318. else if (p.ProcessType == GroupProcessType.AirTicket && isSecondLastNode)
  319. {
  320. isEnaFileUpBtn = true;
  321. }
  322. // 规则3:默认流程节点最后一步启用上传按钮
  323. else if (isLastNode && p.ProcessType != GroupProcessType.FeeSettle)
  324. {
  325. isEnaFileUpBtn = true;
  326. }
  327. // 处理签证子节点
  328. List<VisaProcessNode> visaSubNodes = new();
  329. if (p.ProcessType == GroupProcessType.Visa && n.NodeOrder == 1)
  330. {
  331. visaSubNodes = JsonConvert.DeserializeObject<List<VisaProcessNode>>(n.Remark ?? "[]")
  332. ?? new List<VisaProcessNode>();
  333. }
  334. // 获取操作人姓名(使用字典提升性能)
  335. string operatorName = "-";
  336. if (n.Operator.HasValue && userDict.TryGetValue(n.Operator.Value, out var name))
  337. {
  338. operatorName = name;
  339. }
  340. return new
  341. {
  342. n.Id,
  343. n.ProcessId,
  344. n.NodeOrder,
  345. n.NodeName,
  346. n.OverallStatus,
  347. StatusText = n.OverallStatus.GetEnumDescription(),
  348. Operator = operatorName,
  349. OpeateTime = n.OperationTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-",
  350. ActualDone = n.ActualDone?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
  351. n.NodeDescTips,
  352. isEnaAssistBtn, // 是否启用财务流程首节点协助按钮
  353. n.IsAssist, // 财务流程首节点 存储值
  354. isEnaFileUpBtn, // 是否启用上传文件按钮
  355. n.IsFileUp, // 票据上传节点 存储值
  356. isEnaPartBtn, // 是否启用参与按钮
  357. n.IsPart, // 参与按钮 存储值
  358. visaSubNodes // 签证节点类型使用
  359. };
  360. }).ToList()
  361. };
  362. }).ToList();
  363. return new Result { Code = 200, Data = processes, Msg = "查询成功!" };
  364. }
  365. /// <summary>
  366. /// 更新节点状态
  367. /// </summary>
  368. /// <param name="nodeId">节点ID</param>
  369. /// <param name="currUserId">当前用户ID</param>
  370. /// <param name="processStatus">流程状态,默认为已完成</param>
  371. /// <returns>操作结果</returns>
  372. public async Task<Result> UpdateNodeStatusAsync(int nodeId, int currUserId, ProcessStatus processStatus = ProcessStatus.Completed)
  373. {
  374. try
  375. {
  376. // 使用事务确保数据一致性
  377. var result = await _sqlSugar.Ado.UseTranAsync(async () =>
  378. {
  379. // 1. 获取并验证节点
  380. var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
  381. .FirstAsync(n => n.Id == nodeId && n.IsDel == 0) ?? throw new BusinessException("当前节点不存在或已被删除。");
  382. // 2. 获取流程信息,检查ProcessType
  383. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  384. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0) ?? throw new BusinessException("关联的流程不存在。");
  385. // 3. 节点操作验证
  386. ValidateNodeOperation(node, processStatus);
  387. // 4. 存储更新前的值
  388. var before = new Grp_ProcessNode()
  389. {
  390. Id = node.Id,
  391. ProcessId = node.ProcessId,
  392. NodeName = node.NodeName,
  393. NodeOrder = node.NodeOrder,
  394. OverallStatus = node.OverallStatus,
  395. Operator = node.Operator,
  396. OperationTime = node.OperationTime,
  397. IsCurrent = node.IsCurrent,
  398. };
  399. // 5. 更新节点状态
  400. node.OverallStatus = processStatus;
  401. node.Operator = currUserId;
  402. node.OperationTime = DateTime.Now;
  403. var updateCount = await _sqlSugar.Updateable(node)
  404. .UpdateColumns(n => new
  405. {
  406. n.OverallStatus,
  407. n.Operator,
  408. n.OperationTime
  409. })
  410. .ExecuteCommandAsync();
  411. if (updateCount == 0)
  412. {
  413. throw new BusinessException("节点状态更新失败。");
  414. }
  415. // 6. 记录节点日志
  416. await LogNodeOpAsync(before, node, "Update", currUserId);
  417. // 7. 如果是完成当前节点,处理流程流转
  418. // 当前节点或者流程类型为商邀可进入状态流转
  419. if (processStatus == ProcessStatus.Completed && (node.IsCurrent || process.ProcessType == GroupProcessType.Invitation))
  420. {
  421. await ProcessCurrentNodeCompletionAsync(node, currUserId);
  422. }
  423. return new Result { Code = StatusCodes.Status200OK, Msg = "操作成功。" };
  424. });
  425. return result.IsSuccess ? result.Data : new Result
  426. {
  427. Code = StatusCodes.Status500InternalServerError,
  428. Msg = result.ErrorMessage
  429. };
  430. }
  431. catch (BusinessException ex)
  432. {
  433. // 业务异常
  434. return new Result { Code = StatusCodes.Status400BadRequest, Msg = ex.Message };
  435. }
  436. catch (Exception ex)
  437. {
  438. // 系统异常
  439. return new Result { Code = StatusCodes.Status500InternalServerError, Msg = "系统错误,请稍后重试" };
  440. }
  441. }
  442. /// <summary>
  443. /// 验证节点操作权限
  444. /// </summary>
  445. /// <param name="node">流程节点</param>
  446. /// <param name="targetStatus">目标状态</param>
  447. private static void ValidateNodeOperation(Grp_ProcessNode node, ProcessStatus targetStatus)
  448. {
  449. // 验证节点是否已完成
  450. if (node.OverallStatus == ProcessStatus.Completed)
  451. {
  452. throw new BusinessException("当前节点已完成,不可重复操作。");
  453. }
  454. // 验证状态流转是否合法(可选)
  455. //if (targetStatus != ProcessStatus.Completed)
  456. //{
  457. // throw new BusinessException("未开始或者进行中的节点只能重新完成,不可进行其他操作。");
  458. //}
  459. // 验证是否尝试将已完成节点改为其他状态
  460. if (node.OverallStatus == ProcessStatus.Completed && targetStatus != ProcessStatus.Completed)
  461. {
  462. throw new BusinessException("已完成节点不可修改状态。");
  463. }
  464. }
  465. /// <summary>
  466. /// 处理当前节点完成后的流程流转
  467. /// </summary>
  468. private async Task ProcessCurrentNodeCompletionAsync(Grp_ProcessNode currentNode, int currUserId)
  469. {
  470. // 1. 获取流程信息
  471. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  472. .FirstAsync(p => p.Id == currentNode.ProcessId && p.IsDel == 0);
  473. if (process == null)
  474. {
  475. throw new BusinessException("关联的流程不存在。");
  476. }
  477. var processBefore = new Grp_ProcessOverview()
  478. {
  479. Id = process.Id,
  480. GroupId = process.GroupId,
  481. ProcessOrder = process.ProcessOrder,
  482. ProcessType = process.ProcessType,
  483. OverallStatus = process.OverallStatus,
  484. StartTime = process.StartTime,
  485. EndTime = process.EndTime,
  486. UpdatedUserId = process.UpdatedUserId,
  487. UpdatedTime = process.UpdatedTime
  488. };
  489. // 2. 取消当前节点的当前状态
  490. var before = new Grp_ProcessNode()
  491. {
  492. Id = currentNode.Id,
  493. ProcessId = currentNode.ProcessId,
  494. NodeName = currentNode.NodeName,
  495. NodeOrder = currentNode.NodeOrder,
  496. OverallStatus = currentNode.OverallStatus,
  497. Operator = currentNode.Operator,
  498. OperationTime = currentNode.OperationTime,
  499. IsCurrent = currentNode.IsCurrent,
  500. };
  501. currentNode.IsCurrent = false;
  502. await _sqlSugar.Updateable(currentNode)
  503. .UpdateColumns(n => new { n.IsCurrent })
  504. .ExecuteCommandAsync();
  505. // 2.1 记录节点日志 取消当前节点状态
  506. await LogNodeOpAsync(before, currentNode, "Update", currUserId);
  507. // 3. 查找并激活下一个节点 商邀节点单独处理
  508. if (process.ProcessType == GroupProcessType.Invitation)
  509. {
  510. var invitaNodeStatus = await _sqlSugar.Queryable<Grp_ProcessNode>()
  511. .Where(x => x.IsDel == 0 && x.ProcessId == currentNode.ProcessId)
  512. .ToListAsync();
  513. int completedCount = invitaNodeStatus.Count(n => n.OverallStatus == ProcessStatus.Completed);
  514. int nodeCount = invitaNodeStatus.Count;
  515. if (completedCount == nodeCount) //全部子节点完成,该流程完成
  516. {
  517. process.OverallStatus = ProcessStatus.Completed;
  518. process.EndTime = DateTime.Now;
  519. }
  520. }
  521. else
  522. {
  523. var nextNode = await _sqlSugar.Queryable<Grp_ProcessNode>()
  524. .Where(n => n.ProcessId == currentNode.ProcessId
  525. && n.NodeOrder == currentNode.NodeOrder + 1
  526. && n.IsDel == 0)
  527. .FirstAsync();
  528. if (nextNode != null)
  529. {
  530. var nextNodeBefore = new Grp_ProcessNode()
  531. {
  532. Id = nextNode.Id,
  533. ProcessId = nextNode.ProcessId,
  534. NodeName = nextNode.NodeName,
  535. NodeOrder = nextNode.NodeOrder,
  536. OverallStatus = nextNode.OverallStatus,
  537. Operator = nextNode.Operator,
  538. OperationTime = nextNode.OperationTime,
  539. IsCurrent = nextNode.IsCurrent,
  540. };
  541. // 激活下一个节点
  542. nextNode.IsCurrent = true;
  543. nextNode.OverallStatus = ProcessStatus.InProgress;
  544. //nextNode.Operator = currUserId;
  545. //nextNode.OperationTime = DateTime.Now;
  546. var updateCount = await _sqlSugar.Updateable(nextNode)
  547. .UpdateColumns(n => new
  548. {
  549. n.IsCurrent,
  550. n.OverallStatus,
  551. n.Operator,
  552. n.OperationTime
  553. })
  554. .ExecuteCommandAsync();
  555. if (updateCount == 0)
  556. {
  557. throw new BusinessException("激活下一节点失败");
  558. }
  559. // 1.1 记录节点日志 激活下一节点当前节点状态
  560. await LogNodeOpAsync(nextNodeBefore, nextNode, "Start", currUserId);
  561. // 更新流程状态为进行中
  562. process.OverallStatus = ProcessStatus.InProgress;
  563. }
  564. else
  565. {
  566. // 下一节点不存在,整个流程完成
  567. process.OverallStatus = ProcessStatus.Completed;
  568. process.EndTime = DateTime.Now;
  569. }
  570. }
  571. // 4. 更新流程信息
  572. process.UpdatedUserId = currUserId;
  573. process.UpdatedTime = DateTime.Now;
  574. var processUpdateCount = await _sqlSugar.Updateable(process)
  575. .UpdateColumns(p => new
  576. {
  577. p.OverallStatus,
  578. p.EndTime,
  579. p.UpdatedUserId,
  580. p.UpdatedTime
  581. })
  582. .ExecuteCommandAsync();
  583. if (processUpdateCount == 0)
  584. {
  585. throw new BusinessException("流程状态更新失败。");
  586. }
  587. //记录流程日志
  588. await LogProcessOpAsync(processBefore, process, "Update", currUserId);
  589. }
  590. /// <summary>
  591. /// 更新签证节点信息及状态
  592. /// </summary>
  593. /// <param name="dto">签证节点更新数据传输对象</param>
  594. /// <returns>操作结果</returns>
  595. public async Task<Result> UpdateVisaNodeDetailsAsync(GroupProcessUpdateVisaNodeDetailsDto dto)
  596. {
  597. // 1. 获取并验证节点和流程
  598. var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
  599. .FirstAsync(n => n.Id == dto.NodeId && n.IsDel == 0)
  600. ?? throw new BusinessException("当前节点不存在或已被删除。");
  601. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  602. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0)
  603. ?? throw new BusinessException("当前流程不存在或已被删除。");
  604. if (process.ProcessType != GroupProcessType.Visa)
  605. {
  606. throw new BusinessException("当前流程节点不为签证流程,不可编辑。");
  607. }
  608. // 2. 检查签证子节点 字段信息是否全部填写
  609. var allSubNodesCompleted = dto.VisaSubNodes?.All(subNode => EntityExtensions.IsCompleted(subNode)) ?? false;
  610. // 2.1 存储更新前流程及节点信息
  611. var nodeBefore = new Grp_ProcessNode()
  612. {
  613. Id = node.Id,
  614. ProcessId = node.ProcessId,
  615. NodeName = node.NodeName,
  616. NodeOrder = node.NodeOrder,
  617. OverallStatus = node.OverallStatus,
  618. Operator = node.Operator,
  619. OperationTime = node.OperationTime,
  620. IsCurrent = node.IsCurrent,
  621. };
  622. var processBefore = new Grp_ProcessOverview()
  623. {
  624. Id = process.Id,
  625. GroupId = process.GroupId,
  626. ProcessOrder = process.ProcessOrder,
  627. ProcessType = process.ProcessType,
  628. OverallStatus = process.OverallStatus,
  629. StartTime = process.StartTime,
  630. EndTime = process.EndTime,
  631. UpdatedUserId = process.UpdatedUserId,
  632. UpdatedTime = process.UpdatedTime
  633. };
  634. // 3. 更新节点信息
  635. node.Remark = JsonConvert.SerializeObject(dto.VisaSubNodes);
  636. node.Operator = dto.CurrUserId;
  637. node.OperationTime = DateTime.Now;
  638. if (allSubNodesCompleted)
  639. {
  640. node.OverallStatus = ProcessStatus.Completed;
  641. process.OverallStatus = ProcessStatus.Completed;
  642. process.EndTime = DateTime.Now;
  643. process.UpdatedUserId = dto.CurrUserId;
  644. process.UpdatedTime = DateTime.Now;
  645. // 更新流程状态
  646. await _sqlSugar.Updateable(process)
  647. .UpdateColumns(p => new
  648. {
  649. p.OverallStatus,
  650. p.EndTime,
  651. p.UpdatedUserId,
  652. p.UpdatedTime
  653. })
  654. .ExecuteCommandAsync();
  655. //记录流程日志
  656. await LogProcessOpAsync(processBefore, process, "Update", dto.CurrUserId);
  657. }
  658. // 4. 保存节点更新
  659. await _sqlSugar.Updateable(node)
  660. .UpdateColumns(n => new
  661. {
  662. n.Remark,
  663. n.Operator,
  664. n.OperationTime,
  665. n.OverallStatus
  666. })
  667. .ExecuteCommandAsync();
  668. //记录节点日志
  669. await LogNodeOpAsync(nodeBefore, node, "Update", dto.CurrUserId);
  670. return new Result { Code = 200, Msg = "节点信息更新成功。" };
  671. }
  672. /// <summary>
  673. /// 更新节点信息及状态
  674. /// </summary>
  675. /// <param name="dto">签证节点更新数据传输对象</param>
  676. /// <returns>操作结果</returns>
  677. public async Task<Result> SetActualDoneAsync(GroupProcessSetActualDoneDto dto)
  678. {
  679. int nodeId = dto.NodeId;
  680. var isDtNul = DateTime.TryParse(dto.ActualDone, out DateTime actualDone);
  681. int currUserId = dto.CurrUserId;
  682. bool isAssist = dto.IsAssist;
  683. bool isFileUp = dto.IsFileUp;
  684. bool isPart = dto.IsPart;
  685. // 1. 获取并验证节点和流程
  686. var node = await _sqlSugar.Queryable<Grp_ProcessNode>()
  687. .FirstAsync(n => n.Id == nodeId && n.IsDel == 0)
  688. ?? throw new BusinessException("当前节点不存在或已被删除。");
  689. var process = await _sqlSugar.Queryable<Grp_ProcessOverview>()
  690. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0)
  691. ?? throw new BusinessException("当前流程不存在或已被删除。");
  692. // 2.1 存储更新前流程及节点信息
  693. var nodeBefore = new Grp_ProcessNode()
  694. {
  695. Id = node.Id,
  696. ProcessId = node.ProcessId,
  697. NodeName = node.NodeName,
  698. NodeOrder = node.NodeOrder,
  699. OverallStatus = node.OverallStatus,
  700. Operator = node.Operator,
  701. OperationTime = node.OperationTime,
  702. IsCurrent = node.IsCurrent
  703. };
  704. if (isDtNul)
  705. {
  706. node.ActualDone = actualDone;
  707. }
  708. node.IsAssist = isAssist;
  709. node.IsFileUp = isFileUp;
  710. node.IsPart = isPart;
  711. // 3. 保存节点更新
  712. await _sqlSugar.Updateable(node)
  713. .UpdateColumns(n => new
  714. {
  715. ActualDone = isDtNul ? node.ActualDone : null,
  716. n.IsAssist,
  717. n.IsFileUp,
  718. n.IsPart,
  719. })
  720. .ExecuteCommandAsync();
  721. //记录节点日志
  722. await LogNodeOpAsync(nodeBefore, node, "Update", currUserId);
  723. return new Result { Code = 200, Msg = "实际操作时间设置成功。" };
  724. }
  725. #endregion
  726. #region 会务流程
  727. /// <summary>
  728. /// 设置节点流程模板
  729. /// </summary>
  730. /// <param name="groupId"></param>
  731. /// <param name="currUserId"></param>
  732. /// <returns></returns>
  733. public async Task<List<Grp_ConfProcessOverview>> DefaultConfProcessTemps(int groupId, int currUserId)
  734. {
  735. var temps = new List<Grp_ConfProcessOverview>();
  736. //团组验证
  737. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  738. if (groupInfo == null) return temps;
  739. //// 检查是否已存在流程
  740. //var existingProcesses = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  741. // .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  742. // .ToListAsync();
  743. //if (existingProcesses.Any()) return temps;
  744. #region 会务流程
  745. //参与人 participators
  746. var defaultParticipators = new List<int>()
  747. {
  748. 213, //李新江
  749. };
  750. var defaultPorc1 = new List<Grp_ConfProcessNode>() {
  751. Grp_ConfProcessNode.Create(1,"方案/报价(含成本)","", ProcessStatus.InProgress,true,false, currUserId,defaultParticipators),
  752. Grp_ConfProcessNode.Create(2,"项目前期比选/招投标相关文件","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  753. Grp_ConfProcessNode.Create(3,"参与投标","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  754. Grp_ConfProcessNode.Create(4,"拟定/签订合同","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  755. Grp_ConfProcessNode.Create(5,"场地预订/物料设计/对接活动所需的供应商/嘉宾邀约","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  756. Grp_ConfProcessNode.Create(6,"现场执行","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  757. Grp_ConfProcessNode.Create(7,"验收报告/决算表","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  758. Grp_ConfProcessNode.Create(8,"跟进项目收款","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  759. };
  760. temps.Add(Grp_ConfProcessOverview.Create(groupId, 1, ProcessStatus.InProgress, currUserId, defaultPorc1));
  761. var defaultPorc2 = new List<Grp_ConfProcessNode>() {
  762. Grp_ConfProcessNode.Create(1,"方案/报价(含成本)","", ProcessStatus.InProgress,true,false, currUserId,defaultParticipators),
  763. Grp_ConfProcessNode.Create(2,"拟定/签订合同","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  764. Grp_ConfProcessNode.Create(3,"场地预订/物料设计/对接活动所需的供应商/嘉宾邀约","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  765. Grp_ConfProcessNode.Create(4,"现场执行","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  766. Grp_ConfProcessNode.Create(5,"验收报告/决算表","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  767. Grp_ConfProcessNode.Create(6,"跟进项目收款","", ProcessStatus.UnStarted,false,false, currUserId,defaultParticipators),
  768. };
  769. temps.Add(Grp_ConfProcessOverview.Create(groupId, 2, ProcessStatus.InProgress, currUserId, defaultPorc2));
  770. #endregion
  771. return temps;
  772. }
  773. /// <summary>
  774. /// 团组会务流程初始化
  775. /// </summary>
  776. /// <param name="groupId">团组Id</param>
  777. /// <param name="currUserId">当前用户Id</param>
  778. /// <param name="nodeTempId">节点模板Id</param>
  779. /// <returns>创建的流程信息</returns>
  780. public async Task<Result> ConfProcessInitAsync(int groupId, int currUserId,int nodeTempId = 1)
  781. {
  782. //团组验证
  783. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  784. if (groupInfo == null)
  785. {
  786. return new Result { Code = 400, Msg = "团组不存在" };
  787. }
  788. // 检查是否已存在流程
  789. var existingProcesses = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  790. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  791. .ToListAsync();
  792. if (existingProcesses.Any())
  793. {
  794. return new Result { Code = 400, Msg = "团组会务流程已存在" };
  795. }
  796. // 定义默认的流程节点
  797. var temps = await DefaultConfProcessTemps(groupId, currUserId);
  798. if (temps == null || temps.Count < 1)
  799. return new Result { Code = 400, Msg = "团组会务默认流程不存在" };
  800. var process = temps.FirstOrDefault(x => x.ProcessOrder == nodeTempId);
  801. process.CreateUserId = currUserId;
  802. _sqlSugar.BeginTran();
  803. try
  804. {
  805. var processId = await _sqlSugar.Insertable(process).ExecuteReturnIdentityAsync();
  806. if (processId < 1)
  807. {
  808. _sqlSugar.RollbackTran();
  809. return new Result { Code = 400, Msg = "团组会务流程进度总览添加失败!" };
  810. }
  811. process.Id = processId;
  812. // 记录流程日志
  813. await LogConfProcessOpAsync(null, process, "Create", currUserId);
  814. var nodes = process.Nodes.Select((nodeDto, index) => new Grp_ConfProcessNode
  815. {
  816. ProcessId = processId,
  817. NodeName = nodeDto.NodeName,
  818. NodeOrder = nodeDto.NodeOrder,
  819. Participator = JsonConvert.SerializeObject(nodeDto.Participators),
  820. OverallStatus = nodeDto.OverallStatus,
  821. NodeDescTips = nodeDto.NodeDescTips,
  822. //Country = nodeDto.Country,
  823. IsCurrent = nodeDto.IsCurrent,
  824. IsFileUp = nodeDto.IsFileUp,
  825. Remark = nodeDto.Remark,
  826. CreateUserId = currUserId,
  827. }).ToList();
  828. var nodeIds = await _sqlSugar.Insertable(nodes).ExecuteCommandAsync();
  829. if (nodeIds < 1)
  830. {
  831. _sqlSugar.RollbackTran();
  832. return new Result { Code = 400, Msg = "团组流程进度流程节点添加失败!" };
  833. }
  834. //设置节点ID
  835. nodes = await _sqlSugar.Queryable<Grp_ConfProcessNode>().Where(x => x.IsDel == 0 && x.ProcessId == processId).ToListAsync();
  836. //记录节点日志
  837. foreach (var node in nodes)
  838. {
  839. await LogConfNodeOpAsync(null, node, "Create", currUserId);
  840. }
  841. _sqlSugar.CommitTran();
  842. return new Result { Code = 200, Msg = "添加成功!" };
  843. }
  844. catch (Exception ex)
  845. {
  846. _sqlSugar.RollbackTran();
  847. return new Result { Code = 500, Msg = $"操作失败!msg:{ex.Message}" };
  848. }
  849. }
  850. /// <summary>
  851. /// 获取团组会务流程及节点详情
  852. /// </summary>
  853. /// <param name="groupId">团组Id</param>
  854. /// <param name="currUserId">当前用户Id</param>
  855. /// <returns></returns>
  856. public async Task<Result> ConfProcessesDetailsAsync(int groupId, int currUserId = 4)
  857. {
  858. //团组验证
  859. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  860. if (groupInfo == null)
  861. {
  862. return new Result { Code = 400, Msg = "团组不存在" };
  863. }
  864. // 检查是否已存在流程
  865. var existingProcesses = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  866. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  867. .ToListAsync();
  868. if (!existingProcesses.Any())
  869. {
  870. //新建团组流程
  871. var res = await ConfProcessInitAsync(groupId, currUserId);
  872. if (res.Code != 200)
  873. {
  874. return res;
  875. }
  876. }
  877. var users = await _sqlSugar.Queryable<Sys_Users>().Select(x => new { x.Id, x.CnName }).ToListAsync();
  878. var processData = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  879. .Where(p => p.GroupId == groupId && p.IsDel == 0)
  880. .Mapper(p => p.Nodes, p => p.Nodes.First().ProcessId)
  881. .ToListAsync();
  882. // 预先构建用户字典,提升查询性能
  883. var userDict = users.ToDictionary(u => u.Id, u => u.CnName);
  884. bool isNodeTemplSwitchable = true;
  885. var processes = processData.Select(p =>
  886. {
  887. var orderedNodes = p.Nodes.OrderBy(n => n.NodeOrder).ToList();
  888. var totalNodes = orderedNodes.Count;
  889. isNodeTemplSwitchable = p.OverallStatus != ProcessStatus.Completed;
  890. return new ConfProcessOverInfoView()
  891. {
  892. Id = p.Id,
  893. GroupId = p.GroupId,
  894. ProcessType = p.ProcessType,
  895. ProcessName = p.ProcessType.GetEnumDescription(),
  896. Nodes = orderedNodes.Select((n, index) =>
  897. {
  898. //var isLastNode = index == totalNodes - 1;
  899. //// 文件上传按钮启用规则
  900. //bool isEnaFileUpBtn = false;
  901. //if (isLastNode )
  902. //{
  903. // isEnaFileUpBtn = true;
  904. //}
  905. // 获取操作人姓名(使用字典提升性能)
  906. string operatorName = "-";
  907. if (n.Operator.HasValue && userDict.TryGetValue(n.Operator.Value, out var name))
  908. {
  909. operatorName = name;
  910. }
  911. return new ConfProcessNodeInfoView()
  912. {
  913. Id = n.Id,
  914. ProcessId = n.ProcessId,
  915. NodeOrder = n.NodeOrder,
  916. NodeName = n.NodeName,
  917. NodeDescTips = n.NodeDescTips,
  918. OverallStatus = n.OverallStatus,
  919. Participators = JsonConvert.DeserializeObject<List<int>>(n.Participator),
  920. StatusText = n.OverallStatus.GetEnumDescription(),
  921. Operator = operatorName,
  922. OpeateTime = n.OperationTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-",
  923. ActualDone = n.ActualDone?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
  924. IsFileUp = n.IsFileUp, // 票据上传节点 存储值
  925. };
  926. }).ToList()
  927. };
  928. }).ToList();
  929. var view = new ConfProcessOverInfo()
  930. {
  931. IsNodeTemplSwitchable = isNodeTemplSwitchable,
  932. ConfProcess = processes
  933. };
  934. return new Result { Code = 200, Data = view, Msg = "查询成功!" };
  935. }
  936. /// <summary>
  937. /// 更新节点模板
  938. /// </summary>
  939. /// <param name="groupId">团组Id</param>
  940. /// <param name="nodeTempId">节点模板Id</param>
  941. /// <param name="currUserId">当前用户Id</param>
  942. /// <returns></returns>
  943. public async Task<Result> ConfProcessChangeNodeTempSaveAsync(int groupId, int nodeTempId, int currUserId)
  944. {
  945. //节点模板id验证
  946. var nodeTempIds = new List<int>() { 1, 2 };
  947. if (!nodeTempIds.Contains(nodeTempId))
  948. {
  949. return new Result { Code = 400, Msg = "请传入有效的节点模板Id" };
  950. }
  951. //团组验证
  952. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(g => g.Id == groupId);
  953. if (groupInfo == null)
  954. {
  955. return new Result { Code = 400, Msg = "团组不存在" };
  956. }
  957. _sqlSugar.BeginTran();
  958. try
  959. {
  960. // 检查是否已存在流程
  961. var existingProcesses = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  962. .Where(p => p.IsDel == 0 && p.GroupId == groupId)
  963. .ToListAsync();
  964. if (existingProcesses.Any())
  965. {
  966. //团组会流程完成 不可切换模板
  967. if (existingProcesses.Where(x => x.OverallStatus == ProcessStatus.Completed).ToList().Count > 0)
  968. {
  969. _sqlSugar.RollbackTran();
  970. return new Result { Code = 400, Msg = $"当前团组会务流程已完成,不可切换节点模板。" };
  971. }
  972. //删除 原有的节点模板
  973. var parentIds = existingProcesses.Select(x => x.Id).ToList();
  974. var updProcesses = await _sqlSugar.Updateable<Grp_ConfProcessOverview>()
  975. .SetColumns(x => x.DeleteUserId == currUserId)
  976. .SetColumns(x => x.DeleteTime == DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
  977. .SetColumns(x => x.IsDel == 1)
  978. .Where(x => parentIds.Contains(x.Id))
  979. .ExecuteCommandAsync();
  980. var updProcessNodes = await _sqlSugar.Updateable<Grp_ConfProcessNode>()
  981. .SetColumns(x => x.DeleteUserId == currUserId)
  982. .SetColumns(x => x.DeleteTime == DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
  983. .SetColumns(x => x.IsDel == 1)
  984. .Where(x => parentIds.Contains(x.ProcessId))
  985. .ExecuteCommandAsync();
  986. }
  987. //更新模板节点信息
  988. var result = await ConfProcessInitAsync(groupId,currUserId,nodeTempId);
  989. if (result.Code == 200)
  990. {
  991. var infoResult = await ConfProcessesDetailsAsync(groupId, currUserId);
  992. if (infoResult.Code == 200)
  993. {
  994. _sqlSugar.CommitTran();
  995. return infoResult;
  996. }
  997. }
  998. _sqlSugar.RollbackTran();
  999. return new Result { Code = 500, Msg = $"操作失败!Msg:{result.Msg}" };
  1000. }
  1001. catch (Exception ex)
  1002. {
  1003. _sqlSugar.RollbackTran();
  1004. return new Result { Code = 500, Msg = $"操作失败!Msg:{ex.Message}" };
  1005. }
  1006. }
  1007. /// <summary>
  1008. /// 更新团组会务流程节点状态
  1009. /// </summary>
  1010. /// <param name="nodeId">节点ID</param>
  1011. /// <param name="currUserId">当前用户ID</param>
  1012. /// <param name="processStatus">流程状态,默认为已完成</param>
  1013. /// <returns>操作结果</returns>
  1014. public async Task<Result> UpdateConfNodeStatusAsync(int nodeId, int currUserId, ProcessStatus processStatus = ProcessStatus.Completed)
  1015. {
  1016. try
  1017. {
  1018. // 使用事务确保数据一致性
  1019. var result = await _sqlSugar.Ado.UseTranAsync(async () =>
  1020. {
  1021. // 1. 获取并验证节点
  1022. var node = await _sqlSugar.Queryable<Grp_ConfProcessNode>()
  1023. .FirstAsync(n => n.Id == nodeId && n.IsDel == 0) ?? throw new BusinessException("当前节点不存在或已被删除。");
  1024. // 2. 获取流程信息,检查ProcessType
  1025. var process = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  1026. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0) ?? throw new BusinessException("关联的流程不存在。");
  1027. // 3. 节点操作验证
  1028. ValidateConfNodeOperation(node, processStatus);
  1029. // 4. 存储更新前的值
  1030. var before = new Grp_ConfProcessNode()
  1031. {
  1032. Id = node.Id,
  1033. ProcessId = node.ProcessId,
  1034. NodeName = node.NodeName,
  1035. NodeDescTips = node.NodeDescTips,
  1036. NodeOrder = node.NodeOrder,
  1037. Participator = node.Participator,
  1038. OverallStatus = node.OverallStatus,
  1039. Operator = node.Operator,
  1040. OperationTime = node.OperationTime,
  1041. IsCurrent = node.IsCurrent,
  1042. ActualDone = node.ActualDone,
  1043. IsFileUp = node.IsFileUp,
  1044. };
  1045. // 5. 更新节点状态
  1046. node.OverallStatus = processStatus;
  1047. node.Operator = currUserId;
  1048. node.OperationTime = DateTime.Now;
  1049. var updateCount = await _sqlSugar.Updateable(node)
  1050. .UpdateColumns(n => new
  1051. {
  1052. n.OverallStatus,
  1053. n.Operator,
  1054. n.OperationTime
  1055. })
  1056. .ExecuteCommandAsync();
  1057. if (updateCount == 0)
  1058. {
  1059. throw new BusinessException("节点状态更新失败。");
  1060. }
  1061. // 6. 记录节点日志
  1062. await LogConfNodeOpAsync(before, node, "Update", currUserId);
  1063. // 7. 如果是完成当前节点,处理流程流转
  1064. if (processStatus == ProcessStatus.Completed)
  1065. {
  1066. await ConfProcessCurrentNodeCompletionAsync(node, currUserId);
  1067. }
  1068. return new Result { Code = StatusCodes.Status200OK, Msg = "操作成功。" };
  1069. });
  1070. return result.IsSuccess ? result.Data : new Result
  1071. {
  1072. Code = StatusCodes.Status500InternalServerError,
  1073. Msg = result.ErrorMessage
  1074. };
  1075. }
  1076. catch (BusinessException ex)
  1077. {
  1078. // 业务异常
  1079. return new Result { Code = StatusCodes.Status400BadRequest, Msg = ex.Message };
  1080. }
  1081. catch (Exception ex)
  1082. {
  1083. // 系统异常
  1084. return new Result { Code = StatusCodes.Status500InternalServerError, Msg = "系统错误,请稍后重试" };
  1085. }
  1086. }
  1087. /// <summary>
  1088. /// 验证会务流程节点操作权限
  1089. /// </summary>
  1090. /// <param name="node">流程节点</param>
  1091. /// <param name="targetStatus">目标状态</param>
  1092. private static void ValidateConfNodeOperation(Grp_ConfProcessNode node, ProcessStatus targetStatus)
  1093. {
  1094. // 验证节点是否已完成
  1095. if (node.OverallStatus == ProcessStatus.Completed)
  1096. {
  1097. throw new BusinessException("当前节点已完成,不可重复操作。");
  1098. }
  1099. // 验证状态流转是否合法(可选)
  1100. //if (targetStatus != ProcessStatus.Completed)
  1101. //{
  1102. // throw new BusinessException("未开始或者进行中的节点只能重新完成,不可进行其他操作。");
  1103. //}
  1104. // 验证是否尝试将已完成节点改为其他状态
  1105. if (node.OverallStatus == ProcessStatus.Completed && targetStatus != ProcessStatus.Completed)
  1106. {
  1107. throw new BusinessException("已完成节点不可修改状态。");
  1108. }
  1109. }
  1110. /// <summary>
  1111. /// 处理当前会务流程节点完成后的流程流转
  1112. /// </summary>
  1113. private async Task ConfProcessCurrentNodeCompletionAsync(Grp_ConfProcessNode currentNode, int currUserId)
  1114. {
  1115. // 1. 获取流程信息
  1116. var process = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  1117. .FirstAsync(p => p.Id == currentNode.ProcessId && p.IsDel == 0) ?? throw new BusinessException("关联的流程不存在。");
  1118. var processBefore = new Grp_ConfProcessOverview()
  1119. {
  1120. Id = process.Id,
  1121. GroupId = process.GroupId,
  1122. ProcessOrder = process.ProcessOrder,
  1123. ProcessType = process.ProcessType,
  1124. OverallStatus = process.OverallStatus,
  1125. StartTime = process.StartTime,
  1126. EndTime = process.EndTime,
  1127. UpdatedUserId = process.UpdatedUserId,
  1128. UpdatedTime = process.UpdatedTime
  1129. };
  1130. // 2. 取消当前节点的当前状态
  1131. var before = new Grp_ConfProcessNode()
  1132. {
  1133. Id = currentNode.Id,
  1134. ProcessId = currentNode.ProcessId,
  1135. NodeName = currentNode.NodeName,
  1136. NodeOrder = currentNode.NodeOrder,
  1137. OverallStatus = currentNode.OverallStatus,
  1138. Operator = currentNode.Operator,
  1139. OperationTime = currentNode.OperationTime,
  1140. IsCurrent = currentNode.IsCurrent,
  1141. };
  1142. currentNode.IsCurrent = false;
  1143. await _sqlSugar.Updateable(currentNode)
  1144. .UpdateColumns(n => new { n.IsCurrent })
  1145. .ExecuteCommandAsync();
  1146. // 2.1 记录节点日志 取消当前节点状态
  1147. await LogConfNodeOpAsync(before, currentNode, "Update", currUserId);
  1148. // 3. 查找并激活下一个节点
  1149. var nextNode = await _sqlSugar.Queryable<Grp_ConfProcessNode>()
  1150. .Where(n => n.ProcessId == currentNode.ProcessId
  1151. && n.NodeOrder == currentNode.NodeOrder + 1
  1152. && n.IsDel == 0)
  1153. .FirstAsync();
  1154. if (nextNode != null)
  1155. {
  1156. var nextNodeBefore = new Grp_ConfProcessNode()
  1157. {
  1158. Id = nextNode.Id,
  1159. ProcessId = nextNode.ProcessId,
  1160. NodeName = nextNode.NodeName,
  1161. NodeOrder = nextNode.NodeOrder,
  1162. OverallStatus = nextNode.OverallStatus,
  1163. Operator = nextNode.Operator,
  1164. OperationTime = nextNode.OperationTime,
  1165. IsCurrent = nextNode.IsCurrent,
  1166. };
  1167. // 激活下一个节点
  1168. nextNode.IsCurrent = true;
  1169. nextNode.OverallStatus = ProcessStatus.InProgress;
  1170. var updateCount = await _sqlSugar.Updateable(nextNode)
  1171. .UpdateColumns(n => new
  1172. {
  1173. n.IsCurrent,
  1174. n.OverallStatus,
  1175. n.Operator,
  1176. n.OperationTime
  1177. })
  1178. .ExecuteCommandAsync();
  1179. if (updateCount == 0)
  1180. {
  1181. throw new BusinessException("激活下一节点失败");
  1182. }
  1183. // 1.1 记录节点日志 激活下一节点当前节点状态
  1184. await LogConfNodeOpAsync(nextNodeBefore, nextNode, "Start", currUserId);
  1185. // 更新流程状态为进行中
  1186. process.OverallStatus = ProcessStatus.InProgress;
  1187. }
  1188. else
  1189. {
  1190. // 下一节点不存在,整个流程完成
  1191. process.OverallStatus = ProcessStatus.Completed;
  1192. process.EndTime = DateTime.Now;
  1193. }
  1194. // 4. 更新流程信息
  1195. process.UpdatedUserId = currUserId;
  1196. process.UpdatedTime = DateTime.Now;
  1197. var processUpdateCount = await _sqlSugar.Updateable(process)
  1198. .UpdateColumns(p => new
  1199. {
  1200. p.OverallStatus,
  1201. p.EndTime,
  1202. p.UpdatedUserId,
  1203. p.UpdatedTime
  1204. })
  1205. .ExecuteCommandAsync();
  1206. if (processUpdateCount == 0)
  1207. {
  1208. throw new BusinessException("流程状态更新失败。");
  1209. }
  1210. //记录流程日志
  1211. await LogConfProcessOpAsync(processBefore, process, "Update", currUserId);
  1212. }
  1213. /// <summary>
  1214. /// 更新节点信息
  1215. /// </summary>
  1216. /// <param name="dto">签证节点更新数据传输对象</param>
  1217. /// <returns>操作结果</returns>
  1218. public async Task<Result> SetNodeInfoAsync(ConfProcessSetActualDoneDto dto)
  1219. {
  1220. //参与人验证
  1221. if (dto.Participators?.Count < 1)
  1222. {
  1223. throw new BusinessException("参与人不能为空。");
  1224. }
  1225. int nodeId = dto.NodeId;
  1226. var isDtNul = DateTime.TryParse(dto.ActualDone, out DateTime actualDone);
  1227. int currUserId = dto.CurrUserId;
  1228. bool isFileUp = dto.IsFileUp;
  1229. // 1. 获取并验证节点和流程
  1230. var node = await _sqlSugar.Queryable<Grp_ConfProcessNode>()
  1231. .FirstAsync(n => n.Id == nodeId && n.IsDel == 0)
  1232. ?? throw new BusinessException("当前节点不存在或已被删除。");
  1233. var process = await _sqlSugar.Queryable<Grp_ConfProcessOverview>()
  1234. .FirstAsync(p => p.Id == node.ProcessId && p.IsDel == 0)
  1235. ?? throw new BusinessException("当前流程不存在或已被删除。");
  1236. // 2.1 存储更新前流程及节点信息
  1237. var nodeBefore = new Grp_ConfProcessNode()
  1238. {
  1239. Id = node.Id,
  1240. ProcessId = node.ProcessId,
  1241. NodeName = node.NodeName,
  1242. NodeOrder = node.NodeOrder,
  1243. OverallStatus = node.OverallStatus,
  1244. Operator = node.Operator,
  1245. OperationTime = node.OperationTime,
  1246. IsCurrent = node.IsCurrent
  1247. };
  1248. if (isDtNul)
  1249. {
  1250. node.ActualDone = actualDone;
  1251. }
  1252. node.IsFileUp = isFileUp;
  1253. node.Participator = JsonConvert.SerializeObject(node.Participators);
  1254. // 3. 保存节点更新
  1255. await _sqlSugar.Updateable(node)
  1256. .UpdateColumns(n => new
  1257. {
  1258. ActualDone = isDtNul ? node.ActualDone : null,
  1259. n.Participator,
  1260. n.IsFileUp,
  1261. })
  1262. .ExecuteCommandAsync();
  1263. //记录节点日志
  1264. await LogConfNodeOpAsync(nodeBefore, node, "Update", currUserId);
  1265. return new Result { Code = 200, Msg = "实际操作时间设置成功。" };
  1266. }
  1267. #endregion
  1268. #region 操作日志
  1269. #region 团组流程
  1270. /// <summary>
  1271. /// 记录流程操作日志
  1272. /// </summary>
  1273. /// <param name="before">操作前</param>
  1274. /// <param name="after">操作后</param>
  1275. /// <param name="opType">操作类型(Create - 创建、Update - 更新、Complete - 完成)</param>
  1276. /// <param name="operId">操作人ID</param>
  1277. /// <returns>异步任务</returns>
  1278. public async Task LogProcessOpAsync(Grp_ProcessOverview before, Grp_ProcessOverview after, string opType, int operId)
  1279. {
  1280. var chgDetails = GetProcessChgDetails(before, after);
  1281. var log = new Grp_ProcessLog
  1282. {
  1283. ProcessId = after?.Id ?? before?.Id,
  1284. GroupId = after?.GroupId ?? before?.GroupId ?? 0,
  1285. OpType = opType,
  1286. OpDesc = GenerateProcessOpDesc(opType, before, after, chgDetails),
  1287. BeforeData = before != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  1288. AfterData = after != null ? JsonConvert.SerializeObject(after, GetJsonSettings()) : null,
  1289. ChgFields = string.Join(",", chgDetails.Select(x => x.FieldName)),
  1290. CreateUserId = operId
  1291. };
  1292. await _sqlSugar.Insertable(log).ExecuteCommandAsync();
  1293. }
  1294. /// <summary>
  1295. /// 记录节点操作日志
  1296. /// </summary>
  1297. /// <param name="before">操作前</param>
  1298. /// <param name="after">操作后</param>
  1299. /// <param name="opType">操作类型(Create - 创建、Update - 更新、Start - 启动、Complete - 完成)</param>
  1300. /// <param name="operId">操作人ID</param>
  1301. /// <returns>异步任务</returns>
  1302. public async Task LogNodeOpAsync(Grp_ProcessNode before, Grp_ProcessNode after, string opType, int operId)
  1303. {
  1304. var chgDetails = GetNodeChgDetails(before, after);
  1305. var log = new Grp_ProcessLog
  1306. {
  1307. NodeId = after?.Id ?? before?.Id,
  1308. ProcessId = after?.ProcessId ?? before?.ProcessId,
  1309. GroupId = 0, // 通过流程ID关联获取
  1310. OpType = opType,
  1311. OpDesc = GenerateNodeOpDesc(opType, before, after, chgDetails),
  1312. BeforeData = before != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  1313. AfterData = after != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  1314. ChgFields = string.Join(",", chgDetails.Select(x => x.FieldName)),
  1315. CreateUserId = operId
  1316. };
  1317. await _sqlSugar.Insertable(log).ExecuteCommandAsync();
  1318. }
  1319. /// <summary>
  1320. /// 获取流程变更详情
  1321. /// </summary>
  1322. /// <param name="before">变更前</param>
  1323. /// <param name="after">变更后</param>
  1324. /// <returns>变更详情</returns>
  1325. private List<FieldChgDetail> GetProcessChgDetails(Grp_ProcessOverview before, Grp_ProcessOverview after)
  1326. {
  1327. var chgDetails = new List<FieldChgDetail>();
  1328. if (before == null || after == null) return chgDetails;
  1329. var props = typeof(Grp_ProcessOverview).GetProperties(BindingFlags.Public | BindingFlags.Instance)
  1330. .Where(p => p.CanRead && p.CanWrite && !IsExclField(p.Name));
  1331. foreach (var prop in props)
  1332. {
  1333. var beforeVal = prop.GetValue(before);
  1334. var afterVal = prop.GetValue(after);
  1335. if (!Equals(beforeVal, afterVal))
  1336. {
  1337. chgDetails.Add(new FieldChgDetail
  1338. {
  1339. FieldName = prop.Name,
  1340. BeforeValue = FormatVal(beforeVal),
  1341. AfterValue = FormatVal(afterVal)
  1342. });
  1343. }
  1344. }
  1345. return chgDetails;
  1346. }
  1347. /// <summary>
  1348. /// 获取节点变更详情
  1349. /// </summary>
  1350. /// <param name="before">变更前</param>
  1351. /// <param name="after">变更后</param>
  1352. /// <returns>变更详情</returns>
  1353. private List<FieldChgDetail> GetNodeChgDetails(Grp_ProcessNode before, Grp_ProcessNode after)
  1354. {
  1355. var chgDetails = new List<FieldChgDetail>();
  1356. if (before == null || after == null) return chgDetails;
  1357. var props = typeof(Grp_ProcessNode).GetProperties(BindingFlags.Public | BindingFlags.Instance)
  1358. .Where(p => p.CanRead && p.CanWrite && !IsExclField(p.Name));
  1359. foreach (var prop in props)
  1360. {
  1361. var beforeVal = prop.GetValue(before);
  1362. var afterVal = prop.GetValue(after);
  1363. if (!Equals(beforeVal, afterVal))
  1364. {
  1365. chgDetails.Add(new FieldChgDetail
  1366. {
  1367. FieldName = prop.Name,
  1368. BeforeValue = FormatVal(beforeVal),
  1369. AfterValue = FormatVal(afterVal)
  1370. });
  1371. }
  1372. }
  1373. return chgDetails;
  1374. }
  1375. /// <summary>
  1376. /// 生成流程操作描述
  1377. /// </summary>
  1378. /// <param name="opType">操作类型</param>
  1379. /// <param name="before">操作前</param>
  1380. /// <param name="after">操作后</param>
  1381. /// <param name="chgDetails">变更详情</param>
  1382. /// <returns>操作描述</returns>
  1383. private string GenerateProcessOpDesc(string opType, Grp_ProcessOverview before,
  1384. Grp_ProcessOverview after, List<FieldChgDetail> chgDetails)
  1385. {
  1386. var processType = after?.ProcessType ?? before?.ProcessType;
  1387. var processName = GetProcessTypeName(processType);
  1388. if (!chgDetails.Any())
  1389. {
  1390. return opType switch
  1391. {
  1392. "Create" => $"创建流程:{processName}",
  1393. "Update" => $"更新流程:{processName} - 无变更",
  1394. "Start" => $"启动流程:{processName}",
  1395. "Complete" => $"完成流程:{processName}",
  1396. "Delete" => $"删除流程:{processName}",
  1397. _ => $"{opType}:{processName}"
  1398. };
  1399. }
  1400. var chgDesc = string.Join("; ", chgDetails.Select(x =>
  1401. $"{GetFieldDisplayName(x.FieldName)} ({x.BeforeValue} -> {x.AfterValue})"));
  1402. return $"{GetOpTypeDisplay(opType)}:{processName} - {chgDesc}";
  1403. }
  1404. /// <summary>
  1405. /// 生成节点操作描述
  1406. /// </summary>
  1407. /// <param name="opType">操作类型</param>
  1408. /// <param name="before">操作前</param>
  1409. /// <param name="after">操作后</param>
  1410. /// <param name="chgDetails">变更详情</param>
  1411. /// <returns>操作描述</returns>
  1412. private string GenerateNodeOpDesc(string opType, Grp_ProcessNode before,
  1413. Grp_ProcessNode after, List<FieldChgDetail> chgDetails)
  1414. {
  1415. var nodeName = after?.NodeName ?? before?.NodeName;
  1416. if (!chgDetails.Any())
  1417. {
  1418. return opType switch
  1419. {
  1420. "Create" => $"创建节点:{nodeName}",
  1421. "Update" => $"更新节点:{nodeName} - 无变更",
  1422. "Start" => $"启动节点:{nodeName}",
  1423. "Complete" => $"完成节点:{nodeName}",
  1424. //"Delete" => $"删除节点:{nodeName}",
  1425. _ => $"{opType}:{nodeName}"
  1426. };
  1427. }
  1428. var chgDesc = string.Join("; ", chgDetails.Select(x =>
  1429. $"{GetFieldDisplayName(x.FieldName)} ({x.BeforeValue} -> {x.AfterValue})"));
  1430. return $"{GetOpTypeDisplay(opType)}:{nodeName} - {chgDesc}";
  1431. }
  1432. /// <summary>
  1433. /// 获取流程类型名称
  1434. /// </summary>
  1435. /// <param name="processType">流程类型</param>
  1436. /// <returns>流程名称</returns>
  1437. private static string GetProcessTypeName(GroupProcessType? processType)
  1438. {
  1439. return processType switch
  1440. {
  1441. GroupProcessType.Invitation => "商邀报批",
  1442. GroupProcessType.Visa => "签证",
  1443. GroupProcessType.AirTicket => "机票",
  1444. GroupProcessType.Hotel => "酒店",
  1445. GroupProcessType.LocalGuide => "地接",
  1446. GroupProcessType.FeeSettle => "费用结算",
  1447. _ => "未知流程"
  1448. };
  1449. }
  1450. /// <summary>
  1451. /// 获取流程日志
  1452. /// </summary>
  1453. /// <param name="processId">流程ID</param>
  1454. /// <returns>日志列表</returns>
  1455. public async Task<List<Grp_ProcessLog>> GetProcessLogsAsync(int processId)
  1456. {
  1457. return await _sqlSugar.Queryable<Grp_ProcessLog>()
  1458. .Where(x => x.ProcessId == processId)
  1459. .OrderByDescending(x => x.CreateTime)
  1460. .ToListAsync();
  1461. }
  1462. /// <summary>
  1463. /// 获取团组流程日志
  1464. /// </summary>
  1465. /// <param name="groupId">团组ID</param>
  1466. /// <returns>日志列表</returns>
  1467. public async Task<List<Grp_ProcessLog>> GetGroupLogsAsync(int groupId)
  1468. {
  1469. return await _sqlSugar.Queryable<Grp_ProcessLog>()
  1470. .Where(x => x.GroupId == groupId)
  1471. .OrderByDescending(x => x.CreateTime)
  1472. .ToListAsync();
  1473. }
  1474. #endregion
  1475. #region 会务流程
  1476. /// <summary>
  1477. /// 记录会务流程操作日志
  1478. /// </summary>
  1479. /// <param name="before">操作前</param>
  1480. /// <param name="after">操作后</param>
  1481. /// <param name="opType">操作类型(Create - 创建、Update - 更新、Complete - 完成)</param>
  1482. /// <param name="operId">操作人ID</param>
  1483. /// <returns>异步任务</returns>
  1484. public async Task LogConfProcessOpAsync(Grp_ConfProcessOverview before, Grp_ConfProcessOverview after, string opType, int operId)
  1485. {
  1486. var chgDetails = GetConfProcessChgDetails(before, after);
  1487. var log = new Grp_ConfProcessLog
  1488. {
  1489. ProcessId = after?.Id ?? before?.Id,
  1490. GroupId = after?.GroupId ?? before?.GroupId ?? 0,
  1491. OpType = opType,
  1492. OpDesc = GenerateConfProcessOpDesc(opType, before, after, chgDetails),
  1493. BeforeData = before != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  1494. AfterData = after != null ? JsonConvert.SerializeObject(after, GetJsonSettings()) : null,
  1495. ChgFields = string.Join(",", chgDetails.Select(x => x.FieldName)),
  1496. CreateUserId = operId
  1497. };
  1498. await _sqlSugar.Insertable(log).ExecuteCommandAsync();
  1499. }
  1500. /// <summary>
  1501. /// 记录会务节点操作日志
  1502. /// </summary>
  1503. /// <param name="before">操作前</param>
  1504. /// <param name="after">操作后</param>
  1505. /// <param name="opType">操作类型(Create - 创建、Update - 更新、Start - 启动、Complete - 完成)</param>
  1506. /// <param name="operId">操作人ID</param>
  1507. /// <returns>异步任务</returns>
  1508. public async Task LogConfNodeOpAsync(Grp_ConfProcessNode before, Grp_ConfProcessNode after, string opType, int operId)
  1509. {
  1510. var chgDetails = GetConfNodeChgDetails(before, after);
  1511. var log = new Grp_ConfProcessLog
  1512. {
  1513. NodeId = after?.Id ?? before?.Id,
  1514. ProcessId = after?.ProcessId ?? before?.ProcessId,
  1515. GroupId = 0, // 通过流程ID关联获取
  1516. OpType = opType,
  1517. OpDesc = GenerateConfNodeOpDesc(opType, before, after, chgDetails),
  1518. BeforeData = before != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  1519. AfterData = after != null ? JsonConvert.SerializeObject(before, GetJsonSettings()) : null,
  1520. ChgFields = string.Join(",", chgDetails.Select(x => x.FieldName)),
  1521. CreateUserId = operId
  1522. };
  1523. await _sqlSugar.Insertable(log).ExecuteCommandAsync();
  1524. }
  1525. /// <summary>
  1526. /// 获取流程变更详情
  1527. /// </summary>
  1528. /// <param name="before">变更前</param>
  1529. /// <param name="after">变更后</param>
  1530. /// <returns>变更详情</returns>
  1531. private static List<FieldChgDetail> GetConfProcessChgDetails(Grp_ConfProcessOverview before, Grp_ConfProcessOverview after)
  1532. {
  1533. var chgDetails = new List<FieldChgDetail>();
  1534. if (before == null || after == null) return chgDetails;
  1535. var props = typeof(Grp_ConfProcessOverview).GetProperties(BindingFlags.Public | BindingFlags.Instance)
  1536. .Where(p => p.CanRead && p.CanWrite && !IsExclField(p.Name));
  1537. foreach (var prop in props)
  1538. {
  1539. var beforeVal = prop.GetValue(before);
  1540. var afterVal = prop.GetValue(after);
  1541. if (!Equals(beforeVal, afterVal))
  1542. {
  1543. chgDetails.Add(new FieldChgDetail
  1544. {
  1545. FieldName = prop.Name,
  1546. BeforeValue = FormatVal(beforeVal),
  1547. AfterValue = FormatVal(afterVal)
  1548. });
  1549. }
  1550. }
  1551. return chgDetails;
  1552. }
  1553. /// <summary>
  1554. /// 获取节点变更详情
  1555. /// </summary>
  1556. /// <param name="before">变更前</param>
  1557. /// <param name="after">变更后</param>
  1558. /// <returns>变更详情</returns>
  1559. private static List<FieldChgDetail> GetConfNodeChgDetails(Grp_ConfProcessNode before, Grp_ConfProcessNode after)
  1560. {
  1561. var chgDetails = new List<FieldChgDetail>();
  1562. if (before == null || after == null) return chgDetails;
  1563. var props = typeof(Grp_ConfProcessNode).GetProperties(BindingFlags.Public | BindingFlags.Instance)
  1564. .Where(p => p.CanRead && p.CanWrite && !IsExclField(p.Name));
  1565. foreach (var prop in props)
  1566. {
  1567. var beforeVal = prop.GetValue(before);
  1568. var afterVal = prop.GetValue(after);
  1569. if (!Equals(beforeVal, afterVal))
  1570. {
  1571. chgDetails.Add(new FieldChgDetail
  1572. {
  1573. FieldName = prop.Name,
  1574. BeforeValue = FormatVal(beforeVal),
  1575. AfterValue = FormatVal(afterVal)
  1576. });
  1577. }
  1578. }
  1579. return chgDetails;
  1580. }
  1581. /// <summary>
  1582. /// 生成流程操作描述
  1583. /// </summary>
  1584. /// <param name="opType">操作类型</param>
  1585. /// <param name="before">操作前</param>
  1586. /// <param name="after">操作后</param>
  1587. /// <param name="chgDetails">变更详情</param>
  1588. /// <returns>操作描述</returns>
  1589. private static string GenerateConfProcessOpDesc(string opType, Grp_ConfProcessOverview before,
  1590. Grp_ConfProcessOverview after, List<FieldChgDetail> chgDetails)
  1591. {
  1592. var processType = after?.ProcessType ?? before?.ProcessType;
  1593. var processName = GetConfProcessTypeName(processType);
  1594. if (!chgDetails.Any())
  1595. {
  1596. return opType switch
  1597. {
  1598. "Create" => $"创建流程:{processName}",
  1599. "Update" => $"更新流程:{processName} - 无变更",
  1600. "Start" => $"启动流程:{processName}",
  1601. "Complete" => $"完成流程:{processName}",
  1602. "Delete" => $"删除流程:{processName}",
  1603. _ => $"{opType}:{processName}"
  1604. };
  1605. }
  1606. var chgDesc = string.Join("; ", chgDetails.Select(x =>
  1607. $"{GetFieldDisplayName(x.FieldName)} ({x.BeforeValue} -> {x.AfterValue})"));
  1608. return $"{GetOpTypeDisplay(opType)}:{processName} - {chgDesc}";
  1609. }
  1610. /// <summary>
  1611. /// 生成节点操作描述
  1612. /// </summary>
  1613. /// <param name="opType">操作类型</param>
  1614. /// <param name="before">操作前</param>
  1615. /// <param name="after">操作后</param>
  1616. /// <param name="chgDetails">变更详情</param>
  1617. /// <returns>操作描述</returns>
  1618. private static string GenerateConfNodeOpDesc(string opType, Grp_ConfProcessNode before,
  1619. Grp_ConfProcessNode after, List<FieldChgDetail> chgDetails)
  1620. {
  1621. var nodeName = after?.NodeName ?? before?.NodeName;
  1622. if (!chgDetails.Any())
  1623. {
  1624. return opType switch
  1625. {
  1626. "Create" => $"创建节点:{nodeName}",
  1627. "Update" => $"更新节点:{nodeName} - 无变更",
  1628. "Start" => $"启动节点:{nodeName}",
  1629. "Complete" => $"完成节点:{nodeName}",
  1630. //"Delete" => $"删除节点:{nodeName}",
  1631. _ => $"{opType}:{nodeName}"
  1632. };
  1633. }
  1634. var chgDesc = string.Join("; ", chgDetails.Select(x =>
  1635. $"{GetFieldDisplayName(x.FieldName)} ({x.BeforeValue} -> {x.AfterValue})"));
  1636. return $"{GetOpTypeDisplay(opType)}:{nodeName} - {chgDesc}";
  1637. }
  1638. /// <summary>
  1639. /// 获取流程类型名称
  1640. /// </summary>
  1641. /// <param name="processType">流程类型</param>
  1642. /// <returns>流程名称</returns>
  1643. private static string GetConfProcessTypeName(ConfProcessType? processType)
  1644. {
  1645. return processType switch
  1646. {
  1647. ConfProcessType.Conference => "会务",
  1648. _ => "未知流程"
  1649. };
  1650. }
  1651. /// <summary>
  1652. /// 获取会务流程日志
  1653. /// </summary>
  1654. /// <param name="processId">流程ID</param>
  1655. /// <returns>日志列表</returns>
  1656. public async Task<List<Grp_ConfProcessLog>> GetConfProcessLogsAsync(int processId)
  1657. {
  1658. return await _sqlSugar.Queryable<Grp_ConfProcessLog>()
  1659. .Where(x => x.ProcessId == processId)
  1660. .OrderByDescending(x => x.CreateTime)
  1661. .ToListAsync();
  1662. }
  1663. /// <summary>
  1664. /// 获取团组会务流程日志
  1665. /// </summary>
  1666. /// <param name="groupId">团组ID</param>
  1667. /// <returns>日志列表</returns>
  1668. public async Task<List<Grp_ConfProcessLog>> GetGroupConfLogsAsync(int groupId)
  1669. {
  1670. return await _sqlSugar.Queryable<Grp_ConfProcessLog>()
  1671. .Where(x => x.GroupId == groupId)
  1672. .OrderByDescending(x => x.CreateTime)
  1673. .ToListAsync();
  1674. }
  1675. #endregion
  1676. #region 日志私有方法
  1677. /// <summary>
  1678. /// 获取JSON序列化设置
  1679. /// </summary>
  1680. /// <returns>JSON设置</returns>
  1681. private static JsonSerializerSettings GetJsonSettings()
  1682. {
  1683. return new JsonSerializerSettings
  1684. {
  1685. ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
  1686. NullValueHandling = NullValueHandling.Ignore,
  1687. DateFormatString = "yyyy-MM-dd HH:mm:ss",
  1688. Formatting = Formatting.None
  1689. };
  1690. }
  1691. /// <summary>
  1692. /// 获取操作类型显示
  1693. /// </summary>
  1694. /// <param name="opType">操作类型</param>
  1695. /// <returns>显示名称</returns>
  1696. private static string GetOpTypeDisplay(string opType)
  1697. {
  1698. return opType switch
  1699. {
  1700. "Create" => "创建",
  1701. "Update" => "更新",
  1702. "Start" => "启动",
  1703. "Complete" => "完成",
  1704. "Delete" => "删除",
  1705. "StatusChg" => "状态变更",
  1706. _ => opType
  1707. };
  1708. }
  1709. /// <summary>
  1710. /// 获取字段显示名称
  1711. /// </summary>
  1712. /// <param name="fieldName">字段名</param>
  1713. /// <returns>显示名称</returns>
  1714. private static string GetFieldDisplayName(string fieldName)
  1715. {
  1716. return fieldName switch
  1717. {
  1718. "OverallStatus" => "状态",
  1719. "ProcessOrder" => "流程顺序",
  1720. "StartTime" => "开始时间",
  1721. "EndTime" => "结束时间",
  1722. "UpdatedUserId" => "更新人",
  1723. "UpdatedTime" => "更新时间",
  1724. "NodeOrder" => "节点顺序",
  1725. "NodeName" => "节点名称",
  1726. "NodeDescTips" => "节点描述提示",
  1727. "IsCurrent" => "当前节点",
  1728. "Participator" => "参与人",
  1729. "Operator" => "操作人",
  1730. "OperationTime" => "操作时间",
  1731. "ActualDone" => "实际完成时间",
  1732. "IsFileUp" => "是否上传文件(签证、机票、酒店、地接 流程结尾节点使用)",
  1733. "IsAssist" => "是否协助(财务流程首节点使用)",
  1734. "IsPart" => "是否参与(商邀 第五步使用)",
  1735. _ => fieldName
  1736. };
  1737. }
  1738. /// <summary>
  1739. /// 格式化值显示
  1740. /// </summary>
  1741. /// <param name="value">值</param>
  1742. /// <returns>格式化值</returns>
  1743. private static string FormatVal(object value)
  1744. {
  1745. if (value == null) return "空";
  1746. if (value is ProcessStatus status)
  1747. {
  1748. return status switch
  1749. {
  1750. ProcessStatus.UnStarted => "未开始",
  1751. ProcessStatus.InProgress => "进行中",
  1752. ProcessStatus.Completed => "已完成",
  1753. _ => status.ToString()
  1754. };
  1755. }
  1756. if (value is bool boolVal) return boolVal ? "是" : "否";
  1757. if (value is DateTime dateVal) return dateVal.ToString("yyyy-MM-dd HH:mm");
  1758. var strVal = value.ToString();
  1759. return string.IsNullOrEmpty(strVal) ? "空" : strVal;
  1760. }
  1761. /// <summary>
  1762. /// 检查是否排除字段
  1763. /// </summary>
  1764. /// <param name="fieldName">字段名</param>
  1765. /// <returns>是否排除</returns>
  1766. private static bool IsExclField(string fieldName)
  1767. {
  1768. var exclFields = new List<string>
  1769. {
  1770. "Id", "CreateTime", "CreateUserId", "Nodes", "Process" // 导航属性
  1771. };
  1772. return exclFields.Contains(fieldName);
  1773. }
  1774. #endregion
  1775. #endregion
  1776. }
  1777. }