Browse Source

添加文件

weicky 4 years ago
commit
6cf93904b9
59 changed files with 14214 additions and 0 deletions
  1. 2 0
      .env
  2. 5 0
      .gitignore
  3. 10 0
      etc/config.json
  4. 17 0
      make.sh
  5. 587 0
      resources/bootstrap-3.4.1-dist/css/bootstrap-theme.css
  6. 1 0
      resources/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map
  7. 6 0
      resources/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css
  8. 1 0
      resources/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map
  9. 6834 0
      resources/bootstrap-3.4.1-dist/css/bootstrap.css
  10. 1 0
      resources/bootstrap-3.4.1-dist/css/bootstrap.css.map
  11. 6 0
      resources/bootstrap-3.4.1-dist/css/bootstrap.min.css
  12. 1 0
      resources/bootstrap-3.4.1-dist/css/bootstrap.min.css.map
  13. BIN
      resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot
  14. 288 0
      resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg
  15. BIN
      resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf
  16. BIN
      resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff
  17. BIN
      resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2
  18. 2580 0
      resources/bootstrap-3.4.1-dist/js/bootstrap.js
  19. 6 0
      resources/bootstrap-3.4.1-dist/js/bootstrap.min.js
  20. 13 0
      resources/bootstrap-3.4.1-dist/js/npm.js
  21. 24 0
      resources/css/common.css
  22. 201 0
      resources/js/common.js
  23. 2 0
      resources/js/jquery-3.4.1.min.js
  24. 83 0
      sql/db.sql
  25. 177 0
      src/cnphper.com/model/accounts.go
  26. 13 0
      src/cnphper.com/model/model.go
  27. 253 0
      src/cnphper.com/model/rediscfg.go
  28. 137 0
      src/cnphper.com/model/syscfg.go
  29. 37 0
      src/cnphper.com/redisdog/common.go
  30. 173 0
      src/cnphper.com/redisdog/index.go
  31. 43 0
      src/cnphper.com/redisdog/log_account.go
  32. 43 0
      src/cnphper.com/redisdog/log_autoprocess.go
  33. 43 0
      src/cnphper.com/redisdog/log_syslog.go
  34. 43 0
      src/cnphper.com/redisdog/log_warn.go
  35. 63 0
      src/cnphper.com/redisdog/login.go
  36. 72 0
      src/cnphper.com/redisdog/mailsender.go
  37. 221 0
      src/cnphper.com/redisdog/main.go
  38. 42 0
      src/cnphper.com/redisdog/monitor.go
  39. 90 0
      src/cnphper.com/redisdog/profile.go
  40. 81 0
      src/cnphper.com/redisdog/session.go
  41. 69 0
      src/cnphper.com/redisdog/setting.go
  42. 87 0
      src/cnphper.com/redisdog/sql.go
  43. 271 0
      src/cnphper.com/redisdog/syscfg_account.go
  44. 50 0
      src/cnphper.com/redisdog/syscfg_misc.go
  45. 318 0
      src/cnphper.com/redisdog/syscfg_redis.go
  46. 50 0
      src/cnphper.com/redisdog/syscfg_warn.go
  47. 4 0
      start.sh
  48. 3 0
      stop.sh
  49. 193 0
      tmpl/dump.tmpl
  50. 11 0
      tmpl/header.tmpl
  51. 132 0
      tmpl/index.tmpl
  52. 61 0
      tmpl/login.tmpl
  53. 55 0
      tmpl/navbar.tmpl
  54. 60 0
      tmpl/profile/passwd.tmpl
  55. 243 0
      tmpl/syscfg/account.tmpl
  56. 10 0
      tmpl/syscfg/mailgroup.tmpl
  57. 65 0
      tmpl/syscfg/misc.tmpl
  58. 323 0
      tmpl/syscfg/redis.tmpl
  59. 10 0
      tmpl/syscfg/warn.tmpl

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+cd $(dirname ${BASH_SOURCE-$0})
+export GOPATH=$(pwd)

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+bin/redisdog
+db/
+pkg/
+log/
+src/github.com/

+ 10 - 0
etc/config.json

@@ -0,0 +1,10 @@
+{
+	"Host": "192.168.56.102",
+	"Port": "8080",
+	"Database": "/mnt/workroot_sf/go/redisdog/db/db.sqlite",
+	"RootPwd": "FuckYou!",
+	"LogDir": "/mnt/workroot_sf/go/redisdog/log",
+	"TmplDir": "/mnt/workroot_sf/go/redisdog/tmpl",
+	"ResourcesDir": "/mnt/workroot_sf/go/redisdog/resources",
+	"SessionTTL": 900
+}

+ 17 - 0
make.sh

@@ -0,0 +1,17 @@
+#!/bin/bash
+
+cd $(dirname $0)
+ROOT=$(pwd)
+
+source ./.env
+
+INSTALL="src/cnphper.com/model src/cnphper.com/redisdog"
+
+for ITEM in $INSTALL
+do
+	echo $ITEM
+	cd $ROOT/$ITEM
+	go install
+done
+
+cd $ROOT

+ 587 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap-theme.css

@@ -0,0 +1,587 @@
+/*!
+ * Bootstrap v3.4.1 (https://getbootstrap.com/)
+ * Copyright 2011-2019 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+.btn-default.disabled,
+.btn-primary.disabled,
+.btn-success.disabled,
+.btn-info.disabled,
+.btn-warning.disabled,
+.btn-danger.disabled,
+.btn-default[disabled],
+.btn-primary[disabled],
+.btn-success[disabled],
+.btn-info[disabled],
+.btn-warning[disabled],
+.btn-danger[disabled],
+fieldset[disabled] .btn-default,
+fieldset[disabled] .btn-primary,
+fieldset[disabled] .btn-success,
+fieldset[disabled] .btn-info,
+fieldset[disabled] .btn-warning,
+fieldset[disabled] .btn-danger {
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+.btn-default .badge,
+.btn-primary .badge,
+.btn-success .badge,
+.btn-info .badge,
+.btn-warning .badge,
+.btn-danger .badge {
+  text-shadow: none;
+}
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+.btn-default {
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
+  background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  text-shadow: 0 1px 0 #fff;
+  border-color: #ccc;
+}
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+.btn-default.disabled,
+.btn-default[disabled],
+fieldset[disabled] .btn-default,
+.btn-default.disabled:hover,
+.btn-default[disabled]:hover,
+fieldset[disabled] .btn-default:hover,
+.btn-default.disabled:focus,
+.btn-default[disabled]:focus,
+fieldset[disabled] .btn-default:focus,
+.btn-default.disabled.focus,
+.btn-default[disabled].focus,
+fieldset[disabled] .btn-default.focus,
+.btn-default.disabled:active,
+.btn-default[disabled]:active,
+fieldset[disabled] .btn-default:active,
+.btn-default.disabled.active,
+.btn-default[disabled].active,
+fieldset[disabled] .btn-default.active {
+  background-color: #e0e0e0;
+  background-image: none;
+}
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
+  background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #245580;
+}
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #265a88;
+  background-position: 0 -15px;
+}
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #265a88;
+  border-color: #245580;
+}
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled.focus,
+.btn-primary[disabled].focus,
+fieldset[disabled] .btn-primary.focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+  background-color: #265a88;
+  background-image: none;
+}
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+}
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+.btn-success.disabled,
+.btn-success[disabled],
+fieldset[disabled] .btn-success,
+.btn-success.disabled:hover,
+.btn-success[disabled]:hover,
+fieldset[disabled] .btn-success:hover,
+.btn-success.disabled:focus,
+.btn-success[disabled]:focus,
+fieldset[disabled] .btn-success:focus,
+.btn-success.disabled.focus,
+.btn-success[disabled].focus,
+fieldset[disabled] .btn-success.focus,
+.btn-success.disabled:active,
+.btn-success[disabled]:active,
+fieldset[disabled] .btn-success:active,
+.btn-success.disabled.active,
+.btn-success[disabled].active,
+fieldset[disabled] .btn-success.active {
+  background-color: #419641;
+  background-image: none;
+}
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+}
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+.btn-info.disabled,
+.btn-info[disabled],
+fieldset[disabled] .btn-info,
+.btn-info.disabled:hover,
+.btn-info[disabled]:hover,
+fieldset[disabled] .btn-info:hover,
+.btn-info.disabled:focus,
+.btn-info[disabled]:focus,
+fieldset[disabled] .btn-info:focus,
+.btn-info.disabled.focus,
+.btn-info[disabled].focus,
+fieldset[disabled] .btn-info.focus,
+.btn-info.disabled:active,
+.btn-info[disabled]:active,
+fieldset[disabled] .btn-info:active,
+.btn-info.disabled.active,
+.btn-info[disabled].active,
+fieldset[disabled] .btn-info.active {
+  background-color: #2aabd2;
+  background-image: none;
+}
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+}
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+.btn-warning.disabled,
+.btn-warning[disabled],
+fieldset[disabled] .btn-warning,
+.btn-warning.disabled:hover,
+.btn-warning[disabled]:hover,
+fieldset[disabled] .btn-warning:hover,
+.btn-warning.disabled:focus,
+.btn-warning[disabled]:focus,
+fieldset[disabled] .btn-warning:focus,
+.btn-warning.disabled.focus,
+.btn-warning[disabled].focus,
+fieldset[disabled] .btn-warning.focus,
+.btn-warning.disabled:active,
+.btn-warning[disabled]:active,
+fieldset[disabled] .btn-warning:active,
+.btn-warning.disabled.active,
+.btn-warning[disabled].active,
+fieldset[disabled] .btn-warning.active {
+  background-color: #eb9316;
+  background-image: none;
+}
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+}
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+.btn-danger.disabled,
+.btn-danger[disabled],
+fieldset[disabled] .btn-danger,
+.btn-danger.disabled:hover,
+.btn-danger[disabled]:hover,
+fieldset[disabled] .btn-danger:hover,
+.btn-danger.disabled:focus,
+.btn-danger[disabled]:focus,
+fieldset[disabled] .btn-danger:focus,
+.btn-danger.disabled.focus,
+.btn-danger[disabled].focus,
+fieldset[disabled] .btn-danger.focus,
+.btn-danger.disabled:active,
+.btn-danger[disabled]:active,
+fieldset[disabled] .btn-danger:active,
+.btn-danger.disabled.active,
+.btn-danger[disabled].active,
+fieldset[disabled] .btn-danger.active {
+  background-color: #c12e2a;
+  background-image: none;
+}
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+  background-color: #e8e8e8;
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
+  background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
+  background-repeat: repeat-x;
+  background-color: #2e6da4;
+}
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
+  background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8));
+  background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+}
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
+  background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+}
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+}
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
+  background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  border-radius: 4px;
+}
+.navbar-inverse .navbar-nav > .open > a,
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
+  background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+  box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+}
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+@media (max-width: 767px) {
+  .navbar .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #fff;
+    background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+    background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
+    background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
+    background-repeat: repeat-x;
+  }
+}
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+}
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+}
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+}
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
+  background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+}
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
+  background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
+  background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-striped {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+}
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #286090;
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
+  background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+}
+.list-group-item.active .badge,
+.list-group-item.active:hover .badge,
+.list-group-item.active:focus .badge {
+  text-shadow: none;
+}
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
+  background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
+  background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
+  background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+/*# sourceMappingURL=bootstrap-theme.css.map */

File diff suppressed because it is too large
+ 1 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map


File diff suppressed because it is too large
+ 6 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css


File diff suppressed because it is too large
+ 1 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map


File diff suppressed because it is too large
+ 6834 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap.css


File diff suppressed because it is too large
+ 1 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap.css.map


File diff suppressed because it is too large
+ 6 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap.min.css


File diff suppressed because it is too large
+ 1 - 0
resources/bootstrap-3.4.1-dist/css/bootstrap.min.css.map


BIN
resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot


File diff suppressed because it is too large
+ 288 - 0
resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg


BIN
resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf


BIN
resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff


BIN
resources/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2


File diff suppressed because it is too large
+ 2580 - 0
resources/bootstrap-3.4.1-dist/js/bootstrap.js


File diff suppressed because it is too large
+ 6 - 0
resources/bootstrap-3.4.1-dist/js/bootstrap.min.js


+ 13 - 0
resources/bootstrap-3.4.1-dist/js/npm.js

@@ -0,0 +1,13 @@
+// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
+require('../../js/transition.js')
+require('../../js/alert.js')
+require('../../js/button.js')
+require('../../js/carousel.js')
+require('../../js/collapse.js')
+require('../../js/dropdown.js')
+require('../../js/modal.js')
+require('../../js/tooltip.js')
+require('../../js/popover.js')
+require('../../js/scrollspy.js')
+require('../../js/tab.js')
+require('../../js/affix.js')

+ 24 - 0
resources/css/common.css

@@ -0,0 +1,24 @@
+html{
+	font-size: 14px;
+}
+body{
+	padding-top: 70px;
+}
+.pannel{
+	margin: 10px 0px;
+	border: 1px solid #ddd;
+	border-radius: 4px;
+}
+.pannel_title, .pannel_body, .pannel_foot{
+	padding: 10px;
+}
+.pannel_title{
+	border-bottom: 1px solid #ddd;
+}
+.pannel_title h3{
+	margin: 0px;
+	font-size: 1.2em;
+}
+.pannel_foot{
+	border-top: 1px solid #ddd;
+}

+ 201 - 0
resources/js/common.js

