ops_event_task.go 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  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 testTaskNo string
  532. if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
  533. testTaskNo = s.generateTaskNo()
  534. }
  535. if entity.TaskType == opsdevmodel.TaskTypeBug {
  536. testTaskNo = s.generateTaskNo()
  537. }
  538. // 使用事务
  539. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  540. // 1. 更新任务状态
  541. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  542. if err != nil {
  543. g.Log().Error(err)
  544. return myerrors.DbError("完成任务失败")
  545. }
  546. // 2. 创建过程记录
  547. var handleContent string
  548. if entity.TaskType == opsdevmodel.TaskTypeFeatureTest && req.TestResult != "" {
  549. testResultText := "通过"
  550. if req.TestResult == "fail" {
  551. testResultText = "不通过"
  552. }
  553. handleContent = fmt.Sprintf("测试结果:%s <br/>测试时间:%s", testResultText, gtime.Now().Format("Y-m-d"))
  554. } else if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete {
  555. // 系统发版完成记录
  556. handleContent = "发版完成<br/>任务标题: " + entity.TaskTitle + "<br/>发版工时: " + gconv.String(req.ActualWorkHour) + "小时"
  557. if req.Remark != "" {
  558. handleContent += "<br/>发版说明: " + req.Remark
  559. }
  560. if len(req.DevTaskIds) > 0 {
  561. handleContent += "<br/>关联研发任务数: " + gconv.String(len(req.DevTaskIds)) + "个"
  562. }
  563. } else {
  564. // 普通任务完成记录
  565. handleContent = "完成任务<br/>任务标题: " + entity.TaskTitle + "<br/>实际工作量: " + gconv.String(req.ActualWorkHour) + "小时"
  566. }
  567. recordData := g.Map{
  568. s.RecordDao.Columns.TaskId: req.Id,
  569. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  570. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  571. s.RecordDao.Columns.HandleContent: handleContent,
  572. }
  573. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  574. result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  575. if err != nil {
  576. g.Log().Error(err)
  577. return myerrors.DbError("新增过程记录失败")
  578. }
  579. recordId, _ := result.LastInsertId()
  580. // 3. 保存附件
  581. if len(req.Attachments) > 0 {
  582. for _, att := range req.Attachments {
  583. attData := g.Map{
  584. s.AttachmentDao.Columns.TaskId: req.Id,
  585. s.AttachmentDao.Columns.TaskRecordId: recordId,
  586. s.AttachmentDao.Columns.FileName: att.FileName,
  587. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  588. s.AttachmentDao.Columns.FileType: att.FileType,
  589. }
  590. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  591. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  592. if err != nil {
  593. g.Log().Error(err)
  594. return myerrors.DbError("保存附件信息失败")
  595. }
  596. }
  597. }
  598. // 4. 根据任务类型自动创建下游任务
  599. // 4.1 功能开发完成时,自动创建功能测试任务(原4.2)
  600. if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
  601. testTaskData := g.Map{
  602. s.TaskDao.Columns.TaskNo: testTaskNo,
  603. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  604. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  605. s.TaskDao.Columns.EventId: entity.EventId,
  606. s.TaskDao.Columns.EventType: entity.EventType,
  607. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  608. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  609. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  610. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
  611. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  612. s.TaskDao.Columns.Priority: entity.Priority,
  613. s.TaskDao.Columns.TaskParentId: req.Id,
  614. }
  615. service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  616. testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
  617. if err != nil {
  618. g.Log().Error(err)
  619. if isDuplicateEntryError(err) {
  620. return err
  621. }
  622. return myerrors.DbError("自动创建功能测试任务失败")
  623. }
  624. testTaskId, _ := testResult.LastInsertId()
  625. // 创建功能测试任务的过程记录
  626. testRecordData := g.Map{
  627. s.RecordDao.Columns.TaskId: testTaskId,
  628. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  629. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  630. s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由功能开发任务自动创建",
  631. }
  632. service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  633. _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
  634. if err != nil {
  635. g.Log().Error(err)
  636. return myerrors.DbError("新增功能测试任务过程记录失败")
  637. }
  638. }
  639. // 4.2b BUG任务完成时,自动创建功能测试任务
  640. if entity.TaskType == opsdevmodel.TaskTypeBug {
  641. testTaskData := g.Map{
  642. s.TaskDao.Columns.TaskNo: testTaskNo,
  643. s.TaskDao.Columns.ProjectId: entity.ProjectId,
  644. s.TaskDao.Columns.ProjectName: entity.ProjectName,
  645. s.TaskDao.Columns.EventId: entity.EventId,
  646. s.TaskDao.Columns.EventType: entity.EventType,
  647. s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
  648. s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
  649. s.TaskDao.Columns.FunctionName: entity.FunctionName,
  650. s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
  651. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
  652. s.TaskDao.Columns.Priority: entity.Priority,
  653. s.TaskDao.Columns.TaskParentId: req.Id,
  654. }
  655. if entity.TaskParentId > 0 {
  656. var parentTask opsdevmodel.OpsEventTask
  657. err := s.TaskDao.TX(tx).FieldsEx(s.TaskDao.Columns.DeletedTime).
  658. WherePri(s.TaskDao.Columns.Id, entity.TaskParentId).
  659. Scan(&parentTask)
  660. if err == nil && parentTask.Id > 0 {
  661. testTaskData[s.TaskDao.Columns.OpsUserId] = parentTask.OpsUserId
  662. testTaskData[s.TaskDao.Columns.OpsUserName] = parentTask.OpsUserName
  663. }
  664. }
  665. service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
  666. testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
  667. if err != nil {
  668. g.Log().Error(err)
  669. if isDuplicateEntryError(err) {
  670. return err
  671. }
  672. return myerrors.DbError("自动创建功能测试任务失败")
  673. }
  674. testTaskId, _ := testResult.LastInsertId()
  675. // 创建功能测试任务的过程记录
  676. testRecordData := g.Map{
  677. s.RecordDao.Columns.TaskId: testTaskId,
  678. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  679. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  680. s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由BUG任务自动创建",
  681. }
  682. service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  683. _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
  684. if err != nil {
  685. g.Log().Error(err)
  686. return myerrors.DbError("新增功能测试任务过程记录失败")
  687. }
  688. }
  689. // 4.3 系统发版完成时,保存关联的研发任务到ops_event_task_release表
  690. if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete && len(req.DevTaskIds) > 0 {
  691. for _, devTaskId := range req.DevTaskIds {
  692. releaseData := g.Map{
  693. s.ReleaseDao.Columns.ReleaseTaskId: req.Id,
  694. s.ReleaseDao.Columns.DevTaskId: devTaskId,
  695. s.ReleaseDao.Columns.ProjectId: entity.ProjectId,
  696. }
  697. service.SetCreatedInfo(releaseData, s.GetCxtUserId(), s.GetCxtUserName())
  698. _, err := s.ReleaseDao.TX(tx).Data(releaseData).Insert()
  699. if err != nil {
  700. g.Log().Error(err)
  701. return myerrors.DbError("保存发版关联任务失败")
  702. }
  703. }
  704. }
  705. // 5. 任务完成时自动更新关联的交付事件状态为完成并生成过程记录
  706. if entity.EventId > 0 && entity.EventType == opsdevmodel.EventTypeDelivery {
  707. // 触发事件自动完成的任务类型:10(需求评审)、30(功能测试-通过)、38(系统发版)、40(系统发版/硬件发货)、41(硬件安装)
  708. if s.shouldAutoCompleteEvent(entity.TaskType, req.TestResult) {
  709. eventData := g.Map{
  710. s.EventDao.Columns.DeliveryEventStatus: opsdevmodel.DeliveryEventStatusClosed,
  711. s.EventDao.Columns.CompleteTime: gtime.Now(),
  712. }
  713. service.SetUpdatedInfo(eventData, s.GetCxtUserId(), s.GetCxtUserName())
  714. _, err := s.EventDao.TX(tx).FieldsEx(service.UpdateFieldEx...).
  715. Data(eventData).
  716. WherePri(s.EventDao.Columns.Id, entity.EventId).
  717. Update()
  718. if err != nil {
  719. g.Log().Error(err)
  720. return myerrors.DbError("自动更新事件状态失败")
  721. }
  722. eventRecordData := g.Map{
  723. s.EventRecordDao.Columns.DeliveryEventId: entity.EventId,
  724. s.EventRecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  725. s.EventRecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  726. s.EventRecordDao.Columns.HandleContent: "研发任务已完成,自动关闭事件<br/>任务编号: " + entity.TaskNo + "<br/>任务标题: " + entity.TaskTitle,
  727. }
  728. service.SetCreatedInfo(eventRecordData, s.GetCxtUserId(), s.GetCxtUserName())
  729. _, err = s.EventRecordDao.TX(tx).Data(eventRecordData).Insert()
  730. if err != nil {
  731. g.Log().Error(err)
  732. return myerrors.DbError("新增事件过程记录失败")
  733. }
  734. }
  735. }
  736. return nil
  737. })
  738. }
  739. // Pause 暂停任务
  740. func (s *OpsEventTaskService) Pause(req *opsdevmodel.OpsEventTaskPauseReq) error {
  741. // 校验数据是否存在
  742. var entity opsdevmodel.OpsEventTask
  743. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  744. if err != nil {
  745. g.Log().Error(err)
  746. return myerrors.DbError("查询任务数据失败")
  747. }
  748. if entity.Id <= 0 {
  749. return myerrors.TipsError("任务数据不存在")
  750. }
  751. // 只有处理中(20)状态可以暂停
  752. if !s.canPause(entity.TaskStatus) {
  753. return myerrors.TipsError("只有处理中的任务可以暂停")
  754. }
  755. // 构造更新数据
  756. data := g.Map{
  757. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusPaused, // 暂停
  758. }
  759. // 补齐审计字段
  760. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  761. // 使用事务
  762. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  763. // 1. 更新任务状态
  764. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  765. if err != nil {
  766. g.Log().Error(err)
  767. return myerrors.DbError("暂停任务失败")
  768. }
  769. // 2. 创建过程记录
  770. recordData := g.Map{
  771. s.RecordDao.Columns.TaskId: req.Id,
  772. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  773. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  774. s.RecordDao.Columns.HandleContent: "暂停任务<br/>暂停原因: " + req.Remark,
  775. }
  776. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  777. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  778. if err != nil {
  779. g.Log().Error(err)
  780. return myerrors.DbError("新增过程记录失败")
  781. }
  782. return nil
  783. })
  784. }
  785. // Block 阻塞任务
  786. func (s *OpsEventTaskService) Block(req *opsdevmodel.OpsEventTaskBlockReq) error {
  787. // 校验数据是否存在
  788. var entity opsdevmodel.OpsEventTask
  789. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  790. if err != nil {
  791. g.Log().Error(err)
  792. return myerrors.DbError("查询任务数据失败")
  793. }
  794. if entity.Id <= 0 {
  795. return myerrors.TipsError("任务数据不存在")
  796. }
  797. // 只有处理中(20)或暂停(25)状态可以阻塞
  798. if !s.canBlock(entity.TaskStatus) {
  799. return myerrors.TipsError("只有处理中或暂停状态的任务可以阻塞")
  800. }
  801. // 构造更新数据
  802. data := g.Map{
  803. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusBlocked, // 阻塞
  804. }
  805. // 补齐审计字段
  806. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  807. // 使用事务
  808. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  809. // 1. 更新任务状态
  810. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  811. if err != nil {
  812. g.Log().Error(err)
  813. return myerrors.DbError("阻塞任务失败")
  814. }
  815. // 2. 创建过程记录
  816. recordData := g.Map{
  817. s.RecordDao.Columns.TaskId: req.Id,
  818. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  819. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  820. s.RecordDao.Columns.HandleContent: "阻塞任务<br/>阻塞原因: " + req.Remark,
  821. }
  822. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  823. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  824. if err != nil {
  825. g.Log().Error(err)
  826. return myerrors.DbError("新增过程记录失败")
  827. }
  828. return nil
  829. })
  830. }
  831. // Cancel 作废任务
  832. func (s *OpsEventTaskService) Cancel(req *opsdevmodel.OpsEventTaskCancelReq) error {
  833. // 校验数据是否存在
  834. var entity opsdevmodel.OpsEventTask
  835. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
  836. if err != nil {
  837. g.Log().Error(err)
  838. return myerrors.DbError("查询任务数据失败")
  839. }
  840. if entity.Id <= 0 {
  841. return myerrors.TipsError("任务数据不存在")
  842. }
  843. // 已完成(30)的任务不能作废
  844. if !s.canCancel(entity.TaskStatus) {
  845. return myerrors.TipsError("已完成的任务不能作废")
  846. }
  847. // 构造更新数据
  848. data := g.Map{
  849. s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCancelled, // 作废
  850. }
  851. // 补齐审计字段
  852. service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
  853. // 使用事务
  854. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  855. // 1. 更新任务状态
  856. _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
  857. if err != nil {
  858. g.Log().Error(err)
  859. return myerrors.DbError("作废任务失败")
  860. }
  861. // 2. 创建过程记录
  862. recordData := g.Map{
  863. s.RecordDao.Columns.TaskId: req.Id,
  864. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  865. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  866. s.RecordDao.Columns.HandleContent: "作废任务<br/>作废原因: " + req.Remark,
  867. }
  868. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  869. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  870. if err != nil {
  871. g.Log().Error(err)
  872. return myerrors.DbError("新增过程记录失败")
  873. }
  874. return nil
  875. })
  876. }
  877. // GetRecords 获取任务过程记录列表(包含附件)
  878. func (s *OpsEventTaskService) GetRecords(req *opsdevmodel.OpsEventTaskRecordSearchReq) ([]*opsdevmodel.OpsEventTaskRecordWithAttachments, error) {
  879. var records []*opsdevmodel.OpsEventTaskRecord
  880. err := s.RecordDao.Where(s.RecordDao.Columns.TaskId, req.TaskId).
  881. Order(s.RecordDao.Columns.CreatedTime + " desc").
  882. Scan(&records)
  883. if err != nil {
  884. g.Log().Error(err)
  885. return nil, myerrors.DbError("查询过程记录失败")
  886. }
  887. result := make([]*opsdevmodel.OpsEventTaskRecordWithAttachments, 0, len(records))
  888. for _, record := range records {
  889. recordRsp := &opsdevmodel.OpsEventTaskRecordWithAttachments{
  890. OpsEventTaskRecord: *record,
  891. Attachments: []*opsdevmodel.OpsEventTaskAttachment{},
  892. }
  893. // 查询该记录关联的附件
  894. if record.Id > 0 {
  895. var attachments []*opsdevmodel.OpsEventTaskAttachment
  896. err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskRecordId, record.Id).
  897. Scan(&attachments)
  898. if err != nil {
  899. g.Log().Error(err)
  900. } else {
  901. recordRsp.Attachments = attachments
  902. }
  903. }
  904. result = append(result, recordRsp)
  905. }
  906. return result, nil
  907. }
  908. // generateTaskNo 生成任务编号
  909. func (s *OpsEventTaskService) generateTaskNo() string {
  910. // 格式: TSK + 年月日 + 4位序列号
  911. now := gtime.Now()
  912. prefix := "TSK" + now.Format("Ymd")
  913. // 使用数据库序列生成唯一序号(按天重置)
  914. seqVal, err := s.TaskDao.DB.GetValue("SELECT next_day_reset_val('task_no_seq')")
  915. if err != nil {
  916. // 如果序列不存在或出错,使用备用方案:查询当天最大序号+1
  917. var maxNoResult struct {
  918. TaskNo string
  919. }
  920. err = s.TaskDao.Where(s.TaskDao.Columns.TaskNo+" like ?", prefix+"%").Order(s.TaskDao.Columns.TaskNo + " desc").Fields(s.TaskDao.Columns.TaskNo).Scan(&maxNoResult)
  921. if err != nil || maxNoResult.TaskNo == "" {
  922. return prefix + "0001"
  923. }
  924. maxNoStr := maxNoResult.TaskNo
  925. if len(maxNoStr) >= len(prefix)+4 {
  926. seq := maxNoStr[len(prefix):]
  927. seqNum := gconv.Int(seq)
  928. seqNum++
  929. return prefix + fmt.Sprintf("%04d", seqNum)
  930. }
  931. return prefix + "0001"
  932. }
  933. return prefix + fmt.Sprintf("%04d", seqVal.Int())
  934. }
  935. // 状态校验辅助方法
  936. func (s *OpsEventTaskService) canEdit(status string) bool {
  937. return status != opsdevmodel.TaskStatusCompleted && status != opsdevmodel.TaskStatusCancelled
  938. }
  939. func (s *OpsEventTaskService) canSchedule(status string) bool {
  940. return status == opsdevmodel.TaskStatusTodo
  941. }
  942. func (s *OpsEventTaskService) canStart(status string) bool {
  943. return status == opsdevmodel.TaskStatusTodo || status == opsdevmodel.TaskStatusPaused || status == opsdevmodel.TaskStatusBlocked
  944. }
  945. func (s *OpsEventTaskService) canComplete(status string) bool {
  946. return status == opsdevmodel.TaskStatusProcessing
  947. }
  948. func (s *OpsEventTaskService) canPause(status string) bool {
  949. return status == opsdevmodel.TaskStatusProcessing
  950. }
  951. func (s *OpsEventTaskService) canBlock(status string) bool {
  952. return status == opsdevmodel.TaskStatusProcessing || status == opsdevmodel.TaskStatusPaused
  953. }
  954. // shouldAutoCompleteEvent 判断当前任务完成时是否应自动完成关联的交付事件
  955. func (s *OpsEventTaskService) shouldAutoCompleteEvent(taskType, testResult string) bool {
  956. switch taskType {
  957. case "10": // 需求评审 — 完成即触发
  958. return true
  959. case "30": // 功能测试 — 仅通过时触发
  960. return testResult == "pass"
  961. case "38": // 系统发版(事件关联) — 完成即触发
  962. return true
  963. default:
  964. return false
  965. }
  966. }
  967. func (s *OpsEventTaskService) canCancel(status string) bool {
  968. return status != opsdevmodel.TaskStatusCompleted
  969. }
  970. // getProjectName 根据项目ID获取项目名称
  971. func (s *OpsEventTaskService) getProjectName(projectId int) string {
  972. if projectId <= 0 {
  973. return ""
  974. }
  975. var project opsdevmodel.OpsDeliveryProject
  976. err := s.ProjectDao.FieldsEx(s.ProjectDao.Columns.DeletedTime).
  977. WherePri(s.ProjectDao.Columns.Id, projectId).
  978. Scan(&project)
  979. if err != nil {
  980. g.Log().Error(err)
  981. return ""
  982. }
  983. return project.ProjectName
  984. }
  985. // GetAttachments 获取任务附件列表
  986. func (s *OpsEventTaskService) GetAttachments(taskId int) ([]*opsdevmodel.OpsEventTaskAttachment, error) {
  987. var attachments []*opsdevmodel.OpsEventTaskAttachment
  988. err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskId, taskId).
  989. Order(s.AttachmentDao.Columns.CreatedTime + " desc").
  990. Scan(&attachments)
  991. if err != nil {
  992. g.Log().Error(err)
  993. return nil, myerrors.DbError("查询附件失败")
  994. }
  995. return attachments, nil
  996. }
  997. // GetTaskReleaseList 根据发版任务ID查询关联的开发任务列表
  998. func (s *OpsEventTaskService) GetTaskReleaseList(req *opsdevmodel.OpsEventTaskReleaseListReq) ([]*opsdevmodel.OpsEventTaskReleaseRsp, error) {
  999. // 1. 查询关联表获取关联的任务ID列表
  1000. var releaseList []*opsdevmodel.OpsEventTaskRelease
  1001. err := s.ReleaseDao.Where(s.ReleaseDao.Columns.ReleaseTaskId, req.ReleaseTaskId).
  1002. Scan(&releaseList)
  1003. if err != nil {
  1004. g.Log().Error(err)
  1005. return nil, myerrors.DbError("查询发布版本关联记录失败")
  1006. }
  1007. if len(releaseList) == 0 {
  1008. return []*opsdevmodel.OpsEventTaskReleaseRsp{}, nil
  1009. }
  1010. // 2. 提取开发任务ID列表
  1011. devTaskIds := make([]int, 0, len(releaseList))
  1012. for _, r := range releaseList {
  1013. devTaskIds = append(devTaskIds, r.DevTaskId)
  1014. }
  1015. // 3. 查询任务详情
  1016. var taskList []*opsdevmodel.OpsEventTask
  1017. err = s.TaskDao.WhereIn(s.TaskDao.Columns.Id, devTaskIds).
  1018. Scan(&taskList)
  1019. if err != nil {
  1020. g.Log().Error(err)
  1021. return nil, myerrors.DbError("查询关联任务详情失败")
  1022. }
  1023. // 4. 构建响应数据
  1024. result := make([]*opsdevmodel.OpsEventTaskReleaseRsp, 0, len(taskList))
  1025. for _, task := range taskList {
  1026. result = append(result, &opsdevmodel.OpsEventTaskReleaseRsp{
  1027. Id: task.Id,
  1028. DevTaskId: task.Id,
  1029. TaskNo: task.TaskNo,
  1030. TaskTitle: task.TaskTitle,
  1031. TaskType: task.TaskType,
  1032. TaskStatus: task.TaskStatus,
  1033. OpsUserName: task.OpsUserName,
  1034. ProjectId: task.ProjectId,
  1035. CreatedTime: task.CreatedTime.Format("Y-m-d H:i:s"),
  1036. })
  1037. }
  1038. return result, nil
  1039. }
  1040. // AddRecord 添加任务过程记录
  1041. func (s *OpsEventTaskService) AddRecord(req *opsdevmodel.OpsEventTaskRecordAddReq) error {
  1042. // 构造记录数据
  1043. recordData := g.Map{
  1044. s.RecordDao.Columns.TaskId: req.TaskId,
  1045. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  1046. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  1047. s.RecordDao.Columns.HandleContent: req.HandleContent,
  1048. }
  1049. // 补齐审计字段
  1050. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  1051. // 使用事务控制
  1052. return s.RecordDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  1053. // 1. 创建过程记录
  1054. result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
  1055. if err != nil {
  1056. g.Log().Error(err)
  1057. return myerrors.DbError("添加过程记录失败")
  1058. }
  1059. // 获取新创建的记录ID
  1060. recordId, err := result.LastInsertId()
  1061. if err != nil {
  1062. g.Log().Error(err)
  1063. return myerrors.DbError("获取记录ID失败")
  1064. }
  1065. // 2. 如果有附件,保存附件
  1066. if len(req.Attachments) > 0 {
  1067. for _, att := range req.Attachments {
  1068. attData := g.Map{
  1069. s.AttachmentDao.Columns.TaskId: req.TaskId,
  1070. s.AttachmentDao.Columns.TaskRecordId: int(recordId),
  1071. s.AttachmentDao.Columns.FileName: att.FileName,
  1072. s.AttachmentDao.Columns.FileUrl: att.FileUrl,
  1073. s.AttachmentDao.Columns.FileType: att.FileType,
  1074. }
  1075. service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
  1076. _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
  1077. if err != nil {
  1078. g.Log().Error(err)
  1079. return myerrors.DbError("保存附件失败")
  1080. }
  1081. }
  1082. }
  1083. return nil
  1084. })
  1085. }
  1086. // AddWorkHour 添加工时登记(累加actual_work_hour并记录过程)
  1087. func (s *OpsEventTaskService) AddWorkHour(req *opsdevmodel.OpsEventTaskWorkHourAddReq) error {
  1088. var entity opsdevmodel.OpsEventTask
  1089. err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.TaskId).Scan(&entity)
  1090. if err != nil {
  1091. g.Log().Error(err)
  1092. return myerrors.DbError("查询任务数据失败")
  1093. }
  1094. if entity.Id <= 0 {
  1095. return myerrors.TipsError("任务数据不存在")
  1096. }
  1097. if !s.canAddWorkHour(entity.TaskStatus) {
  1098. return myerrors.TipsError("只有处理中的任务可以登记工时")
  1099. }
  1100. return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
  1101. workHourData := g.Map{
  1102. s.WorkHourDao.Columns.TaskId: req.TaskId,
  1103. s.WorkHourDao.Columns.OpsUserId: s.GetCxtUserId(),
  1104. s.WorkHourDao.Columns.OpsUserName: s.GetCxtUserName(),
  1105. s.WorkHourDao.Columns.ActualWorkDate: req.WorkDate,
  1106. s.WorkHourDao.Columns.ActualWorkHour: req.ActualHour,
  1107. s.WorkHourDao.Columns.Remark: req.Remark,
  1108. }
  1109. service.SetCreatedInfo(workHourData, s.GetCxtUserId(), s.GetCxtUserName())
  1110. _, err := s.WorkHourDao.TX(tx).Data(workHourData).Insert()
  1111. if err != nil {
  1112. g.Log().Error(err)
  1113. return myerrors.DbError("工时登记失败")
  1114. }
  1115. newActualWorkHour := entity.ActualWorkHour + req.ActualHour
  1116. updateData := g.Map{
  1117. s.TaskDao.Columns.ActualWorkHour: newActualWorkHour,
  1118. }
  1119. service.SetUpdatedInfo(updateData, s.GetCxtUserId(), s.GetCxtUserName())
  1120. _, err = s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(updateData).WherePri(s.TaskDao.Columns.Id, req.TaskId).Update()
  1121. if err != nil {
  1122. g.Log().Error(err)
  1123. return myerrors.DbError("更新实际工时失败")
  1124. }
  1125. handleContent := fmt.Sprintf("工时登记<br/>工作日期: %s<br/>实际工时: %s小时<br/>工作进展: %s",
  1126. req.WorkDate, gconv.String(req.ActualHour), req.Remark)
  1127. recordData := g.Map{
  1128. s.RecordDao.Columns.TaskId: req.TaskId,
  1129. s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
  1130. s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
  1131. s.RecordDao.Columns.HandleContent: handleContent,
  1132. }
  1133. service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
  1134. _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
  1135. if err != nil {
  1136. g.Log().Error(err)
  1137. return myerrors.DbError("新增过程记录失败")
  1138. }
  1139. return nil
  1140. })
  1141. }
  1142. // GetWorkHourList 获取工时登记列表
  1143. func (s *OpsEventTaskService) GetWorkHourList(req *opsdevmodel.OpsEventTaskWorkHourListReq) ([]*opsdevmodel.OpsEventTaskWorkHourRsp, error) {
  1144. var entities []*opsdevmodel.OpsEventTaskWorkHour
  1145. err := s.WorkHourDao.FieldsEx(s.WorkHourDao.Columns.DeletedTime).
  1146. Where(s.WorkHourDao.Columns.TaskId, req.TaskId).
  1147. Order(s.WorkHourDao.Columns.CreatedTime + " desc").
  1148. Scan(&entities)
  1149. if err != nil {
  1150. g.Log().Error(err)
  1151. return nil, myerrors.DbError("查询工时登记列表失败")
  1152. }
  1153. list := make([]*opsdevmodel.OpsEventTaskWorkHourRsp, 0, len(entities))
  1154. for _, entity := range entities {
  1155. workDate := ""
  1156. if entity.ActualWorkDate != nil {
  1157. workDate = entity.ActualWorkDate.Format("Y-m-d")
  1158. }
  1159. createdTime := ""
  1160. if entity.CreatedTime != nil {
  1161. createdTime = entity.CreatedTime.Format("Y-m-d H:i:s")
  1162. }
  1163. list = append(list, &opsdevmodel.OpsEventTaskWorkHourRsp{
  1164. Id: entity.Id,
  1165. TaskId: entity.TaskId,
  1166. WorkDate: workDate,
  1167. ActualHour: entity.ActualWorkHour,
  1168. Remark: entity.Remark,
  1169. CreatedName: entity.CreatedName,
  1170. CreatedTime: createdTime,
  1171. })
  1172. }
  1173. return list, nil
  1174. }
  1175. // GetDashboardData 获取工作台看板数据(周视图)
  1176. func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsdevmodel.OpsEventTaskWorkHourDashboardRsp, error) {
  1177. type workHourRow struct {
  1178. TaskId int `json:"taskId"`
  1179. ActualWorkDate string `json:"actualWorkDate"`
  1180. ActualWorkHour float64 `json:"actualWorkHour"`
  1181. OpsUserId int `json:"opsUserId"`
  1182. TaskNo string `json:"taskNo"`
  1183. TaskTitle string `json:"taskTitle"`
  1184. TaskType string `json:"taskType"`
  1185. TaskStatus string `json:"taskStatus"`
  1186. Priority string `json:"priority"`
  1187. ProjectName string `json:"projectName"`
  1188. TaskActualHour float64 `json:"taskActualHour"`
  1189. EstimateHour float64 `json:"estimateHour"`
  1190. PlanEndTime string `json:"planEndTime"`
  1191. }
  1192. userId := s.GetCxtUserId()
  1193. db := g.DB(s.Tenant)
  1194. sql := `
  1195. SELECT
  1196. wh.task_id, DATE(wh.actual_work_date) AS actual_work_date, wh.actual_work_hour, wh.ops_user_id,
  1197. t.id, t.task_no, t.task_title, t.task_type, t.task_status,
  1198. t.priority, t.project_name, t.actual_work_hour AS task_actual_hour,
  1199. t.estimate_work_hour, IFNULL(t.plan_end_time, '') AS plan_end_time
  1200. FROM ops_event_task_work_hour wh
  1201. LEFT JOIN ops_event_task t ON wh.task_id = t.id AND t.deleted_time IS NULL
  1202. WHERE wh.actual_work_date >= ? AND wh.actual_work_date <= ?
  1203. AND wh.ops_user_id = ?
  1204. AND wh.deleted_time IS NULL
  1205. ORDER BY wh.task_id, wh.actual_work_date
  1206. `
  1207. var rows []workHourRow
  1208. err := db.GetScan(&rows, sql, startDate, endDate+" 23:59:59", userId)
  1209. if err != nil {
  1210. g.Log().Error(err)
  1211. return nil, myerrors.DbError("查询工时数据失败")
  1212. }
  1213. type dayAcc struct {
  1214. totalHours float64
  1215. taskMap map[int]*opsdevmodel.DashboardTaskRsp
  1216. }
  1217. dayMap := make(map[string]*dayAcc, 7)
  1218. seenTasks := make(map[int]bool)
  1219. for i := range rows {
  1220. row := &rows[i]
  1221. date := row.ActualWorkDate
  1222. if _, ok := dayMap[date]; !ok {
  1223. dayMap[date] = &dayAcc{taskMap: make(map[int]*opsdevmodel.DashboardTaskRsp)}
  1224. }
  1225. acc := dayMap[date]
  1226. acc.totalHours += row.ActualWorkHour
  1227. if _, ok := seenTasks[row.TaskId]; !ok {
  1228. seenTasks[row.TaskId] = true
  1229. }
  1230. if _, ok := acc.taskMap[row.TaskId]; !ok {
  1231. planEndTime := ""
  1232. if len(row.PlanEndTime) >= 10 {
  1233. planEndTime = row.PlanEndTime[:10]
  1234. }
  1235. acc.taskMap[row.TaskId] = &opsdevmodel.DashboardTaskRsp{
  1236. Id: row.TaskId,
  1237. TaskNo: row.TaskNo,
  1238. TaskTitle: row.TaskTitle,
  1239. TaskType: row.TaskType,
  1240. TaskStatus: row.TaskStatus,
  1241. Priority: row.Priority,
  1242. ProjectName: row.ProjectName,
  1243. ActualWorkHour: row.TaskActualHour,
  1244. EstimateWorkHour: row.EstimateHour,
  1245. PlanEndTime: planEndTime,
  1246. }
  1247. }
  1248. }
  1249. userName := s.GetCxtUserName()
  1250. overdueCount, err := db.GetValue(
  1251. "SELECT COUNT(1) FROM ops_event_task WHERE plan_end_time < NOW() AND ops_user_name = ? AND task_status NOT IN (?,?) AND deleted_time IS NULL",
  1252. userName,
  1253. opsdevmodel.TaskStatusCompleted, opsdevmodel.TaskStatusCancelled,
  1254. )
  1255. if err != nil {
  1256. g.Log().Error(err)
  1257. return nil, myerrors.DbError("查询超期任务数失败")
  1258. }
  1259. var weekTotal float64
  1260. days := make([]*opsdevmodel.DashboardDayRsp, 0, 7)
  1261. for i := 0; i < 7; i++ {
  1262. date := startDate
  1263. if i > 0 {
  1264. d := gtime.New(startDate).AddDate(0, 0, i)
  1265. date = d.Format("Y-m-d")
  1266. }
  1267. targetHours := 8.0
  1268. if i >= 5 {
  1269. targetHours = 0
  1270. }
  1271. totalHours := 0.0
  1272. var tasks []*opsdevmodel.DashboardTaskRsp
  1273. if acc, ok := dayMap[date]; ok {
  1274. totalHours = acc.totalHours
  1275. for _, t := range acc.taskMap {
  1276. tasks = append(tasks, t)
  1277. }
  1278. }
  1279. weekTotal += totalHours
  1280. days = append(days, &opsdevmodel.DashboardDayRsp{
  1281. Date: date,
  1282. TotalHours: totalHours,
  1283. TargetHours: targetHours,
  1284. Tasks: tasks,
  1285. })
  1286. }
  1287. weekTargetHours := 40.0
  1288. return &opsdevmodel.OpsEventTaskWorkHourDashboardRsp{
  1289. WeekTargetHours: weekTargetHours,
  1290. WeekTotalHours: weekTotal,
  1291. OverdueCount: overdueCount.Int(),
  1292. Days: days,
  1293. }, nil
  1294. }
  1295. // canAddWorkHour 检查是否可以登记工时(仅处理中状态)
  1296. func (s *OpsEventTaskService) canAddWorkHour(status string) bool {
  1297. return status == opsdevmodel.TaskStatusProcessing
  1298. }