From 0c67feb154ff2026772c98c18670594417942fe5 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Sat, 15 Aug 2026 04:47:40 +0800 Subject: [PATCH 1/2] feat: add project-level ops type dictionary Introduce CRUD APIs, storage, default seeds and dms-common client helpers for ops types, aligned with environment tags. Co-authored-by: Cursor --- api/dms/service/v1/ops_type.go | 52 + api/swagger.json | 1768 +++++++++++++++-- api/swagger.yaml | 1043 +++++++++- internal/apiserver/service/dms_controller.go | 3 + .../apiserver/service/ops_type_controller.go | 148 ++ internal/apiserver/service/router.go | 6 + internal/dms/biz/ops_type.go | 250 +++ internal/dms/biz/ops_type_test.go | 300 +++ internal/dms/biz/project.go | 15 +- internal/dms/biz/repo_fields.go | 1 + internal/dms/service/ops_type.go | 101 + internal/dms/service/service.go | 8 +- internal/dms/storage/model/model.go | 10 + internal/dms/storage/ops_type.go | 122 ++ internal/pkg/locale/active.en.toml | 5 + internal/pkg/locale/active.zh.toml | 5 + internal/pkg/locale/message_zh.go | 9 + pkg/dms-common/api/dms/v1/ops_type.go | 25 + pkg/dms-common/api/dms/v1/router.go | 5 + pkg/dms-common/dmsobject/ops_type.go | 37 + 20 files changed, 3687 insertions(+), 226 deletions(-) create mode 100644 api/dms/service/v1/ops_type.go create mode 100644 internal/apiserver/service/ops_type_controller.go create mode 100644 internal/dms/biz/ops_type.go create mode 100644 internal/dms/biz/ops_type_test.go create mode 100644 internal/dms/service/ops_type.go create mode 100644 internal/dms/storage/ops_type.go create mode 100644 pkg/dms-common/api/dms/v1/ops_type.go create mode 100644 pkg/dms-common/dmsobject/ops_type.go diff --git a/api/dms/service/v1/ops_type.go b/api/dms/service/v1/ops_type.go new file mode 100644 index 000000000..75284e320 --- /dev/null +++ b/api/dms/service/v1/ops_type.go @@ -0,0 +1,52 @@ +package v1 + +import ( + base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" + dmsCommonV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" +) + +// swagger:model +type CreateOpsTypeReq struct { + // swagger:ignore + ProjectUID string `param:"project_uid" json:"project_uid" validate:"required"` + Name string `json:"ops_type_name" validate:"required,min=1,max=50"` +} + +// swagger:model +type UpdateOpsTypeReq struct { + // swagger:ignore + OpsTypeUID string `param:"ops_type_uid" json:"ops_type_uid" validate:"required"` + // swagger:ignore + ProjectUID string `param:"project_uid" json:"project_uid" validate:"required"` + + Name string `json:"ops_type_name" validate:"required,min=1,max=50"` +} + +// swagger:parameters ListOpsTypes +type ListOpsTypeReq struct { + // in:path + // Required: true + ProjectUID string `param:"project_uid" json:"project_uid" validate:"required"` + // in:query + PageIndex uint32 `query:"page_index" json:"page_index"` + // in:query + // Required: true + PageSize uint32 `query:"page_size" json:"page_size" validate:"required"` +} + +// swagger:model ListOpsTypesReply +type ListOpsTypesReply struct { + Data []*dmsCommonV1.OpsType `json:"data"` + Total int64 `json:"total_nums"` + base.GenericResp +} + +// swagger:parameters DeleteOpsType +type DeleteOpsTypeReq struct { + // in:path + // Required: true + OpsTypeUID string `param:"ops_type_uid" json:"ops_type_uid" validate:"required"` + // in:path + // Required: true + ProjectUID string `param:"project_uid" json:"project_uid" validate:"required"` +} diff --git a/api/swagger.json b/api/swagger.json index 185eb5c74..56367a26b 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -80,7 +80,7 @@ "parameters": [ { "description": "Update a companynotice", - "name": "company_notice", + "name": "body", "in": "body", "required": true, "schema": { @@ -104,6 +104,176 @@ } } }, + "/v1/dms/configurations/access_restriction": { + "get": { + "tags": [ + "Configuration" + ], + "summary": "Get access restriction configuration.", + "operationId": "GetAccessRestriction", + "responses": { + "200": { + "description": "GetAccessRestrictionReply", + "schema": { + "$ref": "#/definitions/GetAccessRestrictionReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + }, + "patch": { + "tags": [ + "Configuration" + ], + "summary": "Update access restriction switch.", + "operationId": "UpdateAccessRestriction", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateAccessRestrictionReq" + } + } + ], + "responses": { + "200": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/configurations/access_restriction/client_ip": { + "get": { + "tags": [ + "Configuration" + ], + "summary": "Get current request client IP for access restriction.", + "operationId": "GetAccessRestrictionClientIP", + "responses": { + "200": { + "description": "GetAccessRestrictionClientIPReply", + "schema": { + "$ref": "#/definitions/GetAccessRestrictionClientIPReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/configurations/access_restriction/rules": { + "post": { + "tags": [ + "Configuration" + ], + "summary": "Create access whitelist rule.", + "operationId": "CreateAccessWhitelistRule", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateAccessWhitelistRuleReq" + } + } + ], + "responses": { + "200": { + "description": "CreateAccessWhitelistRuleReply", + "schema": { + "$ref": "#/definitions/CreateAccessWhitelistRuleReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/configurations/access_restriction/rules/{rule_uid}": { + "put": { + "tags": [ + "Configuration" + ], + "summary": "Update access whitelist rule.", + "operationId": "UpdateAccessWhitelistRule", + "parameters": [ + { + "type": "string", + "name": "rule_uid", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateAccessWhitelistRuleReq" + } + } + ], + "responses": { + "200": { + "description": "UpdateAccessWhitelistRuleReply", + "schema": { + "$ref": "#/definitions/UpdateAccessWhitelistRuleReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + }, + "delete": { + "tags": [ + "Configuration" + ], + "summary": "Delete access whitelist rule.", + "operationId": "DeleteAccessWhitelistRule", + "responses": { + "200": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/configurations/feishu": { "get": { "tags": [ @@ -135,7 +305,7 @@ "parameters": [ { "description": "update feishu configuration", - "name": "update_feishu_configuration", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UpdateFeishuConfigurationReq" @@ -168,7 +338,7 @@ "parameters": [ { "description": "test feishu configuration", - "name": "test_feishu_configuration", + "name": "body", "in": "body", "required": true, "schema": { @@ -223,7 +393,7 @@ "parameters": [ { "description": "update ldap configuration", - "name": "ldap", + "name": "body", "in": "body", "required": true, "schema": { @@ -391,7 +561,7 @@ "parameters": [ { "description": "update login configuration", - "name": "login", + "name": "body", "in": "body", "required": true, "schema": { @@ -469,7 +639,7 @@ "parameters": [ { "description": "update oauth2 configuration", - "name": "oauth2", + "name": "body", "in": "body", "required": true, "schema": { @@ -524,7 +694,7 @@ "parameters": [ { "description": "update sms configuration", - "name": "update_sms_configuration", + "name": "body", "in": "body", "required": true, "schema": { @@ -558,7 +728,7 @@ "parameters": [ { "description": "user name", - "name": "username", + "name": "body", "in": "body", "required": true, "schema": { @@ -592,7 +762,7 @@ "parameters": [ { "description": "test sms configuration", - "name": "test_sms_configuration", + "name": "body", "in": "body", "required": true, "schema": { @@ -626,7 +796,7 @@ "parameters": [ { "description": "verify sms code", - "name": "code", + "name": "body", "in": "body", "required": true, "schema": { @@ -635,7 +805,7 @@ }, { "description": "user name", - "name": "username", + "name": "body", "in": "body", "required": true, "schema": { @@ -690,7 +860,7 @@ "parameters": [ { "description": "update smtp configuration", - "name": "smtp_configuration", + "name": "body", "in": "body", "required": true, "schema": { @@ -724,7 +894,7 @@ "parameters": [ { "description": "test smtp configuration", - "name": "test_smtp_configuration", + "name": "body", "in": "body", "required": true, "schema": { @@ -802,7 +972,7 @@ "parameters": [ { "description": "更新系统变量配置", - "name": "system_variables", + "name": "body", "in": "body", "required": true, "schema": { @@ -857,7 +1027,7 @@ "parameters": [ { "description": "webhook configuration", - "name": "webhook_config", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UpdateWebHookConfigurationReq" @@ -934,7 +1104,7 @@ "parameters": [ { "description": "update wechat configuration", - "name": "update_wechat_configuration", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UpdateWeChatConfigurationReq" @@ -967,7 +1137,7 @@ "parameters": [ { "description": "test wechat configuration", - "name": "test_wechat_configuration", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/TestWeChatConfigurationReq" @@ -997,99 +1167,272 @@ ], "summary": "Get global data export workflows.", "operationId": "GetGlobalDataExportWorkflows", - "responses": { - "200": { - "description": "GetGlobalDataExportWorkflowsReply", - "schema": { - "$ref": "#/definitions/GetGlobalDataExportWorkflowsReply" - } - }, - "default": { - "description": "GenericResp", - "schema": { - "$ref": "#/definitions/GenericResp" - } - } - } - } - }, - "/v1/dms/db_service_sync_tasks": { - "get": { - "tags": [ - "DBServiceSyncTask" - ], - "summary": "List database synchronization tasks.", - "operationId": "ListDBServiceSyncTasks", - "responses": { - "200": { - "description": "ListDBServiceSyncTasksReply", - "schema": { - "$ref": "#/definitions/ListDBServiceSyncTasksReply" - } - }, - "default": { - "description": "GenericResp", - "schema": { - "$ref": "#/definitions/GenericResp" - } - } - } - }, - "post": { - "tags": [ - "DBServiceSyncTask" - ], - "summary": "Add database synchronization task.", - "operationId": "AddDBServiceSyncTask", "parameters": [ { - "description": "Add new db service sync task", - "name": "db_service_sync_task", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/AddDBServiceSyncTaskReq" - } - } - ], - "responses": { - "200": { - "description": "AddDBServiceReply", - "schema": { - "$ref": "#/definitions/AddDBServiceReply" - } + "type": "array", + "items": { + "enum": [ + "wait_for_approve", + "wait_for_export", + "exporting", + "rejected", + "cancel", + "failed", + "finish" + ], + "type": "string" + }, + "x-go-name": "FilterStatusList", + "description": "filter status list", + "name": "filter_status_list", + "in": "query" }, - "default": { - "description": "GenericResp", - "schema": { - "$ref": "#/definitions/GenericResp" - } - } - } - } - }, - "/v1/dms/db_service_sync_tasks/tips": { - "get": { - "tags": [ - "DBServiceSyncTask" - ], - "summary": "List database synchronization task tips.", - "operationId": "ListDBServiceSyncTaskTips", - "responses": { - "200": { - "description": "ListDBServiceSyncTaskTipsReply", - "schema": { - "$ref": "#/definitions/ListDBServiceSyncTaskTipsReply" - } + { + "type": "string", + "x-go-name": "FilterDBServiceUid", + "description": "filter db service uid", + "name": "filter_db_service_uid", + "in": "query" }, - "default": { - "description": "GenericResp", - "schema": { - "$ref": "#/definitions/GenericResp" - } - } - } - } + { + "type": "string", + "x-go-name": "FilterCurrentStepAssigneeUserId", + "description": "filter project priority", + "name": "filter_current_step_assignee_user_id", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "FilterProjectUids", + "description": "filter project uids", + "name": "filter_project_uids", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterProjectUid", + "description": "filter project uid", + "name": "filter_project_uid", + "in": "query" + }, + { + "type": "integer", + "format": "uint32", + "x-go-name": "PageSize", + "description": "the maximum count of member to be returned", + "name": "page_size", + "in": "query", + "required": true + }, + { + "type": "integer", + "format": "uint32", + "x-go-name": "PageIndex", + "description": "the offset of members to be returned, default is 0", + "name": "page_index", + "in": "query" + }, + { + "enum": [ + "wait_for_approve", + "wait_for_export", + "exporting", + "rejected", + "cancel", + "failed", + "finish" + ], + "type": "string", + "x-go-enum-desc": "wait_for_approve DataExportWorkflowStatusWaitForApprove\nwait_for_export DataExportWorkflowStatusWaitForExport\nexporting DataExportWorkflowStatusWaitForExporting\nrejected DataExportWorkflowStatusRejected\ncancel DataExportWorkflowStatusCancel\nfailed DataExportWorkflowStatusFailed\nfinish DataExportWorkflowStatusFinish", + "x-go-name": "FilterByStatus", + "description": "filter the status\nwait_for_approve DataExportWorkflowStatusWaitForApprove\nwait_for_export DataExportWorkflowStatusWaitForExport\nexporting DataExportWorkflowStatusWaitForExporting\nrejected DataExportWorkflowStatusRejected\ncancel DataExportWorkflowStatusCancel\nfailed DataExportWorkflowStatusFailed\nfinish DataExportWorkflowStatusFinish", + "name": "filter_by_status", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterByCreateUserUid", + "description": "filter create user id", + "name": "filter_by_create_user_uid", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterCurrentStepAssigneeUserUid", + "description": "filter current assignee user id", + "name": "filter_current_step_assignee_user_uid", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterByDBServiceUid", + "description": "filter db_service id", + "name": "filter_by_db_service_uid", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FuzzyKeyword", + "description": "filter fuzzy key word for id/name", + "name": "fuzzy_keyword", + "in": "query" + }, + { + "type": "boolean", + "x-go-name": "CheckUserCanAccess", + "description": "enable OR-based self-relevant filtering (creator OR assignee OR viewable db_service)", + "name": "check_user_can_access", + "in": "query" + }, + { + "type": "string", + "x-go-name": "CurrentUserID", + "description": "current user id for check_user_can_access filter", + "name": "current_user_id", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ViewableDBServiceUids", + "description": "db_service UIDs with view_others_workflow permission for check_user_can_access", + "name": "viewable_db_service_uids", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterCreateTimeFrom", + "description": "filter create time from", + "name": "filter_create_time_from", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterCreateTimeTo", + "description": "filter create time to", + "name": "filter_create_time_to", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterUpdateTimeFrom", + "description": "filter update time from", + "name": "filter_update_time_from", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterUpdateTimeTo", + "description": "filter update time to", + "name": "filter_update_time_to", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterByOpsTypeUid", + "description": "filter by ops type uid(单选;空则不筛;不提供「未设置」;与项目内列表同构)", + "name": "filter_by_ops_type_uid", + "in": "query" + } + ], + "responses": { + "200": { + "description": "GetGlobalDataExportWorkflowsReply", + "schema": { + "$ref": "#/definitions/GetGlobalDataExportWorkflowsReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/db_service_sync_tasks": { + "get": { + "tags": [ + "DBServiceSyncTask" + ], + "summary": "List database synchronization tasks.", + "operationId": "ListDBServiceSyncTasks", + "responses": { + "200": { + "description": "ListDBServiceSyncTasksReply", + "schema": { + "$ref": "#/definitions/ListDBServiceSyncTasksReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + }, + "post": { + "tags": [ + "DBServiceSyncTask" + ], + "summary": "Add database synchronization task.", + "operationId": "AddDBServiceSyncTask", + "parameters": [ + { + "description": "Add new db service sync task", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AddDBServiceSyncTaskReq" + } + } + ], + "responses": { + "200": { + "description": "AddDBServiceReply", + "schema": { + "$ref": "#/definitions/AddDBServiceReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/db_service_sync_tasks/tips": { + "get": { + "tags": [ + "DBServiceSyncTask" + ], + "summary": "List database synchronization task tips.", + "operationId": "ListDBServiceSyncTaskTips", + "responses": { + "200": { + "description": "ListDBServiceSyncTaskTipsReply", + "schema": { + "$ref": "#/definitions/ListDBServiceSyncTaskTipsReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } }, "/v1/dms/db_service_sync_tasks/{db_service_sync_task_uid}": { "get": { @@ -1138,7 +1481,7 @@ }, { "description": "update db service sync task", - "name": "db_service_sync_task", + "name": "body", "in": "body", "required": true, "schema": { @@ -1467,7 +1810,7 @@ "parameters": [ { "description": "add gateway", - "name": "add_gateway", + "name": "body", "in": "body", "required": true, "schema": { @@ -1561,7 +1904,7 @@ }, { "description": "update gateway", - "name": "update_gateway", + "name": "body", "in": "body", "required": true, "schema": { @@ -1675,7 +2018,7 @@ { "x-go-name": "Notification", "description": "notification", - "name": "notification", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/Notification" @@ -1739,7 +2082,7 @@ "operationId": "BindOauth2User", "parameters": [ { - "name": "BindOauth2UserReq", + "name": "body", "in": "body", "required": true, "schema": { @@ -1927,7 +2270,7 @@ "parameters": [ { "description": "Add new operation record", - "name": "operation_record", + "name": "body", "in": "body", "required": true, "schema": { @@ -2082,7 +2425,7 @@ "parameters": [ { "description": "Register dms plugin", - "name": "plugin", + "name": "body", "in": "body", "required": true, "schema": { @@ -2214,7 +2557,7 @@ "parameters": [ { "description": "Add new Project", - "name": "project", + "name": "body", "in": "body", "required": true, "schema": { @@ -2292,7 +2635,7 @@ "parameters": [ { "description": "business tag to be created", - "name": "business_tag", + "name": "body", "in": "body", "required": true, "schema": { @@ -2333,7 +2676,7 @@ }, { "description": "the business tag to be updated", - "name": "business_tag", + "name": "body", "in": "body", "required": true, "schema": { @@ -2397,7 +2740,7 @@ "parameters": [ { "description": "check db_services have enough privileges", - "name": "db_services", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/CheckDBServicesPrivilegesReq" @@ -2511,6 +2854,21 @@ "description": "filter fuzzy key word for id/name", "name": "fuzzy_keyword", "in": "query" + }, + { + "type": "integer", + "format": "uint64", + "x-go-name": "FilterWorkflowTemplateId", + "description": "filter by workflow template id", + "name": "filter_workflow_template_id", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterByOpsTypeUid", + "description": "filter by ops type uid(单选;空则不筛;不提供「未设置」)", + "name": "filter_by_ops_type_uid", + "in": "query" } ], "responses": { @@ -2539,7 +2897,7 @@ "parameters": [ { "description": "check db_service is connectable", - "name": "db_services", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/DBServiceConnectionReq" @@ -2572,7 +2930,7 @@ "parameters": [ { "description": "check db_services is connectable", - "name": "db_services", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/DBServicesConnectionReq" @@ -2653,7 +3011,7 @@ "parameters": [ { "description": "import projects", - "name": "projects", + "name": "body", "in": "body", "required": true, "schema": { @@ -2688,7 +3046,7 @@ "parameters": [ { "description": "new db services", - "name": "db_services", + "name": "body", "in": "body", "required": true, "schema": { @@ -2876,7 +3234,7 @@ }, { "description": "Update a project", - "name": "project", + "name": "body", "in": "body", "required": true, "schema": { @@ -3206,7 +3564,7 @@ }, { "description": "add data export workflow", - "name": "data_export_tasks", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/AddDataExportTaskReq" @@ -3451,6 +3809,21 @@ "description": "filter fuzzy key word for id/name", "name": "fuzzy_keyword", "in": "query" + }, + { + "type": "integer", + "format": "uint64", + "x-go-name": "FilterWorkflowTemplateId", + "description": "filter by workflow template id", + "name": "filter_workflow_template_id", + "in": "query" + }, + { + "type": "string", + "x-go-name": "FilterByOpsTypeUid", + "description": "filter by ops type uid(单选;空则不筛;不提供「未设置」)", + "name": "filter_by_ops_type_uid", + "in": "query" } ], "responses": { @@ -3484,7 +3857,7 @@ }, { "description": "add data export workflow", - "name": "data_export_workflow", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/AddDataExportWorkflowReq" @@ -3523,7 +3896,7 @@ "required": true }, { - "name": "payload", + "name": "body", "in": "body", "required": true, "schema": { @@ -3547,20 +3920,63 @@ } } }, - "/v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}": { + "/v1/dms/projects/{project_uid}/data_export_workflows/workflow_template_used": { "get": { + "description": "Soft-deleted SQLE templates do not block already-created workflow runtime; this is a best-effort hint.", "tags": [ "DataExportWorkflows" ], - "summary": "Get data_export workflow.", - "operationId": "GetDataExportWorkflow", + "summary": "Check whether a data-export workflow template id is referenced by local workflows.", + "operationId": "CheckDataExportWorkflowTemplateUsed", "parameters": [ { "type": "string", - "x-go-name": "DataExportWorkflowUid", - "name": "data_export_workflow_uid", - "in": "path", - "required": true + "x-go-name": "ProjectUid", + "description": "project id", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "uint64", + "x-go-name": "WorkflowTemplateId", + "description": "workflow template id", + "name": "workflow_template_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "CheckDataExportWorkflowTemplateUsedReply", + "schema": { + "$ref": "#/definitions/CheckDataExportWorkflowTemplateUsedReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}": { + "get": { + "tags": [ + "DataExportWorkflows" + ], + "summary": "Get data_export workflow.", + "operationId": "GetDataExportWorkflow", + "parameters": [ + { + "type": "string", + "x-go-name": "DataExportWorkflowUid", + "name": "data_export_workflow_uid", + "in": "path", + "required": true }, { "type": "string", @@ -3609,6 +4025,13 @@ "name": "data_export_workflow_uid", "in": "path", "required": true + }, + { + "$ref": "#/definitions/ApproveDataExportWorkflowPayload", + "x-go-name": "Payload", + "description": "optional body; omit / empty payload / empty reason → \"\"", + "name": "payload", + "in": "query" } ], "responses": { @@ -3734,7 +4157,7 @@ "required": true }, { - "name": "payload", + "name": "body", "in": "body", "required": true, "schema": { @@ -3913,7 +4336,7 @@ }, { "description": "Add new db service", - "name": "db_service", + "name": "body", "in": "body", "required": true, "schema": { @@ -3954,7 +4377,7 @@ }, { "description": "check db_service is connectable", - "name": "db_service", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/CheckDBServiceIsConnectableReq" @@ -3994,7 +4417,7 @@ }, { "description": "check db_services is connectable", - "name": "db_services", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/CheckDBServicesIsConnectableReq" @@ -4035,7 +4458,7 @@ }, { "description": "new db services", - "name": "db_services", + "name": "body", "in": "body", "required": true, "schema": { @@ -4179,7 +4602,7 @@ }, { "description": "Update a DB service", - "name": "db_service", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UpdateDBServiceReq" @@ -4581,7 +5004,7 @@ }, { "description": "脱敏效果预览请求体", - "name": "preview_req", + "name": "body", "in": "body", "required": true, "schema": { @@ -4622,7 +5045,7 @@ }, { "description": "批量创建或更新脱敏规则的配置", - "name": "masking_rule_configs_req", + "name": "body", "in": "body", "required": true, "schema": { @@ -4732,7 +5155,7 @@ }, { "description": "脱敏规则信息", - "name": "rule", + "name": "body", "in": "body", "required": true, "schema": { @@ -4822,7 +5245,7 @@ }, { "description": "脱敏规则更新信息", - "name": "rule", + "name": "body", "in": "body", "required": true, "schema": { @@ -4955,7 +5378,7 @@ }, { "description": "敏感数据发现任务信息", - "name": "task", + "name": "body", "in": "body", "required": true, "schema": { @@ -5153,7 +5576,7 @@ }, { "description": "敏感数据发现任务信息", - "name": "task", + "name": "body", "in": "body", "required": true, "schema": { @@ -5602,7 +6025,7 @@ }, { "description": "脱敏模板信息", - "name": "masking_template", + "name": "body", "in": "body", "required": true, "schema": { @@ -5650,7 +6073,7 @@ }, { "description": "脱敏模板信息", - "name": "masking_template", + "name": "body", "in": "body", "required": true, "schema": { @@ -5814,7 +6237,7 @@ }, { "description": "unmasking workflow info", - "name": "unmasking_workflow", + "name": "body", "in": "body", "required": true, "schema": { @@ -5942,7 +6365,7 @@ }, { "description": "approve unmasking workflow info (optional, only carries approve_reason)", - "name": "approve_unmasking_workflow", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/ApproveUnmaskingWorkflow" @@ -6069,7 +6492,7 @@ }, { "description": "reject unmasking workflow info", - "name": "reject_unmasking_workflow", + "name": "body", "in": "body", "required": true, "schema": { @@ -6176,7 +6599,7 @@ }, { "description": "Add new member group", - "name": "member_group", + "name": "body", "in": "body", "required": true, "schema": { @@ -6296,7 +6719,7 @@ }, { "description": "Update a member group", - "name": "member_group", + "name": "body", "in": "body", "required": true, "schema": { @@ -6442,7 +6865,7 @@ }, { "description": "Add new member", - "name": "member", + "name": "body", "in": "body", "required": true, "schema": { @@ -6573,7 +6996,7 @@ }, { "description": "Update a member", - "name": "member", + "name": "body", "in": "body", "required": true, "schema": { @@ -6636,6 +7059,173 @@ } } }, + "/v1/dms/projects/{project_uid}/ops_types": { + "get": { + "tags": [ + "Project" + ], + "summary": "List ops types.", + "operationId": "ListOpsTypes", + "parameters": [ + { + "type": "string", + "x-go-name": "ProjectUID", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "uint32", + "x-go-name": "PageIndex", + "name": "page_index", + "in": "query" + }, + { + "type": "integer", + "format": "uint32", + "x-go-name": "PageSize", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ListOpsTypesReply", + "schema": { + "$ref": "#/definitions/ListOpsTypesReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + }, + "post": { + "tags": [ + "Project" + ], + "summary": "Create a new ops type.", + "operationId": "CreateOpsType", + "parameters": [ + { + "type": "string", + "description": "project uid", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "the name of ops type to be created", + "name": "ops_type_name", + "in": "body", + "required": true + } + ], + "responses": { + "200": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/projects/{project_uid}/ops_types/{ops_type_uid}": { + "put": { + "tags": [ + "Project" + ], + "summary": "Update an existing ops type.", + "operationId": "UpdateOpsType", + "parameters": [ + { + "type": "string", + "description": "project uid", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "ops type id", + "name": "ops_type_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "the name of ops type to be updated", + "name": "ops_type_name", + "in": "body", + "required": true + } + ], + "responses": { + "200": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + }, + "delete": { + "tags": [ + "Project" + ], + "summary": "Delete an existing ops type.", + "operationId": "DeleteOpsType", + "parameters": [ + { + "type": "string", + "x-go-name": "OpsTypeUID", + "name": "ops_type_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "ProjectUID", + "name": "project_uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/projects/{project_uid}/unarchive": { "put": { "tags": [ @@ -6679,7 +7269,7 @@ "parameters": [ { "description": "register dms proxy", - "name": "dms_proxy_target", + "name": "body", "in": "body", "required": true, "schema": { @@ -7015,7 +7605,7 @@ "parameters": [ { "description": "Add new role", - "name": "role", + "name": "body", "in": "body", "required": true, "schema": { @@ -7056,7 +7646,7 @@ }, { "description": "Update a role", - "name": "role", + "name": "body", "in": "body", "required": true, "schema": { @@ -7121,7 +7711,7 @@ "parameters": [ { "description": "Add a new session", - "name": "session", + "name": "body", "in": "body", "required": true, "schema": { @@ -7206,9 +7796,199 @@ ], "responses": { "200": { - "description": "GetUserBySessionReply", + "description": "GetUserBySessionReply", + "schema": { + "$ref": "#/definitions/GetUserBySessionReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/statistic/user_activity/daily_trend": { + "get": { + "tags": [ + "UserActivity" + ], + "summary": "List user activity daily trend.", + "operationId": "ListUserActivityDailyTrend", + "parameters": [ + { + "type": "string", + "x-go-name": "FilterDateFrom", + "name": "filter_date_from", + "in": "query", + "required": true + }, + { + "type": "string", + "x-go-name": "FilterDateTo", + "name": "filter_date_to", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ListUserActivityDailyTrendReply", + "schema": { + "$ref": "#/definitions/ListUserActivityDailyTrendReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/statistic/user_activity/hourly_distribution": { + "get": { + "tags": [ + "UserActivity" + ], + "summary": "List user activity hourly distribution for a date.", + "operationId": "ListUserActivityHourlyDistribution", + "parameters": [ + { + "type": "string", + "x-go-name": "StatDate", + "name": "stat_date", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ListUserActivityHourlyDistributionReply", + "schema": { + "$ref": "#/definitions/ListUserActivityHourlyDistributionReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/statistic/user_activity/module_distribution": { + "get": { + "tags": [ + "UserActivity" + ], + "summary": "List user activity module distribution for a date.", + "operationId": "ListUserActivityModuleDistribution", + "parameters": [ + { + "type": "string", + "x-go-name": "StatDate", + "name": "stat_date", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ListUserActivityModuleDistributionReply", + "schema": { + "$ref": "#/definitions/ListUserActivityModuleDistributionReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/statistic/user_activity/summary": { + "get": { + "tags": [ + "UserActivity" + ], + "summary": "Get user activity summary KPI for a date.", + "operationId": "GetUserActivitySummary", + "parameters": [ + { + "type": "string", + "x-go-name": "StatDate", + "name": "stat_date", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "GetUserActivitySummaryReply", + "schema": { + "$ref": "#/definitions/GetUserActivitySummaryReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, + "/v1/dms/statistic/user_activity/users": { + "get": { + "tags": [ + "UserActivity" + ], + "summary": "List user activity ranking/details.", + "operationId": "ListUserActivityUsers", + "parameters": [ + { + "type": "string", + "x-go-name": "FilterDateFrom", + "name": "filter_date_from", + "in": "query", + "required": true + }, + { + "type": "string", + "x-go-name": "FilterDateTo", + "name": "filter_date_to", + "in": "query", + "required": true + }, + { + "type": "integer", + "format": "uint32", + "x-go-name": "PageIndex", + "name": "page_index", + "in": "query", + "required": true + }, + { + "type": "integer", + "format": "uint32", + "x-go-name": "PageSize", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ListUserActivityUsersReply", "schema": { - "$ref": "#/definitions/GetUserBySessionReply" + "$ref": "#/definitions/ListUserActivityUsersReply" } }, "default": { @@ -7288,7 +8068,7 @@ "parameters": [ { "description": "Add new user group", - "name": "user_group", + "name": "body", "in": "body", "required": true, "schema": { @@ -7329,7 +8109,7 @@ }, { "description": "Update a user group", - "name": "user_group", + "name": "body", "in": "body", "required": true, "schema": { @@ -7525,7 +8305,7 @@ "parameters": [ { "description": "Update current user", - "name": "current_user", + "name": "body", "in": "body", "required": true, "schema": { @@ -7557,7 +8337,7 @@ "parameters": [ { "description": "Add new user", - "name": "user", + "name": "body", "in": "body", "required": true, "schema": { @@ -7590,7 +8370,7 @@ "operationId": "GenAccessToken", "parameters": [ { - "name": "expiration_days", + "name": "body", "in": "body", "required": true, "schema": { @@ -7624,7 +8404,7 @@ "parameters": [ { "description": "Add a new session", - "name": "session", + "name": "body", "in": "body", "required": true, "schema": { @@ -7653,7 +8433,7 @@ "tags": [ "User" ], - "summary": "Get user info, This API is used by other component such as sqle\u0026auth to get user info.", + "summary": "Get user info, This API is used by other component such as sqle&auth to get user info.", "operationId": "GetUser", "parameters": [ { @@ -7696,7 +8476,7 @@ }, { "description": "Update a user", - "name": "user", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UpdateUserReq" @@ -7755,7 +8535,7 @@ "tags": [ "User" ], - "summary": "Get user op permission info, This API is used by other component such as sqle\u0026auth to check user permissions.", + "summary": "Get user op permission info, This API is used by other component such as sqle&auth to check user permissions.", "operationId": "GetUserOpPermission", "parameters": [ { @@ -7775,7 +8555,7 @@ { "x-go-name": "UserOpPermission", "description": "user op permission info", - "name": "user_op_permission", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UserOpPermission" @@ -7808,7 +8588,7 @@ "parameters": [ { "description": "webhooks", - "name": "webhook_message", + "name": "body", "in": "body", "required": true, "schema": { @@ -8079,7 +8859,7 @@ "parameters": [ { "description": "Add new Project", - "name": "project", + "name": "body", "in": "body", "required": true, "schema": { @@ -8113,7 +8893,7 @@ "parameters": [ { "description": "import projects", - "name": "projects", + "name": "body", "in": "body", "required": true, "schema": { @@ -8147,7 +8927,7 @@ "parameters": [ { "description": "new db services", - "name": "db_services", + "name": "body", "in": "body", "required": true, "schema": { @@ -8259,7 +9039,7 @@ }, { "description": "Update a project", - "name": "project", + "name": "body", "in": "body", "required": true, "schema": { @@ -8436,7 +9216,7 @@ }, { "description": "Add new db service", - "name": "db_service", + "name": "body", "in": "body", "required": true, "schema": { @@ -8477,7 +9257,7 @@ }, { "description": "new db services", - "name": "db_services", + "name": "body", "in": "body", "required": true, "schema": { @@ -8569,7 +9349,7 @@ }, { "description": "Update a DB service", - "name": "db_service", + "name": "body", "in": "body", "schema": { "$ref": "#/definitions/UpdateDBServiceReqV2" @@ -8594,6 +9374,33 @@ } }, "definitions": { + "AccessRestrictionClientIP": { + "type": "object", + "properties": { + "client_ip": { + "type": "string", + "x-go-name": "ClientIP" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "AccessRestrictionConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "x-go-name": "Enabled" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AccessWhitelistRuleItem" + }, + "x-go-name": "Rules" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "AccessTokenInfo": { "type": "object", "properties": { @@ -8612,6 +9419,32 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, + "AccessWhitelistRuleItem": { + "type": "object", + "properties": { + "policy_type": { + "type": "string", + "x-go-name": "PolicyType" + }, + "remark": { + "type": "string", + "x-go-name": "Remark" + }, + "source": { + "type": "string", + "x-go-name": "Source" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + }, + "updated_at": { + "type": "string", + "x-go-name": "UpdatedAt" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "ActivateUnmaskingWorkflowViewReply": { "type": "object", "properties": { @@ -9610,6 +10443,17 @@ }, "x-go-package": "github.com/actiontech/dms/internal/data_masking/biz" }, + "ApproveDataExportWorkflowPayload": { + "type": "object", + "properties": { + "reason": { + "description": "optional approval comment; empty / omitted means no comment", + "type": "string", + "x-go-name": "Reason" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "ApproveUnmaskingWorkflow": { "type": "object", "properties": { @@ -10131,6 +10975,38 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "CheckDataExportWorkflowTemplateUsedReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "format": "int64", + "x-go-name": "Count" + }, + "is_used": { + "type": "boolean", + "x-go-name": "IsUsed" + } + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "CheckDbConnectable": { "type": "object", "required": [ @@ -10461,6 +11337,44 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "CreateAccessWhitelistRuleReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/AccessWhitelistRuleItem" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "CreateAccessWhitelistRuleReq": { + "type": "object", + "properties": { + "policy_type": { + "type": "string", + "x-go-name": "PolicyType" + }, + "remark": { + "type": "string", + "x-go-name": "Remark" + }, + "source": { + "type": "string", + "x-go-name": "Source" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "CreateBusinessTagReq": { "type": "object", "properties": { @@ -10473,6 +11387,10 @@ "CreateEnvironmentTagReq": { "type": "object", "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, "environment_name": { "type": "string", "x-go-name": "Name" @@ -10480,6 +11398,16 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "CreateOpsTypeReq": { + "type": "object", + "properties": { + "ops_type_name": { + "type": "string", + "x-go-name": "Name" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "CreateUnmaskingSQLItem": { "type": "object", "properties": { @@ -11166,6 +12094,12 @@ "x-go-name": "Name", "example": "d1" }, + "ops_type_uid": { + "description": "ops type uid from project dictionary; omit or empty means unset; immutable after create", + "type": "string", + "x-go-name": "OpsTypeUID", + "example": "7002001" + }, "tasks": { "description": "export task info", "type": "array", @@ -11174,6 +12108,13 @@ }, "x-go-name": "Tasks", "example": "[export_task_uid1,export_task_uid2]" + }, + "workflow_template_id": { + "description": "workflow template id from SQLE; omit or 0 to use project default data_export template", + "type": "integer", + "format": "uint64", + "x-go-name": "WorkflowTemplateId", + "example": 1 } }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" @@ -11354,6 +12295,10 @@ "EnvironmentTag": { "type": "object", "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, "name": { "description": "环境属性标签最多50个字符", "type": "string", @@ -11467,6 +12412,46 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/base/v1" }, + "GetAccessRestrictionClientIPReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/AccessRestrictionClientIP" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "GetAccessRestrictionReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/AccessRestrictionConfig" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "GetBasicInfoReply": { "type": "object", "properties": { @@ -11675,6 +12660,9 @@ "type": "string", "x-go-name": "Desc" }, + "ops_type": { + "$ref": "#/definitions/OpsType" + }, "unmasking_workflow": { "$ref": "#/definitions/DataExportRelatedUnmaskingWorkflow" }, @@ -11692,6 +12680,15 @@ }, "x-go-name": "WorkflowRecordHistory" }, + "workflow_template_id": { + "type": "integer", + "format": "uint64", + "x-go-name": "WorkflowTemplateId" + }, + "workflow_template_name": { + "type": "string", + "x-go-name": "WorkflowTemplateName" + }, "workflow_uid": { "type": "string", "x-go-name": "WorkflowID" @@ -11908,7 +12905,7 @@ "$ref": "#/definitions/MaskingOverviewDashboard" }, "databases": { - "description": "database_name -\u003e database node", + "description": "database_name -> database node", "type": "object", "additionalProperties": { "$ref": "#/definitions/MaskingOverviewDatabaseNode" @@ -12614,6 +13611,26 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, + "GetUserActivitySummaryReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/UserActivitySummary" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "GetUserBySessionReply": { "type": "object", "properties": { @@ -12807,6 +13824,9 @@ "type": "string", "x-go-name": "Description" }, + "ops_type": { + "$ref": "#/definitions/OpsType" + }, "project_info": { "$ref": "#/definitions/ProjectInfo" }, @@ -13930,6 +14950,9 @@ "type": "string", "x-go-name": "Type" }, + "environment_tag": { + "$ref": "#/definitions/EnvironmentTag" + }, "host": { "type": "string", "x-go-name": "Host" @@ -14128,7 +15151,7 @@ "x-go-name": "ExportSQLType" }, "export_status": { - "description": "导出执行状态 success/failed/not_executed;未开始可为空", + "description": "导出执行状态:success / failed / not_executed;未开始可为空", "type": "string", "enum": [ "success", @@ -14223,6 +15246,9 @@ "format": "date-time", "x-go-name": "ExportedAt" }, + "ops_type": { + "$ref": "#/definitions/OpsType" + }, "project_info": { "$ref": "#/definitions/ProjectInfo" }, @@ -14252,6 +15278,15 @@ "type": "string", "x-go-name": "WorkflowName" }, + "workflow_template_id": { + "type": "integer", + "format": "uint64", + "x-go-name": "WorkflowTemplateId" + }, + "workflow_template_name": { + "type": "string", + "x-go-name": "WorkflowTemplateName" + }, "workflow_uid": { "type": "string", "x-go-name": "WorkflowID" @@ -15055,17 +16090,6 @@ "ListMemberRoleWithOpRange": { "type": "object", "properties": { - "member_group": { - "$ref": "#/definitions/ProjectMemberGroup" - }, - "op_permissions": { - "description": "member op permissions", - "type": "array", - "items": { - "$ref": "#/definitions/UidWithName" - }, - "x-go-name": "OpPermissions" - }, "op_range_type": { "description": "op permission range type, only support db service now\nunknown OpRangeTypeUnknown\nglobal OpRangeTypeGlobal 全局权限: 该权限只能被用户使用\nproject OpRangeTypeProject 项目权限: 该权限只能被成员使用\ndb_service OpRangeTypeDBService 项目内的数据源权限: 该权限只能被成员使用", "type": "string", @@ -15090,7 +16114,7 @@ "$ref": "#/definitions/UidWithName" } }, - "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, "ListMemberTipsItem": { "type": "object", @@ -15222,7 +16246,37 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, - "ListOpPermissionReply": { + "ListOpPermissionReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "description": "List op_permission reply", + "type": "array", + "items": { + "$ref": "#/definitions/ListOpPermission" + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + }, + "total_nums": { + "type": "integer", + "format": "int64", + "x-go-name": "Total" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "ListOpsTypesReply": { "type": "object", "properties": { "code": { @@ -15232,10 +16286,9 @@ "x-go-name": "Code" }, "data": { - "description": "List op_permission reply", "type": "array", "items": { - "$ref": "#/definitions/ListOpPermission" + "$ref": "#/definitions/OpsType" }, "x-go-name": "Data" }, @@ -15978,6 +17031,107 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, + "ListUserActivityDailyTrendReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/UserActivityDailyTrendItem" + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "ListUserActivityHourlyDistributionReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/UserActivityHourlyDistributionItem" + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "ListUserActivityModuleDistributionReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/UserActivityModuleDistributionItem" + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "ListUserActivityUsersReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/UserActivityUserItem" + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + }, + "total_nums": { + "type": "integer", + "format": "uint64", + "x-go-name": "TotalNums" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "ListUserGroup": { "description": "A dms user group", "type": "object", @@ -16252,7 +17406,7 @@ "type": "object", "properties": { "tables": { - "description": "table_name -\u003e table overview data", + "description": "table_name -> table overview data", "type": "object", "additionalProperties": { "$ref": "#/definitions/MaskingOverviewTableData" @@ -16912,6 +18066,22 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "OpsType": { + "type": "object", + "title": "OpsType 项目级运维类型字典项(对标 EnvironmentTag,无色标)。", + "properties": { + "name": { + "description": "运维类型名称最多50个字符", + "type": "string", + "x-go-name": "Name" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + } + }, + "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" + }, "Param": { "type": "object", "properties": { @@ -18164,7 +19334,7 @@ "type": "object", "properties": { "tables": { - "description": "table_name -\u003e table node", + "description": "table_name -> table node", "type": "object", "additionalProperties": { "$ref": "#/definitions/SuspectedSensitiveTableNode" @@ -18178,7 +19348,7 @@ "type": "object", "properties": { "databases": { - "description": "database_name -\u003e database node", + "description": "database_name -> database node", "type": "object", "additionalProperties": { "$ref": "#/definitions/SuspectedSensitiveDatabaseNode" @@ -18192,7 +19362,7 @@ "type": "object", "properties": { "fields": { - "description": "field_name -\u003e scan result", + "description": "field_name -> scan result", "type": "object", "additionalProperties": { "$ref": "#/definitions/SensitiveFieldScanResult" @@ -18245,6 +19415,11 @@ "url": { "type": "string", "x-go-name": "Url" + }, + "user_request_log_expired_hours": { + "type": "integer", + "format": "int64", + "x-go-name": "UserRequestLogExpiredHours" } }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" @@ -19137,6 +20312,36 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "UpdateAccessRestrictionReq": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "x-go-name": "Enabled" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "UpdateAccessWhitelistRuleReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/AccessWhitelistRuleItem" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "UpdateBusinessTagReq": { "type": "object", "properties": { @@ -19459,6 +20664,10 @@ "UpdateEnvironmentTagReq": { "type": "object", "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, "environment_name": { "type": "string", "x-go-name": "Name" @@ -19753,6 +20962,16 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "UpdateOpsTypeReq": { + "type": "object", + "properties": { + "ops_type_name": { + "type": "string", + "x-go-name": "Name" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "UpdateProject": { "type": "object", "properties": { @@ -20158,6 +21377,11 @@ "url": { "type": "string", "x-go-name": "Url" + }, + "user_request_log_expired_hours": { + "type": "integer", + "format": "int64", + "x-go-name": "UserRequestLogExpiredHours" } }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" @@ -20411,6 +21635,154 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "UserActivityDailyTrendItem": { + "type": "object", + "properties": { + "dau": { + "type": "integer", + "format": "int64", + "x-go-name": "DAU" + }, + "error_count": { + "type": "integer", + "format": "int64", + "x-go-name": "ErrorCount" + }, + "request_count": { + "type": "integer", + "format": "int64", + "x-go-name": "RequestCount" + }, + "stat_date": { + "type": "string", + "x-go-name": "StatDate" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "UserActivityHourlyDistributionItem": { + "type": "object", + "properties": { + "active_users": { + "type": "integer", + "format": "int64", + "x-go-name": "ActiveUsers" + }, + "request_count": { + "type": "integer", + "format": "int64", + "x-go-name": "RequestCount" + }, + "stat_hour": { + "type": "integer", + "format": "int64", + "x-go-name": "StatHour" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "UserActivityModuleDistributionItem": { + "type": "object", + "properties": { + "module_code": { + "type": "string", + "x-go-name": "ModuleCode" + }, + "module_name": { + "type": "string", + "x-go-name": "ModuleName" + }, + "percent": { + "type": "number", + "format": "double", + "x-go-name": "Percent" + }, + "request_count": { + "type": "integer", + "format": "int64", + "x-go-name": "RequestCount" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "UserActivitySummary": { + "type": "object", + "properties": { + "avg_request_per_user": { + "type": "number", + "format": "double", + "x-go-name": "AvgRequestPerUser" + }, + "dau": { + "type": "integer", + "format": "int64", + "x-go-name": "DAU" + }, + "error_count": { + "type": "integer", + "format": "int64", + "x-go-name": "ErrorCount" + }, + "error_rate": { + "type": "number", + "format": "double", + "x-go-name": "ErrorRate" + }, + "peak_hour": { + "type": "integer", + "format": "int64", + "x-go-name": "PeakHour" + }, + "peak_hour_requests": { + "type": "integer", + "format": "int64", + "x-go-name": "PeakHourRequests" + }, + "request_count": { + "type": "integer", + "format": "int64", + "x-go-name": "RequestCount" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "UserActivityUserItem": { + "type": "object", + "properties": { + "active_days": { + "type": "integer", + "format": "int64", + "x-go-name": "ActiveDays" + }, + "last_active_at": { + "type": "string", + "format": "date-time", + "x-go-name": "LastActiveAt" + }, + "request_count": { + "type": "integer", + "format": "int64", + "x-go-name": "RequestCount" + }, + "top_module_code": { + "type": "string", + "x-go-name": "TopModuleCode" + }, + "top_module_name": { + "type": "string", + "x-go-name": "TopModuleName" + }, + "user_name": { + "type": "string", + "x-go-name": "UserName" + }, + "user_uid": { + "type": "string", + "x-go-name": "UserUID" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "UserBindProject": { "type": "object", "properties": { diff --git a/api/swagger.yaml b/api/swagger.yaml index 2b75867a9..9a0080098 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2,6 +2,25 @@ basePath: / consumes: - application/json definitions: + AccessRestrictionClientIP: + properties: + client_ip: + type: string + x-go-name: ClientIP + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + AccessRestrictionConfig: + properties: + enabled: + type: boolean + x-go-name: Enabled + rules: + items: + $ref: '#/definitions/AccessWhitelistRuleItem' + type: array + x-go-name: Rules + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 AccessTokenInfo: properties: access_token: @@ -15,6 +34,25 @@ definitions: x-go-name: ExpiredTime type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + AccessWhitelistRuleItem: + properties: + policy_type: + type: string + x-go-name: PolicyType + remark: + type: string + x-go-name: Remark + source: + type: string + x-go-name: Source + uid: + type: string + x-go-name: UID + updated_at: + type: string + x-go-name: UpdatedAt + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 ActivateUnmaskingWorkflowViewReply: properties: code: @@ -771,6 +809,14 @@ definitions: x-go-name: Warnings type: object x-go-package: github.com/actiontech/dms/internal/data_masking/biz + ApproveDataExportWorkflowPayload: + properties: + reason: + description: optional approval comment; empty / omitted means no comment + type: string + x-go-name: Reason + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 ApproveUnmaskingWorkflow: properties: approve_reason: @@ -1151,6 +1197,30 @@ definitions: x-go-name: DBServices type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + CheckDataExportWorkflowTemplateUsedReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + properties: + count: + format: int64 + type: integer + x-go-name: Count + is_used: + type: boolean + x-go-name: IsUsed + type: object + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 CheckDbConnectable: properties: additional_params: @@ -1425,6 +1495,34 @@ definitions: - masking_rule_configs type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + CreateAccessWhitelistRuleReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/AccessWhitelistRuleItem' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + CreateAccessWhitelistRuleReq: + properties: + policy_type: + type: string + x-go-name: PolicyType + remark: + type: string + x-go-name: Remark + source: + type: string + x-go-name: Source + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 CreateBusinessTagReq: properties: business_tag: @@ -1433,11 +1531,21 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v1 CreateEnvironmentTagReq: properties: + color: + type: string + x-go-name: Color environment_name: type: string x-go-name: Name type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + CreateOpsTypeReq: + properties: + ops_type_name: + type: string + x-go-name: Name + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 CreateUnmaskingSQLItem: properties: sql_content: @@ -1991,6 +2099,11 @@ definitions: example: d1 type: string x-go-name: Name + ops_type_uid: + description: ops type uid from project dictionary; omit or empty means unset; immutable after create + example: "7002001" + type: string + x-go-name: OpsTypeUID tasks: description: export task info example: '[export_task_uid1,export_task_uid2]' @@ -1998,6 +2111,12 @@ definitions: $ref: '#/definitions/Task' type: array x-go-name: Tasks + workflow_template_id: + description: workflow template id from SQLE; omit or 0 to use project default data_export template + example: 1 + format: uint64 + type: integer + x-go-name: WorkflowTemplateId required: - name - tasks @@ -2139,6 +2258,9 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v1 EnvironmentTag: properties: + color: + type: string + x-go-name: Color name: description: 环境属性标签最多50个字符 type: string @@ -2222,6 +2344,36 @@ definitions: x-go-name: Message type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/base/v1 + GetAccessRestrictionClientIPReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/AccessRestrictionClientIP' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + GetAccessRestrictionReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/AccessRestrictionConfig' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 GetBasicInfoReply: properties: code: @@ -2339,7 +2491,7 @@ definitions: type: string x-go-name: ExportFailReason export_fail_stage: - description: '失败阶段 wire:task_schedule / connect / prepare / sql_execute / file_generate;非失败可省略' + description: 失败阶段 wire:task_schedule / connect / prepare / sql_execute / file_generate;非失败可省略 type: string x-go-name: ExportFailStage export_file_type: @@ -2386,6 +2538,8 @@ definitions: desc: type: string x-go-name: Desc + ops_type: + $ref: '#/definitions/OpsType' unmasking_workflow: $ref: '#/definitions/DataExportRelatedUnmaskingWorkflow' workflow_name: @@ -2398,6 +2552,13 @@ definitions: $ref: '#/definitions/WorkflowRecord' type: array x-go-name: WorkflowRecordHistory + workflow_template_id: + format: uint64 + type: integer + x-go-name: WorkflowTemplateId + workflow_template_name: + type: string + x-go-name: WorkflowTemplateName workflow_uid: type: string x-go-name: WorkflowID @@ -3125,6 +3286,21 @@ definitions: x-go-name: WxID type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + GetUserActivitySummaryReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/UserActivitySummary' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 GetUserBySessionReply: properties: code: @@ -3272,6 +3448,8 @@ definitions: desc: type: string x-go-name: Description + ops_type: + $ref: '#/definitions/OpsType' project_info: $ref: '#/definitions/ProjectInfo' status: @@ -4163,6 +4341,8 @@ definitions: db_type: type: string x-go-name: Type + environment_tag: + $ref: '#/definitions/EnvironmentTag' host: type: string x-go-name: Host @@ -4321,7 +4501,7 @@ definitions: type: string x-go-name: ExportSQLType export_status: - description: 导出执行状态 success/failed/not_executed;未开始可为空 + description: 导出执行状态:success / failed / not_executed;未开始可为空 enum: - success - failed @@ -4392,6 +4572,8 @@ definitions: format: date-time type: string x-go-name: ExportedAt + ops_type: + $ref: '#/definitions/OpsType' project_info: $ref: '#/definitions/ProjectInfo' project_name: @@ -4422,6 +4604,13 @@ definitions: workflow_name: type: string x-go-name: WorkflowName + workflow_template_id: + format: uint64 + type: integer + x-go-name: WorkflowTemplateId + workflow_template_name: + type: string + x-go-name: WorkflowTemplateName workflow_uid: type: string x-go-name: WorkflowID @@ -5066,14 +5255,6 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v1 ListMemberRoleWithOpRange: properties: - member_group: - $ref: '#/definitions/ProjectMemberGroup' - op_permissions: - description: member op permissions - items: - $ref: '#/definitions/UidWithName' - type: array - x-go-name: OpPermissions op_range_type: description: |- op permission range type, only support db service now @@ -5102,7 +5283,7 @@ definitions: role_uid: $ref: '#/definitions/UidWithName' type: object - x-go-package: github.com/actiontech/dms/api/dms/service/v1 + x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 ListMemberTipsItem: properties: user_id: @@ -5230,6 +5411,28 @@ definitions: x-go-name: Total type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + ListOpsTypesReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + items: + $ref: '#/definitions/OpsType' + type: array + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + total_nums: + format: int64 + type: integer + x-go-name: Total + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 ListPendingApprovalRequestsReply: properties: code: @@ -5905,6 +6108,82 @@ definitions: x-go-name: WxID type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + ListUserActivityDailyTrendReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + items: + $ref: '#/definitions/UserActivityDailyTrendItem' + type: array + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + ListUserActivityHourlyDistributionReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + items: + $ref: '#/definitions/UserActivityHourlyDistributionItem' + type: array + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + ListUserActivityModuleDistributionReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + items: + $ref: '#/definitions/UserActivityModuleDistributionItem' + type: array + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + ListUserActivityUsersReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + items: + $ref: '#/definitions/UserActivityUserItem' + type: array + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + total_nums: + format: uint64 + type: integer + x-go-name: TotalNums + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 ListUserGroup: description: A dms user group properties: @@ -6742,6 +7021,18 @@ definitions: x-go-name: UserName type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + OpsType: + properties: + name: + description: 运维类型名称最多50个字符 + type: string + x-go-name: Name + uid: + type: string + x-go-name: UID + title: OpsType 项目级运维类型字典项(对标 EnvironmentTag,无色标)。 + type: object + x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 Param: properties: desc: @@ -7801,6 +8092,10 @@ definitions: url: type: string x-go-name: Url + user_request_log_expired_hours: + format: int64 + type: integer + x-go-name: UserRequestLogExpiredHours type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 TableColumn: @@ -8569,24 +8864,46 @@ definitions: x-go-name: WorkflowID type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 - UpdateBusinessTagReq: + UpdateAccessRestrictionReq: properties: - business_tag: - $ref: '#/definitions/BusinessTag' + enabled: + type: boolean + x-go-name: Enabled type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 - UpdateCompanyNotice: - description: A companynotice + UpdateAccessWhitelistRuleReply: properties: - end_time: - description: notice show end time - format: date-time - type: string - x-go-name: EndTime - notice_str: - description: companynotice info - type: string - x-go-name: NoticeStr + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/AccessWhitelistRuleItem' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UpdateBusinessTagReq: + properties: + business_tag: + $ref: '#/definitions/BusinessTag' + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UpdateCompanyNotice: + description: A companynotice + properties: + end_time: + description: notice show end time + format: date-time + type: string + x-go-name: EndTime + notice_str: + description: companynotice info + type: string + x-go-name: NoticeStr start_time: description: notice show start time format: date-time @@ -8820,6 +9137,9 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v2 UpdateEnvironmentTagReq: properties: + color: + type: string + x-go-name: Color environment_name: type: string x-go-name: Name @@ -9031,6 +9351,13 @@ definitions: $ref: '#/definitions/UpdateMember' type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UpdateOpsTypeReq: + properties: + ops_type_name: + type: string + x-go-name: Name + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 UpdateProject: properties: business: @@ -9369,6 +9696,10 @@ definitions: url: type: string x-go-name: Url + user_request_log_expired_hours: + format: int64 + type: integer + x-go-name: UserRequestLogExpiredHours type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 UpdateUser: @@ -9563,6 +9894,119 @@ definitions: - name type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UserActivityDailyTrendItem: + properties: + dau: + format: int64 + type: integer + x-go-name: DAU + error_count: + format: int64 + type: integer + x-go-name: ErrorCount + request_count: + format: int64 + type: integer + x-go-name: RequestCount + stat_date: + type: string + x-go-name: StatDate + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UserActivityHourlyDistributionItem: + properties: + active_users: + format: int64 + type: integer + x-go-name: ActiveUsers + request_count: + format: int64 + type: integer + x-go-name: RequestCount + stat_hour: + format: int64 + type: integer + x-go-name: StatHour + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UserActivityModuleDistributionItem: + properties: + module_code: + type: string + x-go-name: ModuleCode + module_name: + type: string + x-go-name: ModuleName + percent: + format: double + type: number + x-go-name: Percent + request_count: + format: int64 + type: integer + x-go-name: RequestCount + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UserActivitySummary: + properties: + avg_request_per_user: + format: double + type: number + x-go-name: AvgRequestPerUser + dau: + format: int64 + type: integer + x-go-name: DAU + error_count: + format: int64 + type: integer + x-go-name: ErrorCount + error_rate: + format: double + type: number + x-go-name: ErrorRate + peak_hour: + format: int64 + type: integer + x-go-name: PeakHour + peak_hour_requests: + format: int64 + type: integer + x-go-name: PeakHourRequests + request_count: + format: int64 + type: integer + x-go-name: RequestCount + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UserActivityUserItem: + properties: + active_days: + format: int64 + type: integer + x-go-name: ActiveDays + last_active_at: + format: date-time + type: string + x-go-name: LastActiveAt + request_count: + format: int64 + type: integer + x-go-name: RequestCount + top_module_code: + type: string + x-go-name: TopModuleCode + top_module_name: + type: string + x-go-name: TopModuleName + user_name: + type: string + x-go-name: UserName + user_uid: + type: string + x-go-name: UserUID + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 UserBindProject: properties: is_manager: @@ -9884,6 +10328,116 @@ paths: $ref: '#/definitions/GenericResp' tags: - CompanyNotice + /v1/dms/configurations/access_restriction: + get: + operationId: GetAccessRestriction + responses: + "200": + description: GetAccessRestrictionReply + schema: + $ref: '#/definitions/GetAccessRestrictionReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Get access restriction configuration. + tags: + - Configuration + patch: + operationId: UpdateAccessRestriction + parameters: + - in: body + name: access_restriction + required: true + schema: + $ref: '#/definitions/UpdateAccessRestrictionReq' + responses: + "200": + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Update access restriction switch. + tags: + - Configuration + /v1/dms/configurations/access_restriction/client_ip: + get: + operationId: GetAccessRestrictionClientIP + responses: + "200": + description: GetAccessRestrictionClientIPReply + schema: + $ref: '#/definitions/GetAccessRestrictionClientIPReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Get current request client IP for access restriction. + tags: + - Configuration + /v1/dms/configurations/access_restriction/rules: + post: + operationId: CreateAccessWhitelistRule + parameters: + - in: body + name: rule + required: true + schema: + $ref: '#/definitions/CreateAccessWhitelistRuleReq' + responses: + "200": + description: CreateAccessWhitelistRuleReply + schema: + $ref: '#/definitions/CreateAccessWhitelistRuleReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Create access whitelist rule. + tags: + - Configuration + /v1/dms/configurations/access_restriction/rules/{rule_uid}: + delete: + operationId: DeleteAccessWhitelistRule + responses: + "200": + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Delete access whitelist rule. + tags: + - Configuration + put: + operationId: UpdateAccessWhitelistRule + parameters: + - in: path + name: rule_uid + required: true + type: string + - in: body + name: rule + required: true + schema: + $ref: '#/definitions/UpdateAccessWhitelistRuleReq' + responses: + "200": + description: UpdateAccessWhitelistRuleReply + schema: + $ref: '#/definitions/UpdateAccessWhitelistRuleReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Update access whitelist rule. + tags: + - Configuration /v1/dms/configurations/feishu: get: operationId: GetFeishuConfiguration @@ -10464,6 +11018,148 @@ paths: /v1/dms/dashboard/data_export_workflows: get: operationId: GetGlobalDataExportWorkflows + parameters: + - description: filter status list + in: query + items: + enum: + - wait_for_approve + - wait_for_export + - exporting + - rejected + - cancel + - failed + - finish + type: string + name: filter_status_list + type: array + x-go-name: FilterStatusList + - description: filter db service uid + in: query + name: filter_db_service_uid + type: string + x-go-name: FilterDBServiceUid + - description: filter project priority + in: query + name: filter_current_step_assignee_user_id + type: string + x-go-name: FilterCurrentStepAssigneeUserId + - description: filter project uids + in: query + items: + type: string + name: filter_project_uids + type: array + x-go-name: FilterProjectUids + - description: filter project uid + in: query + name: filter_project_uid + type: string + x-go-name: FilterProjectUid + - description: the maximum count of member to be returned + format: uint32 + in: query + name: page_size + required: true + type: integer + x-go-name: PageSize + - description: the offset of members to be returned, default is 0 + format: uint32 + in: query + name: page_index + type: integer + x-go-name: PageIndex + - description: |- + filter the status + wait_for_approve DataExportWorkflowStatusWaitForApprove + wait_for_export DataExportWorkflowStatusWaitForExport + exporting DataExportWorkflowStatusWaitForExporting + rejected DataExportWorkflowStatusRejected + cancel DataExportWorkflowStatusCancel + failed DataExportWorkflowStatusFailed + finish DataExportWorkflowStatusFinish + enum: + - wait_for_approve + - wait_for_export + - exporting + - rejected + - cancel + - failed + - finish + in: query + name: filter_by_status + type: string + x-go-enum-desc: |- + wait_for_approve DataExportWorkflowStatusWaitForApprove + wait_for_export DataExportWorkflowStatusWaitForExport + exporting DataExportWorkflowStatusWaitForExporting + rejected DataExportWorkflowStatusRejected + cancel DataExportWorkflowStatusCancel + failed DataExportWorkflowStatusFailed + finish DataExportWorkflowStatusFinish + x-go-name: FilterByStatus + - description: filter create user id + in: query + name: filter_by_create_user_uid + type: string + x-go-name: FilterByCreateUserUid + - description: filter current assignee user id + in: query + name: filter_current_step_assignee_user_uid + type: string + x-go-name: FilterCurrentStepAssigneeUserUid + - description: filter db_service id + in: query + name: filter_by_db_service_uid + type: string + x-go-name: FilterByDBServiceUid + - description: filter fuzzy key word for id/name + in: query + name: fuzzy_keyword + type: string + x-go-name: FuzzyKeyword + - description: enable OR-based self-relevant filtering (creator OR assignee OR viewable db_service) + in: query + name: check_user_can_access + type: boolean + x-go-name: CheckUserCanAccess + - description: current user id for check_user_can_access filter + in: query + name: current_user_id + type: string + x-go-name: CurrentUserID + - description: db_service UIDs with view_others_workflow permission for check_user_can_access + in: query + items: + type: string + name: viewable_db_service_uids + type: array + x-go-name: ViewableDBServiceUids + - description: filter create time from + in: query + name: filter_create_time_from + type: string + x-go-name: FilterCreateTimeFrom + - description: filter create time to + in: query + name: filter_create_time_to + type: string + x-go-name: FilterCreateTimeTo + - description: filter update time from + in: query + name: filter_update_time_from + type: string + x-go-name: FilterUpdateTimeFrom + - description: filter update time to + in: query + name: filter_update_time_to + type: string + x-go-name: FilterUpdateTimeTo + - description: filter by ops type uid(单选;空则不筛;不提供「未设置」;与项目内列表同构) + in: query + name: filter_by_ops_type_uid + type: string + x-go-name: FilterByOpsTypeUid responses: "200": description: GetGlobalDataExportWorkflowsReply @@ -11756,6 +12452,17 @@ paths: name: fuzzy_keyword type: string x-go-name: FuzzyKeyword + - description: filter by workflow template id + format: uint64 + in: query + name: filter_workflow_template_id + type: integer + x-go-name: FilterWorkflowTemplateId + - description: filter by ops type uid(单选;空则不筛;不提供「未设置」) + in: query + name: filter_by_ops_type_uid + type: string + x-go-name: FilterByOpsTypeUid responses: "200": description: ListDataExportWorkflowsReply @@ -11835,6 +12542,11 @@ paths: required: true type: string x-go-name: DataExportWorkflowUid + - $ref: '#/definitions/ApproveDataExportWorkflowPayload' + description: optional body; omit / empty payload / empty reason → "" + in: query + name: payload + x-go-name: Payload responses: "200": description: GenericResp @@ -11961,6 +12673,36 @@ paths: summary: Cancel data export workflows. tags: - DataExportWorkflows + /v1/dms/projects/{project_uid}/data_export_workflows/workflow_template_used: + get: + description: Soft-deleted SQLE templates do not block already-created workflow runtime; this is a best-effort hint. + operationId: CheckDataExportWorkflowTemplateUsed + parameters: + - description: project id + in: path + name: project_uid + required: true + type: string + x-go-name: ProjectUid + - description: workflow template id + format: uint64 + in: query + name: workflow_template_id + required: true + type: integer + x-go-name: WorkflowTemplateId + responses: + "200": + description: CheckDataExportWorkflowTemplateUsedReply + schema: + $ref: '#/definitions/CheckDataExportWorkflowTemplateUsedReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Check whether a data-export workflow template id is referenced by local workflows. + tags: + - DataExportWorkflows /v1/dms/projects/{project_uid}/db_services: get: deprecated: true @@ -13959,6 +14701,119 @@ paths: summary: List member tips. tags: - Member + /v1/dms/projects/{project_uid}/ops_types: + get: + operationId: ListOpsTypes + parameters: + - in: path + name: project_uid + required: true + type: string + x-go-name: ProjectUID + - format: uint32 + in: query + name: page_index + type: integer + x-go-name: PageIndex + - format: uint32 + in: query + name: page_size + required: true + type: integer + x-go-name: PageSize + responses: + "200": + description: ListOpsTypesReply + schema: + $ref: '#/definitions/ListOpsTypesReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: List ops types. + tags: + - Project + post: + operationId: CreateOpsType + parameters: + - description: project uid + in: path + name: project_uid + required: true + type: string + - description: the name of ops type to be created + in: body + name: ops_type_name + required: true + type: string + responses: + "200": + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Create a new ops type. + tags: + - Project + /v1/dms/projects/{project_uid}/ops_types/{ops_type_uid}: + delete: + operationId: DeleteOpsType + parameters: + - in: path + name: ops_type_uid + required: true + type: string + x-go-name: OpsTypeUID + - in: path + name: project_uid + required: true + type: string + x-go-name: ProjectUID + responses: + "200": + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Delete an existing ops type. + tags: + - Project + put: + operationId: UpdateOpsType + parameters: + - description: project uid + in: path + name: project_uid + required: true + type: string + - description: ops type id + in: path + name: ops_type_uid + required: true + type: string + - description: the name of ops type to be updated + in: body + name: ops_type_name + required: true + type: string + responses: + "200": + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Update an existing ops type. + tags: + - Project /v1/dms/projects/{project_uid}/unarchive: put: operationId: UnarchiveProject @@ -14183,6 +15038,17 @@ paths: name: fuzzy_keyword type: string x-go-name: FuzzyKeyword + - description: filter by workflow template id + format: uint64 + in: query + name: filter_workflow_template_id + type: integer + x-go-name: FilterWorkflowTemplateId + - description: filter by ops type uid(单选;空则不筛;不提供「未设置」) + in: query + name: filter_by_ops_type_uid + type: string + x-go-name: FilterByOpsTypeUid responses: "200": description: ListDataExportWorkflowsReply @@ -14786,6 +15652,133 @@ paths: summary: Get current user. tags: - Session + /v1/dms/statistic/user_activity/daily_trend: + get: + operationId: ListUserActivityDailyTrend + parameters: + - in: query + name: filter_date_from + required: true + type: string + x-go-name: FilterDateFrom + - in: query + name: filter_date_to + required: true + type: string + x-go-name: FilterDateTo + responses: + "200": + description: ListUserActivityDailyTrendReply + schema: + $ref: '#/definitions/ListUserActivityDailyTrendReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: List user activity daily trend. + tags: + - UserActivity + /v1/dms/statistic/user_activity/hourly_distribution: + get: + operationId: ListUserActivityHourlyDistribution + parameters: + - in: query + name: stat_date + required: true + type: string + x-go-name: StatDate + responses: + "200": + description: ListUserActivityHourlyDistributionReply + schema: + $ref: '#/definitions/ListUserActivityHourlyDistributionReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: List user activity hourly distribution for a date. + tags: + - UserActivity + /v1/dms/statistic/user_activity/module_distribution: + get: + operationId: ListUserActivityModuleDistribution + parameters: + - in: query + name: stat_date + required: true + type: string + x-go-name: StatDate + responses: + "200": + description: ListUserActivityModuleDistributionReply + schema: + $ref: '#/definitions/ListUserActivityModuleDistributionReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: List user activity module distribution for a date. + tags: + - UserActivity + /v1/dms/statistic/user_activity/summary: + get: + operationId: GetUserActivitySummary + parameters: + - in: query + name: stat_date + required: true + type: string + x-go-name: StatDate + responses: + "200": + description: GetUserActivitySummaryReply + schema: + $ref: '#/definitions/GetUserActivitySummaryReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Get user activity summary KPI for a date. + tags: + - UserActivity + /v1/dms/statistic/user_activity/users: + get: + operationId: ListUserActivityUsers + parameters: + - in: query + name: filter_date_from + required: true + type: string + x-go-name: FilterDateFrom + - in: query + name: filter_date_to + required: true + type: string + x-go-name: FilterDateTo + - format: uint32 + in: query + name: page_index + required: true + type: integer + x-go-name: PageIndex + - format: uint32 + in: query + name: page_size + required: true + type: integer + x-go-name: PageSize + responses: + "200": + description: ListUserActivityUsersReply + schema: + $ref: '#/definitions/ListUserActivityUsersReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: List user activity ranking/details. + tags: + - UserActivity /v1/dms/user_groups: get: operationId: ListUserGroups diff --git a/internal/apiserver/service/dms_controller.go b/internal/apiserver/service/dms_controller.go index cfae66707..103cb3259 100644 --- a/internal/apiserver/service/dms_controller.go +++ b/internal/apiserver/service/dms_controller.go @@ -3756,6 +3756,9 @@ func (ctl *DMSController) AddDataExportWorkflow(c echo.Context) error { if errors.Is(err, biz.ErrDataExportWorkflowNameDuplicate) { return NewErrResp(c, errors.New(locale.Bundle.LocalizeMsgByCtx(c.Request().Context(), locale.DataExportWorkflowNameDuplicateErr)), apiError.BadRequestErr) } + if errors.Is(err, biz.ErrOpsTypeNotBelongToProject) { + return NewErrResp(c, biz.ErrOpsTypeNotBelongToProject, apiError.BadRequestErr) + } return NewErrResp(c, err, apiError.DMSServiceErr) } return NewOkRespWithReply(c, reply) diff --git a/internal/apiserver/service/ops_type_controller.go b/internal/apiserver/service/ops_type_controller.go new file mode 100644 index 000000000..18ad08122 --- /dev/null +++ b/internal/apiserver/service/ops_type_controller.go @@ -0,0 +1,148 @@ +package service + +import ( + aV1 "github.com/actiontech/dms/api/dms/service/v1" + apiError "github.com/actiontech/dms/internal/apiserver/pkg/error" + "github.com/actiontech/dms/pkg/dms-common/api/jwt" + "github.com/labstack/echo/v4" +) + +// swagger:operation POST /v1/dms/projects/{project_uid}/ops_types Project CreateOpsType +// +// Create a new ops type. +// +// --- +// parameters: +// - name: project_uid +// description: project uid +// in: path +// required: true +// type: string +// - name: ops_type_name +// description: the name of ops type to be created +// in: body +// required: true +// type: string +// responses: +// '200': +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +// default: +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) CreateOpsType(c echo.Context) error { + req := new(aV1.CreateOpsTypeReq) + err := bindAndValidateReq(c, req) + if nil != err { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + err = ctl.DMS.CreateOpsType(c.Request().Context(), req.ProjectUID, currentUserUid, req.Name) + if nil != err { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + return NewOkResp(c) +} + +// swagger:operation PUT /v1/dms/projects/{project_uid}/ops_types/{ops_type_uid} Project UpdateOpsType +// +// Update an existing ops type. +// +// --- +// parameters: +// - name: project_uid +// description: project uid +// in: path +// required: true +// type: string +// - name: ops_type_uid +// description: ops type id +// in: path +// required: true +// type: string +// - name: ops_type_name +// description: the name of ops type to be updated +// required: true +// in: body +// type: string +// responses: +// '200': +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +// default: +// description: GenericResp +// schema: +// "$ref": "#/definitions/GenericResp" +func (ctl *DMSController) UpdateOpsType(c echo.Context) error { + req := new(aV1.UpdateOpsTypeReq) + err := bindAndValidateReq(c, req) + if nil != err { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + err = ctl.DMS.UpdateOpsType(c.Request().Context(), req.ProjectUID, currentUserUid, req.OpsTypeUID, req.Name) + if nil != err { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkResp(c) +} + +// swagger:route DELETE /v1/dms/projects/{project_uid}/ops_types/{ops_type_uid} Project DeleteOpsType +// +// Delete an existing ops type. +// +// responses: +// 200: body:GenericResp +// default: body:GenericResp +func (ctl *DMSController) DeleteOpsType(c echo.Context) error { + req := new(aV1.DeleteOpsTypeReq) + err := bindAndValidateReq(c, req) + if nil != err { + return NewErrResp(c, err, apiError.BadRequestErr) + } + + currentUserUid, err := jwt.GetUserUidStrFromContext(c) + if err != nil { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + + err = ctl.DMS.DeleteOpsType(c.Request().Context(), req.ProjectUID, currentUserUid, req.OpsTypeUID) + if nil != err { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkResp(c) +} + +// swagger:route GET /v1/dms/projects/{project_uid}/ops_types Project ListOpsTypes +// +// List ops types. +// +// responses: +// 200: body:ListOpsTypesReply +// default: body:GenericResp +func (ctl *DMSController) ListOpsTypes(c echo.Context) error { + req := new(aV1.ListOpsTypeReq) + err := bindAndValidateReq(c, req) + if nil != err { + return NewErrResp(c, err, apiError.BadRequestErr) + } + reply, err := ctl.DMS.ListOpsTypes(c.Request().Context(), req) + if nil != err { + return NewErrResp(c, err, apiError.DMSServiceErr) + } + return NewOkRespWithReply(c, reply) +} diff --git a/internal/apiserver/service/router.go b/internal/apiserver/service/router.go index 01aacdecd..263c9392f 100644 --- a/internal/apiserver/service/router.go +++ b/internal/apiserver/service/router.go @@ -78,6 +78,12 @@ func (s *APIServer) initRouter() error { environmentTagV1.PUT("/:environment_tag_uid", s.DMSController.UpdateEnvironmentTag) environmentTagV1.DELETE("/:environment_tag_uid", s.DMSController.DeleteEnvironmentTag) + opsTypeV1 := v1.Group(dmsV1.OpsTypeRouterGroup) + opsTypeV1.POST("", s.DMSController.CreateOpsType) + opsTypeV1.GET("", s.DMSController.ListOpsTypes) + opsTypeV1.PUT("/:ops_type_uid", s.DMSController.UpdateOpsType) + opsTypeV1.DELETE("/:ops_type_uid", s.DMSController.DeleteOpsType) + dbServiceSyncTaskV1 := v1.Group("/dms/db_service_sync_tasks") dbServiceSyncTaskV1.GET("/tips", s.DMSController.ListDBServiceSyncTaskTips) dbServiceSyncTaskV1.GET("", s.DMSController.ListDBServiceSyncTasks) diff --git a/internal/dms/biz/ops_type.go b/internal/dms/biz/ops_type.go new file mode 100644 index 000000000..35acae44d --- /dev/null +++ b/internal/dms/biz/ops_type.go @@ -0,0 +1,250 @@ +package biz + +import ( + "context" + "fmt" + "strings" + + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + pkgRand "github.com/actiontech/dms/pkg/rand" +) + +// OpsTypeRepo 运维类型字典存储接口(对标 EnvironmentTagRepo)。 +type OpsTypeRepo interface { + CreateOpsType(ctx context.Context, opsType *OpsType) error + UpdateOpsType(ctx context.Context, opsTypeUID, opsTypeName string) error + DeleteOpsType(ctx context.Context, opsTypeUID string) error + GetOpsTypeByName(ctx context.Context, projectUID, name string) (bool, *OpsType, error) + GetOpsTypeByUID(ctx context.Context, uid string) (*OpsType, error) + ListOpsTypes(ctx context.Context, options *ListOpsTypesOption) ([]*OpsType, int64, error) +} + +// OpsType 项目级运维类型字典项。 +type OpsType struct { + UID string + Name string + ProjectUID string +} + +// ListOpsTypesOption 按项目分页列举运维类型。 +type ListOpsTypesOption struct { + Limit int + Offset int + ProjectUID string +} + +// OpsTypeUsecase 运维类型字典业务规则(权限 / 重名对齐 EnvironmentTagUsecase)。 +type OpsTypeUsecase struct { + opsTypeRepo OpsTypeRepo + projectUsecase *ProjectUsecase + opPermissionVerifyUsecase *OpPermissionVerifyUsecase + log *utilLog.Helper +} + +func NewOpsTypeUsecase(opsTypeRepo OpsTypeRepo, logger utilLog.Logger, + projectUsecase *ProjectUsecase, + opPermissionVerifyUsecase *OpPermissionVerifyUsecase) *OpsTypeUsecase { + return &OpsTypeUsecase{ + opsTypeRepo: opsTypeRepo, + projectUsecase: projectUsecase, + opPermissionVerifyUsecase: opPermissionVerifyUsecase, + log: utilLog.NewHelper(logger, utilLog.WithMessageKey("biz.ops_type")), + } +} + +// 默认运维类型名称(存库中文规范名;展示 i18n 见 service.localizeOpsTypeDisplayName)。 +var defaultOpsTypeNames = []string{ + "数据修改", + "数据提取", + "服务维护", + "配置修改", +} + +func (uc *OpsTypeUsecase) newOpsType(projectUID, name string) (*OpsType, error) { + uid, err := pkgRand.GenStrUid() + if err != nil { + return nil, err + } + if name == "" || projectUID == "" { + return nil, fmt.Errorf("ops type name or project is empty") + } + return &OpsType{ + UID: uid, + Name: name, + ProjectUID: projectUID, + }, nil +} + +// InitDefaultOpsTypes 新建项目预置默认运维类型(对标 InitDefaultEnvironmentTags)。 +func (uc *OpsTypeUsecase) InitDefaultOpsTypes(ctx context.Context, projectUID, currentUserUID string) (err error) { + for _, name := range defaultOpsTypeNames { + err = uc.CreateOpsType(ctx, projectUID, currentUserUID, name) + if err != nil { + uc.log.Errorf("create ops type failed: %v", err) + return fmt.Errorf("create ops type failed: %w", err) + } + } + return nil +} + +// ensureDefaultOpsTypes 空字典幂等补种:已存在则跳过;并发下唯一索引冲突后按名复检。 +func (uc *OpsTypeUsecase) ensureDefaultOpsTypes(ctx context.Context, projectUID string) error { + for _, name := range defaultOpsTypeNames { + exist, _, err := uc.opsTypeRepo.GetOpsTypeByName(ctx, projectUID, name) + if err != nil { + uc.log.Errorf("get ops type by name failed: %v", err) + return err + } + if exist { + continue + } + opsType, err := uc.newOpsType(projectUID, name) + if err != nil { + uc.log.Errorf("new ops type failed: %v", err) + return err + } + if err = uc.opsTypeRepo.CreateOpsType(ctx, opsType); err != nil { + existAfter, _, getErr := uc.opsTypeRepo.GetOpsTypeByName(ctx, projectUID, name) + if getErr == nil && existAfter { + continue + } + uc.log.Errorf("create ops type failed: %v", err) + return err + } + } + return nil +} + +func (uc *OpsTypeUsecase) CreateOpsType(ctx context.Context, projectUID, currentUserUID, name string) error { + name = strings.TrimSpace(name) + + if err := uc.projectUsecase.isProjectActive(ctx, projectUID); err != nil { + return fmt.Errorf("create ops type error: %v", err) + } + + if canOpProject, err := uc.opPermissionVerifyUsecase.CanOpProject(ctx, currentUserUID, projectUID, false); err != nil { + return fmt.Errorf("check user is project admin or golobal op permission failed: %v", err) + } else if !canOpProject { + return fmt.Errorf("user is not project admin or golobal op permission user") + } + + exist, _, err := uc.GetOpsTypeByName(ctx, projectUID, name) + if err != nil { + uc.log.Errorf("get ops type by name failed: %v", err) + return err + } + if exist { + return fmt.Errorf("the ops type %s already exists in the current project", name) + } + + opsType, err := uc.newOpsType(projectUID, name) + if err != nil { + uc.log.Errorf("new ops type failed: %v", err) + return err + } + if err = uc.opsTypeRepo.CreateOpsType(ctx, opsType); err != nil { + uc.log.Errorf("create ops type failed: %v", err) + return err + } + return nil +} + +func (uc *OpsTypeUsecase) UpdateOpsType(ctx context.Context, projectUID, currentUserUID, opsTypeUID, name string) error { + name = strings.TrimSpace(name) + + if err := uc.projectUsecase.isProjectActive(ctx, projectUID); err != nil { + return fmt.Errorf("update ops type error: %v", err) + } + + if canOpProject, err := uc.opPermissionVerifyUsecase.CanOpProject(ctx, currentUserUID, projectUID, false); err != nil { + return fmt.Errorf("check user is project admin or golobal op permission failed: %v", err) + } else if !canOpProject { + return fmt.Errorf("user is not project admin or golobal op permission user") + } + + if opsTypeUID == "" || name == "" { + return fmt.Errorf("ops type name or uid is empty, please check: id %v, name %v", opsTypeUID, name) + } + + if _, err := uc.opsTypeRepo.GetOpsTypeByUID(ctx, opsTypeUID); err != nil { + uc.log.Errorf("get ops type failed: %v", err) + return err + } + + exist, existing, err := uc.GetOpsTypeByName(ctx, projectUID, name) + if err != nil { + uc.log.Errorf("get ops type by name failed: %v", err) + return err + } + if exist && existing.UID != opsTypeUID { + return fmt.Errorf("the ops type %s already exists in the current project", name) + } + + if err = uc.opsTypeRepo.UpdateOpsType(ctx, opsTypeUID, name); err != nil { + uc.log.Errorf("update ops type failed: %v", err) + return err + } + return nil +} + +func (uc *OpsTypeUsecase) DeleteOpsType(ctx context.Context, projectUID, currentUserUID, opsTypeUID string) error { + if err := uc.projectUsecase.isProjectActive(ctx, projectUID); err != nil { + return fmt.Errorf("delete ops type error: %v", err) + } + + if canOpProject, err := uc.opPermissionVerifyUsecase.CanOpProject(ctx, currentUserUID, projectUID, false); err != nil { + return fmt.Errorf("check user is project admin or golobal op permission failed: %v", err) + } else if !canOpProject { + return fmt.Errorf("user is not project admin or golobal op permission user") + } + + if _, err := uc.opsTypeRepo.GetOpsTypeByUID(ctx, opsTypeUID); err != nil { + uc.log.Errorf("get ops type failed: %v", err) + return err + } + if err := uc.opsTypeRepo.DeleteOpsType(ctx, opsTypeUID); err != nil { + uc.log.Errorf("delete ops type failed: %v", err) + return err + } + return nil +} + +func (uc *OpsTypeUsecase) ListOpsTypes(ctx context.Context, options *ListOpsTypesOption) ([]*OpsType, int64, error) { + opsTypes, count, err := uc.opsTypeRepo.ListOpsTypes(ctx, options) + if err != nil { + uc.log.Errorf("list ops types failed: %v", err) + return nil, 0, err + } + // 存量空字典:首次读取幂等补种默认 4 项(D9)。 + if count == 0 && options != nil && options.ProjectUID != "" { + if err = uc.ensureDefaultOpsTypes(ctx, options.ProjectUID); err != nil { + uc.log.Errorf("ensure default ops types failed: %v", err) + return nil, 0, err + } + opsTypes, count, err = uc.opsTypeRepo.ListOpsTypes(ctx, options) + if err != nil { + uc.log.Errorf("list ops types after seed failed: %v", err) + return nil, 0, err + } + } + return opsTypes, count, nil +} + +func (uc *OpsTypeUsecase) GetOpsTypeByName(ctx context.Context, projectUID, name string) (bool, *OpsType, error) { + name = strings.TrimSpace(name) + exist, opsType, err := uc.opsTypeRepo.GetOpsTypeByName(ctx, projectUID, name) + if err != nil { + uc.log.Errorf("get ops type failed: %v", err) + return false, nil, err + } + return exist, opsType, nil +} + +func (uc *OpsTypeUsecase) GetOpsTypeByUID(ctx context.Context, uid string) (*OpsType, error) { + opsType, err := uc.opsTypeRepo.GetOpsTypeByUID(ctx, uid) + if err != nil { + uc.log.Errorf("get ops type failed: %v", err) + return nil, err + } + return opsType, nil +} diff --git a/internal/dms/biz/ops_type_test.go b/internal/dms/biz/ops_type_test.go new file mode 100644 index 000000000..34b4f9275 --- /dev/null +++ b/internal/dms/biz/ops_type_test.go @@ -0,0 +1,300 @@ +package biz + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + + pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + "github.com/stretchr/testify/assert" +) + +// fakeOpsTypeRepo 内存实现,供 biz 规则自验。 +type fakeOpsTypeRepo struct { + byUID map[string]*OpsType + create []*OpsType +} + +func newFakeOpsTypeRepo() *fakeOpsTypeRepo { + return &fakeOpsTypeRepo{byUID: map[string]*OpsType{}} +} + +func (f *fakeOpsTypeRepo) CreateOpsType(_ context.Context, opsType *OpsType) error { + cp := *opsType + f.byUID[opsType.UID] = &cp + f.create = append(f.create, &cp) + return nil +} + +func (f *fakeOpsTypeRepo) UpdateOpsType(_ context.Context, opsTypeUID, opsTypeName string) error { + ot, ok := f.byUID[opsTypeUID] + if !ok { + return fmt.Errorf("ops type not found") + } + ot.Name = opsTypeName + return nil +} + +func (f *fakeOpsTypeRepo) DeleteOpsType(_ context.Context, opsTypeUID string) error { + if _, ok := f.byUID[opsTypeUID]; !ok { + return fmt.Errorf("ops type not found") + } + delete(f.byUID, opsTypeUID) + return nil +} + +func (f *fakeOpsTypeRepo) GetOpsTypeByName(_ context.Context, projectUID, name string) (bool, *OpsType, error) { + for _, ot := range f.byUID { + if ot.ProjectUID == projectUID && ot.Name == name { + cp := *ot + return true, &cp, nil + } + } + return false, nil, nil +} + +func (f *fakeOpsTypeRepo) GetOpsTypeByUID(_ context.Context, uid string) (*OpsType, error) { + ot, ok := f.byUID[uid] + if !ok { + return nil, fmt.Errorf("ops type not found") + } + cp := *ot + return &cp, nil +} + +func (f *fakeOpsTypeRepo) ListOpsTypes(_ context.Context, options *ListOpsTypesOption) ([]*OpsType, int64, error) { + out := make([]*OpsType, 0) + for _, ot := range f.byUID { + if ot.ProjectUID == options.ProjectUID { + cp := *ot + out = append(out, &cp) + } + } + return out, int64(len(out)), nil +} + +func newOpsTypeUsecaseForTest(repo OpsTypeRepo, projectUID string, opRepo OpPermissionVerifyRepo, users map[string]*User) *OpsTypeUsecase { + logger := utilLog.NewMyLogger(io.Discard) + projectRepo := &fakeProjectRepoForPassword{ + project: &Project{UID: projectUID, Status: ProjectStatusActive}, + } + projectUC := &ProjectUsecase{ + repo: projectRepo, + log: utilLog.NewHelper(logger, utilLog.WithMessageKey("biz.project.test")), + } + if users == nil { + users = map[string]*User{} + } + opUC := NewOpPermissionVerifyUsecase(logger, nil, opRepo, &mockUserRepo{users: users}) + return NewOpsTypeUsecase(repo, logger, projectUC, opUC) +} + +func TestOpsTypeUsecase_CreateUpdatePermissionAndDuplicate(t *testing.T) { + const projectUID = "2088162858587131904" // ops-type-proj-a + const projectAdminUID = "proj-admin-1" + const memberUID = "proj-member-1" + + repo := newFakeOpsTypeRepo() + opRepo := &mockOpPermissionVerifyRepo{ + projectPermissions: map[string]map[string]map[string]bool{ + projectAdminUID: { + projectUID: {pkgConst.UIDOfOpPermissionProjectAdmin: true}, + }, + memberUID: { + projectUID: {}, + }, + }, + globalPermissions: map[string][]*OpPermission{}, + } + users := map[string]*User{ + pkgConst.UIDOfUserAdmin: {UID: pkgConst.UIDOfUserAdmin, BusinessWritePermission: true}, + projectAdminUID: {UID: projectAdminUID}, + memberUID: {UID: memberUID}, + } + uc := newOpsTypeUsecaseForTest(repo, projectUID, opRepo, users) + ctx := context.Background() + + t.Run("project_admin_create_ok", func(t *testing.T) { + err := uc.CreateOpsType(ctx, projectUID, projectAdminUID, " 数据修改 ") + assert.NoError(t, err) + exist, got, err := uc.GetOpsTypeByName(ctx, projectUID, "数据修改") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "数据修改", got.Name) + assert.Equal(t, projectUID, got.ProjectUID) + }) + + t.Run("duplicate_name_rejected", func(t *testing.T) { + err := uc.CreateOpsType(ctx, projectUID, projectAdminUID, "数据修改") + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "already exists"), "err=%v", err) + }) + + t.Run("duplicate_after_trim_rejected", func(t *testing.T) { + err := uc.CreateOpsType(ctx, projectUID, projectAdminUID, "\t数据修改\n") + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "already exists"), "err=%v", err) + }) + + t.Run("member_create_rejected", func(t *testing.T) { + err := uc.CreateOpsType(ctx, projectUID, memberUID, "服务维护") + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "not project admin"), "err=%v", err) + }) + + t.Run("platform_admin_create_ok", func(t *testing.T) { + err := uc.CreateOpsType(ctx, projectUID, pkgConst.UIDOfUserAdmin, "服务维护") + assert.NoError(t, err) + }) + + t.Run("project_admin_rename_ok", func(t *testing.T) { + exist, got, err := uc.GetOpsTypeByName(ctx, projectUID, "服务维护") + assert.NoError(t, err) + assert.True(t, exist) + err = uc.UpdateOpsType(ctx, projectUID, projectAdminUID, got.UID, " 配置修改 ") + assert.NoError(t, err) + exist, renamed, err := uc.GetOpsTypeByName(ctx, projectUID, "配置修改") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, got.UID, renamed.UID) + }) + + t.Run("rename_to_existing_rejected", func(t *testing.T) { + exist, got, err := uc.GetOpsTypeByName(ctx, projectUID, "配置修改") + assert.NoError(t, err) + assert.True(t, exist) + err = uc.UpdateOpsType(ctx, projectUID, projectAdminUID, got.UID, "数据修改") + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "already exists"), "err=%v", err) + }) + + t.Run("member_update_rejected", func(t *testing.T) { + exist, got, err := uc.GetOpsTypeByName(ctx, projectUID, "配置修改") + assert.NoError(t, err) + assert.True(t, exist) + err = uc.UpdateOpsType(ctx, projectUID, memberUID, got.UID, "其它名称") + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "not project admin"), "err=%v", err) + }) + + t.Run("member_delete_rejected", func(t *testing.T) { + exist, got, err := uc.GetOpsTypeByName(ctx, projectUID, "配置修改") + assert.NoError(t, err) + assert.True(t, exist) + err = uc.DeleteOpsType(ctx, projectUID, memberUID, got.UID) + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "not project admin"), "err=%v", err) + }) + + t.Run("list_readable_without_write_perm", func(t *testing.T) { + list, count, err := uc.ListOpsTypes(ctx, &ListOpsTypesOption{ProjectUID: projectUID, Limit: 100, Offset: 0}) + assert.NoError(t, err) + assert.GreaterOrEqual(t, count, int64(1)) + assert.NotEmpty(t, list) + }) + + t.Run("project_admin_delete_ok", func(t *testing.T) { + exist, got, err := uc.GetOpsTypeByName(ctx, projectUID, "配置修改") + assert.NoError(t, err) + assert.True(t, exist) + err = uc.DeleteOpsType(ctx, projectUID, projectAdminUID, got.UID) + assert.NoError(t, err) + exist, _, err = uc.GetOpsTypeByName(ctx, projectUID, "配置修改") + assert.NoError(t, err) + assert.False(t, exist) + }) +} + +func TestOpsTypeUsecase_InitAndListSeed(t *testing.T) { + const projectUID = "seed-proj-uid" + const projectAdminUID = "seed-admin-1" + + expected := []string{"数据修改", "数据提取", "服务维护", "配置修改"} + + opRepo := &mockOpPermissionVerifyRepo{ + projectPermissions: map[string]map[string]map[string]bool{ + projectAdminUID: { + projectUID: {pkgConst.UIDOfOpPermissionProjectAdmin: true}, + }, + }, + globalPermissions: map[string][]*OpPermission{}, + } + users := map[string]*User{ + projectAdminUID: {UID: projectAdminUID}, + } + ctx := context.Background() + + t.Run("init_default_on_create_project", func(t *testing.T) { + repo := newFakeOpsTypeRepo() + uc := newOpsTypeUsecaseForTest(repo, projectUID, opRepo, users) + err := uc.InitDefaultOpsTypes(ctx, projectUID, projectAdminUID) + assert.NoError(t, err) + list, count, err := repo.ListOpsTypes(ctx, &ListOpsTypesOption{ProjectUID: projectUID, Limit: 100, Offset: 0}) + assert.NoError(t, err) + assert.Equal(t, int64(4), count) + gotNames := make([]string, 0, len(list)) + for _, ot := range list { + gotNames = append(gotNames, ot.Name) + } + assert.ElementsMatch(t, expected, gotNames) + }) + + t.Run("list_empty_seeds_then_idempotent", func(t *testing.T) { + repo := newFakeOpsTypeRepo() + uc := newOpsTypeUsecaseForTest(repo, projectUID, opRepo, users) + + list1, count1, err := uc.ListOpsTypes(ctx, &ListOpsTypesOption{ProjectUID: projectUID, Limit: 100, Offset: 0}) + assert.NoError(t, err) + assert.Equal(t, int64(4), count1) + gotNames := make([]string, 0, len(list1)) + for _, ot := range list1 { + gotNames = append(gotNames, ot.Name) + } + assert.ElementsMatch(t, expected, gotNames) + + list2, count2, err := uc.ListOpsTypes(ctx, &ListOpsTypesOption{ProjectUID: projectUID, Limit: 100, Offset: 0}) + assert.NoError(t, err) + assert.Equal(t, int64(4), count2) + assert.Equal(t, count1, count2) + assert.Len(t, list2, 4) + assert.Equal(t, 4, len(repo.create), "second list must not create again") + }) + + t.Run("ensure_tolerates_concurrent_duplicate", func(t *testing.T) { + repo := &racingOpsTypeRepo{fakeOpsTypeRepo: newFakeOpsTypeRepo()} + uc := newOpsTypeUsecaseForTest(repo, projectUID, opRepo, users) + // 预先写入一项,并让 Create 对同名返回 duplicate(模拟并发赢家已插入)。 + pre, err := uc.newOpsType(projectUID, "数据修改") + assert.NoError(t, err) + assert.NoError(t, repo.fakeOpsTypeRepo.CreateOpsType(ctx, pre)) + repo.failNextCreateFor = "数据提取" + + err = uc.ensureDefaultOpsTypes(ctx, projectUID) + assert.NoError(t, err) + _, count, err := repo.ListOpsTypes(ctx, &ListOpsTypesOption{ProjectUID: projectUID, Limit: 100, Offset: 0}) + assert.NoError(t, err) + assert.Equal(t, int64(4), count) + }) +} + +// racingOpsTypeRepo 模拟唯一索引竞态:指定名称首次 Create 失败但随后 Get 可见。 +type racingOpsTypeRepo struct { + *fakeOpsTypeRepo + failNextCreateFor string +} + +func (r *racingOpsTypeRepo) CreateOpsType(ctx context.Context, opsType *OpsType) error { + if r.failNextCreateFor != "" && opsType.Name == r.failNextCreateFor { + name := r.failNextCreateFor + r.failNextCreateFor = "" + // 另一并发已写入 + cp := *opsType + cp.UID = "race-winner-" + name + r.byUID[cp.UID] = &cp + return fmt.Errorf("Error 1062: Duplicate entry for key project_uid_ops_type_name") + } + return r.fakeOpsTypeRepo.CreateOpsType(ctx, opsType) +} diff --git a/internal/dms/biz/project.go b/internal/dms/biz/project.go index ed6107ac9..df9124e42 100644 --- a/internal/dms/biz/project.go +++ b/internal/dms/biz/project.go @@ -92,6 +92,7 @@ type ProjectUsecase struct { memberUsecase *MemberUsecase businessTagUsecase *BusinessTagUsecase environmentTagUsecase *EnvironmentTagUsecase + opsTypeUsecase *OpsTypeUsecase opPermissionVerifyUsecase *OpPermissionVerifyUsecase pluginUsecase *PluginUsecase log *utilLog.Helper @@ -102,7 +103,8 @@ func NewProjectUsecase(log utilLog.Logger, tx TransactionGenerator, repo Project opPermissionVerifyUsecase *OpPermissionVerifyUsecase, pluginUsecase *PluginUsecase, businessTagUsecase *BusinessTagUsecase, - environmentTagUsecase *EnvironmentTagUsecase) *ProjectUsecase { + environmentTagUsecase *EnvironmentTagUsecase, + opsTypeUsecase *OpsTypeUsecase) *ProjectUsecase { return &ProjectUsecase{ tx: tx, repo: repo, @@ -111,6 +113,7 @@ func NewProjectUsecase(log utilLog.Logger, tx TransactionGenerator, repo Project pluginUsecase: pluginUsecase, businessTagUsecase: businessTagUsecase, environmentTagUsecase: environmentTagUsecase, + opsTypeUsecase: opsTypeUsecase, opPermissionVerifyUsecase: opPermissionVerifyUsecase, } } @@ -207,6 +210,16 @@ func (d *ProjectUsecase) InitProjects(ctx context.Context) (err error) { ) return fmt.Errorf("init default environment tags failed: %v", err) } + // 初始化运维类型字典 + err = d.opsTypeUsecase.InitDefaultOpsTypes(ctx, n.UID, n.CreateUserUID) + if err != nil { + d.log.Error("init default ops types failed", + "error", err, + "project_uid", n.UID, + "create_user_uid", n.CreateUserUID, + ) + return fmt.Errorf("init default ops types failed: %v", err) + } } d.log.Debug("init project success") return nil diff --git a/internal/dms/biz/repo_fields.go b/internal/dms/biz/repo_fields.go index e4f4f5e6f..bf1ccbb83 100644 --- a/internal/dms/biz/repo_fields.go +++ b/internal/dms/biz/repo_fields.go @@ -374,6 +374,7 @@ const ( WorkflowFieldWorkflowRecordUid WorkflowField = "workflow_record_uid" WorkflowFieldWorkflowTemplateId WorkflowField = "workflow_template_id" WorkflowFieldWorkflowTemplateName WorkflowField = "workflow_template_name" + WorkflowFieldOpsTypeUID WorkflowField = "ops_type_uid" WorkflowFieldWorkflowRecord WorkflowField = "workflowrecord" ) diff --git a/internal/dms/service/ops_type.go b/internal/dms/service/ops_type.go new file mode 100644 index 000000000..cc75b555a --- /dev/null +++ b/internal/dms/service/ops_type.go @@ -0,0 +1,101 @@ +package service + +import ( + "context" + "fmt" + + v1 "github.com/actiontech/dms/api/dms/service/v1" + "github.com/actiontech/dms/internal/dms/biz" + "github.com/actiontech/dms/internal/pkg/locale" + dmsCommonV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" + "github.com/nicksnyder/go-i18n/v2/i18n" +) + +// defaultOpsTypeLocaleByCanonicalName 存库中文规范名 → locale 消息(用户改名后不再映射)。 +var defaultOpsTypeLocaleByCanonicalName = map[string]*i18n.Message{ + locale.DefaultOpsTypeNameDataModification.Other: locale.DefaultOpsTypeNameDataModification, + locale.DefaultOpsTypeNameDataExtraction.Other: locale.DefaultOpsTypeNameDataExtraction, + locale.DefaultOpsTypeNameServiceMaintenance.Other: locale.DefaultOpsTypeNameServiceMaintenance, + locale.DefaultOpsTypeNameConfigModification.Other: locale.DefaultOpsTypeNameConfigModification, +} + +func localizeOpsTypeDisplayName(ctx context.Context, name string) string { + if msg, ok := defaultOpsTypeLocaleByCanonicalName[name]; ok { + return locale.Bundle.LocalizeMsgByCtx(ctx, msg) + } + return name +} + +func (d *DMSService) CreateOpsType(ctx context.Context, projectUid, currentUserUid, opsTypeName string) (err error) { + d.log.Infof("CreateOpsType.req=%v", opsTypeName) + defer func() { + d.log.Infof("CreateOpsType.req=%v;error=%v", opsTypeName, err) + }() + + if err := d.OpsTypeUsecase.CreateOpsType(ctx, projectUid, currentUserUid, opsTypeName); err != nil { + return fmt.Errorf("create ops type failed: %w", err) + } + + return nil +} + +func (d *DMSService) UpdateOpsType(ctx context.Context, projectUid, currentUserUid string, opsTypeUID, opsTypeName string) (err error) { + d.log.Infof("UpdateOpsType.req=%v", opsTypeName) + defer func() { + d.log.Infof("UpdateOpsType.req=%v;error=%v", opsTypeName, err) + }() + + if err := d.OpsTypeUsecase.UpdateOpsType(ctx, projectUid, currentUserUid, opsTypeUID, opsTypeName); err != nil { + return fmt.Errorf("update ops type failed: %w", err) + } + return nil +} + +func (d *DMSService) DeleteOpsType(ctx context.Context, projectUid, currentUserUid string, opsTypeUID string) (err error) { + d.log.Infof("DeleteOpsType.req=%v", opsTypeUID) + defer func() { + d.log.Infof("DeleteOpsType.req=%v;error=%v", opsTypeUID, err) + }() + + // 对标 DeleteEnvironmentTag:被数据导出工单引用时不允许删除(AC-B05;本期不校验 SQL 上线侧)。 + count, err := d.DataExportWorkflowUsecase.CountDataExportWorkflowsByOpsTypeUID(ctx, projectUid, opsTypeUID) + if err != nil { + return fmt.Errorf("check ops type reference failed: %w", err) + } + if count > 0 { + return fmt.Errorf("%s", locale.Bundle.LocalizeMsgByCtx(ctx, locale.ErrOpsTypeReferencedByDataExportWorkflow)) + } + + if err := d.OpsTypeUsecase.DeleteOpsType(ctx, projectUid, currentUserUid, opsTypeUID); err != nil { + return fmt.Errorf("delete ops type failed: %w", err) + } + + return nil +} + +func (d *DMSService) ListOpsTypes(ctx context.Context, req *v1.ListOpsTypeReq) (reply *v1.ListOpsTypesReply, err error) { + d.log.Infof("ListOpsTypes.req=%v", *req) + defer func() { + d.log.Infof("ListOpsTypes.req=%v;error=%v", *req, err) + }() + limit, offset := d.GetLimitAndOffset(req.PageIndex, req.PageSize) + bizOpsTypes, count, err := d.OpsTypeUsecase.ListOpsTypes(ctx, &biz.ListOpsTypesOption{ + Limit: limit, + Offset: offset, + ProjectUID: req.ProjectUID, + }) + if err != nil { + return nil, fmt.Errorf("list ops types failed: %w", err) + } + opsTypes := make([]*dmsCommonV1.OpsType, 0, len(bizOpsTypes)) + for _, bizOpsType := range bizOpsTypes { + opsTypes = append(opsTypes, &dmsCommonV1.OpsType{ + UID: bizOpsType.UID, + Name: localizeOpsTypeDisplayName(ctx, bizOpsType.Name), + }) + } + return &v1.ListOpsTypesReply{ + Data: opsTypes, + Total: count, + }, nil +} diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index be35fa383..cc4dafb76 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -19,6 +19,7 @@ type DMSService struct { DBServiceUsecase *biz.DBServiceUsecase DBServiceSyncTaskUsecase *biz.DBServiceSyncTaskUsecase EnvironmentTagUsecase *biz.EnvironmentTagUsecase + OpsTypeUsecase *biz.OpsTypeUsecase LoginConfigurationUsecase *biz.LoginConfigurationUsecase UserUsecase *biz.UserUsecase UserGroupUsecase *biz.UserGroupUsecase @@ -84,13 +85,15 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer // 预定义解决usecase循环依赖问题 memberUsecase := biz.MemberUsecase{} environmentTagUsecase := biz.EnvironmentTagUsecase{} + opsTypeUsecase := biz.OpsTypeUsecase{} businessTagUsecase := biz.NewBusinessTagUsecase(storage.NewBusinessTagRepo(logger, st), logger) projectRepo := storage.NewProjectRepo(logger, st) - projectUsecase := biz.NewProjectUsecase(logger, tx, projectRepo, &memberUsecase, opPermissionVerifyUsecase, pluginUseCase, businessTagUsecase, &environmentTagUsecase) + projectUsecase := biz.NewProjectUsecase(logger, tx, projectRepo, &memberUsecase, opPermissionVerifyUsecase, pluginUseCase, businessTagUsecase, &environmentTagUsecase, &opsTypeUsecase) dbServiceRepo := storage.NewDBServiceRepo(logger, st) dmsProxyTargetRepo := storage.NewProxyTargetRepo(logger, st) resourceOverviewUsecase := biz.NewResourceOverviewUsecase(logger, projectRepo, dbServiceRepo, *opPermissionVerifyUsecase, storage.NewResourceOverviewRepo(logger, st), dmsProxyTargetRepo) environmentTagUsecase = *biz.NewEnvironmentTagUsecase(storage.NewEnvironmentTagRepo(logger, st), logger, projectUsecase, opPermissionVerifyUsecase) + opsTypeUsecase = *biz.NewOpsTypeUsecase(storage.NewOpsTypeRepo(logger, st), logger, projectUsecase, opPermissionVerifyUsecase) discoveryTaskRepo := storage.NewSensitiveDataDiscoveryTaskRepo(logger, st) dbServiceUseCase := biz.NewDBServiceUsecase(logger, dbServiceRepo, discoveryTaskRepo, pluginUseCase, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, &environmentTagUsecase) dbServiceTaskRepo := storage.NewDBServiceSyncTaskRepo(logger, st) @@ -164,7 +167,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer if err != nil { return nil, fmt.Errorf("failed to initialize unmasking workflow usecase: %v", err) } - DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, dbServiceUseCase, unmaskingWorkflowUsecase, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) + DataExportWorkflowUsecase := biz.NewDataExportWorkflowUsecase(logger, tx, workflowRepo, dataExportTaskRepo, dbServiceRepo, dataExportMaskingConfigRepo, dataExportMaskingRuleRepo, opPermissionVerifyUsecase, projectUsecase, &opsTypeUsecase, dmsProxyTargetRepo, clusterUsecase, webhookConfigurationUsecase, userUsecase, systemVariableUsecase, dbServiceUseCase, unmaskingWorkflowUsecase, fmt.Sprintf("%s:%d", opts.ReportHost, opts.APIServiceOpts.Port)) dataMaskingUsecase, stopDataMaskingScheduler, err := initDataMaskingUsecase(logger, st, dbServiceUseCase, clusterUsecase, dmsProxyTargetRepo) if err != nil { return nil, fmt.Errorf("failed to initialize data masking usecase: %v", err) @@ -189,6 +192,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer ResourceOverviewUsecase: resourceOverviewUsecase, BusinessTagUsecase: businessTagUsecase, EnvironmentTagUsecase: &environmentTagUsecase, + OpsTypeUsecase: &opsTypeUsecase, PluginUsecase: pluginUseCase, DBServiceUsecase: dbServiceUseCase, DBServiceSyncTaskUsecase: dbServiceTaskUsecase, diff --git a/internal/dms/storage/model/model.go b/internal/dms/storage/model/model.go index 3a83c2ef5..20d6e4c5f 100644 --- a/internal/dms/storage/model/model.go +++ b/internal/dms/storage/model/model.go @@ -58,6 +58,7 @@ var AutoMigrateList = []interface{}{ UserLoginSession{}, CbOperationLog{}, EnvironmentTag{}, + OpsType{}, Gateway{}, SystemVariable{}, OperationRecord{}, @@ -100,6 +101,13 @@ type EnvironmentTag struct { Color string `json:"color" gorm:"size:32;column:color"` } +// OpsType 项目级运维类型字典项(对标 EnvironmentTag,无色标)。 +type OpsType struct { + Model + ProjectUID string `json:"project_uid" gorm:"size:32;column:project_uid;uniqueIndex:project_uid_ops_type_name"` + OpsTypeName string `json:"ops_type_name" gorm:"size:191;column:ops_type_name;not null;uniqueIndex:project_uid_ops_type_name"` +} + type ExtraParameters struct { SqleConfig *SQLEConfig `json:"sqle_config"` AdditionalParam params.Params `json:"additional_param"` @@ -492,6 +500,8 @@ type Workflow struct { WorkflowRecordUid string `json:"workflow_record_uid" gorm:"size:32;column:workflow_record_uid"` WorkflowTemplateId uint `json:"workflow_template_id" gorm:"column:workflow_template_id;default:0;index" example:"1"` WorkflowTemplateName string `json:"workflow_template_name" gorm:"size:255;column:workflow_template_name" example:""` + // OpsTypeUID 运维类型字典项标识(引用式存储;空表示未设置;创建后不可改) + OpsTypeUID string `json:"ops_type_uid" gorm:"size:32;column:ops_type_uid;default:''"` WorkflowRecord *WorkflowRecord `gorm:"foreignkey:WorkflowUid"` } diff --git a/internal/dms/storage/ops_type.go b/internal/dms/storage/ops_type.go new file mode 100644 index 000000000..93798da94 --- /dev/null +++ b/internal/dms/storage/ops_type.go @@ -0,0 +1,122 @@ +package storage + +import ( + "context" + "fmt" + + "github.com/actiontech/dms/internal/dms/biz" + pkgErr "github.com/actiontech/dms/internal/dms/pkg/errors" + "github.com/actiontech/dms/internal/dms/storage/model" + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + "gorm.io/gorm" +) + +var _ biz.OpsTypeRepo = (*OpsTypeRepo)(nil) + +type OpsTypeRepo struct { + *Storage + log *utilLog.Helper +} + +func NewOpsTypeRepo(log utilLog.Logger, s *Storage) *OpsTypeRepo { + return &OpsTypeRepo{ + Storage: s, + log: utilLog.NewHelper(log, utilLog.WithMessageKey("storage.ops_type")), + } +} + +func (repo *OpsTypeRepo) toModel(opsType *biz.OpsType) *model.OpsType { + return &model.OpsType{ + OpsTypeName: opsType.Name, + Model: model.Model{UID: opsType.UID}, + ProjectUID: opsType.ProjectUID, + } +} + +func (repo *OpsTypeRepo) toBiz(opsType *model.OpsType) *biz.OpsType { + return &biz.OpsType{ + Name: opsType.OpsTypeName, + UID: opsType.UID, + ProjectUID: opsType.ProjectUID, + } +} + +func (repo *OpsTypeRepo) CreateOpsType(ctx context.Context, opsType *biz.OpsType) error { + return transaction(repo.log, ctx, repo.db, func(tx *gorm.DB) error { + if err := tx.WithContext(ctx).Create(repo.toModel(opsType)).Error; err != nil { + return pkgErr.WrapStorageErr(repo.log, fmt.Errorf("failed to create ops type: %v", err)) + } + return nil + }) +} + +func (repo *OpsTypeRepo) UpdateOpsType(ctx context.Context, opsTypeUID, opsTypeName string) error { + return transaction(repo.log, ctx, repo.db, func(tx *gorm.DB) error { + if err := tx.WithContext(ctx).Model(&model.OpsType{}).Where("uid = ?", opsTypeUID).Updates(map[string]interface{}{ + "ops_type_name": opsTypeName, + }).Error; err != nil { + return pkgErr.WrapStorageErr(repo.log, fmt.Errorf("failed to update ops type: %v", err)) + } + return nil + }) +} + +func (repo *OpsTypeRepo) DeleteOpsType(ctx context.Context, opsTypeUID string) error { + return transaction(repo.log, ctx, repo.db, func(tx *gorm.DB) error { + if err := tx.WithContext(ctx).Where("uid = ?", opsTypeUID).Delete(&model.OpsType{}).Error; err != nil { + return pkgErr.WrapStorageErr(repo.log, fmt.Errorf("failed to delete ops type: %v", err)) + } + return nil + }) +} + +func (repo *OpsTypeRepo) GetOpsTypeByName(ctx context.Context, projectUID, name string) (bool, *biz.OpsType, error) { + var opsType model.OpsType + if err := repo.db.WithContext(ctx).Where("ops_type_name = ? AND project_uid = ?", name, projectUID).First(&opsType).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return false, nil, nil + } + return false, nil, fmt.Errorf("failed to get ops type by name: %w", err) + } + return true, repo.toBiz(&opsType), nil +} + +func (repo *OpsTypeRepo) GetOpsTypeByUID(ctx context.Context, uid string) (*biz.OpsType, error) { + var opsType model.OpsType + if err := repo.db.WithContext(ctx).Where("uid = ?", uid).First(&opsType).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, pkgErr.ErrStorageNoData + } + return nil, fmt.Errorf("failed to get ops type by uid: %w", err) + } + return repo.toBiz(&opsType), nil +} + +func (repo *OpsTypeRepo) ListOpsTypes(ctx context.Context, options *biz.ListOpsTypesOption) ([]*biz.OpsType, int64, error) { + var opsTypes []*model.OpsType + db := repo.db.WithContext(ctx) + + query := db.Model(&model.OpsType{}).Where("project_uid = ?", options.ProjectUID) + if options.Limit >= 0 { + query = query.Limit(options.Limit) + } + if options.Offset >= 0 { + query = query.Offset(options.Offset) + } + + if err := query.Find(&opsTypes).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list ops types: %w", err) + } + + var count int64 + if err := repo.db.WithContext(ctx).Model(&model.OpsType{}).Where("project_uid = ?", options.ProjectUID).Count(&count).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count ops types: %w", err) + } + + bizOpsTypes := make([]*biz.OpsType, 0, len(opsTypes)) + for _, opsType := range opsTypes { + bizOpsTypes = append(bizOpsTypes, repo.toBiz(opsType)) + } + + return bizOpsTypes, count, nil +} diff --git a/internal/pkg/locale/active.en.toml b/internal/pkg/locale/active.en.toml index 495c0aa17..b1cbe1331 100644 --- a/internal/pkg/locale/active.en.toml +++ b/internal/pkg/locale/active.en.toml @@ -43,6 +43,10 @@ DataWorkflowExportSuccess = "✅ Data Export Workflow Execute Succeeded" DataWorkflowReject = "❌ Data Export Workflow Rejected" DataWorkflowWaitExporting = "⏳ Data Export Workflow Waiting Export" DataWorkflowWaiting = "🔍 Data Export Workflow Pending " +DefaultOpsTypeNameConfigModification = "Configuration Modification" +DefaultOpsTypeNameDataExtraction = "Data Extraction" +DefaultOpsTypeNameDataModification = "Data Modification" +DefaultOpsTypeNameServiceMaintenance = "Service Maintenance" DescOpPermissionAuditWorkflow = "Approve/reject workflow; Users with this permission can approve/reject workflow" DescOpPermissionAuthDBServiceData = "Authorize db instance data permissions; Users with this permission can authorize db instance data permissions" DescOpPermissionCreateOptimization = "Create SQL optimization; Users with this permission can create SQL optimization" @@ -66,6 +70,7 @@ DescRoleDevEngineer = "Users with this permission can create/edit workflow, exec DescRoleDevManager = "Users with this permission can view others' created workflow, approve/reject workflow, configure pipelines, view others' created SQL optimization" DescRoleOpsEngineer = "Users with this permission can view others' created workflow, execute workflow, create scan task, view others' created scan task, create data export task" DescRoleProjectAdmin = "Project admin" +ErrOpsTypeReferencedByDataExportWorkflow = "This operations type is referenced by data export workflows and cannot be deleted" IDBPCErrDB2DbNameInvalid = "DB2 database name is incorrect" IDBPCErrDataExportRuleTemplateInvalid = "Data Export audit rule template does not exist or is incompatible with the db instance type" IDBPCErrDbTypeInvalid = "DB instance type is invalid or corresponding plugin is not installed" diff --git a/internal/pkg/locale/active.zh.toml b/internal/pkg/locale/active.zh.toml index d37c75f54..f1b5bc5c0 100644 --- a/internal/pkg/locale/active.zh.toml +++ b/internal/pkg/locale/active.zh.toml @@ -43,6 +43,10 @@ DataWorkflowExportSuccess = "✅ 数据导出成功" DataWorkflowReject = "❌ 数据导出工单被驳回" DataWorkflowWaitExporting = "⏳ 数据导出工单待导出" DataWorkflowWaiting = "🔍 数据导出工单待审批" +DefaultOpsTypeNameConfigModification = "配置修改" +DefaultOpsTypeNameDataExtraction = "数据提取" +DefaultOpsTypeNameDataModification = "数据修改" +DefaultOpsTypeNameServiceMaintenance = "服务维护" DescOpPermissionAuditWorkflow = "审核/驳回工单;拥有该权限的用户可以审核/驳回工单" DescOpPermissionAuthDBServiceData = "授权数据源数据权限;拥有该权限的用户可以授权数据源数据权限" DescOpPermissionCreateOptimization = "创建智能调优;拥有该权限的用户可以创建智能调优" @@ -84,6 +88,7 @@ DescRoleDevEngineer = "拥有该权限的用户可以创建/编辑工单,SQL工 DescRoleDevManager = "拥有该权限的用户可以创建/编辑工单,审核/驳回工单,配置流水线,查看他人创建的智能调优" DescRoleOpsEngineer = "拥有该权限的用户可以上线工单,查看他人创建的工单,创建智能扫描,查看他人的扫描任务,数据导出" DescRoleProjectAdmin = "project admin" +ErrOpsTypeReferencedByDataExportWorkflow = "该运维类型已被数据导出工单引用,无法删除" IDBPCErrDB2DbNameInvalid = "DB2数据库名错误" IDBPCErrDataExportRuleTemplateInvalid = "数据导出审核规则模板不存在或数据源类型不匹配" IDBPCErrDbTypeInvalid = "数据源类型不规范或对应插件未安装" diff --git a/internal/pkg/locale/message_zh.go b/internal/pkg/locale/message_zh.go index 72b228e86..df602e198 100644 --- a/internal/pkg/locale/message_zh.go +++ b/internal/pkg/locale/message_zh.go @@ -245,6 +245,15 @@ var ( OAuth2SyncSessionErr = &i18n.Message{ID: "OAuth2SyncSessionErr", Other: "同步OAuth2会话失败: %v"} ) +// Ops Type(默认字典名存库为中文规范名;展示与拒删提示按请求语言本地化) +var ( + DefaultOpsTypeNameDataModification = &i18n.Message{ID: "DefaultOpsTypeNameDataModification", Other: "数据修改"} + DefaultOpsTypeNameDataExtraction = &i18n.Message{ID: "DefaultOpsTypeNameDataExtraction", Other: "数据提取"} + DefaultOpsTypeNameServiceMaintenance = &i18n.Message{ID: "DefaultOpsTypeNameServiceMaintenance", Other: "服务维护"} + DefaultOpsTypeNameConfigModification = &i18n.Message{ID: "DefaultOpsTypeNameConfigModification", Other: "配置修改"} + ErrOpsTypeReferencedByDataExportWorkflow = &i18n.Message{ID: "ErrOpsTypeReferencedByDataExportWorkflow", Other: "该运维类型已被数据导出工单引用,无法删除"} +) + // Data Export Workflow var ( DataExportWorkflowNameDuplicateErr = &i18n.Message{ID: "DataExportWorkflowNameDuplicateErr", Other: "工单名称重复了,请您修改工单名称后重新提交工单。"} diff --git a/pkg/dms-common/api/dms/v1/ops_type.go b/pkg/dms-common/api/dms/v1/ops_type.go new file mode 100644 index 000000000..3b0bfe123 --- /dev/null +++ b/pkg/dms-common/api/dms/v1/ops_type.go @@ -0,0 +1,25 @@ +package v1 + +import base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" + +// OpsType 项目级运维类型字典项(对标 EnvironmentTag,无色标)。 +// swagger:model OpsType +type OpsType struct { + UID string `json:"uid,omitempty"` + // 运维类型名称最多50个字符 + Name string `json:"name" validate:"max=50"` +} + +// ListOpsTypeReq 按项目分页列举运维类型(供 dmsobject / 跨服务调用)。 +type ListOpsTypeReq struct { + ProjectUID string `param:"project_uid" json:"project_uid" validate:"required"` + PageIndex uint32 `query:"page_index" json:"page_index"` + PageSize uint32 `query:"page_size" json:"page_size" validate:"required"` +} + +// ListOpsTypesReply 运维类型列表响应。 +type ListOpsTypesReply struct { + Data []*OpsType `json:"data"` + Total int64 `json:"total_nums"` + base.GenericResp +} diff --git a/pkg/dms-common/api/dms/v1/router.go b/pkg/dms-common/api/dms/v1/router.go index 8d9989d00..b5c9e77a5 100644 --- a/pkg/dms-common/api/dms/v1/router.go +++ b/pkg/dms-common/api/dms/v1/router.go @@ -16,6 +16,7 @@ var ( UserRouterGroup = "/dms/users" DBServiceRouterGroup = "/dms/projects/:project_uid/db_services" DBEnvironmentTagGroup = "/dms/projects/:project_uid/environment_tags" + OpsTypeRouterGroup = "/dms/projects/:project_uid/ops_types" ProxyRouterGroup = "/dms/proxys" PluginRouterGroup = "/dms/plugins" MemberRouterGroup = "/dms/projects/:project_uid/members" @@ -96,3 +97,7 @@ func GetDBConnectionAbleRouter() string { func GetGlobalDataExportWorkflowsRouter() string { return fmt.Sprintf("%s%s", CurrentGroupVersion, "/dms/dashboard/data_export_workflows") } + +func GetOpsTypesRouter(projectUid string) string { + return fmt.Sprintf("%s%s", CurrentGroupVersion, strings.Replace(OpsTypeRouterGroup, ":project_uid", projectUid, 1)) +} diff --git a/pkg/dms-common/dmsobject/ops_type.go b/pkg/dms-common/dmsobject/ops_type.go new file mode 100644 index 000000000..f7585b857 --- /dev/null +++ b/pkg/dms-common/dmsobject/ops_type.go @@ -0,0 +1,37 @@ +package dmsobject + +import ( + "context" + "fmt" + "net/url" + + dmsV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" + pkgHttp "github.com/actiontech/dms/pkg/dms-common/pkg/http" +) + +// ListOpsTypes 按项目批量读取运维类型字典(只读 list;供 SQLE 等调用方一次拉全量后内存映射)。 +func ListOpsTypes(ctx context.Context, dmsAddr string, req dmsV1.ListOpsTypeReq) ([]*dmsV1.OpsType, int64, error) { + header := map[string]string{ + "Authorization": pkgHttp.DefaultDMSToken, + } + + baseURL, err := url.Parse(fmt.Sprintf("%s%s", dmsAddr, dmsV1.GetOpsTypesRouter(req.ProjectUID))) + if err != nil { + return nil, 0, fmt.Errorf("failed to parse base URL: %v", err) + } + + query := url.Values{} + query.Set("page_size", fmt.Sprintf("%d", req.PageSize)) + query.Set("page_index", fmt.Sprintf("%d", req.PageIndex)) + baseURL.RawQuery = query.Encode() + + reply := &dmsV1.ListOpsTypesReply{} + if err := pkgHttp.Get(ctx, baseURL.String(), header, nil, reply); err != nil { + return nil, 0, fmt.Errorf("failed to list ops types from %v: %v", baseURL.String(), err) + } + if reply.Code != 0 { + return nil, 0, fmt.Errorf("http reply code(%v) error: %v", reply.Code, reply.Message) + } + + return reply.Data, reply.Total, nil +} From 948c6552a3f708e2f791bf4aff265b44262febb6 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Sat, 15 Aug 2026 04:47:40 +0800 Subject: [PATCH 2/2] feat: attach ops type to data export workflows Persist optional ops_type_uid on create, ignore updates, and expose name/filter on list/detail/dashboard paths. Co-authored-by: Cursor --- api/dms/service/v1/data_export_workflow.go | 38 ++++++--- internal/dms/biz/data_export_workflow.go | 25 +++++- internal/dms/service/cloudbeaver.go | 4 +- internal/dms/service/data_export_workflow.go | 83 +++++++++++++++++++ internal/dms/storage/convert.go | 2 + internal/dms/storage/workflow.go | 54 +++++++++++- .../service/sql_workbench_service.go | 4 +- pkg/dms-common/api/dms/v1/workflow.go | 3 + .../dmsobject/data_export_workflow.go | 4 + 9 files changed, 200 insertions(+), 17 deletions(-) diff --git a/api/dms/service/v1/data_export_workflow.go b/api/dms/service/v1/data_export_workflow.go index 8a0330145..d64e04c54 100644 --- a/api/dms/service/v1/data_export_workflow.go +++ b/api/dms/service/v1/data_export_workflow.go @@ -32,6 +32,10 @@ type DataExportWorkflow struct { // Required: false // example: 1 WorkflowTemplateId uint `json:"workflow_template_id"` + // ops type uid from project dictionary; omit or empty means unset; immutable after create + // Required: false + // example: 7002001 + OpsTypeUID string `json:"ops_type_uid"` } // swagger:model AddDataExportWorkflowReply @@ -83,9 +87,12 @@ type ListDataExportWorkflowsReq struct { // filter by workflow template id // in:query FilterWorkflowTemplateId uint `json:"filter_workflow_template_id" query:"filter_workflow_template_id"` + // filter by ops type uid(单选;空则不筛;不提供「未设置」) + // in:query + FilterByOpsTypeUid string `json:"filter_by_ops_type_uid" query:"filter_by_ops_type_uid"` } -// swagger:parameters FilterGlobalDataExportWorkflowReq +// swagger:parameters GetGlobalDataExportWorkflows type FilterGlobalDataExportWorkflowReq struct { // filter status list // in:query @@ -145,6 +152,9 @@ type FilterGlobalDataExportWorkflowReq struct { // filter update time to // in:query FilterUpdateTimeTo string `json:"filter_update_time_to" query:"filter_update_time_to"` + // filter by ops type uid(单选;空则不筛;不提供「未设置」;与项目内列表同构) + // in:query + FilterByOpsTypeUid string `json:"filter_by_ops_type_uid" query:"filter_by_ops_type_uid"` } // swagger:model GetGlobalDataExportWorkflowsReply @@ -163,6 +173,8 @@ type GlobalDataExportWorkflow struct { CreatedAt time.Time `json:"created_at"` // 数据导出工单的创建时间 UpdatedAt time.Time `json:"updated_at"` // 数据导出工单的更新时间 Status DataExportWorkflowStatus `json:"status"` // 数据导出工单的状态 + // OpsType 运维类型(按工单所属项目字典批量解析);未设置或字典项已删时省略 + OpsType *dmsCommonV1.OpsType `json:"ops_type,omitempty"` CurrentStepAssigneeUsers []UidWithName `json:"current_step_assignee_user_list"` // 工单待操作人 DBServiceInfos []*dmsCommonV1.DBServiceUidWithNameInfo `json:"db_service_info,omitempty"` // 所属数据源信息 @@ -189,6 +201,8 @@ type ListDataExportWorkflow struct { Status DataExportWorkflowStatus `json:"status"` // 数据导出工单的状态 WorkflowTemplateId uint `json:"workflow_template_id"` // 创建时关联的审批模板 ID(历史工单可为 0) WorkflowTemplateName string `json:"workflow_template_name"` // 创建时冗余保存的审批模板名称 + // OpsType 运维类型(项目字典批量解析);未设置或字典项已删时省略,供前端「-」约定 + OpsType *dmsCommonV1.OpsType `json:"ops_type,omitempty"` CurrentStepAssigneeUsers []UidWithName `json:"current_step_assignee_user_list"` // 工单待操作人 DBServiceInfos []*dmsCommonV1.DBServiceUidWithNameInfo `json:"db_service_info,omitempty"` // 所属数据源信息 @@ -237,15 +251,17 @@ const ( ) type GetDataExportWorkflow struct { - Name string `json:"workflow_name"` - WorkflowID string `json:"workflow_uid"` - Desc string `json:"desc,omitempty"` - CreateUser UidWithName `json:"create_user"` - CreateTime *time.Time `json:"create_time"` - WorkflowTemplateId uint `json:"workflow_template_id"` - WorkflowTemplateName string `json:"workflow_template_name"` - WorkflowRecord WorkflowRecord `json:"workflow_record"` - WorkflowRecordHistory []WorkflowRecord `json:"workflow_record_history"` + Name string `json:"workflow_name"` + WorkflowID string `json:"workflow_uid"` + Desc string `json:"desc,omitempty"` + CreateUser UidWithName `json:"create_user"` + CreateTime *time.Time `json:"create_time"` + WorkflowTemplateId uint `json:"workflow_template_id"` + WorkflowTemplateName string `json:"workflow_template_name"` + // OpsType 运维类型(项目字典解析);未设置或字典项已删时省略,供前端「-」约定 + OpsType *dmsCommonV1.OpsType `json:"ops_type,omitempty"` + WorkflowRecord WorkflowRecord `json:"workflow_record"` + WorkflowRecordHistory []WorkflowRecord `json:"workflow_record_history"` // UnmaskingWorkflow 关联的查看原文工单摘要;无关联时为 null UnmaskingWorkflow *DataExportRelatedUnmaskingWorkflow `json:"unmasking_workflow"` } @@ -299,7 +315,7 @@ type WorkflowRecord struct { CurrentStepNumber uint `json:"current_step_number,omitempty"` Status DataExportWorkflowStatus `json:"status"` // 导出失败摘要(人类可读);失败类状态时有值,成功/非导出失败为空 - ExportFailSummary string `json:"export_fail_summary,omitempty"` + ExportFailSummary string `json:"export_fail_summary,omitempty"` Steps []*WorkflowStep `json:"workflow_step_list,omitempty"` } diff --git a/internal/dms/biz/data_export_workflow.go b/internal/dms/biz/data_export_workflow.go index f00e8b62c..d3cc6bd7d 100644 --- a/internal/dms/biz/data_export_workflow.go +++ b/internal/dms/biz/data_export_workflow.go @@ -14,6 +14,9 @@ import ( var ErrDataExportWorkflowNameDuplicate = errors.New("data export workflow name duplicate") +// ErrOpsTypeNotBelongToProject 所选运维类型标识不属于本项目字典(或不存在)。 +var ErrOpsTypeNotBelongToProject = errors.New("所选运维类型不属于本项目字典") + type DataExportWorkflowStatus string const ( @@ -72,8 +75,10 @@ type Workflow struct { WorkflowRecordUid string WorkflowTemplateId uint WorkflowTemplateName string - Tasks []Task - TaskIds []string + // OpsTypeUID 运维类型字典项标识;空表示未设置;创建后不可改 + OpsTypeUID string + Tasks []Task + TaskIds []string WorkflowRecord *WorkflowRecord DBServiceInfos []*dmsCommonV1.DBServiceUidWithNameInfo // 所属数据源信息 @@ -118,6 +123,8 @@ type WorkflowRepo interface { UpdateWorkflowStatusById(ctx context.Context, dataExportWorkflowUid string, status DataExportWorkflowStatus) error // UpdateWorkflowExportStatusById 更新工单导出状态与失败摘要(进入 exporting 时 summary 传空以清空上一轮) UpdateWorkflowExportStatusById(ctx context.Context, dataExportWorkflowUid string, status DataExportWorkflowStatus, exportFailSummary string) error + // UpdateWorkflowColumns 更新 workflows 行字段;强制忽略 ops_type_uid(创建后不可改)。 + UpdateWorkflowColumns(ctx context.Context, workflowUID string, updates map[string]interface{}) error GetDataExportWorkflowsByIds(ctx context.Context, dataExportWorkflowUid []string) ([]*Workflow, error) CancelWorkflow(ctx context.Context, workflowRecordIds []string, workflowSteps []*WorkflowStep, operateId string) error AuditWorkflow(ctx context.Context, dataExportWorkflowUid string, status DataExportWorkflowStatus, step *WorkflowStep, operateId, reason string) error @@ -133,6 +140,8 @@ type WorkflowRepo interface { AuditWorkflowAndAdvanceStep(ctx context.Context, workflowRecordUid string, step *WorkflowStep, nextStepId uint64, operateId, reason string) error // CountDataExportWorkflowsByTemplateId 供 SQLE 跨库引用检查:模板软删不影响已建工单流转,仅统计本地冗余引用。 CountDataExportWorkflowsByTemplateId(ctx context.Context, projectUID string, templateID uint) (int64, error) + // CountDataExportWorkflowsByOpsTypeUID 供运维类型删除前引用校验:统计本项目数据导出工单对该类型标识的引用数。 + CountDataExportWorkflowsByOpsTypeUID(ctx context.Context, projectUID, opsTypeUID string) (int64, error) } type DataExportWorkflowUsecase struct { @@ -145,6 +154,7 @@ type DataExportWorkflowUsecase struct { dmsProxyTargetRepo ProxyTargetRepo opPermissionVerifyUsecase *OpPermissionVerifyUsecase projectUsecase *ProjectUsecase + opsTypeUsecase *OpsTypeUsecase clusterUsecase *ClusterUsecase webhookUsecase *WebHookConfigurationUsecase userUsecase *UserUsecase @@ -155,7 +165,7 @@ type DataExportWorkflowUsecase struct { reportHost string } -func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator, repo WorkflowRepo, dataExportTaskRepo DataExportTaskRepo, dbServiceRepo DBServiceRepo, maskingConfigRepo DataExportMaskingConfigRepo, maskingRuleRepo DataExportMaskingRuleRepo, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, projectUsecase *ProjectUsecase, proxyTargetRepo ProxyTargetRepo, clusterUseCase *ClusterUsecase, webhookUsecase *WebHookConfigurationUsecase, userUsecase *UserUsecase, systemVariableUsecase *SystemVariableUsecase, dbServiceUsecase *DBServiceUsecase, unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase, reportHost string) *DataExportWorkflowUsecase { +func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator, repo WorkflowRepo, dataExportTaskRepo DataExportTaskRepo, dbServiceRepo DBServiceRepo, maskingConfigRepo DataExportMaskingConfigRepo, maskingRuleRepo DataExportMaskingRuleRepo, opPermissionVerifyUsecase *OpPermissionVerifyUsecase, projectUsecase *ProjectUsecase, opsTypeUsecase *OpsTypeUsecase, proxyTargetRepo ProxyTargetRepo, clusterUseCase *ClusterUsecase, webhookUsecase *WebHookConfigurationUsecase, userUsecase *UserUsecase, systemVariableUsecase *SystemVariableUsecase, dbServiceUsecase *DBServiceUsecase, unmaskingWorkflowUsecase *dataMaskingBiz.UnmaskingWorkflowUsecase, reportHost string) *DataExportWorkflowUsecase { return &DataExportWorkflowUsecase{ tx: tx, repo: repo, @@ -164,6 +174,7 @@ func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator maskingRuleRepo: maskingRuleRepo, opPermissionVerifyUsecase: opPermissionVerifyUsecase, projectUsecase: projectUsecase, + opsTypeUsecase: opsTypeUsecase, dmsProxyTargetRepo: proxyTargetRepo, dataExportTaskRepo: dataExportTaskRepo, clusterUsecase: clusterUseCase, @@ -177,6 +188,14 @@ func NewDataExportWorkflowUsecase(logger utilLog.Logger, tx TransactionGenerator } } +// CountDataExportWorkflowsByOpsTypeUID 统计本项目数据导出工单对运维类型标识的引用数(删除字典前引用校验)。 +func (d *DataExportWorkflowUsecase) CountDataExportWorkflowsByOpsTypeUID(ctx context.Context, projectUID, opsTypeUID string) (int64, error) { + if opsTypeUID == "" { + return 0, nil + } + return d.repo.CountDataExportWorkflowsByOpsTypeUID(ctx, projectUID, opsTypeUID) +} + type ListWorkflowsOption struct { PageNumber uint32 LimitPerPage uint32 diff --git a/internal/dms/service/cloudbeaver.go b/internal/dms/service/cloudbeaver.go index 3978fb40d..42cc753b3 100644 --- a/internal/dms/service/cloudbeaver.go +++ b/internal/dms/service/cloudbeaver.go @@ -43,12 +43,14 @@ func NewAndInitCloudbeaverService(logger utilLog.Logger, opts *conf.DMSOptions) // 预定义解决usecase循环依赖问题 memberUsecase := &biz.MemberUsecase{} environmentTagUsecase := biz.EnvironmentTagUsecase{} + opsTypeUsecase := biz.OpsTypeUsecase{} businessTagUsecase := biz.NewBusinessTagUsecase(storage.NewBusinessTagRepo(logger, st), logger) dmsProxyTargetRepo := storage.NewProxyTargetRepo(logger, st) projectRepo := storage.NewProjectRepo(logger, st) - projectUsecase := biz.NewProjectUsecase(logger, tx, projectRepo, memberUsecase, opPermissionVerifyUsecase, pluginUseCase, businessTagUsecase, &environmentTagUsecase) + projectUsecase := biz.NewProjectUsecase(logger, tx, projectRepo, memberUsecase, opPermissionVerifyUsecase, pluginUseCase, businessTagUsecase, &environmentTagUsecase, &opsTypeUsecase) dbServiceRepo := storage.NewDBServiceRepo(logger, st) environmentTagUsecase = *biz.NewEnvironmentTagUsecase(storage.NewEnvironmentTagRepo(logger, st), logger, projectUsecase, opPermissionVerifyUsecase) + opsTypeUsecase = *biz.NewOpsTypeUsecase(storage.NewOpsTypeRepo(logger, st), logger, projectUsecase, opPermissionVerifyUsecase) discoveryTaskRepo := storage.NewSensitiveDataDiscoveryTaskRepo(logger, st) dbServiceUseCase := biz.NewDBServiceUsecase(logger, dbServiceRepo, discoveryTaskRepo, pluginUseCase, opPermissionVerifyUsecase, projectUsecase, dmsProxyTargetRepo, &environmentTagUsecase) diff --git a/internal/dms/service/data_export_workflow.go b/internal/dms/service/data_export_workflow.go index 9b00811be..689afcace 100644 --- a/internal/dms/service/data_export_workflow.go +++ b/internal/dms/service/data_export_workflow.go @@ -9,6 +9,7 @@ import ( "github.com/actiontech/dms/internal/dms/biz" pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" "github.com/actiontech/dms/internal/pkg/locale" + dmsCommonV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" ) func (d *DMSService) AddDataExportWorkflow(ctx context.Context, req *dmsV1.AddDataExportWorkflowReq, currentUserUid string) (reply *dmsV1.AddDataExportWorkflowReply, err error) { @@ -23,6 +24,7 @@ func (d *DMSService) AddDataExportWorkflow(ctx context.Context, req *dmsV1.AddDa Tasks: tasks, ProjectUID: req.ProjectUid, WorkflowTemplateId: req.DataExportWorkflow.WorkflowTemplateId, + OpsTypeUID: req.DataExportWorkflow.OpsTypeUID, } uid, err := d.DataExportWorkflowUsecase.AddDataExportWorkflow(ctx, currentUserUid, args) if err != nil { @@ -88,6 +90,14 @@ func (d *DMSService) ListDataExportWorkflow(ctx context.Context, req *dmsV1.List }) } + if req.FilterByOpsTypeUid != "" { + andConditions = append(andConditions, pkgConst.FilterCondition{ + Field: string(biz.WorkflowFieldOpsTypeUID), + Operator: pkgConst.FilterOperatorEqual, + Value: req.FilterByOpsTypeUid, + }) + } + if len(andConditions) > 0 { filterByOptions.Groups = append(filterByOptions.Groups, pkgConst.NewConditionGroup(pkgConst.FilterLogicAnd, andConditions...)) } @@ -140,6 +150,9 @@ func (d *DMSService) ListDataExportWorkflow(ctx context.Context, req *dmsV1.List } } + // 按项目批量解析运维类型名称(D8:禁止逐条请求字典) + opsTypeNameByProject := d.buildOpsTypeNameMapsByProjects(ctx, projectUIDs) + ret := make([]*dmsV1.ListDataExportWorkflow, len(workflows)) for i, w := range workflows { ret[i] = &dmsV1.ListDataExportWorkflow{ @@ -152,6 +165,7 @@ func (d *DMSService) ListDataExportWorkflow(ctx context.Context, req *dmsV1.List Status: dmsV1.DataExportWorkflowStatus(w.WorkflowRecord.Status), WorkflowTemplateId: w.WorkflowTemplateId, WorkflowTemplateName: w.WorkflowTemplateName, + OpsType: resolveOpsTypeFromNameMap(w.OpsTypeUID, opsTypeNameByProject[w.ProjectUID]), } creater := convertBizUidWithName(d.UserUsecase.GetBizUserWithNameByUids(ctx, []string{w.CreateUserUID})) if len(creater) > 0 { @@ -183,6 +197,9 @@ func (d *DMSService) GetGlobalWorkflowsList(ctx context.Context, req *dmsV1.Filt } } + // 跨项目按所属项目字典批量解析名称(D8:禁止逐条请求) + opsTypeNameByProject := d.buildOpsTypeNameMapsByProjects(ctx, projectUIDs) + ret := make([]*dmsV1.GlobalDataExportWorkflow, len(workflows)) for i, w := range workflows { ret[i] = &dmsV1.GlobalDataExportWorkflow{ @@ -194,6 +211,7 @@ func (d *DMSService) GetGlobalWorkflowsList(ctx context.Context, req *dmsV1.Filt UpdatedAt: w.WorkflowRecord.UpdateTime, Status: dmsV1.DataExportWorkflowStatus(w.WorkflowRecord.Status), DBServiceInfos: w.DBServiceInfos, + OpsType: resolveOpsTypeFromNameMap(w.OpsTypeUID, opsTypeNameByProject[w.ProjectUID]), } creater := convertBizUidWithName(d.UserUsecase.GetBizUserIncludeDeletedWithNameByUids(ctx, []string{w.CreateUserUID})) @@ -223,6 +241,7 @@ func (d *DMSService) GetDataExportWorkflow(ctx context.Context, req *dmsV1.GetDa CreateTime: &w.CreateTime, WorkflowTemplateId: w.WorkflowTemplateId, WorkflowTemplateName: w.WorkflowTemplateName, + OpsType: d.resolveDataExportWorkflowOpsType(ctx, w.ProjectUID, w.OpsTypeUID), WorkflowRecord: dmsV1.WorkflowRecord{ CurrentStepNumber: uint(w.WorkflowRecord.CurrentWorkflowStepId), Status: dmsV1.DataExportWorkflowStatus(w.WorkflowRecord.Status), @@ -256,6 +275,70 @@ func (d *DMSService) GetDataExportWorkflow(ctx context.Context, req *dmsV1.GetDa }, nil } +// resolveDataExportWorkflowOpsType 按项目字典解析运维类型展示名;未设置/已删/非本项目 → nil(前端「-」)。 +func (d *DMSService) resolveDataExportWorkflowOpsType(ctx context.Context, projectUID, opsTypeUID string) *dmsCommonV1.OpsType { + if opsTypeUID == "" || d.OpsTypeUsecase == nil { + return nil + } + opsType, err := d.OpsTypeUsecase.GetOpsTypeByUID(ctx, opsTypeUID) + if err != nil || opsType == nil { + return nil + } + if opsType.ProjectUID != "" && projectUID != "" && opsType.ProjectUID != projectUID { + return nil + } + return &dmsCommonV1.OpsType{ + UID: opsType.UID, + Name: localizeOpsTypeDisplayName(ctx, opsType.Name), + } +} + +const listOpsTypesBatchPageSize = 1000 + +// buildOpsTypeNameMapsByProjects 按项目一次拉取字典,构建 uid→name(列表批量回填,禁止逐条 Get)。 +func (d *DMSService) buildOpsTypeNameMapsByProjects(ctx context.Context, projectUIDs map[string]bool) map[string]map[string]string { + out := make(map[string]map[string]string, len(projectUIDs)) + if d.OpsTypeUsecase == nil { + return out + } + for projectUID := range projectUIDs { + if projectUID == "" { + continue + } + opsTypes, _, err := d.OpsTypeUsecase.ListOpsTypes(ctx, &biz.ListOpsTypesOption{ + ProjectUID: projectUID, + Limit: listOpsTypesBatchPageSize, + Offset: 0, + }) + if err != nil { + continue + } + nameByUID := make(map[string]string, len(opsTypes)) + for _, ot := range opsTypes { + if ot == nil || ot.UID == "" { + continue + } + nameByUID[ot.UID] = localizeOpsTypeDisplayName(ctx, ot.Name) + } + out[projectUID] = nameByUID + } + return out +} + +func resolveOpsTypeFromNameMap(opsTypeUID string, nameByUID map[string]string) *dmsCommonV1.OpsType { + if opsTypeUID == "" || nameByUID == nil { + return nil + } + name, ok := nameByUID[opsTypeUID] + if !ok { + return nil + } + return &dmsCommonV1.OpsType{ + UID: opsTypeUID, + Name: name, + } +} + func (d *DMSService) ExportDataExportWorkflow(ctx context.Context, req *dmsV1.ExportDataExportWorkflowReq, currentUserUid string) error { return d.DataExportWorkflowUsecase.ExportDataExportWorkflow(ctx, req.ProjectUid, req.DataExportWorkflowUid, currentUserUid) diff --git a/internal/dms/storage/convert.go b/internal/dms/storage/convert.go index 70c1f5355..6ace5ecee 100644 --- a/internal/dms/storage/convert.go +++ b/internal/dms/storage/convert.go @@ -1051,6 +1051,7 @@ func convertBizWorkflow(b *biz.Workflow) *model.Workflow { WorkflowRecordUid: b.WorkflowRecordUid, WorkflowTemplateId: b.WorkflowTemplateId, WorkflowTemplateName: b.WorkflowTemplateName, + OpsTypeUID: b.OpsTypeUID, } if b.WorkflowRecord != nil { workflow.WorkflowRecord = convertBizWorkflowRecord(b.WorkflowRecord) @@ -1072,6 +1073,7 @@ func convertModelWorkflow(m *model.Workflow) (w *biz.Workflow, err error) { WorkflowRecordUid: m.WorkflowRecordUid, WorkflowTemplateId: m.WorkflowTemplateId, WorkflowTemplateName: m.WorkflowTemplateName, + OpsTypeUID: m.OpsTypeUID, TaskIds: m.GetTaskIds(), } if m.WorkflowRecord != nil { diff --git a/internal/dms/storage/workflow.go b/internal/dms/storage/workflow.go index b91dcd1f8..6ed2d537a 100644 --- a/internal/dms/storage/workflow.go +++ b/internal/dms/storage/workflow.go @@ -44,6 +44,29 @@ func (d *WorkflowRepo) SaveWorkflow(ctx context.Context, dataExportWorkflow *biz return nil } +// UpdateWorkflowColumns 更新 workflows 行字段;强制忽略 ops_type_uid(创建后不可改)。 +func (d *WorkflowRepo) UpdateWorkflowColumns(ctx context.Context, workflowUID string, updates map[string]interface{}) error { + if updates == nil { + return nil + } + safe := make(map[string]interface{}, len(updates)) + for k, v := range updates { + if k == "ops_type_uid" { + continue + } + safe[k] = v + } + if len(safe) == 0 { + return nil + } + return transaction(d.log, ctx, d.db, func(tx *gorm.DB) error { + if err := tx.WithContext(ctx).Model(&model.Workflow{}).Where("uid = ?", workflowUID).Updates(safe).Error; err != nil { + return fmt.Errorf("failed to update workflow columns: %v", err) + } + return nil + }) +} + func (d *WorkflowRepo) IsDataExportWorkflowNameDuplicate(ctx context.Context, projectUID, workflowName string) (bool, error) { var count int64 if err := transaction(d.log, ctx, d.db, func(tx *gorm.DB) error { @@ -400,6 +423,28 @@ func (d *WorkflowRepo) CountDataExportWorkflowsByTemplateId(ctx context.Context, return count, nil } +// CountDataExportWorkflowsByOpsTypeUID 统计本项目数据导出工单对指定运维类型的引用数(含任意状态;空串不计)。 +func (d *WorkflowRepo) CountDataExportWorkflowsByOpsTypeUID(ctx context.Context, projectUID, opsTypeUID string) (int64, error) { + if opsTypeUID == "" { + return 0, nil + } + var count int64 + if err := transaction(d.log, ctx, d.db, func(tx *gorm.DB) error { + db := tx.WithContext(ctx).Model(&model.Workflow{}). + Where("workflow_type = ? AND ops_type_uid = ?", biz.DataExportWorkflowEventType.String(), opsTypeUID) + if projectUID != "" { + db = db.Where("project_uid = ?", projectUID) + } + if err := db.Count(&count).Error; err != nil { + return fmt.Errorf("failed to count workflows by ops type uid: %v", err) + } + return nil + }); err != nil { + return 0, err + } + return count, nil +} + func (d *WorkflowRepo) DeleteDataExportWorkflowsByIds(ctx context.Context, dataExportWorkflowUids []string) error { if len(dataExportWorkflowUids) == 0 { return nil @@ -452,7 +497,8 @@ SELECT wr.uid AS workflow_record_uid, t.db_service_uid AS db_service_uid, ds.name AS db_service_name, - wr.updated_at AS update_time + wr.updated_at AS update_time, + w.ops_type_uid AS ops_type_uid {{- template "body" . -}} ORDER BY wr.updated_at DESC @@ -546,6 +592,10 @@ AND wr.updated_at >= :filter_update_time_from AND wr.updated_at <= :filter_update_time_to {{- end }} +{{- if .filter_by_ops_type_uid }} +AND w.ops_type_uid = :filter_by_ops_type_uid +{{- end }} + {{ end }} ` @@ -569,6 +619,7 @@ func (d *WorkflowRepo) GetGlobalWorkflowsByParameterMap(ctx context.Context, dat WorkflowRecordUID string `gorm:"column:workflow_record_uid"` DBServiceUID string `gorm:"column:db_service_uid"` DBServiceName string `gorm:"column:db_service_name"` + OpsTypeUID string `gorm:"column:ops_type_uid"` } var results []workflowQueryResult @@ -616,6 +667,7 @@ func (d *WorkflowRepo) GetGlobalWorkflowsByParameterMap(ctx context.Context, dat CreateTime: result.CreateTime, CreateUserUID: result.CreateUserUID, Status: result.Status, + OpsTypeUID: result.OpsTypeUID, ProjectInfo: &dmsCommonV1.ProjectInfo{ ProjectUid: result.ProjectUID, ProjectName: result.ProjectName, diff --git a/internal/sql_workbench/service/sql_workbench_service.go b/internal/sql_workbench/service/sql_workbench_service.go index 504835468..c8285efb8 100644 --- a/internal/sql_workbench/service/sql_workbench_service.go +++ b/internal/sql_workbench/service/sql_workbench_service.go @@ -175,13 +175,15 @@ func NewAndInitSqlWorkbenchService(logger utilLog.Logger, opts *conf.DMSOptions) // 初始化项目相关 memberUsecase := &biz.MemberUsecase{} environmentTagUsecase := biz.EnvironmentTagUsecase{} + opsTypeUsecase := biz.OpsTypeUsecase{} businessTagUsecase := biz.NewBusinessTagUsecase(storage.NewBusinessTagRepo(logger, st), logger) projectRepo := storage.NewProjectRepo(logger, st) - projectUsecase := biz.NewProjectUsecase(logger, tx, projectRepo, memberUsecase, opPermissionVerifyUsecase, pluginUsecase, businessTagUsecase, &environmentTagUsecase) + projectUsecase := biz.NewProjectUsecase(logger, tx, projectRepo, memberUsecase, opPermissionVerifyUsecase, pluginUsecase, businessTagUsecase, &environmentTagUsecase, &opsTypeUsecase) // 初始化数据源相关 dbServiceRepo := storage.NewDBServiceRepo(logger, st) environmentTagUsecase = *biz.NewEnvironmentTagUsecase(storage.NewEnvironmentTagRepo(logger, st), logger, projectUsecase, opPermissionVerifyUsecase) + opsTypeUsecase = *biz.NewOpsTypeUsecase(storage.NewOpsTypeRepo(logger, st), logger, projectUsecase, opPermissionVerifyUsecase) proxyTargetRepo := storage.NewProxyTargetRepo(logger, st) discoveryTaskRepo := storage.NewSensitiveDataDiscoveryTaskRepo(logger, st) dbServiceUsecase := biz.NewDBServiceUsecase(logger, dbServiceRepo, discoveryTaskRepo, pluginUsecase, opPermissionVerifyUsecase, projectUsecase, proxyTargetRepo, &environmentTagUsecase) diff --git a/pkg/dms-common/api/dms/v1/workflow.go b/pkg/dms-common/api/dms/v1/workflow.go index dd20172f1..0115ce236 100644 --- a/pkg/dms-common/api/dms/v1/workflow.go +++ b/pkg/dms-common/api/dms/v1/workflow.go @@ -23,6 +23,8 @@ type FilterGlobalDataExportWorkflowReq struct { FilterCreateTimeTo string `json:"filter_create_time_to" query:"filter_create_time_to"` FilterUpdateTimeFrom string `json:"filter_update_time_from" query:"filter_update_time_from"` FilterUpdateTimeTo string `json:"filter_update_time_to" query:"filter_update_time_to"` + // FilterByOpsTypeUid 按运维类型单选筛选(空则不筛;不提供「未设置」) + FilterByOpsTypeUid string `json:"filter_by_ops_type_uid" query:"filter_by_ops_type_uid"` // CheckUserCanAccess enables OR-based self-relevant filtering: // (creator OR current assignee OR viewable db_service). When true, @@ -53,6 +55,7 @@ type ListDataExportWorkflow struct { CreatedAt time.Time `json:"created_at"` // 数据导出工单的创建时间 UpdatedAt time.Time `json:"updated_at"` // 数据导出工单的更新时间 Status DataExportWorkflowStatus `json:"status"` // 数据导出工单的状态 + OpsType *OpsType `json:"ops_type,omitempty"` // 运维类型;未设置或字典已删时省略 CurrentStepAssigneeUsers []UidWithName `json:"current_step_assignee_user_list"` // 工单待操作人 DBServiceInfos []*DBServiceUidWithNameInfo `json:"db_service_info,omitempty"` // 所属数据源信息 ProjectInfo *ProjectInfo `json:"project_info,omitempty"` // 所属项目信息 diff --git a/pkg/dms-common/dmsobject/data_export_workflow.go b/pkg/dms-common/dmsobject/data_export_workflow.go index c3e32ab6e..8da141887 100644 --- a/pkg/dms-common/dmsobject/data_export_workflow.go +++ b/pkg/dms-common/dmsobject/data_export_workflow.go @@ -94,6 +94,10 @@ func GetGlobalDataExportWorkflowsList(ctx context.Context, dmsAddr string, req d query.Set("filter_update_time_to", req.FilterUpdateTimeTo) } + if req.FilterByOpsTypeUid != "" { + query.Set("filter_by_ops_type_uid", req.FilterByOpsTypeUid) + } + // 将查询参数附加到 URL baseURL.RawQuery = query.Encode()