@@ -0,0 +1,201 @@
+$.extend($, {
+	'alert': function(options) {
+		if (Object.prototype.toString.call(options) == '[object String]') {
+			options = {
+				'content': options
+			};
+		}
+		var options = $.extend({
+			'content'	: '',
+			'title'		: '提示',
+			'init'		: function(e, dom){},
+			'callback'	: function(e, dom){},
+			'size'		: '',
+			'bgcolor'	: '',
+			'btntext'	: '确认',
+			'btnbg'		: 'btn-primary',
+			'btncancel'	: false
+		}, options);
+		var id = 'msgbox_id_' + Math.floor(Math.random() * 100000000);
+		var btncancel = options.btncancel ? '<button type="button" class="btn btn-sm btn-default" data-dismiss="modal" aria-label="Close">取消</button>' : '';
+		var html = `<div class="modal fade" tabindex="-1" role="dialog" id="${id}">
+		  <div class="modal-dialog ${options.size}" role="document">
+		    <div class="modal-content ${options.bgcolor}">
+		      <div class="modal-header">
+		        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+		          <span aria-hidden="true">&times;</span>
+		        </button>
+		        <h4 class="modal-title">${options.title}</h4>
+		      </div>
+		      <div class="modal-body">${options.content}</div>
+		      <div class="modal-footer">
+		        <button type="button" class="btn btn-sm ${options.btnbg}">${options.btntext}</button> ${btncancel}
+		      </div>
+		    </div>
+		  </div>
+		</div>`;
+		var jq = $(html);
+		$(document.body).append(jq);
+		$('.btn-primary', jq).click(function(e){
+			if (options.callback && options.callback(jq) !== false) {
+				jq.modal('hide');
+			}
+		});
+		jq.on('show.bs.modal', function(e){
+			if (options.init) {
+				options.init(jq);
+			}
+		});
+		jq.on('hidden.bs.modal', function(e){
+			$(this).remove();
+		});
+		jq.modal('show');
+	}
+});
+
+function time2str(timestamp, cnFormat) {
+	var obj = new Date(timestamp * 1000);
+	if(cnFormat) {
+		var Y = obj.getFullYear(),
+			m = obj.getMonth() + 1,
+			d = obj.getDate(),
+			H = obj.getHours(),
+			i = obj.getMinutes(),
+			s = obj.getSeconds();
+		return Y
+			+ '-' + (m < 10 ? '0'+m : m)
+			+ '-' + (d < 10 ? '0'+d : d)
+			+ ' ' + (H < 10 ? '0'+H : H)
+			+ ':' + (i < 10 ? '0'+i : i)
+			+ ':' + (s < 10 ? '0'+s : s);
+	} else {
+		return obj.toLocaleString();
+	}
+}
+
+function size2str(size) {
+	if(size < 1024) {
+		return size + '字节';
+	} else if(size < 1024*1024) {
+		return numberRound(size / 1024, 2) + 'K';
+	} else if(size < 1024*1024*1024) {
+		return numberRound(size / 1024 / 1024, 2) + 'M';
+	} else {
+		return numberRound(size / 1024 / 1024 / 1024, 2) + 'G';
+	}
+}
+
+function numberFormat(n, len) {
+	n = parseInt(n);
+	len = len == 4 ? 4 : 3;
+	if(n == NaN) {
+		return 0;
+	}
+	var sign = n < 0 ? '-' : '';
+	var str = '' + Math.abs(n), ret = '';
+	var mod = str.length % len;
+	if(mod > 0) {
+		ret += str.substr(0, mod) + ',';
+	}
+	for(var i=mod; i<str.length; i+=len) {
+		ret += str.substr(i, len) + ',';
+	}
+	return sign + ret.substr(0, ret.length - 1);
+}
+
+function numberRound(n, len) {
+	n = parseFloat(n);
+	len = len || 2;
+	if(n == NaN || len < 1) {
+		return 0;
+	}
+	var x = Math.pow(10, len);
+	var str = '' + Math.round(n * x) / x;
+	var pos = str.indexOf('.');
+	var declen = pos == -1 ? 0 : str.length - pos - 1;
+	if (declen == len) {
+		return str;
+	}
+	if (pos == -1) {
+		str += '.';
+	}
+	for (var i=declen; i<len; i++) {
+		str += '0';
+	}
+	return str;
+}
+
+function pager(options) {
+	options = $.extend({
+		'id': '',
+		'total': 0,
+		'page_size': 20,
+		'page_num': 1,
+		'bar_size': 9,
+		'show_prev_next': true,
+		'show_first_last': true,
+		'callback': function(page) {}
+	}, options);
+	if (options.id == '') {
+		return;
+	}
+	if (options.bar_size % 2 == 0) {
+		options.bar_size++;
+	}
+	var bar_side = (options.bar_size-1) / 2;
+	var pages = Math.ceil(parseInt(options.total) / parseInt(options.page_size));
+	if (options.page_num < 1 || options.page_num > pages) {
+		return;
+	}
+	var html = '<nav id="pager"><ul class="pagination">';
+	if (options.show_first_last) {
+		if (options.page_num == 1) {
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="1">首页</a></li>';
+		} else {
+			html += '<li><a class="page-link" href="#" data-value="1">首页</a></li>';
+		}
+	}
+	if (options.show_prev_next) {
+		if (options.page_num > 1) {
+			html += '<li><a class="page-link" href="#" data-value="' + (options.page_num-1) + '">前页</a></li>';
+		} else {
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="1">前页</a></li>';
+		}
+	}
+	if (pages <= options.bar_size) {
+		for (var i=1; i<=pages; i++) {
+			if (i == options.page_num) {
+				html += '<li class="disabled"><a class="page-link" href="#" data-value="' + i + '">' + i + '</a></li>';
+			} else {
+				html += '<li><a class="page-link" href="#" data-value="' + i + '">' + i + '</a></li>';
+			}
+		}
+	} else if (options.page_num <= bar_side) {
+		
+	} else if (options.page_num >= pages - bar_side) {
+		
+	} else {
+		
+	}
+	if (options.show_prev_next) {
+		if (options.page_num < pages) {
+			html += '<li><a class="page-link" href="#" data-value="' + (options.page_num+1) + '">后页</a></li>';
+		} else {
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="' + pages + '">后页</a></li>';
+		}
+	}
+	if (options.show_first_last) {
+		if (options.page_num == pages) {
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="' + pages + '">末页</a></li>';
+		} else {
+			html += '<li><a class="page-link" href="#" data-value="' + pages + '">末页</a></li>';
+		}
+	}
+	html += '</ul></nav>';
+	var jq = $('#' + options.id);
+	jq.html(html);
+	$('a', jq).click(function(){
+		options.callback($(this).attr('data-value'));
+		return false;
+	});
+}

File diff suppressed because it is too large
+ 2 - 0
resources/js/jquery-3.4.1.min.js


+ 83 - 0
sql/db.sql

@@ -0,0 +1,83 @@
+/*
+服务IP与端口
+邮件发送方式(SMTP|sendmail)
+SMTP ACCOUNT
+SMTP PWD
+SMTP USER
+SMTP SERVER
+SMTP TIMEOUT
+*/
+
+CREATE TABLE IF NOT EXISTS syscfg (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  cfg_key TEXT UNIQUE,
+  cfg_value TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS accounts (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  account TEXT UNIQUE,
+  name TEXT NOT NULL,
+  password TEXT NOT NULL,
+  last_login INTEGER NOT NULL DEFAULT 0,
+  is_super INTEGER NOT NULL DEFAULT 0,
+  disabled INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS rediscfg (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  address TEXT UNIQUE,
+  remark TEXT NOT NULL DEFAULT '',
+  password TEXT NOT NULL DEFAULT '',
+  max_connect_wait INTEGER NOT NULL DEFAULT 5,
+  max_status_failed INTEGER NOT NULL DEFAULT 1,
+  min_memory_free INTEGER NOT NULL DEFAULT 0,
+  min_memory_free_pc INTEGER NOT NULL DEFAULT 0,
+  max_memory_usage INTEGER NOT NULL DEFAULT 16000000000,
+  max_connection INTEGER NOT NULL DEFAULT 1000,
+  max_evi_increased INTEGER NOT NULL DEFAULT 1,
+  max_qps INTEGER NOT NULL DEFAULT 50000,
+  mail_list TEXT NOT NULL DEFAULT '',
+  disabled INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS syslog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL,
+  log_time INTEGER NOT NULL,
+  log_msg TEXT COMMENT NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_account ON syslog(account_id);
+
+CREATE TABLE IF NOT EXISTS statuslog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  redis_id INTEGER NOT NULL,
+  check_time INTEGER NOT NULL,
+  status INTEGER NOT NULL DEFAULT 0,
+  info TEXT NOT NULL DEFAULT '',
+  memory_usage INTEGER NOT NULL DEFAULT 0,
+  memory_usage_pc INTEGER NOT NULL DEFAULT 0,
+  connection INTEGER NOT NULL DEFAULT 0,
+  qps INTEGER NOT NULL DEFAULT 0,
+  evi_increased INTEGER NOT NULL DEFAULT 0,
+  UNIQUE (redis_id, check_time)
+);
+
+CREATE TABLE IF NOT EXISTS warnlog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  redis_id INTEGER NOT NULL,
+  warn_time INTEGER NOT NULL,
+  warn_msg TEXT NOT NULL,
+  warn_status INTEGER NOT NULL DEFAULT 0,
+  UNIQUE (redis_id, warn_time)
+);
+
+CREATE TABLE IF NOT EXISTS processlog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  redis_id INTEGER NOT NULL,
+  process_time INTEGER NOT NULL,
+  process_info TEXT NOT NULL DEFAULT '',
+  process_status INTEGER NOT NULL DEFAULT 0,
+  UNIQUE (redis_id, process_time)
+);

+ 177 - 0
src/cnphper.com/model/accounts.go

@@ -0,0 +1,177 @@
+package model
+
+import (
+	"crypto/md5"
+	"database/sql"
+	"errors"
+	"fmt"
+)
+
+type Accounts struct {
+	Model
+}
+
+type AccountsRow struct {
+	Id        int64
+	Account   string
+	Name      string
+	Password  string
+	LastLogin int64
+	IsSuper   bool
+	Disabled  bool
+}
+
+func NewAccounts(db *sql.DB) *Accounts {
+	return &Accounts{Model{db: db}}
+}
+
+func passwordConvert(pwd string) string {
+	pwdMd5 := fmt.Sprintf("%x", md5.Sum([]byte(pwd)))
+	return fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("^redisdog|%s|pwd$", pwdMd5))))
+}
+
+func (m *Accounts) Insert(row *AccountsRow) (int64, error) {
+	if row.Id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	if row.Account == "" {
+		return 0, errors.New("账号不能为空")
+	}
+	if len(row.Password) < 6 {
+		return 0, errors.New("密码长度不能小于6")
+	}
+	disabledInt := 0
+	if row.Disabled {
+		disabledInt = 1
+	}
+	passwordEnc := passwordConvert(row.Password)
+	ret, err := m.db.Exec(`insert into accounts(account,name,password,is_super,disabled) values(?,?,?,?,?)`, row.Account, row.Name, passwordEnc, row.IsSuper, disabledInt)
+	if err != nil {
+		return 0, nil
+	}
+	return ret.LastInsertId()
+}
+
+func (m *Accounts) Delete(id int64) (int64, error) {
+	if id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	ret, err := m.db.Exec("delete from accounts where id=?", id)
+	if err != nil {
+		return 0, err
+	}
+	return ret.RowsAffected()
+}
+
+func (m *Accounts) Update(row *AccountsRow) (int64, error) {
+	if row.Id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	if row.Account == "" {
+		return 0, errors.New("账号不能为空")
+	}
+	disabledInt := 0
+	if row.Disabled {
+		disabledInt = 1
+	}
+	ret, err := m.db.Exec(`update accounts set account=?,name=?,is_super=?,disabled=? where id=?`, row.Account, row.Name, row.IsSuper, disabledInt, row.Id)
+	if err != nil {
+		return 0, err
+	}
+	return ret.RowsAffected()
+}
+
+func (m *Accounts) UpdatePassword(id int64, password string) (int64, error) {
+	if id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	if len(password) < 6 {
+		return 0, errors.New("密码长度不能小于6")
+	}
+	passwordEnc := passwordConvert(password)
+	ret, err := m.db.Exec(`update accounts set password=? where id=?`, passwordEnc, id)
+	if err != nil {
+		return 0, err
+	}
+	return ret.RowsAffected()
+}
+
+func (m *Accounts) Get(id int64) (*AccountsRow, error) {
+	if id <= 0 {
+		return nil, errors.New("ID必须是大于0的整数")
+	}
+	result := m.db.QueryRow("select id,account,name,last_login,is_super,disabled from accounts where id=?", id)
+	row := AccountsRow{}
+	err := result.Scan(
+		&row.Id,
+		&row.Account,
+		&row.Name,
+		&row.LastLogin,
+		&row.IsSuper,
+		&row.Disabled,
+	)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, errors.New("记录不存在")
+		} else {
+			return nil, err
+		}
+	} else {
+		return &row, nil
+	}
+}
+
+func (m *Accounts) GetAll() ([]*AccountsRow, error) {
+	result, err := m.db.Query(`select id,account,name,last_login,is_super,disabled from accounts order by id`)
+	if err != nil {
+		return nil, err
+	}
+	rows := make([]*AccountsRow, 0)
+	for result.Next() {
+		row := AccountsRow{}
+		err = result.Scan(
+			&row.Id,
+			&row.Account,
+			&row.Name,
+			&row.LastLogin,
+			&row.IsSuper,
+			&row.Disabled,
+		)
+		if err != nil {
+			continue
+		}
+		rows = append(rows, &row)
+	}
+	return rows, nil
+}
+
+func (m *Accounts) Login(account, password string) (*AccountsRow, error) {
+	if account == "" {
+		return nil, errors.New("账号不能为空")
+	}
+	if len(password) < 6 {
+		return nil, errors.New("密码长度不能小于6")
+	}
+	passwordEnc := passwordConvert(password)
+	result := m.db.QueryRow("SELECT id,account,name,last_login,is_super,disabled FROM accounts WHERE account=? AND password=? LIMIT 1", account, passwordEnc)
+	row := AccountsRow{}
+	err := result.Scan(
+		&row.Id,
+		&row.Account,
+		&row.Name,
+		&row.LastLogin,
+		&row.IsSuper,
+		&row.Disabled,
+	)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, errors.New("用户名或密码错误")
+		} else {
+			return nil, err
+		}
+	} else if row.Disabled {
+		return nil, errors.New("账号已被禁用")
+	} else {
+		return &row, nil
+	}
+}

+ 13 - 0
src/cnphper.com/model/model.go

@@ -0,0 +1,13 @@
+package model
+
+import (
+	"database/sql"
+)
+
+type Model struct {
+	db *sql.DB
+}
+
+func (m *Model) GetDB() *sql.DB {
+	return m.db
+}

+ 253 - 0
src/cnphper.com/model/rediscfg.go

