diff --git a/doc/XXL-JOB官方文档.md b/doc/XXL-JOB官方文档.md index f05dc8d6..984239f2 100644 --- a/doc/XXL-JOB官方文档.md +++ b/doc/XXL-JOB官方文档.md @@ -1469,7 +1469,7 @@ Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段 - 1、[规划中] 移除quartz:精简底层实现,优化已知问题; - 触发:单节点周期性触发,运行事件如delayqueue; - 调度:集群竞争,负载方式协同处理,竞争-加入时间轮-释放-竞争; -- 2、[规划中] 用户管理:支持在线维护系统用户; +- 2、用户管理:支持在线维护系统用户; - 3、[规划中] 权限管理:执行器为粒度分配权限,核心操作校验权限,暂定管理员、普通用户两种角色; - 4、调度日志优化:支持设置日志保留天数,过期日志天维度记录报表,并清理;调度报表汇总实时数据和报表; - 5、调度线程池参数调优; diff --git a/doc/db/tables_xxl_job.sql b/doc/db/tables_xxl_job.sql index 8b57760b..6eb0bfcc 100644 --- a/doc/db/tables_xxl_job.sql +++ b/doc/db/tables_xxl_job.sql @@ -224,10 +224,20 @@ CREATE TABLE `XXL_JOB_QRTZ_TRIGGER_GROUP` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `XXL_JOB_QRTZ_USER` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL COMMENT '账号', + `password` varchar(50) NOT NULL COMMENT '密码', + `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员', + `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割', + PRIMARY KEY (`id`), + UNIQUE KEY `i_username` (`username`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `XXL_JOB_QRTZ_TRIGGER_GROUP`(`id`, `app_name`, `title`, `order`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 1, 0, NULL); INSERT INTO `XXL_JOB_QRTZ_TRIGGER_INFO`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', ''); +INSERT INTO `XXL_JOB_QRTZ_USER`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', '49ba59abbe56e057', 1, NULL); commit; diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java new file mode 100644 index 00000000..8db2f41a --- /dev/null +++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java @@ -0,0 +1,124 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author xuxueli 2019-05-04 16:39:50 + */ +@Controller +@RequestMapping("/user") +public class UserController { + + @Resource + private XxlJobUserDao xxlJobUserDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + + @RequestMapping + public String index(Model model) { + + // 执行器列表 + List groupList = xxlJobGroupDao.findAll(); + model.addAttribute("groupList", groupList); + + return "user/user.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String username) { + + // page list + List list = xxlJobUserDao.pageList(start, length, username); + int list_count = xxlJobUserDao.pageListCount(start, length, username); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/add") + @ResponseBody + public ReturnT add(XxlJobUser xxlJobUser) { + + // valid username + if (!StringUtils.hasText(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") ); + } + xxlJobUser.setUsername(xxlJobUser.getUsername().trim()); + if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // valid password + if (!StringUtils.hasText(xxlJobUser.getPassword())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") ); + } + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + + // check repeat + XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername()); + if (existUser != null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") ); + } + + // write + xxlJobUserDao.save(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/update") + @ResponseBody + public ReturnT update(XxlJobUser xxlJobUser) { + + // valid password + if (StringUtils.hasText(xxlJobUser.getPassword())) { + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + } else { + xxlJobUser.setPassword(null); + } + + // write + xxlJobUserDao.update(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/remove") + @ResponseBody + public ReturnT remove(int id) { + xxlJobUserDao.delete(id); + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java new file mode 100644 index 00000000..7de4f6f6 --- /dev/null +++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java @@ -0,0 +1,54 @@ +package com.xxl.job.admin.core.model; + +/** + * @author xuxueli 2019-05-04 16:43:12 + */ +public class XxlJobUser { + + private int id; + private String username; // 账号 + private String password; // 密码 + private int role; // 角色:0-普通用户、1-管理员 + private String permission; // 权限:执行器ID列表,多个逗号分割 + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRole() { + return role; + } + + public void setRole(int role) { + this.role = role; + } + + public String getPermission() { + return permission; + } + + public void setPermission(String permission) { + this.permission = permission; + } + +} diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java new file mode 100644 index 00000000..821ad41a --- /dev/null +++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java @@ -0,0 +1,29 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * @author xuxueli 2019-05-04 16:44:59 + */ +@Mapper +public interface XxlJobUserDao { + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username); + + public XxlJobUser loadByUserName(@Param("username") String username); + + public int save(XxlJobUser xxlJobUser); + + public int update(XxlJobUser xxlJobUser); + + public int delete(@Param("id") int id); + +} diff --git a/xxl-job-admin/src/main/resources/i18n/message.properties b/xxl-job-admin/src/main/resources/i18n/message.properties index 2765c235..9ca35819 100644 --- a/xxl-job-admin/src/main/resources/i18n/message.properties +++ b/xxl-job-admin/src/main/resources/i18n/message.properties @@ -31,6 +31,7 @@ system_unvalid=非法 system_not_found=不存在 system_nav=导航 system_digits=整数 +system_lengh_limit=长度限制 ## daterangepicker daterangepicker_ranges_recent_hour=最近一小时 @@ -229,6 +230,19 @@ jobconf_trigger_type_parent=父任务触发 jobconf_trigger_type_api=API触发 jobconf_trigger_type_retry=失败重试触发 +## user +user_manage=用户管理 +user_username=账号 +user_password=密码 +user_role=角色 +user_role_admin=管理员 +user_role_normal=普通用户 +user_permission=权限 +user_add=新增用户 +user_update=更新用户 +user_username_repeat=账号重复 +user_password_update_placeholder=请输入新密码,为空则不更新密码 + ## help job_help=使用教程 job_help_document=官方文档 \ No newline at end of file diff --git a/xxl-job-admin/src/main/resources/i18n/message_en.properties b/xxl-job-admin/src/main/resources/i18n/message_en.properties index 8fd9ac3a..61458ab1 100644 --- a/xxl-job-admin/src/main/resources/i18n/message_en.properties +++ b/xxl-job-admin/src/main/resources/i18n/message_en.properties @@ -31,6 +31,7 @@ system_unvalid=illegal system_not_found=not exist system_nav=Navigation system_digits=digits +system_lengh_limit=Length limit ## daterangepicker daterangepicker_ranges_recent_hour=recent one hour @@ -229,6 +230,19 @@ jobconf_trigger_type_parent=Parent job trigger jobconf_trigger_type_api=Api trigger jobconf_trigger_type_retry=Fail retry trigger +## user +user_manage==User Manage +user_username=Username +user_password=Password +user_role=Role +user_role_admin=Admin User +user_role_normal=Normal User +user_permission=Permission +user_add=Add User +user_update=Edit User +user_username_repeat=Username Repeat +user_password_update_placeholder=Please input password, empty means not update + ## help job_help=Tutorial job_help_document=Official Document \ No newline at end of file diff --git a/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml b/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml new file mode 100644 index 00000000..41813801 --- /dev/null +++ b/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + t.id, + t.username, + t.password, + t.role, + t.permission + + + + + + + + + + INSERT INTO XXL_JOB_QRTZ_USER ( + username, + password, + role, + permission + ) VALUES ( + #{username}, + #{password}, + #{role}, + #{permission} + ); + + + + UPDATE XXL_JOB_QRTZ_USER + SET + + password = #{password}, + + role = #{role}, + permission = #{permission} + WHERE id = #{id} + + + + DELETE + FROM XXL_JOB_QRTZ_USER + WHERE id = #{id} + + + \ No newline at end of file diff --git a/xxl-job-admin/src/main/resources/static/js/user.index.1.js b/xxl-job-admin/src/main/resources/static/js/user.index.1.js new file mode 100644 index 00000000..7051ba0f --- /dev/null +++ b/xxl-job-admin/src/main/resources/static/js/user.index.1.js @@ -0,0 +1,298 @@ +$(function() { + + // init date tables + var userListTable = $("#user_list").dataTable({ + "deferRender": true, + "processing" : true, + "serverSide": true, + "ajax": { + url: base_url + "/user/pageList", + type:"post", + data : function ( d ) { + var obj = {}; + obj.username = $('#username').val(); + obj.start = d.start; + obj.length = d.length; + return obj; + } + }, + "searching": false, + "ordering": false, + //"scrollX": true, // scroll x,close self-adaption + "columns": [ + { + "data": 'id', + "visible" : false, + "width":'10%' + }, + { + "data": 'username', + "visible" : true, + "width":'20%' + }, + { + "data": 'password', + "visible" : true, + "width":'20%' + }, + { + "data": 'role', + "visible" : true, + "width":'10%', + "render": function ( data, type, row ) { + if (data == 1) { + return I18n.user_role_admin + } else { + return I18n.user_role_normal + } + } + }, + { + "data": 'permission', + "width":'10%', + "visible" : true + }, + { + "data": I18n.system_opt , + "width":'15%', + "render": function ( data, type, row ) { + return function(){ + // html + tableData['key'+row.id] = row; + var html = '

