Переглянути джерело

feature:交付项目事件管理及事件任务功能增强

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
程健 11 годин тому
батько
коміт
4736575db1

+ 5 - 5
opms_parent/app/handler/opsdev/delivery_project_event.go

@@ -51,7 +51,7 @@ func (h *DeliveryProjectEventHandler) Create(ctx context.Context, req *eventmode
 		return err
 	}
 
-	rsp.Data = g.Map{"msg": "新增成功"}
+	rsp.Data = g.Map{"message": "新增成功"}
 	return nil
 }
 
@@ -73,7 +73,7 @@ func (h *DeliveryProjectEventHandler) UpdateById(ctx context.Context, req *event
 		return err
 	}
 
-	rsp.Data = g.Map{"msg": "更新成功"}
+	rsp.Data = g.Map{"message": "更新成功"}
 	return nil
 }
 
@@ -95,7 +95,7 @@ func (h *DeliveryProjectEventHandler) DeleteByIds(ctx context.Context, req *even
 		return err
 	}
 
-	rsp.Data = g.Map{"msg": "删除成功"}
+	rsp.Data = g.Map{"message": "删除成功"}
 	return nil
 }
 
@@ -186,7 +186,7 @@ func (h *DeliveryProjectEventHandler) AddRecord(ctx context.Context, req *eventm
 		return err
 	}
 
-	rsp.Data = g.Map{"msg": "添加成功"}
+	rsp.Data = g.Map{"message": "添加成功"}
 	return nil
 }
 
@@ -208,6 +208,6 @@ func (h *DeliveryProjectEventHandler) Cancel(ctx context.Context, req *eventmode
 		return err
 	}
 
-	rsp.Data = g.Map{"msg": "作废成功"}
+	rsp.Data = g.Map{"message": "作废成功"}
 	return nil
 }

+ 11 - 12
opms_parent/app/handler/opsdev/ops_event_task.go

@@ -40,7 +40,7 @@ func (h *OpsEventTaskHandler) Create(ctx context.Context, req *opsdevmodel.OpsEv
 	if err := s.Create(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "新增成功"}
+	rsp.Data = g.Map{"message": "新增成功"}
 	return nil
 }
 
@@ -56,7 +56,7 @@ func (h *OpsEventTaskHandler) UpdateById(ctx context.Context, req *opsdevmodel.O
 	if err := s.UpdateById(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "更新成功"}
+	rsp.Data = g.Map{"message": "更新成功"}
 	return nil
 }
 
@@ -72,7 +72,7 @@ func (h *OpsEventTaskHandler) DeleteByIds(ctx context.Context, req *opsdevmodel.
 	if err := s.DeleteByIds(req.Ids); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "删除成功"}
+	rsp.Data = g.Map{"message": "删除成功"}
 	return nil
 }
 
@@ -105,7 +105,7 @@ func (h *OpsEventTaskHandler) Schedule(ctx context.Context, req *opsdevmodel.Ops
 	if err := s.Schedule(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "排期成功"}
+	rsp.Data = g.Map{"message": "排期成功"}
 	return nil
 }
 
@@ -121,13 +121,12 @@ func (h *OpsEventTaskHandler) Start(ctx context.Context, req *opsdevmodel.OpsEve
 	if err := s.Start(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "任务已开始"}
+	rsp.Data = g.Map{"message": "任务已开始"}
 	return nil
 }
 
 // Complete 完成
 func (h *OpsEventTaskHandler) Complete(ctx context.Context, req *opsdevmodel.OpsEventTaskCompleteReq, rsp *comm_def.CommonMsg) error {
-	g.Log().Infof("Handler received req: %+v", req)
 	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
 		return myerrors.ValidError(err.Error())
 	}
@@ -138,7 +137,7 @@ func (h *OpsEventTaskHandler) Complete(ctx context.Context, req *opsdevmodel.Ops
 	if err := s.Complete(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "任务已完成"}
+	rsp.Data = g.Map{"message": "任务已完成"}
 	return nil
 }
 
@@ -154,7 +153,7 @@ func (h *OpsEventTaskHandler) Pause(ctx context.Context, req *opsdevmodel.OpsEve
 	if err := s.Pause(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "任务已暂停"}
+	rsp.Data = g.Map{"message": "任务已暂停"}
 	return nil
 }
 
@@ -170,7 +169,7 @@ func (h *OpsEventTaskHandler) Block(ctx context.Context, req *opsdevmodel.OpsEve
 	if err := s.Block(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "任务已阻塞"}
+	rsp.Data = g.Map{"message": "任务已阻塞"}
 	return nil
 }
 
@@ -186,7 +185,7 @@ func (h *OpsEventTaskHandler) Cancel(ctx context.Context, req *opsdevmodel.OpsEv
 	if err := s.Cancel(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "任务已作废"}
+	rsp.Data = g.Map{"message": "任务已作废"}
 	return nil
 }
 