@@ -0,0 +1,253 @@
+package model
+
+import (
+	"database/sql"
+	"errors"
+)
+
+type RedisCfg struct {
+	Model
+}
+
+type RedisCfgRow struct {
+	Id              int64
+	Address         string
+	Remark          string
+	Password        string
+	MaxConnectWait  int64
+	MaxStatusFailed int64
+	MinMemoryFree   int64
+	MinMemoryFreePC int64
+	MaxMemoryUsage  int64
+	MaxConnection   int64
+	MaxEviIncreased int64
+	MaxQPS          int64
+	MailList        string
+	Disabled        bool
+}
+
+func NewRedisCfg(db *sql.DB) *RedisCfg {
+	return &RedisCfg{
+		Model{
+			db: db,
+		},
+	}
+}
+
+func (m *RedisCfg) Insert(row *RedisCfgRow) (int64, error) {
+	if row.Id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	if row.Address == "" {
+		return 0, errors.New("Address不能为空")
+	}
+	disabledInt := 0
+	if row.Disabled {
+		disabledInt = 1
+	}
+	ret, err := m.db.Exec(`insert into rediscfg(
+			address,
+			remark,
+			password,
+			max_connect_wait,
+			max_status_failed,
+			min_memory_free,
+			min_memory_free_pc,
+			max_memory_usage,
+			max_connection,
+			max_evi_increased,
+			max_qps,
+			mail_list,
+			disabled
+		) values(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
+		row.Address,
+		row.Remark,
+		row.Password,
+		row.MaxConnectWait,
+		row.MaxStatusFailed,
+		row.MinMemoryFree,
+		row.MinMemoryFreePC,
+		row.MaxMemoryUsage,
+		row.MaxConnection,
+		row.MaxEviIncreased,
+		row.MaxQPS,
+		row.MailList,
+		disabledInt,
+	)
+	if err != nil {
+		return 0, err
+	}
+	return ret.LastInsertId()
+}
+
+func (m *RedisCfg) Update(row *RedisCfgRow) (int64, error) {
+	if row.Id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	if row.Address == "" {
+		return 0, errors.New("Address不能为空")
+	}
+	disabledInt := 0
+	if row.Disabled {
+		disabledInt = 1
+	}
+	ret, err := m.db.Exec(`update rediscfg set
+			address=?,
+			remark=?,
+			password=?,
+			max_connect_wait=?,
+			max_status_failed=?,
+			min_memory_free=?,
+			min_memory_free_pc=?,
+			max_memory_usage=?,
+			max_connection=?,
+			max_evi_increased=?,
+			max_qps=?,
+			mail_list=?,
+			disabled=?
+		where id=?`,
+		row.Address,
+		row.Remark,
+		row.Password,
+		row.MaxConnectWait,
+		row.MaxStatusFailed,
+		row.MinMemoryFree,
+		row.MinMemoryFreePC,
+		row.MaxMemoryUsage,
+		row.MaxConnection,
+		row.MaxEviIncreased,
+		row.MaxQPS,
+		row.MailList,
+		disabledInt,
+		row.Id,
+	)
+	if err != nil {
+		return 0, nil
+	}
+	return ret.RowsAffected()
+}
+
+func (m *RedisCfg) Delete(id int64) (int64, error) {
+	if id <= 0 {
+		return 0, errors.New("ID必须是大于0的整数")
+	}
+	ret, err := m.db.Exec("delete from rediscfg where id=?", id)
+	if err != nil {
+		return 0, err
+	}
+	return ret.RowsAffected()
+}
+
+func (m *RedisCfg) Get(id int64) (*RedisCfgRow, error) {
+	if id <= 0 {
+		return nil, errors.New("ID必须是大于0的整数")
+	}
+	result := m.db.QueryRow(`select id,
+			address,
+			remark,
+			password,
+			max_connect_wait,
+			max_status_failed,
+			min_memory_free,
+			min_memory_free_pc,
+			max_memory_usage,
+			max_connection,
+			max_evi_increased,
+			max_qps,
+			mail_list,
+			disabled from rediscfg where id=?`,
+		id,
+	)
+	row := RedisCfgRow{}
+	err := result.Scan(
+		&row.Id,
+		&row.Address,
+		&row.Remark,
+		&row.Password,
+		&row.MaxConnectWait,
+		&row.MaxStatusFailed,
+		&row.MinMemoryFree,
+		&row.MinMemoryFreePC,
+		&row.MaxMemoryUsage,
+		&row.MaxConnection,
+		&row.MaxEviIncreased,
+		&row.MaxQPS,
+		&row.MailList,
+		&row.Disabled,
+	)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, errors.New("记录不存在")
+		} else {
+			return nil, err
+		}
+	} else {
+		return &row, nil
+	}
+}
+
+func (m *RedisCfg) GetAll(disabledInt int) ([]*RedisCfgRow, error) {
+	var sql string
+	if disabledInt < 0 {
+		sql = `select id,
+			address,
+			remark,
+			password,
+			max_connect_wait,
+			max_status_failed,
+			min_memory_free,
+			min_memory_free_pc,
+			max_memory_usage,
+			max_connection,
+			max_evi_increased,
+			max_qps,
+			mail_list,
+			disabled
+		from rediscfg where disabled>? order by id`
+	} else {
+		sql = `select id,
+			address,
+			remark,
+			password,
+			max_connect_wait,
+			max_status_failed,
+			min_memory_free,
+			min_memory_free_pc,
+			max_memory_usage,
+			max_connection,
+			max_evi_increased,
+			max_qps,
+			mail_list,
+			disabled
+		from rediscfg where disabled=? order by id`
+	}
+	result, err := m.db.Query(sql, disabledInt)
+	if err != nil {
+		return nil, err
+	}
+	rows := make([]*RedisCfgRow, 0)
+	for result.Next() {
+		row := RedisCfgRow{}
+		err = result.Scan(
+			&row.Id,
+			&row.Address,
+			&row.Remark,
+			&row.Password,
+			&row.MaxConnectWait,
+			&row.MaxStatusFailed,
+			&row.MinMemoryFree,
+			&row.MinMemoryFreePC,
+			&row.MaxMemoryUsage,
+			&row.MaxConnection,
+			&row.MaxEviIncreased,
+			&row.MaxQPS,
+			&row.MailList,
+			&row.Disabled,
+		)
+		if err != nil {
+			continue
+		}
+		rows = append(rows, &row)
+	}
+	return rows, nil
+}

+ 137 - 0
src/cnphper.com/model/syscfg.go

@@ -0,0 +1,137 @@
+package model
+
+import (
+	"database/sql"
+	"errors"
+)
+
+type SysCfg struct {
+	Model
+}
+
+type SysCfgRow struct {
+	Id       int64
+	CfgKey   string
+	CfgValue string
+}
+
+func NewSysCfg(db *sql.DB) *SysCfg {
+	return &SysCfg{
+		Model{
+			db: db,
+		},
+	}
+}
+
+func (m *SysCfg) Insert(key, value string) (int64, error) {
+	if key == "" {
+		return 0, errors.New("KEY不能为空")
+	}
+	ret, err := m.db.Exec("insert into syscfg(cfg_key,cfg_value) values(?,?)", key, value)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.LastInsertId()
+	}
+}
+
+func (m *SysCfg) Delete(id int64) (int64, error) {
+	if id <= 0 {
+		return 0, errors.New("ID必须是正整数")
+	}
+	ret, err := m.db.Exec("delete from syscfg where id=?", id)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *SysCfg) DeleteByKey(key string) (int64, error) {
+	if key == "" {
+		return 0, errors.New("KEY不能为空")
+	}
+	ret, err := m.db.Exec("delete from syscfg where cfg_key=?", key)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *SysCfg) Update(id int64, value string) (int64, error) {
+	if id <= 0 {
+		return 0, errors.New("ID必须是正整数")
+	}
+	ret, err := m.db.Exec("update syscfg set cfg_value=? where id=?", value, id)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *SysCfg) UpdateByKey(key, value string) (int64, error) {
+	if key == "" {
+		return 0, errors.New("KEY不能为空")
+	}
+	ret, err := m.db.Exec("update syscfg set cfg_value=? where cfg_key=?", value, key)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *SysCfg) Get(id int64) (*SysCfgRow, error) {
+	if id <= 0 {
+		return nil, errors.New("ID必须是正整数")
+	}
+	result := m.db.QueryRow("select id,cfg_key,cfg_value from syscfg where id=?", id)
+	row := SysCfgRow{}
+	err := result.Scan(&row.Id, &row.CfgKey, &row.CfgValue)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, errors.New("记录不存在")
+		} else {
+			return nil, err
+		}
+	} else {
+		return &row, nil
+	}
+}
+
+func (m *SysCfg) GetByKey(key string) (*SysCfgRow, error) {
+	if key == "" {
+		return nil, errors.New("KEY不能为空")
+	}
+	result := m.db.QueryRow("select id,cfg_key,cfg_value from syscfg where cfg_key=?", key)
+	row := SysCfgRow{}
+	err := result.Scan(&row.Id, &row.CfgKey, &row.CfgValue)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, errors.New("记录不存在")
+		} else {
+			return nil, err
+		}
+	} else {
+		return &row, nil
+	}
+}
+
+func (m *SysCfg) GetAll() ([]*SysCfgRow, error) {
+	result, err := m.db.Query("select id,cfg_key,cfg_value from syscfg order by id")
+	if err != nil {
+		return nil, err
+	}
+	rows := make([]*SysCfgRow, 0)
+	for result.Next() {
+		row := SysCfgRow{}
+		err = result.Scan(&row.Id, &row.CfgKey, &row.CfgValue)
+		if err != nil {
+			continue
+		}
+		rows = append(rows, &row)
+	}
+	return rows, nil
+}

+ 37 - 0
src/cnphper.com/redisdog/common.go

@@ -0,0 +1,37 @@
+package main
+
+import (
+	"net/http"
+)
+
+type ErrorRet struct {
+	Errno int    `json:"errno"`
+	Error string `json:"error"`
+}
+
+func getSession(req *http.Request) (*Session, bool) {
+	var sessid string
+	cookies := req.Cookies()
+	if len(cookies) > 0 {
+		for _, item := range cookies {
+			if item.Name == "sessid" && item.Value != "" {
+				sessid = item.Value
+				break
+			}
+		}
+	}
+	if sessid == "" {
+		return nil, false
+	} else {
+		return SessPoll.Get(sessid)
+	}
+}
+
+func checkLogin(resp http.ResponseWriter, req *http.Request) (*Session, bool) {
+	sess, ok := getSession(req)
+	if !ok {
+		resp.Header().Set("Location", "/login")
+		resp.WriteHeader(302)
+	}
+	return sess, ok
+}

+ 173 - 0
src/cnphper.com/redisdog/index.go

@@ -0,0 +1,173 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"cnphper.com/model"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type RedisInfo struct {
+	Id      int64
+	Address string
+	Remark  string
+	Error   string
+	Data    map[string]string
+}
+
+type StatRet struct {
+	Errno int          `json:"errno"`
+	Error string       `json:"error"`
+	Data  []*RedisInfo `json:"data"`
+}
+
+type InfoRet struct {
+	Errno int        `json:"errno"`
+	Error string     `json:"error"`
+	Data  *RedisInfo `json:"data"`
+}
+
+func parseRedisInfo(info string) map[string]string {
+	stats := make(map[string]string, 1024)
+	lines := strings.Split(info, "\r\n")
+	num := len(lines)
+	for i := 0; i < num; i++ {
+		line := strings.TrimSpace(lines[i])
+		if line == "" || line[0] == '#' {
+			continue
+		}
+		parts := strings.SplitN(line, ":", 2)
+		if len(parts) != 2 {
+			continue
+		}
+		stats[parts[0]] = parts[1]
+	}
+	return stats
+}
+
+func queryRedisInfo(item *model.RedisCfgRow) *RedisInfo {
+	info := RedisInfo{
+		Id:      item.Id,
+		Address: item.Address,
+		Remark:  item.Remark,
+	}
+	timeout := time.Second * time.Duration(item.MaxConnectWait)
+	options := []redis.DialOption{
+		redis.DialConnectTimeout(timeout),
+		redis.DialReadTimeout(timeout),
+		redis.DialWriteTimeout(timeout),
+	}
+	if item.Password != "" {
+		options = append(options, redis.DialPassword(item.Password))
+	}
+	conn, err := redis.Dial("tcp", item.Address, options...)
+	defer conn.Close()
+	if err == nil {
+		str, err := redis.String(conn.Do("info"))
+		if err == nil {
+			info.Data = parseRedisInfo(str)
+		} else {
+			info.Error = err.Error()
+		}
+	} else {
+		info.Error = err.Error()
+	}
+	return &info
+}
+
+func index_default(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "index.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("index.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		resp.WriteHeader(500)
+		io.WriteString(resp, err.Error())
+		return
+	}
+	tmpl.Execute(resp, struct {
+		Sess  *Session
+		Req   *http.Request
+		Title string
+	}{
+		sess,
+		req,
+		"首页",
+	})
+}
+
+func index_stats(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	rows, err := mdlRedisCfg.GetAll(0)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	stats := make([]*RedisInfo, 0)
+	for _, row := range rows {
+		stats = append(stats, queryRedisInfo(row))
+	}
+	json, _ := json.Marshal(StatRet{Errno: 0, Error: "", Data: stats})
+	resp.Write(json)
+}
+
+func index_info(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	idstr := req.Form.Get("id")
+	if idstr == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "缺少ID字段!"})
+		resp.Write(json)
+		return
+	}
+	id := int(0)
+	_, err := fmt.Sscanf(idstr, "%d", &id)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "ID的值不是整数!"})
+		resp.Write(json)
+		return
+	}
+	if id <= 0 {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "ID的值必须大于0!"})
+		resp.Write(json)
+		return
+	}
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	row, err := mdlRedisCfg.Get(int64(id))
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: err.Error()})
+		resp.Write(json)
+	} else {
+		info := queryRedisInfo(row)
+		if info.Error == "" {
+			json, _ := json.Marshal(InfoRet{Errno: 0, Error: "", Data: info})
+			resp.Write(json)
+		} else {
+			json, _ := json.Marshal(ErrorRet{Errno: 5, Error: info.Error})
+			resp.Write(json)
+		}
+	}
+}

+ 43 - 0
src/cnphper.com/redisdog/log_account.go

@@ -0,0 +1,43 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+)
+
+func log_account(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "log", "account.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("account.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"用户操作日志",
+		})
+	}
+}
+
+func log_account_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}

+ 43 - 0
src/cnphper.com/redisdog/log_autoprocess.go

@@ -0,0 +1,43 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+)
+
+func log_autoprocess(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "log", "autoprocess.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("autoprocess.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"Redis扩容日志",
+		})
+	}
+}
+
+func log_autoprocess_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}

+ 43 - 0
src/cnphper.com/redisdog/log_syslog.go

@@ -0,0 +1,43 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+)
+
+func log_syslog(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "log", "syslog.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("syslog.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"系统错误日志",
+		})
+	}
+}
+
+func log_syslog_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}

+ 43 - 0
src/cnphper.com/redisdog/log_warn.go

@@ -0,0 +1,43 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+)
+
+func log_warn(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "log", "warn.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("warn.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"Redis报警日志",
+		})
+	}
+}
+
+func log_warn_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}

+ 63 - 0
src/cnphper.com/redisdog/login.go

@@ -0,0 +1,63 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+
+	"cnphper.com/model"
+)
+
+type LoginRet struct {
+	Errno int     `json:"errno"`
+	Error string  `json:"error"`
+	Data  Session `json:"data"`
+}
+
+func login_index(resp http.ResponseWriter, req *http.Request) {
+	if req.Method == "GET" {
+		//视图输出
+		tmpl, err := template.ParseFiles(filepath.Join(Cfg.TmplDir, "login.tmpl"))
+		if err != nil {
+			io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+		} else {
+			tmpl.Execute(resp, req)
+		}
+	} else if req.Method == "POST" {
+		req.ParseForm()
+		account := req.PostForm.Get("account")
+		passwd := req.PostForm.Get("passwd")
+		if account == "" {
+			json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "用户名不能为空!"})
+			resp.Write(json)
+		} else if passwd == "" {
+			json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "密码不能为空!"})
+			resp.Write(json)
+		} else {
+			mdlAccounts := model.NewAccounts(Db)
+			row, err := mdlAccounts.Login(account, passwd)
+			if err != nil {
+				json, _ := json.Marshal(ErrorRet{Errno: 3, Error: err.Error()})
+				resp.Write(json)
+			} else {
+				sessid := makeSessionId(req.RemoteAddr)
+				sess := Session{
+					Sess:    sessid,
+					Account: row,
+				}
+				SessPoll.Set(sessid, &sess)
+				cookie := &http.Cookie{
+					Name:  "sessid",
+					Value: sessid,
+					Path:  "/",
+				}
+				resp.Header().Add("Set-Cookie", cookie.String())
+				json, _ := json.Marshal(LoginRet{Errno: 0, Error: "", Data: sess})
+				resp.Write(json)
+			}
+		}
+	}
+}

+ 72 - 0
src/cnphper.com/redisdog/mailsender.go

@@ -0,0 +1,72 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/smtp"
+	"time"
+)
+
+type MailRequest struct {
+	sendto  []string
+	title   string
+	content string
+}
+
+type MailSender struct {
+	queue chan *MailRequest
+}
+
+func NewMailSender() *MailSender {
+	return &MailSender{
+		queue: make(chan *MailRequest, 100),
+	}
+}
+
+func (s *MailSender) Loop() {
+	timer := time.NewTimer(time.Second * 3)
+	timeouted := false
+	for {
+		list := make([]*MailRequest, 0)
+		//一次取10条记录
+		for i := 0; i < 10; i++ {
+			timer.Reset()
+			select {
+			case req, ok = <-s.queue:
+				timer.Stop()
+				if ok {
+					list = append(list, req)
+				}
+			case <-timer.C:
+				timeouted = true
+			}
+			//如果不足10条也返回
+			if timeouted {
+				break
+			}
+		}
+		//发送邮件
+		if len(list) > 0 {
+			//取SMTP配置
+			cfg := SysSetting.GetMulti([]string{"smtp_host", "smtp_port", "smtp_user", "smtp_pwd", "smtp_sender"})
+			auth := smtp.PlainAuth("", cfg["smtp_user"], cfg["smtp_pwd"], cfg["smtp_host"])
+			address := fmt.Sprintf("%s:%d", cfg["smtp_port"], cfg["smtp_port"])
+			//循环发送
+			for _, req := range list {
+				err := smtp.SendMail(address, auth, cfg["smtp_sender"], req.sendto, []byte(req.content))
+				if err != nil {
+					log.Printf("SMTP send mail failed: %s\n", err.Error())
+				}
+			}
+		}
+	}
+}
+
+func (s *MailSender) Push(req *MailRequest) {
+	timer := time.NewTimer(time.Second * 3)
+	select {
+	case s.queue <- req:
+		timer.Stop()
+	case <-timer.C:
+	}
+}

+ 221 - 0
src/cnphper.com/redisdog/main.go

@@ -0,0 +1,221 @@
+package main
+
+import (
+	"database/sql"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"html/template"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"strings"
+	"syscall"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+type Config struct {
+	Host         string
+	Port         string
+	Database     string
+	RootPwd      string
+	LogDir       string
+	TmplDir      string
+	ResourcesDir string
+	SessionTTL   int64
+}
+
+var (
+	Cfg         *Config
+	Db          *sql.DB
+	SessPoll    *SessionPoll
+	TmplFuncMap template.FuncMap
+	Srv         *http.Server
+	Sender      *MailSender
+	SysSetting  Setting
+)
+
+func loadConfig() *Config {
+	var (
+		cfgfile string
+		cfg     *Config = &Config{}
+	)
+	flag.StringVar(&cfgfile, "c", "", "config file")
+	flag.Parse()
+	if cfgfile == "" {
+		log.Fatalf("Usage: %s -c <config file>\n", os.Args[0])
+	}
+
+	file, err := os.OpenFile(cfgfile, os.O_RDONLY, 0644)
+	if err != nil {
+		log.Fatal(err)
+	}
+	decoder := json.NewDecoder(file)
+	if err = decoder.Decode(cfg); err != nil {
+		log.Fatal(err)
+	}
+	if cfg.Port == "" {
+		log.Fatalf("Port is empty!\n")
+	}
+	if cfg.Database == "" {
+		log.Fatalf("Database is empty!\n")
+	}
+	if cfg.RootPwd == "" {
+		log.Fatalf("RootPwd is empty!\n")
+	}
+	if cfg.LogDir == "" {
+		log.Fatalf("LogDir is empty!\n")
+	}
+	if cfg.TmplDir == "" {
+		log.Fatalf("TmplDir is empty!\n")
+	}
+	if cfg.ResourcesDir == "" {
+		log.Fatalf("ResourcesDir is empty!\n")
+	}
+	if cfg.SessionTTL == 0 {
+		cfg.SessionTTL = 900
+	}
+	return cfg
+}
+
+func waitSignal() {
+	ch := make(chan os.Signal, 1)
+	signal.Notify(ch, syscall.SIGUSR1)
+	signal.Notify(ch, syscall.SIGQUIT)
+	for {
+		sig := <-ch
+		switch sig {
+		//重启
+		case syscall.SIGUSR1:
+			close(ch)
+			if Srv != nil {
+				Srv.Close()
+			}
+			syscall.Exec(os.Args[0], os.Args[1:], nil)
+		//退出
+		case syscall.SIGQUIT:
+			close(ch)
+			if Srv != nil {
+				Srv.Close()
+			}
+			os.Exit(0)
+		}
+	}
+}
+
+func openDatabase(dbfile string) *sql.DB {
+	db, err := sql.Open("sqlite3", dbfile)
+	if err != nil {
+		log.Fatalf("sql.Open() Error: %s\n", err.Error())
+	}
+
+	db.Exec(Sql_Table_Syscfg)
+	db.Exec(Sql_Table_Accounts)
+	db.Exec(Sql_Table_Rediscfg)
+	db.Exec(Sql_Table_Syslog)
+	db.Exec(Sql_Index_Syslog)
+	db.Exec(Sql_Table_Statuslog)
+	db.Exec(Sql_Table_Warnlog)
+	db.Exec(Sql_Table_Processlog)
+	db.Exec(Sql_Values_Accounts)
+
+	return db
+}
+
+func loadSystemSetting() map[string]string {
+
+}
+
+func start_http_server() *http.Server {
+	handle := http.NewServeMux()
+	//登录
+	handle.HandleFunc("/login", login_index)
+	//首页
+	handle.HandleFunc("/", index_default)
+	handle.HandleFunc("/index/stats", index_stats)
+	handle.HandleFunc("/index/info", index_info)
+	//用户操作
+	handle.HandleFunc("/profile/logout", profile_logout)
+	handle.HandleFunc("/profile/passwd", profile_passwd)
+	//配置-redis
+	handle.HandleFunc("/syscfg/redis", syscfg_redis)
+	handle.HandleFunc("/syscfg/redis_list", syscfg_redis_list)
+	handle.HandleFunc("/syscfg/redis_get", syscfg_redis_get)
+	handle.HandleFunc("/syscfg/redis_set", syscfg_redis_set)
+	handle.HandleFunc("/syscfg/redis_del", syscfg_redis_del)
+	//配置-报警
+	handle.HandleFunc("/syscfg/warn", syscfg_warn)
+	handle.HandleFunc("/syscfg/warn_get", syscfg_warn_get)
+	handle.HandleFunc("/syscfg/warn_set", syscfg_warn_set)
+	//配置-账号
+	handle.HandleFunc("/syscfg/account", syscfg_account)
+	handle.HandleFunc("/syscfg/account_list", syscfg_account_list)
+	handle.HandleFunc("/syscfg/account_get", syscfg_account_get)
+	handle.HandleFunc("/syscfg/account_set", syscfg_account_set)
+	handle.HandleFunc("/syscfg/account_del", syscfg_account_del)
+	handle.HandleFunc("/syscfg/account_reset_pwd", syscfg_account_reset_pwd)
+	//配置-杂项
+	handle.HandleFunc("/syscfg/misc", syscfg_misc)
+	handle.HandleFunc("/syscfg/misc_get", syscfg_misc_get)
+	handle.HandleFunc("/syscfg/misc_set", syscfg_misc_set)
+	//日志-报警
+	handle.HandleFunc("/log/warn", log_warn)
+	handle.HandleFunc("/log/warn_list", log_warn_list)
+	//日志-扩容
+	handle.HandleFunc("/log/autoprocess", log_autoprocess)
+	handle.HandleFunc("/log/autoprocess_list", log_autoprocess_list)
+	//日志-系统错误
+	handle.HandleFunc("/log/syslog", log_syslog)
+	handle.HandleFunc("/log/syslog_list", log_syslog_list)
+	//日志-后台操作
+	handle.HandleFunc("/log/account", log_account)
+	handle.HandleFunc("/log/account_list", log_account_list)
+	//资源文件
+	handle.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir(Cfg.ResourcesDir))))
+	//启动HTTP服务
+	srv := &http.Server{
+		Addr:    fmt.Sprintf("%s:%s", Cfg.Host, Cfg.Port),
+		Handler: handle,
+	}
+	go func(s *http.Server) {
+		if err := s.ListenAndServe(); err != nil {
+			log.Fatal(err)
+		}
+	}(srv)
+	return srv
+}
+
+func main() {
+	//加载基础配置
+	Cfg = loadConfig()
+
+	//连接DB
+	Db = openDatabase(Cfg.Database)
+	defer Db.Close()
+
+	//创建HTTP会话池
+	SessPoll = NewSessionPoll(Cfg.SessionTTL)
+
+	//添加HTML模板函数
+	TmplFuncMap = make(template.FuncMap)
+	TmplFuncMap["has_prefix"] = strings.HasPrefix
+
+	//从DB加载业务配置
+	SysSetting = NewSetting()
+	SysSetting.Load()
+
+	//开始HTTP服务
+	Srv = start_http_server()
+
+	//开始监控Redis状态
+	go monitor_loop()
+
+	//开始处理邮件发送队列
+	Sender = NewMailSender()
+	go Sender.Loop()
+
+	//监听信号
+	waitSignal()
+}

+ 42 - 0
src/cnphper.com/redisdog/monitor.go

@@ -0,0 +1,42 @@
+package main
+
+import (
+	"strconv"
+	"time"
+
+	"cnphper.com/model"
+)
+
+func monitor_loop() {
+	delay := 30
+	for {
+		if str, ok := SysSetting.Get("misc_check_delay"); ok {
+			if num, err := strconv.Atoi(str); err == nil {
+				delay = num
+			}
+		}
+		time.Sleep(time.Second * time.Duration(delay))
+		monitor()
+	}
+}
+
+func monitor() {
+	mdl := model.NewRedisCfg(Db)
+	rows, err := mdl.GetAll(0)
+	if err != nil {
+		return
+	}
+	for _, row := range rows {
+		info := queryRedisInfo(row)
+		if info.Error != "" {
+			continue
+		}
+		//检查Redis状态(分配内容空间、使用内存空间、使用内存占比、连接数、淘汰记录数、QPS)
+
+		//如果状态异常检查是否连接出错次数达到上限
+
+		//判断是否需要自动扩容
+
+		//添加任务到邮件发送队列
+	}
+}

+ 90 - 0
src/cnphper.com/redisdog/profile.go

@@ -0,0 +1,90 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	"cnphper.com/model"
+)
+
+func profile_logout(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if ok {
+		SessPoll.Del(sess.Sess)
+	}
+	resp.Header().Set("Location", "/login")
+	resp.WriteHeader(302)
+}
+
+func profile_passwd(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	if req.Method == "GET" {
+		//视图输出
+		files := []string{
+			filepath.Join(Cfg.TmplDir, "profile", "passwd.tmpl"),
+			filepath.Join(Cfg.TmplDir, "header.tmpl"),
+			filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+		}
+		tmpl, err := template.New("passwd.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+		if err != nil {
+			io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+		} else {
+			tmpl.Execute(resp, struct {
+				Sess  *Session
+				Req   *http.Request
+				Title string
+			}{
+				sess,
+				req,
+				"修改密码",
+			})
+		}
+	} else {
+		req.ParseForm()
+		oldpwd := strings.TrimSpace(req.PostForm.Get("oldpwd"))
+		newpwd := strings.TrimSpace(req.PostForm.Get("newpwd"))
+		if oldpwd == "" {
+			json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "旧密码不能为空!"})
+			resp.Write(json)
+		} else if len(oldpwd) < 6 {
+			json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "旧密码格式不正确空!"})
+			resp.Write(json)
+		} else if newpwd == "" {
+			json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "新密码不能为空!"})
+			resp.Write(json)
+		} else if len(newpwd) < 6 {
+			json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "旧密码格式不正确空!"})
+			resp.Write(json)
+		} else if oldpwd == newpwd {
+			json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "新密码不能与旧密码相同!"})
+			resp.Write(json)
+		} else {
+			mdlAccounts := model.NewAccounts(Db)
+			row, err := mdlAccounts.Login(sess.Account.Account, oldpwd)
+			if err != nil {
+				json, _ := json.Marshal(ErrorRet{Errno: 6, Error: "旧密码不正确!"})
+				resp.Write(json)
+			}
+			affected, err := mdlAccounts.UpdatePassword(row.Id, newpwd)
+			if err != nil {
+				json, _ := json.Marshal(ErrorRet{Errno: 7, Error: err.Error()})
+				resp.Write(json)
+			}
+			if affected > 0 {
+				json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+				resp.Write(json)
+			} else {
+				json, _ := json.Marshal(ErrorRet{Errno: 8, Error: "未知错误!"})
+				resp.Write(json)
+			}
+		}
+	}
+}

+ 81 - 0
src/cnphper.com/redisdog/session.go

@@ -0,0 +1,81 @@
+package main
+
+import (
+	"crypto/md5"
+	"fmt"
+	"math/rand"
+	"time"
+
+	"cnphper.com/model"
+)
+
+type Session struct {
+	Sess    string             `json:"sess"`
+	Expire  int64              `json:"expire"`
+	Account *model.AccountsRow `json:"account"`
+}
+
+type SessionPoll struct {
+	poll map[string]*Session
+	lock chan int
+	ttl  int64
+}
+
+func NewSessionPoll(ttl int64) *SessionPoll {
+	p := &SessionPoll{
+		poll: make(map[string]*Session, 1024),
+		lock: make(chan int, 1),
+		ttl:  ttl,
+	}
+	go p.timer()
+	return p
+}
+
+func (p *SessionPoll) timer() {
+	for {
+		time.Sleep(time.Minute * 1)
+		ts := time.Now().Unix()
+		expired := make([]string, len(p.poll))
+		cnt := 0
+		p.lock <- 1
+		for id, s := range p.poll {
+			if s.Expire <= ts {
+				expired[cnt] = id
+				cnt++
+			}
+		}
+		for _, id := range expired {
+			delete(p.poll, id)
+		}
+		<-p.lock
+	}
+}
+
+func (p *SessionPoll) Get(sessid string) (*Session, bool) {
+	p.lock <- 1
+	s, ok := p.poll[sessid]
+	if ok {
+		p.poll[sessid].Expire = time.Now().Unix() + p.ttl
+	}
+	<-p.lock
+	return s, ok
+}
+
+func (p *SessionPoll) Set(sessid string, sess *Session) {
+	if sess.Expire == 0 {
+		sess.Expire = time.Now().Unix() + p.ttl
+	}
+	p.lock <- 1
+	p.poll[sessid] = sess
+	<-p.lock
+}
+
+func (p *SessionPoll) Del(sessid string) {
+	p.lock <- 1
+	delete(p.poll, sessid)
+	<-p.lock
+}
+
+func makeSessionId(cliAddr string) string {
+	return fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("sess|%s|%d|d", cliAddr, time.Now().Unix(), rand.Int63()))))
+}

+ 69 - 0
src/cnphper.com/redisdog/setting.go

@@ -0,0 +1,69 @@
+package main
+
+import (
+	"cnphper.com/model"
+)
+
+type Setting struct {
+	lock chan int
+	data map[string]string
+}
+
+func NewSetting() *Setting {
+	return &Setting{
+		lock: make(chan int, 1),
+		data: make(map[string]string),
+	}
+}
+
+func (s *Setting) Load() error {
+	mdl := model.NewSysCfg(Db)
+	values, err := mdl.GetAll()
+	if err != nil {
+		return err
+	}
+	s.lock <- 1
+	for k, v := range values {
+		s.data[k] = v
+	}
+	<-s.lock
+	return nil
+}
+
+func (s *Setting) Get(k string) (string, bool) {
+	s.lock <- 1
+	v, ok := s.data[k]
+	<-s.lock
+	return v, ok
+}
+
+func (s *Setting) Set(k, v string) {
+	s.lock <- 1
+	s.data[k] = v
+	<-s.lock
+}
+
+func (s *Setting) GetMulti(keys []string) map[string]string {
+	s.lock <- 1
+	cpy := make(map[string]string)
+	for _, key := range keys {
+		v, ok := s.data[key]
+		if ok {
+			cpy[key] = v
+		} else {
+			cpy[key] = ""
+		}
+	}
+	<-s.lock
+	return cpy
+}
+
+func (s *Setting) GetAll() map[string]string {
+	s.lock <- 1
+	cpy := make(map[string]string)
+	for k, v := range s.data {
+		cpy[k] = v
+	}
+	<-s.lock
+	return cpy
+}

+ 87 - 0
src/cnphper.com/redisdog/sql.go

@@ -0,0 +1,87 @@
+package main
+
+var (
+	Sql_Table_Syscfg = `
+CREATE TABLE IF NOT EXISTS syscfg (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  cfg_key TEXT UNIQUE,
+  cfg_value TEXT NOT NULL
+)`
+	Sql_Table_Accounts = `
+CREATE TABLE IF NOT EXISTS accounts (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  account TEXT UNIQUE,
+  name TEXT NOT NULL,
+  password TEXT NOT NULL,
+  last_login INTEGER NOT NULL DEFAULT 0,
+  is_super INTEGER NOT NULL DEFAULT 0,
+  disabled INTEGER NOT NULL DEFAULT 0
+)`
+	Sql_Table_Rediscfg = `
+CREATE TABLE IF NOT EXISTS rediscfg (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  address TEXT UNIQUE,
+  remark TEXT NOT NULL DEFAULT '',
+  password TEXT NOT NULL DEFAULT '',
+  max_connect_wait INTEGER NOT NULL DEFAULT 5,
+  max_status_failed INTEGER NOT NULL DEFAULT 1,
+  min_memory_free INTEGER NOT NULL DEFAULT 0,
+  min_memory_free_pc INTEGER NOT NULL DEFAULT 0,
+  max_memory_usage INTEGER NOT NULL DEFAULT 16000000000,
+  max_connection INTEGER NOT NULL DEFAULT 1000,
+  max_evi_increased INTEGER NOT NULL DEFAULT 1,
+  max_qps INTEGER NOT NULL DEFAULT 50000,
+  mail_list TEXT NOT NULL DEFAULT '',
+  disabled INTEGER NOT NULL DEFAULT 0
+)`
+	Sql_Table_Syslog = `
+CREATE TABLE IF NOT EXISTS syslog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL,
+  log_time INTEGER NOT NULL,
+  log_msg TEXT COMMENT NOT NULL
+)`
+	Sql_Index_Syslog    = `CREATE INDEX IF NOT EXISTS idx_account ON syslog(account_id)`
+	Sql_Table_Statuslog = `
+CREATE TABLE IF NOT EXISTS statuslog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  redis_id INTEGER NOT NULL,
+  check_time INTEGER NOT NULL,
+  status INTEGER NOT NULL DEFAULT 0,
+  info TEXT NOT NULL DEFAULT '',
+  memory_usage INTEGER NOT NULL DEFAULT 0,
+  memory_usage_pc INTEGER NOT NULL DEFAULT 0,
+  connection INTEGER NOT NULL DEFAULT 0,
+  qps INTEGER NOT NULL DEFAULT 0,
+  evi_increased INTEGER NOT NULL DEFAULT 0,
+  UNIQUE (redis_id, check_time)
+)`
+	Sql_Table_Warnlog = `
+CREATE TABLE IF NOT EXISTS warnlog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  redis_id INTEGER NOT NULL,
+  warn_time INTEGER NOT NULL,
+  warn_msg TEXT NOT NULL,
+  warn_status INTEGER NOT NULL DEFAULT 0,
+  UNIQUE (redis_id, warn_time)
+)`
+	Sql_Table_Processlog = `
+CREATE TABLE IF NOT EXISTS processlog (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  redis_id INTEGER NOT NULL,
+  process_time INTEGER NOT NULL,
+  process_info TEXT NOT NULL DEFAULT '',
+  process_status INTEGER NOT NULL DEFAULT 0,
+  UNIQUE (redis_id, process_time)
+)`
+	Sql_Values_Accounts = `INSERT OR IGNORE INTO accounts VALUES(NULL,'root','超管','bfa7db1d229563267ef3e0caf6712157',0,1,0)`
+	Sql_Values_Syscfg   = `
+INSERT OR IGNORE INTO syscfg VALUES
+(NULL,'smtp_host','smtp.163.com'),
+(NULL,'smtp_port','25'),
+(NULL,'smtp_user',''),
+(NULL,'smtp_pwd',''),
+(NULL,'smtp_sender',''),
+(NULL,'misc_check_delay','30'),
+`
+)

+ 271 - 0
src/cnphper.com/redisdog/syscfg_account.go

@@ -0,0 +1,271 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"regexp"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type SyscfgAccountListRet struct {
+	Errno int                  `json:"errno"`
+	Error string               `json:"error"`
+	Data  []*model.AccountsRow `json:"data"`
+}
+
+type SyscfgAccountGetRet struct {
+	Errno int                `json:"errno"`
+	Error string             `json:"error"`
+	Data  *model.AccountsRow `json:"data"`
+}
+
+func syscfg_account(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "syscfg", "account.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("account.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"账号管理",
+		})
+	}
+}
+
+func syscfg_account_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	mdlAccounts := model.NewAccounts(Db)
+	list, err := mdlAccounts.GetAll()
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: err.Error()})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(SyscfgAccountListRet{Errno: 0, Error: "", Data: list})
+		resp.Write(json)
+	}
+}
+
+func syscfg_account_get(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	idStr := req.Form.Get("id")
+	if idStr == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
+		resp.Write(json)
+		return
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	mdlAccounts := model.NewAccounts(Db)
+	item, err := mdlAccounts.Get(int64(id))
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: err.Error()})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(SyscfgAccountGetRet{Errno: 0, Error: "", Data: item})
+		resp.Write(json)
+	}
+}
+
+func syscfg_account_set(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	Id := req.PostForm.Get("Id")
+	Account := req.PostForm.Get("Account")
+	Name := req.PostForm.Get("Name")
+	Password := req.PostForm.Get("Password")
+	IsSuper := req.PostForm.Get("IsSuper")
+	Disabled := req.PostForm.Get("Disabled")
+	IdInt, err := strconv.Atoi(Id)
+	if err != nil {
+		IdInt = 0
+	}
+	if Account == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "账号不能为空!"})
+		resp.Write(json)
+		return
+	}
+	reg := regexp.MustCompile(`^[A-Za-z]\w{1,19}$`)
+	if !reg.MatchString(Account) {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "账号格式不正确!"})
+		resp.Write(json)
+		return
+	}
+	if Name == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "姓名不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if IdInt == 0 {
+		if Password == "" {
+			json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "密码不能为空!"})
+			resp.Write(json)
+			return
+		}
+		if len(Password) < 6 {
+			json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "密码长度不能小于6!"})
+			resp.Write(json)
+			return
+		}
+	}
+	if IsSuper == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 6, Error: "请选择是否管理员!"})
+		resp.Write(json)
+		return
+	}
+	if Disabled == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 7, Error: "状态不能为空!"})
+		resp.Write(json)
+		return
+	}
+	IsSuperInt, err := strconv.Atoi(IsSuper)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 8, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	DisabledInt, err := strconv.Atoi(Disabled)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 9, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	newRow := model.AccountsRow{
+		Id:       int64(IdInt),
+		Account:  Account,
+		Name:     Name,
+		Password: Password,
+		IsSuper:  IsSuperInt != 0,
+		Disabled: DisabledInt != 0,
+	}
+	mdlAccounts := model.NewAccounts(Db)
+	if IdInt > 0 {
+		affected, err := mdlAccounts.Update(&newRow)
+		if err != nil {
+			json, _ := json.Marshal(ErrorRet{Errno: 10, Error: err.Error()})
+			resp.Write(json)
+		} else if affected > 0 {
+			json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+			resp.Write(json)
+		} else {
+			json, _ := json.Marshal(ErrorRet{Errno: 11, Error: "更新失败!"})
+			resp.Write(json)
+		}
+	} else {
+		newRowId, err := mdlAccounts.Insert(&newRow)
+		if err != nil {
+			json, _ := json.Marshal(ErrorRet{Errno: 22, Error: err.Error()})
+			resp.Write(json)
+		} else if newRowId > 0 {
+			json, _ := json.Marshal(SyscfgRedisAddRet{Errno: 0, Error: "", Data: newRowId})
+			resp.Write(json)
+		} else {
+			json, _ := json.Marshal(ErrorRet{Errno: 23, Error: "新增失败!"})
+			resp.Write(json)
+		}
+	}
+}
+
+func syscfg_account_del(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	idStr := req.Form.Get("id")
+	if idStr == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
+		resp.Write(json)
+		return
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	mdlAccounts := model.NewAccounts(Db)
+	affected, err := mdlAccounts.Delete(int64(id))
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: err.Error()})
+		resp.Write(json)
+	} else if affected > 0 {
+		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "操作失败!"})
+		resp.Write(json)
+	}
+}
+
+func syscfg_account_reset_pwd(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	idStr := req.PostForm.Get("id")
+	if idStr == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
+		resp.Write(json)
+		return
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	password := req.PostForm.Get("password")
+	if len(password) < 6 {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "密码长度不能小于6位!"})
+		resp.Write(json)
+		return
+	}
+	mdlAccounts := model.NewAccounts(Db)
+	affected, err := mdlAccounts.UpdatePassword(int64(id), password)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: err.Error()})
+		resp.Write(json)
+	} else if affected > 0 {
+		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "操作失败!"})
+		resp.Write(json)
+	}
+}

+ 50 - 0
src/cnphper.com/redisdog/syscfg_misc.go

@@ -0,0 +1,50 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+)
+
+func syscfg_misc(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "syscfg", "misc.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("misc.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"其它配置",
+		})
+	}
+}
+
+func syscfg_misc_get(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}
+
+func syscfg_misc_set(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}

+ 318 - 0
src/cnphper.com/redisdog/syscfg_redis.go

@@ -0,0 +1,318 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type SyscfgRedisListRet struct {
+	Errno int                  `json:"errno"`
+	Error string               `json:"error"`
+	Data  []*model.RedisCfgRow `json:"data"`
+}
+
+type SyscfgRedisGetRet struct {
+	Errno int                `json:"errno"`
+	Error string             `json:"error"`
+	Data  *model.RedisCfgRow `json:"data"`
+}
+
+type SyscfgRedisAddRet struct {
+	Errno int    `json:"errno"`
+	Error string `json:"error"`
+	Data  int64  `json:"data"`
+}
+
+func syscfg_redis(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "syscfg", "redis.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("redis.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"Redis列表管理",
+		})
+	}
+}
+
+func syscfg_redis_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	rows, err := mdlRedisCfg.GetAll(-1)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: err.Error()})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(SyscfgRedisListRet{Errno: 0, Error: "", Data: rows})
+		resp.Write(json)
+	}
+}
+
+func syscfg_redis_get(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	idStr := req.Form.Get("id")
+	if idStr == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
+		resp.Write(json)
+		return
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	item, err := mdlRedisCfg.Get(int64(id))
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: err.Error()})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(SyscfgRedisGetRet{Errno: 0, Error: "", Data: item})
+		resp.Write(json)
+	}
+}
+
+func syscfg_redis_set(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	Id := req.PostForm.Get("Id")
+	Address := req.PostForm.Get("Address")
+	Remark := req.PostForm.Get("Remark")
+	Password := req.PostForm.Get("Password")
+	MaxConnectWait := req.PostForm.Get("MaxConnectWait")
+	MaxStatusFailed := req.PostForm.Get("MaxStatusFailed")
+	MinMemoryFree := req.PostForm.Get("MinMemoryFree")
+	MinMemoryFreePC := req.PostForm.Get("MinMemoryFreePC")
+	MaxMemoryUsage := req.PostForm.Get("MaxMemoryUsage")
+	MaxConnection := req.PostForm.Get("MaxConnection")
+	MaxEviIncreased := req.PostForm.Get("MaxEviIncreased")
+	MaxQPS := req.PostForm.Get("MaxQPS")
+	MailList := req.PostForm.Get("MailList")
+	Disabled := req.PostForm.Get("Disabled")
+	if Address == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "Redis服务地址不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if Remark == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "Redis备注名称不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MaxConnectWait == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "连接超时时长不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MaxStatusFailed == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "最大状态检测失败次数不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MinMemoryFree == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "最小可用内存空间不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MinMemoryFreePC == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 6, Error: "最小可用内存空间百分比不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MaxMemoryUsage == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 7, Error: "最大可用内存空间不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MaxConnection == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 8, Error: "最大并发连接数不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MaxEviIncreased == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 9, Error: "最大新增淘汰记录数不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MaxQPS == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 10, Error: "最大QPS值不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if MailList == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 11, Error: "报警邮件接收邮箱列表不能为空!"})
+		resp.Write(json)
+		return
+	}
+	if Disabled == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 12, Error: "状态不能为空!"})
+		resp.Write(json)
+		return
+	}
+	IdInt, err := strconv.Atoi(Id)
+	if err != nil {
+		IdInt = 0
+	}
+	MaxConnectWaitInt, err := strconv.Atoi(MaxConnectWait)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 13, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MaxStatusFailedInt, err := strconv.Atoi(MaxStatusFailed)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 14, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MinMemoryFreeInt, err := strconv.Atoi(MinMemoryFree)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 15, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MinMemoryFreePCInt, err := strconv.Atoi(MinMemoryFreePC)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 16, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MaxMemoryUsageInt, err := strconv.Atoi(MaxMemoryUsage)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 17, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MaxConnectionInt, err := strconv.Atoi(MaxConnection)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 18, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MaxEviIncreasedInt, err := strconv.Atoi(MaxEviIncreased)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 19, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	MaxQPSInt, err := strconv.Atoi(MaxQPS)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 20, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	DisabledInt, err := strconv.Atoi(Disabled)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 21, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	newRow := model.RedisCfgRow{
+		Id:              int64(IdInt),
+		Address:         Address,
+		Remark:          Remark,
+		Password:        Password,
+		MaxConnectWait:  int64(MaxConnectWaitInt),
+		MaxStatusFailed: int64(MaxStatusFailedInt),
+		MinMemoryFree:   int64(MinMemoryFreeInt),
+		MinMemoryFreePC: int64(MinMemoryFreePCInt),
+		MaxMemoryUsage:  int64(MaxMemoryUsageInt),
+		MaxConnection:   int64(MaxConnectionInt),
+		MaxEviIncreased: int64(MaxEviIncreasedInt),
+		MaxQPS:          int64(MaxQPSInt),
+		MailList:        MailList,
+		Disabled:        DisabledInt != 0,
+	}
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	if IdInt > 0 {
+		affected, err := mdlRedisCfg.Update(&newRow)
+		if err != nil {
+			json, _ := json.Marshal(ErrorRet{Errno: 22, Error: err.Error()})
+			resp.Write(json)
+		} else if affected > 0 {
+			json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+			resp.Write(json)
+		} else {
+			json, _ := json.Marshal(ErrorRet{Errno: 23, Error: "更新失败!"})
+			resp.Write(json)
+		}
+	} else {
+		newRowId, err := mdlRedisCfg.Insert(&newRow)
+		if err != nil {
+			json, _ := json.Marshal(ErrorRet{Errno: 22, Error: err.Error()})
+			resp.Write(json)
+		} else if newRowId > 0 {
+			json, _ := json.Marshal(SyscfgRedisAddRet{Errno: 0, Error: "", Data: newRowId})
+			resp.Write(json)
+		} else {
+			json, _ := json.Marshal(ErrorRet{Errno: 23, Error: "新增失败!"})
+			resp.Write(json)
+		}
+	}
+}
+
+func syscfg_redis_del(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	idStr := req.Form.Get("id")
+	if idStr == "" {
+		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
+		resp.Write(json)
+		return
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
+		resp.Write(json)
+		return
+	}
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	affected, err := mdlRedisCfg.Delete(int64(id))
+	if err != nil {
+		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: err.Error()})
+		resp.Write(json)
+	} else if affected > 0 {
+		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+		resp.Write(json)
+	} else {
+		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "操作失败!"})
+		resp.Write(json)
+	}
+}

+ 50 - 0
src/cnphper.com/redisdog/syscfg_warn.go

@@ -0,0 +1,50 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+)
+
+func syscfg_warn(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "syscfg", "warn.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("warn.tmpl").Funcs(TmplFuncMap).ParseFiles(files...)
+	if err != nil {
+		io.WriteString(resp, fmt.Sprintf("Error: %s\n", err.Error()))
+	} else {
+		tmpl.Execute(resp, struct {
+			Sess  *Session
+			Req   *http.Request
+			Title string
+		}{
+			sess,
+			req,
+			"报警配置",
+		})
+	}
+}
+
+func syscfg_warn_get(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}
+
+func syscfg_warn_set(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+}

+ 4 - 0
start.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+nohup bin/redisdog -c etc/config.json > log/stdout.txt 2>log/stderr.txt &
+

+ 3 - 0
stop.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+pkill redisdog

+ 193 - 0
tmpl/dump.tmpl

@@ -0,0 +1,193 @@
+<!doctype html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<title>Request Data Dump</title>
+<style type="text/css">
+body{
+	font-size: 14px;
+}
+h1{
+	text-align: center;
+	font-size: 1.5em;
+}
+hr{
+	border: none;
+	border-bottom: 1px solid #000000;
+}
+input{
+	width: 500px;
+}
+table{
+	width: 80%;
+	margin: 10px auto;
+	border-collapse: collapse;
+}
+th{
+	background-color: #660066;
+	color: #FFFFFF;
+}
+th, td{
+	padding: 3px 10px;
+	border: 1px solid #000000;
+}
+</style>
+<script type="text/javascript">
+function get() {
+	var form = document.getElementById('mainform');
+	form.method = 'GET';
+	form.submit();
+	return false;
+}
+function post() {
+	var form = document.getElementById('mainform');
+	form.method = 'POST';
+	form.submit();
+	return false;
+}
+</script>
+</head>
+<body>
+<h1>请求数据回显</h1>
+
+<hr />
+<form action="dump" method="POST" id="mainform">
+<table>
+	<tr>
+		<td>Name</td>
+		<td><input type="text" name="name" value="{{.Form.Get "name"}}" /></td>
+	</tr>
+	<tr>
+		<td>Age</td>
+		<td><input type="text" name="age" value="{{.Form.Get "age"}}" /></td>
+	</tr>
+	<tr>
+		<td>City</td>
+		<td><input type="text" name="city" value="{{.Form.Get "city"}}" /></td>
+	</tr>
+	<tr>
+		<td>Company</td>
+		<td><input type="text" name="company" value="{{.Form.Get "company"}}" /></td>
+	</tr>
+	<tr>
+		<td colspan="2">
+			<button type="button" onclick="get();">GET Request</button>
+			<button type="button" onclick="post();">POST Request</button>
+		</td>
+	</tr>
+</table>
+<hr />
+
+<table>
+	<tr>
+		<th colspan="2">HEADER</th>
+	</tr>
+	{{range $k, $v := .Header}}
+	<tr>
+		<td>{{$k}}</td>
+		<td>
+		{{range $v}}
+		{{.}}<br />
+		{{end}}
+		</td>
+	</tr>
+	{{end}}
+</table>
+
+<table>
+	<tr>
+		<th colspan="2">SERVER</th>
+	</tr>
+	<tr>
+		<td>Server Address</td>
+		<td>{{.Host}}</td>
+	</tr>
+	<tr>
+		<td>Remote Address</td>
+		<td>{{.RemoteAddr}}</td>
+	</tr>
+	<tr>
+		<td>Protocol</td>
+		<td>{{.Proto}}</td>
+	</tr>
+	<tr>
+		<td>Request Method</td>
+		<td>{{.Method}}</td>
+	</tr>
+	<tr>
+		<td>Request URI</td>
+		<td>{{.RequestURI}}</td>
+	</tr>
+	<tr>
+		<td>Path</td>
+		<td>{{.URL.Path}}</td>
+	</tr>
+	<tr>
+		<td>RawQuery</td>
+		<td>{{.URL.RawQuery}}</td>
+	</tr>
+</table>
+
+<table>
+	<tr>
+		<th colspan="2">COOKIE</th>
+	</tr>
+	{{range .Cookies}}
+	<tr>
+		<td>{{.Name}}</td>
+		<td>{{.Value}}</td>
+	</tr>
+	{{end}}
+</table>
+
+<table>
+	<tr>
+		<th colspan="2">REQUEST</th>
+	</tr>
+	{{range $k, $v := .Form}}
+	<tr>
+		<td>{{$k}}</td>
+		<td>
+		{{range $v}}
+		{{.}}<br />
+		{{end}}
+		</td>
+	</tr>
+	{{end}}
+</table>
+
+<table>
+	<tr>
+		<th colspan="2">GET</th>
+	</tr>
+	{{range $k, $v := .URL.Query}}
+	<tr>
+		<td>{{$k}}</td>
+		<td>
+		{{range $v}}
+		{{.}}<br />
+		{{end}}
+		</td>
+	</tr>
+	{{end}}
+</table>
+
+{{if and (eq .Method "POST") (eq "application/x-www-form-urlencoded" (.Header.Get "Content-Type"))}}
+<table>
+	<tr>
+		<th colspan="2">POST</th>
+	</tr>
+	{{range $k, $v := .PostForm}}
+	<tr>
+		<td>{{$k}}</td>
+		<td>
+		{{range $v}}
+		{{.}}<br />
+		{{end}}
+		</td>
+	</tr>
+	{{end}}
+</table>
+{{end}}
+</body>
+</html>

+ 11 - 0
tmpl/header.tmpl

@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+<link rel="stylesheet" href="/resources/bootstrap-3.4.1-dist/css/bootstrap.min.css" />
+<link rel="stylesheet" href="/resources/css/common.css" />
+<title>RedisDog - {{.Title}}</title>
+<script type="text/javascript" src="/resources/js/jquery-3.4.1.min.js"></script>
+<script type="text/javascript" src="/resources/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script>
+<script type="text/javascript" src="/resources/js/common.js"></script>

+ 132 - 0
tmpl/index.tmpl

@@ -0,0 +1,132 @@
+{{template "header.tmpl" .}}
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<div class="container-fluid">
+	<table class="table table-bordered table-striped">
+		<thead>
+			<tr>
+				<th colspan="23" class="text-center">Redis列表&nbsp;[<a href="#" id="btn_refresh">刷新</a>]</th>
+			</tr>
+			<tr>
+				<th>ID</th>
+				<th>服务地址</th>
+				<th>备注</th>
+				<th>版本号</th>
+				<th>进程ID</th>
+				<th>上线时长</th>
+				<th>CPU(USR/SYS)</th>
+				<th>OPS/QPS</th>
+				<th>系统内存</th>
+				<th>分配内存</th>
+				<th>已用内存</th>
+				<th>淘汰策略</th>
+				<th>过期数</th>
+				<th>丢弃数</th>
+				<th>当前连接</th>
+				<th>阻塞连接</th>
+				<th>拒绝连接</th>
+				<th>取命中</th>
+				<th>取未命中</th>
+				<th>DB0使用</th>
+				<th>详情</th>
+			</tr>
+		</thead>
+		<tbody id="list"></tbody>
+	</table>
+</div>
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+$(function(){
+	function load_stats() {
+		$('#list').empty();
+		$.get("/index/stats", {}, function(resp){
+			if (resp && resp.errno == 0) {
+				var html = '';
+				for (var i=0; i<resp.data.length; i++) {
+					var item = resp.data[i];
+					if (typeof item.Data.db0 == 'undefined') {
+						item.Data.db0 = '-';
+					}
+					var qps = parseInt(item.Data.instantaneous_ops_per_sec), qps_flag = '';
+					if (qps > 30000) {
+						qps_flag = 'danger';
+					} else if (qps > 10000) {
+						qps_flag = 'warning';
+					}
+					var maxmemory = parseInt(item.Data.maxmemory), maxmemory_flag = '';
+					if (maxmemory == 0) {
+						maxmemory_flag = 'danger';
+					}
+					var used_memory_pc = (maxmemory > 0 ? parseInt(item.Data.used_memory) / maxmemory : 0), used_memory_flag = '';
+					if (used_memory_pc > 0.9) {
+						maxmemory_flag = 'danger';
+					} else if (used_memory_pc > 0.8) {
+						maxmemory_flag = 'warning';
+					}
+					var evicted_keys_flag = parseInt(item.Data.evicted_keys) > 0 ? 'warning' : '';
+					var connected_clients_flag = parseInt(item.Data.connected_clients) > 1000 ? 'warning' : '';
+					var blocked_clients_flag = parseInt(item.Data.blocked_clients) > 0 ? 'warning' : '';
+					html += `<tr>
+						<td>${item.Id}</td>
+						<td>${item.Address}</td>
+						<td>${item.Remark}</td>
+						<td>${item.Data.redis_version}</td>
+						<td>${item.Data.process_id}</td>
+						<td>${item.Data.uptime_in_days}天</td>
+						<td>${item.Data.used_cpu_user}<i class="text-primary">s</i>/${item.Data.used_cpu_sys}<i class="text-primary">s</i></td>
+						<td class="${qps_flag}">${item.Data.instantaneous_ops_per_sec}</td>
+						<td>${item.Data.total_system_memory_human}</td>
+						<td class="${maxmemory_flag}">${item.Data.maxmemory_human}</td>
+						<td class="${used_memory_flag}'">${item.Data.used_memory_human}</td>
+						<td>${item.Data.maxmemory_policy}</td>
+						<td>${item.Data.expired_keys}</td>
+						<td class="${evicted_keys_flag}">${item.Data.evicted_keys}</td>
+						<td class="${connected_clients_flag}">${item.Data.connected_clients}</td>
+						<td class="${blocked_clients_flag}">${item.Data.blocked_clients}</td>
+						<td>${item.Data.rejected_connections}</td>
+						<td>${item.Data.keyspace_hits}</td>
+						<td>${item.Data.keyspace_misses}</td>
+						<td>${item.Data.db0}</td>
+						<td><a href="#" data-value="${item.Id}">查看</a></td>
+					</tr>`;
+				}
+				$('#list').html(html);
+				$('#list a').click(function(){
+					var id = $(this).attr('data-value');
+					$.get('/index/info', {'id': id}, function(resp){
+						if (resp && resp.data) {
+							var html = '<table class="table table-bordered table-striped">';
+							html += '<thead><th>Key</th><th>值</th></tr></thead><tbody>';
+							for(var k in resp.data.Data) {
+								html += '<tr>';
+								html += '<td>' + k + '</td>';
+								html += '<td>' + resp.data.Data[k] + '</td>';
+								html += '</tr>';
+							}
+							html += '</tbody></table>';
+							$.alert({
+								'title': 'Redis [' + resp.data.Address + '] 状态详情',
+								'content': html,
+								'size': 'modal-lg'
+							});
+						}
+					}, 'json');
+					return false;
+				});
+			}
+		}, 'json');
+	}
+
+	$('#btn_refresh').click(function(){
+		load_stats();
+		return false;
+	});
+
+	load_stats();
+});
+</script>
+</body>
+</html>

+ 61 - 0
tmpl/login.tmpl

@@ -0,0 +1,61 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+<link rel="stylesheet" href="/resources/bootstrap-3.4.1-dist/css/bootstrap.min.css" />
+<title>RedisDog - 登录</title>
+<style type="text/css">
+body{
+	padding: 0px 10px;
+	font-size: 14px;
+}
+h1{
+	margin: 30px;
+	text-align: center;
+}
+</style>
+</head>
+<body>
+<div class="container" style="width:400px;">
+	<h1>RedisDog</h1>
+	<form action="/login" method="POST" id="loginform">
+		<div class="form-group">
+			<label for=""><strong>用户名</strong></label>
+			<input type="text" class="form-control" name="account" maxlength="20" value="" />
+		</div>
+		<div class="form-group">
+			<label for=""><strong>密码</strong></label>
+			<input type="password" class="form-control" name="passwd" maxlength="20" value="" />
+		</div>
+		<button type="submit" class="btn btn-primary">提交</button>
+		<button type="button" class="btn btn-default">忘记密码</button>
+	</form>
+</div>
+<script type="text/javascript" src="/resources/js/jquery-3.4.1.min.js"></script>
+<script type="text/javascript" src="/resources/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script>
+<script type="text/javascript" src="/resources/js/common.js"></script>
+<script type="text/javascript">
+$(function(){
+	$('#loginform').submit(function(){
+		if (this.account.value == '') {
+			$.alert('请填写要登录的账号!');
+		} else if (/\W/.test(this.account.value)) {
+			$.alert('账号格式不正确!');
+		} else if (this.passwd.value == '') {
+			$.alert('请填写账号密码!');
+		} else {
+			$.post(this.action, $(this).serialize(), function(resp){
+				if (resp.errno) {
+					$.alert(resp.error);
+				} else {
+					document.location = 'index';
+				}
+			}, 'json');
+		}
+		return false;
+	});
+});
+</script>
+</body>
+</html>

+ 55 - 0
tmpl/navbar.tmpl

@@ -0,0 +1,55 @@
+<nav class="navbar navbar-default navbar-fixed-top">
+	<div class="container-fluid">
+		<div class="navbar-header">
+			<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar_menus" aria-expanded="false">
+		        <span class="sr-only">切换导航</span>
+		        <span class="icon-bar"></span>
+		        <span class="icon-bar"></span>
+		        <span class="icon-bar"></span>
+			</button>
+			<a class="navbar-brand" href="/" onclick="return false;"><strong>RedisDog</strong></a>
+		</div>
+		<div class="collapse navbar-collapse" id="navbar_menus">
+			<ul class="nav navbar-nav">
+				<li class="{{if or (eq .Req.URL.Path "/") (eq .Req.URL.Path "/index")}}active{{end}}">
+					<a class="" href="/index"><i class="glyphicon glyphicon-home"></i>&nbsp;首页</a>
+				</li>
+				<li class="ropdown {{if has_prefix .Req.URL.Path "/log/"}}active{{end}}">
+					<a class="dropdown-toggle" href="#" id="nav_log" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+						<i class="glyphicon glyphicon-info-sign"></i>&nbsp;日志查看&nbsp;<span class="caret"></span>
+					</a>
+					<ul class="dropdown-menu" aria-labelledby="nav_log">
+						<li><a class="{{if eq .Req.URL.Path "/log/warn"}}active{{end}}" href="/log/warn">Redis报警日志</a></li>
+						<li><a class="{{if eq .Req.URL.Path "/log/autoprocess"}}active{{end}}" href="/log/autoprocess">Redis扩容日志</a></li>
+						<li><a class="{{if eq .Req.URL.Path "/log/syslog"}}active{{end}}" href="/log/syslog">系统错误日志</a></li>
+						<li><a class="{{if eq .Req.URL.Path "/log/account"}}active{{end}}" href="/log/account">用户操作日志</a></li>
+					</ul>
+				</li>
+				<li class="dropdown {{if has_prefix .Req.URL.Path "/syscfg/"}}active{{end}}">
+					<a class="dropdown-toggle" href="#" id="nav_syscfg" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+						<i class="glyphicon glyphicon-cog"></i>&nbsp;系统管理&nbsp;<span class="caret"></span>
+					</a>
+					<ul class="dropdown-menu" aria-labelledby="nav_syscfg">
+						<li><a class="{{if eq .Req.URL.Path "/syscfg/redis"}}active{{end}}" href="/syscfg/redis">Redis列表管理</a></li>
+						{{if .Sess.Account.IsSuper}}
+						<li><a class="{{if eq .Req.URL.Path "/syscfg/warn"}}active{{end}}" href="/syscfg/warn">报警配置</a></li>
+						<li><a class="{{if eq .Req.URL.Path "/syscfg/account"}}active{{end}}" href="/syscfg/account">账号管理</a></li>
+						<li><a class="{{if eq .Req.URL.Path "/syscfg/misc"}}active{{end}}" href="/syscfg/misc">其它配置</a></li>
+						{{end}}
+					</ul>
+				</li>
+			</ul>
+			<ul class="nav navbar-nav navbar-right">
+				<li class="nav-item dropdown {{if has_prefix .Req.URL.Path "/profile/"}}active{{end}}">
+					<a class="dropdown-toggle" href="#" id="nav_profile" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+					<i class="glyphicon glyphicon-user"></i>&nbsp;&nbsp;{{.Sess.Account.Name}}&nbsp;({{.Sess.Account.Account}})&nbsp;<span class="caret"></span>
+					</a>
+					<ul class="dropdown-menu" aria-labelledby="nav_profile">
+						<li><a class="{{if eq .Req.URL.Path "/profile/passwd"}}active{{end}}" href="/profile/passwd">修改密码</a></li>
+						<li><a class="{{if eq .Req.URL.Path "/profile/logout"}}active{{end}}" href="/profile/logout">退出登录</a></li>
+					</ul>
+				</li>
+			</ul>
+		</div>
+	</div>
+</nav>

+ 60 - 0
tmpl/profile/passwd.tmpl

@@ -0,0 +1,60 @@
+{{template "header.tmpl" .}}
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<div class="container-fluid">
+	<form class="center-block" id="mainform" style="width:400px;">
+		<div class="form-group">
+			<label>ID</label>
+			<input type="text" class="form-control" name="" value="{{.Sess.Account.Id}}" maxlength="20" readonly="readonly" />
+		</div>
+		<div class="form-group">
+			<label>用户名</label>
+			<input type="text" class="form-control" name="" value="{{.Sess.Account.Account}}" maxlength="20" readonly="readonly" />
+		</div>
+		<div class="form-group">
+			<label>旧密码</label>
+			<input type="password" class="form-control" name="oldpwd" value="" maxlength="20" />
+		</div>
+		<div class="form-group">
+			<label>新密码(6位以上)</label>
+			<input type="password" class="form-control" name="newpwd" value="" maxlength="20" />
+		</div>
+		<div class="form-group">
+			<label>重复新密码</label>
+			<input type="password" class="form-control" name="newpwd2" value="" maxlength="20" />
+		</div>
+		<button class="btn btn-primary">提交</button>
+	</form>
+</div>
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+$(function(){
+	$('#mainform').submit(function(){
+		if (this.oldpwd.value == '') {
+			$.alert({'content': '旧密码不能为空!'});
+		} else if (this.oldpwd.value.length < 6) {
+			$.alert({'content': '旧密码格式不正确!'});
+		} else if (this.newpwd.value == '') {
+			$.alert({'content': '新密码不能为空!'});
+		} else if (this.newpwd.value.length < 6) {
+			$.alert({'content': '新密码格式不正确!'});
+		} else if (this.newpwd2.value != this.newpwd.value) {
+			$.alert({'content': '新密码两次输入不一致!'});
+		} else {
+			$.post('/profile/passwd', $(this).serialize(), function(resp){
+				if (resp.errno) {
+					$.alert({'content': resp.error});
+				} else {
+					$.alert({'content': '修改成功!'});
+				}
+			}, 'json');
+		}
+		return false;
+	});
+});
+</script>
+</body>
+</html>

+ 243 - 0
tmpl/syscfg/account.tmpl

@@ -0,0 +1,243 @@
+{{template "header.tmpl" .}}
+<style type="text/css">
+#filterform{
+	margin: 10px 0px;
+}
+#filterform .form-group{
+	margin-right: 20px;
+}
+</style>
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<div class="container-fluid">
+	<table class="table table-bordered" style="margin-bottom:0px;">
+		<thead>
+			<tr>
+				<th colspan="7" class="text-center"><big>账号列表</big>
+					<span style="margin-left:10px;">
+						<button class="btn btn-sm btn-warning" href="#" id="add_btn"><i class="glyphicon glyphicon-plus"></i> 点击这里添加</button>
+					</span>
+				</th>
+			</tr>
+			<tr>
+				<th>ID</th>
+				<th>账号</th>
+				<th>姓名</th>
+				<th>最后登录</th>
+				<th>是否管理员</th>
+				<th>状态</th>
+				<th>操作</th>
+			</tr>
+		</thead>
+		<tbody id="list"></tbody>
+	</table>
+</div>
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+
+function randomPassword(length) {
+	var chars = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9','`','-','=','~','!','@','#','$','%','^','*','(',')','_','+','[',']',';',',','.','{','}',':','<','>','?'];
+	var pwd = '';
+	for (var i=0; i<length; i++) {
+		var k = Math.floor(Math.random() * chars.length);
+		pwd += chars[k];
+	}
+	return pwd;
+}
+
+$(function(){
+	//加载列表
+	function account_list() {
+		var list = $('#list');
+		list.empty();
+		$.post('/syscfg/account_list', {}, function(resp){
+			if (resp && resp.errno == 0 && resp.data) {
+				var html = '';
+				for (var i=0; i<resp.data.length; i++) {
+					var item = resp.data[i];
+					html += '<tr>';
+					html += '<td>' + item.Id + '</td>';
+					html += '<td>' + item.Account + '</td>';
+					html += '<td>' + item.Name + '</td>';
+					html += '<td>' + (item.LastLogin == '' ? '<i class="text-muted">无</i>' : item.LastLogin) + '</td>';
+					html += '<td class="' + (item.IsSuper ? 'text-danger' : '') + '">' + (item.IsSuper ? '是' : '否') + '</td>';
+					html += '<td class="' + (item.Disabled ? 'danger' : '') + '">' + (item.Disabled ? '禁用' : '正常') + '</td>';
+					html += '<td>';
+					html += '	<button class="btn btn-xs btn-primary" type="button" data-value="' + item.Id + '">编辑</button>&nbsp;';
+					html += '	<button class="btn btn-xs btn-warning" type="button" data-value="' + item.Id + '">重置密码</button>&nbsp;';
+					html += '	<button class="btn btn-xs btn-danger" type="button" data-value="' + item.Id + '">删除</button>';
+					html += '</td>';
+					html += '</tr>';
+				}
+				list.html(html);
+				//修改
+				$('.btn-primary', list).click(function(){
+					var id = $(this).attr('data-value');
+					$.get('/syscfg/account_get', {'id': id}, function(resp){
+						if (resp && resp.errno == 0) {
+							account_set(resp.data);
+						}
+					}, 'json');
+					return false;
+				});
+				//重置密码
+				$('.btn-warning', list).click(function(){
+					var id = $(this).attr('data-value');
+					var html = `<form>
+					<div class="form-group">
+						<label>请输入新密码</label>
+						<input type="text" name="Password" class="form-control" maxlength="20" />
+					</div>
+					</form>`;
+					$.alert({
+						'title': '修改密码',
+						'content': html,
+						'size': 'modal-sm',
+						'callback': function(dlg) {
+							var form = $('form', dlg).get(0);
+							if (form.Password.value.length < 6) {
+								$.alert({'content': '密码长度不能小于6!'});
+								return false;
+							} else {
+								$.post('/syscfg/account_reset_pwd', {
+									'id': id,
+									'password': form.Password.value
+								}, function(resp){
+									if (resp && resp.errno == 0) {
+										$.alert({'content': '操作成功!'});
+									} else {
+										$.alert({'content': resp.error ? resp.error : '操作失败!'});
+									}
+								}, 'json');
+							}
+						}
+					});
+					return false;
+				});
+				//删除
+				$('.btn-danger', list).click(function(){
+					var id = $(this).attr('data-value');
+					$.alert({
+						'content': '确定要删除该记录吗?',
+						'btncancel': true,
+						'callback': function() {
+							$.post('/syscfg/account_del', {'id': id}, function(resp){
+								if (resp && resp.errno == 0) {
+									account_list();
+								} else {
+									$.alert({'content': resp.error ? resp.error : '操作失败!'});
+								}
+							}, 'json');
+						}
+					});
+				});
+			}
+		}, 'json');
+		return false;
+	}
+
+	//添加或修改记录
+	function account_set(record){
+		record = $.extend({
+			'Id': 0,
+			'Account': '',
+			'Name': '',
+			'Password': randomPassword(8),
+			'IsSuper': false,
+			'Disabled': false
+		}, record);
+		var html = `<form class="form-horizontal">
+			<div class="form-group id-group" style="display:none;">
+				<label class="col-md-4 control-label">ID</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Id" maxlength="20" value="${record.Id}" readonly="readonly" style="width:100px;" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">账号</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Account" maxlength="20" value="${record.Account}" placeholder="不能为空" style="width:200px;" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">姓名</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Name" maxlength="20" value="${record.Name}" placeholder="不能为空" style="width:200px;" />
+				</div>
+			</div>
+			<div class="form-group pwd-group" style="display:none;">
+				<label class="col-md-4 control-label">初始密码</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Password" maxlength="20" value="${record.Password}" placeholder="不能为空" style="width:200px;" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">是否是管理员</label>
+				<div class="col-md-8">
+					<select class="form-control" name="IsSuper" style="width:100px;">
+						<option value="0">否</option>
+						<option value="1">是</option>
+					</select>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">状态</label>
+				<div class="col-md-8">
+					<select class="form-control" name="Disabled" style="width:100px;">
+						<option value="0">正常</option>
+						<option value="1">禁用</option>
+					</select>
+				</div>
+			</div>
+		</form>`;
+		$.alert({
+			'title': record.Id == 0 ? '添加账号' : '编辑账号',
+			'content': html,
+			'init': function(dlg) {
+				var form = $('form', dlg).get(0);
+				form.IsSuper.value = record.IsSuper ? '1' : '0';
+				form.Disabled.value = record.Disabled ? '1' : '0';
+				if (record.Id == 0) {
+					$('.id-group', form).hide();
+					$('.pwd-group', form).show();
+				} else {
+					$('.id-group', form).show();
+					$('.pwd-group', form).hide();
+				}
+			},
+			'callback': function(dlg){
+				var form = $('form', dlg).get(0);
+				if (form.Account.value.length < 2) {
+					$.alert({'content': '请填写至少2位字符的账号!'});
+				} else if (/\W/.test(form.Account.value) || !/^[A-Za-z]/.test(form.Account.value)) {
+					$.alert({'content': '账号只能使用字母数字和下划线的组合,并且必须使用字母开头!'});
+				} else if (form.Name.value == '') {
+					$.alert({'content': '请填写姓名!'});
+				} else if (record.Id == 0 && form.Password.value.length < 6) {
+					$.alert({'content': '请填写至少6位字符的初始密码!'});
+				} else {
+					$.post('/syscfg/account_set', $(form).serialize(), function(resp){
+						if (resp && resp.errno == 0) {
+							account_list();
+						} else {
+							$.alert({'content': resp.error ? resp.error : '操作失败!'});
+						}
+					}, 'json');
+					return true;
+				}
+				return false;
+			},
+			'btncancel': true
+		});
+		return false;
+	}
+
+	account_list(); //加载页面
+	$('#add_btn').click(account_set); //添加按钮
+});
+</script>
+</body>
+</html>

