浏览代码

Merge branch 'master' into new-beego-path

roberChen 4 年之前
父节点
当前提交
272316f7b4

+ 8 - 7
commands/command.go

@@ -34,17 +34,17 @@ import (
 // RegisterDataBase 注册数据库
 func RegisterDataBase() {
 	logs.Info("正在初始化数据库配置.")
-	dbadapter,_ := web.AppConfig.String("db_adapter")
+	dbadapter, _ := web.AppConfig.String("db_adapter")
 	orm.DefaultTimeLoc = time.Local
 	orm.DefaultRowsLimit = -1
 
 	if strings.EqualFold(dbadapter, "mysql") {
-		host,_ := web.AppConfig.String("db_host")
+		host, _ := web.AppConfig.String("db_host")
 		database, _ := web.AppConfig.String("db_database")
-		username,_ := web.AppConfig.String("db_username")
-		password,_ := web.AppConfig.String("db_password")
+		username, _ := web.AppConfig.String("db_username")
+		password, _ := web.AppConfig.String("db_password")
 
-		timezone,_ := web.AppConfig.String("timezone")
+		timezone, _ := web.AppConfig.String("timezone")
 		location, err := time.LoadLocation(timezone)
 		if err == nil {
 			orm.DefaultTimeLoc = location
@@ -52,7 +52,7 @@ func RegisterDataBase() {
 			logs.Error("加载时区配置信息失败,请检查是否存在 ZONEINFO 环境变量->", err)
 		}
 
-		port,_ := web.AppConfig.String("db_port")
+		port, _ := web.AppConfig.String("db_port")
 
 		dataSource := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s", username, password, host, port, database, url.QueryEscape(timezone))
 
@@ -63,7 +63,7 @@ func RegisterDataBase() {
 
 	} else if strings.EqualFold(dbadapter, "sqlite3") {
 
-		database,_ := web.AppConfig.String("db_database")
+		database, _ := web.AppConfig.String("db_database")
 		if strings.HasPrefix(database, "./") {
 			database = filepath.Join(conf.WorkingDirectory, string(database[1:]))
 		}
@@ -327,6 +327,7 @@ func ResolveCommand(args []string) {
 	web.BConfig.WebConfig.StaticDir["/static"] = filepath.Join(conf.WorkingDirectory, "static")
 	web.BConfig.WebConfig.StaticDir["/uploads"] = uploads
 	web.BConfig.WebConfig.ViewsPath = conf.WorkingDir("views")
+	web.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteDefaultMode
 
 	fonts := conf.WorkingDir("static", "fonts")
 

+ 15 - 0
conf/app.conf.example

@@ -7,6 +7,7 @@ runmode = "${MINDOC_RUN_MODE||dev}"
 sessionon = true
 sessionname = mindoc_id
 copyrequestbody = true
+enablexsrf = "${MINDOC_ENABLE_XSRF||true}"
 
 #系统完整URL(http://doc.iminho.me),如果该项不设置,会从请求头中获取地址。
 baseurl="${MINDOC_BASE_URL}"
@@ -209,6 +210,20 @@ log_level="${MINDOC_LOG_LEVEL||Alert}"
 # 是否异步生成日志,默认是 true
 log_is_async="${MINDOC_LOG_IS_ASYNC||TRUE}"
 
+##########钉钉应用相关配置##############
+
+# 企业钉钉ID
+dingtalk_corpid="${MINDOC_DINGTALK_CORPID}"
+
+# 钉钉AppKey
+dingtalk_app_key="${MINDOC_DINGTALK_APPKEY}"
+
+# 钉钉AppSecret
+dingtalk_app_secret="${MINDOC_DINGTALK_APPSECRET}"
+
+# 钉钉登录默认只读账号
+dingtalk_tmp_reader="${MINDOC_DINGTALK_READER}"
+
 
 
 

+ 61 - 1
controllers/AccountController.go

@@ -9,11 +9,13 @@ import (
 	"html/template"
 
 	"github.com/beego/beego/v2/adapter/logs"
+	"github.com/beego/beego/v2/server/web"
 	"github.com/lifei6671/gocaptcha"
 	"github.com/mindoc-org/mindoc/conf"
 	"github.com/mindoc-org/mindoc/mail"
 	"github.com/mindoc-org/mindoc/models"
 	"github.com/mindoc-org/mindoc/utils"
+	"github.com/mindoc-org/mindoc/utils/dingtalk"
 )
 
 // AccountController 用户登录与注册
@@ -31,8 +33,12 @@ func (c *AccountController) referer() string {
 
 func (c *AccountController) Prepare() {
 	c.BaseController.Prepare()
-	c.EnableXSRF = true
+	c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true)
 	c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML())
+	c.Data["corpID"],_ = web.AppConfig.String("dingtalk_corpid")
+	if !c.EnableXSRF {
+		return
+	}
 	if c.Ctx.Input.IsPost() {
 		token := c.Ctx.Input.Query("_xsrf")
 		if token == "" {
@@ -132,6 +138,60 @@ func (c *AccountController) Login() {
 	}
 }
 
+// 钉钉登录
+func (c *AccountController) DingTalkLogin() {
+	c.Prepare()
+
+	code := c.GetString("dingtalk_code")
+	if code == "" {
+		c.JsonResult(500, "获取身份信息失败", nil)
+	}
+
+	appKey, _ := web.AppConfig.String("dingtalk_app_key")
+	appSecret, _ := web.AppConfig.String("dingtalk_app_secret")
+	tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader")
+
+	if appKey == "" || appSecret == "" || tmpReader == "" {
+		c.JsonResult(500, "未开启钉钉自动登录功能", nil)
+		c.StopRun()
+	}
+
+	dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey)
+	err := dingtalkAgent.GetAccesstoken()
+	if err != nil {
+		logs.Warn("获取钉钉临时Token失败 ->", err)
+		c.JsonResult(500, "自动登录失败", nil)
+		c.StopRun()
+	}
+
+	userid, err := dingtalkAgent.GetUserIDByCode(code)
+	if err != nil {
+		logs.Warn("钉钉自动登录失败 ->", err)
+		c.JsonResult(500, "自动登录失败", nil)
+		c.StopRun()
+	}
+
+	username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
+	if err != nil {
+		logs.Warn("钉钉自动登录失败 ->", err)
+		c.JsonResult(500, "自动登录失败", nil)
+		c.StopRun()
+	}
+
+	member, err := models.NewMember().TmpLogin(tmpReader)
+	if err == nil {
+		member.LastLoginTime = time.Now()
+		_ = member.Update("last_login_time")
+		member.Account = username
+		if avatar != "" {
+			member.Avatar = avatar
+		}
+
+		c.SetMember(*member)
+	}
+	c.JsonResult(0, "ok", username)
+}
+
 // 登录成功后的操作,如重定向到原始请求页面
 func (c *AccountController) LoggedIn(isPost bool) interface{} {
 

+ 1 - 1
go.mod

@@ -5,7 +5,7 @@ go 1.12
 require (
 	github.com/PuerkitoBio/goquery v1.4.1
 	github.com/andybalholm/cascadia v1.2.0 // indirect
-	github.com/beego/beego/v2 v2.0.2-0.20210322114547-10ea897525a5 // indirect
+	github.com/beego/beego/v2 v2.0.2-0.20210322114547-10ea897525a5
 	github.com/boombuler/barcode v1.0.0
 	github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
 	github.com/howeyc/fsnotify v0.9.0

+ 0 - 8
go.sum

@@ -11,9 +11,6 @@ github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGn
 github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
 github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
 github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
-github.com/beego/beego/v2 v2.0.0/go.mod h1:8zyHi1FnWO1mZLwTn62aKRIZF/aIKvkCBB2JYs+eqQI=
-github.com/beego/beego/v2 v2.0.1 h1:07a7Z0Ok5vbqyqh+q53sDPl9LdhKh0ZDy3gbyGrhFnE=
-github.com/beego/beego/v2 v2.0.1/go.mod h1:8zyHi1FnWO1mZLwTn62aKRIZF/aIKvkCBB2JYs+eqQI=
 github.com/beego/beego/v2 v2.0.2-0.20210322114547-10ea897525a5 h1:i0swsv6hmoF6pkeG2S/dvOWwOKFPGDKhrmTlWH6I1nM=
 github.com/beego/beego/v2 v2.0.2-0.20210322114547-10ea897525a5/go.mod h1:JlRUJ/NVNygorqjyt7/lQ8R++KSE0qXvxeIfbnIUd7Q=
 github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
@@ -37,11 +34,8 @@ github.com/coreos/etcd v3.3.25+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
 github.com/couchbase/go-couchbase v0.0.0-20210126152612-8e416c37c8ef/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
-github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
 github.com/couchbase/gomemcached v0.1.2-0.20210126151728-840240974836/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
-github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
 github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
 github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -216,7 +210,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
 golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -268,7 +261,6 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
 golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 2 - 0
models/Errors.go

@@ -19,6 +19,8 @@ var (
 	ErrorMemberPasswordError = errors.New("用户密码错误")
 	//ErrorMemberAuthMethodInvalid 不支持此认证方式
 	ErrMemberAuthMethodInvalid = errors.New("不支持此认证方式")
+	//ErrHTTPServerFail
+	ErrHTTPServerFail = errors.New("系统内部异常")
 	//ErrLDAPConnect 无法连接到LDAP服务器
 	ErrLDAPConnect = errors.New("无法连接到LDAP服务器")
 	//ErrLDAPFirstBind 第一次LDAP绑定失败

+ 15 - 4
models/Member.go

@@ -105,6 +105,17 @@ func (m *Member) Login(account string, password string) (*Member, error) {
 	return member, ErrorMemberPasswordError
 }
 
+// TmpLogin 用于钉钉临时登录
+func (m *Member) TmpLogin(account string) (*Member, error) {
+	o := orm.NewOrm()
+	member := &Member{}
+	err := o.Raw("select * from md_members where account = ? and status = 0 limit 1;", account).QueryRow(member)
+	if err != nil {
+		return member, ErrorMemberPasswordError
+	}
+	return member, nil
+}
+
 //ldapLogin 通过LDAP登陆
 func (m *Member) ldapLogin(account string, password string) (*Member, error) {
 	if web.AppConfig.DefaultBool("ldap_enable", false) {
@@ -187,14 +198,14 @@ func (m *Member) httpLogin(account, password string) (*Member, error) {
 	resp, err := http.PostForm(urlStr, val)
 	if err != nil {
 		logs.Error("通过接口登录失败 -> ", urlStr, account, err)
-		return nil, err
+		return nil, ErrHTTPServerFail
 	}
 	defer resp.Body.Close()
 
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		logs.Error("读取接口返回值失败 -> ", urlStr, account, err)
-		return nil, err
+		return nil, ErrHTTPServerFail
 	}
 	logs.Info("HTTP 登录接口返回数据 ->", string(body))
 
@@ -202,7 +213,7 @@ func (m *Member) httpLogin(account, password string) (*Member, error) {
 
 	if err := json.Unmarshal(body, &result); err != nil {
 		logs.Error("解析接口返回值失败 -> ", urlStr, account, string(body))
-		return nil, errors.New("解析接口返回值失败")
+		return nil, ErrHTTPServerFail
 	}
 
 	if code, ok := result["errcode"]; !ok || code.(float64) != 200 {
@@ -210,7 +221,7 @@ func (m *Member) httpLogin(account, password string) (*Member, error) {
 		if msg, ok := result["message"]; ok {
 			return nil, errors.New(msg.(string))
 		}
-		return nil, errors.New("接口返回值格式不正确")
+		return nil, ErrHTTPServerFail
 	}
 	if m.MemberId <= 0 {
 		member := NewMember()

+ 4 - 4
models/TeamMember.go

@@ -205,16 +205,16 @@ func (m *TeamMember) FindNotJoinMemberByAccount(teamId int, account string, limi
 	}
 	o := orm.NewOrm()
 
-	sql := `select member.member_id,member.account,team.team_member_id
+	sql := `select member.member_id,member.account,member.real_name,team.team_member_id
 from md_members as member 
   left join md_team_member as team on team.team_id = ? and member.member_id = team.member_id
-  where member.account like ? AND team_member_id IS NULL
+  where member.account like ? or member.real_name like ? AND team_member_id IS NULL
   order by member.member_id desc 
 limit ?;`
 
 	members := make([]*Member, 0)
 
-	_, err := o.Raw(sql, teamId, "%"+account+"%", limit).QueryRows(&members)
+	_, err := o.Raw(sql, teamId, "%"+account+"%", "%"+account+"%", limit).QueryRows(&members)
 
 	if err != nil {
 		logs.Error("查询团队用户时出错 ->", err)
@@ -227,7 +227,7 @@ limit ?;`
 	for _, member := range members {
 		item := KeyValueItem{}
 		item.Id = member.MemberId
-		item.Text = member.Account
+		item.Text = member.Account + "[" + member.RealName + "]"
 		items = append(items, item)
 	}
 	result.Result = items

+ 132 - 131
routers/router.go

@@ -1,146 +1,147 @@
 package routers
 
 import (
-	"github.com/beego/beego/v2/adapter"
+	"github.com/beego/beego/v2/server/web"
 	"github.com/mindoc-org/mindoc/controllers"
 )
 
 func init() {
-	adapter.Router("/", &controllers.HomeController{}, "*:Index")
-
-	adapter.Router("/login", &controllers.AccountController{}, "*:Login")
-	adapter.Router("/logout", &controllers.AccountController{}, "*:Logout")
-	adapter.Router("/register", &controllers.AccountController{}, "*:Register")
-	adapter.Router("/find_password", &controllers.AccountController{}, "*:FindPassword")
-	adapter.Router("/valid_email", &controllers.AccountController{}, "post:ValidEmail")
-	adapter.Router("/captcha", &controllers.AccountController{}, "*:Captcha")
-
-	adapter.Router("/manager", &controllers.ManagerController{}, "*:Index")
-	adapter.Router("/manager/users", &controllers.ManagerController{}, "*:Users")
-	adapter.Router("/manager/users/edit/:id", &controllers.ManagerController{}, "*:EditMember")
-	adapter.Router("/manager/member/create", &controllers.ManagerController{}, "post:CreateMember")
-	adapter.Router("/manager/member/delete", &controllers.ManagerController{}, "post:DeleteMember")
-	adapter.Router("/manager/member/update-member-status", &controllers.ManagerController{}, "post:UpdateMemberStatus")
-	adapter.Router("/manager/member/change-member-role", &controllers.ManagerController{}, "post:ChangeMemberRole")
-	adapter.Router("/manager/books", &controllers.ManagerController{}, "*:Books")
-	adapter.Router("/manager/books/edit/:key", &controllers.ManagerController{}, "*:EditBook")
-	adapter.Router("/manager/books/delete", &controllers.ManagerController{}, "*:DeleteBook")
-
-	adapter.Router("/manager/comments", &controllers.ManagerController{}, "*:Comments")
-	adapter.Router("/manager/setting", &controllers.ManagerController{}, "*:Setting")
-	adapter.Router("/manager/books/token", &controllers.ManagerController{}, "post:CreateToken")
-	adapter.Router("/manager/books/transfer", &controllers.ManagerController{}, "post:Transfer")
-	adapter.Router("/manager/books/open", &controllers.ManagerController{}, "post:PrivatelyOwned")
-
-	adapter.Router("/manager/attach/list", &controllers.ManagerController{}, "*:AttachList")
-	adapter.Router("/manager/attach/detailed/:id", &controllers.ManagerController{}, "*:AttachDetailed")
-	adapter.Router("/manager/attach/delete", &controllers.ManagerController{}, "post:AttachDelete")
-	adapter.Router("/manager/label/list", &controllers.ManagerController{}, "get:LabelList")
-	adapter.Router("/manager/label/delete/:id", &controllers.ManagerController{}, "post:LabelDelete")
-
-	//adapter.Router("/manager/config",  &controllers.ManagerController{}, "*:Config")
-
-	adapter.Router("/manager/team", &controllers.ManagerController{}, "*:Team")
-	adapter.Router("/manager/team/create", &controllers.ManagerController{}, "POST:TeamCreate")
-	adapter.Router("/manager/team/edit", &controllers.ManagerController{}, "POST:TeamEdit")
-	adapter.Router("/manager/team/delete", &controllers.ManagerController{}, "POST:TeamDelete")
-
-	adapter.Router("/manager/team/member/list/:id", &controllers.ManagerController{}, "*:TeamMemberList")
-	adapter.Router("/manager/team/member/add", &controllers.ManagerController{}, "POST:TeamMemberAdd")
-	adapter.Router("/manager/team/member/delete", &controllers.ManagerController{}, "POST:TeamMemberDelete")
-	adapter.Router("/manager/team/member/change_role", &controllers.ManagerController{}, "POST:TeamChangeMemberRole")
-	adapter.Router("/manager/team/member/search", &controllers.ManagerController{}, "*:TeamSearchMember")
-
-	adapter.Router("/manager/team/book/list/:id", &controllers.ManagerController{}, "*:TeamBookList")
-	adapter.Router("/manager/team/book/add", &controllers.ManagerController{}, "POST:TeamBookAdd")
-	adapter.Router("/manager/team/book/delete", &controllers.ManagerController{}, "POST:TeamBookDelete")
-	adapter.Router("/manager/team/book/search", &controllers.ManagerController{}, "*:TeamSearchBook")
-
-	adapter.Router("/manager/itemsets", &controllers.ManagerController{}, "*:Itemsets")
-	adapter.Router("/manager/itemsets/edit", &controllers.ManagerController{}, "post:ItemsetsEdit")
-	adapter.Router("/manager/itemsets/delete", &controllers.ManagerController{}, "post:ItemsetsDelete")
-
-	adapter.Router("/setting", &controllers.SettingController{}, "*:Index")
-	adapter.Router("/setting/password", &controllers.SettingController{}, "*:Password")
-	adapter.Router("/setting/upload", &controllers.SettingController{}, "*:Upload")
-
-	adapter.Router("/book", &controllers.BookController{}, "*:Index")
-	adapter.Router("/book/:key/dashboard", &controllers.BookController{}, "*:Dashboard")
-	adapter.Router("/book/:key/setting", &controllers.BookController{}, "*:Setting")
-	adapter.Router("/book/:key/users", &controllers.BookController{}, "*:Users")
-	adapter.Router("/book/:key/release", &controllers.BookController{}, "post:Release")
-	adapter.Router("/book/:key/sort", &controllers.BookController{}, "post:SaveSort")
-	adapter.Router("/book/:key/teams", &controllers.BookController{}, "*:Team")
-
-	adapter.Router("/book/create", &controllers.BookController{}, "*:Create")
-	adapter.Router("/book/itemsets/search", &controllers.BookController{}, "*:ItemsetsSearch")
-
-	adapter.Router("/book/users/create", &controllers.BookMemberController{}, "post:AddMember")
-	adapter.Router("/book/users/change", &controllers.BookMemberController{}, "post:ChangeRole")
-	adapter.Router("/book/users/delete", &controllers.BookMemberController{}, "post:RemoveMember")
-	adapter.Router("/book/users/import", &controllers.BookController{}, "post:Import")
-	adapter.Router("/book/users/copy", &controllers.BookController{}, "post:Copy")
-
-	adapter.Router("/book/setting/save", &controllers.BookController{}, "post:SaveBook")
-	adapter.Router("/book/setting/open", &controllers.BookController{}, "post:PrivatelyOwned")
-	adapter.Router("/book/setting/transfer", &controllers.BookController{}, "post:Transfer")
-	adapter.Router("/book/setting/upload", &controllers.BookController{}, "post:UploadCover")
-	adapter.Router("/book/setting/delete", &controllers.BookController{}, "post:Delete")
-
-	adapter.Router("/book/team/add", &controllers.BookController{}, "POST:TeamAdd")
-	adapter.Router("/book/team/delete", &controllers.BookController{}, "POST:TeamDelete")
-	adapter.Router("/book/team/search", &controllers.BookController{}, "*:TeamSearch")
+	web.Router("/", &controllers.HomeController{}, "*:Index")
+
+	web.Router("/login", &controllers.AccountController{}, "*:Login")
+	web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin")
+	web.Router("/logout", &controllers.AccountController{}, "*:Logout")
+	web.Router("/register", &controllers.AccountController{}, "*:Register")
+	web.Router("/find_password", &controllers.AccountController{}, "*:FindPassword")
+	web.Router("/valid_email", &controllers.AccountController{}, "post:ValidEmail")
+	web.Router("/captcha", &controllers.AccountController{}, "*:Captcha")
+
+	web.Router("/manager", &controllers.ManagerController{}, "*:Index")
+	web.Router("/manager/users", &controllers.ManagerController{}, "*:Users")
+	web.Router("/manager/users/edit/:id", &controllers.ManagerController{}, "*:EditMember")
+	web.Router("/manager/member/create", &controllers.ManagerController{}, "post:CreateMember")
+	web.Router("/manager/member/delete", &controllers.ManagerController{}, "post:DeleteMember")
+	web.Router("/manager/member/update-member-status", &controllers.ManagerController{}, "post:UpdateMemberStatus")
+	web.Router("/manager/member/change-member-role", &controllers.ManagerController{}, "post:ChangeMemberRole")
+	web.Router("/manager/books", &controllers.ManagerController{}, "*:Books")
+	web.Router("/manager/books/edit/:key", &controllers.ManagerController{}, "*:EditBook")
+	web.Router("/manager/books/delete", &controllers.ManagerController{}, "*:DeleteBook")
+
+	web.Router("/manager/comments", &controllers.ManagerController{}, "*:Comments")
+	web.Router("/manager/setting", &controllers.ManagerController{}, "*:Setting")
+	web.Router("/manager/books/token", &controllers.ManagerController{}, "post:CreateToken")
+	web.Router("/manager/books/transfer", &controllers.ManagerController{}, "post:Transfer")
+	web.Router("/manager/books/open", &controllers.ManagerController{}, "post:PrivatelyOwned")
+
+	web.Router("/manager/attach/list", &controllers.ManagerController{}, "*:AttachList")
+	web.Router("/manager/attach/detailed/:id", &controllers.ManagerController{}, "*:AttachDetailed")
+	web.Router("/manager/attach/delete", &controllers.ManagerController{}, "post:AttachDelete")
+	web.Router("/manager/label/list", &controllers.ManagerController{}, "get:LabelList")
+	web.Router("/manager/label/delete/:id", &controllers.ManagerController{}, "post:LabelDelete")
+
+	//web.Router("/manager/config",  &controllers.ManagerController{}, "*:Config")
+
+	web.Router("/manager/team", &controllers.ManagerController{}, "*:Team")
+	web.Router("/manager/team/create", &controllers.ManagerController{}, "POST:TeamCreate")
+	web.Router("/manager/team/edit", &controllers.ManagerController{}, "POST:TeamEdit")
+	web.Router("/manager/team/delete", &controllers.ManagerController{}, "POST:TeamDelete")
+
+	web.Router("/manager/team/member/list/:id", &controllers.ManagerController{}, "*:TeamMemberList")
+	web.Router("/manager/team/member/add", &controllers.ManagerController{}, "POST:TeamMemberAdd")
+	web.Router("/manager/team/member/delete", &controllers.ManagerController{}, "POST:TeamMemberDelete")
+	web.Router("/manager/team/member/change_role", &controllers.ManagerController{}, "POST:TeamChangeMemberRole")
+	web.Router("/manager/team/member/search", &controllers.ManagerController{}, "*:TeamSearchMember")
+
+	web.Router("/manager/team/book/list/:id", &controllers.ManagerController{}, "*:TeamBookList")
+	web.Router("/manager/team/book/add", &controllers.ManagerController{}, "POST:TeamBookAdd")
+	web.Router("/manager/team/book/delete", &controllers.ManagerController{}, "POST:TeamBookDelete")
+	web.Router("/manager/team/book/search", &controllers.ManagerController{}, "*:TeamSearchBook")
+
+	web.Router("/manager/itemsets", &controllers.ManagerController{}, "*:Itemsets")
+	web.Router("/manager/itemsets/edit", &controllers.ManagerController{}, "post:ItemsetsEdit")
+	web.Router("/manager/itemsets/delete", &controllers.ManagerController{}, "post:ItemsetsDelete")
+
+	web.Router("/setting", &controllers.SettingController{}, "*:Index")
+	web.Router("/setting/password", &controllers.SettingController{}, "*:Password")
+	web.Router("/setting/upload", &controllers.SettingController{}, "*:Upload")
+
+	web.Router("/book", &controllers.BookController{}, "*:Index")
+	web.Router("/book/:key/dashboard", &controllers.BookController{}, "*:Dashboard")
+	web.Router("/book/:key/setting", &controllers.BookController{}, "*:Setting")
+	web.Router("/book/:key/users", &controllers.BookController{}, "*:Users")
+	web.Router("/book/:key/release", &controllers.BookController{}, "post:Release")
+	web.Router("/book/:key/sort", &controllers.BookController{}, "post:SaveSort")
+	web.Router("/book/:key/teams", &controllers.BookController{}, "*:Team")
+
+	web.Router("/book/create", &controllers.BookController{}, "*:Create")
+	web.Router("/book/itemsets/search", &controllers.BookController{}, "*:ItemsetsSearch")
+
+	web.Router("/book/users/create", &controllers.BookMemberController{}, "post:AddMember")
+	web.Router("/book/users/change", &controllers.BookMemberController{}, "post:ChangeRole")
+	web.Router("/book/users/delete", &controllers.BookMemberController{}, "post:RemoveMember")
+	web.Router("/book/users/import", &controllers.BookController{}, "post:Import")
+	web.Router("/book/users/copy", &controllers.BookController{}, "post:Copy")
+
+	web.Router("/book/setting/save", &controllers.BookController{}, "post:SaveBook")
+	web.Router("/book/setting/open", &controllers.BookController{}, "post:PrivatelyOwned")
+	web.Router("/book/setting/transfer", &controllers.BookController{}, "post:Transfer")
+	web.Router("/book/setting/upload", &controllers.BookController{}, "post:UploadCover")
+	web.Router("/book/setting/delete", &controllers.BookController{}, "post:Delete")
+
+	web.Router("/book/team/add", &controllers.BookController{}, "POST:TeamAdd")
+	web.Router("/book/team/delete", &controllers.BookController{}, "POST:TeamDelete")
+	web.Router("/book/team/search", &controllers.BookController{}, "*:TeamSearch")
 
 	//管理文章的路由
-	adapter.Router("/manage/blogs", &controllers.BlogController{}, "*:ManageList")
-	adapter.Router("/manage/blogs/setting/?:id", &controllers.BlogController{}, "*:ManageSetting")
-	adapter.Router("/manage/blogs/edit/?:id", &controllers.BlogController{}, "*:ManageEdit")
-	adapter.Router("/manage/blogs/delete", &controllers.BlogController{}, "post:ManageDelete")
-	adapter.Router("/manage/blogs/upload", &controllers.BlogController{}, "post:Upload")
-	adapter.Router("/manage/blogs/attach/:id", &controllers.BlogController{}, "post:RemoveAttachment")
+	web.Router("/manage/blogs", &controllers.BlogController{}, "*:ManageList")
+	web.Router("/manage/blogs/setting/?:id", &controllers.BlogController{}, "*:ManageSetting")
+	web.Router("/manage/blogs/edit/?:id", &controllers.BlogController{}, "*:ManageEdit")
+	web.Router("/manage/blogs/delete", &controllers.BlogController{}, "post:ManageDelete")
+	web.Router("/manage/blogs/upload", &controllers.BlogController{}, "post:Upload")
+	web.Router("/manage/blogs/attach/:id", &controllers.BlogController{}, "post:RemoveAttachment")
 
 	//读文章的路由
-	adapter.Router("/blogs", &controllers.BlogController{}, "*:List")
-	adapter.Router("/blog-attach/:id:int/:attach_id:int", &controllers.BlogController{}, "get:Download")
-	adapter.Router("/blog-:id([0-9]+).html", &controllers.BlogController{}, "*:Index")
+	web.Router("/blogs", &controllers.BlogController{}, "*:List")
+	web.Router("/blog-attach/:id:int/:attach_id:int", &controllers.BlogController{}, "get:Download")
+	web.Router("/blog-:id([0-9]+).html", &controllers.BlogController{}, "*:Index")
 
 	//模板相关接口
-	adapter.Router("/api/template/get", &controllers.TemplateController{}, "get:Get")
-	adapter.Router("/api/template/list", &controllers.TemplateController{}, "post:List")
-	adapter.Router("/api/template/add", &controllers.TemplateController{}, "post:Add")
-	adapter.Router("/api/template/remove", &controllers.TemplateController{}, "post:Delete")
-
-	adapter.Router("/api/attach/remove/", &controllers.DocumentController{}, "post:RemoveAttachment")
-	adapter.Router("/api/:key/edit/?:id", &controllers.DocumentController{}, "*:Edit")
-	adapter.Router("/api/upload", &controllers.DocumentController{}, "post:Upload")
-	adapter.Router("/api/:key/create", &controllers.DocumentController{}, "post:Create")
-	adapter.Router("/api/:key/delete", &controllers.DocumentController{}, "post:Delete")
-	adapter.Router("/api/:key/content/?:id", &controllers.DocumentController{}, "*:Content")
-	adapter.Router("/api/:key/compare/:id", &controllers.DocumentController{}, "*:Compare")
-	adapter.Router("/api/search/user/:key", &controllers.SearchController{}, "*:User")
-
-	adapter.Router("/history/get", &controllers.DocumentController{}, "get:History")
-	adapter.Router("/history/delete", &controllers.DocumentController{}, "*:DeleteHistory")
-	adapter.Router("/history/restore", &controllers.DocumentController{}, "*:RestoreHistory")
-
-	adapter.Router("/docs/:key", &controllers.DocumentController{}, "*:Index")
-	adapter.Router("/docs/:key/:id", &controllers.DocumentController{}, "*:Read")
-	adapter.Router("/docs/:key/search", &controllers.DocumentController{}, "post:Search")
-	adapter.Router("/export/:key", &controllers.DocumentController{}, "*:Export")
-	adapter.Router("/qrcode/:key.png", &controllers.DocumentController{}, "get:QrCode")
-
-	adapter.Router("/attach_files/:key/:attach_id", &controllers.DocumentController{}, "get:DownloadAttachment")
-
-	adapter.Router("/comment/create", &controllers.CommentController{}, "post:Create")
-	adapter.Router("/comment/lists", &controllers.CommentController{}, "get:Lists")
-	adapter.Router("/comment/index", &controllers.CommentController{}, "*:Index")
-
-	adapter.Router("/search", &controllers.SearchController{}, "get:Index")
-
-	adapter.Router("/tag/:key", &controllers.LabelController{}, "get:Index")
-	adapter.Router("/tags", &controllers.LabelController{}, "get:List")
-
-	adapter.Router("/items", &controllers.ItemsetsController{}, "get:Index")
-	adapter.Router("/items/:key", &controllers.ItemsetsController{}, "get:List")
+	web.Router("/api/template/get", &controllers.TemplateController{}, "get:Get")
+	web.Router("/api/template/list", &controllers.TemplateController{}, "post:List")
+	web.Router("/api/template/add", &controllers.TemplateController{}, "post:Add")
+	web.Router("/api/template/remove", &controllers.TemplateController{}, "post:Delete")
+
+	web.Router("/api/attach/remove/", &controllers.DocumentController{}, "post:RemoveAttachment")
+	web.Router("/api/:key/edit/?:id", &controllers.DocumentController{}, "*:Edit")
+	web.Router("/api/upload", &controllers.DocumentController{}, "post:Upload")
+	web.Router("/api/:key/create", &controllers.DocumentController{}, "post:Create")
+	web.Router("/api/:key/delete", &controllers.DocumentController{}, "post:Delete")
+	web.Router("/api/:key/content/?:id", &controllers.DocumentController{}, "*:Content")
+	web.Router("/api/:key/compare/:id", &controllers.DocumentController{}, "*:Compare")
+	web.Router("/api/search/user/:key", &controllers.SearchController{}, "*:User")
+
+	web.Router("/history/get", &controllers.DocumentController{}, "get:History")
+	web.Router("/history/delete", &controllers.DocumentController{}, "*:DeleteHistory")
+	web.Router("/history/restore", &controllers.DocumentController{}, "*:RestoreHistory")
+
+	web.Router("/docs/:key", &controllers.DocumentController{}, "*:Index")
+	web.Router("/docs/:key/:id", &controllers.DocumentController{}, "*:Read")
+	web.Router("/docs/:key/search", &controllers.DocumentController{}, "post:Search")
+	web.Router("/export/:key", &controllers.DocumentController{}, "*:Export")
+	web.Router("/qrcode/:key.png", &controllers.DocumentController{}, "get:QrCode")
+
+	web.Router("/attach_files/:key/:attach_id", &controllers.DocumentController{}, "get:DownloadAttachment")
+
+	web.Router("/comment/create", &controllers.CommentController{}, "post:Create")
+	web.Router("/comment/lists", &controllers.CommentController{}, "get:Lists")
+	web.Router("/comment/index", &controllers.CommentController{}, "*:Index")
+
+	web.Router("/search", &controllers.SearchController{}, "get:Index")
+
+	web.Router("/tag/:key", &controllers.LabelController{}, "get:Index")
+	web.Router("/tags", &controllers.LabelController{}, "get:List")
+
+	web.Router("/items", &controllers.ItemsetsController{}, "get:Index")
+	web.Router("/items/:key", &controllers.ItemsetsController{}, "get:List")
 
 }

+ 18 - 0
static/css/main.css

@@ -1037,6 +1037,24 @@ textarea{
         margin-left: 0;
     }
 }
+.navbar-mobile {
+    display: inline-block;
+    padding: 10px 0;
+    font-size: 14px;
+    line-height: 30px;
+    color: #563d7c;
+}
+.navbar-mobile a {
+    padding-right: 5px;
+}
+
+@media (min-width: 768px) {
+    .navbar-mobile {
+        display: none !important;
+    }
+}
+
+
 
 
 

文件差异内容过多而无法显示
+ 0 - 0
static/js/dingtalk-jsapi.js


+ 147 - 0
utils/dingtalk/dingtalk.go

@@ -0,0 +1,147 @@
+package dingtalk
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+)
+
+// DingTalkAgent 用于钉钉交互
+type DingTalkAgent struct {
+	AppSecret   string
+	AppKey      string
+	AccessToken string
+}
+
+// NewDingTalkAgent 钉钉交互构造函数
+func NewDingTalkAgent(appSecret, appKey string) *DingTalkAgent {
+	return &DingTalkAgent{
+		AppSecret: appSecret,
+		AppKey:    appKey,
+	}
+}
+
+// GetUserIDByCode 通过临时code获取当前用户ID
+func (d *DingTalkAgent) GetUserIDByCode(code string) (string, error) {
+	urlEndpoint, err := url.Parse("https://oapi.dingtalk.com/user/getuserinfo")
+	if err != nil {
+		return "", err
+	}
+
+	query := url.Values{}
+	query.Set("access_token", d.AccessToken)
+	query.Set("code", code)
+
+	urlEndpoint.RawQuery = query.Encode()
+	urlPath := urlEndpoint.String()
+
+	resp, err := http.Get(urlPath)
+	if err != nil {
+		return "", err
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+
+	// 解析钉钉返回数据
+	var rdata map[string]interface{}
+	err = json.Unmarshal(body, &rdata)
+	if err != nil {
+		return "", err
+	}
+
+	errcode := rdata["errcode"].(float64)
+	if errcode != 0 {
+		return "", errors.New(fmt.Sprintf("登录错误: %.0f, %s", errcode, rdata["errmsg"].(string)))
+	}
+
+	userid := rdata["userid"].(string)
+	return userid, nil
+}
+
+// GetUserNameAndAvatarByUserID 通过userid获取当前用户姓名和头像
+func (d *DingTalkAgent) GetUserNameAndAvatarByUserID(userid string) (string, string, error) {
+	urlEndpoint, err := url.Parse("https://oapi.dingtalk.com/topapi/v2/user/get")
+	if err != nil {
+		return "", "", err
+	}
+
+	query := url.Values{}
+	query.Set("access_token", d.AccessToken)
+
+	urlEndpoint.RawQuery = query.Encode()
+	urlPath := urlEndpoint.String()
+
+	resp, err := http.PostForm(urlPath, url.Values{"userid": {userid}})
+	if err != nil {
+		return "", "", err
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", "", err
+	}
+
+	// 解析钉钉返回数据
+	var rdata map[string]interface{}
+	err = json.Unmarshal(body, &rdata)
+	if err != nil {
+		return "", "", err
+	}
+
+	errcode := rdata["errcode"].(float64)
+	if errcode != 0 {
+		return "", "", errors.New(fmt.Sprintf("登录错误: %.0f, %s", errcode, rdata["errmsg"].(string)))
+	}
+
+	userinfo := rdata["result"].(map[string]interface{})
+	username := userinfo["name"].(string)
+	avatar := userinfo["avatar"].(string)
+	return username, avatar, nil
+}
+
+// GetAccesstoken 获取钉钉请求Token
+func (d *DingTalkAgent) GetAccesstoken() (err error) {
+
+	url := fmt.Sprintf("https://oapi.dingtalk.com/gettoken?appkey=%s&appsecret=%s", d.AppKey, d.AppSecret)
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+
+	var i map[string]interface{}
+	err = json.Unmarshal(body, &i)
+	if err != nil {
+		return err
+	}
+
+	if i["errcode"].(float64) == 0 {
+		d.AccessToken = i["access_token"].(string)
+		return nil
+	}
+	return errors.New("accesstoken获取错误:" + i["errmsg"].(string))
+}
+
+func (d *DingTalkAgent) encodeSHA256(message string) string {
+	// 钉钉签名算法实现
+	h := hmac.New(sha256.New, []byte(d.AppSecret))
+	h.Write([]byte(message))
+	sum := h.Sum(nil) // 二进制流
+	tmpMsg := base64.StdEncoding.EncodeToString(sum)
+
+	uv := url.Values{}
+	uv.Add("0", tmpMsg)
+	message = uv.Encode()[2:]
+
+	return message
+}

+ 41 - 0
views/account/login.tpl

@@ -86,6 +86,47 @@
 <!-- Include all compiled plugins (below), or include individual files as needed -->
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
+<script src="{{cdnjs "/static/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
+<script type="text/javascript">
+    $(function () {
+        if (dd.env.platform !== "notInDingTalk"){
+            dd.ready(function() {
+                dd.runtime.permission.requestAuthCode({
+                    corpId: {{ .corpID }} , // 企业id
+                    onSuccess: function (info) {
+                        var index = layer.load(1, {
+                            shade: [0.1, '#fff'] // 0.1 透明度的白色背景
+                        })
+
+                        var formData = $("form").serializeArray()
+                        formData.push({"name": "dingtalk_code", "value": info.code})
+
+                        $.ajax({
+                            url: "{{urlfor "AccountController.DingTalkLogin"}} ",
+                            data: formData,
+                            dataType: "json",
+                            type: "POST",
+                            complete: function(){
+                                layer.close(index)
+                            },
+                            success: function (res) {
+                                if (res.errcode !== 0) {
+                                    layer.msg(res.message)
+                                } else {
+                                    window.location = "{{ urlfor "HomeController.Index"  }}"
+                                }
+                            },
+                            error: function (res) {
+                                layer.msg("发生异常")
+                            }
+                        })
+                    }
+                });
+            });
+        }
+    })
+
+</script>
 <script type="text/javascript">
     $(function () {
         $("#account,#password,#code").on('focus', function () {

+ 1 - 1
views/blog/list.tpl

@@ -30,7 +30,7 @@
             <div class="manual-list">
             {{range $index,$item := .Lists}}
                 <div class="search-item">
-                    <div class="title">{{if eq $item.BlogStatus "password"}}<span class="label">密</span>{{end}} <a href="{{urlfor "BlogController.Index" ":id" $item.BlogId}}" title="{{$item.BlogTitle}}" target="_blank">{{$item.BlogTitle}}</a> </div>
+                    <div class="title">{{if eq $item.BlogStatus "password"}}<span class="label">密</span>{{end}} <a href="{{urlfor "BlogController.Index" ":id" $item.BlogId}}" title="{{$item.BlogTitle}}">{{$item.BlogTitle}}</a> </div>
                     <div class="description">
                     {{$item.BlogExcerpt}}
                     </div>

+ 14 - 0
views/document/default_read.tpl

@@ -30,6 +30,19 @@
     <link href="{{cdncss "/static/css/print.css" "version"}}" media="print" rel="stylesheet">
 
     <script type="text/javascript">window.book={"identify":"{{.Model.Identify}}"};</script>
+    <style>
+        .btn-mobile {
+            position: absolute;
+            right: 10px;
+            top: 10px;
+        }
+
+        @media screen and (min-width: 840px) {
+            .btn-mobile{
+                display: none;
+            }
+        }
+    </style>
 </head>
 <body>
 <div class="m-manual manual-mode-view manual-reader">
@@ -40,6 +53,7 @@
                 <a href="{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" title="{{.Model.BookName}}" class="book-title">{{.Model.BookName}}</a>
                 <span style="font-size: 12px;font-weight: 100;"></span>
             </div>
+            <a href="{{urlfor "HomeController.Index"}}" class="btn btn-default btn-mobile"> <i class="fa fa-home" aria-hidden="true"></i>首页</a>
             <div class="navbar-header pull-right manual-menu">
                 <a href="javascript:window.print();" id="printSinglePage" class="btn btn-default" style="margin-right: 10px;"><i class="fa fa-print"></i> 打印</a>
                 {{if gt .Member.MemberId 0}}

+ 2 - 2
views/home/index.tpl

@@ -26,12 +26,12 @@
                     <div class="list-item">
                         <dl class="manual-item-standard">
                             <dt>
-                                <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" title="{{$item.BookName}}-{{$item.CreateName}}" target="_blank">
+                                <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" title="{{$item.BookName}}-{{$item.CreateName}}">
                                     <img src="{{cdnimg $item.Cover}}" class="cover" alt="{{$item.BookName}}-{{$item.CreateName}}" onerror="this.src='{{cdnimg "static/images/book.jpg"}}';">
                                 </a>
                             </dt>
                             <dd>
-                                <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" class="name" title="{{$item.BookName}}-{{$item.CreateName}}" target="_blank">{{$item.BookName}}</a>
+                                <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" class="name" title="{{$item.BookName}}-{{$item.CreateName}}">{{$item.BookName}}</a>
                             </dd>
                             <dd>
                             <span class="author">

+ 4 - 0
views/widgets/header.tpl

@@ -29,6 +29,10 @@
                     </form>
                 </div>
             </nav>
+            <div style="display: inline-block;" class="navbar-mobile">
+                <a href="{{urlfor "HomeController.Index" }}" title="首页">首页</a>
+                <a href="{{urlfor "BlogController.List" }}" title="文章">文章</a>
+            </div>
 
             <div class="btn-group dropdown-menu-right pull-right slidebar visible-xs-inline-block visible-sm-inline-block">
                 <button class="btn btn-default dropdown-toggle hidden-lg" type="button" data-toggle="dropdown"><i class="fa fa-align-justify"></i></button>

部分文件因为文件数量过多而无法显示