ProcessOverviewRepository.cs 128 KB

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