+ 10 - 0
tmpl/syscfg/mailgroup.tmpl

@@ -0,0 +1,10 @@
+{{template "header.tmpl" .}}
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+</script>
+</body>
+</html>

+ 65 - 0
tmpl/syscfg/misc.tmpl

@@ -0,0 +1,65 @@
+{{template "header.tmpl" .}}
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<div class="container-fluid">
+	<div class="row">
+		<div class="col-md-3">
+			<div class="pannel">
+				<div class="pannel_title">
+					<h3>SMTP配置</h3>
+				</div>
+				<div class="pannel_body">
+					<form style="margin-bottom:10px;">
+						<div class="form-group">
+							<label>服务器地址</label>
+							<input type="text" class="form-control" name="smtp_host" value="smtp.163.com" maxlength="50" />
+						</div>
+						<div class="form-group">
+							<label>服务端口</label>
+							<input type="text" class="form-control" name="smtp_port" value="25" maxlength="5" />
+						</div>
+						<div class="form-group">
+							<label>用户</label>
+							<input type="text" class="form-control" name="smtp_user" value="" maxlength="40" />
+						</div>
+						<div class="form-group">
+							<label>密码</label>
+							<input type="text" class="form-control" name="smtp_pwd" value="" maxlength="20" />
+						</div>
+						<div class="form-group">
+							<label>署名</label>
+							<input type="text" class="form-control" name="smtp_sender" value="" maxlength="20" />
+						</div>
+						<button type="button" class="btn btn-sm btn-primary">保存</button>
+					</form>
+				</div>
+			</div>
+		</div>
+	</div>
+	<div class="row">
+		<div class="col-md-3">
+			<div class="pannel">
+				<div class="pannel_title">
+					<h3>其它</h3>
+				</div>
+				<div class="pannel_body">
+					<form style="margin-bottom:10px;">
+						<div class="form-group">
+							<label>定时循环间隔</label>
+							<input type="text" class="form-control" name="misc_check_delay" value="30" maxlength="4" />
+						</div>
+						<button type="button" class="btn btn-sm btn-primary">保存</button>
+					</form>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+</script>
+</body>
+</html>

