Browse Source

添加第一个能用的版本

weicky 4 years ago
parent
commit
ee3f93fc5b
49 changed files with 2576 additions and 1181 deletions
  1. 3 7
      etc/config.json
  2. BIN
      resources/favicon.ico
  3. 37 15
      resources/js/common.js
  4. 0 83
      sql/db.sql
  5. 12 13
      src/cnphper.com/model/accounts.go
  6. 223 0
      src/cnphper.com/model/monitorlog.go
  7. 116 0
      src/cnphper.com/model/processlog.go
  8. 46 48
      src/cnphper.com/model/rediscfg.go
  9. 47 8
      src/cnphper.com/model/syscfg.go
  10. 95 0
      src/cnphper.com/model/syslog.go
  11. 118 0
      src/cnphper.com/model/warnlog.go
  12. 0 37
      src/cnphper.com/redisdog/common.go
  13. 157 0
      src/cnphper.com/redisdog/functions.go
  14. 68 0
      src/cnphper.com/redisdog/http_debug.go
  15. 129 0
      src/cnphper.com/redisdog/http_index.go
  16. 93 0
      src/cnphper.com/redisdog/http_log_autoprocess.go
  17. 93 0
      src/cnphper.com/redisdog/http_log_monitor.go
  18. 88 0
      src/cnphper.com/redisdog/http_log_syslog.go
  19. 93 0
      src/cnphper.com/redisdog/http_log_warn.go
  20. 5 8
      src/cnphper.com/redisdog/login.go
  21. 14 24
      src/cnphper.com/redisdog/profile.go
  22. 39 66
      src/cnphper.com/redisdog/syscfg_account.go
  23. 114 0
      src/cnphper.com/redisdog/http_syscfg_misc.go
  24. 60 93
      src/cnphper.com/redisdog/syscfg_redis.go
  25. 130 0
      src/cnphper.com/redisdog/http_syscfg_warn.go
  26. 0 173
      src/cnphper.com/redisdog/index.go
  27. 0 43
      src/cnphper.com/redisdog/log_account.go
  28. 0 43
      src/cnphper.com/redisdog/log_autoprocess.go
  29. 0 43
      src/cnphper.com/redisdog/log_syslog.go
  30. 0 43
      src/cnphper.com/redisdog/log_warn.go
  31. 87 0
      src/cnphper.com/redisdog/mail_sender.go
  32. 0 72
      src/cnphper.com/redisdog/mailsender.go
  33. 126 32
      src/cnphper.com/redisdog/main.go
  34. 0 42
      src/cnphper.com/redisdog/monitor.go
  35. 208 0
      src/cnphper.com/redisdog/redis_monitor.go
  36. 0 0
      src/cnphper.com/redisdog/session_pool.go
  37. 30 27
      src/cnphper.com/redisdog/sql.go
  38. 10 10
      src/cnphper.com/redisdog/setting.go
  39. 0 50
      src/cnphper.com/redisdog/syscfg_misc.go
  40. 0 50
      src/cnphper.com/redisdog/syscfg_warn.go
  41. 62 0
      tmpl/debug/sendmail.tmpl
  42. 57 47
      tmpl/index.tmpl
  43. 2 2
      tmpl/navbar.tmpl
  44. 32 22
      tmpl/profile/passwd.tmpl
  45. 2 7
      tmpl/syscfg/account.tmpl
  46. 0 10
      tmpl/syscfg/mailgroup.tmpl
  47. 49 38
      tmpl/syscfg/misc.tmpl
  48. 46 25
      tmpl/syscfg/redis.tmpl
  49. 85 0
      tmpl/syscfg/warn.tmpl

+ 3 - 7
etc/config.json

@@ -1,10 +1,6 @@
 {
-	"Host": "192.168.56.102",
+	"Host": "192.168.56.101",
 	"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
+	"RootPwd": "88888888",
+	"Database": "/srv/redisdog/db.sqlite"
 }

BIN
resources/favicon.ico


+ 37 - 15
resources/js/common.js

@@ -143,23 +143,23 @@ function pager(options) {
 		options.bar_size++;
 	}
 	var bar_side = (options.bar_size-1) / 2;
-	var pages = Math.ceil(parseInt(options.total) / parseInt(options.page_size));
+	var pages = Math.max(Math.ceil(parseInt(options.total) / parseInt(options.page_size)), 1);
 	if (options.page_num < 1 || options.page_num > pages) {
 		return;
 	}
-	var html = '<nav id="pager"><ul class="pagination">';
+	var html = '<nav><ul class="pagination"><li class="disabled"><a href="#">共'+ options.total +'条'+ pages +'页</a></li>';
 	if (options.show_first_last) {
 		if (options.page_num == 1) {
-			html += '<li class="disabled"><a class="page-link" href="#" data-value="1">首页</a></li>';
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="1"><i class="glyphicon glyphicon-step-backward"></i></a></li>';
 		} else {
-			html += '<li><a class="page-link" href="#" data-value="1">首页</a></li>';
+			html += '<li><a class="page-link" href="#" data-value="1"><i class="glyphicon glyphicon-step-backward"></i></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>';
+			html += '<li><a class="page-link" href="#" data-value="' + (options.page_num-1) + '"><i class="glyphicon glyphicon-chevron-left"></i></a></li>';
 		} else {
-			html += '<li class="disabled"><a class="page-link" href="#" data-value="1">前页</a></li>';
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="1"><i class="glyphicon glyphicon-chevron-left"></i></a></li>';
 		}
 	}
 	if (pages <= options.bar_size) {
@@ -170,32 +170,54 @@ function pager(options) {
 				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 <= bar_side + 1) {
+		for (var i=1; i<=options.bar_size; 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 >= pages - bar_side) {
-		
+		for (var i=pages-options.bar_size+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 {
-		
+		for (var i=options.page_num-bar_side, j=0; j<options.bar_size; i++, j++) {
+			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>';
+			}
+		}
 	}
 	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>';
+			html += '<li><a class="page-link" href="#" data-value="' + (options.page_num+1) + '"><i class="glyphicon glyphicon-chevron-right"></i></a></li>';
 		} else {
-			html += '<li class="disabled"><a class="page-link" href="#" data-value="' + pages + '">后页</a></li>';
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="' + pages + '"><i class="glyphicon glyphicon-chevron-right"></i></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>';
+			html += '<li class="disabled"><a class="page-link" href="#" data-value="' + pages + '"><i class="glyphicon glyphicon-step-forward"></i></a></li>';
 		} else {
-			html += '<li><a class="page-link" href="#" data-value="' + pages + '">末页</a></li>';
+			html += '<li><a class="page-link" href="#" data-value="' + pages + '"><i class="glyphicon glyphicon-step-forward"></i></a></a></li>';
 		}
 	}
 	html += '</ul></nav>';
 	var jq = $('#' + options.id);
 	jq.html(html);
 	$('a', jq).click(function(){
-		options.callback($(this).attr('data-value'));
+		var p = $(this).attr('data-value');
+		if (!p || p == "") {
+			return false;
+		}
+		options.callback(parseInt(p));
 		return false;
 	});
 }

+ 0 - 83
sql/db.sql

