|
|
@@ -18,11 +18,14 @@ import (
|
|
|
// OpsEventTaskService 任务业务逻辑实现类
|
|
|
type OpsEventTaskService struct {
|
|
|
*service.ContextService
|
|
|
- TaskDao *opsdevdao.OpsEventTaskDao
|
|
|
- RecordDao *opsdevdao.OpsEventTaskRecordDao
|
|
|
- AttachmentDao *opsdevdao.OpsEventTaskAttachmentDao
|
|
|
- ProjectDao *opsdevdao.OpsDeliveryProjectDao
|
|
|
- ReleaseDao *opsdevdao.OpsEventTaskReleaseDao
|
|
|
+ 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
|
|
|
@@ -36,6 +39,9 @@ func NewOpsEventTaskService(ctx context.Context) (svc *OpsEventTaskService, err
|
|
|
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
|
|
|
}
|
|
|
|
|
|
@@ -48,6 +54,11 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
|
|
|
db = db.Where(s.TaskDao.Columns.ProjectId, req.ProjectId)
|
|
|
}
|
|
|
|
|
|
+ // 关联事件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+"%")
|
|
|
@@ -68,9 +79,14 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
|
|
|
db = db.Where(s.TaskDao.Columns.Priority+" in (?)", req.Priority)
|
|
|
}
|
|
|
|
|
|
- // 执行人模糊查询
|
|
|
- if req.OpsUserName != "" {
|
|
|
- db = db.Where(s.TaskDao.Columns.OpsUserName+" like ?", "%"+req.OpsUserName+"%")
|
|
|
+ // 执行人筛选(多选)
|
|
|
+ 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")
|
|
|
}
|
|
|
|
|
|
// 计划结束日期范围筛选
|
|
|
@@ -139,22 +155,6 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
|
|
|
return 0, nil, myerrors.DbError("数据转换失败")
|
|
|
}
|
|
|
|
|
|
- // 填充项目名称
|
|
|
- for _, item := range list {
|
|
|
- if item.ProjectId > 0 {
|
|
|
- var project opsdevmodel.OpsDeliveryProject
|
|
|
- err := s.ProjectDao.FieldsEx(s.ProjectDao.Columns.DeletedTime).
|
|
|
- WherePri(s.ProjectDao.Columns.Id, item.ProjectId).
|
|
|
- Scan(&project)
|
|
|
- if err != nil {
|
|
|
- g.Log().Error(err)
|
|
|
- } else if project.Id > 0 {
|
|
|
- item.ProjectName = project.ProjectName
|
|
|
- item.ContractNo = project.ContractNo
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -162,15 +162,17 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
|
|
|
func (s *OpsEventTaskService) getSortColumnName(field string) string {
|
|
|
// 前端字段名到数据库列名的映射
|
|
|
fieldMap := map[string]string{
|
|
|
- "taskTitle": s.TaskDao.Columns.TaskTitle,
|
|
|
- "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,
|
|
|
- "createdTime": s.TaskDao.Columns.CreatedTime,
|
|
|
+ "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 {
|
|
|
@@ -179,14 +181,38 @@ func (s *OpsEventTaskService) getSortColumnName(field string) string {
|
|
|
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) error {
|
|
|
+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()
|
|
|
|
|
|
@@ -194,6 +220,7 @@ func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) error
|
|
|
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,
|
|
|
@@ -209,6 +236,7 @@ func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) error
|
|
|
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 != "" {
|
|
|
@@ -227,6 +255,9 @@ func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) error
|
|
|
result, err := s.TaskDao.TX(tx).Data(data).Insert()
|
|
|
if err != nil {
|
|
|
g.Log().Error(err)
|
|
|
+ if isDuplicateEntryError(err) {
|
|
|
+ return err
|
|
|
+ }
|
|
|
return myerrors.DbError("新增任务失败")
|
|
|
}
|
|
|
|
|
|
@@ -246,14 +277,14 @@ func (s *OpsEventTaskService) Create(req *opsdevmodel.OpsEventTaskAddReq) error
|
|
|
}
|
|
|
service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
|
|
|
|
|
|
- _, err = s.RecordDao.TX(tx).Data(recordData).Insert()
|
|
|
+ recordResult, err := s.RecordDao.TX(tx).Data(recordData).Insert()
|
|
|
if err != nil {
|
|
|
g.Log().Error(err)
|
|
|
return myerrors.DbError("新增任务过程记录失败")
|
|
|
}
|
|
|
|
|
|
// 获取过程记录ID
|
|
|
- recordId, err := result.LastInsertId()
|
|
|
+ recordId, err := recordResult.LastInsertId()
|
|
|
if err != nil {
|
|
|
g.Log().Error(err)
|
|
|
return myerrors.DbError("获取过程记录ID失败")
|
|
|
@@ -301,21 +332,50 @@ func (s *OpsEventTaskService) UpdateById(req *opsdevmodel.OpsEventTaskUpdateReq)
|
|
|
return myerrors.TipsError("已完成或已作废状态的任务不允许编辑")
|
|
|
}
|
|
|
|
|
|
- // 构造更新数据
|
|
|
- data := g.Map{
|
|
|
- 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.Priority: req.Priority,
|
|
|
- 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.DefectType: req.DefectType,
|
|
|
- s.TaskDao.Columns.ReleaseVersion: req.ReleaseVersion,
|
|
|
- s.TaskDao.Columns.Remark: req.Remark,
|
|
|
+ // 构造更新数据 - 只更新传入的非空字段
|
|
|
+ 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
|
|
|
}
|
|
|
|
|
|
// 补齐审计字段
|
|
|
@@ -382,20 +442,6 @@ func (s *OpsEventTaskService) GetById(id int) (*opsdevmodel.OpsEventTaskRsp, err
|
|
|
return nil, myerrors.DbError("数据转换失败")
|
|
|
}
|
|
|
|
|
|
- // 查询关联的项目信息
|
|
|
- if entity.ProjectId > 0 {
|
|
|
- var project opsdevmodel.OpsDeliveryProject
|
|
|
- err := s.ProjectDao.FieldsEx(s.ProjectDao.Columns.DeletedTime).
|
|
|
- WherePri(s.ProjectDao.Columns.Id, entity.ProjectId).
|
|
|
- Scan(&project)
|
|
|
- if err != nil {
|
|
|
- g.Log().Error(err)
|
|
|
- } else if project.Id > 0 {
|
|
|
- rsp.ProjectName = project.ProjectName
|
|
|
- rsp.ContractNo = project.ContractNo
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
return &rsp, nil
|
|
|
}
|
|
|
|
|
|
@@ -516,10 +562,12 @@ func (s *OpsEventTaskService) Start(req *opsdevmodel.OpsEventTaskStartReq) error
|
|
|
}
|
|
|
|
|
|
// Complete 完成任务
|
|
|
-func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq) error {
|
|
|
+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)
|
|
|
+ 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("查询任务数据失败")
|
|
|
@@ -533,6 +581,20 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
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 {
|
|
|
// 构造更新数据
|
|
|
data := g.Map{
|
|
|
s.TaskDao.Columns.TaskStatus: opsdevmodel.TaskStatusCompleted, // 已完成
|
|
|
@@ -544,6 +606,18 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
// 补齐审计字段
|
|
|
service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
|
|
|
|
|
|
+ // 预生成下游任务编号(避免事务内查询重复)
|
|
|
+ var devTaskNo, testTaskNo string
|
|
|
+ if entity.TaskType == opsdevmodel.TaskTypeReqReview {
|
|
|
+ devTaskNo = s.generateTaskNo()
|
|
|
+ }
|
|
|
+ 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. 更新任务状态
|
|
|
@@ -561,7 +635,7 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
testResultText = "不通过"
|
|
|
}
|
|
|
handleContent = fmt.Sprintf("测试结果:%s <br/>测试时间:%s", testResultText, gtime.Now().Format("Y-m-d"))
|
|
|
- } else if entity.TaskType == opsdevmodel.TaskTypeSystemRelease && req.IsReleaseComplete {
|
|
|
+ } else if entity.TaskType == opsdevmodel.TaskTypeSystemReleaseEvt && req.IsReleaseComplete {
|
|
|
// 系统发版完成记录
|
|
|
handleContent = "发版完成<br/>任务标题: " + entity.TaskTitle + "<br/>发版工时: " + gconv.String(req.ActualWorkHour) + "小时"
|
|
|
if req.Remark != "" {
|
|
|
@@ -615,7 +689,9 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
// 4.1 需求评审完成时,自动创建功能开发任务
|
|
|
if entity.TaskType == opsdevmodel.TaskTypeReqReview {
|
|
|
devTaskData := g.Map{
|
|
|
+ s.TaskDao.Columns.TaskNo: devTaskNo,
|
|
|
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,
|
|
|
@@ -631,6 +707,9 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
devResult, err := s.TaskDao.TX(tx).Data(devTaskData).Insert()
|
|
|
if err != nil {
|
|
|
g.Log().Error(err)
|
|
|
+ if isDuplicateEntryError(err) {
|
|
|
+ return err
|
|
|
+ }
|
|
|
return myerrors.DbError("自动创建功能开发任务失败")
|
|
|
}
|
|
|
|
|
|
@@ -655,7 +734,9 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
// 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,
|
|
|
@@ -671,6 +752,9 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
testResult, err := s.TaskDao.TX(tx).Data(testTaskData).Insert()
|
|
|
if err != nil {
|
|
|
g.Log().Error(err)
|
|
|
+ if isDuplicateEntryError(err) {
|
|
|
+ return err
|
|
|
+ }
|
|
|
return myerrors.DbError("自动创建功能测试任务失败")
|
|
|
}
|
|
|
|
|
|
@@ -692,8 +776,53 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 4.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,
|
|
|
+ }
|
|
|
+ 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("新增功能测试任务过程记录失败")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 4.3 系统发版完成时,保存关联的研发任务到ops_event_task_release表
|
|
|
- if entity.TaskType == opsdevmodel.TaskTypeSystemRelease && req.IsReleaseComplete && len(req.DevTaskIds) > 0 {
|
|
|
+ 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,
|
|
|
@@ -710,6 +839,41 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 5. 任务完成时自动更新关联的交付事件状态为完成并生成过程记录
|
|
|
+ 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
|
|
|
})
|
|
|
}
|
|
|
@@ -914,31 +1078,32 @@ func (s *OpsEventTaskService) GetRecords(req *opsdevmodel.OpsEventTaskRecordSear
|
|
|
|
|
|
// generateTaskNo 生成任务编号
|
|
|
func (s *OpsEventTaskService) generateTaskNo() string {
|
|
|
- // 格式: TSK + 年月日 + 4位序号
|
|
|
+ // 格式: TSK + 年月日 + 4位序列号
|
|
|
now := gtime.Now()
|
|
|
prefix := "TSK" + now.Format("Ymd")
|
|
|
|
|
|
- // 查询当天最大序号
|
|
|
- var maxNo string
|
|
|
- err := s.TaskDao.Where(s.TaskDao.Columns.TaskNo+" like ?", prefix+"%").Order(s.TaskDao.Columns.TaskNo + " desc").Fields(s.TaskDao.Columns.TaskNo).Scan(&maxNo)
|
|
|
+ // 使用数据库序列生成唯一序号(按天重置)
|
|
|
+ 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"
|
|
|
}
|
|
|
|
|
|
- if maxNo == "" {
|
|
|
- return prefix + "0001"
|
|
|
- }
|
|
|
-
|
|
|
- // 提取序号并+1
|
|
|
- if len(maxNo) >= len(prefix)+4 {
|
|
|
- seq := maxNo[len(prefix):]
|
|
|
- var seqNum int
|
|
|
- gconv.Struct(seq, &seqNum)
|
|
|
- seqNum++
|
|
|
- return prefix + fmt.Sprintf("%04d", seqNum)
|
|
|
- }
|
|
|
-
|
|
|
- return prefix + "0001"
|
|
|
+ return prefix + fmt.Sprintf("%04d", seqVal.Int())
|
|
|
}
|
|
|
|
|
|
// 状态校验辅助方法
|
|
|
@@ -951,7 +1116,7 @@ func (s *OpsEventTaskService) canSchedule(status string) bool {
|
|
|
}
|
|
|
|
|
|
func (s *OpsEventTaskService) canStart(status string) bool {
|
|
|
- return status == opsdevmodel.TaskStatusTodo || status == opsdevmodel.TaskStatusPaused
|
|
|
+ return status == opsdevmodel.TaskStatusTodo || status == opsdevmodel.TaskStatusPaused || status == opsdevmodel.TaskStatusBlocked
|
|
|
}
|
|
|
|
|
|
func (s *OpsEventTaskService) canComplete(status string) bool {
|
|
|
@@ -966,10 +1131,40 @@ 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
|
|
|
@@ -1083,3 +1278,103 @@ func (s *OpsEventTaskService) AddRecord(req *opsdevmodel.OpsEventTaskRecordAddRe
|
|
|
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
|
|
|
+}
|
|
|
+
|
|
|
+// canAddWorkHour 检查是否可以登记工时(仅处理中状态)
|
|
|
+func (s *OpsEventTaskService) canAddWorkHour(status string) bool {
|
|
|
+ return status == opsdevmodel.TaskStatusProcessing
|
|
|
+}
|