ProcessOverviewRepository.cs 127 KB

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