Browse Source

实现找回密码功能

Minho 8 years ago
parent
commit
5c535f5bff

+ 1 - 0
.gitignore

@@ -23,3 +23,4 @@ _testmain.go
 *.test
 *.prof
 .idea
+/conf/app.conf

+ 12 - 12
commands/command.go

@@ -26,7 +26,7 @@ func RegisterDataBase()  {
 
 	port := beego.AppConfig.String("db_port")
 
-	dataSource := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=%s",username,password,host,port,database,url.QueryEscape(timezone))
+	dataSource := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s",username,password,host,port,database,url.QueryEscape(timezone))
 
 
 	orm.RegisterDataBase("default", "mysql", dataSource)
@@ -47,22 +47,23 @@ func RegisterModel()  {
 		new(models.Attachment),
 		new(models.Logger),
 		new(models.CommentVote),
+		new(models.MemberToken),
 	)
 
 }
 
 func Initialization()  {
 
-	o := orm.NewOrm()
-	o.Raw("alter table "+models.NewMember().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewBook().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewRelationship().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewComment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table " +models.NewOption().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewDocument().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewAttachment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewLogger().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
-	o.Raw("alter table "+models.NewCommentVote().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o := orm.NewOrm()
+	//o.Raw("alter table "+models.NewMember().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewBook().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewRelationship().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewComment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table " +models.NewOption().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewDocument().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewAttachment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewLogger().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
+	//o.Raw("alter table "+models.NewCommentVote().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
 
 	options := []models.Option {
 		{ OptionName: "ENABLED_CAPTCHA", OptionValue: "false", OptionTitle:"是否启用验证码"},
@@ -91,7 +92,6 @@ func RegisterLogger()  {
 	logs.EnableFuncCallDepth(true)
 	logs.Async()
 
-	//beego.BeeLogger.DelLogger("console")
 	if _,err := os.Stat("logs/log.log"); os.IsNotExist(err) {
 		if f,err := os.Create("logs/log.log");err == nil {
 			f.Close()

+ 0 - 31
conf/app.conf

@@ -1,31 +0,0 @@
-appname = godoc
-httpport = 8181
-runmode = dev
-sessionon = true
-sessionname = mindoc_id
-copyrequestbody = true
-
-#默认Session生成Key的秘钥
-beegoserversessionkey=123456
-#Session储存方式
-sessionprovider=file
-sessionproviderconfig=./logs
-
-#时区设置
-timezone = Asia/Shanghai
-
-#数据库配置
-db_host=127.0.0.1
-db_port=3306
-db_database=mindoc_db
-db_username=root
-db_password=123456
-
-#项目默认封面
-cover=/static/images/book.jpg
-
-#默认编辑器
-editor=markdown
-
-#上传文件的后缀
-upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif

+ 19 - 1
conf/app.conf.example

@@ -34,4 +34,22 @@ avatar=/static/images/headimgurl.jpg
 token_size=12
 
 #上传文件的后缀
-upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif
+upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif
+
+####################邮件配置######################
+#是否启用邮件
+enable_mail=false
+#每小时限制指定邮箱邮件发送次数
+mail_number=5
+#smtp服务用户名
[email protected]
+#smtp服务器地址
+smtp_host=smtp.ym.163.com
+#smtp密码
+smtp_password=
+#端口号
+smtp_port=25
+#发送邮件的显示名称
[email protected]
+#邮件有效期30分钟
+mail_expired=30

+ 9 - 1
conf/enumerate.go

@@ -52,14 +52,17 @@ func GetDefaultAvatar() string {
 	return beego.AppConfig.DefaultString("avatar","/static/images/headimgurl.jpg")
 }
 
+//获取阅读令牌长度.
 func GetTokenSize() int {
 	return beego.AppConfig.DefaultInt("token_size",12)
 }
 
+//获取默认文档封面.
 func GetDefaultCover() string {
 	return beego.AppConfig.DefaultString("cover","/static/images/book.jpg")
 }
 
+//获取允许的商城文件的类型.
 func GetUploadFileExt()  []string {
 	ext := beego.AppConfig.DefaultString("upload_file_ext","png|jpg|jpeg|gif|txt|doc|docx|pdf")
 	
@@ -76,7 +79,7 @@ func GetUploadFileExt()  []string {
 	}
 	return exts
 }
-
+//判断是否是允许商城的文件类型.
 func IsAllowUploadFileExt(ext string) bool  {
 
 	if strings.HasPrefix(ext,".") {
@@ -90,4 +93,9 @@ func IsAllowUploadFileExt(ext string) bool  {
 		}
 	}
 	return false
+}
+
+//获取当前版本.
+func Version() string {
+	return "v0.1"
 }

+ 38 - 0
conf/mail.go

@@ -0,0 +1,38 @@
+package conf
+
+import (
+	"github.com/astaxie/beego"
+	"strings"
+)
+
+type SmtpConf struct {
+	EnableMail bool
+	MailNumber int
+	SmtpUserName string
+	SmtpHost string
+	SmtpPassword string
+	SmtpPort int
+	FormUserName string
+	MailExpired int
+}
+
+func GetMailConfig() *SmtpConf {
+	user_name := beego.AppConfig.String("smtp_user_name")
+	password := beego.AppConfig.String("smtp_password")
+	smtp_host := beego.AppConfig.String("smtp_host")
+	smtp_port := beego.AppConfig.DefaultInt("smtp_port",25)
+	form_user_name := beego.AppConfig.String("form_user_name")
+	enable_mail := beego.AppConfig.String("enable_mail")
+	mail_number := beego.AppConfig.DefaultInt("mail_number",5)
+
+	c := &SmtpConf{
+		EnableMail : strings.EqualFold(enable_mail,"true"),
+		MailNumber: mail_number,
+		SmtpUserName:user_name,
+		SmtpHost:smtp_host,
+		SmtpPassword:password,
+		FormUserName:form_user_name,
+		SmtpPort:smtp_port,
+	}
+	return c
+}

+ 190 - 8
controllers/account.go

@@ -3,15 +3,16 @@ package controllers
 import (
 	"time"
 	"strings"
+	"regexp"
 
+	"net/smtp"
 	"github.com/lifei6671/godoc/conf"
 	"github.com/lifei6671/godoc/models"
 	"github.com/lifei6671/godoc/utils"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
 	"github.com/lifei6671/gocaptcha"
-
-	"regexp"
+	"strconv"
 )
 
 // AccountController 用户登录与注册.
@@ -30,10 +31,7 @@ func (c *AccountController) Login()  {
 	if cookie,ok := c.GetSecureCookie(conf.GetAppKey(),"login");ok{
 
 		if err := utils.Decode(cookie,&remember); err == nil {
-			member := models.NewMember()
-			member.MemberId = remember.MemberId
-
-			if err := models.NewMember().Find(remember.MemberId); err == nil {
+			if member,err := models.NewMember().Find(remember.MemberId); err == nil {
 				c.SetMember(*member)
 
 				c.Redirect(beego.URLFor("HomeController.Index"), 302)
@@ -137,9 +135,193 @@ func (c *AccountController) Register()  {
 	}
 }
 
-func (p *AccountController) FindPassword()  {
-	p.TplName = "account/find_password.tpl"
+//找回密码.
+func (c *AccountController) FindPassword()  {
+	c.TplName = "account/find_password_setp1.tpl"
+	mail_conf := conf.GetMailConfig()
+
+	if c.Ctx.Input.IsPost() {
+
+		email := c.GetString("email")
+		captcha := c.GetString("code")
+
+		if email == "" {
+			c.JsonResult(6005,"邮箱地址不能为空")
+		}
+		if !mail_conf.EnableMail {
+			c.JsonResult(6004,"未启用邮件服务")
+		}
+
+		//如果开启了验证码
+		if v,ok := c.Option["ENABLED_CAPTCHA"]; ok && strings.EqualFold(v,"true") {
+			v,ok := c.GetSession(conf.CaptchaSessionName).(string);
+			if !ok || !strings.EqualFold(v,captcha){
+				c.JsonResult(6001,"验证码不正确")
+			}
+		}
+
+		member ,err := models.NewMember().FindByFieldFirst("email",email)
+		if err != nil {
+			c.JsonResult(6006,"邮箱不存在")
+		}
+		if member.Status != 0 {
+			c.JsonResult(6007,"账号已被禁用")
+		}
+
+
+		count,err := models.NewMemberToken().FindSendCount(email,time.Now().Add(-1*time.Hour),time.Now())
+
+		if err != nil {
+			beego.Error(err)
+			c.JsonResult(6008,"发送邮件失败")
+		}
+		if count > mail_conf.MailNumber {
+			c.JsonResult(6008,"发送次数太多,请稍候再试")
+		}
+
+		member_token := models.NewMemberToken()
+
+		member_token.Token = string(utils.Krand(32,utils.KC_RAND_KIND_ALL))
+		member_token.Email = email
+		member_token.MemberId = member.MemberId
+		member_token.IsValid = false
+		if _,err := member_token.InsertOrUpdate(); err != nil {
+			c.JsonResult(6009,"邮件发送失败")
+		}
+
+		data := map[string]interface{}{
+			"SITE_NAME" : c.Option["SITE_NAME"],
+			"url" : c.BaseUrl() + beego.URLFor("AccountController.FindPassword", "token",member_token.Token,"mail",email),
+		}
+
+		body,err := c.ExecuteViewPathTemplate("account/mail_template.tpl",data)
+		if err != nil {
+			beego.Error(err)
+			c.JsonResult(6003,"邮件发送失败")
+		}
+
+		go func(mail_conf *conf.SmtpConf,email string,body string) {
+			auth := smtp.PlainAuth(
+				"",
+				mail_conf.SmtpUserName,
+				mail_conf.SmtpPassword,
+				mail_conf.SmtpHost,
+			)
+
+			mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n";
+			subject := "Subject: 找回密码!\n"
+
+			err = smtp.SendMail(
+				mail_conf.SmtpHost + ":" + strconv.Itoa(mail_conf.SmtpPort),
+				auth,
+				mail_conf.FormUserName,
+				[]string{ email },
+				[]byte(subject + mime +"\n" +body),
+			)
+			if err != nil {
+				beego.Error("邮件发送失败 => ",email,err)
+			}
+		}(mail_conf,email,body)
+
+
+		c.JsonResult(0,"ok", c.BaseUrl() + beego.URLFor("AccountController.Login"))
+	}
+
+	token := c.GetString("token")
+	mail := c.GetString("mail")
+
+	if token != "" && mail != "" {
+		member_token,err := models.NewMemberToken().FindByFieldFirst("token",token)
+
+		if err != nil {
+			beego.Error(err)
+			c.Data["ErrorMessage"] = "邮件已失效"
+			c.TplName = "errors/error.tpl"
+			return
+		}
+		sub_time := member_token.SendTime.Sub(time.Now())
+
+		if !strings.EqualFold(member_token.Email,mail) || sub_time.Minutes() > float64(mail_conf.MailExpired) || !member_token.ValidTime.IsZero() {
+			c.Data["ErrorMessage"] = "验证码已过期,请重新操作。"
+			c.TplName = "errors/error.tpl"
+			return
+		}
+		c.Data["Email"] = member_token.Email
+		c.Data["Token"] = member_token.Token
+		c.TplName = "account/find_password_setp2.tpl"
+
+	}
+}
+
+//校验邮件并修改密码.
+func (c *AccountController) ValidEmail() {
+	c.Prepare()
+	password1 := c.GetString("password1")
+	password2 := c.GetString("password2")
+	captcha := c.GetString("code")
+	token := c.GetString("token")
+	mail := c.GetString("mail")
+
+	if password1 == "" {
+		c.JsonResult(6001,"密码不能为空")
+	}
+	if l :=  strings.Count(password1,""); l <6 || l > 50{
+		c.JsonResult(6001,"密码不能为空且必须在6-50个字符之间")
+	}
+	if password2 == ""{
+		c.JsonResult(6002,"确认密码不能为空")
+	}
+	if password1 != password2 {
+		c.JsonResult(6003,"确认密码输入不正确")
+	}
+	if captcha == "" {
+		c.JsonResult(6004,"验证码不能为空")
+	}
+	v,ok := c.GetSession(conf.CaptchaSessionName).(string);
+	if !ok || !strings.EqualFold(v,captcha){
+		c.JsonResult(6001,"验证码不正确")
+	}
+
+	mail_conf := conf.GetMailConfig()
+	member_token,err := models.NewMemberToken().FindByFieldFirst("token",token)
+
+	if err != nil {
+		beego.Error(err)
+		c.JsonResult(6007,"邮件已失效")
+	}
+	sub_time := member_token.SendTime.Sub(time.Now())
+
+	if !strings.EqualFold(member_token.Email,mail) || sub_time.Minutes() > float64(mail_conf.MailExpired) || !member_token.ValidTime.IsZero() {
+
+		c.JsonResult(6008,"验证码已过期,请重新操作。")
+	}
+	member ,err := models.NewMember().Find(member_token.MemberId)
+	if err != nil{
+		beego.Error(err)
+		c.JsonResult(6005,"用户不存在")
+	}
+	hash ,err := utils.PasswordHash(password1);
+
+	if  err != nil {
+		beego.Error(err)
+		c.JsonResult(6006,"保存密码失败")
+	}
+
+	member.Password = hash
+
+	err = member.Update("password")
+	member_token.ValidTime = time.Now()
+	member_token.IsValid = true
+	member_token.InsertOrUpdate()
+
+	if  err != nil {
+		beego.Error(err)
+		c.JsonResult(6006,"保存密码失败")
+	}
+	c.JsonResult(0,"ok",c.BaseUrl() + beego.URLFor("AccountController.Login"))
 }
+
+
 // Logout 退出登录.
 func (c *AccountController) Logout(){
 	c.SetMember(models.Member{});

+ 16 - 6
controllers/base.go

@@ -8,6 +8,8 @@ import (
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego"
 	"strings"
+	"encoding/json"
+	"io"
 )
 
 
@@ -62,17 +64,25 @@ func (c *BaseController) SetMember(member models.Member) {
 
 // JsonResult 响应 json 结果
 func (c *BaseController) JsonResult(errCode int,errMsg string,data ...interface{}){
-	json := make(map[string]interface{},3)
+	jsonData := make(map[string]interface{},3)
 
-	json["errcode"] = errCode
-	json["message"] = errMsg
+	jsonData["errcode"] = errCode
+	jsonData["message"] = errMsg
 
 	if len(data) > 0 && data[0] != nil{
-		json["data"] = data[0]
+		jsonData["data"] = data[0]
 	}
 
-	c.Data["json"] = json
-	c.ServeJSON(true)
+	returnJSON, err := json.Marshal(jsonData)
+
+	if err != nil {
+		beego.Error(err)
+	}
+
+	c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+	io.WriteString(c.Ctx.ResponseWriter,string(returnJSON))
+
 	c.StopRun()
 }
 

+ 1 - 1
controllers/book_member.go

@@ -88,7 +88,7 @@ func (c *BookMemberController) ChangeRole() {
 
 	member := models.NewMember()
 
-	if err := member.Find(member_id); err != nil {
+	if _,err := member.Find(member_id); err != nil {
 		c.JsonResult(6003,"用户不存在")
 	}
 	if member.Status == 1 {

+ 17 - 0
controllers/error.go

@@ -0,0 +1,17 @@
+package controllers
+
+type ErrorController struct {
+	BaseController
+}
+
+func (c *ErrorController) Error404() {
+	c.TplName = "errors/404.tpl"
+}
+
+func (c *ErrorController) Error403() {
+	c.TplName = "errors/403.tpl"
+}
+
+func (c *ErrorController) Error500() {
+	c.TplName = "errors/error.tpl"
+}

+ 2 - 2
controllers/manager.go

@@ -139,7 +139,7 @@ func (c *ManagerController) UpdateMemberStatus()  {
 	}
 	member := models.NewMember()
 
-	if err := member.Find(member_id); err != nil {
+	if _,err := member.Find(member_id); err != nil {
 		c.JsonResult(6002,"用户不存在")
 	}
 	member.Status = status
@@ -168,7 +168,7 @@ func (c *ManagerController) ChangeMemberRole()  {
 	}
 	member := models.NewMember()
 
-	if err := member.Find(member_id); err != nil {
+	if _,err := member.Find(member_id); err != nil {
 		c.JsonResult(6002,"用户不存在")
 	}
 	member.Role = role

+ 1 - 2
controllers/setting.go

@@ -141,8 +141,7 @@ func (c *SettingController) Upload() {
 
 	url := "/" + filePath
 
-	member := models.NewMember()
-	if err := member.Find(c.Member.MemberId);err == nil {
+	if member,err := models.NewMember().Find(c.Member.MemberId);err == nil {
 		member.Avatar = url
 		member.Update()
 		c.SetMember(*member)

+ 3 - 0
main.go

@@ -7,6 +7,7 @@ import (
 	"github.com/lifei6671/godoc/commands"
 	"fmt"
 	"os"
+	"github.com/lifei6671/godoc/controllers"
 )
 
 func main() {
@@ -21,5 +22,7 @@ func main() {
 
 	fmt.Println(os.Args[0])
 
+	beego.ErrorController(&controllers.ErrorController{})
 	beego.Run()
 }
+

+ 1 - 3
models/book_result.go

@@ -73,9 +73,7 @@ func (m *BookResult) FindByIdentify(identify string,member_id int) (*BookResult,
 		return m, ErrPermissionDenied
 	}
 
-	member := NewMember()
-
-	err = member.Find(relationship2.MemberId)
+	member, err := NewMember().Find(relationship2.MemberId)
 	if err != nil {
 		return m, err
 	}

+ 1 - 1
models/comment.go

@@ -118,7 +118,7 @@ func (m *Comment) Insert() error {
 	if m.MemberId > 0 {
 		member := NewMember()
 		//如果用户不存在
-		if err := member.Find(m.MemberId) ; err != nil {
+		if _,err := member.Find(m.MemberId) ; err != nil {
 			return ErrMemberNoExist
 		}
 		//如果用户被禁用

+ 32 - 4
models/member.go

@@ -7,6 +7,9 @@ import (
 	"github.com/lifei6671/godoc/utils"
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego/logs"
+	"errors"
+	"regexp"
+	"strings"
 )
 
 type Member struct {
@@ -14,7 +17,7 @@ type Member struct {
 	Account string 		`orm:"size(100);unique;column(account)" json:"account"`
 	Password string 	`orm:"size(1000);column(password)" json:"-"`
 	Description string	`orm:"column(description);size(2000)" json:"description"`
-	Email string 		`orm:"size(255);column(email);null;default(null)" json:"email"`
+	Email string 		`orm:"size(255);column(email);unique" json:"email"`
 	Phone string 		`orm:"size(255);column(phone);null;default(null)" json:"phone"`
 	Avatar string 		`orm:"size(1000);column(avatar)" json:"avatar"`
 	//用户角色:0 超级管理员 /1 管理员/ 2 普通用户 .
@@ -71,6 +74,22 @@ func (m *Member) Login(account string,password string) (*Member,error) {
 func (m *Member) Add () (error) {
 	o := orm.NewOrm()
 
+	if ok,err := regexp.MatchString(conf.RegexpAccount,m.Account); m.Account == "" || !ok || err != nil {
+		return errors.New("账号只能由英文字母数字组成,且在3-50个字符")
+	}
+	if m.Email == "" {
+		return errors.New("邮箱不能为空")
+	}
+	if  ok,err := regexp.MatchString(conf.RegexpEmail,m.Email); !ok || err != nil || m.Email == "" {
+		return errors.New("邮箱格式不正确")
+	}
+	if l :=  strings.Count(m.Password,""); l <6 || l > 50{
+		return errors.New("密码不能为空且必须在6-50个字符之间")
+	}
+	if c,err :=  o.QueryTable(m.TableNameWithPrefix()).Filter("email",m.Email).Count(); err == nil || c > 0 {
+		return  errors.New("邮箱已被使用")
+	}
+
 	hash ,err := utils.PasswordHash(m.Password);
 
 	if  err != nil {
@@ -92,21 +111,24 @@ func (m *Member) Add () (error) {
 func (m *Member) Update(cols... string) (error) {
 	o := orm.NewOrm()
 
+	if m.Email == "" {
+		return errors.New("邮箱不能为空")
+	}
 	if _,err := o.Update(m,cols...);err != nil {
 		return err
 	}
 	return nil
 }
 
-func (m *Member) Find(id int) error{
+func (m *Member) Find(id int) (*Member,error){
 	o := orm.NewOrm()
 
 	m.MemberId = id
 	if err := o.Read(m); err != nil {
-		return  err
+		return  m,err
 	}
 	m.ResolveRoleName()
-	return nil
+	return m,nil
 }
 
 func (m *Member) ResolveRoleName (){
@@ -163,7 +185,13 @@ func (c *Member) IsAdministrator() bool {
 	return c.Role == 0 || c.Role == 1
 }
 
+func (m *Member) FindByFieldFirst(field string,value interface{}) (*Member,error)  {
+	o := orm.NewOrm()
+
+	err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,value).OrderBy("-member_id").One(m)
 
+	return m,err
+}
 
 
 

+ 66 - 0
models/member_token.go

@@ -0,0 +1,66 @@
+package models
+
+import (
+	"time"
+	"github.com/lifei6671/godoc/conf"
+	"github.com/astaxie/beego/orm"
+)
+
+type MemberToken struct {
+	TokenId int		`orm:"column(token_id);pk;auto;unique" json:"token_id"`
+	MemberId int		`orm:"column(member_id);type(int)" json:"member_id"`
+	Token string		`orm:"column(token);size(255);index" json:"token"`
+	Email string 		`orm:"column(email);size(255)" json:"email"`
+	IsValid bool		`orm:"column(is_valid)" json:"is_valid"`
+	ValidTime time.Time	`orm:"column(valid_time);null" json:"valid_time"`
+	SendTime time.Time	`orm:"column(send_time);auto_now_add;type(datetime)" json:"send_time"`
+}
+
+
+// TableName 获取对应数据库表名.
+func (m *MemberToken) TableName() string {
+	return "member_token"
+}
+// TableEngine 获取数据使用的引擎.
+func (m *MemberToken) TableEngine() string {
+	return "INNODB"
+}
+
+func (m *MemberToken)TableNameWithPrefix() string {
+	return conf.GetDatabasePrefix() +  m.TableName()
+}
+
+func NewMemberToken() *MemberToken {
+	return &MemberToken{}
+}
+
+func (m *MemberToken)  InsertOrUpdate() (*MemberToken,error){
+	o := orm.NewOrm()
+
+	if m.TokenId > 0 {
+		_,err := o.Update(m)
+		return m,err
+	}
+	_,err := o.Insert(m)
+
+	return m,err
+}
+
+func (m *MemberToken) FindByFieldFirst(field string,value interface{}) (*MemberToken,error) {
+	o := orm.NewOrm()
+
+	err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,value).OrderBy("-token_id").One(m)
+
+	return m,err
+}
+
+func (m *MemberToken) FindSendCount(mail string,start_time time.Time,end_time time.Time) (int ,error) {
+	o := orm.NewOrm()
+
+	c,err := o.QueryTable(m.TableNameWithPrefix()).Filter("send_time__gte",start_time.Format("2006-01-02 15:04:05")).Filter("send_time__lte",end_time.Format("2006-01-02 15:04:05")).Count()
+
+	if err != nil {
+		return 0,err
+	}
+	return int(c),nil
+}

+ 7 - 0
routers/filter.go

@@ -22,4 +22,11 @@ func init()  {
 	beego.InsertFilter("/book",beego.BeforeRouter,FilterUser)
 	beego.InsertFilter("/book/*",beego.BeforeRouter,FilterUser)
 	beego.InsertFilter("/api/*",beego.BeforeRouter,FilterUser)
+
+	var FinishRouter = func(ctx *context.Context) {
+		ctx.ResponseWriter.Header().Add("MinDoc-Version",conf.Version())
+		ctx.ResponseWriter.Header().Add("MinDoc-Site","http://www.iminho.me")
+	}
+
+	beego.InsertFilter("/*",beego.BeforeRouter ,FinishRouter, false)
 }

+ 1 - 0
routers/router.go

@@ -12,6 +12,7 @@ func init()  {
 	beego.Router("/logout", &controllers.AccountController{},"*:Logout")
 	beego.Router("/register", &controllers.AccountController{},"*:Register")
 	beego.Router("/find_password", &controllers.AccountController{},"*:FindPassword")
+	beego.Router("/valid_email", &controllers.AccountController{},"post:ValidEmail")
 	beego.Router("/captcha", &controllers.AccountController{},"*:Captcha")
 
 	beego.Router("/manager", &controllers.ManagerController{},"*:Index")

BIN
static/fonts/3Dumb.ttf


BIN
static/fonts/BigBlocko.ttf


BIN
static/fonts/Bitsumishi.ttf


BIN
static/fonts/Comismsh.ttf


BIN
static/fonts/DENNEthree-dee.ttf


BIN
static/fonts/Esquisito.ttf


BIN
static/fonts/KREMLINGEORGIANI3D.ttf


BIN
static/fonts/Pointy.ttf


BIN
static/fonts/chromohv.ttf


+ 16 - 0
static/fonts/lato-100.css

@@ -0,0 +1,16 @@
+/* latin-ext */
+@font-face {
+    font-family: 'Lato';
+    font-style: normal;
+    font-weight: 100;
+    src: local('Lato Hairline'), local('Lato-Hairline'), url(lato/v11/eFRpvGLEW31oiexbYNx7Y_esZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
+    unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+    font-family: 'Lato';
+    font-style: normal;
+    font-weight: 100;
+    src: local('Lato Hairline'), local('Lato-Hairline'), url(lato/v11/GtRkRNTnri0g82CjKnEB0Q.woff2) format('woff2');
+    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
+}

BIN
static/fonts/lato/v11/GtRkRNTnri0g82CjKnEB0Q.woff2


BIN
static/fonts/lato/v11/eFRpvGLEW31oiexbYNx7Y_esZW2xOQ-xsNqO47m55DA.woff2


+ 130 - 0
views/account/find_password_setp1.tpl

@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html lang="zh-cn">
+<head>
+    <meta charset="utf-8">
+    <link rel="shortcut icon" href="/favicon.ico">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="author" content="SmartWiki" />
+    <title>找回密码 - Powered by MinDoc</title>
+
+    <!-- Bootstrap -->
+    <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+    <link href="/static/font-awesome/css/font-awesome.min.css" rel="stylesheet">
+    <link href="/static/css/main.css" rel="stylesheet">
+    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
+    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+    <!--[if lt IE 9]>
+    <script src="/static/html5shiv/3.7.3/html5shiv.min.js"></script>
+    <script src="/static/respond.js/1.4.2/respond.min.js"></script>
+    <![endif]-->
+    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
+    <script src="/static/jquery/1.12.4/jquery.min.js"></script>
+</head>
+<body class="manual-container">
+<header class="navbar navbar-static-top smart-nav navbar-fixed-top manual-header" role="banner">
+    <div class="container">
+        <div class="navbar-header col-sm-12 col-md-6 col-lg-5">
+            <a href="/" class="navbar-brand">MinDoc</a>
+        </div>
+    </div>
+</header>
+<div class="container manual-body">
+    <div class="row login">
+        <div class="login-body">
+            <form role="form" method="post" id="findPasswordForm">
+                <h3 class="text-center">找回密码</h3>
+                <div class="form-group">
+                    <div class="input-group">
+                        <div class="input-group-addon">
+                            <i class="fa fa-at"></i>
+                        </div>
+                        <input type="text" class="form-control" placeholder="邮箱" name="email" id="email" autocomplete="off">
+                    </div>
+                </div>
+                <div class="form-group">
+                    <div class="input-group" style="float: left;width: 195px;">
+                        <div class="input-group-addon">
+                            <i class="fa fa-check-square"></i>
+                        </div>
+                        <input type="text" name="code" id="code" class="form-control" style="width: 150px" maxlength="5" placeholder="验证码" autocomplete="off">&nbsp;
+                    </div>
+                    <img id="captcha-img" style="width: 140px;height: 40px;display: inline-block;float: right" src="{{urlfor "AccountController.Captcha"}}" onclick="this.src='{{urlfor "AccountController.Captcha"}}?key=login&t='+(new Date()).getTime();" title="点击换一张">
+                    <div class="clearfix"></div>
+                </div>
+
+                <div class="form-group">
+                    <button type="submit" id="btnSendMail" class="btn btn-success" style="width: 100%"  data-loading-text="正在处理..." autocomplete="off">找回密码</button>
+                </div>
+
+            </form>
+        </div>
+    </div>
+    <div class="clearfix"></div>
+</div>
+{{template "widgets/footer.tpl" .}}
+<!-- Include all compiled plugins (below), or include individual files as needed -->
+<script src="/static/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
+<script src="/static/layer/layer.js" type="text/javascript"></script>
+<script src="/static/js/jquery.form.js" type="text/javascript"></script>
+<script type="text/javascript">
+    $(function () {
+        $("#email,#code").on('focus',function () {
+            $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');;
+        });
+
+        $(document).keydown(function (e) {
+            var event = document.all ? window.event : e;
+            if(event.keyCode == 13){
+                $("#btn-login").click();
+            }
+        });
+
+        $("#findPasswordForm").ajaxForm({
+            beforeSubmit : function () {
+                var $btn = $(this).button('loading');
+
+                var email = $.trim($("#email").val());
+                if(email === ""){
+                    $("#email").tooltip({placement:"auto",title : "邮箱不能为空",trigger : 'manual'})
+                        .tooltip('show')
+                        .parents('.form-group').addClass('has-error');
+                    $btn.button('reset');
+                    return false;
+
+                }
+                var code = $.trim($("#code").val());
+                if(code === ""){
+                    $("#code").tooltip({title : '验证码不能为空',trigger : 'manual'})
+                        .tooltip('show')
+                        .parents('.form-group').addClass('has-error');
+                    $btn.button('reset');
+                    return false;
+                }
+                $("#btnSendMail").button("loading");
+            },
+            success : function (res) {
+
+                if(res.errcode !== 0){
+                    $("#captcha-img").click();
+                    $("#code").val('');
+                    layer.msg(res.message);
+                    $("#btnSendMail").button('reset');
+                }else{
+                    alert("邮件发送成功,请登录邮箱查看。")
+                    window.location = res.data;
+                }
+            },
+            error :function () {
+                $("#captcha-img").click();
+                $("#code").val('');
+                layer.msg('系统错误');
+                $("#btnSendMail").button('reset');
+            }
+        });
+
+    });
+</script>
+</body>
+</html>

+ 144 - 0
views/account/find_password_setp2.tpl

@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html lang="zh-cn">
+<head>
+    <meta charset="utf-8">
+    <link rel="shortcut icon" href="/favicon.ico">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="author" content="SmartWiki" />
+    <title>找回密码 - Powered by MinDoc</title>
+
+    <!-- Bootstrap -->
+    <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+    <link href="/static/font-awesome/css/font-awesome.min.css" rel="stylesheet">
+    <link href="/static/css/main.css" rel="stylesheet">
+    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
+    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+    <!--[if lt IE 9]>
+    <script src="/static/html5shiv/3.7.3/html5shiv.min.js"></script>
+    <script src="/static/respond.js/1.4.2/respond.min.js"></script>
+    <![endif]-->
+    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
+    <script src="/static/jquery/1.12.4/jquery.min.js"></script>
+</head>
+<body class="manual-container">
+<header class="navbar navbar-static-top smart-nav navbar-fixed-top manual-header" role="banner">
+    <div class="container">
+        <div class="navbar-header col-sm-12 col-md-6 col-lg-5">
+            <a href="/" class="navbar-brand">{{.SITE_NAME}}</a>
+        </div>
+    </div>
+</header>
+<div class="container manual-body">
+    <div class="row login">
+        <div class="login-body">
+            <form role="form" method="post" id="findPasswordForm" action="{{urlfor "AccountController.ValidEmail"}}">
+                <input type="hidden" name="token" value="{{.Token}}">
+                <input type="hidden" name="mail" value="{{.Email}}">
+                <h3 class="text-center">找回密码</h3>
+                <div class="form-group">
+                    <label for="newPasswd">新密码</label>
+                    <input type="password" class="form-control" name="password1" id="newPassword" maxlength="20" placeholder="新密码"  autocomplete="off">
+                </div>
+                <div class="form-group">
+                    <label for="configPasswd">确认密码</label>
+                    <input type="password" class="form-control" id="confirmPassword" name="password2" maxlength="20" placeholder="确认密码"  autocomplete="off">
+                </div>
+
+                <div class="form-group">
+                    <div class="input-group" style="float: left;width: 195px;">
+                        <div class="input-group-addon">
+                            <i class="fa fa-check-square"></i>
+                        </div>
+                        <input type="text" name="code" id="code" class="form-control" style="width: 150px" maxlength="5" placeholder="验证码" autocomplete="off">&nbsp;
+                    </div>
+                    <img id="captcha-img" style="width: 140px;height: 40px;display: inline-block;float: right" src="{{urlfor "AccountController.Captcha"}}" onclick="this.src='{{urlfor "AccountController.Captcha"}}?key=login&t='+(new Date()).getTime();" title="点击换一张">
+                    <div class="clearfix"></div>
+                </div>
+                <div class="form-group">
+                    <button type="submit" id="btnSendMail" class="btn btn-success" style="width: 100%"  data-loading-text="正在处理..." autocomplete="off">找回密码</button>
+                </div>
+
+            </form>
+        </div>
+    </div>
+    <div class="clearfix"></div>
+</div>
+{{template "widgets/footer.tpl" .}}
+<!-- Include all compiled plugins (below), or include individual files as needed -->
+<script src="/static/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
+<script src="/static/layer/layer.js" type="text/javascript"></script>
+<script src="/static/js/jquery.form.js" type="text/javascript"></script>
+<script type="text/javascript">
+    $(function () {
+        $("#email,#code").on('focus',function () {
+            $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');;
+        });
+
+        $(document).keydown(function (e) {
+            var event = document.all ? window.event : e;
+            if(event.keyCode == 13){
+                $("#btn-login").click();
+            }
+        });
+
+        $("#findPasswordForm").ajaxForm({
+            beforeSubmit : function () {
+
+                var newPassword = $.trim($("#newPassword").val());
+                var confirmPassword = $.trim($("#confirmPassword").val());
+                var code = $.trim($("#code").val());
+
+                if(newPassword === ""){
+                    $("#newPassword").tooltip({placement:"auto",title : "密码不能为空",trigger : 'manual'})
+                        .tooltip('show')
+                        .parents('.form-group').addClass('has-error');
+
+                    return false;
+
+                }else if(confirmPassword === ""){
+                    $("#confirmPassword").tooltip({placement:"auto",title : "确认密码不能为空",trigger : 'manual'})
+                        .tooltip('show')
+                        .parents('.form-group').addClass('has-error');
+
+                    return false;
+                }else if(newPassword !== confirmPassword) {
+                    $("#confirmPassword").tooltip({placement:"auto",title : "确认密码输入不正确",trigger : 'manual'})
+                        .tooltip('show')
+                        .parents('.form-group').addClass('has-error');
+
+                    return false;
+                }else if(code === ""){
+                    $("#code").tooltip({title : '验证码不能为空',trigger : 'manual'})
+                        .tooltip('show')
+                        .parents('.form-group').addClass('has-error');
+
+                    return false;
+                }
+
+                $("#btnSendMail").button("loading");
+            },
+            success : function (res) {
+
+                if(res.errcode !== 0){
+                    $("#captcha-img").click();
+                    $("#code").val('');
+                    layer.msg(res.message);
+                    $("#btnSendMail").button('reset');
+                }else{
+                    window.location = res.data;
+                }
+            },
+            error :function () {
+                $("#captcha-img").click();
+                $("#code").val('');
+                layer.msg('系统错误');
+                $("#btnSendMail").button('reset');
+            }
+        });
+
+    });
+</script>
+</body>
+</html>

+ 98 - 0
views/account/mail_template.tpl

@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="author" content="SmartWiki" />
+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
+    <title>找回密码 - Powered by MinDoc</title>
+    <style type="text/css">
+        .ua-macos::-webkit-scrollbar{ display: none; }
+        html,body{background-color: transparent;margin:0;padding: 0;}
+        body{font: 16px/1.5 "Microsoft Yahei", "微软雅黑", verdana;word-wrap:break-word;}
+        .js-dialog{font-size: 14px;}
+        pre, .js-pre {
+            white-space: pre-wrap;
+            white-space: -moz-pre-wrap;
+            white-space: -pre-wrap;
+            white-space: -o-pre-wrap;
+            word-wrap: break-word;
+            font: 16px/1.5 "Microsoft Yahei", "微软雅黑", verdana;
+            padding:8px 10px;margin:0;
+        }
+        .rm_line{border-top:2px solid #F1F1F1; font-size:0; margin:15px 0}
+        .atchImg img{border:2px solid #c3d9ff;}
+        .lnkTxt{ color:#0066CC}
+        .rm_PicArea *{ font-family: "Microsoft Yahei", "微软雅黑", verdana;font-size:16px;font-weight:700;}
+        .fbk3{ color:#333; line-height:160%}
+        .fTip{ font-size:11px; font-weight:normal}
+
+        img{border:none;vertical-align: middle;}
+        iframe{display:none;}
+        *{word-break:break-word;}
+        #neteaseEncryptedMail{display:none;}
+        #jy-translate{
+            position: absolute;
+            max-width: 500px;
+            min-width: 100px;
+            _width:300px;
+            border: 1px solid rgb(204, 204, 204);
+            padding: 4px 18px 4px 10px;
+            background-color: #f9f9f9;
+            -webkit-border-radius:3px;
+            -moz-border-radius:3px;
+            border-radius:3px;
+            -webkit-box-shadow:#dddddd 0px 0px 10px;
+            -moz-box-shadow:#dddddd 0px 0px 10px;
+            box-shadow:#dddddd 0px 0px 10px;
+        }
+        #jy-translate h2,
+        #jy-translate p{color:#555;margin:0;padding:0;}
+        #jy-translate h2{line-height: 28px;font-size: 14px;}
+        #jy-translate p{line-height: 24px;font-size: 12px;}
+        #jy-translate h2 span{font-weight:normal;}
+        .ua-noyahei,
+        .ua-noyahei .pre,
+        .ua-noyahei .js-pre,
+        .ua-noyahei .rm_PicArea *{font-family: \5b8b\4f53, sans-serif;}
+        .ua-macos,
+        .ua-macos .pre,
+        .ua-macos .js-pre,
+        .ua-macos .rm_PicArea *{font-family: "Lucida Grande","Hiragino Sans GB","Hiragino Sans GB W3", verdana;}
+
+        .jy-contact{float: left;}
+        .jy-contact-hover{background: #eee;}
+        .jy-contact img.oprt{width: 23px;height: 23px;border: 0;vertical-align: middle;cursor: pointer;}
+    </style>
+</head>
+<body onunload="" class="js-body">
+<div>
+    <div class="wrapper" style="margin: 20px auto 0; width: 500px; padding-top:16px; padding-bottom:10px;">
+        <div class="header clearfix">
+            <a class="logo" href="https://www.iminho.me/" target="_blank"><b>MinDoc</b></a>
+        </div>
+        <br style="clear:both; height:0">
+        <div class="content" style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #E9E9E9; margin: 2px 0 0; padding: 30px;">
+
+            <p>您好: </p>
+
+            <p>您在 {{.SITE_NAME}} 提交了找回密码申请。<br>如果您没有提交修改密码的申请, 请忽略本邮件</p>
+
+            <p style="border-top: 1px solid #DDDDDD;margin: 15px 0 25px;padding: 15px;">
+                请点击链接继续: <a href="{{.url}}" target="_blank">{{.url}}</a>
+            </p>
+            <p>
+                好的密码,不但应该容易记住,还要尽量符合以下强度标准:
+            <ul>
+                <li>包含大小写字母、数字和符号</li>
+                <li>不少于 10 位 </li>
+                <li>不包含生日、手机号码等易被猜出的信息</li>
+            </ul>
+            </p>
+            <p class="footer" style="border-top: 1px solid #DDDDDD; padding-top:6px; margin-top:25px; color:#838383;">
+                请勿回复本邮件, 此邮箱未受监控, 您不会得到任何回复. 要获得帮助, 请登录网站<br><br>
+                <a href="https://www.iminho.me/" target="_blank">MinDoc</a>
+            </p>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 2 - 0
views/document/default_read.tpl

@@ -110,6 +110,7 @@
                     <div class="article-body  {{if eq .Model.Editor "markdown"}}markdown-body editormd-preview-container{{else}}editor-content{{end}}"  id="page-content">
                         {{.Content}}
                     </div>
+                    {{/*
                     {{if .Model.IsDisplayComment}}
                     <div id="articleComment" class="m-comment">
                         <div class="comment-result">
@@ -154,6 +155,7 @@
                         </div>
                     </div>
                     {{end}}
+*/}}
                 </div>
 
             </div>

+ 52 - 0
views/errors/403.tpl

@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="author" content="Minho" />
+    <link rel="shortcut icon" href="/favicon.ico">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>权限不足 - Powered by MinDoc</title>
+    <link href="/static/fonts/lato-100.css" rel="stylesheet" type="text/css">
+    <style type="text/css">
+        html, body {
+            height: 100%;
+        }
+
+        body {
+            margin: 0;
+            padding: 0;
+            width: 100%;
+            color: #B0BEC5;
+            display: table;
+            font-weight: 100;
+            font-family: 'Lato',"Microsoft Yahei","Helvetica Neue",Helvetica,Arial,sans-serif;
+        }
+
+        .container {
+            text-align: center;
+            display: table-cell;
+            vertical-align: middle;
+        }
+
+        .content {
+            text-align: center;
+            display: inline-block;
+        }
+
+        .title {
+            font-size: 72px;
+            margin-bottom: 40px;
+            color: red;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <div class="content">
+        <div class="title">HTTP 403 : 权限不足</div>
+    </div>
+</div>
+</body>
+</html>

+ 51 - 0
views/errors/404.tpl

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="author" content="Minho" />
+    <link rel="shortcut icon" href="/favicon.ico">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>页面不存在 - Powered by MinDoc</title>
+    <link href="/static/fonts/lato-100.css" rel="stylesheet" type="text/css">
+    <style type="text/css">
+        html, body {
+            height: 100%;
+        }
+
+        body {
+            margin: 0;
+            padding: 0;
+            width: 100%;
+            color: #B0BEC5;
+            display: table;
+            font-weight: 100;
+            font-family: 'Lato',"Microsoft Yahei","Helvetica Neue",Helvetica,Arial,sans-serif;
+        }
+
+        .container {
+            text-align: center;
+            display: table-cell;
+            vertical-align: middle;
+        }
+
+        .content {
+            text-align: center;
+            display: inline-block;
+        }
+
+        .title {
+            font-size: 72px;
+            margin-bottom: 40px;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <div class="content">
+        <div class="title">HTTP 404 : 页面不存在.</div>
+    </div>
+</div>
+</body>
+</html>

+ 59 - 0
views/errors/error.tpl

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="author" content="Minho" />
+    <link rel="shortcut icon" href="/favicon.ico">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>服务器异常 - Powered by MinDoc</title>
+    <link href="/static/fonts/lato-100.css" rel="stylesheet" type="text/css">
+    <style type="text/css">
+        html, body {
+            height: 100%;
+        }
+
+        body {
+            margin: 0;
+            padding: 0;
+            width: 100%;
+            color: #B0BEC5;
+            display: table;
+            font-weight: 100;
+            font-family: 'Lato';
+        }
+
+        .container {
+            text-align: center;
+            display: table-cell;
+            vertical-align: middle;
+        }
+
+        .content {
+            text-align: center;
+            display: inline-block;
+        }
+
+        .title {
+            font-size: 72px;
+            margin-bottom: 40px;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <div class="content">
+        {{if .ErrorMessage}}
+        {{if .ErrorCode}}
+        <div class="title">HTTP {{.ErrorCode}} : {{.ErrorMessage}}</div>
+        {{else}}
+        <div class="title">HTTP 500 : {{.ErrorMessage}}</div>
+        {{end}}
+        {{else}}
+        <div class="title">HTTP 500 : 服务器异常</div>
+        {{end}}
+    </div>
+</div>
+</body>
+</html>