| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672 |
- package opsdev
- import (
- "context"
- "fmt"
- "math"
- "strings"
- "dashoo.cn/opms_libary/myerrors"
- opsdevdao "dashoo.cn/opms_parent/app/dao/opsdev"
- opsdevmodel "dashoo.cn/opms_parent/app/model/opsdev"
- "dashoo.cn/opms_parent/app/service"
- "github.com/gogf/gf/database/gdb"
- "github.com/gogf/gf/frame/g"
- "github.com/gogf/gf/os/gtime"
- "github.com/gogf/gf/util/gconv"
- )
- // OpsEventTaskService 任务业务逻辑实现类
- type OpsEventTaskService struct {
- *service.ContextService
- TaskDao *opsdevdao.OpsEventTaskDao
- RecordDao *opsdevdao.OpsEventTaskRecordDao
- AttachmentDao *opsdevdao.OpsEventTaskAttachmentDao
- ProjectDao *opsdevdao.OpsDeliveryProjectDao
- ReleaseDao *opsdevdao.OpsEventTaskReleaseDao
- WorkHourDao *opsdevdao.OpsEventTaskWorkHourDao
- EventDao *opsdevdao.OpsDeliveryProjectEventDao
- EventRecordDao *opsdevdao.OpsDeliveryProjectEventRecordDao
- }
- // NewOpsEventTaskService 初始化service
- func NewOpsEventTaskService(ctx context.Context) (svc *OpsEventTaskService, err error) {
- svc = new(OpsEventTaskService)
- if svc.ContextService, err = svc.Init(ctx); err != nil {
- return nil, err
- }
- svc.TaskDao = opsdevdao.NewOpsEventTaskDao(svc.Tenant)
- svc.RecordDao = opsdevdao.NewOpsEventTaskRecordDao(svc.Tenant)
- svc.AttachmentDao = opsdevdao.NewOpsEventTaskAttachmentDao(svc.Tenant)
- svc.ProjectDao = opsdevdao.NewOpsDeliveryProjectDao(svc.Tenant)
- svc.ReleaseDao = opsdevdao.NewOpsEventTaskReleaseDao(svc.Tenant)
- svc.WorkHourDao = opsdevdao.NewOpsEventTaskWorkHourDao(svc.Tenant)
- svc.EventDao = opsdevdao.NewOpsDeliveryProjectEventDao(svc.Tenant)
- svc.EventRecordDao = opsdevdao.NewOpsDeliveryProjectEventRecordDao(svc.Tenant)
- return svc, nil
- }
- // GetList 分页查询任务列表
- func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (total int, list []*opsdevmodel.OpsEventTaskRsp, err error) {
- db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime)
- // 项目ID筛选(单选)
- if req.ProjectId > 0 {
- db = db.Where(s.TaskDao.Columns.ProjectId, req.ProjectId)
- }
- // 项目ID列表筛选(多选,优先于单选)
- if len(req.ProjectIds) > 0 {
- db = db.Where(s.TaskDao.Columns.ProjectId+" in (?)", req.ProjectIds)
- }
- // 关联事件ID筛选
- if req.EventId > 0 {
- db = db.Where(s.TaskDao.Columns.EventId, req.EventId)
- }
- // 任务标题模糊查询
- if req.TaskTitle != "" {
- db = db.Where(s.TaskDao.Columns.TaskTitle+" like ?", "%"+req.TaskTitle+"%")
- }
- // 任务类型筛选(多选)
- if len(req.TaskType) > 0 {
- db = db.Where(s.TaskDao.Columns.TaskType+" in (?)", req.TaskType)
- }
- // 任务状态筛选(多选)
- if len(req.TaskStatus) > 0 {
- db = db.Where(s.TaskDao.Columns.TaskStatus+" in (?)", req.TaskStatus)
- }
- // 优先级筛选(多选)
- if len(req.Priority) > 0 {
- db = db.Where(s.TaskDao.Columns.Priority+" in (?)", req.Priority)
- }
- // 执行人筛选(多选)
- if len(req.OpsUserName) > 0 {
- db = db.Where(s.TaskDao.Columns.OpsUserName+" in (?)", req.OpsUserName)
- }
- // 发布版本为空筛选(用于发版任务选择未发版任务)
- if req.ReleaseVersionEmpty {
- db = db.Where(s.TaskDao.Columns.ReleaseVersion + " is null or " + s.TaskDao.Columns.ReleaseVersion + " = ''")
- }
- // 排期状态筛选(plan_start_time/plan_end_time 为 datetime 类型,需用 IS NULL)
- if req.ScheduleStatus == "scheduled" {
- db = db.Where(s.TaskDao.Columns.PlanStartTime + " is not null").Where(s.TaskDao.Columns.PlanEndTime + " is not null")
- } else if req.ScheduleStatus == "unscheduled" {
- db = db.Where(s.TaskDao.Columns.PlanStartTime + " is null or " + s.TaskDao.Columns.PlanEndTime + " is null")
- }
- // 计划结束日期范围筛选
- if req.PlanEndDateStart != "" {
- db = db.Where(s.TaskDao.Columns.PlanEndTime+" >= ?", req.PlanEndDateStart+" 00:00:00")
- }
- if req.PlanEndDateEnd != "" {
- db = db.Where(s.TaskDao.Columns.PlanEndTime+" <= ?", req.PlanEndDateEnd+" 23:59:59")
- }
- // 创建日期范围筛选
- if req.CreatedTimeStart != "" {
- db = db.Where(s.TaskDao.Columns.CreatedTime+" >= ?", req.CreatedTimeStart+" 00:00:00")
- }
- if req.CreatedTimeEnd != "" {
- db = db.Where(s.TaskDao.Columns.CreatedTime+" <= ?", req.CreatedTimeEnd+" 23:59:59")
- }
- // 完成日期范围筛选
- if req.CompleteTimeStart != "" {
- db = db.Where(s.TaskDao.Columns.CompleteTime+" >= ?", req.CompleteTimeStart+" 00:00:00")
- }
- if req.CompleteTimeEnd != "" {
- db = db.Where(s.TaskDao.Columns.CompleteTime+" <= ?", req.CompleteTimeEnd+" 23:59:59")
- }
- // 统计总数
- total, err = db.Count()
- if err != nil {
- g.Log().Error(err)
- return 0, nil, myerrors.DbError("获取任务总数失败")
- }
- // 分页查询
- pageNum, pageSize := req.GetPage()
- var entityList []*opsdevmodel.OpsEventTask
- // 处理排序
- if len(req.SortFields) > 0 {
- orderClauses := []string{}
- for _, sort := range req.SortFields {
- // 将前端字段名转换为数据库列名
- colName := s.getSortColumnName(sort.Field)
- if colName != "" {
- orderClauses = append(orderClauses, colName+" "+strings.ToUpper(sort.Order))
- }
- }
- if len(orderClauses) > 0 {
- db = db.Order(strings.Join(orderClauses, ", "))
- } else {
- db = db.Order(s.TaskDao.Columns.CreatedTime + " desc")
- }
- } else {
- db = db.Order(s.TaskDao.Columns.CreatedTime + " desc")
- }
- err = db.Page(pageNum, pageSize).Scan(&entityList)
- if err != nil {
- g.Log().Error(err)
- return 0, nil, myerrors.DbError("查询任务列表失败")
- }
- // 转换为响应结构体
- if err = gconv.Structs(entityList, &list); err != nil {
- g.Log().Error(err)
- return 0, nil, myerrors.DbError("数据转换失败")
- }
- return
- }
- // getSortColumnName 将前端排序字段名转换为数据库列名
- func (s *OpsEventTaskService) getSortColumnName(field string) string {
- // 前端字段名到数据库列名的映射
- fieldMap := map[string]string{
- "taskTitle": s.TaskDao.Columns.TaskTitle,
- "functionName": s.TaskDao.Columns.FunctionName,
- "taskType": s.TaskDao.Columns.TaskType,
- "taskStatus": s.TaskDao.Columns.TaskStatus,
- "priority": s.TaskDao.Columns.Priority,
- "opsUserName": s.TaskDao.Columns.OpsUserName,
- "planStartTime": s.TaskDao.Columns.PlanStartTime,
- "planEndTime": s.TaskDao.Columns.PlanEndTime,
- "completeTime": s.TaskDao.Columns.CompleteTime,
- "releaseVersion": s.TaskDao.Columns.ReleaseVersion,
- "createdTime": s.TaskDao.Columns.CreatedTime,
- }
- if colName, ok := fieldMap[field]; ok {
- return colName
- }
- return ""
- }
- const maxTaskNoRetries = 3
- // isDuplicateEntryError 判断是否为数据库唯一键冲突
- func isDuplicateEntryError(err error) bool {
- if err == nil {
- return false
- }
- return strings.Contains(strings.ToLower(err.Error()), "duplicate entry")
- }
- // Create 新增任务,包含事务控制和过程记录
- func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) (err error) {
- // 判断任务状态:如果执行人、计划开始时间、计划结束时间都填写了,则状态为处理中,否则为待处理
- taskStatus := opsdevmodel.TaskStatusTodo
- if req.OpsUserId > 0 && req.PlanStartTime != "" && req.PlanEndTime != "" {
- taskStatus = opsdevmodel.TaskStatusProcessing
- }
- for i := 0; i < maxTaskNoRetries; i++ {
- err = s.doCreate(req, taskStatus)
- if err == nil {
- return nil
- }
- if !isDuplicateEntryError(err) {
- return err
- }
- }
- return myerrors.DbError("任务编号生成冲突,请稍后重试")
- }
- // doCreate 执行新增任务(可在任务编号冲突时重试)
- func (s *OpsEventTaskService) doCreate(req *opsdevmodel.OpsEventTaskAddReq, taskStatus string) error {
- // 生成任务编号
- taskNo := s.generateTaskNo()
- // 构造数据
- data := g.Map{
- s.TaskDao.Columns.TaskNo: taskNo,
- s.TaskDao.Columns.ProjectId: req.ProjectId,
- s.TaskDao.Columns.ProjectName: s.getProjectName(req.ProjectId),
- s.TaskDao.Columns.TaskTitle: req.TaskTitle,
- s.TaskDao.Columns.TaskDesc: req.TaskDesc,
- s.TaskDao.Columns.FunctionName: req.FunctionName,
- s.TaskDao.Columns.TaskType: req.TaskType,
- s.TaskDao.Columns.TaskStatus: taskStatus,
- s.TaskDao.Columns.Priority: req.Priority,
- s.TaskDao.Columns.OpsUserId: req.OpsUserId,
- s.TaskDao.Columns.OpsUserName: req.OpsUserName,
- s.TaskDao.Columns.EstimateWorkHour: req.EstimateWorkHour,
- s.TaskDao.Columns.DefectType: req.DefectType,
- s.TaskDao.Columns.ReleaseVersion: req.ReleaseVersion,
- s.TaskDao.Columns.Remark: req.Remark,
- s.TaskDao.Columns.EventId: req.EventId,
- s.TaskDao.Columns.EventType: req.EventType,
- s.TaskDao.Columns.TaskParentId: req.TaskParentId,
- s.TaskDao.Columns.Attribute2: req.Attribute2,
- }
- if req.PlanStartTime != "" {
- data[s.TaskDao.Columns.PlanStartTime] = req.PlanStartTime
- }
- if req.PlanEndTime != "" {
- data[s.TaskDao.Columns.PlanEndTime] = req.PlanEndTime
- }
- // 补齐审计字段
- service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务控制
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 创建任务记录
- result, err := s.TaskDao.TX(tx).Data(data).Insert()
- if err != nil {
- g.Log().Error(err)
- if isDuplicateEntryError(err) {
- return err
- }
- return myerrors.DbError("新增任务失败")
- }
- // 获取新创建的任务ID
- taskId, err := result.LastInsertId()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("获取任务ID失败")
- }
- // 2. 创建任务过程记录
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: taskId,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "创建任务<br/>任务标题: " + req.TaskTitle,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- recordResult, err := s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增任务过程记录失败")
- }
- // 获取过程记录ID
- recordId, err := recordResult.LastInsertId()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("获取过程记录ID失败")
- }
- // 3. 保存附件到任务附件表,关联过程记录
- if len(req.Attachments) > 0 {
- for _, att := range req.Attachments {
- attData := g.Map{
- s.AttachmentDao.Columns.TaskId: taskId,
- s.AttachmentDao.Columns.TaskRecordId: recordId,
- s.AttachmentDao.Columns.FileName: att.FileName,
- s.AttachmentDao.Columns.FileUrl: att.FileUrl,
- s.AttachmentDao.Columns.FileType: att.FileType,
- }
- service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("保存附件信息失败")
- }
- }
- }
- return nil
- })
- }
- // UpdateById 根据ID更新任务
- func (s *OpsEventTaskService) UpdateById(req *opsdevmodel.OpsEventTaskUpdateReq) error {
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 已完成(30)或已作废(90)状态的任务不允许编辑
- if !s.canEdit(entity.TaskStatus) {
- return myerrors.TipsError("已完成或已作废状态的任务不允许编辑")
- }
- // 构造更新数据 - 只更新传入的非空字段
- data := g.Map{}
- if req.TaskTitle != "" {
- data[s.TaskDao.Columns.TaskTitle] = req.TaskTitle
- }
- if req.TaskDesc != "" {
- data[s.TaskDao.Columns.TaskDesc] = req.TaskDesc
- }
- if req.FunctionName != "" {
- data[s.TaskDao.Columns.FunctionName] = req.FunctionName
- }
- if req.TaskType != "" {
- data[s.TaskDao.Columns.TaskType] = req.TaskType
- }
- if req.Priority != "" {
- data[s.TaskDao.Columns.Priority] = req.Priority
- }
- if req.OpsUserId > 0 {
- data[s.TaskDao.Columns.OpsUserId] = req.OpsUserId
- }
- if req.OpsUserName != "" {
- data[s.TaskDao.Columns.OpsUserName] = req.OpsUserName
- }
- if req.PlanStartTime != "" {
- data[s.TaskDao.Columns.PlanStartTime] = req.PlanStartTime
- }
- if req.PlanEndTime != "" {
- data[s.TaskDao.Columns.PlanEndTime] = req.PlanEndTime
- }
- if req.EstimateWorkHour > 0 {
- data[s.TaskDao.Columns.EstimateWorkHour] = req.EstimateWorkHour
- }
- if req.DefectType != "" {
- data[s.TaskDao.Columns.DefectType] = req.DefectType
- }
- if req.ReleaseVersion != "" {
- data[s.TaskDao.Columns.ReleaseVersion] = req.ReleaseVersion
- }
- if req.Remark != "" {
- data[s.TaskDao.Columns.Remark] = req.Remark
- }
- if req.Attribute2 != "" {
- data[s.TaskDao.Columns.Attribute2] = req.Attribute2
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 更新操作,排除不可改字段
- _, err = s.TaskDao.FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("更新任务失败")
- }
- return nil
- }
- // DeleteByIds 根据ID批量删除
- func (s *OpsEventTaskService) DeleteByIds(ids []int64) error {
- if len(ids) == 0 {
- return myerrors.TipsError("请选择需要删除的任务")
- }
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 删除任务
- _, err := s.TaskDao.TX(tx).WhereIn(s.TaskDao.Columns.Id, ids).Delete()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("删除任务失败")
- }
- // 2. 删除关联的过程记录
- _, err = s.RecordDao.TX(tx).WhereIn(s.RecordDao.Columns.TaskId, ids).Delete()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("删除任务过程记录失败")
- }
- // 3. 删除关联的附件
- _, err = s.AttachmentDao.TX(tx).WhereIn(s.AttachmentDao.Columns.TaskId, ids).Delete()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("删除任务附件失败")
- }
- return nil
- })
- }
- // GetById 根据ID查询单条(关联项目信息)
- func (s *OpsEventTaskService) GetById(id int) (*opsdevmodel.OpsEventTaskRsp, error) {
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return nil, myerrors.TipsError("任务数据不存在")
- }
- var rsp opsdevmodel.OpsEventTaskRsp
- if err := gconv.Struct(entity, &rsp); err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("数据转换失败")
- }
- return &rsp, nil
- }
- // Schedule 任务排期
- func (s *OpsEventTaskService) Schedule(req *opsdevmodel.OpsEventTaskScheduleReq) error {
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 只有待处理状态可以排期
- if !s.canSchedule(entity.TaskStatus) {
- return myerrors.TipsError("只有待处理状态的任务可以排期")
- }
- data := g.Map{
- s.TaskDao.Columns.OpsUserId: req.OpsUserId,
- s.TaskDao.Columns.OpsUserName: req.OpsUserName,
- s.TaskDao.Columns.PlanStartTime: req.PlanStartTime,
- s.TaskDao.Columns.PlanEndTime: req.PlanEndTime,
- s.TaskDao.Columns.EstimateWorkHour: req.EstimateWorkHour,
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusProcessing,
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 更新任务
- _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("任务排期失败")
- }
- // 2. 创建过程记录
- handleContent := "任务排期<br/>执行人: " + req.OpsUserName +
- "<br/>计划开始: " + req.PlanStartTime +
- "<br/>计划结束: " + req.PlanEndTime +
- "<br/>预估工时: " + gconv.String(req.EstimateWorkHour) + "小时"
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.Id,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: handleContent,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- return nil
- })
- }
- // Start 开始任务
- func (s *OpsEventTaskService) Start(req *opsdevmodel.OpsEventTaskStartReq) error {
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 只有待处理(10)或暂停(25)状态可以开始
- if !s.canStart(entity.TaskStatus) {
- return myerrors.TipsError("只有待处理或暂停状态的任务可以开始")
- }
- // 构造更新数据
- data := g.Map{
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusProcessing, // 处理中
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 更新任务状态
- _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("开始任务失败")
- }
- // 2. 创建过程记录
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.Id,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "开始处理任务<br/>任务标题: " + entity.TaskTitle,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- return nil
- })
- }
- // Complete 完成任务
- func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq) (err error) {
- g.Log().Infof("Handler received req: %+v", req)
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err = s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 只有处理中(20)状态可以完成
- if !s.canComplete(entity.TaskStatus) {
- return myerrors.TipsError("只有处理中的任务可以完成")
- }
- for i := 0; i < maxTaskNoRetries; i++ {
- err = s.doComplete(req, &entity)
- if err == nil {
- return nil
- }
- if !isDuplicateEntryError(err) {
- return err
- }
- }
- return myerrors.DbError("任务编号生成冲突,请稍后重试")
- }
- // doComplete 执行完成任务(可在下游任务编号冲突时重试)
- func (s *OpsEventTaskService) doComplete(req *opsdevmodel.OpsEventTaskCompleteReq, entity *opsdevmodel.OpsEventTask) error {
- // 解析完成日期
- completeTime := gtime.Now()
- if req.CompleteDate != "" {
- if parsed := gtime.NewFromStrFormat(req.CompleteDate, "Y-m-d"); parsed != nil {
- completeTime = parsed
- }
- }
- // 构造更新数据
- data := g.Map{
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCompleted, // 已完成
- s.TaskDao.Columns.CompleteTime: completeTime,
- s.TaskDao.Columns.ActualWorkHour: req.ActualWorkHour,
- s.TaskDao.Columns.Remark: req.Remark,
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 预生成下游任务编号(避免事务内查询重复)
- var testTaskNo string
- if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
- testTaskNo = s.generateTaskNo()
- }
- if entity.TaskType == opsdevmodel.TaskTypeBug {
- testTaskNo = s.generateTaskNo()
- }
- // 使用事务
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 从工时登记表汇总真实的累计工时(数据权威来源,避免前端异常值覆盖)
- existingWorkHourTotal := entity.ActualWorkHour
- if totalVal, err := g.DB(s.Tenant).GetValue(
- "SELECT COALESCE(SUM(actual_work_hour), 0) FROM ops_event_task_work_hour WHERE task_id = ? AND deleted_time IS NULL",
- req.Id,
- ); err == nil {
- existingWorkHourTotal = totalVal.Float64()
- }
- // 计算实际应写入的工时:不允许用小于真实汇总值的数覆盖
- finalWorkHour := req.ActualWorkHour
- if finalWorkHour < existingWorkHourTotal {
- finalWorkHour = existingWorkHourTotal
- }
- // 更新 data 中的工时字段为修正后的值
- data[s.TaskDao.Columns.ActualWorkHour] = finalWorkHour
- // 2. 更新任务状态与工时
- _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("完成任务失败")
- }
- // 3. 仅在前端额外追加了工时(高于工时表汇总)时,创建差额工时记录
- delta := finalWorkHour - existingWorkHourTotal
- if delta > 0 {
- workHourData := g.Map{
- s.WorkHourDao.Columns.TaskId: req.Id,
- s.WorkHourDao.Columns.OpsUserId: s.GetCxtUserId(),
- s.WorkHourDao.Columns.OpsUserName: s.GetCxtUserName(),
- s.WorkHourDao.Columns.ActualWorkDate: completeTime.Format("Y-m-d"),
- s.WorkHourDao.Columns.ActualWorkHour: delta,
- s.WorkHourDao.Columns.Remark: "完成任务自动登记工时差额",
- }
- service.SetCreatedInfo(workHourData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.WorkHourDao.TX(tx).Data(workHourData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("登记工时差额失败")
- }
- }
- // 4. 创建过程记录
- var handleContent string
- if entity.TaskType == opsdevmodel.TaskTypeFeatureTest && req.TestResult != "" {
- testResultText := "通过"
- if req.TestResult == "fail" {
- testResultText = "不通过"
- }
- handleContent = fmt.Sprintf("测试结果:%s <br/>测试时间:%s", testResultText, gtime.Now().Format("Y-m-d"))
- } else if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete {
- // 系统发版完成记录
- handleContent = "发版完成<br/>任务标题: " + entity.TaskTitle + "<br/>发版工时: " + gconv.String(req.ActualWorkHour) + "小时"
- if req.Remark != "" {
- handleContent += "<br/>发版说明: " + req.Remark
- }
- if len(req.DevTaskIds) > 0 {
- handleContent += "<br/>关联研发任务数: " + gconv.String(len(req.DevTaskIds)) + "个"
- }
- } else {
- // 普通任务完成记录
- handleContent = "完成任务<br/>任务标题: " + entity.TaskTitle + "<br/>实际工作量: " + gconv.String(req.ActualWorkHour) + "小时"
- }
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.Id,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: handleContent,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- recordId, _ := result.LastInsertId()
- // 5. 保存附件
- if len(req.Attachments) > 0 {
- for _, att := range req.Attachments {
- attData := g.Map{
- s.AttachmentDao.Columns.TaskId: req.Id,
- s.AttachmentDao.Columns.TaskRecordId: recordId,
- s.AttachmentDao.Columns.FileName: att.FileName,
- s.AttachmentDao.Columns.FileUrl: att.FileUrl,
- s.AttachmentDao.Columns.FileType: att.FileType,
- }
- service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("保存附件信息失败")
- }
- }
- }
- // 6. 根据任务类型自动创建下游任务
- // 5.1 功能开发完成时,自动创建功能测试任务(原4.2)
- if entity.TaskType == opsdevmodel.TaskTypeFeatureDev {
- testTaskData := g.Map{
- s.TaskDao.Columns.TaskNo: testTaskNo,
- s.TaskDao.Columns.ProjectId: entity.ProjectId,
- s.TaskDao.Columns.ProjectName: entity.ProjectName,
- s.TaskDao.Columns.EventId: entity.EventId,
- s.TaskDao.Columns.EventType: entity.EventType,
- s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
- s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
- s.TaskDao.Columns.FunctionName: entity.FunctionName,
- s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
- s.TaskDao.Columns.Priority: entity.Priority,
- s.TaskDao.Columns.TaskParentId: req.Id,
- }
- service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
- testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
- if err != nil {
- g.Log().Error(err)
- if isDuplicateEntryError(err) {
- return err
- }
- return myerrors.DbError("自动创建功能测试任务失败")
- }
- testTaskId, _ := testResult.LastInsertId()
- // 创建功能测试任务的过程记录
- testRecordData := g.Map{
- s.RecordDao.Columns.TaskId: testTaskId,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由功能开发任务自动创建",
- }
- service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增功能测试任务过程记录失败")
- }
- }
- // 5.2b BUG任务完成时,自动创建功能测试任务
- if entity.TaskType == opsdevmodel.TaskTypeBug {
- testTaskData := g.Map{
- s.TaskDao.Columns.TaskNo: testTaskNo,
- s.TaskDao.Columns.ProjectId: entity.ProjectId,
- s.TaskDao.Columns.ProjectName: entity.ProjectName,
- s.TaskDao.Columns.EventId: entity.EventId,
- s.TaskDao.Columns.EventType: entity.EventType,
- s.TaskDao.Columns.TaskTitle: entity.TaskTitle,
- s.TaskDao.Columns.TaskDesc: entity.TaskDesc,
- s.TaskDao.Columns.FunctionName: entity.FunctionName,
- s.TaskDao.Columns.TaskType: opsdevmodel.TaskTypeFeatureTest, // 功能测试
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusTodo, // 待处理
- s.TaskDao.Columns.Priority: entity.Priority,
- s.TaskDao.Columns.TaskParentId: req.Id,
- }
- if entity.TaskParentId > 0 {
- var parentTask opsdevmodel.OpsEventTask
- err := s.TaskDao.TX(tx).FieldsEx(s.TaskDao.Columns.DeletedTime).
- WherePri(s.TaskDao.Columns.Id, entity.TaskParentId).
- Scan(&parentTask)
- if err == nil && parentTask.Id > 0 {
- testTaskData[s.TaskDao.Columns.OpsUserId] = parentTask.OpsUserId
- testTaskData[s.TaskDao.Columns.OpsUserName] = parentTask.OpsUserName
- }
- }
- service.SetCreatedInfo(testTaskData, s.GetCxtUserId(), s.GetCxtUserName())
- testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
- if err != nil {
- g.Log().Error(err)
- if isDuplicateEntryError(err) {
- return err
- }
- return myerrors.DbError("自动创建功能测试任务失败")
- }
- testTaskId, _ := testResult.LastInsertId()
- // 创建功能测试任务的过程记录
- testRecordData := g.Map{
- s.RecordDao.Columns.TaskId: testTaskId,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "创建功能测试任务<br/>说明: 由BUG任务自动创建",
- }
- service.SetCreatedInfo(testRecordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(testRecordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增功能测试任务过程记录失败")
- }
- }
- // 5.3 系统发版完成时,保存关联的研发任务到ops_event_task_release表
- if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete && len(req.DevTaskIds) > 0 {
- for _, devTaskId := range req.DevTaskIds {
- releaseData := g.Map{
- s.ReleaseDao.Columns.ReleaseTaskId: req.Id,
- s.ReleaseDao.Columns.DevTaskId: devTaskId,
- s.ReleaseDao.Columns.ProjectId: entity.ProjectId,
- }
- service.SetCreatedInfo(releaseData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err := s.ReleaseDao.TX(tx).Data(releaseData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("保存发版关联任务失败")
- }
- }
- }
- // 7. 任务完成时自动更新关联的交付事件状态为完成并生成过程记录
- if entity.EventId > 0 && entity.EventType == opsdevmodel.EventTypeDelivery {
- // 触发事件自动完成的任务类型:10(需求评审)、30(功能测试-通过)、38(系统发版)、40(系统发版/硬件发货)、41(硬件安装)
- if s.shouldAutoCompleteEvent(entity.TaskType, req.TestResult) {
- eventData := g.Map{
- s.EventDao.Columns.DeliveryEventStatus: opsdevmodel.DeliveryEventStatusClosed,
- s.EventDao.Columns.CompleteTime: gtime.Now(),
- }
- service.SetUpdatedInfo(eventData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err := s.EventDao.TX(tx).FieldsEx(service.UpdateFieldEx...).
- Data(eventData).
- WherePri(s.EventDao.Columns.Id, entity.EventId).
- Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("自动更新事件状态失败")
- }
- eventRecordData := g.Map{
- s.EventRecordDao.Columns.DeliveryEventId: entity.EventId,
- s.EventRecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.EventRecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.EventRecordDao.Columns.HandleContent: "研发任务已完成,自动关闭事件<br/>任务编号: " + entity.TaskNo + "<br/>任务标题: " + entity.TaskTitle,
- }
- service.SetCreatedInfo(eventRecordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.EventRecordDao.TX(tx).Data(eventRecordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增事件过程记录失败")
- }
- }
- }
- return nil
- })
- }
- // Pause 暂停任务
- func (s *OpsEventTaskService) Pause(req *opsdevmodel.OpsEventTaskPauseReq) error {
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 只有处理中(20)状态可以暂停
- if !s.canPause(entity.TaskStatus) {
- return myerrors.TipsError("只有处理中的任务可以暂停")
- }
- // 构造更新数据
- data := g.Map{
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusPaused, // 暂停
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 更新任务状态
- _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("暂停任务失败")
- }
- // 2. 创建过程记录
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.Id,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "暂停任务<br/>暂停原因: " + req.Remark,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- return nil
- })
- }
- // Block 阻塞任务
- func (s *OpsEventTaskService) Block(req *opsdevmodel.OpsEventTaskBlockReq) error {
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 只有处理中(20)或暂停(25)状态可以阻塞
- if !s.canBlock(entity.TaskStatus) {
- return myerrors.TipsError("只有处理中或暂停状态的任务可以阻塞")
- }
- // 构造更新数据
- data := g.Map{
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusBlocked, // 阻塞
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 更新任务状态
- _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("阻塞任务失败")
- }
- // 2. 创建过程记录
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.Id,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "阻塞任务<br/>阻塞原因: " + req.Remark,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- return nil
- })
- }
- // Cancel 作废任务
- func (s *OpsEventTaskService) Cancel(req *opsdevmodel.OpsEventTaskCancelReq) error {
- // 校验数据是否存在
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.Id).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- // 已完成(30)的任务不能作废
- if !s.canCancel(entity.TaskStatus) {
- return myerrors.TipsError("已完成的任务不能作废")
- }
- // 构造更新数据
- data := g.Map{
- s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCancelled, // 作废
- }
- // 补齐审计字段
- service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 更新任务状态
- _, err := s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.TaskDao.Columns.Id, req.Id).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("作废任务失败")
- }
- // 2. 创建过程记录
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.Id,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: "作废任务<br/>作废原因: " + req.Remark,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- return nil
- })
- }
- // GetRecords 获取任务过程记录列表(包含附件)
- func (s *OpsEventTaskService) GetRecords(req *opsdevmodel.OpsEventTaskRecordSearchReq) ([]*opsdevmodel.OpsEventTaskRecordWithAttachments, error) {
- var records []*opsdevmodel.OpsEventTaskRecord
- err := s.RecordDao.Where(s.RecordDao.Columns.TaskId, req.TaskId).
- Order(s.RecordDao.Columns.CreatedTime + " desc").
- Scan(&records)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询过程记录失败")
- }
- result := make([]*opsdevmodel.OpsEventTaskRecordWithAttachments, 0, len(records))
- for _, record := range records {
- recordRsp := &opsdevmodel.OpsEventTaskRecordWithAttachments{
- OpsEventTaskRecord: *record,
- Attachments: []*opsdevmodel.OpsEventTaskAttachment{},
- }
- // 查询该记录关联的附件
- if record.Id > 0 {
- var attachments []*opsdevmodel.OpsEventTaskAttachment
- err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskRecordId, record.Id).
- Scan(&attachments)
- if err != nil {
- g.Log().Error(err)
- } else {
- recordRsp.Attachments = attachments
- }
- }
- result = append(result, recordRsp)
- }
- return result, nil
- }
- // generateTaskNo 生成任务编号
- func (s *OpsEventTaskService) generateTaskNo() string {
- // 格式: TSK + 年月日 + 4位序列号
- now := gtime.Now()
- prefix := "TSK" + now.Format("Ymd")
- // 使用数据库序列生成唯一序号(按天重置)
- seqVal, err := s.TaskDao.DB.GetValue("SELECT next_day_reset_val('task_no_seq')")
- if err != nil {
- // 如果序列不存在或出错,使用备用方案:查询当天最大序号+1
- var maxNoResult struct {
- TaskNo string
- }
- err = s.TaskDao.Where(s.TaskDao.Columns.TaskNo+" like ?", prefix+"%").Order(s.TaskDao.Columns.TaskNo + " desc").Fields(s.TaskDao.Columns.TaskNo).Scan(&maxNoResult)
- if err != nil || maxNoResult.TaskNo == "" {
- return prefix + "0001"
- }
- maxNoStr := maxNoResult.TaskNo
- if len(maxNoStr) >= len(prefix)+4 {
- seq := maxNoStr[len(prefix):]
- seqNum := gconv.Int(seq)
- seqNum++
- return prefix + fmt.Sprintf("%04d", seqNum)
- }
- return prefix + "0001"
- }
- return prefix + fmt.Sprintf("%04d", seqVal.Int())
- }
- // 状态校验辅助方法
- func (s *OpsEventTaskService) canEdit(status string) bool {
- return status != opsdevmodel.TaskStatusCompleted && status != opsdevmodel.TaskStatusCancelled
- }
- func (s *OpsEventTaskService) canSchedule(status string) bool {
- return status == opsdevmodel.TaskStatusTodo
- }
- func (s *OpsEventTaskService) canStart(status string) bool {
- return status == opsdevmodel.TaskStatusTodo || status == opsdevmodel.TaskStatusPaused || status == opsdevmodel.TaskStatusBlocked
- }
- func (s *OpsEventTaskService) canComplete(status string) bool {
- return status == opsdevmodel.TaskStatusProcessing
- }
- func (s *OpsEventTaskService) canPause(status string) bool {
- return status == opsdevmodel.TaskStatusProcessing
- }
- func (s *OpsEventTaskService) canBlock(status string) bool {
- return status == opsdevmodel.TaskStatusProcessing || status == opsdevmodel.TaskStatusPaused
- }
- // shouldAutoCompleteEvent 判断当前任务完成时是否应自动完成关联的交付事件
- func (s *OpsEventTaskService) shouldAutoCompleteEvent(taskType, testResult string) bool {
- switch taskType {
- case "10": // 需求评审 — 完成即触发
- return true
- case "30": // 功能测试 — 仅通过时触发
- return testResult == "pass"
- case "38": // 系统发版(事件关联) — 完成即触发
- return true
- default:
- return false
- }
- }
- func (s *OpsEventTaskService) canCancel(status string) bool {
- return status != opsdevmodel.TaskStatusCompleted
- }
- // getProjectName 根据项目ID获取项目名称
- func (s *OpsEventTaskService) getProjectName(projectId int) string {
- if projectId <= 0 {
- return ""
- }
- var project opsdevmodel.OpsDeliveryProject
- err := s.ProjectDao.FieldsEx(s.ProjectDao.Columns.DeletedTime).
- WherePri(s.ProjectDao.Columns.Id, projectId).
- Scan(&project)
- if err != nil {
- g.Log().Error(err)
- return ""
- }
- return project.ProjectName
- }
- // GetAttachments 获取任务附件列表
- func (s *OpsEventTaskService) GetAttachments(taskId int) ([]*opsdevmodel.OpsEventTaskAttachment, error) {
- var attachments []*opsdevmodel.OpsEventTaskAttachment
- err := s.AttachmentDao.Where(s.AttachmentDao.Columns.TaskId, taskId).
- Order(s.AttachmentDao.Columns.CreatedTime + " desc").
- Scan(&attachments)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询附件失败")
- }
- return attachments, nil
- }
- // GetTaskReleaseList 根据发版任务ID查询关联的开发任务列表
- func (s *OpsEventTaskService) GetTaskReleaseList(req *opsdevmodel.OpsEventTaskReleaseListReq) ([]*opsdevmodel.OpsEventTaskReleaseRsp, error) {
- // 1. 查询关联表获取关联的任务ID列表
- var releaseList []*opsdevmodel.OpsEventTaskRelease
- err := s.ReleaseDao.Where(s.ReleaseDao.Columns.ReleaseTaskId, req.ReleaseTaskId).
- Scan(&releaseList)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询发布版本关联记录失败")
- }
- if len(releaseList) == 0 {
- return []*opsdevmodel.OpsEventTaskReleaseRsp{}, nil
- }
- // 2. 提取开发任务ID列表
- devTaskIds := make([]int, 0, len(releaseList))
- for _, r := range releaseList {
- devTaskIds = append(devTaskIds, r.DevTaskId)
- }
- // 3. 查询任务详情
- var taskList []*opsdevmodel.OpsEventTask
- err = s.TaskDao.WhereIn(s.TaskDao.Columns.Id, devTaskIds).
- Scan(&taskList)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询关联任务详情失败")
- }
- // 4. 构建响应数据
- result := make([]*opsdevmodel.OpsEventTaskReleaseRsp, 0, len(taskList))
- for _, task := range taskList {
- result = append(result, &opsdevmodel.OpsEventTaskReleaseRsp{
- Id: task.Id,
- DevTaskId: task.Id,
- TaskNo: task.TaskNo,
- TaskTitle: task.TaskTitle,
- TaskType: task.TaskType,
- TaskStatus: task.TaskStatus,
- OpsUserName: task.OpsUserName,
- ProjectId: task.ProjectId,
- CreatedTime: task.CreatedTime.Format("Y-m-d H:i:s"),
- })
- }
- return result, nil
- }
- // AddRecord 添加任务过程记录
- func (s *OpsEventTaskService) AddRecord(req *opsdevmodel.OpsEventTaskRecordAddReq) error {
- // 构造记录数据
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.TaskId,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: req.HandleContent,
- }
- // 补齐审计字段
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- // 使用事务控制
- return s.RecordDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- // 1. 创建过程记录
- result, err := s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("添加过程记录失败")
- }
- // 获取新创建的记录ID
- recordId, err := result.LastInsertId()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("获取记录ID失败")
- }
- // 2. 如果有附件,保存附件
- if len(req.Attachments) > 0 {
- for _, att := range req.Attachments {
- attData := g.Map{
- s.AttachmentDao.Columns.TaskId: req.TaskId,
- s.AttachmentDao.Columns.TaskRecordId: int(recordId),
- s.AttachmentDao.Columns.FileName: att.FileName,
- s.AttachmentDao.Columns.FileUrl: att.FileUrl,
- s.AttachmentDao.Columns.FileType: att.FileType,
- }
- service.SetCreatedInfo(attData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.AttachmentDao.TX(tx).Data(attData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("保存附件失败")
- }
- }
- }
- return nil
- })
- }
- // AddWorkHour 添加工时登记(累加actual_work_hour并记录过程)
- func (s *OpsEventTaskService) AddWorkHour(req *opsdevmodel.OpsEventTaskWorkHourAddReq) error {
- var entity opsdevmodel.OpsEventTask
- err := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime).WherePri(s.TaskDao.Columns.Id, req.TaskId).Scan(&entity)
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("查询任务数据失败")
- }
- if entity.Id <= 0 {
- return myerrors.TipsError("任务数据不存在")
- }
- if !s.canAddWorkHour(entity.TaskStatus) {
- return myerrors.TipsError("只有处理中的任务可以登记工时")
- }
- return s.TaskDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
- workHourData := g.Map{
- s.WorkHourDao.Columns.TaskId: req.TaskId,
- s.WorkHourDao.Columns.OpsUserId: s.GetCxtUserId(),
- s.WorkHourDao.Columns.OpsUserName: s.GetCxtUserName(),
- s.WorkHourDao.Columns.ActualWorkDate: req.WorkDate,
- s.WorkHourDao.Columns.ActualWorkHour: req.ActualHour,
- s.WorkHourDao.Columns.Remark: req.Remark,
- }
- service.SetCreatedInfo(workHourData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err := s.WorkHourDao.TX(tx).Data(workHourData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("工时登记失败")
- }
- newActualWorkHour := entity.ActualWorkHour + req.ActualHour
- updateData := g.Map{
- s.TaskDao.Columns.ActualWorkHour: newActualWorkHour,
- }
- service.SetUpdatedInfo(updateData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.TaskDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(updateData).WherePri(s.TaskDao.Columns.Id, req.TaskId).Update()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("更新实际工时失败")
- }
- handleContent := fmt.Sprintf("工时登记<br/>工作日期: %s<br/>实际工时: %s小时<br/>工作进展: %s",
- req.WorkDate, gconv.String(req.ActualHour), req.Remark)
- recordData := g.Map{
- s.RecordDao.Columns.TaskId: req.TaskId,
- s.RecordDao.Columns.HandleUserId: s.GetCxtUserId(),
- s.RecordDao.Columns.HandleUserName: s.GetCxtUserName(),
- s.RecordDao.Columns.HandleContent: handleContent,
- }
- service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
- if err != nil {
- g.Log().Error(err)
- return myerrors.DbError("新增过程记录失败")
- }
- return nil
- })
- }
- // GetWorkHourList 获取工时登记列表
- func (s *OpsEventTaskService) GetWorkHourList(req *opsdevmodel.OpsEventTaskWorkHourListReq) ([]*opsdevmodel.OpsEventTaskWorkHourRsp, error) {
- var entities []*opsdevmodel.OpsEventTaskWorkHour
- err := s.WorkHourDao.FieldsEx(s.WorkHourDao.Columns.DeletedTime).
- Where(s.WorkHourDao.Columns.TaskId, req.TaskId).
- Order(s.WorkHourDao.Columns.CreatedTime + " desc").
- Scan(&entities)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询工时登记列表失败")
- }
- list := make([]*opsdevmodel.OpsEventTaskWorkHourRsp, 0, len(entities))
- for _, entity := range entities {
- workDate := ""
- if entity.ActualWorkDate != nil {
- workDate = entity.ActualWorkDate.Format("Y-m-d")
- }
- createdTime := ""
- if entity.CreatedTime != nil {
- createdTime = entity.CreatedTime.Format("Y-m-d H:i:s")
- }
- list = append(list, &opsdevmodel.OpsEventTaskWorkHourRsp{
- Id: entity.Id,
- TaskId: entity.TaskId,
- WorkDate: workDate,
- ActualHour: entity.ActualWorkHour,
- Remark: entity.Remark,
- CreatedName: entity.CreatedName,
- CreatedTime: createdTime,
- })
- }
- return list, nil
- }
- // GetDashboardData 获取工作台看板数据(周视图)
- func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsdevmodel.OpsEventTaskWorkHourDashboardRsp, error) {
- userId := s.GetCxtUserId()
- db := g.DB(s.Tenant)
- type taskRow struct {
- Id int `json:"id"`
- TaskNo string `json:"taskNo"`
- TaskTitle string `json:"taskTitle"`
- TaskType string `json:"taskType"`
- TaskStatus string `json:"taskStatus"`
- Priority string `json:"priority"`
- ProjectName string `json:"projectName"`
- ActualWorkHour float64 `json:"actualWorkHour"`
- EstimateWorkHour float64 `json:"estimateWorkHour"`
- PlanStartDate string `json:"planStartDate"`
- PlanEndTime string `json:"planEndTime"`
- }
- taskSQL := `
- SELECT id, task_no, task_title, task_type, task_status,
- priority, project_name, actual_work_hour, estimate_work_hour,
- DATE(plan_start_time) AS plan_start_date,
- IFNULL(DATE(plan_end_time), '') AS plan_end_time
- FROM ops_event_task
- WHERE ops_user_id = ?
- AND plan_start_time >= ? AND plan_start_time <= ?
- AND deleted_time IS NULL
- ORDER BY plan_start_time, priority
- `
- var taskRows []taskRow
- err := db.GetScan(&taskRows, taskSQL, userId, startDate, endDate+" 23:59:59")
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询任务数据失败")
- }
- type hourRow struct {
- WorkDate string `json:"workDate"`
- TotalHour float64 `json:"totalHour"`
- }
- hourSQL := `
- SELECT DATE(actual_work_date) AS work_date, SUM(actual_work_hour) AS total_hour
- FROM ops_event_task_work_hour
- WHERE ops_user_id = ?
- AND actual_work_date >= ? AND actual_work_date <= ?
- AND deleted_time IS NULL
- GROUP BY DATE(actual_work_date)
- `
- var hourRows []hourRow
- err = db.GetScan(&hourRows, hourSQL, userId, startDate, endDate+" 23:59:59")
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询每日工时失败")
- }
- hourMap := make(map[string]float64, 7)
- for i := range hourRows {
- hourMap[hourRows[i].WorkDate] = hourRows[i].TotalHour
- }
- // 合并会议工时
- meetingSQL := `
- SELECT DATE(work_date) AS work_date, SUM(work_hour) AS total_hour
- FROM plat_meeting_work_hour
- WHERE user_id = ?
- AND work_date >= ? AND work_date <= ?
- AND deleted_time IS NULL
- GROUP BY DATE(work_date)
- `
- var meetingHourRows []hourRow
- if err := db.GetScan(&meetingHourRows, meetingSQL, userId, startDate, endDate+" 23:59:59"); err != nil {
- g.Log().Error(err)
- } else {
- for i := range meetingHourRows {
- hourMap[meetingHourRows[i].WorkDate] += meetingHourRows[i].TotalHour
- }
- }
- taskMap := make(map[string][]*opsdevmodel.DashboardTaskRsp, 7)
- for i := range taskRows {
- row := &taskRows[i]
- date := row.PlanStartDate
- taskMap[date] = append(taskMap[date], &opsdevmodel.DashboardTaskRsp{
- Id: row.Id,
- TaskNo: row.TaskNo,
- TaskTitle: row.TaskTitle,
- TaskType: row.TaskType,
- TaskStatus: row.TaskStatus,
- Priority: row.Priority,
- ProjectName: row.ProjectName,
- ActualWorkHour: row.ActualWorkHour,
- EstimateWorkHour: row.EstimateWorkHour,
- PlanEndTime: row.PlanEndTime,
- })
- }
- overdueCount, err := db.GetValue(
- "SELECT COUNT(1) FROM ops_event_task WHERE plan_end_time < NOW() AND ops_user_id = ? AND task_status NOT IN (?,?) AND deleted_time IS NULL",
- userId,
- opsdevmodel.TaskStatusCompleted, opsdevmodel.TaskStatusCancelled,
- )
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询超期任务数失败")
- }
- var weekTotal float64
- days := make([]*opsdevmodel.DashboardDayRsp, 0, 7)
- for i := 0; i < 7; i++ {
- date := startDate
- if i > 0 {
- d := gtime.New(startDate).AddDate(0, 0, i)
- date = d.Format("Y-m-d")
- }
- targetHours := 8.0
- if i >= 5 {
- targetHours = 0
- }
- totalHours := hourMap[date]
- weekTotal += totalHours
- dayTasks := taskMap[date]
- if dayTasks == nil {
- dayTasks = []*opsdevmodel.DashboardTaskRsp{}
- }
- days = append(days, &opsdevmodel.DashboardDayRsp{
- Date: date,
- TotalHours: totalHours,
- TargetHours: targetHours,
- Tasks: dayTasks,
- })
- }
- weekTargetHours := 40.0
- return &opsdevmodel.OpsEventTaskWorkHourDashboardRsp{
- WeekTargetHours: weekTargetHours,
- WeekTotalHours: weekTotal,
- OverdueCount: overdueCount.Int(),
- Days: days,
- }, nil
- }
- // canAddWorkHour 检查是否可以登记工时(仅处理中状态)
- func (s *OpsEventTaskService) canAddWorkHour(status string) bool {
- return status == opsdevmodel.TaskStatusProcessing
- }
- // scheduleGroupMap 排期统计人员分组映射
- var scheduleGroupMap = map[string]string{
- "徐洲": "Biobank组",
- "贾冀川": "Biobank组",
- "徐凯": "Biobank组",
- "耿嘉强": "Biobank组",
- "范相豪": "Biobank组",
- "刘旗": "Biobank组",
- "周丰林": "LIMS组",
- "刘振林": "LIMS组",
- "张旭伟": "LIMS组",
- "张涵": "LIMS组",
- "王晓宇": "CellSop组",
- "李凯": "CellSop组",
- "王硕": "CellSop组",
- "卢传敏": "BiobankV4",
- "韩明儒": "BiobankV4",
- "石春蕾": "品质部",
- "刘琦": "品质部",
- }
- // GetScheduleStats 获取人员排期统计(按周、按天、按人汇总任务数和预估工时)
- func (s *OpsEventTaskService) GetScheduleStats(req *opsdevmodel.OpsEventTaskScheduleStatReq) (*opsdevmodel.OpsEventTaskScheduleStatRsp, error) {
- type dayRow struct {
- OpsUserId int `json:"opsUserId"`
- OpsUserName string `json:"opsUserName"`
- PlanDate string `json:"planDate"`
- TaskCount int `json:"taskCount"`
- EstimateWorkHour float64 `json:"estimateWorkHour"`
- }
- sql := `
- SELECT
- ops_user_id,
- ops_user_name,
- DATE(plan_start_time) AS plan_date,
- COUNT(*) AS task_count,
- SUM(COALESCE(estimate_work_hour, 0)) AS estimate_work_hour
- FROM ops_event_task
- WHERE deleted_time IS NULL
- AND task_status != ?
- AND plan_start_time >= ?
- AND plan_start_time < ?
- `
- args := []interface{}{opsdevmodel.TaskStatusCancelled, req.WeekStart, req.WeekEnd + " 23:59:59"}
- if req.ProjectId > 0 {
- sql += " AND project_id = ?"
- args = append(args, req.ProjectId)
- }
- allNames := make([]string, 0, len(scheduleGroupMap))
- for name := range scheduleGroupMap {
- allNames = append(allNames, name)
- }
- placeholders := make([]string, len(allNames))
- for i := range placeholders {
- placeholders[i] = "?"
- }
- sql += " AND ops_user_name IN (" + strings.Join(placeholders, ",") + ")"
- for _, name := range allNames {
- args = append(args, name)
- }
- sql += `
- GROUP BY ops_user_id, ops_user_name, DATE(plan_start_time)
- ORDER BY ops_user_name, plan_date
- `
- var rows []dayRow
- err := g.DB(s.Tenant).GetScan(&rows, sql, args...)
- if err != nil {
- g.Log().Error(err)
- return nil, myerrors.DbError("查询排期统计数据失败")
- }
- // 按用户聚合,补全 7 天数据
- userMap := make(map[int]*opsdevmodel.OpsEventTaskUserScheduleStat)
- for _, row := range rows {
- user, ok := userMap[row.OpsUserId]
- if !ok {
- user = &opsdevmodel.OpsEventTaskUserScheduleStat{
- OpsUserId: row.OpsUserId,
- OpsUserName: row.OpsUserName,
- GroupName: scheduleGroupMap[row.OpsUserName],
- DayStats: make([]*opsdevmodel.OpsEventTaskUserDayStat, 7),
- }
- // 初始化 7 天空数据
- weekStart := gtime.New(req.WeekStart)
- for i := 0; i < 7; i++ {
- day := weekStart.AddDate(0, 0, i)
- user.DayStats[i] = &opsdevmodel.OpsEventTaskUserDayStat{
- Date: day.Format("Y-m-d"),
- TaskCount: 0,
- EstimateWorkHour: 0,
- }
- }
- userMap[row.OpsUserId] = user
- }
- // 查找对应 day index
- for _, ds := range user.DayStats {
- if ds.Date == row.PlanDate {
- ds.TaskCount = row.TaskCount
- ds.EstimateWorkHour = row.EstimateWorkHour
- break
- }
- }
- }
- // 计算周合计
- list := make([]*opsdevmodel.OpsEventTaskUserScheduleStat, 0, len(userMap))
- for _, user := range userMap {
- weekTotal := &opsdevmodel.OpsEventTaskUserDayStat{Date: "合计"}
- for _, ds := range user.DayStats {
- weekTotal.TaskCount += ds.TaskCount
- weekTotal.EstimateWorkHour += ds.EstimateWorkHour
- }
- weekTotal.EstimateWorkHour = math.Round(weekTotal.EstimateWorkHour*10) / 10
- user.WeekTotal = weekTotal
- list = append(list, user)
- }
- return &opsdevmodel.OpsEventTaskScheduleStatRsp{List: list}, nil
- }
|