+ 323 - 0
tmpl/syscfg/redis.tmpl

@@ -0,0 +1,323 @@
+{{template "header.tmpl" .}}
+<style type="text/css">
+#filterform{
+	margin: 10px 0px;
+}
+#filterform .form-group{
+	margin-right: 20px;
+}
+</style>
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<div class="container-fluid">
+	<table class="table table-bordered" style="margin-bottom:0px;">
+		<thead>
+			<tr>
+				<th colspan="16" class="text-center"><big>Redis服务列表</big>
+					<span style="margin-left:10px;">
+						<button class="btn btn-sm btn-warning" href="#" id="add_btn"><i class="glyphicon glyphicon-plus"></i> 点击这里添加</button>
+					</span>
+				</th>
+			</tr>
+			<tr>
+				<th>ID</th>
+				<th>服务地址</th>
+				<th>备注名</th>
+				<th>连接密码</th>
+				<th>连接超时(秒)&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="连接超时(单位:秒)" data-content="检测Redis状态时如果连接操作超过该值将会被认为Redis状态异常。" onclick="return false;">?</a></th>
+				<th>最大失败次数&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最大失败次数" data-content="如果Redis状态检测失败连续失败次数达到该值将会触发报警。" onclick="return false;">?</a></th>
+				<th>最小剩余内存&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最小剩余内存" data-content="Redis最小可用的内存空间值,低于该值将会触发自动扩容操作。" onclick="return false;">?</a></th>
+				<th>最小剩余内存(%)&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最小剩余内存(百分比)" data-content="Redis最小可用的内存空间占比(已用值/分配值*100),低于该值将会触发自动扩容操作。" onclick="return false;">?</a></th>
+				<th>最大内存使用&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最大内存使用" data-content="自动扩容的内存上限,Redis的内存使用达到该值将不再自动扩容。" onclick="return false;">?</a></th>
+				<th>最大连接数&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最大连接数" data-content="Redis的最大并发连接数,达到该值将会触发报警。" onclick="return false;">?</a></th>
+				<th>最大新增淘汰数&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最大新增淘汰数" data-content="上次检测之后增加的淘汰记录数上限,达到该值后将触发报警。" onclick="return false;">?</a></th>
+				<th>最大QPS&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最大QPS" data-content="Redis的最大QPS值,达到该值将会触发报警。" onclick="return false;">?</a></th>
+				<th>报警邮箱列表&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="报警邮箱列表" data-content="接收报警邮件的邮箱地址列表,与邮件组合并后作为接收报警邮箱的邮箱列表。" onclick="return false;">?</a></th>
+				<th>状态</th>
+				<th>操作</th>
+			</tr>
+		</thead>
+		<tbody id="list"></tbody>
+	</table>
+</div>
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+
+$(function(){
+	//加载列表
+	function redis_list() {
+		var list = $('#list');
+		list.empty();
+		$.post('/syscfg/redis_list', {}, function(resp){
+			if (resp && resp.errno == 0 && resp.data) {
+				var html = '';
+				for (var i=0; i<resp.data.length; i++) {
+					var item = resp.data[i];
+					html += '<tr>';
+					html += '<td>' + item.Id + '</td>';
+					html += '<td>' + item.Address + '</td>';
+					html += '<td>' + (item.Remark == '' ? '<i class="text-muted">无</i>' : item.Remark) + '</td>';
+					html += '<td>' + (item.Password == '' ? '<i class="text-muted">无</i>' : item.Password) + '</td>';
+					html += '<td>' + item.MaxConnectWait + '</td>';
+					html += '<td>' + item.MaxStatusFailed + '</td>';
+					html += '<td>' + size2str(item.MinMemoryFree) + '</td>';
+					html += '<td>' + (item.MinMemoryFreePC ? item.MinMemoryFreePC + '%' : '<i class="text-muted">无</i>') + '</td>';
+					html += '<td>' + size2str(item.MaxMemoryUsage) + '</td>';
+					html += '<td>' + numberFormat(item.MaxConnection) + '</td>';
+					html += '<td>' + item.MaxEviIncreased + '</td>';
+					html += '<td>' + numberFormat(item.MaxQPS) + '</td>';
+					html += '<td>' + (item.MailList == '' ? '<i class="text-muted">无</i>' : item.MailList) + '</td>';
+					html += '<td' + (item.Disabled ? ' class="text-danger"' : '') + '>' + (item.Disabled ? '禁用' : '正常') + '</td>';
+					html += '<td>';
+					html += '	<button class="btn btn-xs btn-primary" type="button" data-value="' + item.Id + '">编辑</button>&nbsp;';
+					html += '	<button class="btn btn-xs btn-danger" type="button" data-value="' + item.Id + '">删除</button>';
+					html += '</td>';
+					html += '</tr>';
+				}
+				list.html(html);
+				//修改
+				$('.btn-primary', list).click(function(){
+					var id = $(this).attr('data-value');
+					$.get('/syscfg/redis_get', {'id': id}, function(resp){
+						if (resp && resp.errno == 0) {
+							redis_set(resp.data);
+						}
+					}, 'json');
+					return false;
+				});
+				//删除
+				$('.btn-danger', list).click(function(){
+					var id = $(this).attr('data-value');
+					$.alert({
+						'content': '确定要删除该记录吗?',
+						'btncancel': true,
+						'callback': function() {
+							$.post('/syscfg/redis_del', {'id': id}, function(resp){
+								if (resp && resp.errno == 0) {
+									redis_list();
+								} else {
+									$.alert({'content': resp.error ? resp.error : '操作失败!'});
+								}
+							}, 'json');
+						}
+					});
+				});
+			}
+		}, 'json');
+		return false;
+	}
+
+	//添加或修改记录
+	function redis_set(record){
+		record = $.extend({
+			'Id': 0,
+			'Address': '',
+			'Remark': '',
+			'Password': '',
+			'MaxConnectWait': 5,
+			'MaxStatusFailed': 1,
+			'MinMemoryFree': 10*1024*1024,
+			'MinMemoryFreePC': 10,
+			'MaxMemoryUsage': 500*1024*1024,
+			'MaxConnection': 1024,
+			'MaxEviIncreased': 1,
+			'MaxQPS': 30000,
+			'MailList': '',
+			'Disabled': false
+		}, record);
+		var html = `<form class="form-horizontal">
+			<div class="form-group id-group" style="display:none;">
+				<label class="col-md-4 control-label">ID</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Id" maxlength="20" value="${record.Id}" readonly="readonly" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">服务地址(IP:端口)</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Address" maxlength="20" value="${record.Address}" placeholder="IP或域名:端口" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">备注名称</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Remark" maxlength="20" value="${record.Remark}" placeholder="用于后台展示" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">连接密码</label>
+				<div class="col-md-8">
+					<input type="text" class="form-control" name="Password" maxlength="20" value="${record.Password}" placeholder="未设置密码时留空" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">连接超时时长(秒)</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:100px;">
+						<input type="text" class="form-control" name="MaxConnectWait" maxlength="2" value="${record.MaxConnectWait}" />
+						<div class="input-group-addon">秒</div>
+					</div>
+					<p class="help-block">连接等待时长超过该值将当作失败处理</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最大状态检测失败次数</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:100px;">
+						<input type="text" class="form-control" name="MaxStatusFailed" maxlength="2" value="${record.MaxStatusFailed}" />
+						<div class="input-group-addon">次</div>
+					</div>
+					<p class="help-block">连续失败次数达到该值将会触发报警</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最小可用内存空间</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:200px;">
+						<input type="text" class="form-control" name="MinMemoryFree" maxlength="10" value="${record.MinMemoryFree}" />
+						<div class="input-group-addon">字节</div>
+					</div>
+					<p class="help-block">低于该值将自动扩容,设置0跳过该检查项</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最小可用内存空间占比</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:100px;">
+						<input type="text" class="form-control" name="MinMemoryFreePC" maxlength="2" value="${record.MinMemoryFreePC}" />
+						<div class="input-group-addon">%</div>
+					</div>
+					<p class="help-block">低于该值将自动扩容,设置0跳过该检查项</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最大使用内存上限</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:200px;">
+						<input type="text" class="form-control" name="MaxMemoryUsage" maxlength="10" value="${record.MaxMemoryUsage}" />
+						<div class="input-group-addon">字节</div>
+					</div>
+					<p class="help-block">已用内存达到该值将不再自动扩容</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最大客户端并发连接数</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:150px;">
+						<input type="text" class="form-control" name="MaxConnection" maxlength="10" value="${record.MaxConnection}" />
+						<div class="input-group-addon">个</div>
+					</div>
+					<p class="help-block">超过该值将会触发报警</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最大新增淘汰记录数</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:100px;">
+						<input type="text" class="form-control" name="MaxEviIncreased" maxlength="5" value="${record.MaxEviIncreased}" />
+						<div class="input-group-addon">个</div>
+					</div>
+					<p class="help-block">超过该值将会触发报警</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">最大QPS上限值</label>
+				<div class="col-md-8">
+					<div class="input-group" style="width:150px;">
+						<input type="text" class="form-control" name="MaxQPS" maxlength="10" value="${record.MaxQPS}" />
+						<div class="input-group-addon">次/s</div>
+					</div>
+					<p class="help-block">超过该值将会触发报警</p>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">报警邮件接收邮箱列表<br />(多个之间使用“;”间隔)</label>
+				<div class="col-md-8">
+					<textarea class="form-control" name="MailList" rows="4" maxlength="1000">${record.MailList}</textarea>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="col-md-4 control-label">状态</label>
+				<div class="col-md-8">
+					<select class="form-control" name="Disabled" style="width:100px;">
+						<option value="0">正常</option>
+						<option value="1">禁用</option>
+					</select>
+				</div>
+			</div>
+		</form>`;
+		$.alert({
+			'title': record.Id == 0 ? '添加新配置' : '修改配置',
+			'content': html,
+			'init': function(dlg) {
+				var form = $('form', dlg).get(0);
+				form.Disabled.value = record.Disabled ? '1' : '0';
+				$(form.MinMemoryFree).change(function(){
+					this.value = numberFormat(this.value);
+				});
+				$(form.MaxMemoryUsage).change(function(){
+					this.value = numberFormat(this.value);
+				});
+				$(form.MinMemoryFree).change();
+				$(form.MaxMemoryUsage).change();
+				if (record.Id == 0) {
+					$('.id-group', dlg).hide();
+				} else {
+					$('.id-group', dlg).show();
+				}
+			},
+			'callback': function(dlg){
+				var form = $('form', dlg).get(0);
+				form.MinMemoryFree.value = form.MinMemoryFree.value.replace(/,/g, '');
+				form.MaxMemoryUsage.value = form.MaxMemoryUsage.value.replace(/,/g, '');
+				if (form.Address.value == '') {
+					$.alert({'content': '请填写Redis的服务地址!'});
+				} else if (!/[0-9A-Za-z.\-]+:\d{3,5}$/.test(form.Address.value)) {
+					$.alert({'content': 'Redis服务地址格式不正确!'});
+				} else if (form.Remark.value == '') {
+					$.alert({'content': '请填写备注名称!'});
+				} else if (/\D/.test(form.MaxConnectWait.value)) {
+					$.alert({'content': '请填写正确的连接超时时长!'});
+				} else if (/\D/.test(form.MaxStatusFailed.value)) {
+					$.alert({'content': '请填写正确的最大状态检测失败次数!'});
+				} else if (/\D/.test(form.MinMemoryFree.value)) {
+					$.alert({'content': '请填写正确的最小可用内存空间(字节)!'});
+				} else if (/\D/.test(form.MinMemoryFreePC.value)) {
+					$.alert({'content': '请填写正确的最小可用内存空间(百分比)!'});
+				} else if (/\D/.test(form.MaxMemoryUsage.value)) {
+					$.alert({'content': '请填写正确的最大使用内存上限!'});
+				} else if (/\D/.test(form.MaxConnection.value)) {
+					$.alert({'content': '请填写正确的最大客户端并发连接数!'});
+				} else if (/\D/.test(form.MaxEviIncreased.value)) {
+					$.alert({'content': '请填写正确的最大新增淘汰记录数!'});
+				} else if (/\D/.test(form.MaxQPS.value)) {
+					$.alert({'content': '请填写正确的最大QPS上限值!'});
+				} else if (form.MailList.value == '') {
+					$.alert({'content': '请填写报警邮件接收邮箱列表!'});
+				} else {
+					$.post('/syscfg/redis_set', $(form).serialize(), function(resp){
+						if (resp && resp.errno == 0) {
+							redis_list();
+						} else {
+							$.alert({'content': resp.error ? resp.error : '操作失败!'});
+						}
+					}, 'json');
+					return true;
+				}
+				return false;
+			},
+			'btncancel': true
+		});
+		return false;
+	}
+
+	redis_list(); //加载页面
+	$('#add_btn').click(redis_set); //添加按钮
+	$('[data-toggle="popover"]').popover(); //tips
+});
+</script>
+</body>
+</html>

+ 10 - 0
tmpl/syscfg/warn.tmpl

@@ -0,0 +1,10 @@
+{{template "header.tmpl" .}}
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+</script>
+</body>
+</html>