ops_event_task.go 55 KB

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