ops_event_task.go 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859
  1. package opsdev
  2. import (
  3. "context"
  4. "fmt"
  5. "math"
  6. "strings"
  7. "dashoo.cn/opms_libary/myerrors"
  8. opsdevdao "dashoo.cn/opms_parent/app/dao/opsdev"
  9. opsdevmodel "dashoo.cn/opms_parent/app/model/opsdev"
  10. "dashoo.cn/opms_parent/app/service"
  11. "github.com/gogf/gf/database/gdb"
  12. "github.com/gogf/gf/frame/g"
  13. "github.com/gogf/gf/os/gtime"
  14. "github.com/gogf/gf/util/gconv"
  15. )
  16. // OpsEventTaskService 任务业务逻辑实现类
  17. type OpsEventTaskService struct {
  18. *service.ContextService
  19. TaskDao *opsdevdao.OpsEventTaskDao
  20. RecordDao *opsdevdao.OpsEventTaskRecordDao
  21. AttachmentDao *opsdevdao.OpsEventTaskAttachmentDao
  22. ProjectDao *opsdevdao.OpsDeliveryProjectDao
  23. ReleaseDao *opsdevdao.OpsEventTaskReleaseDao
  24. WorkHourDao *opsdevdao.OpsEventTaskWorkHourDao
  25. EventDao *opsdevdao.OpsDeliveryProjectEventDao
  26. EventRecordDao *opsdevdao.OpsDeliveryProjectEventRecordDao
  27. }
  28. // NewOpsEventTaskService 初始化service
  29. func NewOpsEventTaskService(ctx context.Context) (svc *OpsEventTaskService, err error) {
  30. svc = new(OpsEventTaskService)
  31. if svc.ContextService, err = svc.Init(ctx); err != nil {
  32. return nil, err
  33. }
  34. svc.TaskDao = opsdevdao.NewOpsEventTaskDao(svc.Tenant)
  35. svc.RecordDao = opsdevdao.NewOpsEventTaskRecordDao(svc.Tenant)
  36. svc.AttachmentDao = opsdevdao.NewOpsEventTaskAttachmentDao(svc.Tenant)
  37. svc.ProjectDao = opsdevdao.NewOpsDeliveryProjectDao(svc.Tenant)
  38. svc.ReleaseDao = opsdevdao.NewOpsEventTaskReleaseDao(svc.Tenant)
  39. svc.WorkHourDao = opsdevdao.NewOpsEventTaskWorkHourDao(svc.Tenant)
  40. svc.EventDao = opsdevdao.NewOpsDeliveryProjectEventDao(svc.Tenant)
  41. svc.EventRecordDao = opsdevdao.NewOpsDeliveryProjectEventRecordDao(svc.Tenant)
  42. return svc, nil
  43. }
  44. // GetList 分页查询任务列表
  45. func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (total int, list []*opsdevmodel.OpsEventTaskRsp, err error) {
  46. db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime, s.TaskDao.Columns.TaskDesc)
  47. // 项目ID筛选(单选)
  48. if req.ProjectId > 0 {
  49. db = db.Where(s.TaskDao.Columns.ProjectId, req.ProjectId)
  50. }
  51. // 项目ID列表筛选(多选,优先于单选)
  52. if len(req.ProjectIds) > 0 {
  53. db = db.Where(s.TaskDao.Columns.ProjectId+" in (?)", req.ProjectIds)
  54. }
  55. // 产品线筛选:仅 projectId=0 且未指定 projectIds 时生效,通过 EXISTS 关联项目表
  56. if req.ProductLine != "" && req.ProjectId <= 0 && len(req.ProjectIds) == 0 {
  57. db = db.Where(fmt.Sprintf(
  58. "EXISTS (SELECT 1 FROM %s p WHERE p.id = %s AND p.product_line = ? AND p.deleted_time IS NULL)",
  59. s.ProjectDao.Table,
  60. s.TaskDao.Columns.ProjectId,
  61. ), req.ProductLine)
  62. }
  63. // 关联事件ID筛选
  64. if req.EventId > 0 {
  65. db = db.Where(s.TaskDao.Columns.EventId, req.EventId)
  66. }
  67. // 任务标题模糊查询
  68. if req.TaskTitle != "" {
  69. db = db.Where(s.TaskDao.Columns.TaskTitle+" like ?", "%"+req.TaskTitle+"%")
  70. }
  71. // 任务类型筛选(多选)
  72. if len(req.TaskType) > 0 {
  73. db = db.Where(s.TaskDao.Columns.TaskType+" in (?)", req.TaskType)
  74. }
  75. // 任务状态筛选(多选)
  76. if len(req.TaskStatus) > 0 {
  77. db = db.Where(s.TaskDao.Columns.TaskStatus+" in (?)", req.TaskStatus)
  78. }
  79. // 优先级筛选(多选)
  80. if len(req.Priority) > 0 {
  81. db = db.Where(s.TaskDao.Columns.Priority+" in (?)", req.Priority)
  82. }
  83. // 执行人筛选(多选)
  84. if len(req.OpsUserName) > 0 {
  85. db = db.Where(s.TaskDao.Columns.OpsUserName+" in (?)", req.OpsUserName)
  86. }
  87. // 发布版本为空筛选(用于发版任务选择未发版任务)
  88. if req.ReleaseVersionEmpty {
  89. db = db.Where(s.TaskDao.Columns.ReleaseVersion + " is null or " + s.TaskDao.Columns.ReleaseVersion + " = ''")
  90. }
  91. // 排期状态筛选(plan_start_time/plan_end_time 为 datetime 类型,需用 IS NULL)
  92. if req.ScheduleStatus == "scheduled" {
  93. db = db.Where(s.TaskDao.Columns.PlanStartTime + " is not null").Where(s.TaskDao.Columns.PlanEndTime + " is not null")
  94. } else if req.ScheduleStatus == "unscheduled" {
  95. db = db.Where(s.TaskDao.Columns.PlanStartTime + " is null or " + s.TaskDao.Columns.PlanEndTime + " is null")
  96. }
  97. // 计划结束日期范围筛选
  98. if req.PlanEndDateStart != "" {
  99. db = db.Where(s.TaskDao.Columns.PlanEndTime+" >= ?", req.PlanEndDateStart+" 00:00:00")
  100. }
  101. if req.PlanEndDateEnd != "" {
  102. db = db.Where(s.TaskDao.Columns.PlanEndTime+" <= ?", req.PlanEndDateEnd+" 23:59:59")
  103. }
  104. // 创建日期范围筛选
  105. if req.CreatedTimeStart != "" {
  106. db = db.Where(s.TaskDao.Columns.CreatedTime+" >= ?", req.CreatedTimeStart+" 00:00:00")
  107. }
  108. if req.CreatedTimeEnd != "" {
  109. db = db.Where(s.TaskDao.Columns.CreatedTime+" <= ?", req.CreatedTimeEnd+" 23:59:59")
  110. }
  111. // 完成日期范围筛选
  112. if req.CompleteTimeStart != "" {
  113. db = db.Where(s.TaskDao.Columns.CompleteTime+" >= ?", req.CompleteTimeStart+" 00:00:00")
  114. }
  115. if req.CompleteTimeEnd != "" {
  116. db = db.Where(s.TaskDao.Columns.CompleteTime+" <= ?", req.CompleteTimeEnd+" 23:59:59")
  117. }
  118. // 统计总数
  119. total, err = db.Count()
  120. if err != nil {
  121. g.Log().Error(err)
  122. return 0, nil, myerrors.DbError("获取任务总数失败")
  123. }
  124. // 分页查询
  125. pageNum, pageSize := req.GetPage()
  126. var entityList []*opsdevmodel.OpsEventTask
  127. // 处理排序
  128. if len(req.SortFields) > 0 {
  129. orderClauses := []string{}
  130. for _, sort := range req.SortFields {
  131. // 将前端字段名转换为数据库列名
  132. colName := s.getSortColumnName(sort.Field)
  133. if colName != "" {
  134. orderClauses = append(orderClauses, colName+" "+strings.ToUpper(sort.Order))
  135. }
  136. }
  137. if len(orderClauses) > 0 {
  138. db = db.Order(strings.Join(orderClauses, ", "))
  139. } else {
  140. db = db.Order(s.TaskDao.Columns.CreatedTime + " desc")
  141. }
  142. } else {
  143. db = db.Order(s.TaskDao.Columns.CreatedTime + " desc")
  144. }
  145. err = db.Page(pageNum, pageSize).Scan(&entityList)
  146. if err != nil {
  147. g.Log().Error(err)
  148. return 0, nil, myerrors.DbError("查询任务列表失败")
  149. }
  150. // 转换为响应结构体
  151. if err = gconv.Structs(entityList, &list); err != nil {
  152. g.Log().Error(err)
  153. return 0, nil, myerrors.DbError("数据转换失败")
  154. }
  155. return
  156. }
  157. // getSortColumnName 将前端排序字段名转换为数据库列名
  158. func (s *OpsEventTaskService) getSortColumnName(field string) string {
  159. // 前端字段名到数据库列名的映射
  160. fieldMap := map[string]string{
  161. "taskTitle": s.TaskDao.Columns.TaskTitle,
  162. "functionName": s.TaskDao.Columns.FunctionName,
  163. "taskType": s.TaskDao.Columns.TaskType,
  164. "taskStatus": s.TaskDao.Columns.TaskStatus,
  165. "priority": s.TaskDao.Columns.Priority,
  166. "opsUserName": s.TaskDao.Columns.OpsUserName,
  167. "planStartTime": s.TaskDao.Columns.PlanStartTime,
  168. "planEndTime": s.TaskDao.Columns.PlanEndTime,
  169. "completeTime": s.TaskDao.Columns.CompleteTime,
  170. "releaseVersion": s.TaskDao.Columns.ReleaseVersion,
  171. "createdTime": s.TaskDao.Columns.CreatedTime,
  172. }
  173. if colName, ok := fieldMap[field]; ok {
  174. return colName
  175. }
  176. return ""
  177. }
  178. const maxTaskNoRetries = 3
  179. // isDuplicateEntryError 判断是否为数据库唯一键冲突
  180. func isDuplicateEntryError(err error) bool {
  181. if err == nil {
  182. return false
  183. }
  184. return strings.Contains(strings.ToLower(err.Error()), "duplicate entry")
  185. }
  186. // Create 新增任务,包含事务控制和过程记录
  187. func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) (err error) {
  188. // 判断任务状态:如果执行人、计划开始时间、计划结束时间都填写了,则状态为处理中,否则为待处理
  189. taskStatus := opsdevmodel.TaskStatusTodo
  190. if req.OpsUserId > 0 && req.PlanStartTime != "" && req.PlanEndTime != "" {
  191. taskStatus = opsdevmodel.TaskStatusProcessing
  192. }
  193. for i := 0; i < maxTaskNoRetries; i++ {
  194. err = s.doCreate(req, taskStatus)
  195. if err == nil {
  196. return nil
  197. }
  198. if !isDuplicateEntryError(err) {
  199. return err
  200. }
  201. }
  202. return myerrors.DbError("任务编号生成冲突,请稍后重试")
  203. }
  204. // doCreate 执行新增任务(可在任务编号冲突时重试)
  205. func (s *OpsEventTaskService) doCreate(req *opsdevmodel.OpsEventTaskAddReq, taskStatus string) error {
  206. // 生成任务编号
  207. taskNo := s.generateTaskNo()
  208. // 构造数据
  209. data := g.Map{
  210. s.TaskDao.Columns.TaskNo: taskNo,
  211. s.TaskDao.Columns.ProjectId: req.ProjectId,
  212. s.TaskDao.Columns.ProjectName: s.getProjectName(req.ProjectId),
  213. s.TaskDao.Columns.TaskTitle: req.TaskTitle,
  214. s.TaskDao.Columns.TaskDesc: req.TaskDesc,
  215. s.TaskDao.Columns.FunctionName: req.FunctionName,
  216. s.TaskDao.Columns.TaskType: req.TaskType,
  217. s.TaskDao.Columns.TaskStatus: taskStatus,
  218. s.TaskDao.Columns.Priority: req.Priority,
  219. s.TaskDao.Columns.OpsUserId: req.OpsUserId,
  220. s.TaskDao.Columns.OpsUserName: req.OpsUserName,
  221. s.TaskDao.Columns.EstimateWorkHour: req.EstimateWorkHour,
  222. s.TaskDao.Columns.DefectType: req.DefectType,
  223. s.TaskDao.Columns.ReleaseVersion: req.ReleaseVersion,
  224. s.TaskDao.Columns.Remark: req.Remark,
  225. s.TaskDao.Columns.EventId: req.EventId,
  226. s.TaskDao.Columns.EventType: req.EventType,
  227. s.TaskDao.Columns.TaskParentId: req.TaskParentId,
  228. s.TaskDao.Columns.Attribute2: req.Attribute2,
  229. }
  230. if req.PlanStartTime != "" {
  231. data[s.TaskDao.Columns.PlanStartTime] = req.PlanStartTime
  232. }
  233. if req.PlanEndTime != "" {
  234. data[s.TaskDao.Columns.PlanEndTime] = req.PlanEndTime
  235. }
  236. // 补齐审计字段
  237. service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  238. // 使用事务控制
  239. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  240. // 1. 创建任务记录
  241. result, err := s.TaskDao.TX(tx).Data(data).Insert()
  242. if err != nil {
  243. g.Log().Error(err)
  244. if isDuplicateEntryError(err) {
  245. return err
  246. }
  247. return myerrors.DbError("新增任务失败")
  248. }
  249. // 获取新创建的任务ID
  250. taskId, err := result.LastInsertId()
  251. if err != nil {
  252. g.Log().Error(err)
  253. return myerrors.DbError("获取任务ID失败")
  254. }
  255. // 2. 创建任务过程记录
  256. recordData := g.Map{
  257. s.RecordDao.Columns.TaskId: taskId,
  258. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  259. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  260. s.RecordDao.Columns.HandleContent: "创建任务<br/>任务标题: " + req.TaskTitle,
  261. }
  262. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  263. recordResult, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  264. if err != nil {
  265. g.Log().Error(err)
  266. return myerrors.DbError("新增任务过程记录失败")
  267. }
  268. // 获取过程记录ID
  269. recordId, err := recordResult.LastInsertId()
  270. if err != nil {
  271. g.Log().Error(err)
  272. return myerrors.DbError("获取过程记录ID失败")
  273. }
  274. // 3. 保存附件到任务附件表,关联过程记录
  275. if len(req.Attachments) > 0 {
  276. for _, att := range req.Attachments {
  277. attData := g.Map{
  278. s.AttachmentDao.Columns.TaskId: taskId,
  279. s.AttachmentDao.Columns.TaskRecordId: recordId,
  280. s.AttachmentDao.Columns.FileName: att.FileName,
  281. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  282. s.AttachmentDao.Columns.FileType: att.FileType,
  283. }
  284. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  285. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  286. if err != nil {
  287. g.Log().Error(err)
  288. return myerrors.DbError("保存附件信息失败")
  289. }
  290. }
  291. }
  292. return nil
  293. })
  294. }
  295. // UpdateById 根据ID更新任务
  296. func (s *OpsEventTaskService) UpdateById(req *opsdevmodel.OpsEventTaskUpdateReq) error {
  297. // 校验数据是否存在
  298. var entity opsdevmodel.OpsEventTask
  299. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  300. if err != nil {
  301. g.Log().Error(err)
  302. return myerrors.DbError("查询任务数据失败")
  303. }
  304. if entity.Id <= 0 {
  305. return myerrors.TipsError("任务数据不存在")
  306. }
  307. // 已完成(30)或已作废(90)状态的任务不允许编辑
  308. if !s.canEdit(entity.TaskStatus) {
  309. return myerrors.TipsError("已完成或已作废状态的任务不允许编辑")
  310. }
  311. // 构造更新数据 - 只更新传入的非空字段
  312. data := g.Map{}
  313. if req.TaskTitle != "" {
  314. data[s.TaskDao.Columns.TaskTitle] = req.TaskTitle
  315. }
  316. if req.TaskDesc != "" {
  317. data[s.TaskDao.Columns.TaskDesc] = req.TaskDesc
  318. }
  319. if req.FunctionName != "" {
  320. data[s.TaskDao.Columns.FunctionName] = req.FunctionName
  321. }
  322. if req.TaskType != "" {
  323. data[s.TaskDao.Columns.TaskType] = req.TaskType
  324. }
  325. if req.Priority != "" {
  326. data[s.TaskDao.Columns.Priority] = req.Priority
  327. }
  328. if req.OpsUserId > 0 {
  329. data[s.TaskDao.Columns.OpsUserId] = req.OpsUserId
  330. }
  331. if req.OpsUserName != "" {
  332. data[s.TaskDao.Columns.OpsUserName] = req.OpsUserName
  333. }
  334. if req.PlanStartTime != "" {
  335. data[s.TaskDao.Columns.PlanStartTime] = req.PlanStartTime
  336. }
  337. if req.PlanEndTime != "" {
  338. data[s.TaskDao.Columns.PlanEndTime] = req.PlanEndTime
  339. }
  340. if req.EstimateWorkHour > 0 {
  341. data[s.TaskDao.Columns.EstimateWorkHour] = req.EstimateWorkHour
  342. }
  343. if req.DefectType != "" {
  344. data[s.TaskDao.Columns.DefectType] = req.DefectType
  345. }
  346. if req.ReleaseVersion != "" {
  347. data[s.TaskDao.Columns.ReleaseVersion] = req.ReleaseVersion
  348. }
  349. if req.Remark != "" {
  350. data[s.TaskDao.Columns.Remark] = req.Remark
  351. }
  352. if req.Attribute2 != "" {
  353. data[s.TaskDao.Columns.Attribute2] = req.Attribute2
  354. }
  355. // 补齐审计字段
  356. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  357. // 更新操作,排除不可改字段
  358. _, err = s.TaskDao.FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  359. if err != nil {
  360. g.Log().Error(err)
  361. return myerrors.DbError("更新任务失败")
  362. }
  363. return nil
  364. }
  365. // DeleteByIds 根据ID批量删除
  366. func (s *OpsEventTaskService) DeleteByIds(ids []int64) error {
  367. if len(ids) == 0 {
  368. return myerrors.TipsError("请选择需要删除的任务")
  369. }
  370. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  371. // 1. 删除任务
  372. _, err := s.TaskDao.TX(tx).WhereIn(s.TaskDao.Columns.Id, ids).Delete()
  373. if err != nil {
  374. g.Log().Error(err)
  375. return myerrors.DbError("删除任务失败")
  376. }
  377. // 2. 删除关联的过程记录
  378. _, err = s.RecordDao.TX(tx).WhereIn(s.RecordDao.Columns.TaskId, ids).Delete()
  379. if err != nil {
  380. g.Log().Error(err)
  381. return myerrors.DbError("删除任务过程记录失败")
  382. }
  383. // 3. 删除关联的附件
  384. _, err = s.AttachmentDao.TX(tx).WhereIn(s.AttachmentDao.Columns.TaskId, ids).Delete()
  385. if err != nil {
  386. g.Log().Error(err)
  387. return myerrors.DbError("删除任务附件失败")
  388. }
  389. return nil
  390. })
  391. }
  392. // GetById 根据ID查询单条(关联项目信息)
  393. func (s *OpsEventTaskService) GetById(id int) (*opsdevmodel.OpsEventTaskRsp, error) {
  394. var entity opsdevmodel.OpsEventTask
  395. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, id).Scan(&entity)
  396. if err != nil {
  397. g.Log().Error(err)
  398. return nil, myerrors.DbError("查询任务数据失败")
  399. }
  400. if entity.Id <= 0 {
  401. return nil, myerrors.TipsError("任务数据不存在")
  402. }
  403. var rsp opsdevmodel.OpsEventTaskRsp
  404. if err := gconv.Struct(entity, &rsp); err != nil {
  405. g.Log().Error(err)
  406. return nil, myerrors.DbError("数据转换失败")
  407. }
  408. return &rsp, nil
  409. }
  410. // Schedule 任务排期
  411. func (s *OpsEventTaskService) Schedule(req *opsdevmodel.OpsEventTaskScheduleReq) error {
  412. // 校验数据是否存在
  413. var entity opsdevmodel.OpsEventTask
  414. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  415. if err != nil {
  416. g.Log().Error(err)
  417. return myerrors.DbError("查询任务数据失败")
  418. }
  419. if entity.Id <= 0 {
  420. return myerrors.TipsError("任务数据不存在")
  421. }
  422. // 只有待处理状态可以排期
  423. if !s.canSchedule(entity.TaskStatus) {
  424. return myerrors.TipsError("只有待处理状态的任务可以排期")
  425. }
  426. data := g.Map{
  427. s.TaskDao.Columns.OpsUserId: req.OpsUserId,
  428. s.TaskDao.Columns.OpsUserName: req.OpsUserName,
  429. s.TaskDao.Columns.PlanStartTime: req.PlanStartTime,
  430. s.TaskDao.Columns.PlanEndTime: req.PlanEndTime,
  431. s.TaskDao.Columns.EstimateWorkHour: req.EstimateWorkHour,
  432. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusProcessing,
  433. }
  434. // 补齐审计字段
  435. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  436. // 使用事务
  437. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  438. // 1. 更新任务
  439. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  440. if err != nil {
  441. g.Log().Error(err)
  442. return myerrors.DbError("任务排期失败")
  443. }
  444. // 2. 创建过程记录
  445. handleContent := "任务排期<br/>执行人: " + req.OpsUserName +
  446. "<br/>计划开始: " + req.PlanStartTime +
  447. "<br/>计划结束: " + req.PlanEndTime +
  448. "<br/>预估工时: " + gconv.String(req.EstimateWorkHour) + "小时"
  449. recordData := g.Map{
  450. s.RecordDao.Columns.TaskId: req.Id,
  451. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  452. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  453. s.RecordDao.Columns.HandleContent: handleContent,
  454. }
  455. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  456. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  457. if err != nil {
  458. g.Log().Error(err)
  459. return myerrors.DbError("新增过程记录失败")
  460. }
  461. return nil
  462. })
  463. }
  464. // Start 开始任务
  465. func (s *OpsEventTaskService) Start(req *opsdevmodel.OpsEventTaskStartReq) error {
  466. // 校验数据是否存在
  467. var entity opsdevmodel.OpsEventTask
  468. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  469. if err != nil {
  470. g.Log().Error(err)
  471. return myerrors.DbError("查询任务数据失败")
  472. }
  473. if entity.Id <= 0 {
  474. return myerrors.TipsError("任务数据不存在")
  475. }
  476. // 只有待处理(10)或暂停(25)状态可以开始
  477. if !s.canStart(entity.TaskStatus) {
  478. return myerrors.TipsError("只有待处理或暂停状态的任务可以开始")
  479. }
  480. // 构造更新数据
  481. data := g.Map{
  482. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusProcessing, // 处理中
  483. }
  484. // 补齐审计字段
  485. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  486. // 使用事务
  487. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  488. // 1. 更新任务状态
  489. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  490. if err != nil {
  491. g.Log().Error(err)
  492. return myerrors.DbError("开始任务失败")
  493. }
  494. // 2. 创建过程记录
  495. recordData := g.Map{
  496. s.RecordDao.Columns.TaskId: req.Id,
  497. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  498. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  499. s.RecordDao.Columns.HandleContent: "开始处理任务<br/>任务标题: " + entity.TaskTitle,
  500. }
  501. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  502. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  503. if err != nil {
  504. g.Log().Error(err)
  505. return myerrors.DbError("新增过程记录失败")
  506. }
  507. return nil
  508. })
  509. }
  510. // Complete 完成任务
  511. func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq) (err error) {
  512. g.Log().Infof("Handler received req: %+v", req)
  513. // 校验数据是否存在
  514. var entity opsdevmodel.OpsEventTask
  515. err = s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  516. if err != nil {
  517. g.Log().Error(err)
  518. return myerrors.DbError("查询任务数据失败")
  519. }
  520. if entity.Id <= 0 {
  521. return myerrors.TipsError("任务数据不存在")
  522. }
  523. // 只有处理中(20)状态可以完成
  524. if !s.canComplete(entity.TaskStatus) {
  525. return myerrors.TipsError("只有处理中的任务可以完成")
  526. }
  527. for i := 0; i < maxTaskNoRetries; i++ {
  528. err = s.doComplete(req, &entity)
  529. if err == nil {
  530. return nil
  531. }
  532. if !isDuplicateEntryError(err) {
  533. return err
  534. }
  535. }
  536. return myerrors.DbError("任务编号生成冲突,请稍后重试")
  537. }
  538. // doComplete 执行完成任务(可在下游任务编号冲突时重试)
  539. func (s *OpsEventTaskService) doComplete(req *opsdevmodel.OpsEventTaskCompleteReq, entity *opsdevmodel.OpsEventTask) error {
  540. // 解析完成日期
  541. completeTime := gtime.Now()
  542. if req.CompleteDate != "" {
  543. if parsed := gtime.NewFromStrFormat(req.CompleteDate, "Y-m-d"); parsed != nil {
  544. completeTime = parsed
  545. }
  546. }
  547. // 构造更新数据
  548. data := g.Map{
  549. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCompleted, // 已完成
  550. s.TaskDao.Columns.CompleteTime: completeTime,
  551. s.TaskDao.Columns.ActualWorkHour: req.ActualWorkHour,
  552. s.TaskDao.Columns.Remark: req.Remark,
  553. }
  554. // 补齐审计字段
  555. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  556. // 预生成下游任务编号(避免事务内查询重复)
  557. var testTaskNo string
  558. if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
  559. testTaskNo = s.generateTaskNo()
  560. }
  561. if entity.TaskType == opsdevmodel.TaskTypeBug {
  562. testTaskNo = s.generateTaskNo()
  563. }
  564. // 使用事务
  565. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  566. // 1. 从工时登记表汇总真实的累计工时(数据权威来源,避免前端异常值覆盖)
  567. existingWorkHourTotal := entity.ActualWorkHour
  568. if totalVal, err := g.DB(s.Tenant).GetValue(
  569. "SELECT COALESCE(SUM(actual_work_hour), 0) FROM ops_event_task_work_hour WHERE task_id = ? AND deleted_time IS NULL",
  570. req.Id,
  571. ); err == nil {
  572. existingWorkHourTotal = totalVal.Float64()
  573. }
  574. // 计算实际应写入的工时:不允许用小于真实汇总值的数覆盖
  575. finalWorkHour := req.ActualWorkHour
  576. if finalWorkHour < existingWorkHourTotal {
  577. finalWorkHour = existingWorkHourTotal
  578. }
  579. // 更新 data 中的工时字段为修正后的值
  580. data[s.TaskDao.Columns.ActualWorkHour] = finalWorkHour
  581. // 2. 更新任务状态与工时
  582. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  583. if err != nil {
  584. g.Log().Error(err)
  585. return myerrors.DbError("完成任务失败")
  586. }
  587. // 3. 仅在前端额外追加了工时(高于工时表汇总)时,创建差额工时记录
  588. delta := finalWorkHour - existingWorkHourTotal
  589. if delta > 0 {
  590. workHourData := g.Map{
  591. s.WorkHourDao.Columns.TaskId: req.Id,
  592. s.WorkHourDao.Columns.OpsUserId: s.GetCxtUserId(),
  593. s.WorkHourDao.Columns.OpsUserName: s.GetCxtUserName(),
  594. s.WorkHourDao.Columns.ActualWorkDate: completeTime.Format("Y-m-d"),
  595. s.WorkHourDao.Columns.ActualWorkHour: delta,
  596. s.WorkHourDao.Columns.Remark: "完成任务自动登记工时差额",
  597. }
  598. service.SetCreatedInfo(workHourData, s.GetCxtUserId(), s.GetCxtUserName())
  599. _, err = s.WorkHourDao.TX(tx).Data(workHourData).Insert()
  600. if err != nil {
  601. g.Log().Error(err)
  602. return myerrors.DbError("登记工时差额失败")
  603. }
  604. }
  605. // 4. 创建过程记录
  606. var handleContent string
  607. if entity.TaskType == opsdevmodel.TaskTypeFeatureTest && req.TestResult != "" {
  608. testResultText := "通过"
  609. if req.TestResult == "fail" {
  610. testResultText = "不通过"
  611. }
  612. handleContent = fmt.Sprintf("测试结果:%s <br/>测试时间:%s", testResultText, gtime.Now().Format("Y-m-d"))
  613. } else if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete {
  614. // 系统发版完成记录
  615. handleContent = "发版完成<br/>任务标题: " + entity.TaskTitle + "<br/>发版工时: " + gconv.String(req.ActualWorkHour) + "小时"
  616. if req.Remark != "" {
  617. handleContent += "<br/>发版说明: " + req.Remark
  618. }
  619. if len(req.DevTaskIds) > 0 {
  620. handleContent += "<br/>关联研发任务数: " + gconv.String(len(req.DevTaskIds)) + "个"
  621. }
  622. } else {
  623. // 普通任务完成记录
  624. handleContent = "完成任务<br/>任务标题: " + entity.TaskTitle + "<br/>实际工作量: " + gconv.String(req.ActualWorkHour) + "小时"
  625. }
  626. recordData := g.Map{
  627. s.RecordDao.Columns.TaskId: req.Id,
  628. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  629. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  630. s.RecordDao.Columns.HandleContent: handleContent,
  631. }
  632. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  633. result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  634. if err != nil {
  635. g.Log().Error(err)
  636. return myerrors.DbError("新增过程记录失败")
  637. }
  638. recordId, _ := result.LastInsertId()
  639. // 5. 保存附件
  640. if len(req.Attachments) > 0 {
  641. for _, att := range req.Attachments {
  642. attData := g.Map{
  643. s.AttachmentDao.Columns.TaskId: req.Id,
  644. s.AttachmentDao.Columns.TaskRecordId: recordId,
  645. s.AttachmentDao.Columns.FileName: att.FileName,
  646. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  647. s.AttachmentDao.Columns.FileType: att.FileType,
  648. }
  649. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  650. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  651. if err != nil {
  652. g.Log().Error(err)
  653. return myerrors.DbError("保存附件信息失败")
  654. }
  655. }
  656. }
  657. // 6. 根据任务类型自动创建下游任务
  658. // 5.1 功能开发完成时,自动创建功能测试任务(原4.2)
  659. if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
  660. testTaskData := g.Map{
  661. s.TaskDao.Columns.TaskNo: testTaskNo,
  662. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  663. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  664. s.TaskDao.Columns.EventId: entity.EventId,
  665. s.TaskDao.Columns.EventType: entity.EventType,
  666. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  667. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  668. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  669. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
  670. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  671. s.TaskDao.Columns.Priority: entity.Priority,
  672. s.TaskDao.Columns.TaskParentId: req.Id,
  673. }
  674. service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  675. testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
  676. if err != nil {
  677. g.Log().Error(err)
  678. if isDuplicateEntryError(err) {
  679. return err
  680. }
  681. return myerrors.DbError("自动创建功能测试任务失败")
  682. }
  683. testTaskId, _ := testResult.LastInsertId()
  684. // 创建功能测试任务的过程记录
  685. testRecordData := g.Map{
  686. s.RecordDao.Columns.TaskId: testTaskId,
  687. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  688. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  689. s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由功能开发任务自动创建",
  690. }
  691. service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  692. _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
  693. if err != nil {
  694. g.Log().Error(err)
  695. return myerrors.DbError("新增功能测试任务过程记录失败")
  696. }
  697. }
  698. // 5.2b BUG任务完成时,自动创建功能测试任务
  699. if entity.TaskType == opsdevmodel.TaskTypeBug {
  700. testTaskData := g.Map{
  701. s.TaskDao.Columns.TaskNo: testTaskNo,
  702. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  703. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  704. s.TaskDao.Columns.EventId: entity.EventId,
  705. s.TaskDao.Columns.EventType: entity.EventType,
  706. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  707. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  708. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  709. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
  710. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  711. s.TaskDao.Columns.Priority: entity.Priority,
  712. s.TaskDao.Columns.TaskParentId: req.Id,
  713. }
  714. if entity.TaskParentId > 0 {
  715. var parentTask opsdevmodel.OpsEventTask
  716. err := s.TaskDao.TX(tx).FieldsEx(s.TaskDao.Columns.DeletedTime).
  717. WherePri(s.TaskDao.Columns.Id, entity.TaskParentId).
  718. Scan(&parentTask)
  719. if err == nil && parentTask.Id > 0 {
  720. testTaskData[s.TaskDao.Columns.OpsUserId] = parentTask.OpsUserId
  721. testTaskData[s.TaskDao.Columns.OpsUserName] = parentTask.OpsUserName
  722. }
  723. }
  724. service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  725. testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
  726. if err != nil {
  727. g.Log().Error(err)
  728. if isDuplicateEntryError(err) {
  729. return err
  730. }
  731. return myerrors.DbError("自动创建功能测试任务失败")
  732. }
  733. testTaskId, _ := testResult.LastInsertId()
  734. // 创建功能测试任务的过程记录
  735. testRecordData := g.Map{
  736. s.RecordDao.Columns.TaskId: testTaskId,
  737. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  738. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  739. s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由BUG任务自动创建",
  740. }
  741. service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  742. _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
  743. if err != nil {
  744. g.Log().Error(err)
  745. return myerrors.DbError("新增功能测试任务过程记录失败")
  746. }
  747. }
  748. // 5.3 系统发版完成时,保存关联的研发任务到ops_event_task_release表
  749. if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete && len(req.DevTaskIds) > 0 {
  750. for _, devTaskId := range req.DevTaskIds {
  751. releaseData := g.Map{
  752. s.ReleaseDao.Columns.ReleaseTaskId: req.Id,
  753. s.ReleaseDao.Columns.DevTaskId: devTaskId,
  754. s.ReleaseDao.Columns.ProjectId: entity.ProjectId,
  755. }
  756. service.SetCreatedInfo(releaseData, s.GetCxtUserId(), s.GetCxtUserName())
  757. _, err := s.ReleaseDao.TX(tx).Data(releaseData).Insert()
  758. if err != nil {
  759. g.Log().Error(err)
  760. return myerrors.DbError("保存发版关联任务失败")
  761. }
  762. }
  763. }
  764. // 7. 任务完成时自动更新关联的交付事件状态为完成并生成过程记录
  765. if entity.EventId > 0 && entity.EventType == opsdevmodel.EventTypeDelivery {
  766. // 触发事件自动完成的任务类型:10(需求评审)、30(功能测试-通过)、38(系统发版)、40(系统发版/硬件发货)、41(硬件安装)
  767. if s.shouldAutoCompleteEvent(entity.TaskType, req.TestResult) {
  768. eventData := g.Map{
  769. s.EventDao.Columns.DeliveryEventStatus: opsdevmodel.DeliveryEventStatusClosed,
  770. s.EventDao.Columns.CompleteTime: gtime.Now(),
  771. }
  772. service.SetUpdatedInfo(eventData, s.GetCxtUserId(), s.GetCxtUserName())
  773. _, err := s.EventDao.TX(tx).FieldsEx(service.UpdateFieldEx...).
  774. Data(eventData).
  775. WherePri(s.EventDao.Columns.Id, entity.EventId).
  776. Update()
  777. if err != nil {
  778. g.Log().Error(err)
  779. return myerrors.DbError("自动更新事件状态失败")
  780. }
  781. eventRecordData := g.Map{
  782. s.EventRecordDao.Columns.DeliveryEventId: entity.EventId,
  783. s.EventRecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  784. s.EventRecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  785. s.EventRecordDao.Columns.HandleContent: "研发任务已完成,自动关闭事件<br/>任务编号: " + entity.TaskNo + "<br/>任务标题: " + entity.TaskTitle,
  786. }
  787. service.SetCreatedInfo(eventRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  788. _, err = s.EventRecordDao.TX(tx).Data(eventRecordData).Insert()
  789. if err != nil {
  790. g.Log().Error(err)
  791. return myerrors.DbError("新增事件过程记录失败")
  792. }
  793. }
  794. }
  795. return nil
  796. })
  797. }
  798. // Pause 暂停任务
  799. func (s *OpsEventTaskService) Pause(req *opsdevmodel.OpsEventTaskPauseReq) error {
  800. // 校验数据是否存在
  801. var entity opsdevmodel.OpsEventTask
  802. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  803. if err != nil {
  804. g.Log().Error(err)
  805. return myerrors.DbError("查询任务数据失败")
  806. }
  807. if entity.Id <= 0 {
  808. return myerrors.TipsError("任务数据不存在")
  809. }
  810. // 只有处理中(20)状态可以暂停
  811. if !s.canPause(entity.TaskStatus) {
  812. return myerrors.TipsError("只有处理中的任务可以暂停")
  813. }
  814. // 构造更新数据
  815. data := g.Map{
  816. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusPaused, // 暂停
  817. }
  818. // 补齐审计字段
  819. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  820. // 使用事务
  821. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  822. // 1. 更新任务状态
  823. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  824. if err != nil {
  825. g.Log().Error(err)
  826. return myerrors.DbError("暂停任务失败")
  827. }
  828. // 2. 创建过程记录
  829. recordData := g.Map{
  830. s.RecordDao.Columns.TaskId: req.Id,
  831. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  832. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  833. s.RecordDao.Columns.HandleContent: "暂停任务<br/>暂停原因: " + req.Remark,
  834. }
  835. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  836. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  837. if err != nil {
  838. g.Log().Error(err)
  839. return myerrors.DbError("新增过程记录失败")
  840. }
  841. return nil
  842. })
  843. }
  844. // Block 阻塞任务
  845. func (s *OpsEventTaskService) Block(req *opsdevmodel.OpsEventTaskBlockReq) error {
  846. // 校验数据是否存在
  847. var entity opsdevmodel.OpsEventTask
  848. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  849. if err != nil {
  850. g.Log().Error(err)
  851. return myerrors.DbError("查询任务数据失败")
  852. }
  853. if entity.Id <= 0 {
  854. return myerrors.TipsError("任务数据不存在")
  855. }
  856. // 只有处理中(20)或暂停(25)状态可以阻塞
  857. if !s.canBlock(entity.TaskStatus) {
  858. return myerrors.TipsError("只有处理中或暂停状态的任务可以阻塞")
  859. }
  860. // 构造更新数据
  861. data := g.Map{
  862. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusBlocked, // 阻塞
  863. }
  864. // 补齐审计字段
  865. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  866. // 使用事务
  867. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  868. // 1. 更新任务状态
  869. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  870. if err != nil {
  871. g.Log().Error(err)
  872. return myerrors.DbError("阻塞任务失败")
  873. }
  874. // 2. 创建过程记录
  875. recordData := g.Map{
  876. s.RecordDao.Columns.TaskId: req.Id,
  877. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  878. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  879. s.RecordDao.Columns.HandleContent: "阻塞任务<br/>阻塞原因: " + req.Remark,
  880. }
  881. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  882. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  883. if err != nil {
  884. g.Log().Error(err)
  885. return myerrors.DbError("新增过程记录失败")
  886. }
  887. return nil
  888. })
  889. }
  890. // Cancel 作废任务
  891. func (s *OpsEventTaskService) Cancel(req *opsdevmodel.OpsEventTaskCancelReq) error {
  892. // 校验数据是否存在
  893. var entity opsdevmodel.OpsEventTask
  894. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  895. if err != nil {
  896. g.Log().Error(err)
  897. return myerrors.DbError("查询任务数据失败")
  898. }
  899. if entity.Id <= 0 {
  900. return myerrors.TipsError("任务数据不存在")
  901. }
  902. // 已完成(30)的任务不能作废
  903. if !s.canCancel(entity.TaskStatus) {
  904. return myerrors.TipsError("已完成的任务不能作废")
  905. }
  906. // 构造更新数据
  907. data := g.Map{
  908. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCancelled, // 作废
  909. }
  910. // 补齐审计字段
  911. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  912. // 使用事务
  913. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  914. // 1. 更新任务状态
  915. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  916. if err != nil {
  917. g.Log().Error(err)
  918. return myerrors.DbError("作废任务失败")
  919. }
  920. // 2. 创建过程记录
  921. recordData := g.Map{
  922. s.RecordDao.Columns.TaskId: req.Id,
  923. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  924. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  925. s.RecordDao.Columns.HandleContent: "作废任务<br/>作废原因: " + req.Remark,
  926. }
  927. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  928. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  929. if err != nil {
  930. g.Log().Error(err)
  931. return myerrors.DbError("新增过程记录失败")
  932. }
  933. return nil
  934. })
  935. }
  936. // GetRecords 获取任务过程记录列表(包含附件)
  937. func (s *OpsEventTaskService) GetRecords(req *opsdevmodel.OpsEventTaskRecordSearchReq) ([]*opsdevmodel.OpsEventTaskRecordWithAttachments, error) {
  938. var records []*opsdevmodel.OpsEventTaskRecord
  939. err := s.RecordDao.Where(s.RecordDao.Columns.TaskId, req.TaskId).
  940. Order(s.RecordDao.Columns.CreatedTime + " desc").
  941. Scan(&records)
  942. if err != nil {
  943. g.Log().Error(err)
  944. return nil, myerrors.DbError("查询过程记录失败")
  945. }
  946. result := make([]*opsdevmodel.OpsEventTaskRecordWithAttachments, 0, len(records))
  947. for _, record := range records {
  948. recordRsp := &opsdevmodel.OpsEventTaskRecordWithAttachments{
  949. OpsEventTaskRecord: *record,
  950. Attachments: []*opsdevmodel.OpsEventTaskAttachment{},
  951. }
  952. // 查询该记录关联的附件
  953. if record.Id > 0 {
  954. var attachments []*opsdevmodel.OpsEventTaskAttachment
  955. err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskRecordId, record.Id).
  956. Scan(&attachments)
  957. if err != nil {
  958. g.Log().Error(err)
  959. } else {
  960. recordRsp.Attachments = attachments
  961. }
  962. }
  963. result = append(result, recordRsp)
  964. }
  965. return result, nil
  966. }
  967. // generateTaskNo 生成任务编号
  968. func (s *OpsEventTaskService) generateTaskNo() string {
  969. // 格式: TSK + 年月日 + 4位序列号
  970. now := gtime.Now()
  971. prefix := "TSK" + now.Format("Ymd")
  972. // 使用数据库序列生成唯一序号(按天重置)
  973. seqVal, err := s.TaskDao.DB.GetValue("SELECT next_day_reset_val('task_no_seq')")
  974. if err != nil {
  975. // 如果序列不存在或出错,使用备用方案:查询当天最大序号+1
  976. var maxNoResult struct {
  977. TaskNo string
  978. }
  979. err = s.TaskDao.Where(s.TaskDao.Columns.TaskNo+" like ?", prefix+"%").Order(s.TaskDao.Columns.TaskNo + " desc").Fields(s.TaskDao.Columns.TaskNo).Scan(&maxNoResult)
  980. if err != nil || maxNoResult.TaskNo == "" {
  981. return prefix + "0001"
  982. }
  983. maxNoStr := maxNoResult.TaskNo
  984. if len(maxNoStr) >= len(prefix)+4 {
  985. seq := maxNoStr[len(prefix):]
  986. seqNum := gconv.Int(seq)
  987. seqNum++
  988. return prefix + fmt.Sprintf("%04d", seqNum)
  989. }
  990. return prefix + "0001"
  991. }
  992. return prefix + fmt.Sprintf("%04d", seqVal.Int())
  993. }
  994. // 状态校验辅助方法
  995. func (s *OpsEventTaskService) canEdit(status string) bool {
  996. return status != opsdevmodel.TaskStatusCompleted && status != opsdevmodel.TaskStatusCancelled
  997. }
  998. func (s *OpsEventTaskService) canSchedule(status string) bool {
  999. return status == opsdevmodel.TaskStatusTodo
  1000. }
  1001. func (s *OpsEventTaskService) canStart(status string) bool {
  1002. return status == opsdevmodel.TaskStatusTodo || status == opsdevmodel.TaskStatusPaused || status == opsdevmodel.TaskStatusBlocked
  1003. }
  1004. func (s *OpsEventTaskService) canComplete(status string) bool {
  1005. return status == opsdevmodel.TaskStatusProcessing
  1006. }
  1007. func (s *OpsEventTaskService) canPause(status string) bool {
  1008. return status == opsdevmodel.TaskStatusProcessing
  1009. }
  1010. func (s *OpsEventTaskService) canBlock(status string) bool {
  1011. return status == opsdevmodel.TaskStatusProcessing || status == opsdevmodel.TaskStatusPaused
  1012. }
  1013. // shouldAutoCompleteEvent 判断当前任务完成时是否应自动完成关联的交付事件
  1014. func (s *OpsEventTaskService) shouldAutoCompleteEvent(taskType, testResult string) bool {
  1015. switch taskType {
  1016. case "10": // 需求评审 — 完成即触发
  1017. return true
  1018. case "30": // 功能测试 — 仅通过时触发
  1019. return testResult == "pass"
  1020. case "38": // 系统发版(事件关联) — 完成即触发
  1021. return true
  1022. default:
  1023. return false
  1024. }
  1025. }
  1026. func (s *OpsEventTaskService) canCancel(status string) bool {
  1027. return status != opsdevmodel.TaskStatusCompleted
  1028. }
  1029. // getProjectName 根据项目ID获取项目名称
  1030. func (s *OpsEventTaskService) getProjectName(projectId int) string {
  1031. if projectId <= 0 {
  1032. return ""
  1033. }
  1034. var project opsdevmodel.OpsDeliveryProject
  1035. err := s.ProjectDao.FieldsEx(s.ProjectDao.Columns.DeletedTime).
  1036. WherePri(s.ProjectDao.Columns.Id, projectId).
  1037. Scan(&project)
  1038. if err != nil {
  1039. g.Log().Error(err)
  1040. return ""
  1041. }
  1042. return project.ProjectName
  1043. }
  1044. // GetAttachments 获取任务附件列表
  1045. func (s *OpsEventTaskService) GetAttachments(taskId int) ([]*opsdevmodel.OpsEventTaskAttachment, error) {
  1046. var attachments []*opsdevmodel.OpsEventTaskAttachment
  1047. err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskId, taskId).
  1048. Order(s.AttachmentDao.Columns.CreatedTime + " desc").
  1049. Scan(&attachments)
  1050. if err != nil {
  1051. g.Log().Error(err)
  1052. return nil, myerrors.DbError("查询附件失败")
  1053. }
  1054. return attachments, nil
  1055. }
  1056. // GetTaskReleaseList 根据发版任务ID查询关联的开发任务列表
  1057. func (s *OpsEventTaskService) GetTaskReleaseList(req *opsdevmodel.OpsEventTaskReleaseListReq) ([]*opsdevmodel.OpsEventTaskReleaseRsp, error) {
  1058. // 1. 查询关联表获取关联的任务ID列表
  1059. var releaseList []*opsdevmodel.OpsEventTaskRelease
  1060. err := s.ReleaseDao.Where(s.ReleaseDao.Columns.ReleaseTaskId, req.ReleaseTaskId).
  1061. Scan(&releaseList)
  1062. if err != nil {
  1063. g.Log().Error(err)
  1064. return nil, myerrors.DbError("查询发布版本关联记录失败")
  1065. }
  1066. if len(releaseList) == 0 {
  1067. return []*opsdevmodel.OpsEventTaskReleaseRsp{}, nil
  1068. }
  1069. // 2. 提取开发任务ID列表
  1070. devTaskIds := make([]int, 0, len(releaseList))
  1071. for _, r := range releaseList {
  1072. devTaskIds = append(devTaskIds, r.DevTaskId)
  1073. }
  1074. // 3. 查询任务详情
  1075. var taskList []*opsdevmodel.OpsEventTask
  1076. err = s.TaskDao.WhereIn(s.TaskDao.Columns.Id, devTaskIds).
  1077. Scan(&taskList)
  1078. if err != nil {
  1079. g.Log().Error(err)
  1080. return nil, myerrors.DbError("查询关联任务详情失败")
  1081. }
  1082. // 4. 构建响应数据
  1083. result := make([]*opsdevmodel.OpsEventTaskReleaseRsp, 0, len(taskList))
  1084. for _, task := range taskList {
  1085. result = append(result, &opsdevmodel.OpsEventTaskReleaseRsp{
  1086. Id: task.Id,
  1087. DevTaskId: task.Id,
  1088. TaskNo: task.TaskNo,
  1089. TaskTitle: task.TaskTitle,
  1090. TaskType: task.TaskType,
  1091. TaskStatus: task.TaskStatus,
  1092. OpsUserName: task.OpsUserName,
  1093. ProjectId: task.ProjectId,
  1094. CreatedTime: task.CreatedTime.Format("Y-m-d H:i:s"),
  1095. })
  1096. }
  1097. return result, nil
  1098. }
  1099. // AddRecord 添加任务过程记录
  1100. func (s *OpsEventTaskService) AddRecord(req *opsdevmodel.OpsEventTaskRecordAddReq) error {
  1101. // 构造记录数据
  1102. recordData := g.Map{
  1103. s.RecordDao.Columns.TaskId: req.TaskId,
  1104. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  1105. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  1106. s.RecordDao.Columns.HandleContent: req.HandleContent,
  1107. }
  1108. // 补齐审计字段
  1109. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  1110. // 使用事务控制
  1111. return s.RecordDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  1112. // 1. 创建过程记录
  1113. result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  1114. if err != nil {
  1115. g.Log().Error(err)
  1116. return myerrors.DbError("添加过程记录失败")
  1117. }
  1118. // 获取新创建的记录ID
  1119. recordId, err := result.LastInsertId()
  1120. if err != nil {
  1121. g.Log().Error(err)
  1122. return myerrors.DbError("获取记录ID失败")
  1123. }
  1124. // 2. 如果有附件,保存附件
  1125. if len(req.Attachments) > 0 {
  1126. for _, att := range req.Attachments {
  1127. attData := g.Map{
  1128. s.AttachmentDao.Columns.TaskId: req.TaskId,
  1129. s.AttachmentDao.Columns.TaskRecordId: int(recordId),
  1130. s.AttachmentDao.Columns.FileName: att.FileName,
  1131. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  1132. s.AttachmentDao.Columns.FileType: att.FileType,
  1133. }
  1134. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  1135. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  1136. if err != nil {
  1137. g.Log().Error(err)
  1138. return myerrors.DbError("保存附件失败")
  1139. }
  1140. }
  1141. }
  1142. return nil
  1143. })
  1144. }
  1145. // AddWorkHour 添加工时登记(累加actual_work_hour并记录过程)
  1146. func (s *OpsEventTaskService) AddWorkHour(req *opsdevmodel.OpsEventTaskWorkHourAddReq) error {
  1147. var entity opsdevmodel.OpsEventTask
  1148. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.TaskId).Scan(&entity)
  1149. if err != nil {
  1150. g.Log().Error(err)
  1151. return myerrors.DbError("查询任务数据失败")
  1152. }
  1153. if entity.Id <= 0 {
  1154. return myerrors.TipsError("任务数据不存在")
  1155. }
  1156. if !s.canAddWorkHour(entity.TaskStatus) {
  1157. return myerrors.TipsError("只有处理中的任务可以登记工时")
  1158. }
  1159. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  1160. workHourData := g.Map{
  1161. s.WorkHourDao.Columns.TaskId: req.TaskId,
  1162. s.WorkHourDao.Columns.OpsUserId: s.GetCxtUserId(),
  1163. s.WorkHourDao.Columns.OpsUserName: s.GetCxtUserName(),
  1164. s.WorkHourDao.Columns.ActualWorkDate: req.WorkDate,
  1165. s.WorkHourDao.Columns.ActualWorkHour: req.ActualHour,
  1166. s.WorkHourDao.Columns.Remark: req.Remark,
  1167. }
  1168. service.SetCreatedInfo(workHourData, s.GetCxtUserId(), s.GetCxtUserName())
  1169. _, err := s.WorkHourDao.TX(tx).Data(workHourData).Insert()
  1170. if err != nil {
  1171. g.Log().Error(err)
  1172. return myerrors.DbError("工时登记失败")
  1173. }
  1174. newActualWorkHour := entity.ActualWorkHour + req.ActualHour
  1175. updateData := g.Map{
  1176. s.TaskDao.Columns.ActualWorkHour: newActualWorkHour,
  1177. }
  1178. service.SetUpdatedInfo(updateData, s.GetCxtUserId(), s.GetCxtUserName())
  1179. _, err = s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(updateData).WherePri(s.TaskDao.Columns.Id, req.TaskId).Update()
  1180. if err != nil {
  1181. g.Log().Error(err)
  1182. return myerrors.DbError("更新实际工时失败")
  1183. }
  1184. handleContent := fmt.Sprintf("工时登记<br/>工作日期: %s<br/>实际工时: %s小时<br/>工作进展: %s",
  1185. req.WorkDate, gconv.String(req.ActualHour), req.Remark)
  1186. recordData := g.Map{
  1187. s.RecordDao.Columns.TaskId: req.TaskId,
  1188. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  1189. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  1190. s.RecordDao.Columns.HandleContent: handleContent,
  1191. }
  1192. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  1193. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  1194. if err != nil {
  1195. g.Log().Error(err)
  1196. return myerrors.DbError("新增过程记录失败")
  1197. }
  1198. return nil
  1199. })
  1200. }
  1201. // GetWorkHourList 获取工时登记列表
  1202. func (s *OpsEventTaskService) GetWorkHourList(req *opsdevmodel.OpsEventTaskWorkHourListReq) ([]*opsdevmodel.OpsEventTaskWorkHourRsp, error) {
  1203. var entities []*opsdevmodel.OpsEventTaskWorkHour
  1204. err := s.WorkHourDao.FieldsEx(s.WorkHourDao.Columns.DeletedTime).
  1205. Where(s.WorkHourDao.Columns.TaskId, req.TaskId).
  1206. Order(s.WorkHourDao.Columns.CreatedTime + " desc").
  1207. Scan(&entities)
  1208. if err != nil {
  1209. g.Log().Error(err)
  1210. return nil, myerrors.DbError("查询工时登记列表失败")
  1211. }
  1212. list := make([]*opsdevmodel.OpsEventTaskWorkHourRsp, 0, len(entities))
  1213. for _, entity := range entities {
  1214. workDate := ""
  1215. if entity.ActualWorkDate != nil {
  1216. workDate = entity.ActualWorkDate.Format("Y-m-d")
  1217. }
  1218. createdTime := ""
  1219. if entity.CreatedTime != nil {
  1220. createdTime = entity.CreatedTime.Format("Y-m-d H:i:s")
  1221. }
  1222. list = append(list, &opsdevmodel.OpsEventTaskWorkHourRsp{
  1223. Id: entity.Id,
  1224. TaskId: entity.TaskId,
  1225. WorkDate: workDate,
  1226. ActualHour: entity.ActualWorkHour,
  1227. Remark: entity.Remark,
  1228. CreatedName: entity.CreatedName,
  1229. CreatedTime: createdTime,
  1230. })
  1231. }
  1232. return list, nil
  1233. }
  1234. // GetDashboardData 获取工作台看板数据(周视图)
  1235. func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsdevmodel.OpsEventTaskWorkHourDashboardRsp, error) {
  1236. userId := s.GetCxtUserId()
  1237. db := g.DB(s.Tenant)
  1238. type taskRow struct {
  1239. Id int `json:"id"`
  1240. TaskNo string `json:"taskNo"`
  1241. TaskTitle string `json:"taskTitle"`
  1242. TaskType string `json:"taskType"`
  1243. TaskStatus string `json:"taskStatus"`
  1244. Priority string `json:"priority"`
  1245. ProjectName string `json:"projectName"`
  1246. ActualWorkHour float64 `json:"actualWorkHour"`
  1247. EstimateWorkHour float64 `json:"estimateWorkHour"`
  1248. PlanStartDate string `json:"planStartDate"`
  1249. PlanEndTime string `json:"planEndTime"`
  1250. }
  1251. taskSQL := `
  1252. SELECT id, task_no, task_title, task_type, task_status,
  1253. priority, project_name, actual_work_hour, estimate_work_hour,
  1254. DATE(plan_start_time) AS plan_start_date,
  1255. IFNULL(DATE(plan_end_time), '') AS plan_end_time
  1256. FROM ops_event_task
  1257. WHERE ops_user_id = ?
  1258. AND deleted_time IS NULL
  1259. AND (
  1260. -- 有结束日期的任务:时间区间与查询周有交集
  1261. (plan_end_time IS NOT NULL AND plan_end_time != ''
  1262. AND plan_start_time <= ? AND plan_end_time >= ?)
  1263. OR
  1264. -- 无结束日期的任务:仅按开始日期筛选(原逻辑)
  1265. ((plan_end_time IS NULL OR plan_end_time = '')
  1266. AND plan_start_time >= ? AND plan_start_time <= ?)
  1267. )
  1268. ORDER BY plan_start_time, priority
  1269. `
  1270. var taskRows []taskRow
  1271. err := db.GetScan(&taskRows, taskSQL, userId,
  1272. endDate+" 23:59:59", startDate,
  1273. startDate, endDate+" 23:59:59",
  1274. )
  1275. if err != nil {
  1276. g.Log().Error(err)
  1277. return nil, myerrors.DbError("查询任务数据失败")
  1278. }
  1279. type hourRow struct {
  1280. WorkDate string `json:"workDate"`
  1281. TotalHour float64 `json:"totalHour"`
  1282. }
  1283. hourSQL := `
  1284. SELECT DATE(actual_work_date) AS work_date, SUM(actual_work_hour) AS total_hour
  1285. FROM ops_event_task_work_hour
  1286. WHERE ops_user_id = ?
  1287. AND actual_work_date >= ? AND actual_work_date <= ?
  1288. AND deleted_time IS NULL
  1289. GROUP BY DATE(actual_work_date)
  1290. `
  1291. var hourRows []hourRow
  1292. err = db.GetScan(&hourRows, hourSQL, userId, startDate, endDate+" 23:59:59")
  1293. if err != nil {
  1294. g.Log().Error(err)
  1295. return nil, myerrors.DbError("查询每日工时失败")
  1296. }
  1297. hourMap := make(map[string]float64, 7)
  1298. for i := range hourRows {
  1299. hourMap[hourRows[i].WorkDate] = hourRows[i].TotalHour
  1300. }
  1301. // 合并会议工时
  1302. meetingSQL := `
  1303. SELECT DATE(work_date) AS work_date, SUM(work_hour) AS total_hour
  1304. FROM plat_meeting_work_hour
  1305. WHERE user_id = ?
  1306. AND work_date >= ? AND work_date <= ?
  1307. AND deleted_time IS NULL
  1308. GROUP BY DATE(work_date)
  1309. `
  1310. var meetingHourRows []hourRow
  1311. if err := db.GetScan(&meetingHourRows, meetingSQL, userId, startDate, endDate+" 23:59:59"); err != nil {
  1312. g.Log().Error(err)
  1313. } else {
  1314. for i := range meetingHourRows {
  1315. hourMap[meetingHourRows[i].WorkDate] += meetingHourRows[i].TotalHour
  1316. }
  1317. }
  1318. taskMap := make(map[string][]*opsdevmodel.DashboardTaskRsp, 7)
  1319. for i := range taskRows {
  1320. row := &taskRows[i]
  1321. taskItem := &opsdevmodel.DashboardTaskRsp{
  1322. Id: row.Id,
  1323. TaskNo: row.TaskNo,
  1324. TaskTitle: row.TaskTitle,
  1325. TaskType: row.TaskType,
  1326. TaskStatus: row.TaskStatus,
  1327. Priority: row.Priority,
  1328. ProjectName: row.ProjectName,
  1329. ActualWorkHour: row.ActualWorkHour,
  1330. EstimateWorkHour: row.EstimateWorkHour,
  1331. PlanEndTime: row.PlanEndTime,
  1332. }
  1333. taskEndDate := row.PlanEndTime
  1334. if taskEndDate == "" {
  1335. taskMap[row.PlanStartDate] = append(taskMap[row.PlanStartDate], taskItem)
  1336. continue
  1337. }
  1338. start := gtime.New(row.PlanStartDate)
  1339. end := gtime.New(taskEndDate)
  1340. daysCount := int(end.Sub(start).Hours()/24) + 1
  1341. for d := 0; d < daysCount; d++ {
  1342. current := start.AddDate(0, 0, d)
  1343. dateStr := current.Format("Y-m-d")
  1344. if dateStr >= startDate && dateStr <= endDate {
  1345. taskMap[dateStr] = append(taskMap[dateStr], taskItem)
  1346. }
  1347. }
  1348. }
  1349. overdueCount, err := db.GetValue(
  1350. "SELECT COUNT(1) FROM ops_event_task WHERE plan_end_time < NOW() AND ops_user_id = ? AND task_status NOT IN (?,?) AND deleted_time IS NULL",
  1351. userId,
  1352. opsdevmodel.TaskStatusCompleted, opsdevmodel.TaskStatusCancelled,
  1353. )
  1354. if err != nil {
  1355. g.Log().Error(err)
  1356. return nil, myerrors.DbError("查询超期任务数失败")
  1357. }
  1358. var weekTotal float64
  1359. days := make([]*opsdevmodel.DashboardDayRsp, 0, 7)
  1360. for i := 0; i < 7; i++ {
  1361. date := startDate
  1362. if i > 0 {
  1363. d := gtime.New(startDate).AddDate(0, 0, i)
  1364. date = d.Format("Y-m-d")
  1365. }
  1366. targetHours := 8.0
  1367. if i >= 5 {
  1368. targetHours = 0
  1369. }
  1370. totalHours := hourMap[date]
  1371. weekTotal += totalHours
  1372. dayTasks := taskMap[date]
  1373. if dayTasks == nil {
  1374. dayTasks = []*opsdevmodel.DashboardTaskRsp{}
  1375. }
  1376. days = append(days, &opsdevmodel.DashboardDayRsp{
  1377. Date: date,
  1378. TotalHours: totalHours,
  1379. TargetHours: targetHours,
  1380. Tasks: dayTasks,
  1381. })
  1382. }
  1383. weekTargetHours := 40.0
  1384. return &opsdevmodel.OpsEventTaskWorkHourDashboardRsp{
  1385. WeekTargetHours: weekTargetHours,
  1386. WeekTotalHours: weekTotal,
  1387. OverdueCount: overdueCount.Int(),
  1388. Days: days,
  1389. }, nil
  1390. }
  1391. // canAddWorkHour 检查是否可以登记工时(仅处理中状态)
  1392. func (s *OpsEventTaskService) canAddWorkHour(status string) bool {
  1393. return status == opsdevmodel.TaskStatusProcessing
  1394. }
  1395. // scheduleGroupMap 排期统计人员分组映射
  1396. var scheduleGroupMap = map[string]string{
  1397. "徐洲": "Biobank组",
  1398. "贾冀川": "Biobank组",
  1399. "徐凯": "Biobank组",
  1400. "耿嘉强": "Biobank组",
  1401. "范相豪": "Biobank组",
  1402. "刘旗": "Biobank组",
  1403. "周丰林": "LIMS组",
  1404. "刘振林": "LIMS组",
  1405. "张旭伟": "LIMS组",
  1406. "张涵": "LIMS组",
  1407. "王晓宇": "CellSop组",
  1408. "李凯": "CellSop组",
  1409. "王硕": "CellSop组",
  1410. "卢传敏": "BiobankV4",
  1411. "韩明儒": "BiobankV4",
  1412. "石春蕾": "品质部",
  1413. "刘琦": "品质部",
  1414. }
  1415. // GetScheduleStats 获取人员排期统计(按周、按天、按人汇总任务数和预估工时)
  1416. func (s *OpsEventTaskService) GetScheduleStats(req *opsdevmodel.OpsEventTaskScheduleStatReq) (*opsdevmodel.OpsEventTaskScheduleStatRsp, error) {
  1417. type dayRow struct {
  1418. OpsUserId int `json:"opsUserId"`
  1419. OpsUserName string `json:"opsUserName"`
  1420. PlanDate string `json:"planDate"`
  1421. TaskCount int `json:"taskCount"`
  1422. EstimateWorkHour float64 `json:"estimateWorkHour"`
  1423. }
  1424. sql := `
  1425. SELECT
  1426. ops_user_id,
  1427. ops_user_name,
  1428. DATE(plan_start_time) AS plan_date,
  1429. COUNT(*) AS task_count,
  1430. SUM(COALESCE(estimate_work_hour, 0)) AS estimate_work_hour
  1431. FROM ops_event_task
  1432. WHERE deleted_time IS NULL
  1433. AND task_status != ?
  1434. AND plan_start_time >= ?
  1435. AND plan_start_time < ?
  1436. `
  1437. args := []interface{}{opsdevmodel.TaskStatusCancelled, req.WeekStart, req.WeekEnd + " 23:59:59"}
  1438. if req.ProjectId > 0 {
  1439. sql += " AND project_id = ?"
  1440. args = append(args, req.ProjectId)
  1441. }
  1442. allNames := make([]string, 0, len(scheduleGroupMap))
  1443. for name := range scheduleGroupMap {
  1444. allNames = append(allNames, name)
  1445. }
  1446. placeholders := make([]string, len(allNames))
  1447. for i := range placeholders {
  1448. placeholders[i] = "?"
  1449. }
  1450. sql += " AND ops_user_name IN (" + strings.Join(placeholders, ",") + ")"
  1451. for _, name := range allNames {
  1452. args = append(args, name)
  1453. }
  1454. sql += `
  1455. GROUP BY ops_user_id, ops_user_name, DATE(plan_start_time)
  1456. ORDER BY ops_user_name, plan_date
  1457. `
  1458. var rows []dayRow
  1459. err := g.DB(s.Tenant).GetScan(&rows, sql, args...)
  1460. if err != nil {
  1461. g.Log().Error(err)
  1462. return nil, myerrors.DbError("查询排期统计数据失败")
  1463. }
  1464. // 按用户聚合,补全 7 天数据
  1465. userMap := make(map[int]*opsdevmodel.OpsEventTaskUserScheduleStat)
  1466. for _, row := range rows {
  1467. user, ok := userMap[row.OpsUserId]
  1468. if !ok {
  1469. user = &opsdevmodel.OpsEventTaskUserScheduleStat{
  1470. OpsUserId: row.OpsUserId,
  1471. OpsUserName: row.OpsUserName,
  1472. GroupName: scheduleGroupMap[row.OpsUserName],
  1473. DayStats: make([]*opsdevmodel.OpsEventTaskUserDayStat, 7),
  1474. }
  1475. // 初始化 7 天空数据
  1476. weekStart := gtime.New(req.WeekStart)
  1477. for i := 0; i < 7; i++ {
  1478. day := weekStart.AddDate(0, 0, i)
  1479. user.DayStats[i] = &opsdevmodel.OpsEventTaskUserDayStat{
  1480. Date: day.Format("Y-m-d"),
  1481. TaskCount: 0,
  1482. EstimateWorkHour: 0,
  1483. }
  1484. }
  1485. userMap[row.OpsUserId] = user
  1486. }
  1487. // 查找对应 day index
  1488. for _, ds := range user.DayStats {
  1489. if ds.Date == row.PlanDate {
  1490. ds.TaskCount = row.TaskCount
  1491. ds.EstimateWorkHour = row.EstimateWorkHour
  1492. break
  1493. }
  1494. }
  1495. }
  1496. // 计算周合计
  1497. list := make([]*opsdevmodel.OpsEventTaskUserScheduleStat, 0, len(userMap))
  1498. for _, user := range userMap {
  1499. weekTotal := &opsdevmodel.OpsEventTaskUserDayStat{Date: "合计"}
  1500. for _, ds := range user.DayStats {
  1501. weekTotal.TaskCount += ds.TaskCount
  1502. weekTotal.EstimateWorkHour += ds.EstimateWorkHour
  1503. }
  1504. weekTotal.EstimateWorkHour = math.Round(weekTotal.EstimateWorkHour*10) / 10
  1505. user.WeekTotal = weekTotal
  1506. list = append(list, user)
  1507. }
  1508. return &opsdevmodel.OpsEventTaskScheduleStatRsp{List: list}, nil
  1509. }
  1510. func (s *OpsEventTaskService) Export(ctx context.Context, req *opsdevmodel.OpsEventTaskExportReq) (content *opsdevmodel.OpsEventTaskExportContent, err error) {
  1511. db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime, s.TaskDao.Columns.TaskDesc)
  1512. if req.ProjectId > 0 {
  1513. db = db.Where(s.TaskDao.Columns.ProjectId, req.ProjectId)
  1514. }
  1515. if len(req.ProjectIds) > 0 {
  1516. db = db.Where(s.TaskDao.Columns.ProjectId+" in (?)", req.ProjectIds)
  1517. }
  1518. if req.ProductLine != "" && req.ProjectId <= 0 && len(req.ProjectIds) == 0 {
  1519. db = db.Where(fmt.Sprintf(
  1520. "EXISTS (SELECT 1 FROM %s p WHERE p.id = %s AND p.product_line = ? AND p.deleted_time IS NULL)",
  1521. s.ProjectDao.Table, s.TaskDao.Columns.ProjectId,
  1522. ), req.ProductLine)
  1523. }
  1524. if req.EventId > 0 {
  1525. db = db.Where(s.TaskDao.Columns.EventId, req.EventId)
  1526. }
  1527. if req.TaskTitle != "" {
  1528. db = db.Where(s.TaskDao.Columns.TaskTitle+" like ?", "%"+req.TaskTitle+"%")
  1529. }
  1530. if len(req.TaskType) > 0 {
  1531. db = db.Where(s.TaskDao.Columns.TaskType+" in (?)", req.TaskType)
  1532. }
  1533. if len(req.TaskStatus) > 0 {
  1534. db = db.Where(s.TaskDao.Columns.TaskStatus+" in (?)", req.TaskStatus)
  1535. }
  1536. if len(req.Priority) > 0 {
  1537. db = db.Where(s.TaskDao.Columns.Priority+" in (?)", req.Priority)
  1538. }
  1539. if len(req.OpsUserName) > 0 {
  1540. db = db.Where(s.TaskDao.Columns.OpsUserName+" in (?)", req.OpsUserName)
  1541. }
  1542. if req.ScheduleStatus == "scheduled" {
  1543. db = db.Where(s.TaskDao.Columns.PlanStartTime + " is not null").Where(s.TaskDao.Columns.PlanEndTime + " is not null")
  1544. } else if req.ScheduleStatus == "unscheduled" {
  1545. db = db.Where(s.TaskDao.Columns.PlanStartTime + " is null or " + s.TaskDao.Columns.PlanEndTime + " is null")
  1546. }
  1547. if req.PlanStartDateStart != "" {
  1548. db = db.Where(s.TaskDao.Columns.PlanStartTime+" >= ?", req.PlanStartDateStart+" 00:00:00")
  1549. }
  1550. if req.PlanStartDateEnd != "" {
  1551. db = db.Where(s.TaskDao.Columns.PlanStartTime+" <= ?", req.PlanStartDateEnd+" 23:59:59")
  1552. }
  1553. if req.PlanEndDateStart != "" {
  1554. db = db.Where(s.TaskDao.Columns.PlanEndTime+" >= ?", req.PlanEndDateStart+" 00:00:00")
  1555. }
  1556. if req.PlanEndDateEnd != "" {
  1557. db = db.Where(s.TaskDao.Columns.PlanEndTime+" <= ?", req.PlanEndDateEnd+" 23:59:59")
  1558. }
  1559. if req.CreatedTimeStart != "" {
  1560. db = db.Where(s.TaskDao.Columns.CreatedTime+" >= ?", req.CreatedTimeStart+" 00:00:00")
  1561. }
  1562. if req.CreatedTimeEnd != "" {
  1563. db = db.Where(s.TaskDao.Columns.CreatedTime+" <= ?", req.CreatedTimeEnd+" 23:59:59")
  1564. }
  1565. if req.CompleteTimeStart != "" {
  1566. db = db.Where(s.TaskDao.Columns.CompleteTime+" >= ?", req.CompleteTimeStart+" 00:00:00")
  1567. }
  1568. if req.CompleteTimeEnd != "" {
  1569. db = db.Where(s.TaskDao.Columns.CompleteTime+" <= ?", req.CompleteTimeEnd+" 23:59:59")
  1570. }
  1571. var entityList []*opsdevmodel.OpsEventTask
  1572. err = db.Order(s.TaskDao.Columns.CreatedTime + " desc").Scan(&entityList)
  1573. if err != nil {
  1574. g.Log().Error(err)
  1575. return nil, myerrors.DbError("查询任务列表失败")
  1576. }
  1577. taskTypeMap := map[string]string{
  1578. "10": "需求评审",
  1579. "20": "功能开发",
  1580. "25": "缺陷修复",
  1581. "30": "功能测试",
  1582. "35": "BUG",
  1583. "38": "系统发版",
  1584. "40": "系统发版",
  1585. "41": "硬件安装",
  1586. "42": "硬件发货",
  1587. }
  1588. taskStatusMap := map[string]string{
  1589. "10": "待处理",
  1590. "20": "处理中",
  1591. "25": "暂停",
  1592. "30": "已完成",
  1593. "70": "阻塞",
  1594. "90": "作废",
  1595. }
  1596. defectTypeMap := map[string]string{
  1597. "10": "前端",
  1598. "20": "后端",
  1599. }
  1600. exportDataList := make([]map[string]interface{}, 0)
  1601. for _, task := range entityList {
  1602. planStartTime := ""
  1603. if task.PlanStartTime != nil && !task.PlanStartTime.IsZero() {
  1604. planStartTime = task.PlanStartTime.Format("Y-m-d")
  1605. }
  1606. planEndTime := ""
  1607. if task.PlanEndTime != nil && !task.PlanEndTime.IsZero() {
  1608. planEndTime = task.PlanEndTime.Format("Y-m-d")
  1609. }
  1610. completeTime := ""
  1611. if task.CompleteTime != nil && !task.CompleteTime.IsZero() {
  1612. completeTime = task.CompleteTime.Format("Y-m-d")
  1613. }
  1614. createdTime := ""
  1615. if task.CreatedTime != nil && !task.CreatedTime.IsZero() {
  1616. createdTime = task.CreatedTime.Format("Y-m-d H:i:s")
  1617. }
  1618. attribute2Label := "-"
  1619. if task.Attribute2 == "10" {
  1620. attribute2Label = "是"
  1621. } else if task.Attribute2 == "20" {
  1622. attribute2Label = "否"
  1623. }
  1624. exportData := map[string]interface{}{
  1625. "taskNo": task.TaskNo,
  1626. "taskTitle": task.TaskTitle,
  1627. "taskType": taskTypeMap[task.TaskType],
  1628. "taskStatus": taskStatusMap[task.TaskStatus],
  1629. "functionName": task.FunctionName,
  1630. "opsUserName": task.OpsUserName,
  1631. "planStartTime": planStartTime,
  1632. "planEndTime": planEndTime,
  1633. "completeTime": completeTime,
  1634. "estimateWorkHour": gconv.String(task.EstimateWorkHour),
  1635. "actualWorkHour": gconv.String(task.ActualWorkHour),
  1636. "defectType": defectTypeMap[task.DefectType],
  1637. "attribute2": attribute2Label,
  1638. "releaseVersion": task.ReleaseVersion,
  1639. "projectName": task.ProjectName,
  1640. "createdName": task.CreatedName,
  1641. "createdTime": createdTime,
  1642. }
  1643. exportDataList = append(exportDataList, exportData)
  1644. }
  1645. exportContent := new(opsdevmodel.OpsEventTaskExportContent)
  1646. contentBase64, err := service.CommonExportExcel(ctx, "软件交付任务", opsdevmodel.OpsEventTaskExportData{}, exportDataList)
  1647. if err != nil {
  1648. g.Log().Error(err)
  1649. return nil, myerrors.DbError("导出任务数据失败")
  1650. }
  1651. exportContent.Content = contentBase64
  1652. return exportContent, nil
  1653. }