ProcessOverviewRepository.cs 86 KB

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