@@ -1,83 +0,0 @@
-/*
-服务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)
-);

+ 12 - 13
src/cnphper.com/model/accounts.go

@@ -5,6 +5,7 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
+	"time"
 )
 
 type Accounts struct {
@@ -25,15 +26,12 @@ func NewAccounts(db *sql.DB) *Accounts {
 	return &Accounts{Model{db: db}}
 }
 
-func passwordConvert(pwd string) string {
+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("账号不能为空")
 	}
@@ -44,8 +42,8 @@ func (m *Accounts) Insert(row *AccountsRow) (int64, error) {
 	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)
+	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
 	}
@@ -56,7 +54,7 @@ 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)
+	ret, err := m.db.Exec("DELETE FROM accounts WHERE id=?", id)
 	if err != nil {
 		return 0, err
 	}
@@ -74,7 +72,7 @@ func (m *Accounts) Update(row *AccountsRow) (int64, error) {
 	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)
+	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
 	}
@@ -88,8 +86,8 @@ func (m *Accounts) UpdatePassword(id int64, password string) (int64, error) {
 	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)
+	passwordEnc := PasswordConvert(password)
+	ret, err := m.db.Exec(`UPDATE accounts SET password=? WHERE id=?`, passwordEnc, id)
 	if err != nil {
 		return 0, err
 	}
@@ -100,7 +98,7 @@ 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)
+	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,
@@ -122,7 +120,7 @@ func (m *Accounts) Get(id int64) (*AccountsRow, error) {
 }
 
 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`)
+	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
 	}
@@ -152,7 +150,7 @@ func (m *Accounts) Login(account, password string) (*AccountsRow, error) {
 	if len(password) < 6 {
 		return nil, errors.New("密码长度不能小于6")
 	}
-	passwordEnc := passwordConvert(password)
+	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(
@@ -172,6 +170,7 @@ func (m *Accounts) Login(account, password string) (*AccountsRow, error) {
 	} else if row.Disabled {
 		return nil, errors.New("账号已被禁用")
 	} else {
+		m.db.Exec("UPDATE last_login=? WHERE id=?", time.Now().Unix(), row.Id)
 		return &row, nil
 	}
 }

+ 223 - 0
src/cnphper.com/model/monitorlog.go

@@ -0,0 +1,223 @@
+package model
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"time"
+)
+
+type MonitorLog struct {
+	Model
+}
+
+type MonitorLogRow struct {
+	Id           int64
+	RedisId      int64
+	RedisAddress string
+	RedisRemark  string
+	LogTime      int64
+	QueryStatus  bool
+	FailedCount  int64
+	UsedMemory   int64
+	MaxMemory    int64
+	SystemMemory int64
+	Connection   int64
+	QPS          int64
+	EvictedKeys  int64
+	EviIncreased int64
+	LastMailTime int64
+}
+
+func NewMonitorLog(db *sql.DB) *MonitorLog {
+	return &MonitorLog{
+		Model{
+			db: db,
+		},
+	}
+}
+
+func (m *MonitorLog) Add(row *MonitorLogRow) (int64, error) {
+	if row.RedisId <= 0 {
+		return 0, errors.New("Redis的ID必须是正整数")
+	}
+	if row.LogTime <= 0 {
+		row.LogTime = time.Now().Unix()
+	}
+	queryStatusInt := 0
+	if row.QueryStatus {
+		queryStatusInt = 1
+	}
+	ret, err := m.db.Exec(`INSERT INTO monitorlog(redis_id,log_time,query_status,failed_count,used_memory,max_memory,system_memory,connection,qps,evicted_keys,evi_increased) VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
+		row.RedisId,
+		row.LogTime,
+		queryStatusInt,
+		row.FailedCount,
+		row.UsedMemory,
+		row.MaxMemory,
+		row.SystemMemory,
+		row.Connection,
+		row.QPS,
+		row.EvictedKeys,
+		row.EviIncreased,
+	)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.LastInsertId()
+	}
+}
+
+func (m *MonitorLog) CleanUntil(until int64) (int64, error) {
+	ret, err := m.db.Exec("DELETE FROM monitorlog WHERE log_time<?", until)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *MonitorLog) Count(redisId int64) (int64, error) {
+	var (
+		row *sql.Row
+		cnt int64
+	)
+	if redisId == 0 {
+		row = m.db.QueryRow("SELECT COUNT(*) FROM monitorlog")
+	} else {
+		row = m.db.QueryRow("SELECT COUNT(*) FROM monitorlog WHERE redis_id=?", redisId)
+	}
+	if err := row.Scan(&cnt); err != nil {
+		return 0, err
+	} else {
+		return cnt, nil
+	}
+}
+
+func (m *MonitorLog) GetList(redisId int64, offset int64, limit int64, orderDesc bool) ([]*MonitorLogRow, error) {
+	var (
+		orderDir string
+		ret      *sql.Rows
+		err      error
+	)
+	if orderDesc {
+		orderDir = "DESC"
+	} else {
+		orderDir = "ASC"
+	}
+	if redisId == 0 {
+		if limit > 0 {
+			sql := fmt.Sprintf(`SELECT
+						monitorlog.id,
+						monitorlog.redis_id,
+						monitorlog.log_time,
+						monitorlog.query_status,
+						monitorlog.failed_count,
+						monitorlog.used_memory,
+						monitorlog.max_memory,
+						monitorlog.system_memory,
+						monitorlog.connection,
+						monitorlog.qps,
+						monitorlog.evicted_keys,
+						monitorlog.evi_increased,
+						rediscfg.address,
+						rediscfg.remark
+					FROM monitorlog INNER JOIN rediscfg ON monitorlog.redis_id=rediscfg.id ORDER BY monitorlog.id %s LIMIT ?,?`,
+				orderDir,
+			)
+			ret, err = m.db.Query(sql, offset, limit)
+		} else {
+			sql := fmt.Sprintf(`SELECT
+						monitorlog.id,
+						monitorlog.redis_id,
+						monitorlog.log_time,
+						monitorlog.query_status,
+						monitorlog.failed_count,
+						monitorlog.used_memory,
+						monitorlog.max_memory,
+						monitorlog.system_memory,
+						monitorlog.connection,
+						monitorlog.qps,
+						monitorlog.evicted_keys,
+						monitorlog.evi_increased,
+						rediscfg.address,
+						rediscfg.remark
+					FROM monitorlog INNER JOIN rediscfg ON monitorlog.redis_id=rediscfg.id ORDER BY monitorlog.id %s`,
+				orderDir,
+			)
+			ret, err = m.db.Query(sql)
+		}
+
+	} else {
+		if limit > 0 {
+			sql := fmt.Sprintf(`SELECT
+						monitorlog.id,
+						monitorlog.redis_id,
+						monitorlog.log_time,
+						monitorlog.query_status,
+						monitorlog.failed_count,
+						monitorlog.used_memory,
+						monitorlog.max_memory,
+						monitorlog.system_memory,
+						monitorlog.connection,
+						monitorlog.qps,
+						monitorlog.evicted_keys,
+						monitorlog.evi_increased,
+						rediscfg.address,
+						rediscfg.remark
+					FROM monitorlog INNER JOIN rediscfg ON monitorlog.redis_id=rediscfg.id WHERE monitorlog.redis_id=? ORDER BY monitorlog.id %s LIMIT ?,?`,
+				orderDir,
+			)
+			ret, err = m.db.Query(sql, redisId, offset, limit)
+		} else {
+			sql := fmt.Sprintf(`SELECT
+						monitorlog.id,
+						monitorlog.redis_id,
+						monitorlog.log_time,
+						monitorlog.query_status,
+						monitorlog.failed_count,
+						monitorlog.used_memory,
+						monitorlog.max_memory,
+						monitorlog.system_memory,
+						monitorlog.connection,
+						monitorlog.qps,
+						monitorlog.evicted_keys,
+						monitorlog.evi_increased,
+						rediscfg.address,
+						rediscfg.remark
+					FROM monitorlog INNER JOIN rediscfg ON monitorlog.redis_id=rediscfg.id WHERE monitorlog.redis_id=? ORDER BY monitorlog.id %s`,
+				orderDir,
+			)
+			ret, err = m.db.Query(sql, redisId)
+		}
+	}
+	if err != nil {
+		return nil, err
+	} else {
+		rows := make([]*MonitorLogRow, 0)
+		for ret.Next() {
+			row := MonitorLogRow{}
+			err = ret.Scan(
+				&row.Id,
+				&row.RedisId,
+				&row.LogTime,
+				&row.QueryStatus,
+				&row.FailedCount,
+				&row.UsedMemory,
+				&row.MaxMemory,
+				&row.SystemMemory,
+				&row.Connection,
+				&row.QPS,
+				&row.EvictedKeys,
+				&row.EviIncreased,
+				&row.RedisAddress,
+				&row.RedisRemark,
+			)
+			if err != nil {
+				continue
+			}
+			rows = append(rows, &row)
+		}
+		return rows, nil
+	}
+}

+ 116 - 0
src/cnphper.com/model/processlog.go

@@ -0,0 +1,116 @@
+package model
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"time"
+)
+
+type ProcessLog struct {
+	Model
+}
+
+type ProcessLogRow struct {
+	Id              int64
+	RedisId         int64
+	RedisAddress    string
+	RedisRemark     string
+	ProcessTime     int64
+	MaxMemoryBefore int64
+	MaxMemoryAfter  int64
+}
+
+func NewProcessLog(db *sql.DB) *ProcessLog {
+	return &ProcessLog{
+		Model{
+			db: db,
+		},
+	}
+}
+
+func (m *ProcessLog) Add(row *ProcessLogRow) (int64, error) {
+	if row.RedisId <= 0 {
+		return 0, errors.New("Redis的ID必须是正整数")
+	}
+	if row.ProcessTime <= 0 {
+		row.ProcessTime = time.Now().Unix()
+	}
+	ret, err := m.db.Exec("INSERT INTO processlog(redis_id,process_time,maxmemory_before,maxmemory_after) VALUES(?,?,?,?)", row.RedisId, row.ProcessTime, row.MaxMemoryBefore, row.MaxMemoryAfter)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.LastInsertId()
+	}
+}
+
+func (m *ProcessLog) CleanUntil(until int64) (int64, error) {
+	ret, err := m.db.Exec("DELETE FROM processlog WHERE process_time<?", until)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *ProcessLog) Count(redisId int64) (int64, error) {
+	var (
+		row *sql.Row
+		cnt int64
+	)
+	if redisId == 0 {
+		row = m.db.QueryRow("SELECT COUNT(*) FROM processlog")
+	} else {
+		row = m.db.QueryRow("SELECT COUNT(*) FROM processlog WHERE redis_id=?", redisId)
+	}
+	if err := row.Scan(&cnt); err != nil {
+		return 0, err
+	} else {
+		return cnt, nil
+	}
+}
+
+func (m *ProcessLog) GetList(redisId int64, offset int64, limit int64, orderDesc bool) ([]*ProcessLogRow, error) {
+	var (
+		orderDir string
+		ret      *sql.Rows
+		err      error
+	)
+	if orderDesc {
+		orderDir = "DESC"
+	} else {
+		orderDir = "ASC"
+	}
+	if redisId == 0 {
+		if limit > 0 {
+			sql := fmt.Sprintf("SELECT processlog.id,processlog.redis_id,processlog.process_time,processlog.maxmemory_before,processlog.maxmemory_after,rediscfg.address,rediscfg.remark FROM processlog INNER JOIN rediscfg ON processlog.redis_id=rediscfg.id ORDER BY processlog.id %s LIMIT ?,?", orderDir)
+			ret, err = m.db.Query(sql, offset, limit)
+		} else {
+			sql := fmt.Sprintf("SELECT processlog.id,processlog.redis_id,processlog.process_time,processlog.maxmemory_before,processlog.maxmemory_after,rediscfg.address,rediscfg.remark FROM processlog INNER JOIN rediscfg ON processlog.redis_id=rediscfg.id ORDER BY processlog.id %s", orderDir)
+			ret, err = m.db.Query(sql)
+		}
+
+	} else {
+		if limit > 0 {
+			sql := fmt.Sprintf("SELECT processlog.id,processlog.redis_id,processlog.process_time,processlog.maxmemory_before,processlog.maxmemory_after,rediscfg.address,rediscfg.remark FROM processlog INNER JOIN rediscfg ON processlog.redis_id=rediscfg.id WHERE processlog.redis_id=? ORDER BY processlog.id %s LIMIT ?,?", orderDir)
+			ret, err = m.db.Query(sql, redisId, offset, limit)
+		} else {
+			sql := fmt.Sprintf("SELECT processlog.id,processlog.redis_id,processlog.process_time,processlog.maxmemory_before,processlog.maxmemory_after,rediscfg.address,rediscfg.remark FROM processlog INNER JOIN rediscfg ON processlog.redis_id=rediscfg.id WHERE processlog.redis_id=? ORDER BY processlog.id %s", orderDir)
+			ret, err = m.db.Query(sql, redisId)
+		}
+	}
+	if err != nil {
+		return nil, err
+	} else {
+		rows := make([]*ProcessLogRow, 0)
+		for ret.Next() {
+			row := ProcessLogRow{}
+			err = ret.Scan(&row.Id, &row.RedisId, &row.ProcessTime, &row.MaxMemoryBefore, &row.MaxMemoryAfter, &row.RedisAddress, &row.RedisRemark)
+			if err != nil {
+				continue
+			}
+			rows = append(rows, &row)
+		}
+		return rows, nil
+	}
+}

+ 46 - 48
src/cnphper.com/model/rediscfg.go

@@ -10,20 +10,20 @@ type RedisCfg struct {
 }
 
 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
+	Id                 int64
+	Address            string
+	Remark             string
+	Password           string
+	MaxConnectWait     int64
+	MaxStatusFailed    int64
+	MinMemoryFree      int64
+	StepMemoryIncrease int64
+	MaxMemoryUsage     int64
+	MaxConnection      int64
+	MaxEviIncreased    int64
+	MaxQPS             int64
+	MailList           string
+	Disabled           bool
 }
 
 func NewRedisCfg(db *sql.DB) *RedisCfg {
@@ -35,9 +35,6 @@ func NewRedisCfg(db *sql.DB) *RedisCfg {
 }
 
 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不能为空")
 	}
@@ -45,28 +42,28 @@ func (m *RedisCfg) Insert(row *RedisCfgRow) (int64, error) {
 	if row.Disabled {
 		disabledInt = 1
 	}
-	ret, err := m.db.Exec(`insert into rediscfg(
+	ret, err := m.db.Exec(`INSERT INTO rediscfg(
 			address,
 			remark,
 			password,
 			max_connect_wait,
 			max_status_failed,
 			min_memory_free,
-			min_memory_free_pc,
+			step_memory_increase,
 			max_memory_usage,
 			max_connection,
 			max_evi_increased,
 			max_qps,
 			mail_list,
 			disabled
-		) values(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
+		) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
 		row.Address,
 		row.Remark,
 		row.Password,
 		row.MaxConnectWait,
 		row.MaxStatusFailed,
 		row.MinMemoryFree,
-		row.MinMemoryFreePC,
+		row.StepMemoryIncrease,
 		row.MaxMemoryUsage,
 		row.MaxConnection,
 		row.MaxEviIncreased,
@@ -91,28 +88,28 @@ func (m *RedisCfg) Update(row *RedisCfgRow) (int64, error) {
 	if row.Disabled {
 		disabledInt = 1
 	}
-	ret, err := m.db.Exec(`update rediscfg set
+	ret, err := m.db.Exec(`UPDATE rediscfg SET
 			address=?,
 			remark=?,
 			password=?,
 			max_connect_wait=?,
 			max_status_failed=?,
 			min_memory_free=?,
-			min_memory_free_pc=?,
+			step_memory_increase=?,
 			max_memory_usage=?,
 			max_connection=?,
 			max_evi_increased=?,
 			max_qps=?,
 			mail_list=?,
 			disabled=?
-		where id=?`,
+		WHERE id=?`,
 		row.Address,
 		row.Remark,
 		row.Password,
 		row.MaxConnectWait,
 		row.MaxStatusFailed,
 		row.MinMemoryFree,
-		row.MinMemoryFreePC,
+		row.StepMemoryIncrease,
 		row.MaxMemoryUsage,
 		row.MaxConnection,
 		row.MaxEviIncreased,
@@ -131,7 +128,7 @@ 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)
+	ret, err := m.db.Exec("DELETE FROM rediscfg WHERE id=?", id)
 	if err != nil {
 		return 0, err
 	}
@@ -142,20 +139,21 @@ 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=?`,
+	result := m.db.QueryRow(`SELECT id,
+				address,
+				remark,
+				password,
+				max_connect_wait,
+				max_status_failed,
+				min_memory_free,
+				step_memory_increase,
+				max_memory_usage,
+				max_connection,
+				max_evi_increased,
+				max_qps,
+				mail_list,
+				disabled
+			FROM rediscfg WHERE id=?`,
 		id,
 	)
 	row := RedisCfgRow{}
@@ -167,7 +165,7 @@ func (m *RedisCfg) Get(id int64) (*RedisCfgRow, error) {
 		&row.MaxConnectWait,
 		&row.MaxStatusFailed,
 		&row.MinMemoryFree,
-		&row.MinMemoryFreePC,
+		&row.StepMemoryIncrease,
 		&row.MaxMemoryUsage,
 		&row.MaxConnection,
 		&row.MaxEviIncreased,
@@ -189,37 +187,37 @@ func (m *RedisCfg) Get(id int64) (*RedisCfgRow, error) {
 func (m *RedisCfg) GetAll(disabledInt int) ([]*RedisCfgRow, error) {
 	var sql string
 	if disabledInt < 0 {
-		sql = `select id,
+		sql = `SELECT id,
 			address,
 			remark,
 			password,
 			max_connect_wait,
 			max_status_failed,
 			min_memory_free,
-			min_memory_free_pc,
+			step_memory_increase,
 			max_memory_usage,
 			max_connection,
 			max_evi_increased,
 			max_qps,
 			mail_list,
 			disabled
-		from rediscfg where disabled>? order by id`
+		FROM rediscfg WHERE disabled>? ORDER BY id`
 	} else {
-		sql = `select id,
+		sql = `SELECT id,
 			address,
 			remark,
 			password,
 			max_connect_wait,
 			max_status_failed,
 			min_memory_free,
-			min_memory_free_pc,
+			step_memory_increase,
 			max_memory_usage,
 			max_connection,
 			max_evi_increased,
 			max_qps,
 			mail_list,
 			disabled
-		from rediscfg where disabled=? order by id`
+		FROM rediscfg WHERE disabled=? ORDER BY id`
 	}
 	result, err := m.db.Query(sql, disabledInt)
 	if err != nil {
@@ -236,7 +234,7 @@ func (m *RedisCfg) GetAll(disabledInt int) ([]*RedisCfgRow, error) {
 			&row.MaxConnectWait,
 			&row.MaxStatusFailed,
 			&row.MinMemoryFree,
-			&row.MinMemoryFreePC,
+			&row.StepMemoryIncrease,
 			&row.MaxMemoryUsage,
 			&row.MaxConnection,
 			&row.MaxEviIncreased,

+ 47 - 8
src/cnphper.com/model/syscfg.go

@@ -3,6 +3,8 @@ package model
 import (
 	"database/sql"
 	"errors"
+	"fmt"
+	"strings"
 )
 
 type SysCfg struct {
@@ -27,7 +29,7 @@ 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)
+	ret, err := m.db.Exec("INSERT INTO syscfg(cfg_key,cfg_value) VALUES(?,?)", key, value)
 	if err != nil {
 		return 0, err
 	} else {
@@ -39,7 +41,7 @@ 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)
+	ret, err := m.db.Exec("DELETE FROM syscfg WHERE id=?", id)
 	if err != nil {
 		return 0, err
 	} else {
@@ -51,7 +53,7 @@ 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)
+	ret, err := m.db.Exec("DELETE FROM syscfg WHERE cfg_key=?", key)
 	if err != nil {
 		return 0, err
 	} else {
@@ -63,7 +65,7 @@ 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)
+	ret, err := m.db.Exec("UPDATE syscfg SET cfg_value=? WHERE id=?", value, id)
 	if err != nil {
 		return 0, err
 	} else {
@@ -75,7 +77,7 @@ 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)
+	ret, err := m.db.Exec("UPDATE syscfg SET cfg_value=? WHERE cfg_key=?", value, key)
 	if err != nil {
 		return 0, err
 	} else {
@@ -87,7 +89,7 @@ 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)
+	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 {
@@ -105,7 +107,7 @@ 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)
+	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 {
@@ -119,8 +121,33 @@ func (m *SysCfg) GetByKey(key string) (*SysCfgRow, error) {
 	}
 }
 
+func (m *SysCfg) GetByKeys(keys []string) (map[string]string, error) {
+	l := len(keys)
+	if l == 0 {
+		return nil, errors.New("KEY列表不能为空")
+	}
+	keysArgs := make([]interface{}, l)
+	for i := 0; i < l; i++ {
+		keysArgs[i] = keys[i]
+	}
+	placeholders := strings.Join(strings.Split(strings.Repeat("?", l), ""), ",")
+	sql := fmt.Sprintf("SELECT cfg_key,cfg_value FROM syscfg WHERE cfg_key IN(%s)", placeholders)
+	result, err := m.db.Query(sql, keysArgs...)
+	if err != nil {
+		return nil, err
+	} else {
+		values := make(map[string]string)
+		for result.Next() {
+			var k, v string
+			result.Scan(&k, &v)
+			values[k] = v
+		}
+		return values, nil
+	}
+}
+
 func (m *SysCfg) GetAll() ([]*SysCfgRow, error) {
-	result, err := m.db.Query("select id,cfg_key,cfg_value from syscfg order by id")
+	result, err := m.db.Query("SELECT id,cfg_key,cfg_value FROM syscfg ORDER BY id")
 	if err != nil {
 		return nil, err
 	}
@@ -135,3 +162,15 @@ func (m *SysCfg) GetAll() ([]*SysCfgRow, error) {
 	}
 	return rows, nil
 }
+
+func (m *SysCfg) Set(key, value string) (int64, error) {
+	if key == "" {
+		return 0, errors.New("KEY不能为空")
+	}
+	ret, err := m.db.Exec("REPLACE INTO syscfg(cfg_key,cfg_value) VALUES(?,?)", key, value)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}

+ 95 - 0
src/cnphper.com/model/syslog.go

@@ -0,0 +1,95 @@
+package model
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"time"
+)
+
+type SysLog struct {
+	Model
+}
+
+type SysLogRow struct {
+	Id       int64
+	LogTime  int64
+	LogLevel string
+	LogMsg   string
+}
+
+func NewSysLog(db *sql.DB) *SysLog {
+	return &SysLog{
+		Model{
+			db: db,
+		},
+	}
+}
+
+func (m *SysLog) Add(row *SysLogRow) (int64, error) {
+	if row.LogMsg == "" {
+		return 0, errors.New("日志内容不能为空")
+	}
+	if row.LogTime <= 0 {
+		row.LogTime = time.Now().Unix()
+	}
+	ret, err := m.db.Exec("INSERT INTO syslog(log_time,log_level,log_msg) VALUES(?,?,?)", row.LogTime, row.LogLevel, row.LogMsg)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.LastInsertId()
+	}
+}
+
+func (m *SysLog) CleanUntil(until int64) (int64, error) {
+	ret, err := m.db.Exec("DELETE FROM syslog WHERE log_time<?", until)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *SysLog) Count() (int64, error) {
+	var cnt int64
+	row := m.db.QueryRow("SELECT COUNT(*) FROM syslog")
+	if err := row.Scan(&cnt); err != nil {
+		return 0, err
+	} else {
+		return cnt, nil
+	}
+}
+
+func (m *SysLog) GetList(offset int64, limit int64, orderDesc bool) ([]*SysLogRow, error) {
+	var (
+		orderDir string
+		ret      *sql.Rows
+		err      error
+	)
+	if orderDesc {
+		orderDir = "DESC"
+	} else {
+		orderDir = "ASC"
+	}
+	if limit > 0 {
+		sql := fmt.Sprintf("SELECT id,log_time,log_level,log_msg FROM syslog ORDER BY id %s LIMIT ?,?", orderDir)
+		ret, err = m.db.Query(sql, offset, limit)
+	} else {
+		sql := fmt.Sprintf("SELECT id,log_time,log_level,log_msg FROM syslog ORDER BY id %s", orderDir)
+		ret, err = m.db.Query(sql)
+	}
+	if err != nil {
+		return nil, err
+	} else {
+		rows := make([]*SysLogRow, 0)
+		for ret.Next() {
+			row := SysLogRow{}
+			err = ret.Scan(&row.Id, &row.LogTime, &row.LogLevel, &row.LogMsg)
+			if err != nil {
+				continue
+			}
+			rows = append(rows, &row)
+		}
+		return rows, nil
+	}
+}

+ 118 - 0
src/cnphper.com/model/warnlog.go

@@ -0,0 +1,118 @@
+package model
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"time"
+)
+
+type WarnLog struct {
+	Model
+}
+
+type WarnLogRow struct {
+	Id           int64
+	RedisId      int64
+	RedisAddress string
+	RedisRemark  string
+	WarnTime     int64
+	WarnMsg      string
+}
+
+func NewWarnLog(db *sql.DB) *WarnLog {
+	return &WarnLog{
+		Model{
+			db: db,
+		},
+	}
+}
+
+func (m *WarnLog) Add(row *WarnLogRow) (int64, error) {
+	if row.RedisId <= 0 {
+		return 0, errors.New("Redis的ID必须是正整数")
+	}
+	if row.WarnMsg == "" {
+		return 0, errors.New("报警内容不能为空")
+	}
+	if row.WarnTime <= 0 {
+		row.WarnTime = time.Now().Unix()
+	}
+	ret, err := m.db.Exec("INSERT INTO warnlog(redis_id,warn_time,warn_msg) VALUES(?,?,?)", row.RedisId, row.WarnTime, row.WarnMsg)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.LastInsertId()
+	}
+}
+
+func (m *WarnLog) CleanUntil(until int64) (int64, error) {
+	ret, err := m.db.Exec("DELETE FROM warnlog WHERE warn_time<?", until)
+	if err != nil {
+		return 0, err
+	} else {
+		return ret.RowsAffected()
+	}
+}
+
+func (m *WarnLog) Count(redisId int64) (int64, error) {
+	var (
+		row *sql.Row
+		cnt int64
+	)
+	if redisId == 0 {
+		row = m.db.QueryRow("SELECT COUNT(*) FROM warnlog")
+	} else {
+		row = m.db.QueryRow("SELECT COUNT(*) FROM warnlog WHERE redis_id=?", redisId)
+	}
+	if err := row.Scan(&cnt); err != nil {
+		return 0, err
+	} else {
+		return cnt, nil
+	}
+}
+
+func (m *WarnLog) GetList(redisId int64, offset int64, limit int64, orderDesc bool) ([]*WarnLogRow, error) {
+	var (
+		orderDir string
+		ret      *sql.Rows
+		err      error
+	)
+	if orderDesc {
+		orderDir = "DESC"
+	} else {
+		orderDir = "ASC"
+	}
+	if redisId == 0 {
+		if limit > 0 {
+			sql := fmt.Sprintf("SELECT warnlog.id,warnlog.redis_id,warnlog.warn_time,warnlog.warn_msg,rediscfg.address,rediscfg.remark FROM warnlog INNER JOIN rediscfg ON warnlog.redis_id=rediscfg.id ORDER BY warnlog.id %s LIMIT ?,?", orderDir)
+			ret, err = m.db.Query(sql, offset, limit)
+		} else {
+			sql := fmt.Sprintf("SELECT warnlog.id,warnlog.redis_id,warnlog.warn_time,warnlog.warn_msg,rediscfg.address,rediscfg.remark FROM warnlog INNER JOIN rediscfg ON warnlog.redis_id=rediscfg.id ORDER BY warnlog.id %s", orderDir)
+			ret, err = m.db.Query(sql)
+		}
+
+	} else {
+		if limit > 0 {
+			sql := fmt.Sprintf("SELECT warnlog.id,warnlog.redis_id,warnlog.warn_time,warnlog.warn_msg,rediscfg.address,rediscfg.remark FROM warnlog INNER JOIN rediscfg ON warnlog.redis_id=rediscfg.id WHERE warnlog.redis_id=? ORDER BY warnlog.id %s LIMIT ?,?", orderDir)
+			ret, err = m.db.Query(sql, redisId, offset, limit)
+		} else {
+			sql := fmt.Sprintf("SELECT warnlog.id,warnlog.redis_id,warnlog.warn_time,warnlog.warn_msg,rediscfg.address,rediscfg.remark FROM warnlog INNER JOIN rediscfg ON warnlog.redis_id=rediscfg.id WHERE warnlog.redis_id=? ORDER BY warnlog.id %s", orderDir)
+			ret, err = m.db.Query(sql, redisId)
+		}
+	}
+	if err != nil {
+		return nil, err
+	} else {
+		rows := make([]*WarnLogRow, 0)
+		for ret.Next() {
+			row := WarnLogRow{}
+			err = ret.Scan(&row.Id, &row.RedisId, &row.WarnTime, &row.WarnMsg, &row.RedisAddress, &row.RedisRemark)
+			if err != nil {
+				continue
+			}
+			rows = append(rows, &row)
+		}
+		return rows, nil
+	}
+}

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

@@ -1,37 +0,0 @@
-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
-}

+ 157 - 0
src/cnphper.com/redisdog/functions.go

@@ -0,0 +1,157 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"cnphper.com/model"
+	"github.com/gomodule/redigo/redis"
+)
+
+type ErrorRet struct {
+	Errno int    `json:"errno"`
+	Error string `json:"error"`
+}
+
+type RedisInfo struct {
+	Id      int64
+	Address string
+	Remark  string
+	Error   string
+	Data    map[string]string
+}
+
+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
+}
+
+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),
+		redis.DialKeepAlive(time.Duration(0)),
+	}
+	if item.Password != "" {
+		options = append(options, redis.DialPassword(item.Password))
+	}
+	conn, err := redis.Dial("tcp", item.Address, options...)
+	if err != nil {
+		info.Error = err.Error()
+		return &info
+	}
+	defer conn.Close()
+	str, err := redis.String(conn.Do("info"))
+	if err == nil {
+		info.Data = parseRedisInfo(str)
+		config, err := redis.StringMap(conn.Do("config", "get", "save"))
+		if err == nil {
+			info.Data["save"] = config["save"]
+		} else {
+			info.Data["save"] = ""
+			info.Error = err.Error()
+		}
+	} else {
+		info.Error = err.Error()
+	}
+	return &info
+}
+
+func resetRedisConfig(item *model.RedisCfgRow, maxMemory int64) (bool, error) {
+	timeout := time.Second * time.Duration(item.MaxConnectWait)
+	options := []redis.DialOption{
+		redis.DialConnectTimeout(timeout),
+		redis.DialReadTimeout(timeout),
+		redis.DialWriteTimeout(timeout),
+		redis.DialKeepAlive(time.Duration(0)),
+	}
+	if item.Password != "" {
+		options = append(options, redis.DialPassword(item.Password))
+	}
+	conn, err := redis.Dial("tcp", item.Address, options...)
+	if err != nil {
+		return false, err
+	}
+	defer conn.Close()
+	ret, err := redis.String(conn.Do("config", "set", "maxmemory", fmt.Sprintf("%d", maxMemory)))
+	return ret == "OK", err
+}
+
+func ERROR_RET(errno int, errmsg string) []byte {
+	ret, _ := json.Marshal(ErrorRet{Errno: errno, Error: errmsg})
+	return ret
+}
+
+func number2size(n float64) string {
+	if n < 1024 {
+		return fmt.Sprintf("%d", int(n))
+	} else if n < 1024*1024 {
+		return fmt.Sprintf("%.2fKB", n/1024)
+	} else if n < 1024*1024*1024 {
+		return fmt.Sprintf("%.2fMB", n/1024/1024)
+	} else if n < 1024*1024*1024*1024 {
+		return fmt.Sprintf("%.2fGB", n/1024/1024/1024)
+	} else {
+		return fmt.Sprintf("%.2fTB", n/1024/1024/1024/1024)
+	}
+}
+
+func SYSLOG(level, msg string) {
+	if SysLogger == nil {
+		return
+	}
+	SysLogger.Add(&model.SysLogRow{
+		LogLevel: level,
+		LogMsg:   msg,
+	})
+}

+ 68 - 0
src/cnphper.com/redisdog/http_debug.go

@@ -0,0 +1,68 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strings"
+)
+
+func debug_sendmail(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	if req.Method == "GET" {
+		//视图输出
+		files := []string{
+			filepath.Join(Cfg.TmplDir, "debug", "sendmail.tmpl"),
+			filepath.Join(Cfg.TmplDir, "header.tmpl"),
+			filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+		}
+		tmpl, err := template.New("sendmail.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,
+				"Debug - 邮件测试",
+			})
+		}
+	} else {
+		sendto := req.PostForm.Get("sendto")
+		title := req.PostForm.Get("title")
+		content := req.PostForm.Get("content")
+		if sendto == "" {
+			ret, _ := json.Marshal(ErrorRet{Errno: 1, Error: "收件人不能为空"})
+			resp.Write(ret)
+			return
+		}
+		if title == "" {
+			ret, _ := json.Marshal(ErrorRet{Errno: 2, Error: "标题不能为空"})
+			resp.Write(ret)
+			return
+		}
+		if content == "" {
+			ret, _ := json.Marshal(ErrorRet{Errno: 3, Error: "内容不能为空"})
+			resp.Write(ret)
+			return
+		}
+		req := MailRequest{
+			sendto:  strings.Split(sendto, ";"),
+			title:   title,
+			content: content,
+		}
+		Sender.Push(&req)
+		ret, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+		resp.Write(ret)
+	}
+}

+ 129 - 0
src/cnphper.com/redisdog/http_index.go

@@ -0,0 +1,129 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"cnphper.com/model"
+)
+
+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 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 {
+		ret, _ := json.Marshal(ErrorRet{Errno: 0, Error: err.Error()})
+		resp.Write(ret)
+		return
+	}
+	stats := make([]*RedisInfo, 0)
+	for _, row := range rows {
+		stats = append(stats, queryRedisInfo(row))
+	}
+	ret, _ := json.Marshal(StatRet{Errno: 0, Error: "", Data: stats})
+	resp.Write(ret)
+}
+
+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 == "" {
+		resp.Write(ERROR_RET(1, "缺少ID字段!"))
+		return
+	}
+	id := int(0)
+	_, err := fmt.Sscanf(idstr, "%d", &id)
+	if err != nil {
+		resp.Write(ERROR_RET(2, "ID的值不是整数!"))
+		return
+	}
+	if id <= 0 {
+		resp.Write(ERROR_RET(3, "ID的值必须大于0!"))
+		return
+	}
+	mdlRedisCfg := model.NewRedisCfg(Db)
+	row, err := mdlRedisCfg.Get(int64(id))
+	if err != nil {
+		resp.Write(ERROR_RET(4, err.Error()))
+	} else {
+		info := queryRedisInfo(row)
+		if info.Error == "" {
+			ret, _ := json.Marshal(InfoRet{Errno: 0, Error: "", Data: info})
+			resp.Write(ret)
+		} else {
+			resp.Write(ERROR_RET(5, info.Error))
+		}
+	}
+}
+
+func index_favicon(resp http.ResponseWriter, req *http.Request) {
+	file, err := os.OpenFile(filepath.Join(Cfg.ResourcesDir, "favicon.ico"), os.O_RDONLY, 0644)
+	if err != nil {
+		resp.WriteHeader(404)
+		return
+	}
+	defer file.Close()
+	resp.Header().Set("Content-Type", "image/x-icon")
+	data := make([]byte, 1024)
+	for {
+		if l, err := file.Read(data); err == io.EOF {
+			break
+		} else if err != nil {
+			break
+		} else {
+			resp.Write(data[0:l])
+		}
+	}
+}

+ 93 - 0
src/cnphper.com/redisdog/http_log_autoprocess.go

@@ -0,0 +1,93 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type LogProcessLogListRetData struct {
+	Count int64
+	List  []*model.ProcessLogRow
+}
+
+type LogProcessLogListRet struct {
+	Errno int                      `json:"errno"`
+	Error string                   `json:"error"`
+	Data  LogProcessLogListRetData `json:"data"`
+}
+
+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
+	}
+	req.ParseForm()
+	redisId, size, page, desc := 0, 50, 1, true
+	if redisIdStr := req.Form.Get("redis_id"); redisIdStr != "" {
+		if redisId, _ = strconv.Atoi(redisIdStr); redisId <= 1 {
+			redisId = 0
+		}
+	}
+	if sizeStr := req.Form.Get("size"); sizeStr != "" {
+		if size, _ = strconv.Atoi(sizeStr); size <= 1 {
+			size = 50
+		}
+	}
+	if pageStr := req.Form.Get("page"); pageStr != "" {
+		if page, _ = strconv.Atoi(pageStr); page < 1 {
+			page = 1
+		}
+	}
+	if orderStr := req.Form.Get("order"); orderStr != "" {
+		if orderStr == "ASC" {
+			desc = false
+		}
+	}
+	mdl := model.NewProcessLog(Db)
+	cnt, err := mdl.Count(int64(redisId))
+	if err != nil {
+		resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+	} else {
+		logs, err := mdl.GetList(int64(redisId), int64(size*(page-1)), int64(size), desc)
+		if err != nil {
+			resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+		} else {
+			ret, _ := json.Marshal(LogProcessLogListRet{Errno: 0, Error: "", Data: LogProcessLogListRetData{Count: cnt, List: logs}})
+			resp.Write(ret)
+		}
+	}
+}

+ 93 - 0
src/cnphper.com/redisdog/http_log_monitor.go

@@ -0,0 +1,93 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type LogMonitorLogListRetData struct {
+	Count int64
+	List  []*model.MonitorLogRow
+}
+
+type LogMonitorLogListRet struct {
+	Errno int                      `json:"errno"`
+	Error string                   `json:"error"`
+	Data  LogMonitorLogListRetData `json:"data"`
+}
+
+func log_monitor(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	//视图输出
+	files := []string{
+		filepath.Join(Cfg.TmplDir, "log", "monitor.tmpl"),
+		filepath.Join(Cfg.TmplDir, "header.tmpl"),
+		filepath.Join(Cfg.TmplDir, "navbar.tmpl"),
+	}
+	tmpl, err := template.New("monitor.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_monitor_list(resp http.ResponseWriter, req *http.Request) {
+	_, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	redisId, size, page, desc := 0, 50, 1, true
+	if redisIdStr := req.Form.Get("redis_id"); redisIdStr != "" {
+		if redisId, _ = strconv.Atoi(redisIdStr); redisId <= 1 {
+			redisId = 0
+		}
+	}
+	if sizeStr := req.Form.Get("size"); sizeStr != "" {
+		if size, _ = strconv.Atoi(sizeStr); size <= 1 {
+			size = 50
+		}
+	}
+	if pageStr := req.Form.Get("page"); pageStr != "" {
+		if page, _ = strconv.Atoi(pageStr); page < 1 {
+			page = 1
+		}
+	}
+	if orderStr := req.Form.Get("order"); orderStr != "" {
+		if orderStr == "ASC" {
+			desc = false
+		}
+	}
+	mdl := model.NewMonitorLog(Db)
+	cnt, err := mdl.Count(int64(redisId))
+	if err != nil {
+		resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+	} else {
+		logs, err := mdl.GetList(int64(redisId), int64(size*(page-1)), int64(size), desc)
+		if err != nil {
+			resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+		} else {
+			ret, _ := json.Marshal(LogMonitorLogListRet{Errno: 0, Error: "", Data: LogMonitorLogListRetData{Count: cnt, List: logs}})
+			resp.Write(ret)
+		}
+	}
+}

+ 88 - 0
src/cnphper.com/redisdog/http_log_syslog.go

@@ -0,0 +1,88 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type LogSysLogListRetData struct {
+	Count int64
+	List  []*model.SysLogRow
+}
+
+type LogSysLogListRet struct {
+	Errno int                  `json:"errno"`
+	Error string               `json:"error"`
+	Data  LogSysLogListRetData `json:"data"`
+}
+
+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
+	}
+	req.ParseForm()
+	size, page, desc := 50, 1, true
+	if sizeStr := req.Form.Get("size"); sizeStr != "" {
+		if size, _ = strconv.Atoi(sizeStr); size <= 1 {
+			size = 50
+		}
+	}
+	if pageStr := req.Form.Get("page"); pageStr != "" {
+		if page, _ = strconv.Atoi(pageStr); page < 1 {
+			page = 1
+		}
+	}
+	if orderStr := req.Form.Get("order"); orderStr != "" {
+		if orderStr == "ASC" {
+			desc = false
+		}
+	}
+	mdl := model.NewSysLog(Db)
+	cnt, err := mdl.Count()
+	if err != nil {
+		resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+	} else {
+		logs, err := mdl.GetList(int64(size*(page-1)), int64(size), desc)
+		if err != nil {
+			resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+		} else {
+			ret, _ := json.Marshal(LogSysLogListRet{Errno: 0, Error: "", Data: LogSysLogListRetData{Count: cnt, List: logs}})
+			resp.Write(ret)
+		}
+	}
+}

+ 93 - 0
src/cnphper.com/redisdog/http_log_warn.go

@@ -0,0 +1,93 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type LogWarnLogListRetData struct {
+	Count int64
+	List  []*model.WarnLogRow
+}
+
+type LogWarnLogListRet struct {
+	Errno int                   `json:"errno"`
+	Error string                `json:"error"`
+	Data  LogWarnLogListRetData `json:"data"`
+}
+
+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
+	}
+	req.ParseForm()
+	redisId, size, page, desc := 0, 50, 1, true
+	if redisIdStr := req.Form.Get("redis_id"); redisIdStr != "" {
+		if redisId, _ = strconv.Atoi(redisIdStr); redisId <= 1 {
+			redisId = 0
+		}
+	}
+	if sizeStr := req.Form.Get("size"); sizeStr != "" {
+		if size, _ = strconv.Atoi(sizeStr); size <= 1 {
+			size = 50
+		}
+	}
+	if pageStr := req.Form.Get("page"); pageStr != "" {
+		if page, _ = strconv.Atoi(pageStr); page < 1 {
+			page = 1
+		}
+	}
+	if orderStr := req.Form.Get("order"); orderStr != "" {
+		if orderStr == "ASC" {
+			desc = false
+		}
+	}
+	mdl := model.NewWarnLog(Db)
+	cnt, err := mdl.Count(int64(redisId))
+	if err != nil {
+		resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+	} else {
+		logs, err := mdl.GetList(int64(redisId), int64(size*(page-1)), int64(size), desc)
+		if err != nil {
+			resp.Write(ERROR_RET(1, fmt.Sprintf("查询失败:%s", err.Error())))
+		} else {
+			ret, _ := json.Marshal(LogWarnLogListRet{Errno: 0, Error: "", Data: LogWarnLogListRetData{Count: cnt, List: logs}})
+			resp.Write(ret)
+		}
+	}
+}

+ 5 - 8
src/cnphper.com/redisdog/login.go

@@ -31,17 +31,14 @@ func login_index(resp http.ResponseWriter, req *http.Request) {
 		account := req.PostForm.Get("account")
 		passwd := req.PostForm.Get("passwd")
 		if account == "" {
-			json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "用户名不能为空!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(1, "用户名不能为空!"))
 		} else if passwd == "" {
-			json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "密码不能为空!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(2, "密码不能为空!"))
 		} 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)
+				resp.Write(ERROR_RET(3, err.Error()))
 			} else {
 				sessid := makeSessionId(req.RemoteAddr)
 				sess := Session{
@@ -55,8 +52,8 @@ func login_index(resp http.ResponseWriter, req *http.Request) {
 					Path:  "/",
 				}
 				resp.Header().Add("Set-Cookie", cookie.String())
-				json, _ := json.Marshal(LoginRet{Errno: 0, Error: "", Data: sess})
-				resp.Write(json)
+				ret, _ := json.Marshal(LoginRet{Errno: 0, Error: "", Data: sess})
+				resp.Write(ret)
 			}
 		}
 	}

+ 14 - 24
src/cnphper.com/redisdog/profile.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 	"html/template"
 	"io"
@@ -52,38 +51,29 @@ func profile_passwd(resp http.ResponseWriter, req *http.Request) {
 		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)
+			resp.Write(ERROR_RET(1, "旧密码不能为空!"))
 		} else if len(oldpwd) < 6 {
-			json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "旧密码格式不正确空!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(2, "旧密码格式不正确空!"))
 		} else if newpwd == "" {
-			json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "新密码不能为空!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(3, "新密码不能为空!"))
 		} else if len(newpwd) < 6 {
-			json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "旧密码格式不正确空!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(4, "旧密码格式不正确空!"))
 		} else if oldpwd == newpwd {
-			json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "新密码不能与旧密码相同!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(5, "新密码不能与旧密码相同!"))
 		} 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)
+				resp.Write(ERROR_RET(6, "旧密码不正确!"))
 			} else {
-				json, _ := json.Marshal(ErrorRet{Errno: 8, Error: "未知错误!"})
-				resp.Write(json)
+				affected, err := mdlAccounts.UpdatePassword(row.Id, newpwd)
+				if err != nil {
+					resp.Write(ERROR_RET(7, err.Error()))
+				} else if affected > 0 {
+					resp.Write(ERROR_RET(0, ""))
+				} else {
+					resp.Write(ERROR_RET(7, "未知错误!"))
+				}
 			}
 		}
 	}

+ 39 - 66
src/cnphper.com/redisdog/syscfg_account.go

@@ -61,11 +61,10 @@ func syscfg_account_list(resp http.ResponseWriter, req *http.Request) {
 	mdlAccounts := model.NewAccounts(Db)
 	list, err := mdlAccounts.GetAll()
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, err.Error()))
 	} else {
-		json, _ := json.Marshal(SyscfgAccountListRet{Errno: 0, Error: "", Data: list})
-		resp.Write(json)
+		ret, _ := json.Marshal(SyscfgAccountListRet{Errno: 0, Error: "", Data: list})
+		resp.Write(ret)
 	}
 }
 
@@ -77,24 +76,21 @@ func syscfg_account_get(resp http.ResponseWriter, req *http.Request) {
 	req.ParseForm()
 	idStr := req.Form.Get("id")
 	if idStr == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, "ID不能为空!"))
 		return
 	}
 	id, err := strconv.Atoi(idStr)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, err.Error()))
 		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)
+		resp.Write(ERROR_RET(3, err.Error()))
 	} else {
-		json, _ := json.Marshal(SyscfgAccountGetRet{Errno: 0, Error: "", Data: item})
-		resp.Write(json)
+		ret, _ := json.Marshal(SyscfgAccountGetRet{Errno: 0, Error: "", Data: item})
+		resp.Write(ret)
 	}
 }
 
@@ -115,53 +111,44 @@ func syscfg_account_set(resp http.ResponseWriter, req *http.Request) {
 		IdInt = 0
 	}
 	if Account == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "账号不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, "账号不能为空!"))
 		return
 	}
 	reg := regexp.MustCompile(`^[A-Za-z]\w{1,19}$`)
 	if !reg.MatchString(Account) {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "账号格式不正确!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, "账号格式不正确!"))
 		return
 	}
 	if Name == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "姓名不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(3, "姓名不能为空!"))
 		return
 	}
 	if IdInt == 0 {
 		if Password == "" {
-			json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "密码不能为空!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(4, "密码不能为空!"))
 			return
 		}
 		if len(Password) < 6 {
-			json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "密码长度不能小于6!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(5, "密码长度不能小于6!"))
 			return
 		}
 	}
 	if IsSuper == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 6, Error: "请选择是否管理员!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(6, "请选择是否管理员!"))
 		return
 	}
 	if Disabled == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 7, Error: "状态不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(7, "状态不能为空!"))
 		return
 	}
 	IsSuperInt, err := strconv.Atoi(IsSuper)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 8, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(8, err.Error()))
 		return
 	}
 	DisabledInt, err := strconv.Atoi(Disabled)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 9, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(9, err.Error()))
 		return
 	}
 	newRow := model.AccountsRow{
@@ -176,96 +163,82 @@ func syscfg_account_set(resp http.ResponseWriter, req *http.Request) {
 	if IdInt > 0 {
 		affected, err := mdlAccounts.Update(&newRow)
 		if err != nil {
-			json, _ := json.Marshal(ErrorRet{Errno: 10, Error: err.Error()})
-			resp.Write(json)
+			resp.Write(ERROR_RET(10, err.Error()))
 		} else if affected > 0 {
-			json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
-			resp.Write(json)
+			resp.Write(ERROR_RET(0, ""))
 		} else {
-			json, _ := json.Marshal(ErrorRet{Errno: 11, Error: "更新失败!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(11, "更新失败!"))
 		}
 	} else {
 		newRowId, err := mdlAccounts.Insert(&newRow)
 		if err != nil {
-			json, _ := json.Marshal(ErrorRet{Errno: 22, Error: err.Error()})
-			resp.Write(json)
+			resp.Write(ERROR_RET(22, err.Error()))
 		} else if newRowId > 0 {
-			json, _ := json.Marshal(SyscfgRedisAddRet{Errno: 0, Error: "", Data: newRowId})
-			resp.Write(json)
+			ret, _ := json.Marshal(SyscfgRedisAddRet{Errno: 0, Error: "", Data: newRowId})
+			resp.Write(ret)
 		} else {
-			json, _ := json.Marshal(ErrorRet{Errno: 23, Error: "新增失败!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(23, "新增失败!"))
 		}
 	}
 }
 
 func syscfg_account_del(resp http.ResponseWriter, req *http.Request) {
-	_, ok := checkLogin(resp, req)
+	sess, 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)
+		resp.Write(ERROR_RET(1, "ID不能为空!"))
 		return
 	}
 	id, err := strconv.Atoi(idStr)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, err.Error()))
 		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)
+		resp.Write(ERROR_RET(3, err.Error()))
 	} else if affected > 0 {
-		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
-		resp.Write(json)
+		resp.Write(ERROR_RET(0, ""))
+		SYSLOG("WARN", fmt.Sprintf("管理员#%d %s %s删除了账号 #%d", sess.Account.Id, sess.Account.Name, sess.Account.Account, id))
 	} else {
-		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "操作失败!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(4, "操作失败!"))
 	}
 }
 
 func syscfg_account_reset_pwd(resp http.ResponseWriter, req *http.Request) {
-	_, ok := checkLogin(resp, req)
+	sess, 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)
+		resp.Write(ERROR_RET(1, "ID不能为空!"))
 		return
 	}
 	id, err := strconv.Atoi(idStr)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, err.Error()))
 		return
 	}
 	password := req.PostForm.Get("password")
 	if len(password) < 6 {
-		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "密码长度不能小于6位!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(3, "密码长度不能小于6位!"))
 		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)
+		resp.Write(ERROR_RET(4, err.Error()))
 	} else if affected > 0 {
-		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
-		resp.Write(json)
+		resp.Write(ERROR_RET(0, ""))
+		SYSLOG("WARN", fmt.Sprintf("管理员#%d %s %s修改了账号 #%d的密码", sess.Account.Id, sess.Account.Name, sess.Account.Account, id))
 	} else {
-		json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "操作失败!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(5, "操作失败!"))
 	}
 }

+ 114 - 0
src/cnphper.com/redisdog/http_syscfg_misc.go

@@ -0,0 +1,114 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"cnphper.com/model"
+)
+
+type SysCfgMiscGetRet struct {
+	Errno int               `json:"errno"`
+	Error string            `json:"error"`
+	Data  map[string]string `json:"data"`
+}
+
+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
+	}
+	mdl := model.NewSysCfg(Db)
+	values, err := mdl.GetByKeys([]string{"monitor_loop_interval", "monitor_mail_interval", "log_kept_days"})
+	if err != nil {
+		resp.Write(ERROR_RET(1, err.Error()))
+	} else {
+		ret, _ := json.Marshal(SysCfgMiscGetRet{Errno: 0, Error: "", Data: values})
+		resp.Write(ret)
+	}
+}
+
+func syscfg_misc_set(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	moniotrLoopIntervalStr := req.PostForm.Get("monitor_loop_interval")
+	if moniotrLoopIntervalStr == "" {
+		resp.Write(ERROR_RET(1, "检查间隔不能为空"))
+		return
+	}
+	monitorMailIntervalStr := req.PostForm.Get("monitor_mail_interval")
+	if monitorMailIntervalStr == "" {
+		resp.Write(ERROR_RET(2, "报警邮件发送间隔不能为空"))
+		return
+	}
+	logKeptDaysStr := req.PostForm.Get("log_kept_days")
+	if logKeptDaysStr == "" {
+		resp.Write(ERROR_RET(3, "日志保留天数不能为空"))
+		return
+	}
+	if _, err := strconv.Atoi(moniotrLoopIntervalStr); err != nil {
+		resp.Write(ERROR_RET(4, "检查间隔不是有效的数字值"))
+		return
+	}
+	if _, err := strconv.Atoi(monitorMailIntervalStr); err != nil {
+		resp.Write(ERROR_RET(5, "报警邮件发送间隔不是有效的数字值"))
+		return
+	}
+	if _, err := strconv.Atoi(logKeptDaysStr); err != nil {
+		resp.Write(ERROR_RET(6, "日志保留天数不是有效的数字值"))
+		return
+	}
+	mdl := model.NewSysCfg(Db)
+	if _, err := mdl.Set("monitor_loop_interval", moniotrLoopIntervalStr); err != nil {
+		resp.Write(ERROR_RET(7, "操作失败(monitor_loop_interval)"))
+	}
+	if _, err := mdl.Set("monitor_mail_interval", monitorMailIntervalStr); err != nil {
+		resp.Write(ERROR_RET(8, "操作失败(monitor_mail_interval)"))
+	}
+	if _, err := mdl.Set("log_kept_days", logKeptDaysStr); err != nil {
+		resp.Write(ERROR_RET(9, "操作失败(log_kept_days)"))
+	}
+	SYSLOG("WARN", fmt.Sprintf("管理员#%d %s %s修改了其它配置", sess.Account.Id, sess.Account.Account, sess.Account.Account))
+	//更新配置缓存
+	err := SysCfg.Load()
+	if err != nil {
+		resp.Write(ERROR_RET(7, fmt.Sprintf("配置更新成功,但未能成功刷新:%s", err.Error())))
+	} else {
+		resp.Write(ERROR_RET(0, ""))
+	}
+}

+ 60 - 93
src/cnphper.com/redisdog/syscfg_redis.go

@@ -66,11 +66,10 @@ func syscfg_redis_list(resp http.ResponseWriter, req *http.Request) {
 	mdlRedisCfg := model.NewRedisCfg(Db)
 	rows, err := mdlRedisCfg.GetAll(-1)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, err.Error()))
 	} else {
-		json, _ := json.Marshal(SyscfgRedisListRet{Errno: 0, Error: "", Data: rows})
-		resp.Write(json)
+		ret, _ := json.Marshal(SyscfgRedisListRet{Errno: 0, Error: "", Data: rows})
+		resp.Write(ret)
 	}
 }
 
@@ -82,24 +81,21 @@ func syscfg_redis_get(resp http.ResponseWriter, req *http.Request) {
 	req.ParseForm()
 	idStr := req.Form.Get("id")
 	if idStr == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, "ID不能为空!"))
 		return
 	}
 	id, err := strconv.Atoi(idStr)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, err.Error()))
 		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)
+		resp.Write(ERROR_RET(3, err.Error()))
 	} else {
-		json, _ := json.Marshal(SyscfgRedisGetRet{Errno: 0, Error: "", Data: item})
-		resp.Write(json)
+		ret, _ := json.Marshal(SyscfgRedisGetRet{Errno: 0, Error: "", Data: item})
+		resp.Write(ret)
 	}
 }
 
@@ -116,7 +112,7 @@ func syscfg_redis_set(resp http.ResponseWriter, req *http.Request) {
 	MaxConnectWait := req.PostForm.Get("MaxConnectWait")
 	MaxStatusFailed := req.PostForm.Get("MaxStatusFailed")
 	MinMemoryFree := req.PostForm.Get("MinMemoryFree")
-	MinMemoryFreePC := req.PostForm.Get("MinMemoryFreePC")
+	StepMemoryIncrease := req.PostForm.Get("StepMemoryIncrease")
 	MaxMemoryUsage := req.PostForm.Get("MaxMemoryUsage")
 	MaxConnection := req.PostForm.Get("MaxConnection")
 	MaxEviIncreased := req.PostForm.Get("MaxEviIncreased")
@@ -124,63 +120,51 @@ func syscfg_redis_set(resp http.ResponseWriter, req *http.Request) {
 	MailList := req.PostForm.Get("MailList")
 	Disabled := req.PostForm.Get("Disabled")
 	if Address == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "Redis服务地址不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, "Redis服务地址不能为空!"))
 		return
 	}
 	if Remark == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: "Redis备注名称不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, "Redis备注名称不能为空!"))
 		return
 	}
 	if MaxConnectWait == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 3, Error: "连接超时时长不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(3, "连接超时时长不能为空!"))
 		return
 	}
 	if MaxStatusFailed == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "最大状态检测失败次数不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(4, "最大状态检测失败次数不能为空!"))
 		return
 	}
 	if MinMemoryFree == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 5, Error: "最小可用内存空间不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(5, "最小可用内存空间不能为空!"))
 		return
 	}
-	if MinMemoryFreePC == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 6, Error: "最小可用内存空间百分比不能为空!"})
-		resp.Write(json)
+	if StepMemoryIncrease == "" {
+		resp.Write(ERROR_RET(6, "单次扩容大小不能为空!"))
 		return
 	}
 	if MaxMemoryUsage == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 7, Error: "最大可用内存空间不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(7, "最大可用内存空间不能为空!"))
 		return
 	}
 	if MaxConnection == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 8, Error: "最大并发连接数不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(8, "最大并发连接数不能为空!"))
 		return
 	}
 	if MaxEviIncreased == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 9, Error: "最大新增淘汰记录数不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(9, "最大新增淘汰记录数不能为空!"))
 		return
 	}
 	if MaxQPS == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 10, Error: "最大QPS值不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(10, "最大QPS值不能为空!"))
 		return
 	}
 	if MailList == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 11, Error: "报警邮件接收邮箱列表不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(11, "报警邮件接收邮箱列表不能为空!"))
 		return
 	}
 	if Disabled == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 12, Error: "状态不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(12, "状态不能为空!"))
 		return
 	}
 	IdInt, err := strconv.Atoi(Id)
@@ -189,98 +173,86 @@ func syscfg_redis_set(resp http.ResponseWriter, req *http.Request) {
 	}
 	MaxConnectWaitInt, err := strconv.Atoi(MaxConnectWait)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 13, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(13, err.Error()))
 		return
 	}
 	MaxStatusFailedInt, err := strconv.Atoi(MaxStatusFailed)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 14, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(14, err.Error()))
 		return
 	}
 	MinMemoryFreeInt, err := strconv.Atoi(MinMemoryFree)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 15, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(15, err.Error()))
 		return
 	}
-	MinMemoryFreePCInt, err := strconv.Atoi(MinMemoryFreePC)
+	StepMemoryIncreaseInt, err := strconv.Atoi(StepMemoryIncrease)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 16, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(16, err.Error()))
 		return
 	}
 	MaxMemoryUsageInt, err := strconv.Atoi(MaxMemoryUsage)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 17, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(17, err.Error()))
 		return
 	}
 	MaxConnectionInt, err := strconv.Atoi(MaxConnection)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 18, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(18, err.Error()))
 		return
 	}
 	MaxEviIncreasedInt, err := strconv.Atoi(MaxEviIncreased)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 19, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(19, err.Error()))
 		return
 	}
 	MaxQPSInt, err := strconv.Atoi(MaxQPS)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 20, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(20, err.Error()))
 		return
 	}
 	DisabledInt, err := strconv.Atoi(Disabled)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 21, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(21, err.Error()))
 		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,
+		Id:                 int64(IdInt),
+		Address:            Address,
+		Remark:             Remark,
+		Password:           Password,
+		MaxConnectWait:     int64(MaxConnectWaitInt),
+		MaxStatusFailed:    int64(MaxStatusFailedInt),
+		MinMemoryFree:      int64(MinMemoryFreeInt),
+		StepMemoryIncrease: int64(StepMemoryIncreaseInt),
+		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)
+			resp.Write(ERROR_RET(22, err.Error()))
 		} else if affected > 0 {
-			json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
-			resp.Write(json)
+			resp.Write(ERROR_RET(0, ""))
 		} else {
-			json, _ := json.Marshal(ErrorRet{Errno: 23, Error: "更新失败!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(23, "更新失败!"))
 		}
 	} else {
 		newRowId, err := mdlRedisCfg.Insert(&newRow)
 		if err != nil {
-			json, _ := json.Marshal(ErrorRet{Errno: 22, Error: err.Error()})
-			resp.Write(json)
+			ret, _ := json.Marshal(ErrorRet{Errno: 22, Error: err.Error()})
+			resp.Write(ret)
+			resp.Write(ERROR_RET(22, err.Error()))
 		} else if newRowId > 0 {
-			json, _ := json.Marshal(SyscfgRedisAddRet{Errno: 0, Error: "", Data: newRowId})
-			resp.Write(json)
+			ret, _ := json.Marshal(SyscfgRedisAddRet{Errno: 0, Error: "", Data: newRowId})
+			resp.Write(ret)
 		} else {
-			json, _ := json.Marshal(ErrorRet{Errno: 23, Error: "新增失败!"})
-			resp.Write(json)
+			resp.Write(ERROR_RET(23, "新增失败!"))
 		}
 	}
 }
@@ -293,26 +265,21 @@ func syscfg_redis_del(resp http.ResponseWriter, req *http.Request) {
 	req.ParseForm()
 	idStr := req.Form.Get("id")
 	if idStr == "" {
-		json, _ := json.Marshal(ErrorRet{Errno: 1, Error: "ID不能为空!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(1, "ID不能为空!"))
 		return
 	}
 	id, err := strconv.Atoi(idStr)
 	if err != nil {
-		json, _ := json.Marshal(ErrorRet{Errno: 2, Error: err.Error()})
-		resp.Write(json)
+		resp.Write(ERROR_RET(2, err.Error()))
 		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)
+		resp.Write(ERROR_RET(3, err.Error()))
 	} else if affected > 0 {
-		json, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
-		resp.Write(json)
+		resp.Write(ERROR_RET(0, ""))
 	} else {
-		json, _ := json.Marshal(ErrorRet{Errno: 4, Error: "操作失败!"})
-		resp.Write(json)
+		resp.Write(ERROR_RET(4, "操作失败!"))
 	}
 }

+ 130 - 0
src/cnphper.com/redisdog/http_syscfg_warn.go

@@ -0,0 +1,130 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"path/filepath"
+
+	"cnphper.com/model"
+)
+
+type SysCfgWarnGetRet struct {
+	Errno int               `json:"errno"`
+	Error string            `json:"error"`
+	Data  map[string]string `json:"data"`
+}
+
+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
+	}
+	mdl := model.NewSysCfg(Db)
+	values, err := mdl.GetByKeys([]string{"smtp_host", "smtp_port", "smtp_user", "smtp_pwd", "smtp_sender"})
+	if err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 1, Error: err.Error()})
+		resp.Write(ret)
+	} else {
+		ret, _ := json.Marshal(SysCfgWarnGetRet{Errno: 0, Error: "", Data: values})
+		resp.Write(ret)
+	}
+}
+
+func syscfg_warn_set(resp http.ResponseWriter, req *http.Request) {
+	sess, ok := checkLogin(resp, req)
+	if !ok {
+		return
+	}
+	req.ParseForm()
+	smtpHost := req.PostForm.Get("smtp_host")
+	if smtpHost == "" {
+		ret, _ := json.Marshal(ErrorRet{Errno: 1, Error: "SMTP HOST不能为空"})
+		resp.Write(ret)
+		return
+	}
+	smtpPort := req.PostForm.Get("smtp_port")
+	if smtpPort == "" {
+		ret, _ := json.Marshal(ErrorRet{Errno: 2, Error: "SMTP端口不能为空"})
+		resp.Write(ret)
+		return
+	}
+	smtpUser := req.PostForm.Get("smtp_user")
+	if smtpUser == "" {
+		ret, _ := json.Marshal(ErrorRet{Errno: 3, Error: "SMTP用户不能为空"})
+		resp.Write(ret)
+		return
+	}
+	smtpPwd := req.PostForm.Get("smtp_pwd")
+	if smtpPwd == "" {
+		ret, _ := json.Marshal(ErrorRet{Errno: 4, Error: "SMTP密码不能为空"})
+		resp.Write(ret)
+		return
+	}
+	smtpSender := req.PostForm.Get("smtp_sender")
+	if smtpSender == "" {
+		ret, _ := json.Marshal(ErrorRet{Errno: 5, Error: "SMTP署名不能为空"})
+		resp.Write(ret)
+		return
+	}
+	mdl := model.NewSysCfg(Db)
+	if _, err := mdl.Set("smtp_host", smtpHost); err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 6, Error: "操作失败(host)"})
+		resp.Write(ret)
+	}
+	if _, err := mdl.Set("smtp_port", smtpPort); err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 6, Error: "操作失败(port)"})
+		resp.Write(ret)
+	}
+	if _, err := mdl.Set("smtp_user", smtpUser); err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 6, Error: "操作失败(user)"})
+		resp.Write(ret)
+	}
+	if _, err := mdl.Set("smtp_pwd", smtpPwd); err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 6, Error: "操作失败(pwd)"})
+		resp.Write(ret)
+	}
+	if _, err := mdl.Set("smtp_sender", smtpSender); err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 6, Error: "操作失败(sender)"})
+		resp.Write(ret)
+	}
+	SYSLOG("WARN", fmt.Sprintf("管理员#%d %s %s修改了SMTP配置", sess.Account.Id, sess.Account.Account, sess.Account.Account))
+	//更新配置缓存
+	err := SysCfg.Load()
+	if err != nil {
+		ret, _ := json.Marshal(ErrorRet{Errno: 7, Error: fmt.Sprintf("配置更新成功,但未能成功刷新:%s", err.Error())})
+		resp.Write(ret)
+	} else {
+		ret, _ := json.Marshal(ErrorRet{Errno: 0, Error: ""})
+		resp.Write(ret)
+	}
+}

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

@@ -1,173 +0,0 @@
-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)
-		}
-	}
-}

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

@@ -1,43 +0,0 @@
-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
-	}
-}

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

@@ -1,43 +0,0 @@
-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
-	}
-}

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

@@ -1,43 +0,0 @@
-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
-	}
-}

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

@@ -1,43 +0,0 @@
-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
-	}
-}

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

@@ -0,0 +1,87 @@
+package main
+
+import (
+	"fmt"
+	"net/smtp"
+	"strings"
+	"time"
+)
+
+type MailRequest struct {
+	sendto  []string
+	title   string
+	content string
+}
+
+type MailSender struct {
+	queue chan *MailRequest
+}
+
+const (
+	LOOP_DELAY      = 3
+	LOOP_QUEUE_SIZE = 10
+	PUSH_TIMEOUT    = 3
+)
+
+func NewMailSender() *MailSender {
+	return &MailSender{
+		queue: make(chan *MailRequest, 100),
+	}
+}
+
+func (s *MailSender) Loop() {
+	timer := time.NewTimer(time.Second * LOOP_DELAY)
+	timeouted := false
+	for {
+		list := make([]*MailRequest, 0)
+		//一次取10条记录
+		for i := 0; i < LOOP_QUEUE_SIZE; i++ {
+			timer.Reset(time.Second * LOOP_DELAY)
+			select {
+			case req, ok := <-s.queue:
+				timeouted = false
+				timer.Stop()
+				if ok {
+					list = append(list, req)
+				}
+			case <-timer.C:
+				timeouted = true
+			}
+			//如果不足10条也返回
+			if timeouted {
+				break
+			}
+		}
+		//发送邮件
+		if len(list) > 0 {
+			s.Send(list)
+		}
+	}
+}
+
+func (s *MailSender) Send(list []*MailRequest) {
+	if len(list) == 0 {
+		return
+	}
+	//取SMTP配置
+	cfg := SysCfg.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:%s", cfg["smtp_host"], cfg["smtp_port"])
+	//循环发送
+	for _, req := range list {
+		data := "From: " + cfg["smtp_user"] + "\r\nSender: " + cfg["smtp_sender"] + "\r\nTo: " + strings.Join(req.sendto, ";") + "\r\nContent-Type: text/html;charset=utf-8\r\nSubject: " + req.title + "\r\n\r\n" + req.content
+		err := smtp.SendMail(address, auth, cfg["smtp_user"], req.sendto, []byte(data))
+		if err != nil {
+			SYSLOG("ERROR", fmt.Sprintf("邮件发送失败:%s,邮件内容如下:%s", err.Error(), data))
+		}
+	}
+}
+
+func (s *MailSender) Push(req *MailRequest) {
+	timer := time.NewTimer(time.Second * PUSH_TIMEOUT)
+	select {
+	case s.queue <- req:
+		timer.Stop()
+	case <-timer.C:
+	}
+}

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

@@ -1,72 +0,0 @@
-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:
-	}
-}

+ 126 - 32
src/cnphper.com/redisdog/main.go

@@ -10,9 +10,13 @@ import (
 	"net/http"
 	"os"
 	"os/signal"
+	"path/filepath"
+	"strconv"
 	"strings"
 	"syscall"
+	"time"
 
+	"cnphper.com/model"
 	_ "github.com/mattn/go-sqlite3"
 )
 
@@ -32,11 +36,22 @@ var (
 	Db          *sql.DB
 	SessPoll    *SessionPoll
 	TmplFuncMap template.FuncMap
+	SysCfg      *SysCfgCache
 	Srv         *http.Server
 	Sender      *MailSender
-	SysSetting  Setting
+	Monitor     *RedisMonitor
+	SysLogger   *model.SysLog
 )
 
+func changeWordDir() {
+	if binDir, err := filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
+		log.Fatalf("%s\n", err.Error())
+	} else {
+		rootDir := filepath.Dir(binDir)
+		os.Chdir(rootDir)
+	}
+}
+
 func loadConfig() *Config {
 	var (
 		cfgfile string
@@ -48,6 +63,10 @@ func loadConfig() *Config {
 		log.Fatalf("Usage: %s -c <config file>\n", os.Args[0])
 	}
 
+	wd, err := os.Getwd()
+	if err != nil {
+		log.Fatalf("Get work directory failed: %s\n", err.Error())
+	}
 	file, err := os.OpenFile(cfgfile, os.O_RDONLY, 0644)
 	if err != nil {
 		log.Fatal(err)
@@ -57,22 +76,22 @@ func loadConfig() *Config {
 		log.Fatal(err)
 	}
 	if cfg.Port == "" {
-		log.Fatalf("Port is empty!\n")
+		cfg.Port = "8080"
 	}
 	if cfg.Database == "" {
-		log.Fatalf("Database is empty!\n")
+		cfg.Database = filepath.Join(wd, "db", "db.sqlite")
 	}
 	if cfg.RootPwd == "" {
-		log.Fatalf("RootPwd is empty!\n")
+		cfg.RootPwd = "88888888"
 	}
 	if cfg.LogDir == "" {
-		log.Fatalf("LogDir is empty!\n")
+		cfg.LogDir = filepath.Join(wd, "log")
 	}
 	if cfg.TmplDir == "" {
-		log.Fatalf("TmplDir is empty!\n")
+		cfg.TmplDir = filepath.Join(wd, "tmpl")
 	}
 	if cfg.ResourcesDir == "" {
-		log.Fatalf("ResourcesDir is empty!\n")
+		cfg.ResourcesDir = filepath.Join(wd, "resources")
 	}
 	if cfg.SessionTTL == 0 {
 		cfg.SessionTTL = 900
@@ -83,7 +102,7 @@ func loadConfig() *Config {
 func waitSignal() {
 	ch := make(chan os.Signal, 1)
 	signal.Notify(ch, syscall.SIGUSR1)
-	signal.Notify(ch, syscall.SIGQUIT)
+	signal.Notify(ch, syscall.SIGTERM)
 	for {
 		sig := <-ch
 		switch sig {
@@ -93,13 +112,15 @@ func waitSignal() {
 			if Srv != nil {
 				Srv.Close()
 			}
+			SYSLOG("WARN", "服务因重启而停止")
 			syscall.Exec(os.Args[0], os.Args[1:], nil)
 		//退出
-		case syscall.SIGQUIT:
+		case syscall.SIGTERM:
 			close(ch)
 			if Srv != nil {
 				Srv.Close()
 			}
+			SYSLOG("WARN", "服务停止")
 			os.Exit(0)
 		}
 	}
@@ -111,24 +132,59 @@ func openDatabase(dbfile string) *sql.DB {
 		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)
+	_, err = db.Exec(Sql_Table_Syscfg)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Table_Accounts)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Table_Rediscfg)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Table_Syslog)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Index_Syslog)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Table_Monitorlog)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Table_Warnlog)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Index_Warnlog1)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Index_Warnlog2)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Table_Processlog)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec("INSERT OR IGNORE INTO accounts(account,name,password,is_super) VALUES(?,?,?,?)", "root", "超管", model.PasswordConvert(Cfg.RootPwd), 1)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
+	_, err = db.Exec(Sql_Values_Syscfg)
+	if err != nil {
+		log.Fatalf("sql.Exec() Error: %s\n", err.Error())
+	}
 
 	return db
 }
 
-func loadSystemSetting() map[string]string {
-
-}
-
-func start_http_server() *http.Server {
+func startHttpServer() *http.Server {
 	handle := http.NewServeMux()
 	//登录
 	handle.HandleFunc("/login", login_index)
@@ -160,20 +216,26 @@ func start_http_server() *http.Server {
 	handle.HandleFunc("/syscfg/misc", syscfg_misc)
 	handle.HandleFunc("/syscfg/misc_get", syscfg_misc_get)
 	handle.HandleFunc("/syscfg/misc_set", syscfg_misc_set)
-	//日志-报警
+	//日志-Redis监控
+	handle.HandleFunc("/log/monitor", log_monitor)
+	handle.HandleFunc("/log/monitor_list", log_monitor_list)
+	//日志-Redis报警
 	handle.HandleFunc("/log/warn", log_warn)
 	handle.HandleFunc("/log/warn_list", log_warn_list)
-	//日志-扩容
+	//日志-Redis扩容
 	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))))
+	//ico文件
+	handle.HandleFunc("/favicon.ico", func(resp http.ResponseWriter, req *http.Request) {
+		http.ServeFile(resp, req, filepath.Join(Cfg.ResourcesDir, "favicon.ico"))
+	})
+	//测试
+	handle.HandleFunc("/debug/sendmail", debug_sendmail)
 	//启动HTTP服务
 	srv := &http.Server{
 		Addr:    fmt.Sprintf("%s:%s", Cfg.Host, Cfg.Port),
@@ -189,12 +251,16 @@ func start_http_server() *http.Server {
 
 func main() {
 	//加载基础配置
+	changeWordDir()
 	Cfg = loadConfig()
 
 	//连接DB
 	Db = openDatabase(Cfg.Database)
 	defer Db.Close()
 
+	//系统日志表对象
+	SysLogger = model.NewSysLog(Db)
+
 	//创建HTTP会话池
 	SessPoll = NewSessionPoll(Cfg.SessionTTL)
 
@@ -203,19 +269,47 @@ func main() {
 	TmplFuncMap["has_prefix"] = strings.HasPrefix
 
 	//从DB加载业务配置
-	SysSetting = NewSetting()
-	SysSetting.Load()
+	SysCfg = NewSysCfgCache()
+	SysCfg.Load()
 
 	//开始HTTP服务
-	Srv = start_http_server()
+	Srv = startHttpServer()
 
 	//开始监控Redis状态
-	go monitor_loop()
+	Monitor := NewRedisMonitor()
+	go Monitor.Loop()
 
 	//开始处理邮件发送队列
 	Sender = NewMailSender()
 	go Sender.Loop()
 
+	//开始定时清理日志表记录
+	go func() {
+		//取配置
+		days := int(30)
+		for {
+			if str, ok := SysCfg.Get("log_kept_days"); ok {
+				if num, err := strconv.Atoi(str); err == nil {
+					days = num
+				} else {
+					SYSLOG("DEBUG", fmt.Sprintf("表syscfg记录cfg_key=log_kept_days的值不是数字:%s", err.Error()))
+				}
+			}
+			ts := time.Now().Unix() - 86400*int64(days)
+			SysLogger.CleanUntil(ts)
+			mdlMonitorLog := model.NewMonitorLog(Db)
+			mdlMonitorLog.CleanUntil(ts)
+			mdlWarnLog := model.NewWarnLog(Db)
+			mdlWarnLog.CleanUntil(ts)
+			mdlProcessLog := model.NewProcessLog(Db)
+			mdlProcessLog.CleanUntil(ts)
+			time.Sleep(time.Hour * 1)
+		}
+	}()
+
+	//记录启动日志
+	SYSLOG("DEBUG", "服务启动")
+
 	//监听信号
 	waitSignal()
 }

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

@@ -1,42 +0,0 @@
-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)
-
-		//如果状态异常检查是否连接出错次数达到上限
-
-		//判断是否需要自动扩容
-
-		//添加任务到邮件发送队列
-	}
-}

+ 208 - 0
src/cnphper.com/redisdog/redis_monitor.go

@@ -0,0 +1,208 @@
+package main
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"cnphper.com/model"
+)
+
+const (
+	MONITOR_DEFAULT_LOOP_INTERVAL = 30
+	MONITOR_DEFAULT_MAIL_INTERVAL = 3600
+)
+
+type RedisMonitor struct {
+	redisCfg      *model.RedisCfg
+	monitorLogger *model.MonitorLog
+	warnLogger    *model.WarnLog
+	processLogger *model.ProcessLog
+	logs          map[int64]*model.MonitorLogRow
+}
+
+func NewRedisMonitor() *RedisMonitor {
+	return &RedisMonitor{
+		redisCfg:      model.NewRedisCfg(Db),
+		monitorLogger: model.NewMonitorLog(Db),
+		warnLogger:    model.NewWarnLog(Db),
+		processLogger: model.NewProcessLog(Db),
+		logs:          make(map[int64]*model.MonitorLogRow),
+	}
+}
+
+func (m *RedisMonitor) Loop() {
+	interval := MONITOR_DEFAULT_LOOP_INTERVAL
+	for {
+		if str, ok := SysCfg.Get("monitor_loop_interval"); ok {
+			if num, err := strconv.Atoi(str); err == nil {
+				interval = num
+			} else {
+				SYSLOG("DEBUG", fmt.Sprintf("表syscfg记录cfg_key=monitor_loop_interval的值不是数字:%s", err.Error()))
+			}
+		}
+		time.Sleep(time.Second * time.Duration(interval))
+		m.loopStep()
+	}
+}
+
+func (m *RedisMonitor) loopStep() {
+	interval := MONITOR_DEFAULT_MAIL_INTERVAL
+	rows, err := m.redisCfg.GetAll(0)
+	if err != nil {
+		SYSLOG("WARN", fmt.Sprintf("函数model.RedisCfg.GetAll(0)调用失败:%s", err.Error()))
+		return
+	}
+	for _, row := range rows {
+		now, mails := time.Now(), make([]*MailRequest, 0)
+		ts, tstr := now.Unix(), now.String()
+		//取配置
+		if str, ok := SysCfg.Get("monitor_mail_interval"); ok {
+			if num, err := strconv.Atoi(str); err == nil {
+				interval = num
+			} else {
+				SYSLOG("DEBUG", fmt.Sprintf("表syscfg记录cfg_key=monitor_mail_interval的值不是数字:%s", err.Error()))
+			}
+		}
+		//初始化检测数据
+		if _, ok := m.logs[row.Id]; !ok {
+			m.logs[row.Id] = &model.MonitorLogRow{RedisId: row.Id}
+		}
+		m.logs[row.Id].LogTime = ts
+		//查询Redis的状态信息
+		info := queryRedisInfo(row)
+		if info.Error != "" {
+			//更新检测数据
+			m.logs[row.Id].QueryStatus = false
+			m.logs[row.Id].FailedCount++
+			//判断状态异常检查是否连接出错次数达到上限
+			if m.logs[row.Id].FailedCount >= row.MaxStatusFailed {
+				mails = append(mails, &MailRequest{
+					sendto:  strings.Split(row.MailList, ";"),
+					title:   fmt.Sprintf("报警:Redis[%s]状态检测失败", row.Address),
+					content: fmt.Sprintf(`<p>时间:%s</p><p>Redis#%d[%s]状态检测失败:<b>%s</b>,已连续失败:<b>%d</b>次</p>`, tstr, row.Id, row.Address, info.Error, m.logs[row.Id].FailedCount),
+				})
+			}
+			//记录日志
+			SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态查询失败:%s,连接第%d次", row.Id, row.Address, err.Error(), m.logs[row.Id].FailedCount))
+		} else {
+			var usedMemory, maxMemory, systemMemory, connections, qps, evictedKeys, eviIncreased int
+			//已用内存
+			if usedMemory, err = strconv.Atoi(info.Data["used_memory"]); err != nil {
+				SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态字段used_memory值无效:%s", row.Id, row.Address, err.Error()))
+			}
+			//最大可分配内存
+			if maxMemory, err = strconv.Atoi(info.Data["maxmemory"]); err != nil {
+				SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态字段maxmemory值无效:%s", row.Id, row.Address, err.Error()))
+			}
+			//系统内存
+			if systemMemory, err = strconv.Atoi(info.Data["total_system_memory"]); err != nil {
+				SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态字段total_system_memory值无效:%s", row.Id, row.Address, err.Error()))
+			}
+			//连接数
+			if connections, err = strconv.Atoi(info.Data["connected_clients"]); err != nil {
+				SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态字段connected_clients值无效:%s", row.Id, row.Address, err.Error()))
+			}
+			//QPS
+			if qps, err = strconv.Atoi(info.Data["instantaneous_ops_per_sec"]); err != nil {
+				SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态字段instantaneous_ops_per_sec值无效:%s", row.Id, row.Address, err.Error()))
+			}
+			//淘汰数
+			if evictedKeys, err = strconv.Atoi(info.Data["evicted_keys"]); err != nil {
+				SYSLOG("WARN", fmt.Sprintf("Redis#%d[%s]状态字段evicted_keys值无效:%s", row.Id, row.Address, err.Error()))
+			}
+			//新增淘汰数
+			eviIncreased = evictedKeys - int(m.logs[row.Id].EvictedKeys)
+
+			//检查连接的客户端数量是否达到上限
+			if connections >= int(row.MaxConnection) {
+				mails = append(mails, &MailRequest{
+					sendto:  strings.Split(row.MailList, ";"),
+					title:   fmt.Sprintf("报警:Redis[%s]连接数达到上限", row.Address),
+					content: fmt.Sprintf(`<p>时间:%s</p><p>Redis#%d[%s]连接数已达到:<b>%d</b>,上限为:%d</p>`, tstr, row.Id, row.Address, connections, row.MaxConnection),
+				})
+			}
+			//检查QPS是否达到上限
+			if qps >= int(row.MaxQPS) {
+				mails = append(mails, &MailRequest{
+					sendto:  strings.Split(row.MailList, ";"),
+					title:   fmt.Sprintf("报警:Redis[%s]QPS达到上限", row.Address),
+					content: fmt.Sprintf(`<p>时间:%s</p><p>Redis#%d[%s]QPS已达到:<span color="red">%d</span>,上限为:%d</p>`, tstr, row.Id, row.Address, qps, row.MaxQPS),
+				})
+			}
+			//检查新增淘汰数是否达到上限
+			if eviIncreased >= int(row.MaxEviIncreased) {
+				mails = append(mails, &MailRequest{
+					sendto:  strings.Split(row.MailList, ";"),
+					title:   fmt.Sprintf("报警:Redis[%s]新增淘汰记录数达到上限", row.Address),
+					content: fmt.Sprintf(`<p>时间:%s</p><p>Redis#%d[%s]新增淘汰记录数已达到:<span color="red">%d</span>,上限为:%d</p>`, tstr, row.Id, row.Address, eviIncreased, row.MaxEviIncreased),
+				})
+			}
+			//有内存使用限制,并且允许自动扩容
+			if maxMemory > 0 {
+				flag := false
+				//检查当前使用内存是否达到了预警值
+				freeMemory := maxMemory - usedMemory
+				if freeMemory < int(row.MinMemoryFree) {
+					mails = append(mails, &MailRequest{
+						sendto:  strings.Split(row.MailList, ";"),
+						title:   fmt.Sprintf("报警:Redis[%s]可用内存不足", row.Address),
+						content: fmt.Sprintf(`<p>时间:%s</p><p>Redis#%d[%s]可用内存不足,分配:<b>%s</b>,剩余:<b>%s</b>,要求剩余:<b>%s</b></p>`, tstr, row.Id, row.Address, info.Data["maxmemory_human"], number2size(float64(freeMemory)), number2size(float64(row.MinMemoryFree))),
+					})
+					flag = true
+				}
+				//判断是否需要自动扩容
+				if flag && row.StepMemoryIncrease > 0 && maxMemory < int(row.MaxMemoryUsage) {
+					newMaxMemory := int64(maxMemory) + row.StepMemoryIncrease
+					if newMaxMemory > row.MaxMemoryUsage {
+						newMaxMemory = row.MaxMemoryUsage
+					}
+					ret, err := resetRedisConfig(row, newMaxMemory)
+					//判断扩容结果
+					if err != nil {
+						SYSLOG("ERROR", fmt.Sprintf("Redis#%d[%s]扩容失败:%s", row.Id, row.Address, err.Error()))
+					} else {
+						if ret {
+							mails = append(mails, &MailRequest{
+								sendto:  strings.Split(row.MailList, ";"),
+								title:   fmt.Sprintf("通知:Redis[%s]已自动扩容", row.Address),
+								content: fmt.Sprintf(`<p>时间:%s</p><p>Redis#%d[%s]最大可用内存限制已由<b>%s</b>调整为<b>%s</b></p>`, tstr, row.Id, row.Address, info.Data["maxmemory_human"], number2size(float64(newMaxMemory))),
+							})
+							if _, err = m.processLogger.Add(&model.ProcessLogRow{RedisId: row.Id, MaxMemoryBefore: int64(maxMemory), MaxMemoryAfter: newMaxMemory}); err != nil {
+								SYSLOG("ERROR", fmt.Sprintf("Redis#%d[%s]扩容日志写入DB失败:%s", row.Id, row.Address, err.Error()))
+							}
+						} else {
+							SYSLOG("ERROR", fmt.Sprintf("Redis#%d[%s]扩容失败:无改变", row.Id, row.Address))
+						}
+					}
+				}
+			}
+
+			//更新检测数据
+			m.logs[row.Id].QueryStatus = true
+			m.logs[row.Id].FailedCount = 0
+			m.logs[row.Id].UsedMemory = int64(usedMemory)
+			m.logs[row.Id].MaxMemory = int64(maxMemory)
+			m.logs[row.Id].SystemMemory = int64(systemMemory)
+			m.logs[row.Id].Connection = int64(connections)
+			m.logs[row.Id].QPS = int64(qps)
+			m.logs[row.Id].EvictedKeys = int64(evictedKeys)
+			m.logs[row.Id].EviIncreased = int64(eviIncreased)
+		}
+		//添加任务到邮件发送队列
+		if len(mails) > 0 && m.logs[row.Id].LastMailTime < ts-int64(interval) {
+			m.logs[row.Id].LastMailTime = ts
+			for _, mail := range mails {
+				Sender.Push(mail)
+				if _, err = m.warnLogger.Add(&model.WarnLogRow{RedisId: row.Id, WarnMsg: mail.content}); err != nil {
+					SYSLOG("ERROR", fmt.Sprintf("Redis#%d[%s]报警日志写入DB失败:%s", row.Id, row.Address, err.Error()))
+				}
+			}
+		}
+		//记录检查日志
+		if _, err = m.monitorLogger.Add(m.logs[row.Id]); err != nil {
+			SYSLOG("ERROR", fmt.Sprintf("Redis#%d[%s]监控日志写入DB失败:%s", row.Id, row.Address, err.Error()))
+		}
+	}
+}

src/cnphper.com/redisdog/session.go → src/cnphper.com/redisdog/session_pool.go


+ 30 - 27
src/cnphper.com/redisdog/sql.go

@@ -7,6 +7,17 @@ CREATE TABLE IF NOT EXISTS syscfg (
   cfg_key TEXT UNIQUE,
   cfg_value TEXT NOT NULL
 )`
+	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,'monitor_loop_interval','30'),
+(NULL,'monitor_mail_interval','3600'),
+(NULL,'log_kept_days','30')
+`
 	Sql_Table_Accounts = `
 CREATE TABLE IF NOT EXISTS accounts (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -26,7 +37,7 @@ CREATE TABLE IF NOT EXISTS rediscfg (
   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,
+  step_memory_increase 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,
@@ -37,51 +48,43 @@ CREATE TABLE IF NOT EXISTS rediscfg (
 	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
+  log_level TEXT NOT NULL, 
+  log_msg TEXT 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 (
+	Sql_Index_Syslog     = `CREATE INDEX IF NOT EXISTS idx_time ON syslog(log_time)`
+	Sql_Table_Monitorlog = `
+CREATE TABLE IF NOT EXISTS monitorlog (
   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,
+  log_time INTEGER NOT NULL,
+  query_status INTEGER NOT NULL DEFAULT 0,
+  failed_count INTEGER NOT NULL DEFAULT 0,
+  used_memory INTEGER NOT NULL DEFAULT 0,
+  max_memory INTEGER NOT NULL DEFAULT 0,
+  system_memory INTEGER NOT NULL DEFAULT 0,
   connection INTEGER NOT NULL DEFAULT 0,
   qps INTEGER NOT NULL DEFAULT 0,
+  evicted_keys INTEGER NOT NULL DEFAULT 0,
   evi_increased INTEGER NOT NULL DEFAULT 0,
-  UNIQUE (redis_id, check_time)
+  UNIQUE (redis_id, log_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)
+  warn_msg TEXT NOT NULL
 )`
+	Sql_Index_Warnlog1   = `CREATE INDEX IF NOT EXISTS idx_redis ON warnlog(redis_id)`
+	Sql_Index_Warnlog2   = `CREATE INDEX IF NOT EXISTS idx_time ON warnlog(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,
+  maxmemory_before INTEGER NOT NULL DEFAULT 0,
+  maxmemory_after 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'),
-`
 )

+ 10 - 10
src/cnphper.com/redisdog/setting.go

@@ -4,46 +4,46 @@ import (
 	"cnphper.com/model"
 )
 
-type Setting struct {
+type SysCfgCache struct {
 	lock chan int
 	data map[string]string
 }
 
-func NewSetting() *Setting {
-	return &Setting{
+func NewSysCfgCache() *SysCfgCache {
+	return &SysCfgCache{
 		lock: make(chan int, 1),
 		data: make(map[string]string),
 	}
 }
 
-func (s *Setting) Load() error {
+func (s *SysCfgCache) 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
+	for _, item := range values {
+		s.data[item.CfgKey] = item.CfgValue
 	}
 	<-s.lock
 	return nil
 }
 
-func (s *Setting) Get(k string) (string, bool) {
+func (s *SysCfgCache) 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) {
+func (s *SysCfgCache) Set(k, v string) {
 	s.lock <- 1
 	s.data[k] = v
 	<-s.lock
 }
 
-func (s *Setting) GetMulti(keys []string) map[string]string {
+func (s *SysCfgCache) GetMulti(keys []string) map[string]string {
 	s.lock <- 1
 	cpy := make(map[string]string)
 	for _, key := range keys {
@@ -58,7 +58,7 @@ func (s *Setting) GetMulti(keys []string) map[string]string {
 	return cpy
 }
 
-func (s *Setting) GetAll() map[string]string {
+func (s *SysCfgCache) GetAll() map[string]string {
 	s.lock <- 1
 	cpy := make(map[string]string)
 	for k, v := range s.data {

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

@@ -1,50 +0,0 @@
-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
-	}
-}

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

@@ -1,50 +0,0 @@
-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
-	}
-}

+ 62 - 0
tmpl/debug/sendmail.tmpl

@@ -0,0 +1,62 @@
+{{template "header.tmpl" .}}
+</head>
+<body>
+{{template "navbar.tmpl" .}}
+
+<div class="container-fluid">
+	<h4 class="text-center">邮件发送测试</h4>
+	<hr />
+	<div class="row">
+		<div class="col-md-4 col-md-offset-4">
+			<div class="pannel">
+				<div class="pannel_title">
+					<h3>邮件发送测试</h3>
+				</div>
+				<div class="pannel_body">
+					<form id="mainform">
+						<div class="form-group">
+							<label>收件人(多个之间使用;间隔)</label>
+							<input type="text" class="form-control" name="sendto" value="" maxlength="100" />
+						</div>
+						<div class="form-group">
+							<label>标题</label>
+							<input type="text" class="form-control" name="title" value="" maxlength="50" />
+						</div>
+						<div class="form-group">
+							<label>内容</label>
+							<textarea rows="5" name="content" class="form-control"></textarea>
+						</div>
+						<button class="btn btn-sm btn-primary">发送</button>
+						<a class="btn btn-sm btn-default" href="/syscfg/warn">返回SMTP设置</a>
+					</form>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<script type="text/javascript">
+var $SESS = {{.Sess}};
+$(function(){
+	$('#mainform').submit(function(){
+		if (this.sendto.value == '') {
+			$.alert({'content': '收件人不能为空!'});
+		} else if (this.title.value == '') {
+			$.alert({'content': '标题不能为空!'});
+		} else if (this.content.value == '') {
+			$.alert({'content': '发送内容不能为空!'});
+		} else {
+			$.post('/debug/sendmail', $(this).serialize(), function(resp){
+				if (resp.errno) {
+					$.alert({'content': resp.error});
+				} else {
+					$.alert({'content': '操作成功!'});
+				}
+			}, 'json');
+		}
+		return false;
+	});
+});
+</script>
+</body>
+</html>

+ 57 - 47
tmpl/index.tmpl

@@ -4,12 +4,11 @@
 {{template "navbar.tmpl" .}}
 
 <div class="container-fluid">
+	<h4 class="text-center">Redis实时状态查看&nbsp;<small>[<a href="#" id="btn_refresh">刷新</a>]</small></h4>
+	<hr />
 	<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>
@@ -22,6 +21,7 @@
 				<th>分配内存</th>
 				<th>已用内存</th>
 				<th>淘汰策略</th>
+				<th>回写规则</th>
 				<th>过期数</th>
 				<th>丢弃数</th>
 				<th>当前连接</th>
@@ -47,51 +47,61 @@ $(function(){
 				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';
+					if (item.Error == '') {
+						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.save}</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>`;
+					} else {
+						html += `<tr>
+							<td>${item.Id}</td>
+							<td>${item.Address}</td>
+							<td>${item.Remark}</td>
+							<td colspan="21" class="danger">${item.Error}</td>
+						</tr>`;
 					}
-					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(){

+ 2 - 2
tmpl/navbar.tmpl

@@ -19,10 +19,10 @@
 						<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/monitor"}}active{{end}}" href="/log/monitor">Redis监控日志</a></li>
 						<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>
+						<li><a class="{{if eq .Req.URL.Path "/log/syslog"}}active{{end}}" href="/log/syslog">系统日志</a></li>
 					</ul>
 				</li>
 				<li class="dropdown {{if has_prefix .Req.URL.Path "/syscfg/"}}active{{end}}">

+ 32 - 22
tmpl/profile/passwd.tmpl

@@ -4,29 +4,39 @@
 {{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" />
+	<h4 class="text-center">修改密码</h4>
+	<hr />
+	<div class="row">
+		<div class="col-md-4 col-md-offset-4">
+			<div class="pannel">
+				<div class="pannel_body">
+					<form id="mainform">
+						<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>
+			</div>
 		</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>
 </div>
 
 <script type="text/javascript">

+ 2 - 7
tmpl/syscfg/account.tmpl

@@ -12,16 +12,11 @@
 {{template "navbar.tmpl" .}}
 
 <div class="container-fluid">
+	<h4 class="text-center">账号管理 <small>[<a href="#" id="add_btn"><i class="glyphicon glyphicon-plus"></i> 点击这里添加</a>]</small></h4>
+	<hr />
 	<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>

+ 0 - 10
tmpl/syscfg/mailgroup.tmpl

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

+ 49 - 38
tmpl/syscfg/misc.tmpl

@@ -4,53 +4,26 @@
 {{template "navbar.tmpl" .}}
 
 <div class="container-fluid">
+	<h4 class="text-center">其它配置</h4>
+	<hr />
 	<div class="row">
-		<div class="col-md-3">
+		<div class="col-md-4 col-md-offset-4">
 			<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>
+					<form id="mainform">
 						<div class="form-group">
-							<label>用户</label>
-							<input type="text" class="form-control" name="smtp_user" value="" maxlength="40" />
+							<label>Redis状态检查间隔时长</label>
+							<input type="text" class="form-control" name="monitor_loop_interval" value="60" maxlength="10" />
 						</div>
 						<div class="form-group">
-							<label>密码</label>
-							<input type="text" class="form-control" name="smtp_pwd" value="" maxlength="20" />
+							<label>报警邮件发送间隔时长</label>
+							<input type="text" class="form-control" name="monitor_mail_interval" value="300" maxlength="10" />
 						</div>
 						<div class="form-group">
-							<label>署名</label>
-							<input type="text" class="form-control" name="smtp_sender" value="" maxlength="20" />
+							<label>日志保留天数</label>
+							<input type="text" class="form-control" name="log_kept_days" value="30" maxlength="10" />
 						</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>
+						<button type="submit" class="btn btn-sm btn-primary">保存</button>
 					</form>
 				</div>
 			</div>
@@ -60,6 +33,44 @@
 
 <script type="text/javascript">
 var $SESS = {{.Sess}};
+
+$(function(){
+	var form = $('#mainform').get(0);
+	function load_config() {
+		$.get('/syscfg/misc_get', {}, function(resp){
+			if (resp && resp.errno == 0) {
+				if (resp.data.monitor_loop_interval) {
+					form.monitor_loop_interval.value = resp.data.monitor_loop_interval;
+				}
+				if (resp.data.monitor_mail_interval) {
+					form.monitor_mail_interval.value = resp.data.monitor_mail_interval;
+				}
+				if (resp.data.log_kept_days) {
+					form.log_kept_days.value = resp.data.log_kept_days;
+				}
+			}
+		}, 'json');
+	}
+	$(form).submit(function(){
+		if (this.monitor_loop_interval.value == '') {
+			$.alert('请填写Redis状态检查间隔时长!');
+		} else if (this.monitor_mail_interval.value == '') {
+			$.alert('请填写报警邮件发送间隔时长!');
+		} else if (this.log_kept_days.value == '') {
+			$.alert('请填写日志保留天数!');
+		} else {
+			$.post('/syscfg/misc_set', $(this).serialize(), function(resp){
+				if (resp && resp.errno == 0) {
+					$.alert('已更新!');
+				} else {
+					$.alert(resp.error ? resp.error : '操作失败!');
+				}
+			}, 'json');
+		}
+		return false;
+	});
+	load_config();
+});
 </script>
 </body>
 </html>

+ 46 - 25
tmpl/syscfg/redis.tmpl

@@ -12,16 +12,11 @@
 {{template "navbar.tmpl" .}}
 
 <div class="container-fluid">
+	<h4 class="text-center">Redis服务列表 <small>[<a href="#" id="add_btn"><i class="glyphicon glyphicon-plus"></i> 点击这里添加</a>]</small></h4>
+	<hr />
 	<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>
@@ -29,7 +24,7 @@
 				<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="Redis的最大并发连接数,达到该值将会触发报警。" onclick="return false;">?</a></th>
 				<th>最大新增淘汰数&nbsp;<a href="#" data-toggle="popover" data-trigger="focus" title="最大新增淘汰数" data-content="上次检测之后增加的淘汰记录数上限,达到该值后将触发报警。" onclick="return false;">?</a></th>
@@ -64,7 +59,7 @@ $(function(){
 					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.StepMemoryIncrease) + '</td>';
 					html += '<td>' + size2str(item.MaxMemoryUsage) + '</td>';
 					html += '<td>' + numberFormat(item.MaxConnection) + '</td>';
 					html += '<td>' + item.MaxEviIncreased + '</td>';
@@ -120,7 +115,7 @@ $(function(){
 			'MaxConnectWait': 5,
 			'MaxStatusFailed': 1,
 			'MinMemoryFree': 10*1024*1024,
-			'MinMemoryFreePC': 10,
+			'StepMemoryIncrease': 100*1024*1024,
 			'MaxMemoryUsage': 500*1024*1024,
 			'MaxConnection': 1024,
 			'MaxEviIncreased': 1,
@@ -176,31 +171,46 @@ $(function(){
 			<div class="form-group">
 				<label class="col-md-4 control-label">最小可用内存空间</label>
 				<div class="col-md-8">
-					<div class="input-group" style="width:200px;">
+					<div class="input-group">
 						<input type="text" class="form-control" name="MinMemoryFree" maxlength="10" value="${record.MinMemoryFree}" />
-						<div class="input-group-addon">字节</div>
+						<div class="input-group-btn" data-toggle="buttons">
+							<label class="btn btn-default active" data-value="1"><input type="radio" autocomplete="off" /> Byte</label>
+							<label class="btn btn-default" data-value="1024"><input type="radio" autocomplete="off" /> KB</label>
+							<label class="btn btn-default" data-value="1048576"><input type="radio" autocomplete="off" /> MB</label>
+							<label class="btn btn-default" data-value="1073741824"><input type="radio" autocomplete="off" /> GB</label>
+						</div>
 					</div>
-					<p class="help-block">低于该值将自动扩容,设置0跳过该检查项</p>
+					<p class="help-block">正整数,低于该值将自动扩容,设置0跳过该检查项</p>
 				</div>
 			</div>
 			<div class="form-group">
-				<label class="col-md-4 control-label">最小可用内存空间占比</label>
+				<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 class="input-group">
+						<input type="text" class="form-control" name="StepMemoryIncrease" maxlength="10" value="${record.StepMemoryIncrease}" />
+						<div class="input-group-btn" data-toggle="buttons">
+							<label class="btn btn-default active" data-value="1"><input type="radio" autocomplete="off" /> Byte</label>
+							<label class="btn btn-default" data-value="1024"><input type="radio" autocomplete="off" /> KB</label>
+							<label class="btn btn-default" data-value="1048576"><input type="radio" autocomplete="off" /> MB</label>
+							<label class="btn btn-default" data-value="1073741824"><input type="radio" autocomplete="off" /> GB</label>
+						</div>
 					</div>
-					<p class="help-block">低于该值将自动扩容,设置0跳过该检查项</p>
+					<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;">
+					<div class="input-group">
 						<input type="text" class="form-control" name="MaxMemoryUsage" maxlength="10" value="${record.MaxMemoryUsage}" />
-						<div class="input-group-addon">字节</div>
+						<div class="input-group-btn" data-toggle="buttons">
+							<label class="btn btn-default active" data-value="1"><input type="radio" autocomplete="off" /> Byte</label>
+							<label class="btn btn-default" data-value="1024"><input type="radio" autocomplete="off" /> KB</label>
+							<label class="btn btn-default" data-value="1048576"><input type="radio" autocomplete="off" /> MB</label>
+							<label class="btn btn-default" data-value="1073741824"><input type="radio" autocomplete="off" /> GB</label>
+						</div>
 					</div>
-					<p class="help-block">已用内存达到该值将不再自动扩容</p>
+					<p class="help-block">正整数,已用内存达到该值将不再自动扩容</p>
 				</div>
 			</div>
 			<div class="form-group">
@@ -261,8 +271,12 @@ $(function(){
 				$(form.MaxMemoryUsage).change(function(){
 					this.value = numberFormat(this.value);
 				});
+				$(form.StepMemoryIncrease).change(function(){
+					this.value = numberFormat(this.value);
+				});
 				$(form.MinMemoryFree).change();
 				$(form.MaxMemoryUsage).change();
+				$(form.StepMemoryIncrease).change();
 				if (record.Id == 0) {
 					$('.id-group', dlg).hide();
 				} else {
@@ -271,8 +285,15 @@ $(function(){
 			},
 			'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, '');
+				var MinMemoryFreeTimes = parseInt($('label.active', $(form.MinMemoryFree).parent()).attr('data-value'));
+				var MaxMemoryUsageTimes = parseInt($('label.active', $(form.MaxMemoryUsage).parent()).attr('data-value'));
+				var StepMemoryIncreaseTimes = parseInt($('label.active', $(form.StepMemoryIncrease).parent()).attr('data-value'));
+				console.debug(MinMemoryFreeTimes, MaxMemoryUsageTimes, StepMemoryIncreaseTimes);
+				form.MinMemoryFree.value = parseInt(form.MinMemoryFree.value.replace(/,/g, '')) * MinMemoryFreeTimes;
+				form.MaxMemoryUsage.value = parseInt(form.MaxMemoryUsage.value.replace(/,/g, '')) * MaxMemoryUsageTimes;
+				form.StepMemoryIncrease.value = parseInt(form.StepMemoryIncrease.value.replace(/,/g, '')) * StepMemoryIncreaseTimes;
+				$('.input-group-btn label', form).removeClass('active');
+				$('.input-group-btn :first-child', form).addClass('active');
 				if (form.Address.value == '') {
 					$.alert({'content': '请填写Redis的服务地址!'});
 				} else if (!/[0-9A-Za-z.\-]+:\d{3,5}$/.test(form.Address.value)) {
@@ -285,8 +306,8 @@ $(function(){
 					$.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.StepMemoryIncrease.value)) {
+					$.alert({'content': '请填写正确的单次扩容大小!'});
 				} else if (/\D/.test(form.MaxMemoryUsage.value)) {
 					$.alert({'content': '请填写正确的最大使用内存上限!'});
 				} else if (/\D/.test(form.MaxConnection.value)) {

+ 85 - 0
tmpl/syscfg/warn.tmpl

@@ -3,8 +3,93 @@
 <body>
 {{template "navbar.tmpl" .}}
 
+<div class="container-fluid">
+	<h4 class="text-center">报警配置</h4>
+	<hr />
+	<div class="row">
+		<div class="col-md-4 col-md-offset-4">
+			<div class="pannel">
+				<div class="pannel_body">
+					<form id="mainform">
+						<div class="form-group">
+							<label>SMTP服务器地址</label>
+							<input type="text" class="form-control" name="smtp_host" value="" maxlength="50" />
+						</div>
+						<div class="form-group">
+							<label>服务端口</label>
+							<input type="text" class="form-control" name="smtp_port" value="" 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="submit" class="btn btn-sm btn-primary">保存</button>
+						<a class="btn btn-sm btn-default" href="/debug/sendmail">测试邮件发送</a>
+					</form>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
 <script type="text/javascript">
 var $SESS = {{.Sess}};
+
+$(function(){
+	var form = $('#mainform').get(0);
+	function load_config() {
+		$.get('/syscfg/warn_get', {}, function(resp){
+			if (resp && resp.errno == 0) {
+				if (resp.data.smtp_host) {
+					form.smtp_host.value = resp.data.smtp_host;
+				}
+				if (resp.data.smtp_port) {
+					form.smtp_port.value = resp.data.smtp_port;
+				}
+				if (resp.data.smtp_user) {
+					form.smtp_user.value = resp.data.smtp_user;
+				}
+				if (resp.data.smtp_pwd) {
+					form.smtp_pwd.value = resp.data.smtp_pwd;
+				}
+				if (resp.data.smtp_sender) {
+					form.smtp_sender.value = resp.data.smtp_sender;
+				}
+			}
+		}, 'json');
+	}
+	$(form).submit(function(){
+		if (this.smtp_host.value == '') {
+			$.alert('请填写服务地址!');
+		} else if (this.smtp_port.value == '') {
+			$.alert('请填写服务端口!');
+		} else if (this.smtp_user.value == '') {
+			$.alert('请填写账号!');
+		} else if (this.smtp_pwd.value == '') {
+			$.alert('请填写密码!');
+		} else if (this.smtp_sender.value == '') {
+			$.alert('请填写署名!');
+		} else {
+			$.post('/syscfg/warn_set', $(this).serialize(), function(resp){
+				if (resp && resp.errno == 0) {
+					$.alert('已更新!');
+				} else {
+					$.alert(resp.error ? resp.error : '操作失败!');
+				}
+			}, 'json');
+		}
+		return false;
+	});
+	load_config();
+});
 </script>
 </body>
 </html>