diff --git a/common/static/style.css b/common/static/style.css
index 13a36f1c5e..e587b26f3c 100644
--- a/common/static/style.css
+++ b/common/static/style.css
@@ -251,3 +251,14 @@ table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
+
+div.select-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.5); /* 半透明白色 */
+ z-index: 10; /* 确保它位于select元素之上 */
+ cursor: not-allowed; /* 显示“禁止”光标 */
+}
diff --git a/common/templates/config.html b/common/templates/config.html
index 93af8eeb1f..20189f03f4 100755
--- a/common/templates/config.html
+++ b/common/templates/config.html
@@ -183,6 +183,43 @@
SQL上线
placeholder="自动驳回的等级,1表示警告驳回,2和空表示错误才驳回,其他表示不驳回" />
+
+
+
diff --git a/sql/models.py b/sql/models.py
index 405692459d..c964d9724a 100755
--- a/sql/models.py
+++ b/sql/models.py
@@ -938,9 +938,12 @@ class Meta:
("menu_document", "菜单 相关文档"),
("menu_openapi", "菜单 OpenAPI"),
("sql_submit", "提交SQL上线工单"),
+ ("sql_submitbatch", "提交批量SQL上线工单"),
("sql_review", "审核SQL上线工单"),
+ ("sql_reviewbatch", "审核批量SQL上线工单"),
("sql_execute_for_resource_group", "执行SQL上线工单(资源组粒度)"),
("sql_execute", "执行SQL上线工单(仅自己提交的)"),
+ ("sql_executebatch", "执行批量SQL上线工单"),
("sql_analyze", "执行SQL分析"),
("optimize_sqladvisor", "执行SQLAdvisor"),
("optimize_sqltuning", "执行SQLTuning"),
diff --git a/sql/resource_group.py b/sql/resource_group.py
index 6d9fa0226e..04ddd8cffb 100644
--- a/sql/resource_group.py
+++ b/sql/resource_group.py
@@ -148,7 +148,7 @@ def instances(request):
ins = (
ins.filter(**filter_dict)
.order_by(Convert("instance_name", "gbk").asc())
- .values("id", "type", "db_type", "instance_name")
+ .values("id", "type", "db_type", "instance_name", "db_name")
)
rows = [row for row in ins]
result = {"status": 0, "msg": "ok", "data": rows}
diff --git a/sql/sql_workflow.py b/sql/sql_workflow.py
index b812be7c51..d2c4330581 100644
--- a/sql/sql_workflow.py
+++ b/sql/sql_workflow.py
@@ -30,7 +30,7 @@
)
from sql.utils.tasks import add_sql_schedule, del_schedule
from sql.utils.workflow_audit import Audit, get_auditor, AuditException
-from .models import SqlWorkflow
+from .models import SqlWorkflow, Instance, ResourceGroup
logger = logging.getLogger("default")
@@ -229,14 +229,16 @@ def alter_run_date(request):
@permission_required("sql.sql_review", raise_exception=True)
-def passed(request):
+def passed(request, workflow_id=None, audit_remark=None):
"""
审核通过,不执行
:param request:
:return:
"""
- workflow_id = int(request.POST.get("workflow_id", 0))
- audit_remark = request.POST.get("audit_remark", "")
+ if workflow_id is None:
+ workflow_id = int(request.POST.get("workflow_id", 0))
+ audit_remark = request.POST.get("audit_remark", "")
+
if workflow_id == 0:
context = {"errMsg": "workflow_id参数为空."}
return render(request, "error.html", context)
@@ -280,19 +282,22 @@ def passed(request):
return HttpResponseRedirect(reverse("sql:detail", args=(workflow_id,)))
-def execute(request):
+def execute(request, workflow_id=None, mode=None):
"""
执行SQL
:param request:
:return:
"""
+ if workflow_id is None:
+ workflow_id = int(request.POST.get("workflow_id", 0))
+ mode = request.POST.get("mode")
+
# 校验多个权限
if not (
request.user.has_perm("sql.sql_execute")
or request.user.has_perm("sql.sql_execute_for_resource_group")
):
raise PermissionDenied
- workflow_id = int(request.POST.get("workflow_id", 0))
if workflow_id == 0:
context = {"errMsg": "workflow_id参数为空."}
return render(request, "error.html", context)
@@ -310,8 +315,6 @@ def execute(request):
audit_id = Audit.detail_by_workflow_id(
workflow_id=workflow_id, workflow_type=WorkflowType.SQL_REVIEW
).audit_id
- # 根据执行模式进行对应修改
- mode = request.POST.get("mode")
# 交由系统执行
if mode == "auto":
# 修改工单状态为排队中
@@ -432,18 +435,20 @@ def timing_task(request):
return HttpResponseRedirect(reverse("sql:detail", args=(workflow_id,)))
-def cancel(request):
+def cancel(request, workflow_id=None, audit_remark=None):
"""
终止流程
:param request:
:return:
"""
- workflow_id = int(request.POST.get("workflow_id", 0))
+ if workflow_id is None:
+ workflow_id = int(request.POST.get("workflow_id", 0))
+ audit_remark = request.POST.get("cancel_remark")
+
if workflow_id == 0:
context = {"errMsg": "workflow_id参数为空."}
return render(request, "error.html", context)
sql_workflow = SqlWorkflow.objects.get(id=workflow_id)
- audit_remark = request.POST.get("cancel_remark")
if audit_remark is None:
context = {"errMsg": "终止原因不能为空"}
return render(request, "error.html", context)
@@ -525,3 +530,138 @@ def osc_control(request):
json.dumps(result, cls=ExtendJSONEncoder, bigint_as_string=True),
content_type="application/json",
)
+
+
+@permission_required("sql.sql_reviewbatch", raise_exception=True)
+def passedbatch(request):
+ """
+ 批量审核通过,不执行
+ :param request:
+ :return:
+ """
+ workflow_id_list = request.POST["workflowid_array"]
+ workflow_id_list = json.loads(workflow_id_list)
+ sys_config = SysConfig()
+ audit_remark = sys_config.get("batch_passed_remark")
+
+ for workflow_id in workflow_id_list:
+ passed(request, workflow_id, audit_remark)
+
+ return HttpResponseRedirect("/sqlworkflow")
+
+
+@permission_required("sql.sql_executebatch", raise_exception=True)
+def executebatch(request):
+ """
+ 执行SQL
+ :param request:
+ :return:
+ """
+ workflow_id_list = request.POST["workflowid_array"]
+ workflow_id_list = json.loads(workflow_id_list)
+ mode = "auto"
+
+ for workflow_id in workflow_id_list:
+ execute(request, workflow_id, mode)
+ return HttpResponseRedirect("/sqlworkflow")
+
+
+@permission_required("sql.sql_reviewbatch", raise_exception=True)
+def cancelbatch(request):
+ """
+ 终止流程
+ :param request:
+ :return:
+ """
+ workflow_id_list = request.POST["workflowid_array"]
+ workflow_id_list = json.loads(workflow_id_list)
+ sys_config = SysConfig()
+ audit_remark = sys_config.get("batch_cancel_remark")
+
+ for workflow_id in workflow_id_list:
+ cancel(request, workflow_id, audit_remark)
+
+ return HttpResponseRedirect("/sqlworkflow")
+
+
+@permission_required("sql.sql_submitbatch", raise_exception=True)
+def checkbatch(request):
+ """SQL检测按钮, 此处没有产生工单"""
+ sql_content = request.POST.get("sql_content")
+ instance_names = request.POST.get("instance_name")
+ instance_names = json.loads(instance_names)
+ instances = []
+ for instance_name in instance_names:
+ instance = Instance.objects.get(instance_name=instance_name)
+ instances.append(instance)
+
+ result = {"status": 0, "msg": "ok", "data": {}}
+ # 服务器端参数验证
+ if sql_content is None or len(instance_names) == 0:
+ result["status"] = 1
+ result["msg"] = "页面提交参数可能为空"
+ return HttpResponse(json.dumps(result), content_type="application/json")
+ warning_count_totle = 0
+ error_count_totle = 0
+ # 交给engine进行检测
+ check_result_arr = []
+ for instance in instances:
+ try:
+ check_engine = get_engine(instance=instance)
+ check_result = check_engine.execute_check(
+ db_name=instance.db_name, sql=sql_content.strip()
+ )
+ for i in range(len(check_result.to_dict())):
+ check_result.to_dict()[i]["instance"] = instance.instance_name
+ warning_count_totle = warning_count_totle + check_result.warning_count
+ error_count_totle = error_count_totle + check_result.error_count
+ check_result_arr = check_result_arr + check_result.to_dict()
+ except Exception as e:
+ result["status"] = 1
+ result["msg"] = str(e)
+ return HttpResponse(json.dumps(result), content_type="application/json")
+
+ # 处理检测结果
+ result["data"]["rows"] = check_result_arr
+ result["data"]["CheckWarningCount"] = warning_count_totle
+ result["data"]["CheckErrorCount"] = error_count_totle
+ return HttpResponse(json.dumps(result), content_type="application/json")
+
+
+@permission_required("sql.sql_submitbatch", raise_exception=True)
+def rollbackbatch(request):
+ """
+ 回滚流程
+ :param request:
+ :return:
+ """
+ workflow_id_list = request.POST["workflowid_array"]
+ workflow_id_list = json.loads(workflow_id_list)
+ workflow_list = list()
+ for workflow_id in workflow_id_list:
+ workflow = SqlWorkflow.objects.get(id=int(workflow_id))
+ rollback_workflow_name = (
+ f"【回滚工单】原工单Id:{workflow_id} ,{workflow.workflow_name}"
+ )
+ query_engine = get_engine(instance=workflow.instance)
+ list_backup_sql = query_engine.get_rollback(workflow=workflow)
+ ## 获取完整的回滚SQL
+ sql_content = "\n".join(item[1] for item in list_backup_sql)
+ param = {
+ "workflow": {
+ "workflow_name": rollback_workflow_name,
+ "daemon_url": "",
+ "group_id": ResourceGroup.objects.get(
+ group_name=workflow.group_name
+ ).group_id,
+ "instance": workflow.instance.id,
+ "db_name": workflow.db_name,
+ "is_backup": True,
+ "run_date_start": None,
+ "run_date_end": None,
+ },
+ "sql_content": sql_content,
+ }
+ workflow_list.append(param)
+ result = {"status": 0, "msg": "ok", "data": workflow_list}
+ return HttpResponse(json.dumps(result), content_type="application/json")
diff --git a/sql/templates/sqlsubmitbatch.html b/sql/templates/sqlsubmitbatch.html
new file mode 100644
index 0000000000..92bd3072c9
--- /dev/null
+++ b/sql/templates/sqlsubmitbatch.html
@@ -0,0 +1,665 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
+
+
+
+
提交的SQL经检测仍存在个警告信息和个错误信息,请按照平台规范仔细检查!
+
+
+
+
+
+{% endblock content %}
+{% block js %}
+ {% load static %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/sql/templates/sqlworkflow.html b/sql/templates/sqlworkflow.html
index 09b8e8a758..381621d93d 100644
--- a/sql/templates/sqlworkflow.html
+++ b/sql/templates/sqlworkflow.html
@@ -37,12 +37,33 @@
-
+ {% if enable_batch_workflow %}
+
+ {% endif %}
@@ -185,6 +206,10 @@
工单日志
}
},
columns: [{
+ title: '',
+ field: 'checkbox',
+ checkbox: true
+ },{
title: '工单名称',
field: 'workflow_name',
formatter: function (value, row, index) {
@@ -268,6 +293,196 @@ 工单日志
}
+ // 选择批量处理操作
+ function get_batch_oper() {
+ var batch_oper_value = $("#batchOper").val()
+ switch (batch_oper_value) {
+ case "batchSubmit":
+ apply_batch_submit();
+ break;
+ case "batchRollback":
+ apply_batch_rollback();
+ break;
+ case "batchPass":
+ apply_batch_pass();
+ break;
+ case "batchExecute":
+ apply_batch_execute();
+ break;
+ case "batchCancel":
+ apply_batch_cancel();
+ break;
+ }
+ };
+ // 提交批量工单
+ function apply_batch_submit() {
+ var selectElement = document.getElementById('batchOper');
+ selectElement.selectedIndex = 0; // 设定为第一个选项
+ window.location.href = '/submitsqlbatch/';
+ };
+ // 提交批量回滚
+ function apply_batch_rollback() {
+ //获取form对象,判断输入,通过则提交
+ var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections');
+ if(AllSelections.length == 0){
+ alert("请勾选需要提交回滚的工单!!!");
+ var selectElement = document.getElementById('batchOper');
+ selectElement.selectedIndex = 0; // 设定为第一个选项
+ } else {
+ var AllSelectionIds = [];
+ for (var i = 0; i < AllSelections.length; i++) {
+ AllSelectionIds.push(AllSelections[i]['id'])
+ }
+ $("#select-overlay").show();
+ alert("请求已发送后台,请稍等...");
+ $.ajax({
+ url:"/rollbackbatch/",
+ type:"POST",
+ data:{
+ 'workflowid_array': JSON.stringify(AllSelectionIds)
+ },
+ success:function(data) {
+ if(data.status == 1){
+ alert("勾选了包含非【已正常结束】状态的工单,请核查重新提交");
+ location.reload();
+ }else {
+ // console.log(data.data);
+ sqlSubmit(data.data)
+ }
+ }
+ })
+ }
+ };
+ // 提交批量审核
+ function apply_batch_pass() {
+ //获取form对象,判断输入,通过则提交
+ // $("#audit_remark").val($("#remark").val());
+ var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections');
+ if(AllSelections.length == 0){
+ alert("请勾选需要批量审核的工单!!!");
+ var selectElement = document.getElementById('batchOper');
+ selectElement.selectedIndex = 0; // 设定为第一个选项
+ } else {
+ var AllSelectionIds = [];
+ for (var i = 0; i < AllSelections.length; i++) {
+ AllSelectionIds.push(AllSelections[i]['id'])
+ }
+ $("#select-overlay").show();
+ alert("请求已发送后台,请稍等");
+ $.ajax({
+ url:"/passedbatch/",
+ type:"POST",
+ data:{
+ 'workflowid_array': JSON.stringify(AllSelectionIds)
+ },
+ success:function(data) {
+ if(data.status == 1){
+ alert("勾选了包含非【等待审核人审核】状态的工单,请核查重新提交");
+ location.reload();
+ }else {
+ location.reload();
+ }
+ }
+ })
+ }
+ };
+ // 提交批量执行
+ function apply_batch_execute () {
+ var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections');
+ if(AllSelections.length == 0){
+ alert("请勾选需要批量执行的工单!!!");
+ var selectElement = document.getElementById('batchOper');
+ selectElement.selectedIndex = 0; // 设定为第一个选项
+ } else {
+ var AllSelectionIds = [];
+ for (var i = 0; i < AllSelections.length; i++) {
+ AllSelectionIds.push(AllSelections[i]['id'])
+ }
+ var isContinue = confirm("请确认是否立即执行?");
+ $("#select-overlay").show();
+ alert("请求已发送后台,请稍等...");
+ if (isContinue) {
+ $.ajax({
+ url:"/executebatch/",
+ type:"POST",
+ data:{
+ 'workflowid_array': JSON.stringify(AllSelectionIds)
+ },
+ success:function(data) {
+ if(data.status == 1){
+ alert("勾选了包含非【审核通过】状态的工单,请核查重新提交");
+ location.reload();
+ }else {
+ location.reload();
+ }
+ }
+ })
+ } else {
+ location.reload();
+ }
+ }
+ };
+ // 提交批量终止
+ function apply_batch_cancel() {
+ var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections');
+ //获取form对象,判断输入,通过则提交
+ if(AllSelections.length == 0){
+ alert("请勾选需要终止的工单!!!");
+ var selectElement = document.getElementById('batchOper');
+ selectElement.selectedIndex = 0; // 设定为第一个选项
+ } else {
+ var AllSelectionIds = [];
+ for (var i = 0; i < AllSelections.length; i++) {
+ AllSelectionIds.push(AllSelections[i]['id'])
+ }
+ $("#select-overlay").show();
+ alert("请求已发送后台,请稍等...");
+ $.ajax({
+ url:"/cancelbatch/",
+ type:"POST",
+ data:{
+ 'workflowid_array': JSON.stringify(AllSelectionIds)
+ },
+ success:function(data) {
+ if(data.status == 1){
+ alert("勾选了包含非【审核通过】或【等待审核人审核】状态的工单,请核查重新提交");
+ location.reload();
+ }else {
+ location.reload();
+ }
+ }
+ })
+ }
+ };
+
+ function sqlSubmit(workflow_array) {
+ workflow_array.forEach(function(value) {
+ console.log(typeof(value))
+ console.log(value)
+ $.ajax({
+ type: "post",
+ url: "/api/v1/workflow/",
+ dataType: "json",
+ contentType: 'application/json;',
+ data: JSON.stringify(value),
+ beforeSend: function (xhr) {
+ },
+ complete: function () {
+ },
+ success: function (data) {
+ },
+ error: function (XMLHttpRequest, textStatus, errorThrown) {
+ if (XMLHttpRequest.responseJSON) {
+ alert(XMLHttpRequest.responseText)
+ } else {
+ alert(errorThrown);
+ }
+ }
+ });
+ })
+ location.reload();
+ }
+
// 获取操作日志
function getLog(obj) {
var workflow_id = $(obj).attr("workflow_id");
@@ -332,6 +547,10 @@ 工单日志
})
}
+ $("#batchOper").change(function () {
+ get_batch_oper();
+ });
+
//筛选变动自动刷新
$("#navStatus").change(function () {
get_workflow_list();
diff --git a/sql/urls.py b/sql/urls.py
index e2511c79e9..f75fb125d5 100644
--- a/sql/urls.py
+++ b/sql/urls.py
@@ -162,4 +162,10 @@
path("audit/input/", audit_log.audit_input),
path("user/list/", user.lists),
path("user/qrcode//", totp.generate_qrcode),
+ path("submitsqlbatch/", views.submit_sql_batch),
+ path("checkbatch/", sql_workflow.checkbatch),
+ path("passedbatch/", sql_workflow.passedbatch),
+ path("executebatch/", sql_workflow.executebatch),
+ path("cancelbatch/", sql_workflow.cancelbatch),
+ path("rollbackbatch/", sql_workflow.rollbackbatch),
]
diff --git a/sql/views.py b/sql/views.py
index 79011defc2..229e470aa4 100644
--- a/sql/views.py
+++ b/sql/views.py
@@ -147,15 +147,16 @@ def sqlworkflow(request):
)
resource_group = ResourceGroup.objects.filter(group_id__in=resource_group_id)
- return render(
- request,
- "sqlworkflow.html",
- {
- "status_list": SQL_WORKFLOW_CHOICES,
- "instance": instance,
- "resource_group": resource_group,
- },
- )
+ # 获取系统配置
+ archer_config = SysConfig()
+
+ context = {
+ "status_list": SQL_WORKFLOW_CHOICES,
+ "instance": instance,
+ "resource_group": resource_group,
+ "enable_batch_workflow": archer_config.get("enable_batch_workflow"),
+ }
+ return render(request, "sqlworkflow.html", context)
@permission_required("sql.sql_submit", raise_exception=True)
@@ -181,6 +182,32 @@ def submit_sql(request):
return render(request, "sqlsubmit.html", context)
+@permission_required("sql.sql_submitbatch", raise_exception=True)
+def submit_sql_batch(request):
+ """提交SQL的页面"""
+ user = request.user
+ # 获取组信息
+ group_list = user_groups(user)
+
+ # 获取所有有效用户,通知对象
+ active_user = Users.objects.filter(is_active=1)
+
+ # 获取系统配置
+ archer_config = SysConfig()
+
+ # 主动创建标签
+ InstanceTag.objects.get_or_create(
+ tag_code="can_write", defaults={"tag_name": "支持上线", "active": True}
+ )
+
+ context = {
+ "group_list": group_list,
+ "enable_backup_switch": archer_config.get("enable_backup_switch"),
+ "engines": engine_map,
+ }
+ return render(request, "sqlsubmitbatch.html", context)
+
+
def detail(request, workflow_id):
"""展示SQL工单详细页面"""
workflow_detail = get_object_or_404(SqlWorkflow, pk=workflow_id)