Bläddra i källkod

feat: 统一项目权限体系 + 合同权限过滤 + 运维交付功能增强

- 抽取 BuildProjectPermissionWhere 到 base.go 作为公共权限工具函数
- 合同列表/事件列表增加基于角色的权限过滤(RegionalManager/SalesEngineer等)
- 合同事件创建时自动从交付项目获取 ops_user 和 delivery_user
- 合同/运维事件作废时写入操作记录(Record)
- 交付项目事件: 研发任务创建改为前端驱动,支持编辑时补建任务
- 交付项目事件详情返回 HasRdTask 标识
- 运维事件列表/导出/看板增加项目级权限过滤
- 软件交付任务: 产品线筛选、仪表盘跨天任务展开、导出功能
- 项目清单: 支持多状态筛选、运维经理筛选项、ops 角色查询
- 移除 oms_parent_v2 子模块引用
程健 3 veckor sedan
förälder
incheckning
1814efe44b

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

@@ -323,3 +323,15 @@ func (h *OpsEventTaskHandler) GetScheduleStats(ctx context.Context, req *opsdevm
 	return nil
 }
 
+func (h *OpsEventTaskHandler) Export(ctx context.Context, req *opsdevmodel.OpsEventTaskExportReq, rsp *comm_def.CommonMsg) error {
+	s, err := services.NewOpsEventTaskService(ctx)
+	if err != nil {
+		return err
+	}
+	content, err := s.Export(ctx, req)
+	if err != nil {
+		return err
+	}
+	rsp.Data = content
+	return nil
+}

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

@@ -19,12 +19,14 @@ type OpsDeliveryProjectSearchReq struct {
 	Status           string `json:"status" form:"status"`                     // 项目状态
 	ProjectStatus    string `json:"projectStatus" form:"projectStatus"`       // 项目状态(支持多选,逗号分隔)
 	ProductLine      string `json:"productLine" form:"productLine"`           // 产品线
-	DeliveryUserId   int    `json:"deliveryUserId" form:"deliveryUserId"`     // 交付负责人ID
-	SalesUserId      int    `json:"salesUserId" form:"salesUserId"`           // 销售负责人ID
-	ProjectName      string `json:"projectName" form:"projectName"`           // 项目名称
-	SalesUserName    string `json:"salesUserName" form:"salesUserName"`       // 销售负责人
-	DeliveryUserName string `json:"deliveryUserName" form:"deliveryUserName"` // 交付负责人
-	SortField        string `json:"sortField" form:"sortField"`               // 排序字段
+	DeliveryUserId     int    `json:"deliveryUserId" form:"deliveryUserId"`         // 交付负责人ID
+	SalesUserId        int    `json:"salesUserId" form:"salesUserId"`               // 销售负责人ID
+	ProjectName        string `json:"projectName" form:"projectName"`               // 项目名称
+	SalesUserName      string `json:"salesUserName" form:"salesUserName"`           // 销售负责人
+	DeliveryUserName   string `json:"deliveryUserName" form:"deliveryUserName"`     // 交付负责人
+	OpsManagerUserId   int    `json:"opsManagerUserId" form:"opsManagerUserId"`     // 运维负责人ID (attribute4)
+	OpsManagerUserName string `json:"opsManagerUserName" form:"opsManagerUserName"` // 运维负责人姓名 (attribute3)
+	SortField          string `json:"sortField" form:"sortField"`                   // 排序字段
 	SortOrder        string `json:"sortOrder" form:"sortOrder"`               // 排序方式
 }
 

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

@@ -60,6 +60,11 @@ type OpsDeliveryProjectEventAddReq struct {
 	CompleteTime        string `json:"completeTime"`                               // 执行时间
 	ActualWorkHour      float64 `json:"actualWorkHour"`                            // 实际工作量(小时)
 	Attachments         []OpsDeliveryProjectEventAttachmentReq `json:"attachments"` // 附件列表
+	// 研发任务相关字段
+	CreateRdTask      bool   `json:"createRdTask"`      // 是否创建研发任务
+	RdTaskOpsUserId   int    `json:"rdTaskOpsUserId"`   // 研发任务负责人ID
+	RdTaskOpsUserName string `json:"rdTaskOpsUserName"` // 研发任务负责人姓名
+	RdTaskType        string `json:"rdTaskType"`        // 研发任务类型
 }
 
 // OpsDeliveryProjectEventUpdateReq 更新请求
@@ -79,6 +84,11 @@ type OpsDeliveryProjectEventUpdateReq struct {
 	CompleteTime        string  `json:"completeTime"`        // 完成时间(处理/关闭时传入)
 	ActualWorkHour      float64 `json:"actualWorkHour"`      // 实际工作量(处理/关闭时传入)
 	Attribute1          string  `json:"attribute1"`          // 物流单号
+	// 研发任务相关(编辑时创建研发任务)
+	CreateRdTask      bool   `json:"createRdTask"`      // 是否创建研发任务
+	RdTaskOpsUserId   int    `json:"rdTaskOpsUserId"`   // 研发任务负责人ID
+	RdTaskOpsUserName string `json:"rdTaskOpsUserName"` // 研发任务负责人姓名
+	RdTaskType        string `json:"rdTaskType"`        // 研发任务类型
 }
 
 // OpsDeliveryProjectEventDeleteReq 删除请求
@@ -134,6 +144,8 @@ type OpsDeliveryProjectEventRsp struct {
 	DeploymentTime         string `json:"deploymentTime"`         // 部署时间
 	TrialRunTime           string `json:"trialRunTime"`           // 试运行时间
 	GoLiveTime             string `json:"goLiveTime"`             // 上线时间
+	// 研发任务相关
+	HasRdTask bool `json:"hasRdTask"` // 是否已关联研发任务
 }
 
 // OpsDeliveryProjectEventRecordAddReq 事件过程记录新增请求

+ 35 - 0
opms_parent/app/model/opsdev/ops_event_task.go

