ProcessOverviewRepository.cs 128 KB

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