ProcessOverviewRepository.cs 100 KB

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