@@ -61,6 +61,7 @@ type OpsEventTaskSearchReq struct {
 	request.PageReq
 	ProjectId           int         `json:"projectId"`           // 项目ID
 	ProjectIds          []int       `json:"projectIds"`          // 项目ID列表(多选)
+	ProductLine         string      `json:"productLine"`         // 产品线(仅 projectId=0 时生效,关联项目表过滤)
 	EventId             int         `json:"eventId"`             // 关联事件ID
 	TaskTitle           string      `json:"taskTitle"`           // 任务标题(模糊查询)
 	TaskType            []string    `json:"taskType"`            // 任务类型(多选)
@@ -68,6 +69,9 @@ type OpsEventTaskSearchReq struct {
 	Priority            []string    `json:"priority"`            // 优先级(多选)
 	OpsUserName         []string    `json:"opsUserName"`         // 执行人姓名(多选)
 	SortFields          []SortField `json:"sortFields"`          // 排序字段
+	// 计划开始日期范围
+	PlanStartDateStart string `json:"planStartDateStart"` // 计划开始开始日期
+	PlanStartDateEnd   string `json:"planStartDateEnd"`   // 计划开始结束日期
 	// 计划结束日期范围
 	PlanEndDateStart string `json:"planEndDateStart"` // 计划结束开始日期
 	PlanEndDateEnd   string `json:"planEndDateEnd"`   // 计划结束结束日期
@@ -303,3 +307,34 @@ type OpsEventTaskUserScheduleStat struct {
 type OpsEventTaskScheduleStatRsp struct {
 	List []*OpsEventTaskUserScheduleStat `json:"list"` // 用户排期统计列表
 }
+
+// OpsEventTaskExportReq 任务导出请求
+type OpsEventTaskExportReq struct {
+	OpsEventTaskSearchReq
+}
+
+// OpsEventTaskExportData 任务导出数据
+type OpsEventTaskExportData struct {
+	TaskNo           string `json:"taskNo"           export:"任务编号"`
+	TaskTitle        string `json:"taskTitle"        export:"任务标题"`
+	TaskType         string `json:"taskType"         export:"任务类型"`
+	TaskStatus       string `json:"taskStatus"       export:"任务状态"`
+	FunctionName     string `json:"functionName"     export:"功能模块"`
+	OpsUserName      string `json:"opsUserName"      export:"负责人"`
+	PlanStartTime    string `json:"planStartTime"    export:"计划开始时间"`
+	PlanEndTime      string `json:"planEndTime"      export:"计划结束时间"`
+	CompleteTime     string `json:"completeTime"     export:"完成时间"`
+	EstimateWorkHour string `json:"estimateWorkHour" export:"预估工时"`
+	ActualWorkHour   string `json:"actualWorkHour"   export:"实际工时"`
+	DefectType       string `json:"defectType"       export:"缺陷类型"`
+	Attribute2       string `json:"attribute2"       export:"历史遗留"`
+	ReleaseVersion   string `json:"releaseVersion"   export:"发布版本"`
+	ProjectName      string `json:"projectName"      export:"项目名称"`
+	CreatedName      string `json:"createdName"      export:"创建人"`
+	CreatedTime      string `json:"createdTime"      export:"创建时间"`
+}
+
+// OpsEventTaskExportContent 任务导出内容
+type OpsEventTaskExportContent struct {
+	Content string `json:"content"` // 导出内容(base64编码)
+}

+ 2 - 0
opms_parent/app/model/opsdev/project_inventory.go

@@ -15,9 +15,11 @@ type ProjectInventorySearchReq struct {
 	ProjectName           string `json:"projectName" form:"projectName"`                     // 项目名称
 	ProductLine           string `json:"productLine" form:"productLine"`                     // 产品线
 	ProjectStatus         string `json:"projectStatus" form:"projectStatus"`                 // 项目状态
+	ProjectStatusList     []int  `json:"projectStatusList" form:"projectStatusList"`         // 项目状态列表(多选)
 	DeliveryNode          string `json:"deliveryNode" form:"deliveryNode"`                   // 交付节点
 	DeliveryUserId        int    `json:"deliveryUserId" form:"deliveryUserId"`               // 交付负责人ID
 	SalesUserId           int    `json:"salesUserId" form:"salesUserId"`                     // 销售负责人ID
+	OpsManagerUserId      int    `json:"opsManagerUserId" form:"opsManagerUserId"`           // 运维负责人ID (attribute4)
 	PlanDeliveryTimeStart string `json:"planDeliveryTimeStart" form:"planDeliveryTimeStart"` // 计划交付时间开始
 	PlanDeliveryTimeEnd   string `json:"planDeliveryTimeEnd" form:"planDeliveryTimeEnd"`     // 计划交付时间结束
 	PlanAcceptTimeStart   string `json:"planAcceptTimeStart" form:"planAcceptTimeStart"`     // 计划验收时间开始

+ 47 - 0
opms_parent/app/service/base.go

@@ -14,6 +14,7 @@ import (
 	"net/http"
 	"os"
 	"reflect"
+	"strings"
 
 	"dashoo.cn/opms_libary/myerrors"
 	"dashoo.cn/opms_libary/request"
@@ -424,6 +425,52 @@ func StringsContains(s []string, ele string) bool {
 	return false
 }
 
+func BuildProjectPermissionWhere(userId int, roles []string) string {
+	allVisibleRoles := []string{
+		"GeneralManager",
+		"SalesDirector",
+		"ResearchAndDevelopmentDirector",
+		"ResearchAndDevelopmentSupervisor",
+		"PersonnelDirector",
+		"SysAdmin",
+		"GeneralManagerAssistant",
+	}
+
+	for _, role := range allVisibleRoles {
+		if StringsContains(roles, role) {
+			return ""
+		}
+	}
+
+	conditions := []string{}
+
+	if StringsContains(roles, "RegionalManager") {
+		conditions = append(conditions, fmt.Sprintf(
+			"EXISTS (SELECT 1 FROM base_region_auth WHERE user_id = %d AND city_id = sales_region_id)",
+			userId,
+		))
+	}
+
+	if StringsContains(roles, "SalesEngineer") {
+		conditions = append(conditions, fmt.Sprintf("sales_user_id = %d", userId))
+	}
+
+	if StringsContains(roles, "ProjectManager") ||
+		StringsContains(roles, "ProjectDeliveryManager") {
+		conditions = append(conditions, fmt.Sprintf("delivery_user_id = %d", userId))
+	}
+
+	if StringsContains(roles, "OperationsEngineer") {
+		conditions = append(conditions, fmt.Sprintf("attribute4 = %d", userId))
+	}
+
+	if len(conditions) > 0 {
+		return fmt.Sprintf("(%s)", strings.Join(conditions, " OR "))
+	}
+
+	return "1=0"
+}
+
 func UserIdByRoles(db gdb.DB, roles ...string) ([]int, error) {
 	roleId, err := ColumnInt(db.Table("sys_role").Where("role_key in (?)", roles), "id")
 	if err != nil {

+ 48 - 2
opms_parent/app/service/contract/ctr_contract.go

@@ -173,9 +173,55 @@ func (s CtrContractService) DynamicsList(ctx context.Context, req *model.CtrCont
 	return total, ret, err
 }
 
+// buildContractPermissionWhere 构建合同查询权限条件
+// SalesDirector, GeneralManager, ResearchAndDevelopmentDirector, ResearchAndDevelopmentSupervisor, PersonnelDirector 能查看所有合同
+// SalesEngineer 仅能查看自己的合同 (incharge_id = 当前登录用户)
+// RegionalManager 能查看所负责区域的所有合同 (根据 base_region_auth 关联查询)
+func (s CtrContractService) buildContractPermissionWhere() string {
+	// 全部可见角色
+	allVisibleRoles := []string{
+		"SalesDirector",                    // 销售总监
+		"GeneralManager",                   // 总经理
+		"ResearchAndDevelopmentDirector",   // 研发总监
+		"ResearchAndDevelopmentSupervisor", // 研发主管
+		"PersonnelDirector",                // 人事总监
+		"FinancialSupervisor",
+		"SaleAssociate",
+		"Cashier",
+		"SysAdmin", // 系统管理员
+	}
+
+	for _, role := range allVisibleRoles {
+		if service.StringsContains(s.userInfo.Roles, role) {
+			return "" // 全部可见,无额外条件
+		}
+	}
+
+	// 构建权限条件 (使用 EXISTS 避免 IN)
+	where := "(1=0"
+
+	// 大区经理:查看授权区域内所有合同
+	if service.StringsContains(s.userInfo.Roles, "RegionalManager") {
+		where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM base_region_auth WHERE user_id = %d AND deleted_time IS NULL AND city_id = a.cust_city_id)", s.userInfo.Id)
+	}
+
+	// 销售工程师:查看负责人是自己的合同
+	if service.StringsContains(s.userInfo.Roles, "SalesEngineer") {
+		where += fmt.Sprintf(" OR a.incharge_id = %d", s.userInfo.Id)
+	}
+
+	where += ")"
+	return where
+}
+
 func (s CtrContractService) List(ctx context.Context, req *model.CtrContractListReq) (int, []*model.CtrContractListRsp, error) {
-	ctx = context.WithValue(ctx, "contextService", s)
-	dao := s.Dao.DataScope(ctx, "incharge_id").As("a")
+	dao := s.Dao.As("a").Unscoped().Where("a.deleted_time IS NULL")
+
+	// 应用权限过滤
+	permissionWhere := s.buildContractPermissionWhere()
+	if permissionWhere != "" {
+		dao = dao.Where(permissionWhere)
+	}
 
 	if req.SearchText != "" {
 		likestr := fmt.Sprintf("%%%s%%", req.SearchText)

+ 126 - 19
opms_parent/app/service/contract/ctr_contract_event.go

@@ -23,7 +23,9 @@ type ctrContractEventService struct {
 	ContractDao            *contractdao.CtrContractDao
 	DeliveryEventDao       *opsdevdao.OpsDeliveryProjectEventDao
 	DeliveryProjectDao     *opsdevdao.OpsDeliveryProjectDao
+	DeliveryRecordDao      *opsdevdao.OpsDeliveryProjectEventRecordDao
 	OperationDao           *opsdevdao.OpsOperationEventDao
+	OperationRecordDao     *opsdevdao.OpsOperationEventRecordDao
 	DeliveryAttachmentDao  *opsdevdao.OpsDeliveryProjectEventAttachmentDao
 	OperationAttachmentDao *opsdevdao.OpsOperationEventAttachmentDao
 }
@@ -38,12 +40,50 @@ func NewCtrContractEventService(ctx context.Context) (*ctrContractEventService,
 	svc.ContractDao = contractdao.NewCtrContractDao(svc.Tenant)
 	svc.DeliveryEventDao = opsdevdao.NewOpsDeliveryProjectEventDao(svc.Tenant)
 	svc.DeliveryProjectDao = opsdevdao.NewOpsDeliveryProjectDao(svc.Tenant)
+	svc.DeliveryRecordDao = opsdevdao.NewOpsDeliveryProjectEventRecordDao(svc.Tenant)
 	svc.OperationDao = opsdevdao.NewOpsOperationEventDao(svc.Tenant)
+	svc.OperationRecordDao = opsdevdao.NewOpsOperationEventRecordDao(svc.Tenant)
 	svc.DeliveryAttachmentDao = opsdevdao.NewOpsDeliveryProjectEventAttachmentDao(svc.Tenant)
 	svc.OperationAttachmentDao = opsdevdao.NewOpsOperationEventAttachmentDao(svc.Tenant)
 	return svc, nil
 }
 
+// buildContractEventPermissionWhere 构建合同事件查询权限条件
+// 仅能查看自己有权限的合同的事件信息
+func (s *ctrContractEventService) buildContractEventPermissionWhere() string {
+	if s.CxtUser == nil {
+		return ""
+	}
+
+	allVisibleRoles := []string{
+		"SalesDirector",
+		"GeneralManager",
+		"ResearchAndDevelopmentDirector",
+		"ResearchAndDevelopmentSupervisor",
+		"PersonnelDirector",
+		"SysAdmin",
+	}
+
+	for _, role := range allVisibleRoles {
+		if service.StringsContains(s.CxtUser.Roles, role) {
+			return ""
+		}
+	}
+
+	where := "(1=0"
+
+	if service.StringsContains(s.CxtUser.Roles, "RegionalManager") {
+		where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM base_region_auth WHERE user_id = %d AND deleted_time IS NULL AND city_id = cc.cust_city_id)", s.CxtUser.Id)
+	}
+
+	if service.StringsContains(s.CxtUser.Roles, "SalesEngineer") {
+		where += fmt.Sprintf(" OR a.incharge_id = %d", s.CxtUser.Id)
+	}
+
+	where += ")"
+	return where
+}
+
 func (s *ctrContractEventService) GetProjectByContractId(req *contractmodel.CtrContractEventGetProjectReq) (*contractmodel.CtrContractEventProjectRsp, error) {
 	rsp := &contractmodel.CtrContractEventProjectRsp{}
 	type projectRow struct {
@@ -131,11 +171,17 @@ func (s *ctrContractEventService) CreateProjectForContract(req *contractmodel.Ct
 
 func (s *ctrContractEventService) GetList(req *contractmodel.CtrContractEventSearchReq) (total int, list []*contractmodel.CtrContractEventRsp, err error) {
 	baseQuery := s.Dao.DB.Model("ctr_contract_event a").
+		LeftJoin("ctr_contract cc", "a.contract_id = cc.id AND cc.deleted_time IS NULL").
 		LeftJoin("ops_delivery_project_event b", "a.event_type='10' AND a.event_id = b.id AND b.deleted_time IS NULL").
 		LeftJoin("ops_operation_event c", "a.event_type='20' AND a.event_id = c.id AND c.deleted_time IS NULL").
 		Unscoped().
 		Where("a.deleted_time IS NULL")
 
+	permissionWhere := s.buildContractEventPermissionWhere()
+	if permissionWhere != "" {
+		baseQuery = baseQuery.Where(permissionWhere)
+	}
+
 	if req.ContractId != 0 {
 		baseQuery = baseQuery.Where("a.contract_id = ?", req.ContractId)
 	}
@@ -231,8 +277,8 @@ func (s *ctrContractEventService) Create(req *contractmodel.CtrContractEventAddR
 			"cust_name":     req.CustName,
 			"product_line":  req.ProductLine,
 			"is_big":        req.IsBig,
-			"incharge_id":   req.InchargeId,
-			"incharge_name": req.InchargeName,
+			"incharge_id":   s.GetCxtUserId(),
+			"incharge_name": s.GetCxtUserName(),
 			"remark":        req.Remark,
 		}
 		service.SetCreatedInfo(linkData, s.GetCxtUserId(), s.GetCxtUserName())
@@ -247,6 +293,19 @@ func (s *ctrContractEventService) Create(req *contractmodel.CtrContractEventAddR
 }
 
 func (s *ctrContractEventService) createOperationEvent(tx *gdb.TX, req *contractmodel.CtrContractEventAddReq) (int64, string, error) {
+	var project opsdevmodel.OpsDeliveryProject
+	err := s.DeliveryProjectDao.
+		TX(tx).
+		Fields(s.DeliveryProjectDao.Columns.Attribute4, s.DeliveryProjectDao.Columns.Attribute3).
+		Where("contract_id = ? AND deleted_time IS NULL", req.ContractId).
+		OrderDesc(s.DeliveryProjectDao.Columns.Id).
+		Limit(1).
+		Scan(&project)
+	if err != nil {
+		g.Log().Error(err)
+		return 0, "", myerrors.DbError("查询交付项目失败")
+	}
+
 	data := g.Map{
 		"event_title":       req.EventTitle,
 		"event_desc":        req.EventDesc,
@@ -261,11 +320,17 @@ func (s *ctrContractEventService) createOperationEvent(tx *gdb.TX, req *contract
 		"is_ops":            "10",
 		"feedback_reporter": req.FeedbackReporter,
 		"feedback_source":   req.FeedbackSource,
-		"event_status":      opsdevmodel.EventStatusProcessing,
 		"event_no":          generateOperationEventNo(),
-		"ops_user_id":       s.GetCxtUserId(),
-		"ops_user_name":     s.GetCxtUserName(),
 	}
+
+	if project.Attribute4 > 0 {
+		data["event_status"] = opsdevmodel.EventStatusProcessing
+		data["ops_user_id"] = int(project.Attribute4)
+		data["ops_user_name"] = project.Attribute3
+	} else {
+		data["event_status"] = opsdevmodel.EventStatusPending
+	}
+
 	if req.FeedbackDate == "" {
 		data["feedback_date"] = gtime.Now()
 	} else {
@@ -300,7 +365,11 @@ func (s *ctrContractEventService) createDeliveryEvent(tx *gdb.TX, req *contractm
 	var project opsdevmodel.OpsDeliveryProject
 	err := s.DeliveryProjectDao.
 		TX(tx).
-		Fields(s.DeliveryProjectDao.Columns.Id).
+		Fields(
+			s.DeliveryProjectDao.Columns.Id,
+			s.DeliveryProjectDao.Columns.DeliveryUserId,
+			s.DeliveryProjectDao.Columns.DeliveryUserName,
+		).
 		Where("contract_id = ? AND deleted_time IS NULL", req.ContractId).
 		OrderDesc(s.DeliveryProjectDao.Columns.Id).
 		Limit(1).
@@ -322,8 +391,14 @@ func (s *ctrContractEventService) createDeliveryEvent(tx *gdb.TX, req *contractm
 		"feedback_source":       req.FeedbackSource,
 		"delivery_event_status": opsdevmodel.DeliveryEventStatusProcessing,
 		"delivery_event_no":     generateDeliveryEventNo(),
-		"ops_user_id":           s.GetCxtUserId(),
-		"ops_user_name":         s.GetCxtUserName(),
+	}
+
+	if project.DeliveryUserId > 0 {
+		data["ops_user_id"] = project.DeliveryUserId
+		data["ops_user_name"] = project.DeliveryUserName
+	} else {
+		data["ops_user_id"] = s.GetCxtUserId()
+		data["ops_user_name"] = s.GetCxtUserName()
 	}
 	if req.FeedbackDate == "" {
 		data["feedback_date"] = gtime.Now()
@@ -412,6 +487,20 @@ func (s *ctrContractEventService) cancelDeliveryEvent(req *contractmodel.CtrCont
 			g.Log().Error(err)
 			return myerrors.DbError("作废交付事件失败")
 		}
+
+		recordData := g.Map{
+			"delivery_event_id": req.EventId,
+			"handle_user_id":    s.GetCxtUserId(),
+			"handle_user_name":  s.GetCxtUserName(),
+			"handle_content":    "作废事件:" + req.CancelReason,
+		}
+		service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
+		_, err = s.DeliveryRecordDao.TX(tx).Data(recordData).Insert()
+		if err != nil {
+			g.Log().Error(err)
+			return myerrors.DbError("记录交付事件作废过程失败")
+		}
+
 		return nil
 	})
 }
@@ -430,16 +519,34 @@ func (s *ctrContractEventService) cancelOperationEvent(req *contractmodel.CtrCon
 		return myerrors.TipsError("已关闭的事件不能作废")
 	}
 
-	data := g.Map{
-		s.OperationDao.Columns.EventStatus: opsdevmodel.EventStatusClosed,
-		s.OperationDao.Columns.Remark:      req.CancelReason,
-	}
-	service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
+	return s.OperationDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		data := g.Map{
+			s.OperationDao.Columns.EventStatus: opsdevmodel.EventStatusClosed,
+			s.OperationDao.Columns.Remark:      req.CancelReason,
+		}
+		service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
 
-	_, err = s.OperationDao.FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(req.EventId).Update()
-	if err != nil {
-		g.Log().Error(err)
-		return myerrors.DbError("作废运维事件失败")
-	}
-	return nil
+		_, err := s.OperationDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(req.EventId).Update()
+		if err != nil {
+			g.Log().Error(err)
+			return myerrors.DbError("作废运维事件失败")
+		}
+
+		recordData := g.Map{
+			"event_id":        req.EventId,
+			"handle_user_id":  s.GetCxtUserId(),
+			"handle_user_name": s.GetCxtUserName(),
+			"handle_content":  "作废事件:" + req.CancelReason,
+			"operate_type":    "90",
+			"handle_date":     gtime.Now(),
+		}
+		service.SetCreatedInfo(recordData, s.GetCxtUserId(), s.GetCxtUserName())
+		_, err = s.OperationRecordDao.TX(tx).Data(recordData).Insert()
+		if err != nil {
+			g.Log().Error(err)
+			return myerrors.DbError("记录运维事件作废过程失败")
+		}
+
+		return nil
+	})
 }

+ 80 - 56
opms_parent/app/service/opsdev/delivery_project.go

@@ -39,6 +39,9 @@ func NewDeliveryProjectService(ctx context.Context) (*DeliveryProjectService, er
 func (s *DeliveryProjectService) GetList(req *opsdevmodel.OpsDeliveryProjectSearchReq) (int64, []*opsdevmodel.OpsDeliveryProject, error) {
 	m := s.Dao.Ctx(s.Ctx)
 
+	// 过滤已删除数据
+	m = m.Where("deleted_time IS NULL")
+
 	// 权限控制
 	where := s.buildPermissionWhere()
 	if where != "" {
@@ -87,6 +90,12 @@ func (s *DeliveryProjectService) GetList(req *opsdevmodel.OpsDeliveryProjectSear
 	if req.SalesUserId > 0 {
 		m = m.Where("sales_user_id", req.SalesUserId)
 	}
+	if req.OpsManagerUserId > 0 {
+		m = m.Where("attribute4", req.OpsManagerUserId)
+	}
+	if req.OpsManagerUserName != "" {
+		m = m.Where("attribute3 like ?", "%"+req.OpsManagerUserName+"%")
+	}
 
 	// 获取总数
 	total, err := m.Count()
@@ -114,6 +123,11 @@ func (s *DeliveryProjectService) GetList(req *opsdevmodel.OpsDeliveryProjectSear
 		return 0, nil, err
 	}
 
+	// 确保空列表返回 [] 而非 nil
+	if list == nil {
+		list = make([]*opsdevmodel.OpsDeliveryProject, 0)
+	}
+
 	return int64(total), list, nil
 }
 
@@ -121,6 +135,9 @@ func (s *DeliveryProjectService) GetList(req *opsdevmodel.OpsDeliveryProjectSear
 func (s *DeliveryProjectService) GetListAll(req *opsdevmodel.OpsDeliveryProjectSearchReq) (int64, []*opsdevmodel.OpsDeliveryProject, error) {
 	m := s.Dao.Ctx(s.Ctx)
 
+	// 过滤已删除数据
+	m = m.Where("deleted_time IS NULL")
+
 	// 项目状态筛选(支持多选,逗号分隔)
 	if req.ProjectStatus != "" {
 		statusList := strings.Split(req.ProjectStatus, ",")
@@ -163,6 +180,12 @@ func (s *DeliveryProjectService) GetListAll(req *opsdevmodel.OpsDeliveryProjectS
 	if req.SalesUserId > 0 {
 		m = m.Where("sales_user_id", req.SalesUserId)
 	}
+	if req.OpsManagerUserId > 0 {
+		m = m.Where("attribute4", req.OpsManagerUserId)
+	}
+	if req.OpsManagerUserName != "" {
+		m = m.Where("attribute3 like ?", "%"+req.OpsManagerUserName+"%")
+	}
 
 	// 获取总数
 	total, err := m.Count()
@@ -190,6 +213,11 @@ func (s *DeliveryProjectService) GetListAll(req *opsdevmodel.OpsDeliveryProjectS
 		return 0, nil, err
 	}
 
+	// 确保空列表返回 [] 而非 nil
+	if list == nil {
+		list = make([]*opsdevmodel.OpsDeliveryProject, 0)
+	}
+
 	return int64(total), list, nil
 }
 
@@ -202,24 +230,57 @@ func (s *DeliveryProjectService) GetDelegatedProjectList(req *opsdevmodel.OpsDel
 
 	m := s.Dao.Ctx(s.Ctx)
 
-	where := s.buildPermissionWhere()
-	if where != "" {
-		// 查询被授权给当前用户的委托项目 ID
-		delegateModel := eventdao.NewOpsDeliveryProjectDelegateDao(s.Tenant).Ctx(s.Ctx)
-		var delegates []*opsdevmodel.OpsDeliveryProjectDelegate
-		if err := delegateModel.Where("user_id", userId).Scan(&delegates); err != nil {
-			return 0, nil, err
+	// 过滤已删除数据
+	m = m.Where("deleted_time IS NULL")
+
+	allVisibleRoles := []string{
+		"GeneralManager",
+		"SalesDirector",
+		"ResearchAndDevelopmentDirector",
+		"ResearchAndDevelopmentSupervisor",
+		"PersonnelDirector",
+		"SysAdmin",
+		"GeneralManagerAssistant",
+	}
+
+	hasAllVisible := false
+	for _, role := range allVisibleRoles {
+		if service.StringsContains(s.CxtUser.Roles, role) {
+			hasAllVisible = true
+			break
 		}
+	}
+
+	if !hasAllVisible {
+		conditions := []string{}
 
-		var delegatedIds []int
-		for _, d := range delegates {
-			delegatedIds = append(delegatedIds, d.ProjectId)
+		if service.StringsContains(s.CxtUser.Roles, "RegionalManager") {
+			conditions = append(conditions, fmt.Sprintf(
+				"EXISTS (SELECT 1 FROM base_region_auth WHERE base_region_auth.user_id = %d AND base_region_auth.city_id = ops_delivery_project.sales_region_id)",
+				userId,
+			))
 		}
 
-		if len(delegatedIds) > 0 {
-			m = m.Where(fmt.Sprintf("(delivery_user_id = %d OR id IN (%s))", userId, intSliceToSqlIn(delegatedIds)))
+		if service.StringsContains(s.CxtUser.Roles, "SalesEngineer") {
+			conditions = append(conditions, fmt.Sprintf("ops_delivery_project.sales_user_id = %d", userId))
+		}
+
+		if service.StringsContains(s.CxtUser.Roles, "ProjectManager") ||
+			service.StringsContains(s.CxtUser.Roles, "ProjectDeliveryManager") {
+			conditions = append(conditions, fmt.Sprintf(
+				"(ops_delivery_project.delivery_user_id = %d OR EXISTS (SELECT 1 FROM ops_delivery_project_delegate WHERE ops_delivery_project_delegate.user_id = %d AND ops_delivery_project_delegate.project_id = ops_delivery_project.id))",
+				userId, userId,
+			))
+		}
+
+		if service.StringsContains(s.CxtUser.Roles, "OperationsEngineer") {
+			conditions = append(conditions, fmt.Sprintf("ops_delivery_project.attribute4 = %d", userId))
+		}
+
+		if len(conditions) > 0 {
+			m = m.Where(fmt.Sprintf("(%s)", strings.Join(conditions, " OR ")))
 		} else {
-			m = m.Where("delivery_user_id", userId)
+			m = m.Where("1=0")
 		}
 	}
 
@@ -286,6 +347,11 @@ func (s *DeliveryProjectService) GetDelegatedProjectList(req *opsdevmodel.OpsDel
 		return 0, nil, err
 	}
 
+	// 确保空列表返回 [] 而非 nil
+	if list == nil {
+		list = make([]*opsdevmodel.OpsDeliveryProject, 0)
+	}
+
 	return int64(total), list, nil
 }
 
@@ -303,49 +369,7 @@ func intSliceToSqlIn(ids []int) string {
 
 // buildPermissionWhere 构建权限条件
 func (s *DeliveryProjectService) buildPermissionWhere() string {
-	// 全部可见角色
-	allVisibleRoles := []string{
-		"GeneralManager",                   // 总经理
-		"SalesDirector",                    // 销售总监
-		"ResearchAndDevelopmentDirector",   // 研发总监
-		"ResearchAndDevelopmentSupervisor", // 研发主管
-		"PersonnelDirector",                // 人事总监
-		"SysAdmin",                         // 系统管理员
-		"GeneralManagerAssistant",          // 总经理助理
-	}
-
-	for _, role := range allVisibleRoles {
-		if service.StringsContains(s.CxtUser.Roles, role) {
-			return "" // 全部可见,无额外条件
-		}
-	}
-
-	// 构建权限条件
-	where := "(1=0"
-
-	// 大区经理:查看授权区域内所有项目
-	if service.StringsContains(s.CxtUser.Roles, "RegionalManager") {
-		where += fmt.Sprintf(" OR sales_region_id IN (SELECT sale_region_id FROM base_region_auth WHERE user_id = %d)", s.CxtUser.Id)
-	}
-
-	// 销售工程师:查看销售负责人是自己的项目
-	if service.StringsContains(s.CxtUser.Roles, "SalesEngineer") {
-		where += fmt.Sprintf(" OR sales_user_id = %d", s.CxtUser.Id)
-	}
-
-	// 运维工程师:查看运维负责人是自己的项目
-	if service.StringsContains(s.CxtUser.Roles, "OperationsEngineer") {
-		where += fmt.Sprintf(" OR attribute4 = %d", s.CxtUser.Id)
-	}
-
-	// 项目经理、项目交付经理:查看项目负责人是自己的项目
-	if service.StringsContains(s.CxtUser.Roles, "ProjectManager") ||
-		service.StringsContains(s.CxtUser.Roles, "ProjectDeliveryManager") {
-		where += fmt.Sprintf(" OR delivery_user_id = %d", s.CxtUser.Id)
-	}
-
-	where += ")"
-	return where
+	return service.BuildProjectPermissionWhere(s.CxtUser.Id, s.CxtUser.Roles)
 }
 
 // GetEntityById 根据ID获取详情

+ 101 - 26
opms_parent/app/service/opsdev/delivery_project_event.go

@@ -16,16 +16,7 @@ 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 {
@@ -62,6 +53,18 @@ func (s *DeliveryProjectEventService) GetList(req *eventmodel.OpsDeliveryProject
 	// 项目ID筛选
 	if req.ProjectId > 0 {
 		db = db.Where(s.EventDao.Columns.ProjectId, req.ProjectId)
+	} else {
+		userId := s.GetCxtUserId()
+		if userId > 0 {
+			projectWhere := service.BuildProjectPermissionWhere(userId, s.GetCxtUserRoles())
+			if projectWhere != "" {
+				db = db.Where(fmt.Sprintf(
+					"EXISTS (SELECT 1 FROM ops_delivery_project WHERE id = %s AND %s)",
+					s.EventDao.Columns.ProjectId,
+					projectWhere,
+				))
+			}
+		}
 	}
 
 	// 事件标题模糊查询
@@ -319,8 +322,8 @@ func (s *DeliveryProjectEventService) Create(req *eventmodel.OpsDeliveryProjectE
 			}
 		}
 
-		// 4. 特定事件类型自动创建研发任务
-		if taskType, ok := eventTypeToAutoTaskType[req.DeliveryEventType]; ok {
+		// 4. 根据前端选项决定是否创建研发任务
+		if req.CreateRdTask && req.RdTaskType != "" {
 			taskNo := s.generateTaskNo()
 			taskData := g.Map{
 				s.TaskDao.Columns.TaskNo:        taskNo,
@@ -329,18 +332,22 @@ func (s *DeliveryProjectEventService) Create(req *eventmodel.OpsDeliveryProjectE
 				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.TaskType:      req.RdTaskType,
 				s.TaskDao.Columns.TaskStatus:    eventmodel.TaskStatusTodo,
 				s.TaskDao.Columns.Priority:      "20",
 				s.TaskDao.Columns.EventId:       int(eventId),
 				s.TaskDao.Columns.EventType:     eventmodel.EventTypeDelivery,
 			}
+			if req.RdTaskOpsUserId > 0 {
+				taskData[s.TaskDao.Columns.OpsUserId] = req.RdTaskOpsUserId
+				taskData[s.TaskDao.Columns.OpsUserName] = req.RdTaskOpsUserName
+			}
 			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("自动创建研发任务失败")
+				return myerrors.DbError("创建研发任务失败")
 			}
 
 			taskId, _ := taskResult.LastInsertId()
@@ -349,7 +356,7 @@ func (s *DeliveryProjectEventService) Create(req *eventmodel.OpsDeliveryProjectE
 				s.TaskRecordDao.Columns.TaskId:         taskId,
 				s.TaskRecordDao.Columns.HandleUserId:   s.GetCxtUserId(),
 				s.TaskRecordDao.Columns.HandleUserName: s.GetCxtUserName(),
-				s.TaskRecordDao.Columns.HandleContent:  "自动创建任务<br/>说明: 由交付事件自动创建 (" + req.DeliveryEventTitle + ")",
+				s.TaskRecordDao.Columns.HandleContent:  "创建研发任务<br/>说明: 由交付事件创建 (" + req.DeliveryEventTitle + ")",
 			}
 			service.SetCreatedInfo(taskRecordData, s.GetCxtUserId(), s.GetCxtUserName())
 
@@ -455,19 +462,77 @@ func (s *DeliveryProjectEventService) UpdateById(req *eventmodel.OpsDeliveryProj
 	// 补齐审计字段
 	service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
 
-	// 更新操作,排除不可改字段
-	_, err = s.EventDao.FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.EventDao.Columns.Id, req.Id).Update()
-	if err != nil {
-		g.Log().Error(err)
-		return myerrors.DbError("更新事件失败")
-	}
+	// 使用事务控制
+	return s.EventDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		// 更新操作,排除不可改字段
+		_, err = s.EventDao.TX(tx).FieldsEx(service.UpdateFieldEx...).Data(data).WherePri(s.EventDao.Columns.Id, req.Id).Update()
+		if err != nil {
+			g.Log().Error(err)
+			return myerrors.DbError("更新事件失败")
+		}
 
-	// 事件关闭时,根据事件类型自动推进关联项目的状态和节点(只允许前进,不允许回退)
-	if req.DeliveryEventStatus == eventmodel.DeliveryEventStatusClosed {
-		s.advanceProjectStatusOnEventClose(entity.ProjectId, req.DeliveryEventType)
-	}
+		// 编辑时创建研发任务(仅当 createRdTask=true 且该事件尚未关联研发任务)
+		if req.CreateRdTask && req.RdTaskType != "" {
+			// 检查是否已存在关联的研发任务
+			existCount, err := s.TaskDao.TX(tx).Where(s.TaskDao.Columns.EventId, req.Id).
+				Where(s.TaskDao.Columns.EventType, eventmodel.EventTypeDelivery).
+				Count()
+			if err != nil {
+				g.Log().Error(err)
+				return myerrors.DbError("检查研发任务失败")
+			}
+			if existCount == 0 {
+				taskNo := s.generateTaskNo()
+				taskData := g.Map{
+					s.TaskDao.Columns.TaskNo:        taskNo,
+					s.TaskDao.Columns.ProjectId:     entity.ProjectId,
+					s.TaskDao.Columns.ProjectName:   s.getProjectName(entity.ProjectId),
+					s.TaskDao.Columns.TaskTitle:     entity.DeliveryEventTitle,
+					s.TaskDao.Columns.TaskDesc:      entity.DeliveryEventDesc,
+					s.TaskDao.Columns.FunctionName:  entity.DeliveryEventTitle,
+					s.TaskDao.Columns.TaskType:      req.RdTaskType,
+					s.TaskDao.Columns.TaskStatus:    eventmodel.TaskStatusTodo,
+					s.TaskDao.Columns.Priority:      "20",
+					s.TaskDao.Columns.EventId:       req.Id,
+					s.TaskDao.Columns.EventType:     eventmodel.EventTypeDelivery,
+				}
+				if req.RdTaskOpsUserId > 0 {
+					taskData[s.TaskDao.Columns.OpsUserId] = req.RdTaskOpsUserId
+					taskData[s.TaskDao.Columns.OpsUserName] = req.RdTaskOpsUserName
+				}
+				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/>说明: 由交付事件创建 (" + entity.DeliveryEventTitle + ")",
+				}
+				service.SetCreatedInfo(taskRecordData, s.GetCxtUserId(), s.GetCxtUserName())
 
-	return nil
+				_, err = s.TaskRecordDao.TX(tx).Data(taskRecordData).Insert()
+				if err != nil {
+					g.Log().Error(err)
+					return myerrors.DbError("新增任务过程记录失败")
+				}
+			}
+		}
+
+		// 事件关闭时,根据事件类型自动推进关联项目的状态和节点(只允许前进,不允许回退)
+		if req.DeliveryEventStatus == eventmodel.DeliveryEventStatusClosed {
+			s.advanceProjectStatusOnEventClose(entity.ProjectId, req.DeliveryEventType)
+		}
+
+		return nil
+	})
 }
 
 func (s *DeliveryProjectEventService) advanceProjectStatusOnEventClose(projectId int, eventType string) {
@@ -727,6 +792,16 @@ func (s *DeliveryProjectEventService) GetById(id int) (*eventmodel.OpsDeliveryPr
 		return nil, myerrors.DbError("数据转换失败")
 	}
 
+	// 查询是否已关联研发任务
+	taskCount, err := s.TaskDao.Where(s.TaskDao.Columns.EventId, id).
+		Where(s.TaskDao.Columns.EventType, eventmodel.EventTypeDelivery).
+		Count()
+	if err != nil {
+		g.Log().Error(err)
+	} else {
+		rsp.HasRdTask = taskCount > 0
+	}
+
 	// 查询关联的项目信息
 	if entity.ProjectId > 0 {
 		var project eventmodel.OpsDeliveryProject

+ 108 - 14
opms_parent/app/service/opsdev/operation.go

@@ -57,6 +57,12 @@ func NewOperationService(ctx context.Context) (svc *OperationService, err error)
 func (s *OperationService) GetList(req *opsdevmodel.OpsOperationEventSearchReq) (total int, list []*opsdevmodel.OpsOperationEvent, err error) {
 	db := s.Dao.FieldsEx(s.Dao.Columns.DeletedTime).Where(s.Dao.Columns.EventStatus+" != ?", opsdevmodel.EventStatusClosed)
 
+	// 应用项目权限过滤
+	projectPermWhere := s.buildProjectPermissionWhere()
+	if projectPermWhere != "" {
+		db = db.Where(projectPermWhere)
+	}
+
 	if req.EventNo != "" {
 		db = db.Where(s.Dao.Columns.EventNo, req.EventNo)
 	}
@@ -302,6 +308,13 @@ func (s *OperationService) GetHistoryList(req *opsdevmodel.OpsOperationEventHist
 	if req.ScopeType == "my" {
 		db = db.Where(s.Dao.Columns.OpsUserId, s.GetCxtUserId())
 	}
+
+	// 应用项目权限过滤
+	projectPermWhere := s.buildProjectPermissionWhere()
+	if projectPermWhere != "" {
+		db = db.Where(projectPermWhere)
+	}
+
 	if req.EventTitle != "" {
 		db = db.Where(s.Dao.Columns.EventTitle+" like ?", "%"+req.EventTitle+"%")
 	}
@@ -350,6 +363,13 @@ func (s *OperationService) Export(ctx context.Context, req *opsdevmodel.OpsOpera
 	if req.ScopeType == "my" {
 		db = db.Where(s.Dao.Columns.OpsUserId, s.GetCxtUserId())
 	}
+
+	// 应用项目权限过滤
+	projectPermWhere := s.buildProjectPermissionWhere()
+	if projectPermWhere != "" {
+		db = db.Where(projectPermWhere)
+	}
+
 	if req.EventTitle != "" {
 		db = db.Where(s.Dao.Columns.EventTitle+" like ?", "%"+req.EventTitle+"%")
 	}
@@ -379,13 +399,11 @@ func (s *OperationService) Export(ctx context.Context, req *opsdevmodel.OpsOpera
 		return nil, myerrors.DbError("查询运维历史失败")
 	}
 
-	// 获取所有事件ID
 	var eventIds []int
 	for _, item := range list {
 		eventIds = append(eventIds, item.Id)
 	}
 
-	// 获取所有事件的处理记录
 	var records []*opsdevmodel.OpsOperationEventRecord
 	if len(eventIds) > 0 {
 		err = s.RecordDao.WhereIn(s.RecordDao.Columns.EventId, eventIds).Order(s.RecordDao.Columns.HandleDate + " desc").Scan(&records)
@@ -499,6 +517,12 @@ func (s *OperationService) Export(ctx context.Context, req *opsdevmodel.OpsOpera
 func (s *OperationService) ExportNonClosed(ctx context.Context, req *opsdevmodel.OpsOperationEventExport) (content *opsdevmodel.OpsOperationEventHistoryExportContent, err error) {
 	db := s.Dao.FieldsEx(s.Dao.Columns.DeletedTime).Where(s.Dao.Columns.EventStatus+" != ?", opsdevmodel.EventStatusClosed)
 
+	// 应用项目权限过滤
+	projectPermWhere := s.buildProjectPermissionWhere()
+	if projectPermWhere != "" {
+		db = db.Where(projectPermWhere)
+	}
+
 	var list []*opsdevmodel.OpsOperationEvent
 	err = db.Order(s.Dao.Columns.FeedbackDate + " desc").Scan(&list)
 	if err != nil {
@@ -1203,7 +1227,7 @@ func (s *OperationService) GetStats() (g.Map, error) {
 
 	// 按状态统计
 	db := s.Dao.FieldsEx(s.Dao.Columns.DeletedTime)
-	statusList, err := db.GroupBy(s.Dao.Columns.EventStatus).Fields(s.Dao.Columns.EventStatus+", count(*) as count").All()
+	statusList, err := db.GroupBy(s.Dao.Columns.EventStatus).Fields(s.Dao.Columns.EventStatus + ", count(*) as count").All()
 	if err != nil {
 		g.Log().Error(err)
 		return nil, myerrors.DbError("统计事件状态失败")
@@ -1215,7 +1239,7 @@ func (s *OperationService) GetStats() (g.Map, error) {
 	stats["statusCount"] = statusCount
 
 	// 按优先级统计
-	priorityList, err := db.GroupBy(s.Dao.Columns.PriorityLevel).Fields(s.Dao.Columns.PriorityLevel+", count(*) as count").All()
+	priorityList, err := db.GroupBy(s.Dao.Columns.PriorityLevel).Fields(s.Dao.Columns.PriorityLevel + ", count(*) as count").All()
 	if err != nil {
 		g.Log().Error(err)
 		return nil, myerrors.DbError("统计事件优先级失败")
@@ -1227,7 +1251,7 @@ func (s *OperationService) GetStats() (g.Map, error) {
 	stats["priorityCount"] = priorityCount
 
 	// 按类型统计
-	typeList, err := db.GroupBy(s.Dao.Columns.EventType).Fields(s.Dao.Columns.EventType+", count(*) as count").All()
+	typeList, err := db.GroupBy(s.Dao.Columns.EventType).Fields(s.Dao.Columns.EventType + ", count(*) as count").All()
 	if err != nil {
 		g.Log().Error(err)
 		return nil, myerrors.DbError("统计事件类型失败")
@@ -1243,7 +1267,6 @@ func (s *OperationService) GetStats() (g.Map, error) {
 
 // GetKanbanData 获取看板数据
 func (s *OperationService) GetKanbanData(req *opsdevmodel.OpsOperationEventKanbanSearchReq) (g.Map, error) {
-	// 查询所有非关闭状态的事件
 	db := s.Dao.FieldsEx(s.Dao.Columns.DeletedTime).Where(s.Dao.Columns.EventStatus+" != ?", opsdevmodel.EventStatusClosed)
 
 	if req.KeyWords != "" {
@@ -1269,13 +1292,11 @@ func (s *OperationService) GetKanbanData(req *opsdevmodel.OpsOperationEventKanba
 		db = db.Where(s.Dao.Columns.PriorityLevel, req.PriorityLevel)
 	}
 
-	// 待处理可查看全部,处理中/转研发/挂起仅查看当前登录人的
 	userId := s.GetCxtUserId()
 	if userId > 0 {
-		db = db.Where(fmt.Sprintf("(%s = ? OR %s = ?)", s.Dao.Columns.EventStatus, s.Dao.Columns.OpsUserId), opsdevmodel.EventStatusPending, userId)
+		db = db.Where(s.Dao.Columns.OpsUserId, userId)
 	}
 
-	// 排序
 	switch req.SortBy {
 	case "feedbackDateAsc":
 		db = db.Order(s.Dao.Columns.FeedbackDate + " asc")
@@ -1292,12 +1313,11 @@ func (s *OperationService) GetKanbanData(req *opsdevmodel.OpsOperationEventKanba
 		return nil, myerrors.DbError("查询看板数据失败")
 	}
 
-	// 按状态分组(处理中合并 20 + 30)
 	groups := map[string][]*opsdevmodel.OpsOperationEvent{
-		opsdevmodel.EventStatusPending:   make([]*opsdevmodel.OpsOperationEvent, 0), // 待处理
-		opsdevmodel.EventStatusProcessing: make([]*opsdevmodel.OpsOperationEvent, 0), // 处理中
-		opsdevmodel.EventStatusTransfer:  make([]*opsdevmodel.OpsOperationEvent, 0), // 转研发
-		opsdevmodel.EventStatusSuspended: make([]*opsdevmodel.OpsOperationEvent, 0), // 挂起
+		opsdevmodel.EventStatusPending:    make([]*opsdevmodel.OpsOperationEvent, 0),
+		opsdevmodel.EventStatusProcessing: make([]*opsdevmodel.OpsOperationEvent, 0),
+		opsdevmodel.EventStatusTransfer:   make([]*opsdevmodel.OpsOperationEvent, 0),
+		opsdevmodel.EventStatusSuspended:  make([]*opsdevmodel.OpsOperationEvent, 0),
 	}
 
 	for _, item := range list {
@@ -1414,3 +1434,77 @@ func (s *OperationService) AddRecord(req *opsdevmodel.AddRecordReq) error {
 		return nil
 	})
 }
+
+// buildProjectPermissionWhere 构建项目权限过滤条件
+// 通过 contract_id 关联 ops_delivery_project 表,应用与 DeliveryProjectService 相同的权限逻辑
+func (s *OperationService) buildProjectPermissionWhere() string {
+	// 全部可见角色(与 DeliveryProjectService.buildPermissionWhere 保持一致)
+	allVisibleRoles := []string{
+		"GeneralManager",                   // 总经理
+		"SalesDirector",                    // 销售总监
+		"ResearchAndDevelopmentDirector",   // 研发总监
+		"ResearchAndDevelopmentSupervisor", // 研发主管
+		"PersonnelDirector",                // 人事总监
+		"SysAdmin",                         // 系统管理员
+		"GeneralManagerAssistant",          // 总经理助理
+	}
+
+	for _, role := range allVisibleRoles {
+		if service.StringsContains(s.CxtUser.Roles, role) {
+			return "" // 全部可见,无额外条件
+		}
+	}
+
+	userId := s.GetCxtUserId()
+	if userId <= 0 {
+		return "1=0" // 无用户信息,不可见任何数据
+	}
+
+	// 构建权限条件:通过 contract_id 关联 ops_delivery_project 表
+	// 使用 EXISTS 子查询确保性能
+	where := "(1=0"
+
+	// 大区经理:查看授权区域内所有项目的运维事件
+	if service.StringsContains(s.CxtUser.Roles, "RegionalManager") {
+		where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM ops_delivery_project dp "+
+			"JOIN base_region_auth bra ON bra.city_id = dp.sales_region_id "+
+			"WHERE dp.contract_id = ops_operation_event.contract_id "+
+			"AND dp.deleted_time IS NULL "+
+			"AND bra.user_id = %d)", userId)
+	}
+
+	// 销售工程师:查看销售负责人是自己的项目的运维事件
+	if service.StringsContains(s.CxtUser.Roles, "SalesEngineer") {
+		where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM ops_delivery_project dp "+
+			"WHERE dp.contract_id = ops_operation_event.contract_id "+
+			"AND dp.deleted_time IS NULL "+
+			"AND dp.sales_user_id = %d)", userId)
+	}
+
+	// 运维工程师:查看运维负责人是自己的项目的运维事件
+	if service.StringsContains(s.CxtUser.Roles, "OperationsEngineer") {
+		where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM ops_delivery_project dp "+
+			"WHERE dp.contract_id = ops_operation_event.contract_id "+
+			"AND dp.deleted_time IS NULL "+
+			"AND dp.attribute4 = %d)", userId)
+	}
+
+	// 项目经理、项目交付经理:查看交付负责人是自己的项目的运维事件
+	if service.StringsContains(s.CxtUser.Roles, "ProjectManager") ||
+		service.StringsContains(s.CxtUser.Roles, "ProjectDeliveryManager") {
+		where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM ops_delivery_project dp "+
+			"WHERE dp.contract_id = ops_operation_event.contract_id "+
+			"AND dp.deleted_time IS NULL "+
+			"AND dp.delivery_user_id = %d)", userId)
+	}
+
+	// 被授权项目的运维事件(通过 ops_delivery_project_delegate 表)
+	where += fmt.Sprintf(" OR EXISTS (SELECT 1 FROM ops_delivery_project_delegate dpd "+
+		"JOIN ops_delivery_project dp ON dp.id = dpd.project_id "+
+		"WHERE dp.contract_id = ops_operation_event.contract_id "+
+		"AND dp.deleted_time IS NULL "+
+		"AND dpd.user_id = %d)", userId)
+
+	where += ")"
+	return where
+}

+ 193 - 6
opms_parent/app/service/opsdev/ops_event_task.go

@@ -48,7 +48,7 @@ func NewOpsEventTaskService(ctx context.Context) (svc *OpsEventTaskService, err
 
 // GetList 分页查询任务列表
 func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (total int, list []*opsdevmodel.OpsEventTaskRsp, err error) {
-	db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime)
+	db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime, s.TaskDao.Columns.TaskDesc)
 
 	// 项目ID筛选(单选)
 	if req.ProjectId > 0 {
@@ -60,6 +60,15 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
 		db = db.Where(s.TaskDao.Columns.ProjectId+" in (?)", req.ProjectIds)
 	}
 
+	// 产品线筛选:仅 projectId=0 且未指定 projectIds 时生效,通过 EXISTS 关联项目表
+	if req.ProductLine != "" && req.ProjectId <= 0 && len(req.ProjectIds) == 0 {
+		db = db.Where(fmt.Sprintf(
+			"EXISTS (SELECT 1 FROM %s p WHERE p.id = %s AND p.product_line = ? AND p.deleted_time IS NULL)",
+			s.ProjectDao.Table,
+			s.TaskDao.Columns.ProjectId,
+		), req.ProductLine)
+	}
+
 	// 关联事件ID筛选
 	if req.EventId > 0 {
 		db = db.Where(s.TaskDao.Columns.EventId, req.EventId)
@@ -1421,12 +1430,23 @@ func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsd
 			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
+		  AND (
+			-- 有结束日期的任务:时间区间与查询周有交集
+			(plan_end_time IS NOT NULL AND plan_end_time != ''
+			 AND plan_start_time <= ? AND plan_end_time >= ?)
+			OR
+			-- 无结束日期的任务:仅按开始日期筛选(原逻辑)
+			((plan_end_time IS NULL OR plan_end_time = '')
+			 AND plan_start_time >= ? AND plan_start_time <= ?)
+		  )
 		ORDER BY plan_start_time, priority
 	`
 	var taskRows []taskRow
-	err := db.GetScan(&taskRows, taskSQL, userId, startDate, endDate+" 23:59:59")
+	err := db.GetScan(&taskRows, taskSQL, userId,
+		endDate+" 23:59:59", startDate,
+		startDate, endDate+" 23:59:59",
+	)
 	if err != nil {
 		g.Log().Error(err)
 		return nil, myerrors.DbError("查询任务数据失败")
@@ -1477,8 +1497,7 @@ func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsd
 	taskMap := make(map[string][]*opsdevmodel.DashboardTaskRsp, 7)
 	for i := range taskRows {
 		row := &taskRows[i]
-		date := row.PlanStartDate
-		taskMap[date] = append(taskMap[date], &opsdevmodel.DashboardTaskRsp{
+		taskItem := &opsdevmodel.DashboardTaskRsp{
 			Id:               row.Id,
 			TaskNo:           row.TaskNo,
 			TaskTitle:        row.TaskTitle,
@@ -1489,7 +1508,24 @@ func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsd
 			ActualWorkHour:   row.ActualWorkHour,
 			EstimateWorkHour: row.EstimateWorkHour,
 			PlanEndTime:      row.PlanEndTime,
-		})
+		}
+
+		taskEndDate := row.PlanEndTime
+		if taskEndDate == "" {
+			taskMap[row.PlanStartDate] = append(taskMap[row.PlanStartDate], taskItem)
+			continue
+		}
+
+		start := gtime.New(row.PlanStartDate)
+		end := gtime.New(taskEndDate)
+		daysCount := int(end.Sub(start).Hours()/24) + 1
+		for d := 0; d < daysCount; d++ {
+			current := start.AddDate(0, 0, d)
+			dateStr := current.Format("Y-m-d")
+			if dateStr >= startDate && dateStr <= endDate {
+				taskMap[dateStr] = append(taskMap[dateStr], taskItem)
+			}
+		}
 	}
 
 	overdueCount, err := db.GetValue(
@@ -1669,4 +1705,155 @@ func (s *OpsEventTaskService) GetScheduleStats(req *opsdevmodel.OpsEventTaskSche
 	return &opsdevmodel.OpsEventTaskScheduleStatRsp{List: list}, nil
 }
 
+func (s *OpsEventTaskService) Export(ctx context.Context, req *opsdevmodel.OpsEventTaskExportReq) (content *opsdevmodel.OpsEventTaskExportContent, err error) {
+	db := s.TaskDao.FieldsEx(s.TaskDao.Columns.DeletedTime, s.TaskDao.Columns.TaskDesc)
+
+	if req.ProjectId > 0 {
+		db = db.Where(s.TaskDao.Columns.ProjectId, req.ProjectId)
+	}
+	if len(req.ProjectIds) > 0 {
+		db = db.Where(s.TaskDao.Columns.ProjectId+" in (?)", req.ProjectIds)
+	}
+	if req.ProductLine != "" && req.ProjectId <= 0 && len(req.ProjectIds) == 0 {
+		db = db.Where(fmt.Sprintf(
+			"EXISTS (SELECT 1 FROM %s p WHERE p.id = %s AND p.product_line = ? AND p.deleted_time IS NULL)",
+			s.ProjectDao.Table, s.TaskDao.Columns.ProjectId,
+		), req.ProductLine)
+	}
+	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.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.PlanStartDateStart != "" {
+		db = db.Where(s.TaskDao.Columns.PlanStartTime+" >= ?", req.PlanStartDateStart+" 00:00:00")
+	}
+	if req.PlanStartDateEnd != "" {
+		db = db.Where(s.TaskDao.Columns.PlanStartTime+" <= ?", req.PlanStartDateEnd+" 23:59:59")
+	}
+	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")
+	}
+
+	var entityList []*opsdevmodel.OpsEventTask
+	err = db.Order(s.TaskDao.Columns.CreatedTime + " desc").Scan(&entityList)
+	if err != nil {
+		g.Log().Error(err)
+		return nil, myerrors.DbError("查询任务列表失败")
+	}
+
+	taskTypeMap := map[string]string{
+		"10": "需求评审",
+		"20": "功能开发",
+		"25": "缺陷修复",
+		"30": "功能测试",
+		"35": "BUG",
+		"38": "系统发版",
+		"40": "系统发版",
+		"41": "硬件安装",
+		"42": "硬件发货",
+	}
+	taskStatusMap := map[string]string{
+		"10": "待处理",
+		"20": "处理中",
+		"25": "暂停",
+		"30": "已完成",
+		"70": "阻塞",
+		"90": "作废",
+	}
+	defectTypeMap := map[string]string{
+		"10": "前端",
+		"20": "后端",
+	}
+
+	exportDataList := make([]map[string]interface{}, 0)
+	for _, task := range entityList {
+		planStartTime := ""
+		if task.PlanStartTime != nil && !task.PlanStartTime.IsZero() {
+			planStartTime = task.PlanStartTime.Format("Y-m-d")
+		}
+		planEndTime := ""
+		if task.PlanEndTime != nil && !task.PlanEndTime.IsZero() {
+			planEndTime = task.PlanEndTime.Format("Y-m-d")
+		}
+		completeTime := ""
+		if task.CompleteTime != nil && !task.CompleteTime.IsZero() {
+			completeTime = task.CompleteTime.Format("Y-m-d")
+		}
+		createdTime := ""
+		if task.CreatedTime != nil && !task.CreatedTime.IsZero() {
+			createdTime = task.CreatedTime.Format("Y-m-d H:i:s")
+		}
+
+		attribute2Label := "-"
+		if task.Attribute2 == "10" {
+			attribute2Label = "是"
+		} else if task.Attribute2 == "20" {
+			attribute2Label = "否"
+		}
+
+		exportData := map[string]interface{}{
+			"taskNo":           task.TaskNo,
+			"taskTitle":        task.TaskTitle,
+			"taskType":         taskTypeMap[task.TaskType],
+			"taskStatus":       taskStatusMap[task.TaskStatus],
+			"functionName":     task.FunctionName,
+			"opsUserName":      task.OpsUserName,
+			"planStartTime":    planStartTime,
+			"planEndTime":      planEndTime,
+			"completeTime":     completeTime,
+			"estimateWorkHour": gconv.String(task.EstimateWorkHour),
+			"actualWorkHour":   gconv.String(task.ActualWorkHour),
+			"defectType":       defectTypeMap[task.DefectType],
+			"attribute2":       attribute2Label,
+			"releaseVersion":   task.ReleaseVersion,
+			"projectName":      task.ProjectName,
+			"createdName":      task.CreatedName,
+			"createdTime":      createdTime,
+		}
+		exportDataList = append(exportDataList, exportData)
+	}
+
+	exportContent := new(opsdevmodel.OpsEventTaskExportContent)
+	contentBase64, err := service.CommonExportExcel(ctx, "软件交付任务", opsdevmodel.OpsEventTaskExportData{}, exportDataList)
+	if err != nil {
+		g.Log().Error(err)
+		return nil, myerrors.DbError("导出任务数据失败")
+	}
+	exportContent.Content = contentBase64
+	return exportContent, nil
+}
 

+ 17 - 2
opms_parent/app/service/opsdev/project_inventory.go

@@ -58,6 +58,9 @@ func (s *ProjectInventoryService) GetList(req *opsdev.ProjectInventorySearchReq)
 	if req.ProjectStatus != "" {
 		db = db.Where("ops_delivery_project.project_status", req.ProjectStatus)
 	}
+	if len(req.ProjectStatusList) > 0 {
+		db = db.WhereIn("ops_delivery_project.project_status", req.ProjectStatusList)
+	}
 	if req.DeliveryNode != "" {
 		db = db.Where("ops_delivery_project.delivery_node", req.DeliveryNode)
 	}
@@ -67,6 +70,9 @@ func (s *ProjectInventoryService) GetList(req *opsdev.ProjectInventorySearchReq)
 	if req.SalesUserId > 0 {
 		db = db.Where("ops_delivery_project.sales_user_id", req.SalesUserId)
 	}
+	if req.OpsManagerUserId > 0 {
+		db = db.Where("ops_delivery_project.attribute4", req.OpsManagerUserId)
+	}
 	if req.PlanDeliveryTimeStart != "" {
 		db = db.WhereGTE("ops_delivery_project.plan_delivery_time", req.PlanDeliveryTimeStart)
 	}
@@ -164,10 +170,12 @@ func (s *ProjectInventoryService) GetProjectManagers(roleType string) ([]*opsdev
 		roleKeys = []string{"ProjectDeliveryManager"}
 	case "sales":
 		roleKeys = []string{"SalesEngineer"}
+	case "ops":
+		roleKeys = []string{"OperationsEngineer"}
 	case "all":
-		roleKeys = []string{"ProjectManager", "ProjectDeliveryManager"}
+		roleKeys = []string{"ProjectManager", "ProjectDeliveryManager", "OperationsEngineer"}
 	default:
-		roleKeys = []string{"ProjectManager", "ProjectDeliveryManager"}
+		roleKeys = []string{"ProjectManager", "ProjectDeliveryManager", "OperationsEngineer"}
 	}
 
 	db := s.Dao.DB.Table("sys_user")
@@ -185,6 +193,7 @@ func (s *ProjectInventoryService) GetProjectManagers(roleType string) ([]*opsdev
 		"sys_user.user_name",
 		"sys_user.nick_name",
 		"sys_dept.dept_name",
+		"sys_role.role_name",
 	).Scan(&records)
 	if err != nil {
 		return nil, err
@@ -267,6 +276,9 @@ func (s *ProjectInventoryService) Export(ctx context.Context, req *opsdev.Projec
 	if req.ProjectStatus != "" {
 		db = db.Where("ops_delivery_project.project_status", req.ProjectStatus)
 	}
+	if len(req.ProjectStatusList) > 0 {
+		db = db.WhereIn("ops_delivery_project.project_status", req.ProjectStatusList)
+	}
 	if req.DeliveryNode != "" {
 		db = db.Where("ops_delivery_project.delivery_node", req.DeliveryNode)
 	}
@@ -276,6 +288,9 @@ func (s *ProjectInventoryService) Export(ctx context.Context, req *opsdev.Projec
 	if req.SalesUserId > 0 {
 		db = db.Where("ops_delivery_project.sales_user_id", req.SalesUserId)
 	}
+	if req.OpsManagerUserId > 0 {
+		db = db.Where("ops_delivery_project.attribute4", req.OpsManagerUserId)
+	}
 	if req.PlanDeliveryTimeStart != "" {
 		db = db.WhereGTE("ops_delivery_project.plan_delivery_time", req.PlanDeliveryTimeStart)
 	}

BIN
opms_parent/oms_parent_v2


+ 79 - 0
opms_parent/schema/opsdev_index_optimization.sql

@@ -0,0 +1,79 @@
+-- ============================================================================
+-- opsdev 模块数据库索引优化脚本
+-- 生成时间: 2026-05-28
+-- 目标库: dashoo_oms
+-- 说明: 当前核心查询表几乎零索引,全部走全表扫描,前台查询慢。
+--       数据量不大(最大 4231 行),索引开销可忽略。
+-- 注意: 每条 ALTER 独立执行,单条失败不影响后续。
+-- ============================================================================
+
+-- ============================================================================
+-- 1. ops_delivery_project — 交付项目(所有列表查询核心表,0 索引)
+--    影响: GetList / GetListAll / GetDelegatedProjectList / 项目盘点
+-- ============================================================================
+ALTER TABLE ops_delivery_project ADD INDEX idx_deleted_delivery_user (deleted_time, delivery_user_id);
+ALTER TABLE ops_delivery_project ADD INDEX idx_deleted_project_status (deleted_time, project_status);
+ALTER TABLE ops_delivery_project ADD INDEX idx_deleted_product_line (deleted_time, product_line);
+ALTER TABLE ops_delivery_project ADD INDEX idx_deleted_sales_user (deleted_time, sales_user_id);
+ALTER TABLE ops_delivery_project ADD INDEX idx_deleted_contract (deleted_time, contract_id);
+ALTER TABLE ops_delivery_project ADD INDEX idx_deleted_attr4 (deleted_time, attribute4);
+ALTER TABLE ops_delivery_project ADD INDEX idx_contract_no (contract_no);
+
+-- ============================================================================
+-- 2. ops_operation_event — 运维事件(列表/历史/看板/统计,0 索引)
+--    影响: GetList / GetHistoryList / GetKanbanData / GetStats / Export
+-- ============================================================================
+ALTER TABLE ops_operation_event ADD INDEX idx_event_status_deleted (event_status, deleted_time);
+ALTER TABLE ops_operation_event ADD INDEX idx_ops_user_deleted (ops_user_id, deleted_time);
+ALTER TABLE ops_operation_event ADD INDEX idx_feedback_date (feedback_date);
+ALTER TABLE ops_operation_event ADD INDEX idx_contract_deleted (contract_id, deleted_time);
+
+-- ============================================================================
+-- 3. ops_event_task_work_hour — 研发任务工时(看板核心表,0 索引)
+--    影响: GetDashboardData (周视图工时汇总)
+-- ============================================================================
+ALTER TABLE ops_event_task_work_hour ADD INDEX idx_user_date_deleted (ops_user_id, actual_work_date, deleted_time);
+ALTER TABLE ops_event_task_work_hour ADD INDEX idx_task_deleted (task_id, deleted_time);
+
+-- ============================================================================
+-- 4. ops_event_task — 研发任务(已有部分索引,补关键复合索引)
+--    影响: GetList / GetDashboardData / GetScheduleStats
+-- ============================================================================
+ALTER TABLE ops_event_task ADD INDEX idx_ops_user_plan (ops_user_id, deleted_time, plan_start_time);
+ALTER TABLE ops_event_task ADD INDEX idx_plan_end_status (plan_end_time, task_status, deleted_time);
+ALTER TABLE ops_event_task ADD INDEX idx_event_type_id (event_type, event_id, deleted_time);
+
+-- ============================================================================
+-- 5. 记录表 — 外键查询全表扫描
+-- ============================================================================
+ALTER TABLE ops_event_task_record ADD INDEX idx_task_id (task_id);
+ALTER TABLE ops_delivery_project_event_record ADD INDEX idx_delivery_event_id (delivery_event_id);
+ALTER TABLE ops_operation_event_record ADD INDEX idx_event_id (event_id);
+
+-- ============================================================================
+-- 6. 附件表 — 外键查询全表扫描
+-- ============================================================================
+ALTER TABLE ops_delivery_project_event_attachment ADD INDEX idx_delivery_event_id (delivery_event_id);
+ALTER TABLE ops_delivery_project_event_attachment ADD INDEX idx_event_record_id (event_record_id);
+ALTER TABLE ops_operation_event_attachment ADD INDEX idx_event_id (event_id);
+ALTER TABLE ops_operation_event_attachment ADD INDEX idx_event_record_id (event_record_id);
+ALTER TABLE ops_event_task_attachment ADD INDEX idx_task_id (task_id);
+ALTER TABLE ops_event_task_attachment ADD INDEX idx_task_record_id (task_record_id);
+
+-- ============================================================================
+-- 7. ops_operation_work_hour — 运维工时(统计 UNION ALL 查询)
+-- ============================================================================
+ALTER TABLE ops_operation_work_hour ADD INDEX idx_work_date_deleted (work_date, deleted_time);
+
+-- ============================================================================
+-- 8. ctr_contract — 合同表(项目盘点 LEFT JOIN)
+-- ============================================================================
+ALTER TABLE ctr_contract ADD INDEX idx_contract_code (contract_code);
+
+-- ============================================================================
+-- 9. 补充: base_region_auth 索引可能不匹配查询条件
+--    当前索引: (user_id, sale_region_id)
+--    实际查询: user_id = ? AND city_id = sales_region_id (用的是 city_id 列)
+--    当前数据量小影响不大,但建议确认后调整。
+--    ALTER TABLE base_region_auth ADD INDEX idx_user_city (user_id, city_id);
+-- ============================================================================