ProcessOverviewRepository.cs 126 KB

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