ops_event_task.go 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380
  1. package opsdev
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "dashoo.cn/opms_libary/myerrors"
  7. opsdevdao "dashoo.cn/opms_parent/app/dao/opsdev"
  8. opsdevmodel "dashoo.cn/opms_parent/app/model/opsdev"
  9. "dashoo.cn/opms_parent/app/service"
  10. "github.com/gogf/gf/database/gdb"
  11. "github.com/gogf/gf/frame/g"
  12. "github.com/gogf/gf/os/gtime"
  13. "github.com/gogf/gf/util/gconv"
  14. )
  15. // OpsEventTaskService 任务业务逻辑实现类
  16. type OpsEventTaskService struct {
  17. *service.ContextService
  18. TaskDao *opsdevdao.OpsEventTaskDao
  19. RecordDao *opsdevdao.OpsEventTaskRecordDao
  20. AttachmentDao *opsdevdao.OpsEventTaskAttachmentDao
  21. ProjectDao *opsdevdao.OpsDeliveryProjectDao
  22. ReleaseDao *opsdevdao.OpsEventTaskReleaseDao
  23. WorkHourDao *opsdevdao.OpsEventTaskWorkHourDao
  24. EventDao *opsdevdao.OpsDeliveryProjectEventDao
  25. EventRecordDao *opsdevdao.OpsDeliveryProjectEventRecordDao
  26. }
  27. // NewOpsEventTaskService 初始化service
  28. func NewOpsEventTaskService(ctx context.Context) (svc *OpsEventTaskService, err error) {
  29. svc = new(OpsEventTaskService)
  30. if svc.ContextService, err = svc.Init(ctx); err != nil {
  31. return nil, err
  32. }
  33. svc.TaskDao = opsdevdao.NewOpsEventTaskDao(svc.Tenant)
  34. svc.RecordDao = opsdevdao.NewOpsEventTaskRecordDao(svc.Tenant)
  35. svc.AttachmentDao = opsdevdao.NewOpsEventTaskAttachmentDao(svc.Tenant)
  36. svc.ProjectDao = opsdevdao.NewOpsDeliveryProjectDao(svc.Tenant)
  37. svc.ReleaseDao = opsdevdao.NewOpsEventTaskReleaseDao(svc.Tenant)
  38. svc.WorkHourDao = opsdevdao.NewOpsEventTaskWorkHourDao(svc.Tenant)
  39. svc.EventDao = opsdevdao.NewOpsDeliveryProjectEventDao(svc.Tenant)
  40. svc.EventRecordDao = opsdevdao.NewOpsDeliveryProjectEventRecordDao(svc.Tenant)
  41. return svc, nil
  42. }
  43. // GetList 分页查询任务列表
  44. func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (total int, list []*opsdevmodel.OpsEventTaskRsp, err error) {
  45. db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime)
  46. // 项目ID筛选
  47. if req.ProjectId > 0 {
  48. db = db.Where(s.TaskDao.Columns.ProjectId, req.ProjectId)
  49. }
  50. // 关联事件ID筛选
  51. if req.EventId > 0 {
  52. db = db.Where(s.TaskDao.Columns.EventId, req.EventId)
  53. }
  54. // 任务标题模糊查询
  55. if req.TaskTitle != "" {
  56. db = db.Where(s.TaskDao.Columns.TaskTitle+" like ?", "%"+req.TaskTitle+"%")
  57. }
  58. // 任务类型筛选(多选)
  59. if len(req.TaskType) > 0 {
  60. db = db.Where(s.TaskDao.Columns.TaskType+" in (?)", req.TaskType)
  61. }
  62. // 任务状态筛选(多选)
  63. if len(req.TaskStatus) > 0 {
  64. db = db.Where(s.TaskDao.Columns.TaskStatus+" in (?)", req.TaskStatus)
  65. }
  66. // 优先级筛选(多选)
  67. if len(req.Priority) > 0 {
  68. db = db.Where(s.TaskDao.Columns.Priority+" in (?)", req.Priority)
  69. }
  70. // 执行人筛选(多选)
  71. if len(req.OpsUserName) > 0 {
  72. db = db.Where(s.TaskDao.Columns.OpsUserName+" in (?)", req.OpsUserName)
  73. }
  74. // 发布版本为空筛选(用于发版任务选择未发版任务)
  75. if req.ReleaseVersionEmpty {
  76. db = db.Where(s.TaskDao.Columns.ReleaseVersion + " is null")
  77. }
  78. // 计划结束日期范围筛选
  79. if req.PlanEndDateStart != "" {
  80. db = db.Where(s.TaskDao.Columns.PlanEndTime+" >= ?", req.PlanEndDateStart+" 00:00:00")
  81. }
  82. if req.PlanEndDateEnd != "" {
  83. db = db.Where(s.TaskDao.Columns.PlanEndTime+" <= ?", req.PlanEndDateEnd+" 23:59:59")
  84. }
  85. // 创建日期范围筛选
  86. if req.CreatedTimeStart != "" {
  87. db = db.Where(s.TaskDao.Columns.CreatedTime+" >= ?", req.CreatedTimeStart+" 00:00:00")
  88. }
  89. if req.CreatedTimeEnd != "" {
  90. db = db.Where(s.TaskDao.Columns.CreatedTime+" <= ?", req.CreatedTimeEnd+" 23:59:59")
  91. }
  92. // 完成日期范围筛选
  93. if req.CompleteTimeStart != "" {
  94. db = db.Where(s.TaskDao.Columns.CompleteTime+" >= ?", req.CompleteTimeStart+" 00:00:00")
  95. }
  96. if req.CompleteTimeEnd != "" {
  97. db = db.Where(s.TaskDao.Columns.CompleteTime+" <= ?", req.CompleteTimeEnd+" 23:59:59")
  98. }
  99. // 统计总数
  100. total, err = db.Count()
  101. if err != nil {
  102. g.Log().Error(err)
  103. return 0, nil, myerrors.DbError("获取任务总数失败")
  104. }
  105. // 分页查询
  106. pageNum, pageSize := req.GetPage()
  107. var entityList []*opsdevmodel.OpsEventTask
  108. // 处理排序
  109. if len(req.SortFields) > 0 {
  110. orderClauses := []string{}
  111. for _, sort := range req.SortFields {
  112. // 将前端字段名转换为数据库列名
  113. colName := s.getSortColumnName(sort.Field)
  114. if colName != "" {
  115. orderClauses = append(orderClauses, colName+" "+strings.ToUpper(sort.Order))
  116. }
  117. }
  118. if len(orderClauses) > 0 {
  119. db = db.Order(strings.Join(orderClauses, ", "))
  120. } else {
  121. db = db.Order(s.TaskDao.Columns.CreatedTime + " desc")
  122. }
  123. } else {
  124. db = db.Order(s.TaskDao.Columns.CreatedTime + " desc")
  125. }
  126. err = db.Page(pageNum, pageSize).Scan(&entityList)
  127. if err != nil {
  128. g.Log().Error(err)
  129. return 0, nil, myerrors.DbError("查询任务列表失败")
  130. }
  131. // 转换为响应结构体
  132. if err = gconv.Structs(entityList, &list); err != nil {
  133. g.Log().Error(err)
  134. return 0, nil, myerrors.DbError("数据转换失败")
  135. }
  136. return
  137. }
  138. // getSortColumnName 将前端排序字段名转换为数据库列名
  139. func (s *OpsEventTaskService) getSortColumnName(field string) string {
  140. // 前端字段名到数据库列名的映射
  141. fieldMap := map[string]string{
  142. "taskTitle": s.TaskDao.Columns.TaskTitle,
  143. "functionName": s.TaskDao.Columns.FunctionName,
  144. "taskType": s.TaskDao.Columns.TaskType,
  145. "taskStatus": s.TaskDao.Columns.TaskStatus,
  146. "priority": s.TaskDao.Columns.Priority,
  147. "opsUserName": s.TaskDao.Columns.OpsUserName,
  148. "planStartTime": s.TaskDao.Columns.PlanStartTime,
  149. "planEndTime": s.TaskDao.Columns.PlanEndTime,
  150. "completeTime": s.TaskDao.Columns.CompleteTime,
  151. "releaseVersion": s.TaskDao.Columns.ReleaseVersion,
  152. "createdTime": s.TaskDao.Columns.CreatedTime,
  153. }
  154. if colName, ok := fieldMap[field]; ok {
  155. return colName
  156. }
  157. return ""
  158. }
  159. const maxTaskNoRetries = 3
  160. // isDuplicateEntryError 判断是否为数据库唯一键冲突
  161. func isDuplicateEntryError(err error) bool {
  162. if err == nil {
  163. return false
  164. }
  165. return strings.Contains(strings.ToLower(err.Error()), "duplicate entry")
  166. }
  167. // Create 新增任务,包含事务控制和过程记录
  168. func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) (err error) {
  169. // 判断任务状态:如果执行人、计划开始时间、计划结束时间都填写了,则状态为处理中,否则为待处理
  170. taskStatus := opsdevmodel.TaskStatusTodo
  171. if req.OpsUserId > 0 && req.PlanStartTime != "" && req.PlanEndTime != "" {
  172. taskStatus = opsdevmodel.TaskStatusProcessing
  173. }
  174. for i := 0; i < maxTaskNoRetries; i++ {
  175. err = s.doCreate(req, taskStatus)
  176. if err == nil {
  177. return nil
  178. }
  179. if !isDuplicateEntryError(err) {
  180. return err
  181. }
  182. }
  183. return myerrors.DbError("任务编号生成冲突,请稍后重试")
  184. }
  185. // doCreate 执行新增任务(可在任务编号冲突时重试)
  186. func (s *OpsEventTaskService) doCreate(req *opsdevmodel.OpsEventTaskAddReq, taskStatus string) error {
  187. // 生成任务编号
  188. taskNo := s.generateTaskNo()
  189. // 构造数据
  190. data := g.Map{
  191. s.TaskDao.Columns.TaskNo: taskNo,
  192. s.TaskDao.Columns.ProjectId: req.ProjectId,
  193. s.TaskDao.Columns.ProjectName: s.getProjectName(req.ProjectId),
  194. s.TaskDao.Columns.TaskTitle: req.TaskTitle,
  195. s.TaskDao.Columns.TaskDesc: req.TaskDesc,
  196. s.TaskDao.Columns.FunctionName: req.FunctionName,
  197. s.TaskDao.Columns.TaskType: req.TaskType,
  198. s.TaskDao.Columns.TaskStatus: taskStatus,
  199. s.TaskDao.Columns.Priority: req.Priority,
  200. s.TaskDao.Columns.OpsUserId: req.OpsUserId,
  201. s.TaskDao.Columns.OpsUserName: req.OpsUserName,
  202. s.TaskDao.Columns.EstimateWorkHour: req.EstimateWorkHour,
  203. s.TaskDao.Columns.DefectType: req.DefectType,
  204. s.TaskDao.Columns.ReleaseVersion: req.ReleaseVersion,
  205. s.TaskDao.Columns.Remark: req.Remark,
  206. s.TaskDao.Columns.EventId: req.EventId,
  207. s.TaskDao.Columns.EventType: req.EventType,
  208. s.TaskDao.Columns.TaskParentId: req.TaskParentId,
  209. s.TaskDao.Columns.Attribute2: req.Attribute2,
  210. }
  211. if req.PlanStartTime != "" {
  212. data[s.TaskDao.Columns.PlanStartTime] = req.PlanStartTime
  213. }
  214. if req.PlanEndTime != "" {
  215. data[s.TaskDao.Columns.PlanEndTime] = req.PlanEndTime
  216. }
  217. // 补齐审计字段
  218. service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  219. // 使用事务控制
  220. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  221. // 1. 创建任务记录
  222. result, err := s.TaskDao.TX(tx).Data(data).Insert()
  223. if err != nil {
  224. g.Log().Error(err)
  225. if isDuplicateEntryError(err) {
  226. return err
  227. }
  228. return myerrors.DbError("新增任务失败")
  229. }
  230. // 获取新创建的任务ID
  231. taskId, err := result.LastInsertId()
  232. if err != nil {
  233. g.Log().Error(err)
  234. return myerrors.DbError("获取任务ID失败")
  235. }
  236. // 2. 创建任务过程记录
  237. recordData := g.Map{
  238. s.RecordDao.Columns.TaskId: taskId,
  239. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  240. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  241. s.RecordDao.Columns.HandleContent: "创建任务<br/>任务标题: " + req.TaskTitle,
  242. }
  243. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  244. recordResult, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  245. if err != nil {
  246. g.Log().Error(err)
  247. return myerrors.DbError("新增任务过程记录失败")
  248. }
  249. // 获取过程记录ID
  250. recordId, err := recordResult.LastInsertId()
  251. if err != nil {
  252. g.Log().Error(err)
  253. return myerrors.DbError("获取过程记录ID失败")
  254. }
  255. // 3. 保存附件到任务附件表,关联过程记录
  256. if len(req.Attachments) > 0 {
  257. for _, att := range req.Attachments {
  258. attData := g.Map{
  259. s.AttachmentDao.Columns.TaskId: taskId,
  260. s.AttachmentDao.Columns.TaskRecordId: recordId,
  261. s.AttachmentDao.Columns.FileName: att.FileName,
  262. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  263. s.AttachmentDao.Columns.FileType: att.FileType,
  264. }
  265. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  266. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  267. if err != nil {
  268. g.Log().Error(err)
  269. return myerrors.DbError("保存附件信息失败")
  270. }
  271. }
  272. }
  273. return nil
  274. })
  275. }
  276. // UpdateById 根据ID更新任务
  277. func (s *OpsEventTaskService) UpdateById(req *opsdevmodel.OpsEventTaskUpdateReq) error {
  278. // 校验数据是否存在
  279. var entity opsdevmodel.OpsEventTask
  280. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  281. if err != nil {
  282. g.Log().Error(err)
  283. return myerrors.DbError("查询任务数据失败")
  284. }
  285. if entity.Id <= 0 {
  286. return myerrors.TipsError("任务数据不存在")
  287. }
  288. // 已完成(30)或已作废(90)状态的任务不允许编辑
  289. if !s.canEdit(entity.TaskStatus) {
  290. return myerrors.TipsError("已完成或已作废状态的任务不允许编辑")
  291. }
  292. // 构造更新数据 - 只更新传入的非空字段
  293. data := g.Map{}
  294. if req.TaskTitle != "" {
  295. data[s.TaskDao.Columns.TaskTitle] = req.TaskTitle
  296. }
  297. if req.TaskDesc != "" {
  298. data[s.TaskDao.Columns.TaskDesc] = req.TaskDesc
  299. }
  300. if req.FunctionName != "" {
  301. data[s.TaskDao.Columns.FunctionName] = req.FunctionName
  302. }
  303. if req.TaskType != "" {
  304. data[s.TaskDao.Columns.TaskType] = req.TaskType
  305. }
  306. if req.Priority != "" {
  307. data[s.TaskDao.Columns.Priority] = req.Priority
  308. }
  309. if req.OpsUserId > 0 {
  310. data[s.TaskDao.Columns.OpsUserId] = req.OpsUserId
  311. }
  312. if req.OpsUserName != "" {
  313. data[s.TaskDao.Columns.OpsUserName] = req.OpsUserName
  314. }
  315. if req.PlanStartTime != "" {
  316. data[s.TaskDao.Columns.PlanStartTime] = req.PlanStartTime
  317. }
  318. if req.PlanEndTime != "" {
  319. data[s.TaskDao.Columns.PlanEndTime] = req.PlanEndTime
  320. }
  321. if req.EstimateWorkHour > 0 {
  322. data[s.TaskDao.Columns.EstimateWorkHour] = req.EstimateWorkHour
  323. }
  324. if req.DefectType != "" {
  325. data[s.TaskDao.Columns.DefectType] = req.DefectType
  326. }
  327. if req.ReleaseVersion != "" {
  328. data[s.TaskDao.Columns.ReleaseVersion] = req.ReleaseVersion
  329. }
  330. if req.Remark != "" {
  331. data[s.TaskDao.Columns.Remark] = req.Remark
  332. }
  333. if req.Attribute2 != "" {
  334. data[s.TaskDao.Columns.Attribute2] = req.Attribute2
  335. }
  336. // 补齐审计字段
  337. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  338. // 更新操作,排除不可改字段
  339. _, err = s.TaskDao.FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  340. if err != nil {
  341. g.Log().Error(err)
  342. return myerrors.DbError("更新任务失败")
  343. }
  344. return nil
  345. }
  346. // DeleteByIds 根据ID批量删除
  347. func (s *OpsEventTaskService) DeleteByIds(ids []int64) error {
  348. if len(ids) == 0 {
  349. return myerrors.TipsError("请选择需要删除的任务")
  350. }
  351. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  352. // 1. 删除任务
  353. _, err := s.TaskDao.TX(tx).WhereIn(s.TaskDao.Columns.Id, ids).Delete()
  354. if err != nil {
  355. g.Log().Error(err)
  356. return myerrors.DbError("删除任务失败")
  357. }
  358. // 2. 删除关联的过程记录
  359. _, err = s.RecordDao.TX(tx).WhereIn(s.RecordDao.Columns.TaskId, ids).Delete()
  360. if err != nil {
  361. g.Log().Error(err)
  362. return myerrors.DbError("删除任务过程记录失败")
  363. }
  364. // 3. 删除关联的附件
  365. _, err = s.AttachmentDao.TX(tx).WhereIn(s.AttachmentDao.Columns.TaskId, ids).Delete()
  366. if err != nil {
  367. g.Log().Error(err)
  368. return myerrors.DbError("删除任务附件失败")
  369. }
  370. return nil
  371. })
  372. }
  373. // GetById 根据ID查询单条(关联项目信息)
  374. func (s *OpsEventTaskService) GetById(id int) (*opsdevmodel.OpsEventTaskRsp, error) {
  375. var entity opsdevmodel.OpsEventTask
  376. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, id).Scan(&entity)
  377. if err != nil {
  378. g.Log().Error(err)
  379. return nil, myerrors.DbError("查询任务数据失败")
  380. }
  381. if entity.Id <= 0 {
  382. return nil, myerrors.TipsError("任务数据不存在")
  383. }
  384. var rsp opsdevmodel.OpsEventTaskRsp
  385. if err := gconv.Struct(entity, &rsp); err != nil {
  386. g.Log().Error(err)
  387. return nil, myerrors.DbError("数据转换失败")
  388. }
  389. return &rsp, nil
  390. }
  391. // Schedule 任务排期
  392. func (s *OpsEventTaskService) Schedule(req *opsdevmodel.OpsEventTaskScheduleReq) error {
  393. // 校验数据是否存在
  394. var entity opsdevmodel.OpsEventTask
  395. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  396. if err != nil {
  397. g.Log().Error(err)
  398. return myerrors.DbError("查询任务数据失败")
  399. }
  400. if entity.Id <= 0 {
  401. return myerrors.TipsError("任务数据不存在")
  402. }
  403. // 只有待处理状态可以排期
  404. if !s.canSchedule(entity.TaskStatus) {
  405. return myerrors.TipsError("只有待处理状态的任务可以排期")
  406. }
  407. data := g.Map{
  408. s.TaskDao.Columns.OpsUserId: req.OpsUserId,
  409. s.TaskDao.Columns.OpsUserName: req.OpsUserName,
  410. s.TaskDao.Columns.PlanStartTime: req.PlanStartTime,
  411. s.TaskDao.Columns.PlanEndTime: req.PlanEndTime,
  412. s.TaskDao.Columns.EstimateWorkHour: req.EstimateWorkHour,
  413. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusProcessing,
  414. }
  415. // 补齐审计字段
  416. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  417. // 使用事务
  418. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  419. // 1. 更新任务
  420. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  421. if err != nil {
  422. g.Log().Error(err)
  423. return myerrors.DbError("任务排期失败")
  424. }
  425. // 2. 创建过程记录
  426. handleContent := "任务排期<br/>执行人: " + req.OpsUserName +
  427. "<br/>计划开始: " + req.PlanStartTime +
  428. "<br/>计划结束: " + req.PlanEndTime +
  429. "<br/>预估工时: " + gconv.String(req.EstimateWorkHour) + "小时"
  430. recordData := g.Map{
  431. s.RecordDao.Columns.TaskId: req.Id,
  432. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  433. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  434. s.RecordDao.Columns.HandleContent: handleContent,
  435. }
  436. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  437. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  438. if err != nil {
  439. g.Log().Error(err)
  440. return myerrors.DbError("新增过程记录失败")
  441. }
  442. return nil
  443. })
  444. }
  445. // Start 开始任务
  446. func (s *OpsEventTaskService) Start(req *opsdevmodel.OpsEventTaskStartReq) error {
  447. // 校验数据是否存在
  448. var entity opsdevmodel.OpsEventTask
  449. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  450. if err != nil {
  451. g.Log().Error(err)
  452. return myerrors.DbError("查询任务数据失败")
  453. }
  454. if entity.Id <= 0 {
  455. return myerrors.TipsError("任务数据不存在")
  456. }
  457. // 只有待处理(10)或暂停(25)状态可以开始
  458. if !s.canStart(entity.TaskStatus) {
  459. return myerrors.TipsError("只有待处理或暂停状态的任务可以开始")
  460. }
  461. // 构造更新数据
  462. data := g.Map{
  463. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusProcessing, // 处理中
  464. }
  465. // 补齐审计字段
  466. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  467. // 使用事务
  468. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  469. // 1. 更新任务状态
  470. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  471. if err != nil {
  472. g.Log().Error(err)
  473. return myerrors.DbError("开始任务失败")
  474. }
  475. // 2. 创建过程记录
  476. recordData := g.Map{
  477. s.RecordDao.Columns.TaskId: req.Id,
  478. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  479. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  480. s.RecordDao.Columns.HandleContent: "开始处理任务<br/>任务标题: " + entity.TaskTitle,
  481. }
  482. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  483. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  484. if err != nil {
  485. g.Log().Error(err)
  486. return myerrors.DbError("新增过程记录失败")
  487. }
  488. return nil
  489. })
  490. }
  491. // Complete 完成任务
  492. func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq) (err error) {
  493. g.Log().Infof("Handler received req: %+v", req)
  494. // 校验数据是否存在
  495. var entity opsdevmodel.OpsEventTask
  496. err = s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  497. if err != nil {
  498. g.Log().Error(err)
  499. return myerrors.DbError("查询任务数据失败")
  500. }
  501. if entity.Id <= 0 {
  502. return myerrors.TipsError("任务数据不存在")
  503. }
  504. // 只有处理中(20)状态可以完成
  505. if !s.canComplete(entity.TaskStatus) {
  506. return myerrors.TipsError("只有处理中的任务可以完成")
  507. }
  508. for i := 0; i < maxTaskNoRetries; i++ {
  509. err = s.doComplete(req, &entity)
  510. if err == nil {
  511. return nil
  512. }
  513. if !isDuplicateEntryError(err) {
  514. return err
  515. }
  516. }
  517. return myerrors.DbError("任务编号生成冲突,请稍后重试")
  518. }
  519. // doComplete 执行完成任务(可在下游任务编号冲突时重试)
  520. func (s *OpsEventTaskService) doComplete(req *opsdevmodel.OpsEventTaskCompleteReq, entity *opsdevmodel.OpsEventTask) error {
  521. // 构造更新数据
  522. data := g.Map{
  523. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCompleted, // 已完成
  524. s.TaskDao.Columns.CompleteTime: gtime.Now(),
  525. s.TaskDao.Columns.ActualWorkHour: req.ActualWorkHour,
  526. s.TaskDao.Columns.Remark: req.Remark,
  527. }
  528. // 补齐审计字段
  529. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  530. // 预生成下游任务编号(避免事务内查询重复)
  531. var devTaskNo, testTaskNo string
  532. if entity.TaskType == opsdevmodel.TaskTypeReqReview {
  533. devTaskNo = s.generateTaskNo()
  534. }
  535. if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
  536. testTaskNo = s.generateTaskNo()
  537. }
  538. if entity.TaskType == opsdevmodel.TaskTypeBug {
  539. testTaskNo = s.generateTaskNo()
  540. }
  541. // 使用事务
  542. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  543. // 1. 更新任务状态
  544. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  545. if err != nil {
  546. g.Log().Error(err)
  547. return myerrors.DbError("完成任务失败")
  548. }
  549. // 2. 创建过程记录
  550. var handleContent string
  551. if entity.TaskType == opsdevmodel.TaskTypeFeatureTest && req.TestResult != "" {
  552. testResultText := "通过"
  553. if req.TestResult == "fail" {
  554. testResultText = "不通过"
  555. }
  556. handleContent = fmt.Sprintf("测试结果:%s <br/>测试时间:%s", testResultText, gtime.Now().Format("Y-m-d"))
  557. } else if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete {
  558. // 系统发版完成记录
  559. handleContent = "发版完成<br/>任务标题: " + entity.TaskTitle + "<br/>发版工时: " + gconv.String(req.ActualWorkHour) + "小时"
  560. if req.Remark != "" {
  561. handleContent += "<br/>发版说明: " + req.Remark
  562. }
  563. if len(req.DevTaskIds) > 0 {
  564. handleContent += "<br/>关联研发任务数: " + gconv.String(len(req.DevTaskIds)) + "个"
  565. }
  566. } else {
  567. // 普通任务完成记录
  568. handleContent = "完成任务<br/>任务标题: " + entity.TaskTitle + "<br/>实际工作量: " + gconv.String(req.ActualWorkHour) + "小时"
  569. }
  570. recordData := g.Map{
  571. s.RecordDao.Columns.TaskId: req.Id,
  572. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  573. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  574. s.RecordDao.Columns.HandleContent: handleContent,
  575. }
  576. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  577. result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  578. if err != nil {
  579. g.Log().Error(err)
  580. return myerrors.DbError("新增过程记录失败")
  581. }
  582. recordId, _ := result.LastInsertId()
  583. // 3. 保存附件
  584. if len(req.Attachments) > 0 {
  585. for _, att := range req.Attachments {
  586. attData := g.Map{
  587. s.AttachmentDao.Columns.TaskId: req.Id,
  588. s.AttachmentDao.Columns.TaskRecordId: recordId,
  589. s.AttachmentDao.Columns.FileName: att.FileName,
  590. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  591. s.AttachmentDao.Columns.FileType: att.FileType,
  592. }
  593. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  594. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  595. if err != nil {
  596. g.Log().Error(err)
  597. return myerrors.DbError("保存附件信息失败")
  598. }
  599. }
  600. }
  601. // 4. 根据任务类型自动创建下游任务
  602. // 4.1 需求评审完成时,自动创建功能开发任务
  603. if entity.TaskType == opsdevmodel.TaskTypeReqReview {
  604. devTaskData := g.Map{
  605. s.TaskDao.Columns.TaskNo: devTaskNo,
  606. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  607. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  608. s.TaskDao.Columns.EventId: entity.EventId,
  609. s.TaskDao.Columns.EventType: entity.EventType,
  610. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  611. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  612. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  613. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureDev, // 功能开发
  614. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  615. s.TaskDao.Columns.Priority: entity.Priority,
  616. s.TaskDao.Columns.TaskParentId: req.Id,
  617. }
  618. service.SetCreatedInfo(devTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  619. devResult, err := s.TaskDao.TX(tx).Data(devTaskData).Insert()
  620. if err != nil {
  621. g.Log().Error(err)
  622. if isDuplicateEntryError(err) {
  623. return err
  624. }
  625. return myerrors.DbError("自动创建功能开发任务失败")
  626. }
  627. devTaskId, _ := devResult.LastInsertId()
  628. // 创建功能开发任务的过程记录
  629. devRecordData := g.Map{
  630. s.RecordDao.Columns.TaskId: devTaskId,
  631. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  632. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  633. s.RecordDao.Columns.HandleContent: "创建功能开发任务<br/>说明: 由需求评审任务自动创建",
  634. }
  635. service.SetCreatedInfo(devRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  636. _, err = s.RecordDao.TX(tx).Data(devRecordData).Insert()
  637. if err != nil {
  638. g.Log().Error(err)
  639. return myerrors.DbError("新增功能开发任务过程记录失败")
  640. }
  641. }
  642. // 4.2 功能开发完成时,自动创建功能测试任务
  643. if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
  644. testTaskData := g.Map{
  645. s.TaskDao.Columns.TaskNo: testTaskNo,
  646. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  647. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  648. s.TaskDao.Columns.EventId: entity.EventId,
  649. s.TaskDao.Columns.EventType: entity.EventType,
  650. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  651. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  652. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  653. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
  654. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  655. s.TaskDao.Columns.Priority: entity.Priority,
  656. s.TaskDao.Columns.TaskParentId: req.Id,
  657. }
  658. service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  659. testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
  660. if err != nil {
  661. g.Log().Error(err)
  662. if isDuplicateEntryError(err) {
  663. return err
  664. }
  665. return myerrors.DbError("自动创建功能测试任务失败")
  666. }
  667. testTaskId, _ := testResult.LastInsertId()
  668. // 创建功能测试任务的过程记录
  669. testRecordData := g.Map{
  670. s.RecordDao.Columns.TaskId: testTaskId,
  671. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  672. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  673. s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由功能开发任务自动创建",
  674. }
  675. service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  676. _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
  677. if err != nil {
  678. g.Log().Error(err)
  679. return myerrors.DbError("新增功能测试任务过程记录失败")
  680. }
  681. }
  682. // 4.2b BUG任务完成时,自动创建功能测试任务
  683. if entity.TaskType == opsdevmodel.TaskTypeBug {
  684. testTaskData := g.Map{
  685. s.TaskDao.Columns.TaskNo: testTaskNo,
  686. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  687. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  688. s.TaskDao.Columns.EventId: entity.EventId,
  689. s.TaskDao.Columns.EventType: entity.EventType,
  690. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  691. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  692. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  693. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
  694. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  695. s.TaskDao.Columns.Priority: entity.Priority,
  696. s.TaskDao.Columns.TaskParentId: req.Id,
  697. }
  698. service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  699. testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
  700. if err != nil {
  701. g.Log().Error(err)
  702. if isDuplicateEntryError(err) {
  703. return err
  704. }
  705. return myerrors.DbError("自动创建功能测试任务失败")
  706. }
  707. testTaskId, _ := testResult.LastInsertId()
  708. // 创建功能测试任务的过程记录
  709. testRecordData := g.Map{
  710. s.RecordDao.Columns.TaskId: testTaskId,
  711. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  712. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  713. s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由BUG任务自动创建",
  714. }
  715. service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  716. _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
  717. if err != nil {
  718. g.Log().Error(err)
  719. return myerrors.DbError("新增功能测试任务过程记录失败")
  720. }
  721. }
  722. // 4.3 系统发版完成时,保存关联的研发任务到ops_event_task_release表
  723. if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete && len(req.DevTaskIds) > 0 {
  724. for _, devTaskId := range req.DevTaskIds {
  725. releaseData := g.Map{
  726. s.ReleaseDao.Columns.ReleaseTaskId: req.Id,
  727. s.ReleaseDao.Columns.DevTaskId: devTaskId,
  728. s.ReleaseDao.Columns.ProjectId: entity.ProjectId,
  729. }
  730. service.SetCreatedInfo(releaseData, s.GetCxtUserId(), s.GetCxtUserName())
  731. _, err := s.ReleaseDao.TX(tx).Data(releaseData).Insert()
  732. if err != nil {
  733. g.Log().Error(err)
  734. return myerrors.DbError("保存发版关联任务失败")
  735. }
  736. }
  737. }
  738. // 5. 任务完成时自动更新关联的交付事件状态为完成并生成过程记录
  739. if entity.EventId > 0 && entity.EventType == opsdevmodel.EventTypeDelivery {
  740. // 触发事件自动完成的任务类型:10(需求评审)、30(功能测试-通过)、38(系统发版)、40(系统发版/硬件发货)、41(硬件安装)
  741. if s.shouldAutoCompleteEvent(entity.TaskType, req.TestResult) {
  742. eventData := g.Map{
  743. s.EventDao.Columns.DeliveryEventStatus: opsdevmodel.DeliveryEventStatusClosed,
  744. s.EventDao.Columns.CompleteTime: gtime.Now(),
  745. }
  746. service.SetUpdatedInfo(eventData, s.GetCxtUserId(), s.GetCxtUserName())
  747. _, err := s.EventDao.TX(tx).FieldsEx(service.UpdateFieldEx...).
  748. Data(eventData).
  749. WherePri(s.EventDao.Columns.Id, entity.EventId).
  750. Update()
  751. if err != nil {
  752. g.Log().Error(err)
  753. return myerrors.DbError("自动更新事件状态失败")
  754. }
  755. eventRecordData := g.Map{
  756. s.EventRecordDao.Columns.DeliveryEventId: entity.EventId,
  757. s.EventRecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  758. s.EventRecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  759. s.EventRecordDao.Columns.HandleContent: "研发任务已完成,自动关闭事件<br/>任务编号: " + entity.TaskNo + "<br/>任务标题: " + entity.TaskTitle,
  760. }
  761. service.SetCreatedInfo(eventRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  762. _, err = s.EventRecordDao.TX(tx).Data(eventRecordData).Insert()
  763. if err != nil {
  764. g.Log().Error(err)
  765. return myerrors.DbError("新增事件过程记录失败")
  766. }
  767. }
  768. }
  769. return nil
  770. })
  771. }
  772. // Pause 暂停任务
  773. func (s *OpsEventTaskService) Pause(req *opsdevmodel.OpsEventTaskPauseReq) error {
  774. // 校验数据是否存在
  775. var entity opsdevmodel.OpsEventTask
  776. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  777. if err != nil {
  778. g.Log().Error(err)
  779. return myerrors.DbError("查询任务数据失败")
  780. }
  781. if entity.Id <= 0 {
  782. return myerrors.TipsError("任务数据不存在")
  783. }
  784. // 只有处理中(20)状态可以暂停
  785. if !s.canPause(entity.TaskStatus) {
  786. return myerrors.TipsError("只有处理中的任务可以暂停")
  787. }
  788. // 构造更新数据
  789. data := g.Map{
  790. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusPaused, // 暂停
  791. }
  792. // 补齐审计字段
  793. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  794. // 使用事务
  795. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  796. // 1. 更新任务状态
  797. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  798. if err != nil {
  799. g.Log().Error(err)
  800. return myerrors.DbError("暂停任务失败")
  801. }
  802. // 2. 创建过程记录
  803. recordData := g.Map{
  804. s.RecordDao.Columns.TaskId: req.Id,
  805. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  806. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  807. s.RecordDao.Columns.HandleContent: "暂停任务<br/>暂停原因: " + req.Remark,
  808. }
  809. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  810. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  811. if err != nil {
  812. g.Log().Error(err)
  813. return myerrors.DbError("新增过程记录失败")
  814. }
  815. return nil
  816. })
  817. }
  818. // Block 阻塞任务
  819. func (s *OpsEventTaskService) Block(req *opsdevmodel.OpsEventTaskBlockReq) error {
  820. // 校验数据是否存在
  821. var entity opsdevmodel.OpsEventTask
  822. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  823. if err != nil {
  824. g.Log().Error(err)
  825. return myerrors.DbError("查询任务数据失败")
  826. }
  827. if entity.Id <= 0 {
  828. return myerrors.TipsError("任务数据不存在")
  829. }
  830. // 只有处理中(20)或暂停(25)状态可以阻塞
  831. if !s.canBlock(entity.TaskStatus) {
  832. return myerrors.TipsError("只有处理中或暂停状态的任务可以阻塞")
  833. }
  834. // 构造更新数据
  835. data := g.Map{
  836. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusBlocked, // 阻塞
  837. }
  838. // 补齐审计字段
  839. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  840. // 使用事务
  841. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  842. // 1. 更新任务状态
  843. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  844. if err != nil {
  845. g.Log().Error(err)
  846. return myerrors.DbError("阻塞任务失败")
  847. }
  848. // 2. 创建过程记录
  849. recordData := g.Map{
  850. s.RecordDao.Columns.TaskId: req.Id,
  851. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  852. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  853. s.RecordDao.Columns.HandleContent: "阻塞任务<br/>阻塞原因: " + req.Remark,
  854. }
  855. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  856. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  857. if err != nil {
  858. g.Log().Error(err)
  859. return myerrors.DbError("新增过程记录失败")
  860. }
  861. return nil
  862. })
  863. }
  864. // Cancel 作废任务
  865. func (s *OpsEventTaskService) Cancel(req *opsdevmodel.OpsEventTaskCancelReq) error {
  866. // 校验数据是否存在
  867. var entity opsdevmodel.OpsEventTask
  868. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  869. if err != nil {
  870. g.Log().Error(err)
  871. return myerrors.DbError("查询任务数据失败")
  872. }
  873. if entity.Id <= 0 {
  874. return myerrors.TipsError("任务数据不存在")
  875. }
  876. // 已完成(30)的任务不能作废
  877. if !s.canCancel(entity.TaskStatus) {
  878. return myerrors.TipsError("已完成的任务不能作废")
  879. }
  880. // 构造更新数据
  881. data := g.Map{
  882. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCancelled, // 作废
  883. }
  884. // 补齐审计字段
  885. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  886. // 使用事务
  887. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  888. // 1. 更新任务状态
  889. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  890. if err != nil {
  891. g.Log().Error(err)
  892. return myerrors.DbError("作废任务失败")
  893. }
  894. // 2. 创建过程记录
  895. recordData := g.Map{
  896. s.RecordDao.Columns.TaskId: req.Id,
  897. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  898. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  899. s.RecordDao.Columns.HandleContent: "作废任务<br/>作废原因: " + req.Remark,
  900. }
  901. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  902. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  903. if err != nil {
  904. g.Log().Error(err)
  905. return myerrors.DbError("新增过程记录失败")
  906. }
  907. return nil
  908. })
  909. }
  910. // GetRecords 获取任务过程记录列表(包含附件)
  911. func (s *OpsEventTaskService) GetRecords(req *opsdevmodel.OpsEventTaskRecordSearchReq) ([]*opsdevmodel.OpsEventTaskRecordWithAttachments, error) {
  912. var records []*opsdevmodel.OpsEventTaskRecord
  913. err := s.RecordDao.Where(s.RecordDao.Columns.TaskId, req.TaskId).
  914. Order(s.RecordDao.Columns.CreatedTime + " desc").
  915. Scan(&records)
  916. if err != nil {
  917. g.Log().Error(err)
  918. return nil, myerrors.DbError("查询过程记录失败")
  919. }
  920. result := make([]*opsdevmodel.OpsEventTaskRecordWithAttachments, 0, len(records))
  921. for _, record := range records {
  922. recordRsp := &opsdevmodel.OpsEventTaskRecordWithAttachments{
  923. OpsEventTaskRecord: *record,
  924. Attachments: []*opsdevmodel.OpsEventTaskAttachment{},
  925. }
  926. // 查询该记录关联的附件
  927. if record.Id > 0 {
  928. var attachments []*opsdevmodel.OpsEventTaskAttachment
  929. err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskRecordId, record.Id).
  930. Scan(&attachments)
  931. if err != nil {
  932. g.Log().Error(err)
  933. } else {
  934. recordRsp.Attachments = attachments
  935. }
  936. }
  937. result = append(result, recordRsp)
  938. }
  939. return result, nil
  940. }
  941. // generateTaskNo 生成任务编号
  942. func (s *OpsEventTaskService) generateTaskNo() string {
  943. // 格式: TSK + 年月日 + 4位序列号
  944. now := gtime.Now()
  945. prefix := "TSK" + now.Format("Ymd")
  946. // 使用数据库序列生成唯一序号(按天重置)
  947. seqVal, err := s.TaskDao.DB.GetValue("SELECT next_day_reset_val('task_no_seq')")
  948. if err != nil {
  949. // 如果序列不存在或出错,使用备用方案:查询当天最大序号+1
  950. var maxNoResult struct {
  951. TaskNo string
  952. }
  953. err = s.TaskDao.Where(s.TaskDao.Columns.TaskNo+" like ?", prefix+"%").Order(s.TaskDao.Columns.TaskNo + " desc").Fields(s.TaskDao.Columns.TaskNo).Scan(&maxNoResult)
  954. if err != nil || maxNoResult.TaskNo == "" {
  955. return prefix + "0001"
  956. }
  957. maxNoStr := maxNoResult.TaskNo
  958. if len(maxNoStr) >= len(prefix)+4 {
  959. seq := maxNoStr[len(prefix):]
  960. seqNum := gconv.Int(seq)
  961. seqNum++
  962. return prefix + fmt.Sprintf("%04d", seqNum)
  963. }
  964. return prefix + "0001"
  965. }
  966. return prefix + fmt.Sprintf("%04d", seqVal.Int())
  967. }
  968. // 状态校验辅助方法
  969. func (s *OpsEventTaskService) canEdit(status string) bool {
  970. return status != opsdevmodel.TaskStatusCompleted && status != opsdevmodel.TaskStatusCancelled
  971. }
  972. func (s *OpsEventTaskService) canSchedule(status string) bool {
  973. return status == opsdevmodel.TaskStatusTodo
  974. }
  975. func (s *OpsEventTaskService) canStart(status string) bool {
  976. return status == opsdevmodel.TaskStatusTodo || status == opsdevmodel.TaskStatusPaused || status == opsdevmodel.TaskStatusBlocked
  977. }
  978. func (s *OpsEventTaskService) canComplete(status string) bool {
  979. return status == opsdevmodel.TaskStatusProcessing
  980. }
  981. func (s *OpsEventTaskService) canPause(status string) bool {
  982. return status == opsdevmodel.TaskStatusProcessing
  983. }
  984. func (s *OpsEventTaskService) canBlock(status string) bool {
  985. return status == opsdevmodel.TaskStatusProcessing || status == opsdevmodel.TaskStatusPaused
  986. }
  987. // shouldAutoCompleteEvent 判断当前任务完成时是否应自动完成关联的交付事件
  988. func (s *OpsEventTaskService) shouldAutoCompleteEvent(taskType, testResult string) bool {
  989. switch taskType {
  990. case "10": // 需求评审 — 完成即触发
  991. return true
  992. case "30": // 功能测试 — 仅通过时触发
  993. return testResult == "pass"
  994. case "38": // 系统发版(事件关联) — 完成即触发
  995. return true
  996. default:
  997. return false
  998. }
  999. }
  1000. func (s *OpsEventTaskService) canCancel(status string) bool {
  1001. return status != opsdevmodel.TaskStatusCompleted
  1002. }
  1003. // getProjectName 根据项目ID获取项目名称
  1004. func (s *OpsEventTaskService) getProjectName(projectId int) string {
  1005. if projectId <= 0 {
  1006. return ""
  1007. }
  1008. var project opsdevmodel.OpsDeliveryProject
  1009. err := s.ProjectDao.FieldsEx(s.ProjectDao.Columns.DeletedTime).
  1010. WherePri(s.ProjectDao.Columns.Id, projectId).
  1011. Scan(&project)
  1012. if err != nil {
  1013. g.Log().Error(err)
  1014. return ""
  1015. }
  1016. return project.ProjectName
  1017. }
  1018. // GetAttachments 获取任务附件列表
  1019. func (s *OpsEventTaskService) GetAttachments(taskId int) ([]*opsdevmodel.OpsEventTaskAttachment, error) {
  1020. var attachments []*opsdevmodel.OpsEventTaskAttachment
  1021. err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskId, taskId).
  1022. Order(s.AttachmentDao.Columns.CreatedTime + " desc").
  1023. Scan(&attachments)
  1024. if err != nil {
  1025. g.Log().Error(err)
  1026. return nil, myerrors.DbError("查询附件失败")
  1027. }
  1028. return attachments, nil
  1029. }
  1030. // GetTaskReleaseList 根据发版任务ID查询关联的开发任务列表
  1031. func (s *OpsEventTaskService) GetTaskReleaseList(req *opsdevmodel.OpsEventTaskReleaseListReq) ([]*opsdevmodel.OpsEventTaskReleaseRsp, error) {
  1032. // 1. 查询关联表获取关联的任务ID列表
  1033. var releaseList []*opsdevmodel.OpsEventTaskRelease
  1034. err := s.ReleaseDao.Where(s.ReleaseDao.Columns.ReleaseTaskId, req.ReleaseTaskId).
  1035. Scan(&releaseList)
  1036. if err != nil {
  1037. g.Log().Error(err)
  1038. return nil, myerrors.DbError("查询发布版本关联记录失败")
  1039. }
  1040. if len(releaseList) == 0 {
  1041. return []*opsdevmodel.OpsEventTaskReleaseRsp{}, nil
  1042. }
  1043. // 2. 提取开发任务ID列表
  1044. devTaskIds := make([]int, 0, len(releaseList))
  1045. for _, r := range releaseList {
  1046. devTaskIds = append(devTaskIds, r.DevTaskId)
  1047. }
  1048. // 3. 查询任务详情
  1049. var taskList []*opsdevmodel.OpsEventTask
  1050. err = s.TaskDao.WhereIn(s.TaskDao.Columns.Id, devTaskIds).
  1051. Scan(&taskList)
  1052. if err != nil {
  1053. g.Log().Error(err)
  1054. return nil, myerrors.DbError("查询关联任务详情失败")
  1055. }
  1056. // 4. 构建响应数据
  1057. result := make([]*opsdevmodel.OpsEventTaskReleaseRsp, 0, len(taskList))
  1058. for _, task := range taskList {
  1059. result = append(result, &opsdevmodel.OpsEventTaskReleaseRsp{
  1060. Id: task.Id,
  1061. DevTaskId: task.Id,
  1062. TaskNo: task.TaskNo,
  1063. TaskTitle: task.TaskTitle,
  1064. TaskType: task.TaskType,
  1065. TaskStatus: task.TaskStatus,
  1066. OpsUserName: task.OpsUserName,
  1067. ProjectId: task.ProjectId,
  1068. CreatedTime: task.CreatedTime.Format("Y-m-d H:i:s"),
  1069. })
  1070. }
  1071. return result, nil
  1072. }
  1073. // AddRecord 添加任务过程记录
  1074. func (s *OpsEventTaskService) AddRecord(req *opsdevmodel.OpsEventTaskRecordAddReq) error {
  1075. // 构造记录数据
  1076. recordData := g.Map{
  1077. s.RecordDao.Columns.TaskId: req.TaskId,
  1078. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  1079. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  1080. s.RecordDao.Columns.HandleContent: req.HandleContent,
  1081. }
  1082. // 补齐审计字段
  1083. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  1084. // 使用事务控制
  1085. return s.RecordDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  1086. // 1. 创建过程记录
  1087. result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  1088. if err != nil {
  1089. g.Log().Error(err)
  1090. return myerrors.DbError("添加过程记录失败")
  1091. }
  1092. // 获取新创建的记录ID
  1093. recordId, err := result.LastInsertId()
  1094. if err != nil {
  1095. g.Log().Error(err)
  1096. return myerrors.DbError("获取记录ID失败")
  1097. }
  1098. // 2. 如果有附件,保存附件
  1099. if len(req.Attachments) > 0 {
  1100. for _, att := range req.Attachments {
  1101. attData := g.Map{
  1102. s.AttachmentDao.Columns.TaskId: req.TaskId,
  1103. s.AttachmentDao.Columns.TaskRecordId: int(recordId),
  1104. s.AttachmentDao.Columns.FileName: att.FileName,
  1105. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  1106. s.AttachmentDao.Columns.FileType: att.FileType,
  1107. }
  1108. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  1109. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  1110. if err != nil {
  1111. g.Log().Error(err)
  1112. return myerrors.DbError("保存附件失败")
  1113. }
  1114. }
  1115. }
  1116. return nil
  1117. })
  1118. }
  1119. // AddWorkHour 添加工时登记(累加actual_work_hour并记录过程)
  1120. func (s *OpsEventTaskService) AddWorkHour(req *opsdevmodel.OpsEventTaskWorkHourAddReq) error {
  1121. var entity opsdevmodel.OpsEventTask
  1122. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.TaskId).Scan(&entity)
  1123. if err != nil {
  1124. g.Log().Error(err)
  1125. return myerrors.DbError("查询任务数据失败")
  1126. }
  1127. if entity.Id <= 0 {
  1128. return myerrors.TipsError("任务数据不存在")
  1129. }
  1130. if !s.canAddWorkHour(entity.TaskStatus) {
  1131. return myerrors.TipsError("只有处理中的任务可以登记工时")
  1132. }
  1133. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  1134. workHourData := g.Map{
  1135. s.WorkHourDao.Columns.TaskId: req.TaskId,
  1136. s.WorkHourDao.Columns.OpsUserId: s.GetCxtUserId(),
  1137. s.WorkHourDao.Columns.OpsUserName: s.GetCxtUserName(),
  1138. s.WorkHourDao.Columns.ActualWorkDate: req.WorkDate,
  1139. s.WorkHourDao.Columns.ActualWorkHour: req.ActualHour,
  1140. s.WorkHourDao.Columns.Remark: req.Remark,
  1141. }
  1142. service.SetCreatedInfo(workHourData, s.GetCxtUserId(), s.GetCxtUserName())
  1143. _, err := s.WorkHourDao.TX(tx).Data(workHourData).Insert()
  1144. if err != nil {
  1145. g.Log().Error(err)
  1146. return myerrors.DbError("工时登记失败")
  1147. }
  1148. newActualWorkHour := entity.ActualWorkHour + req.ActualHour
  1149. updateData := g.Map{
  1150. s.TaskDao.Columns.ActualWorkHour: newActualWorkHour,
  1151. }
  1152. service.SetUpdatedInfo(updateData, s.GetCxtUserId(), s.GetCxtUserName())
  1153. _, err = s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(updateData).WherePri(s.TaskDao.Columns.Id, req.TaskId).Update()
  1154. if err != nil {
  1155. g.Log().Error(err)
  1156. return myerrors.DbError("更新实际工时失败")
  1157. }
  1158. handleContent := fmt.Sprintf("工时登记<br/>工作日期: %s<br/>实际工时: %s小时<br/>工作进展: %s",
  1159. req.WorkDate, gconv.String(req.ActualHour), req.Remark)
  1160. recordData := g.Map{
  1161. s.RecordDao.Columns.TaskId: req.TaskId,
  1162. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  1163. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  1164. s.RecordDao.Columns.HandleContent: handleContent,
  1165. }
  1166. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  1167. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  1168. if err != nil {
  1169. g.Log().Error(err)
  1170. return myerrors.DbError("新增过程记录失败")
  1171. }
  1172. return nil
  1173. })
  1174. }
  1175. // GetWorkHourList 获取工时登记列表
  1176. func (s *OpsEventTaskService) GetWorkHourList(req *opsdevmodel.OpsEventTaskWorkHourListReq) ([]*opsdevmodel.OpsEventTaskWorkHourRsp, error) {
  1177. var entities []*opsdevmodel.OpsEventTaskWorkHour
  1178. err := s.WorkHourDao.FieldsEx(s.WorkHourDao.Columns.DeletedTime).
  1179. Where(s.WorkHourDao.Columns.TaskId, req.TaskId).
  1180. Order(s.WorkHourDao.Columns.CreatedTime + " desc").
  1181. Scan(&entities)
  1182. if err != nil {
  1183. g.Log().Error(err)
  1184. return nil, myerrors.DbError("查询工时登记列表失败")
  1185. }
  1186. list := make([]*opsdevmodel.OpsEventTaskWorkHourRsp, 0, len(entities))
  1187. for _, entity := range entities {
  1188. workDate := ""
  1189. if entity.ActualWorkDate != nil {
  1190. workDate = entity.ActualWorkDate.Format("Y-m-d")
  1191. }
  1192. createdTime := ""
  1193. if entity.CreatedTime != nil {
  1194. createdTime = entity.CreatedTime.Format("Y-m-d H:i:s")
  1195. }
  1196. list = append(list, &opsdevmodel.OpsEventTaskWorkHourRsp{
  1197. Id: entity.Id,
  1198. TaskId: entity.TaskId,
  1199. WorkDate: workDate,
  1200. ActualHour: entity.ActualWorkHour,
  1201. Remark: entity.Remark,
  1202. CreatedName: entity.CreatedName,
  1203. CreatedTime: createdTime,
  1204. })
  1205. }
  1206. return list, nil
  1207. }
  1208. // canAddWorkHour 检查是否可以登记工时(仅处理中状态)
  1209. func (s *OpsEventTaskService) canAddWorkHour(status string) bool {
  1210. return status == opsdevmodel.TaskStatusProcessing
  1211. }