@@ -253,7 +252,7 @@ func (h *OpsEventTaskHandler) AddRecord(ctx context.Context, req *opsdevmodel.Op
 	if err := s.AddRecord(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "添加成功"}
+	rsp.Data = g.Map{"message": "添加成功"}
 	return nil
 }
 
@@ -269,7 +268,7 @@ func (h *OpsEventTaskHandler) AddWorkHour(ctx context.Context, req *opsdevmodel.
 	if err := s.AddWorkHour(req); err != nil {
 		return err
 	}
-	rsp.Data = g.Map{"msg": "工时登记成功"}
+	rsp.Data = g.Map{"message": "工时登记成功"}
 	return nil
 }
 

+ 2 - 1
opms_parent/app/handler/opsdev/project_inventory.go

@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"dashoo.cn/common_definition/comm_def"
+	"dashoo.cn/opms_libary/myerrors"
 	"dashoo.cn/opms_parent/app/model/opsdev"
 	opssrv "dashoo.cn/opms_parent/app/service/opsdev"
 	"github.com/gogf/gf/frame/g"
@@ -40,7 +41,7 @@ func (h *ProjectInventoryHandler) GetProjectManagers(ctx context.Context, req *o
 
 func (h *ProjectInventoryHandler) GetContractProducts(ctx context.Context, req *opsdev.ContractProductReq, rsp *comm_def.CommonMsg) error {
 	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
-		return err
+		return myerrors.ValidError(err.Error())
 	}
 	srv, err := opssrv.NewProjectInventoryService(ctx)
 	if err != nil {

+ 3 - 3
opms_parent/app/model/opsdev/operation.go

@@ -45,7 +45,7 @@ type OpsOperationEventReq struct {
 
 // UpdateOpsOperationEventReq 运维事件更新请求
 type UpdateOpsOperationEventReq struct {
-	Id int `json:"id" v:"min:1#ID不能为空"` // 主键
+	Id int64 `json:"id" v:"min:1#ID不能为空"` // 主键
 	OpsOperationEventReq
 }
 
@@ -71,8 +71,8 @@ type OpsOperationEventRecordWithAttachments struct {
 
 // OpsOperationEventAttachmentReq 附件上传请求
 type OpsOperationEventAttachmentReq struct {
-	EventId       int    `json:"eventId"       v:"min:1#事件ID不能为空"`    // 事件ID
-	EventRecordId int    `json:"eventRecordId"`                       // 事件记录ID
+	EventId       int64  `json:"eventId"       v:"min:1#事件ID不能为空"`    // 事件ID
+	EventRecordId int64  `json:"eventRecordId"`                       // 事件记录ID
 	FileName      string `json:"fileName"      v:"required#文件名不能为空"`  // 文件名
 	FileUrl       string `json:"fileUrl"       v:"required#文件地址不能为空"` // 文件地址
 	FileType      string `json:"fileType"`                            // 文件类型

+ 0 - 6
opms_parent/app/model/opsdev/ops_delivery_project.go

@@ -74,11 +74,6 @@ type OpsDeliveryProjectUpdateReq struct {
 	Remark           string `json:"remark"`
 }
 
-// OpsDeliveryProjectDeleteReq 删除请求
-type OpsDeliveryProjectDeleteReq struct {
-	Ids []int `json:"ids" v:"required#ID不能为空"`
-}
-
 // AssignDeliveryUserReq 指派/改派交付负责人请求
 type AssignDeliveryUserReq struct {
 	ProjectId        int    `json:"projectId" v:"required#项目ID不能为空"`      // 项目ID
@@ -93,4 +88,3 @@ type AssignDeliveryUserReq struct {
 	Remark           string `json:"remark"`                                     // 备注
 }
 
-// Fill with you ideas below.

+ 6 - 0
opms_parent/app/model/opsdev/ops_delivery_project_event.go

@@ -54,6 +54,11 @@ type OpsDeliveryProjectEventAddReq struct {
 	FeedbackSource      string `json:"feedbackSource" v:"required#反馈来源不能为空"`     // 反馈来源
 	FeedbackReporter    string `json:"feedbackReporter" v:"required#反馈人不能为空"`    // 反馈人
 	OnSite              string `json:"onSite"`                                     // 是否现场
+	OpsUserId           int    `json:"opsUserId"`                                  // 执行人ID
+	OpsUserName         string `json:"opsUserName"`                                // 执行人姓名
+	Attribute1          string `json:"attribute1"`                                 // 物流单号
+	CompleteTime        string `json:"completeTime"`                               // 执行时间
+	ActualWorkHour      float64 `json:"actualWorkHour"`                            // 实际工作量(小时)
 	Attachments         []OpsDeliveryProjectEventAttachmentReq `json:"attachments"` // 附件列表
 }
 
@@ -73,6 +78,7 @@ type OpsDeliveryProjectEventUpdateReq struct {
 	CompleteDesc        string  `json:"completeDesc"`        // 处理说明(处理/关闭时传入)
 	CompleteTime        string  `json:"completeTime"`        // 完成时间(处理/关闭时传入)
 	ActualWorkHour      float64 `json:"actualWorkHour"`      // 实际工作量(处理/关闭时传入)
+	Attribute1          string  `json:"attribute1"`          // 物流单号
 }
 
 // OpsDeliveryProjectEventDeleteReq 删除请求

+ 0 - 1
opms_parent/app/model/opsdev/ops_delivery_project_event_attachment.go

@@ -11,4 +11,3 @@ import (
 // OpsDeliveryProjectEventAttachment is the golang structure for table ops_delivery_project_event_attachment.
 type OpsDeliveryProjectEventAttachment internal.OpsDeliveryProjectEventAttachment
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_delivery_project_event_record.go

@@ -11,4 +11,3 @@ import (
 // OpsDeliveryProjectEventRecord is the golang structure for table ops_delivery_project_event_record.
 type OpsDeliveryProjectEventRecord internal.OpsDeliveryProjectEventRecord
 
-// Fill with you ideas below.

+ 21 - 17
opms_parent/app/model/opsdev/ops_event_task.go

@@ -25,11 +25,14 @@ const (
 
 // Task Type Constants
 const (
-	TaskTypeReqReview     = "10" // 需求评审
-	TaskTypeFeatureDev    = "20" // 功能开发
-	TaskTypeFeatureTest   = "30" // 功能测试
-	TaskTypeBug           = "35" // BUG
-	TaskTypeSystemRelease = "40" // 系统发版
+	TaskTypeReqReview         = "10" // 需求评审
+	TaskTypeFeatureDev        = "20" // 功能开发
+	TaskTypeFeatureTest       = "30" // 功能测试
+	TaskTypeBug               = "35" // BUG
+	TaskTypeSystemRelease     = "40" // 系统发版
+	TaskTypeSystemReleaseEvt  = "38" // 系统发版(事件关联自动创建)
+	TaskTypeHardwareDeliver   = "42" // 硬件发货
+	TaskTypeHardwareInstall   = "41" // 硬件安装
 )
 
 // Priority Constants
@@ -56,13 +59,14 @@ type Attachment struct {
 // OpsEventTaskSearchReq 任务列表查询请求
 type OpsEventTaskSearchReq struct {
 	request.PageReq
-	ProjectId        int         `json:"projectId"`        // 项目ID
-	TaskTitle        string      `json:"taskTitle"`        // 任务标题(模糊查询)
-	TaskType         []string    `json:"taskType"`         // 任务类型(多选)
-	TaskStatus       []string    `json:"taskStatus"`       // 任务状态(多选)
-	Priority         []string    `json:"priority"`         // 优先级(多选)
-	OpsUserName      []string    `json:"opsUserName"`      // 执行人姓名(多选)
-	SortFields       []SortField `json:"sortFields"`       // 排序字段
+	ProjectId           int         `json:"projectId"`           // 项目ID
+	EventId             int         `json:"eventId"`             // 关联事件ID
+	TaskTitle           string      `json:"taskTitle"`           // 任务标题(模糊查询)
+	TaskType            []string    `json:"taskType"`            // 任务类型(多选)
+	TaskStatus          []string    `json:"taskStatus"`          // 任务状态(多选)
+	Priority            []string    `json:"priority"`            // 优先级(多选)
+	OpsUserName         []string    `json:"opsUserName"`         // 执行人姓名(多选)
+	SortFields          []SortField `json:"sortFields"`          // 排序字段
 	// 计划结束日期范围
 	PlanEndDateStart string `json:"planEndDateStart"` // 计划结束开始日期
 	PlanEndDateEnd   string `json:"planEndDateEnd"`   // 计划结束结束日期
@@ -72,6 +76,8 @@ type OpsEventTaskSearchReq struct {
 	// 完成日期范围
 	CompleteTimeStart string `json:"completeTimeStart"` // 完成开始日期
 	CompleteTimeEnd   string `json:"completeTimeEnd"`   // 完成结束日期
+	// 发布版本为空查询(用于发版任务选择未发版任务)
+	ReleaseVersionEmpty bool `json:"releaseVersionEmpty"` // 是否查询release_version为空的任务
 }
 
 // OpsEventTaskAddReq 新增任务请求
@@ -93,6 +99,7 @@ type OpsEventTaskAddReq struct {
 	EventId          int          `json:"eventId"`                                     // 关联事件ID
 	EventType        string       `json:"eventType"`                                   // 关联事件类型
 	TaskParentId     int          `json:"taskParentId"`                                // 父任务ID
+	Attribute2       string       `json:"attribute2"`                                  // 历史遗留(10-是 20-否)
 	Attachments      []Attachment `json:"attachments"`                                 // 附件列表
 }
 
@@ -112,6 +119,7 @@ type OpsEventTaskUpdateReq struct {
 	DefectType       string  `json:"defectType"`                   // 缺陷类型(10前端 20后端)
 	ReleaseVersion   string  `json:"releaseVersion"`               // 发布版本
 	Remark           string  `json:"remark"`                       // 备注
+	Attribute2       string  `json:"attribute2"`                   // 历史遗留(10-是 20-否)
 }
 
 // OpsEventTaskScheduleReq 任务排期请求
@@ -194,6 +202,7 @@ type OpsEventTaskRsp struct {
 	FunctionName   string `json:"functionName"`   // 功能模块
 	DefectType     string `json:"defectType"`     // 缺陷类型
 	ReleaseVersion string `json:"releaseVersion"` // 发布版本
+	Attribute2     string `json:"attribute2"`     // 历史遗留(10-是 20-否)
 	// 关联信息
 	ProjectName string `json:"projectName"` // 项目名称
 	ContractNo  string `json:"contractNo"`  // 合同编号
@@ -222,11 +231,6 @@ type IdReq struct {
 	Id int `json:"id" v:"required#ID不能为空"` // ID
 }
 
-// IdsReq 批量ID请求
-type IdsReq struct {
-	Ids []int64 `json:"ids" v:"required#ID列表不能为空"` // ID列表
-}
-
 // OpsEventTaskReleaseListReq 查询发布版本关联任务请求
 type OpsEventTaskReleaseListReq struct {
 	ReleaseTaskId int `json:"releaseTaskId" v:"required#发版任务ID不能为空"` // 发版任务ID

+ 0 - 1
opms_parent/app/model/opsdev/ops_event_task_attachment.go

@@ -11,4 +11,3 @@ import (
 // OpsEventTaskAttachment is the golang structure for table ops_event_task_attachment.
 type OpsEventTaskAttachment internal.OpsEventTaskAttachment
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_event_task_record.go

@@ -11,4 +11,3 @@ import (
 // OpsEventTaskRecord is the golang structure for table ops_event_task_record.
 type OpsEventTaskRecord internal.OpsEventTaskRecord
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_event_task_release.go

@@ -11,4 +11,3 @@ import (
 // OpsEventTaskRelease is the golang structure for table ops_event_task_release.
 type OpsEventTaskRelease internal.OpsEventTaskRelease
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_event_task_work_hour.go

@@ -11,4 +11,3 @@ import (
 // OpsEventTaskWorkHour is the golang structure for table ops_event_task_work_hour.
 type OpsEventTaskWorkHour internal.OpsEventTaskWorkHour
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_operation_event.go

@@ -11,4 +11,3 @@ import (
 // OpsOperationEvent is the golang structure for table ops_operation_event.
 type OpsOperationEvent internal.OpsOperationEvent
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_operation_event_attachment.go

@@ -11,4 +11,3 @@ import (
 // OpsOperationEventAttachment is the golang structure for table ops_operation_event_attachment.
 type OpsOperationEventAttachment internal.OpsOperationEventAttachment
 
-// Fill with you ideas below.

+ 0 - 1
opms_parent/app/model/opsdev/ops_operation_event_record.go

@@ -11,4 +11,3 @@ import (
 // OpsOperationEventRecord is the golang structure for table ops_operation_event_record.
 type OpsOperationEventRecord internal.OpsOperationEventRecord
 
-// Fill with you ideas below.

+ 207 - 29
opms_parent/app/service/opsdev/delivery_project_event.go

@@ -2,6 +2,7 @@ package opsdev
 
 import (
 	"context"
+	"fmt"
 	"strings"
 
 	"dashoo.cn/opms_libary/myerrors"
@@ -14,6 +15,17 @@ import (
 	"github.com/gogf/gf/util/gconv"
 )
 
+// eventTypeToAutoTaskType 交付事件类型→自动创建研发任务类型的映射
+var eventTypeToAutoTaskType = map[string]string{
+	"31": eventmodel.TaskTypeReqReview,        // 需求评审 → 需求评审
+	"32": eventmodel.TaskTypeFeatureDev,       // 功能调整 → 功能开发
+	"33": eventmodel.TaskTypeFeatureDev,       // 二开需求 → 功能开发
+	"35": eventmodel.TaskTypeFeatureDev,       // 系统缺陷 → 功能开发
+	"38": eventmodel.TaskTypeSystemReleaseEvt, // 系统发版 → 系统发版(事件)
+	"40": eventmodel.TaskTypeSystemReleaseEvt, // 硬件发货 → 系统发版(事件)
+	"41": eventmodel.TaskTypeSystemReleaseEvt, // 硬件安装 → 系统发版(事件)
+}
+
 // DeliveryProjectEventService 交付项目事件业务逻辑实现类
 type DeliveryProjectEventService struct {
 	*service.ContextService
@@ -147,6 +159,36 @@ func (s *DeliveryProjectEventService) GetList(req *eventmodel.OpsDeliveryProject
 		return 0, nil, myerrors.DbError("数据转换失败")
 	}
 
+	if len(list) > 0 {
+		projectIds := make([]int, 0, len(list))
+		for _, item := range list {
+			if item.ProjectId > 0 {
+				projectIds = append(projectIds, item.ProjectId)
+			}
+		}
+
+		if len(projectIds) > 0 {
+			var projects []*eventmodel.OpsDeliveryProject
+			err := s.ProjectDao.Fields(
+				s.ProjectDao.Columns.Id,
+				s.ProjectDao.Columns.ProjectName,
+			).WhereIn(s.ProjectDao.Columns.Id, projectIds).Scan(&projects)
+			if err != nil {
+				g.Log().Error(err)
+			} else {
+				projectMap := make(map[int]string)
+				for _, p := range projects {
+					projectMap[p.Id] = p.ProjectName
+				}
+				for _, item := range list {
+					if name, ok := projectMap[item.ProjectId]; ok {
+						item.ProjectName = name
+					}
+				}
+			}
+		}
+	}
+
 	return
 }
 
@@ -174,18 +216,43 @@ func (s *DeliveryProjectEventService) Create(req *eventmodel.OpsDeliveryProjectE
 	// 生成事件编码
 	eventNo := s.generateEventNo()
 
-	// 构造数据
+	// 硬件发货(40)和硬件安装(41)默认状态为处理中(20)
+	status := "10" // 待处理
+	if req.DeliveryEventType == "40" || req.DeliveryEventType == "41" {
+		status = "20"
+	}
+
+	// 构造数据,负责人默认为当前登录人,允许前端传入
+	opsUserId := s.GetCxtUserId()
+	opsUserName := s.GetCxtUserName()
+	if req.OpsUserId > 0 {
+		opsUserId = req.OpsUserId
+		opsUserName = req.OpsUserName
+	}
+
 	data := g.Map{
 		s.EventDao.Columns.ProjectId:           req.ProjectId,
 		s.EventDao.Columns.DeliveryEventNo:     eventNo,
 		s.EventDao.Columns.DeliveryEventTitle:  req.DeliveryEventTitle,
 		s.EventDao.Columns.DeliveryEventDesc:   req.DeliveryEventDesc,
 		s.EventDao.Columns.DeliveryEventType:   req.DeliveryEventType,
-		s.EventDao.Columns.DeliveryEventStatus: "10", // 待处理
+		s.EventDao.Columns.DeliveryEventStatus: status,
 		s.EventDao.Columns.FeedbackSource:      req.FeedbackSource,
 		s.EventDao.Columns.FeedbackReporter:    req.FeedbackReporter,
 		s.EventDao.Columns.FeedbackDate:        gtime.Now(),
 		s.EventDao.Columns.OnSite:              req.OnSite,
+		s.EventDao.Columns.OpsUserId:           opsUserId,
+		s.EventDao.Columns.OpsUserName:         opsUserName,
+	}
+
+	if req.Attribute1 != "" {
+		data[s.EventDao.Columns.Attribute1] = req.Attribute1
+	}
+	if req.CompleteTime != "" {
+		data[s.EventDao.Columns.CompleteTime] = req.CompleteTime
+	}
+	if req.ActualWorkHour > 0 {
+		data[s.EventDao.Columns.ActualWorkHour] = req.ActualWorkHour
 	}
 
 	// 补齐审计字段
@@ -216,18 +283,25 @@ func (s *DeliveryProjectEventService) Create(req *eventmodel.OpsDeliveryProjectE
 		}
 		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("新增事件过程记录失败")
 		}
 
+		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.DeliveryEventId: eventId,
 					s.AttachmentDao.Columns.EventId:         eventId,
+					s.AttachmentDao.Columns.EventRecordId:   recordId,
 					s.AttachmentDao.Columns.FileName:        att.FileName,
 					s.AttachmentDao.Columns.FileUrl:         att.FileUrl,
 					s.AttachmentDao.Columns.FileType:        att.FileType,
@@ -242,6 +316,47 @@ func (s *DeliveryProjectEventService) Create(req *eventmodel.OpsDeliveryProjectE
 			}
 		}
 
+		// 4. 特定事件类型自动创建研发任务
+		if taskType, ok := eventTypeToAutoTaskType[req.DeliveryEventType]; ok {
+			taskNo := s.generateTaskNo()
+			taskData := 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.DeliveryEventTitle,
+				s.TaskDao.Columns.TaskDesc:      req.DeliveryEventDesc,
+				s.TaskDao.Columns.FunctionName:  req.DeliveryEventTitle,
+				s.TaskDao.Columns.TaskType:      taskType,
+				s.TaskDao.Columns.TaskStatus:    eventmodel.TaskStatusTodo,
+				s.TaskDao.Columns.Priority:      "20",
+				s.TaskDao.Columns.EventId:       int(eventId),
+				s.TaskDao.Columns.EventType:     eventmodel.EventTypeDelivery,
+			}
+			service.SetCreatedInfo(taskData, s.GetCxtUserId(), s.GetCxtUserName())
+
+			taskResult, err := s.TaskDao.TX(tx).Data(taskData).Insert()
+			if err != nil {
+				g.Log().Error(err)
+				return myerrors.DbError("自动创建研发任务失败")
+			}
+
+			taskId, _ := taskResult.LastInsertId()
+
+			taskRecordData := g.Map{
+				s.TaskRecordDao.Columns.TaskId:         taskId,
+				s.TaskRecordDao.Columns.HandleUserId:   s.GetCxtUserId(),
+				s.TaskRecordDao.Columns.HandleUserName: s.GetCxtUserName(),
+				s.TaskRecordDao.Columns.HandleContent:  "自动创建任务<br/>说明: 由交付事件自动创建 (" + req.DeliveryEventTitle + ")",
+			}
+			service.SetCreatedInfo(taskRecordData, s.GetCxtUserId(), s.GetCxtUserName())
+
+			_, err = s.TaskRecordDao.TX(tx).Data(taskRecordData).Insert()
+			if err != nil {
+				g.Log().Error(err)
+				return myerrors.DbError("新增任务过程记录失败")
+			}
+		}
+
 		return nil
 	})
 }
@@ -262,13 +377,25 @@ func (s *DeliveryProjectEventService) UpdateById(req *eventmodel.OpsDeliveryProj
 	// 构造更新数据 - 从请求中动态构建,支持前端传来的任何有效字段
 	data := g.Map{}
 
-	// 基础字段(编辑时可能更新)
-	data[s.EventDao.Columns.DeliveryEventTitle] = req.DeliveryEventTitle
-	data[s.EventDao.Columns.DeliveryEventDesc] = req.DeliveryEventDesc
-	data[s.EventDao.Columns.DeliveryEventType] = req.DeliveryEventType
-	data[s.EventDao.Columns.FeedbackSource] = req.FeedbackSource
-	data[s.EventDao.Columns.FeedbackReporter] = req.FeedbackReporter
-	data[s.EventDao.Columns.OnSite] = req.OnSite
+	// 基础字段(编辑时可能更新,但关闭/处理事件时不传这些字段)
+	if req.DeliveryEventTitle != "" {
+		data[s.EventDao.Columns.DeliveryEventTitle] = req.DeliveryEventTitle
+	}
+	if req.DeliveryEventDesc != "" {
+		data[s.EventDao.Columns.DeliveryEventDesc] = req.DeliveryEventDesc
+	}
+	if req.DeliveryEventType != "" {
+		data[s.EventDao.Columns.DeliveryEventType] = req.DeliveryEventType
+	}
+	if req.FeedbackSource != "" {
+		data[s.EventDao.Columns.FeedbackSource] = req.FeedbackSource
+	}
+	if req.FeedbackReporter != "" {
+		data[s.EventDao.Columns.FeedbackReporter] = req.FeedbackReporter
+	}
+	if req.OnSite != "" {
+		data[s.EventDao.Columns.OnSite] = req.OnSite
+	}
 
 	// 处理状态和结果字段(关闭事件时传入)
 	if req.DeliveryEventStatus != "" {
@@ -295,6 +422,11 @@ func (s *DeliveryProjectEventService) UpdateById(req *eventmodel.OpsDeliveryProj
 		data[s.EventDao.Columns.OpsUserName] = req.OpsUserName
 	}
 
+	// 物流单号
+	if req.Attribute1 != "" {
+		data[s.EventDao.Columns.Attribute1] = req.Attribute1
+	}
+
 	// 补齐审计字段
 	service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
 
@@ -400,6 +532,7 @@ func (s *DeliveryProjectEventService) Cancel(req *eventmodel.OpsDeliveryProjectE
 			for _, att := range req.Attachments {
 				attData := g.Map{
 					s.AttachmentDao.Columns.DeliveryEventId: req.Id,
+					s.AttachmentDao.Columns.EventId:         req.Id,
 					s.AttachmentDao.Columns.EventRecordId:   recordId,
 					s.AttachmentDao.Columns.FileName:        att.FileName,
 					s.AttachmentDao.Columns.FileUrl:         att.FileUrl,
@@ -605,13 +738,14 @@ func (s *DeliveryProjectEventService) AddRecord(req *eventmodel.OpsDeliveryProje
 		// 2. 保存附件
 		if len(req.Attachments) > 0 {
 			for _, att := range req.Attachments {
-				attData := g.Map{
-					s.AttachmentDao.Columns.DeliveryEventId: req.DeliveryEventId,
-					s.AttachmentDao.Columns.EventRecordId:   recordId,
-					s.AttachmentDao.Columns.FileName:        att.FileName,
-					s.AttachmentDao.Columns.FileUrl:         att.FileUrl,
-					s.AttachmentDao.Columns.FileType:        att.FileType,
-				}
+			attData := g.Map{
+				s.AttachmentDao.Columns.DeliveryEventId: req.DeliveryEventId,
+				s.AttachmentDao.Columns.EventId:         req.DeliveryEventId,
+				s.AttachmentDao.Columns.EventRecordId:   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()
@@ -626,6 +760,49 @@ func (s *DeliveryProjectEventService) AddRecord(req *eventmodel.OpsDeliveryProje
 	})
 }
 
+// generateTaskNo 生成任务编号(与OpsEventTaskService保持统一格式)
+func (s *DeliveryProjectEventService) generateTaskNo() string {
+	now := gtime.Now()
+	prefix := "TSK" + now.Format("Ymd")
+
+	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"
+}
+
+// getProjectName 根据项目ID获取项目名称
+func (s *DeliveryProjectEventService) getProjectName(projectId int) string {
+	if projectId <= 0 {
+		return ""
+	}
+	var project eventmodel.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
+}
+
 // generateEventNo 生成事件编码
 func (s *DeliveryProjectEventService) generateEventNo() string {
 	// 格式: EVT + 年月日 + 4位序号
@@ -633,23 +810,24 @@ func (s *DeliveryProjectEventService) generateEventNo() string {
 	prefix := "EVT" + now.Format("Ymd")
 
 	// 查询当天最大序号
-	var maxNo string
-	err := s.EventDao.Where(s.EventDao.Columns.DeliveryEventNo+" like ?", prefix+"%").Order(s.EventDao.Columns.DeliveryEventNo + " desc").Fields(s.EventDao.Columns.DeliveryEventNo).Scan(&maxNo)
-	if err != nil {
-		return prefix + "0001"
-	}
-
-	if maxNo == "" {
+	var maxNoResult struct {
+		DeliveryEventNo string
+	}
+	err := s.EventDao.Where(s.EventDao.Columns.DeliveryEventNo+" like ?", prefix+"%").
+		Order(s.EventDao.Columns.DeliveryEventNo + " desc").
+		Fields(s.EventDao.Columns.DeliveryEventNo).
+		Scan(&maxNoResult)
+	if err != nil || maxNoResult.DeliveryEventNo == "" {
 		return prefix + "0001"
 	}
 
 	// 提取序号并+1
-	if len(maxNo) >= len(prefix)+4 {
-		seq := maxNo[len(prefix):]
-		var seqNum int
-		gconv.Struct(seq, &seqNum)
+	maxNoStr := maxNoResult.DeliveryEventNo
+	if len(maxNoStr) >= len(prefix)+4 {
+		seq := maxNoStr[len(prefix):]
+		seqNum := gconv.Int(seq)
 		seqNum++
-		return prefix + gconv.String(seqNum)
+		return prefix + fmt.Sprintf("%04d", seqNum)
 	}
 
 	return prefix + "0001"

+ 239 - 48
opms_parent/app/service/opsdev/ops_event_task.go

@@ -18,12 +18,14 @@ import (
 // OpsEventTaskService 任务业务逻辑实现类
 type OpsEventTaskService struct {
 	*service.ContextService
-	TaskDao       *opsdevdao.OpsEventTaskDao
-	RecordDao     *opsdevdao.OpsEventTaskRecordDao
-	AttachmentDao *opsdevdao.OpsEventTaskAttachmentDao
-	ProjectDao    *opsdevdao.OpsDeliveryProjectDao
-	ReleaseDao    *opsdevdao.OpsEventTaskReleaseDao
-	WorkHourDao   *opsdevdao.OpsEventTaskWorkHourDao
+	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
@@ -38,6 +40,8 @@ func NewOpsEventTaskService(ctx context.Context) (svc *OpsEventTaskService, err
 	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
 }
 
@@ -50,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+"%")
@@ -75,6 +84,11 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
 		db = db.Where(s.TaskDao.Columns.OpsUserName+" in (?)", req.OpsUserName)
 	}
 
+	// 发布版本为空筛选(用于发版任务选择未发版任务)
+	if req.ReleaseVersionEmpty {
+		db = db.Where(s.TaskDao.Columns.ReleaseVersion + " is null")
+	}
+
 	// 计划结束日期范围筛选
 	if req.PlanEndDateStart != "" {
 		db = db.Where(s.TaskDao.Columns.PlanEndTime+" >= ?", req.PlanEndDateStart+" 00:00:00")
@@ -167,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()
 
@@ -198,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 != "" {
@@ -216,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("新增任务失败")
 		}
 
@@ -235,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失败")
@@ -290,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
 	}
 
 	// 补齐审计字段
@@ -491,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("查询任务数据失败")
@@ -508,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, // 已完成
@@ -527,6 +614,9 @@ func (s *OpsEventTaskService) Complete(req *opsdevmodel.OpsEventTaskCompleteReq)
 	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 {
@@ -545,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 != "" {
@@ -617,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("自动创建功能开发任务失败")
 			}
 
@@ -659,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("自动创建功能测试任务失败")
 			}
 
@@ -680,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,
@@ -698,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
 	})
 }
@@ -902,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")
+	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())
 }
 
 // 状态校验辅助方法
@@ -939,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 {
@@ -954,6 +1131,20 @@ 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
 }

+ 7 - 7
opms_parent/app/service/opsdev/project_inventory.go

@@ -42,7 +42,7 @@ func NewProjectInventoryService(ctx context.Context) (*ProjectInventoryService,
 // GetList 获取项目清单列表
 func (s *ProjectInventoryService) GetList(req *opsdev.ProjectInventorySearchReq) (int, []*opsdev.ProjectInventoryRsp, error) {
 	db := s.Dao.DB.Table("ops_delivery_project")
-	db = db.LeftJoin("ctr_contract", "ctr_contract.nbo_id = ops_delivery_project.contract_id")
+	db = db.LeftJoin("ctr_contract", "ctr_contract.id = ops_delivery_project.contract_id")
 	db = db.LeftJoin("cust_customer", "cust_customer.id = ops_delivery_project.cust_id")
 	db = db.Where("ops_delivery_project.deleted_time IS NULL")
 
@@ -139,12 +139,12 @@ func (s *ProjectInventoryService) GetList(req *opsdev.ProjectInventorySearchReq)
 		"ops_delivery_project.sales_user_name",
 		"ops_delivery_project.plan_delivery_time",
 		"ops_delivery_project.plan_accept_time",
-		"ctr_contract.id as contract_id",
-		"ctr_contract.contract_code as contract_no",
+		"ops_delivery_project.contract_id",
+		"ops_delivery_project.contract_no",
 		"ctr_contract.contract_amount",
 		"ctr_contract.collected_amount",
 		"ops_delivery_project.cust_id",
-		"cust_customer.cust_name",
+		"ops_delivery_project.cust_name",
 	).Scan(&records)
 	if err != nil {
 		return 0, nil, err
@@ -251,7 +251,7 @@ func (s *ProjectInventoryService) GetDeliveryNodes() []g.Map {
 // Export 导出项目清单
 func (s *ProjectInventoryService) Export(ctx context.Context, req *opsdev.ProjectInventoryExportReq) (content *opsdev.ProjectInventoryExportContent, err error) {
 	db := s.Dao.DB.Table("ops_delivery_project")
-	db = db.LeftJoin("ctr_contract", "ctr_contract.nbo_id = ops_delivery_project.contract_id")
+	db = db.LeftJoin("ctr_contract", "ctr_contract.id = ops_delivery_project.contract_id")
 	db = db.LeftJoin("cust_customer", "cust_customer.id = ops_delivery_project.cust_id")
 	db = db.Where("ops_delivery_project.deleted_time IS NULL")
 
@@ -325,10 +325,10 @@ func (s *ProjectInventoryService) Export(ctx context.Context, req *opsdev.Projec
 		"ops_delivery_project.sales_user_name",
 		"ops_delivery_project.plan_delivery_time",
 		"ops_delivery_project.plan_accept_time",
-		"ctr_contract.contract_code as contract_no",
+		"ops_delivery_project.contract_no",
 		"ctr_contract.contract_amount",
 		"ctr_contract.collected_amount",
-		"cust_customer.cust_name",
+		"ops_delivery_project.cust_name",
 	).Scan(&records)
 	if err != nil {
 		return nil, err

+ 15 - 0
opms_parent/schema/function.sql

@@ -50,6 +50,21 @@ BEGIN
 END $
 DELIMITER ;
 
+-- 添加日期字段(用于按天重置)
+alter table plat_sequence add `day` date DEFAULT NULL COMMENT '日期' after `year`;
+
+-- 创建按天重置序列号的函数
+DROP FUNCTION IF EXISTS next_day_reset_val;
+DELIMITER $
+CREATE FUNCTION next_day_reset_val (seq_name VARCHAR(50)) RETURNS INTEGER LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER COMMENT '' BEGIN
+    -- 如果日期不是今天,重置序列为0
+    UPDATE plat_sequence SET current_value=0, `day`=CURDATE() WHERE `day` IS NULL OR `day` != CURDATE();
+    -- 递增序列号
+    UPDATE plat_sequence SET current_value=current_value+increment WHERE name = seq_name;
+    RETURN currval(seq_name);
+END $
+DELIMITER ;
+
 -- INSERT INTO plat_sequence VALUES ('customer_code', 1000, 1, '', 1000, '系统管理员', '2023-02-09 10:27:42', null, null, null, null);
 -- INSERT INTO plat_sequence VALUES ('contract_code', 1000, 1, '', 1000, '系统管理员', '2023-02-09 10:27:42', null, null, null, null);
 -- INSERT INTO plat_sequence VALUES ('consult_code', 1000, 1, null, '', 1000, '系统管理员', '2023-02-09 10:27:42', null, null, null, null);