'+ + ' '+ + ' '+ + '

'; + + return html; + }; + } + } + ], + "language" : { + "sProcessing" : I18n.dataTable_sProcessing , + "sLengthMenu" : I18n.dataTable_sLengthMenu , + "sZeroRecords" : I18n.dataTable_sZeroRecords , + "sInfo" : I18n.dataTable_sInfo , + "sInfoEmpty" : I18n.dataTable_sInfoEmpty , + "sInfoFiltered" : I18n.dataTable_sInfoFiltered , + "sInfoPostFix" : "", + "sSearch" : I18n.dataTable_sSearch , + "sUrl" : "", + "sEmptyTable" : I18n.dataTable_sEmptyTable , + "sLoadingRecords" : I18n.dataTable_sLoadingRecords , + "sInfoThousands" : ",", + "oPaginate" : { + "sFirst" : I18n.dataTable_sFirst , + "sPrevious" : I18n.dataTable_sPrevious , + "sNext" : I18n.dataTable_sNext , + "sLast" : I18n.dataTable_sLast + }, + "oAria" : { + "sSortAscending" : I18n.dataTable_sSortAscending , + "sSortDescending" : I18n.dataTable_sSortDescending + } + } + }); + + // table data + var tableData = {}; + + // search btn + $('#searchBtn').on('click', function(){ + userListTable.fnDraw(); + }); + + // job operate + $("#user_list").on('click', '.delete',function() { + var id = $(this).parent('p').attr("id"); + + layer.confirm( I18n.system_ok + I18n.system_opt_del + '?', { + icon: 3, + title: I18n.system_tips , + btn: [ I18n.system_ok, I18n.system_cancel ] + }, function(index){ + layer.close(index); + + $.ajax({ + type : 'POST', + url : base_url + "/user/remove", + data : { + "id" : id + }, + dataType : "json", + success : function(data){ + if (data.code == 200) { + layer.msg( I18n.system_success ); + userListTable.fnDraw(false); + } else { + layer.msg( data.msg || I18n.system_opt_del + I18n.system_fail ); + } + } + }); + }); + }); + + // add + $(".add").click(function(){ + $('#addModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var addModalValidate = $("#addModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + username : { + required : true, + rangelength:[4, 20] + }, + password : { + required : true, + rangelength:[4, 20] + } + }, + messages : { + username : { + required : I18n.system_please_input + I18n.user_username, + rangelength: I18n.system_lengh_limit + "[4-20]" + }, + password : { + required : I18n.system_please_input + I18n.user_password, + rangelength: I18n.system_lengh_limit + "[4-20]" + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + + var permissionArr = []; + $("#addModal .form input[name=permission]:checked").each(function(){ + permissionArr.push($(this).val()); + }); + + var paramData = { + "username": $("#addModal .form input[name=username]").val(), + "password": $("#addModal .form input[name=password]").val(), + "role": $("#addModal .form input[name=role]:checked").val(), + "permission": permissionArr.join(',') + }; + + $.post(base_url + "/user/add", paramData, function(data, status) { + if (data.code == "200") { + $('#addModal').modal('hide'); + + layer.msg( I18n.system_add_suc ); + userListTable.fnDraw(); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_add_fail), + icon: '2' + }); + } + }); + } + }); + $("#addModal").on('hide.bs.modal', function () { + $("#addModal .form")[0].reset(); + addModalValidate.resetForm(); + $("#addModal .form .form-group").removeClass("has-error"); + $(".remote_panel").show(); // remote + }); + + // update + $("#user_list").on('click', '.update',function() { + + var id = $(this).parent('p').attr("id"); + var row = tableData['key'+id]; + + // base data + $("#updateModal .form input[name='id']").val( row.id ); + $("#updateModal .form input[name='username']").val( row.username ); + $("#updateModal .form input[name='password']").val( '' ); + $("#updateModal .form input[name='role']").each(function () { + if($(this).val() == row.role) { + $(this).prop("checked",true); + } else { + $(this).prop("checked",false); + } + }); + var permissionArr = []; + if (row.permission) { + permissionArr = row.permission.split(","); + } + $("#updateModal .form input[name='permission']").removeProp('checked'); + $("#updateModal .form input[name='permission']").each(function () { + if($.inArray($(this).val(), permissionArr) > -1) { + $(this).prop("checked",true); + } else { + $(this).prop("checked",false); + } + }); + + // show + $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var updateModalValidate = $("#updateModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + + var permissionArr =[]; + $("#updateModal .form input[name=permission]:checked").each(function(){ + permissionArr.push($(this).val()); + }); + + var paramData = { + "id": $("#updateModal .form input[name=id]").val(), + "username": $("#updateModal .form input[name=username]").val(), + "password": $("#updateModal .form input[name=password]").val(), + "role": $("#updateModal .form input[name=role]:checked").val(), + "permission": permissionArr.join(',') + }; + + $.post(base_url + "/user/update", paramData, function(data, status) { + if (data.code == "200") { + $('#updateModal').modal('hide'); + + layer.msg( I18n.system_update_suc ); + userListTable.fnDraw(); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_update_fail), + icon: '2' + }); + } + }); + } + }); + $("#updateModal").on('hide.bs.modal', function () { + $("#updateModal .form")[0].reset(); + updateModalValidate.resetForm(); + $("#updateModal .form .form-group").removeClass("has-error"); + $(".remote_panel").show(); // remote + }); + +}); diff --git a/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl b/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl index 171ff026..88f6a296 100644 --- a/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl +++ b/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl @@ -103,6 +103,7 @@ + diff --git a/xxl-job-admin/src/main/resources/templates/user/user.index.ftl b/xxl-job-admin/src/main/resources/templates/user/user.index.ftl new file mode 100644 index 00000000..b44a663c --- /dev/null +++ b/xxl-job-admin/src/main/resources/templates/user/user.index.ftl @@ -0,0 +1,179 @@ + + + + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + + + ${I18n.admin_name} + +sidebar-collapse"> +
+ + <@netCommon.commonHeader /> + + <@netCommon.commonLeft "user" /> + + +
+ +
+

${I18n.user_manage}

+
+ + +
+ +
+
+
+ ${I18n.user_username} + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + + + + + + + + + + + + +
ID${I18n.user_username}${I18n.user_password}${I18n.user_role}${I18n.user_permission}${I18n.system_opt}
+
+
+
+
+
+
+ + + <@netCommon.commonFooter /> +
+ + + + + + + +<@netCommon.commonScript /> + + + + + + +