URL | +Priority | +Change Frequency | +LastChange (GMT) | +
---|---|---|---|
+ |
+
+ |
+
+ |
+
+ |
+
将在3秒后跳转到首页
`) + } else { + data["error"] = template.HTML(`链接无效或过期,请重新操作。忘记密码?`) + } + return render(ctx, contentTpl, data) + } + + data["valid"] = true + data["code"] = uuid + // 提交修改密码 + if passwd != "" && method == "POST" { + // 简单校验 + if len(passwd) < 6 || len(passwd) > 32 { + data["error"] = "密码长度必须在6到32个字符之间" + } else if passwd != ctx.FormValue("pass2") { + data["error"] = "两次密码输入不一致" + } else { + // 更新密码 + _, err := logic.DefaultUser.ResetPasswd(context.EchoContext(ctx), email, passwd) + if err != nil { + data["error"] = "对不起,服务器错误,请重试!" + } else { + data["success"] = template.HTML(`密码重置成功,将在3秒后跳转到登录页面
`) + } + } + } + return render(ctx, contentTpl, data) +} + +// Logout 注销 +func (AccountController) Logout(ctx echo.Context) error { + // 删除cookie信息 + session := GetCookieSession(ctx) + session.Options = &sessions.Options{Path: "/", MaxAge: -1} + session.Save(Request(ctx), ResponseWriter(ctx)) + // 重定向得到原页面 + return ctx.Redirect(http.StatusSeeOther, ctx.Request().Referer()) +} + +// Unbind 第三方账号解绑 +func (AccountController) Unbind(ctx echo.Context) error { + bindId := ctx.FormValue("bind_id") + me := ctx.Get("user").(*model.Me) + logic.DefaultThirdUser.UnBindUser(context.EchoContext(ctx), bindId, me) + + return ctx.Redirect(http.StatusSeeOther, "/account/edit#connection") +} diff --git a/internal/http/controller/admin/article.go b/internal/http/controller/admin/article.go new file mode 100644 index 00000000..ff9d5d44 --- /dev/null +++ b/internal/http/controller/admin/article.go @@ -0,0 +1,168 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ArticleController struct{} + +// 注册路由 +func (self ArticleController) RegisterRoute(g *echo.Group) { + g.GET("/crawl/article/list", self.ArticleList) + g.POST("/crawl/article/query.html", self.ArticleQuery) + g.POST("/crawl/article/move", self.MoveToTopic) + g.Match([]string{"GET", "POST"}, "/crawl/article/new", self.CrawlArticle) + g.Match([]string{"GET", "POST"}, "/crawl/article/publish", self.Publish) + g.Match([]string{"GET", "POST"}, "/crawl/article/modify", self.Modify) +} + +// ArticleList 所有文章(分页) +func (ArticleController) ArticleList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), nil, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "article/list.html,article/query.html", data) +} + +// ArticleQuery +func (ArticleController) ArticleQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"id", "domain", "title"}) + + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), conds, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "article/query.html", data) +} + +// CrawlArticle +func (ArticleController) CrawlArticle(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + urls := strings.Split(ctx.FormValue("urls"), "\n") + + var ( + errMsg string + err error + ) + for _, url := range urls { + url = strings.TrimSpace(url) + + if strings.HasPrefix(url, "http") { + _, err = logic.DefaultArticle.ParseArticle(context.EchoContext(ctx), url, false) + } else { + isAll := false + websiteInfo := strings.Split(url, ":") + if len(websiteInfo) >= 2 { + isAll = goutils.MustBool(websiteInfo[1]) + } + err = logic.DefaultAutoCrawl.CrawlWebsite(strings.TrimSpace(websiteInfo[0]), isAll) + } + + if err != nil { + errMsg = err.Error() + } + } + + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + return render(ctx, "article/new.html", data) +} + +// Publish +func (self ArticleController) Publish(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultArticle.PublishFromAdmin(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + return success(ctx, nil) + } + + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "article/publish.html", data) +} + +// Modify +func (self ArticleController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.ArticleList))) + } + + data["article"] = article + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "article/modify.html", data) +} + +// MoveToTopic 放入 Topic 中 +func (self ArticleController) MoveToTopic(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + err := logic.DefaultArticle.MoveToTopic(context.EchoContext(ctx), ctx.QueryParam("id"), user) + + if err != nil { + return fail(ctx, 1, err.Error()) + } + return success(ctx, nil) +} diff --git a/internal/http/controller/admin/authority.go b/internal/http/controller/admin/authority.go new file mode 100644 index 00000000..7118fe97 --- /dev/null +++ b/internal/http/controller/admin/authority.go @@ -0,0 +1,155 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type AuthorityController struct{} + +// 注册路由 +func (self AuthorityController) RegisterRoute(g *echo.Group) { + g.GET("/user/auth/list", self.AuthList) + g.POST("/user/auth/query.html", self.AuthQuery) +} + +// AuthList 所有权限(分页) +func (AuthorityController) AuthList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + total := len(logic.Authorities) + newLimit := limit + if total < limit { + newLimit = total + } + + data := map[string]interface{}{ + "datalist": logic.Authorities[(curPage - 1):newLimit], + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "authority/list.html,authority/query.html", data) +} + +func (AuthorityController) AuthQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + conds := parseConds(ctx, []string{"route", "name"}) + + authorities, total := logic.DefaultAuthority.FindAuthoritiesByPage(context.EchoContext(ctx), conds, curPage, limit) + + if authorities == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": authorities, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "authority/query.html", data) +} + +// func NewAuthorityHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// if req.PostFormValue("submit") == "1" { +// user, _ := filter.CurrentUser(req) +// username := user["username"].(string) + +// errMsg, err := service.SaveAuthority(req.PostForm, username) +// if err != nil { +// data["ok"] = 0 +// data["error"] = errMsg +// } else { +// data["ok"] = 1 +// data["msg"] = "添加成功" +// } +// } else { +// menu1, menu2 := service.GetMenus() +// allmenu2, _ := json.Marshal(menu2) + +// // 设置内容模板 +// req.Form.Set(filter.CONTENT_TPL_KEY, "/template/admin/authority/new.html") +// data["allmenu1"] = menu1 +// data["allmenu2"] = string(allmenu2) +// } + +// filter.SetData(req, data) +// } + +// func ModifyAuthorityHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// if req.PostFormValue("submit") == "1" { +// user, _ := filter.CurrentUser(req) +// username := user["username"].(string) + +// errMsg, err := service.SaveAuthority(req.PostForm, username) +// if err != nil { +// data["ok"] = 0 +// data["error"] = errMsg +// } else { +// data["ok"] = 1 +// data["msg"] = "修改成功" +// } +// } else { +// menu1, menu2 := service.GetMenus() +// allmenu2, _ := json.Marshal(menu2) + +// authority := service.FindAuthority(req.FormValue("aid")) + +// if authority == nil || authority.Aid == 0 { +// rw.WriteHeader(http.StatusInternalServerError) +// return +// } + +// // 设置内容模板 +// req.Form.Set(filter.CONTENT_TPL_KEY, "/template/admin/authority/modify.html") +// data["allmenu1"] = menu1 +// data["allmenu2"] = string(allmenu2) +// data["authority"] = authority +// } + +// filter.SetData(req, data) +// } + +// func DelAuthorityHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// aid := req.FormValue("aid") + +// if _, err := strconv.Atoi(aid); err != nil { +// data["ok"] = 0 +// data["error"] = "aid不是整型" + +// filter.SetData(req, data) +// return +// } + +// if err := service.DelAuthority(aid); err != nil { +// data["ok"] = 0 +// data["error"] = "删除失败!" +// } else { +// data["ok"] = 1 +// data["msg"] = "删除成功!" +// } + +// filter.SetData(req, data) +// } diff --git a/internal/http/controller/admin/base.go b/internal/http/controller/admin/base.go new file mode 100644 index 00000000..215d8328 --- /dev/null +++ b/internal/http/controller/admin/base.go @@ -0,0 +1,94 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +func parsePage(ctx echo.Context) (curPage, limit int) { + curPage = goutils.MustInt(ctx.FormValue("page"), 1) + limit = goutils.MustInt(ctx.FormValue("limit"), 20) + return +} + +func parseConds(ctx echo.Context, fields []string) map[string]string { + conds := make(map[string]string) + + for _, field := range fields { + if value := ctx.FormValue(field); value != "" { + conds[field] = value + } + } + + return conds +} + +func getLogger(ctx echo.Context) *logger.Logger { + return logic.GetLogger(context.EchoContext(ctx)) +} + +// render html 输出 +func render(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + return RenderAdmin(ctx, contentTpl, data) +} + +func renderQuery(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + return RenderQuery(ctx, contentTpl, data) +} + +func success(ctx echo.Context, data interface{}) error { + result := map[string]interface{}{ + "ok": 1, + "msg": "操作成功", + "data": data, + } + + b, err := json.Marshal(result) + if err != nil { + return err + } + + go func(b []byte) { + if cacheKey := ctx.Get(nosql.CacheKey); cacheKey != nil { + nosql.DefaultLRUCache.CompressAndAdd(cacheKey, b, nosql.NewCacheData()) + } + }(b) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + return ctx.JSONBlob(http.StatusOK, b) +} + +func fail(ctx echo.Context, code int, msg string) error { + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + result := map[string]interface{}{ + "ok": 0, + "error": msg, + } + + getLogger(ctx).Errorln("operate fail:", result) + + return ctx.JSON(http.StatusOK, result) +} diff --git a/internal/http/controller/admin/index.go b/internal/http/controller/admin/index.go new file mode 100644 index 00000000..9835979d --- /dev/null +++ b/internal/http/controller/admin/index.go @@ -0,0 +1,15 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + echo "github.com/labstack/echo/v4" +) + +func AdminIndex(ctx echo.Context) error { + return render(ctx, "index.html", nil) +} diff --git a/internal/http/controller/admin/metrics.go b/internal/http/controller/admin/metrics.go new file mode 100644 index 00000000..5f7d83be --- /dev/null +++ b/internal/http/controller/admin/metrics.go @@ -0,0 +1,64 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "expvar" + + "net/http" + "strconv" + "time" + + "github.com/studygolang/studygolang/global" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +var ( + onlineStats = expvar.NewMap("online_stats") + loginUserNum expvar.Int + visitorUserNum expvar.Int +) + +type MetricsController struct{} + +// 注册路由 +func (self MetricsController) RegisterRoute(g *echo.Group) { + g.GET("/debug/vars", self.DebugExpvar) + g.GET("/user/is_online", self.IsOnline) +} + +func (self MetricsController) DebugExpvar(ctx echo.Context) error { + loginUserNum.Set(int64(logic.Book.LoginLen())) + visitorUserNum.Set(int64(logic.Book.Len())) + + onlineStats.Set("login_user_num", &loginUserNum) + onlineStats.Set("visitor_user_num", &visitorUserNum) + onlineStats.Set("uptime", expvar.Func(self.calculateUptime)) + onlineStats.Set("login_user_data", logic.Book.LoginUserData()) + + handler := expvar.Handler() + handler.ServeHTTP(ResponseWriter(ctx), Request(ctx)) + return nil +} + +func (self MetricsController) IsOnline(ctx echo.Context) error { + uid := goutils.MustInt(ctx.FormValue("uid")) + + onlineInfo := map[string]int{"online": logic.Book.Len()} + message := logic.NewMessage(logic.WsMsgOnline, onlineInfo) + logic.Book.PostMessage(uid, message) + + return ctx.HTML(http.StatusOK, strconv.FormatBool(logic.Book.UserIsOnline(uid))) +} + +func (self MetricsController) calculateUptime() interface{} { + return time.Since(global.App.LaunchTime).String() +} diff --git a/internal/http/controller/admin/node.go b/internal/http/controller/admin/node.go new file mode 100644 index 00000000..54883d76 --- /dev/null +++ b/internal/http/controller/admin/node.go @@ -0,0 +1,98 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type NodeController struct{} + +// 注册路由 +func (self NodeController) RegisterRoute(g *echo.Group) { + g.GET("/community/node/list", self.List) + g.Match([]string{"GET", "POST"}, "/community/node/modify", self.Modify) + g.POST("/community/node/modify_seq", self.ModifySeq) +} + +// List 所有主题节点 +func (NodeController) List(ctx echo.Context) error { + treeNodes := logic.DefaultNode.FindParallelTree(context.EchoContext(ctx)) + + nidMap := make(map[int]int) + keySlice := make([]int, len(treeNodes)) + + for i, node := range treeNodes { + nidMap[node.Nid] = i + 1 + + if node.Parent > 0 { + keySlice[i] = nidMap[node.Parent] + } else { + keySlice[i] = 0 + } + } + + data := map[string]interface{}{ + "nodes": treeNodes, + "key_slice": keySlice, + } + + return render(ctx, "topic/node.html", data) +} + +func (NodeController) Modify(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultNode.Modify(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + global.TopicNodeChan <- struct{}{} + return success(ctx, nil) + } + + treeNodes := logic.DefaultNode.FindParallelTree(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "nodes": treeNodes, + } + + nid := goutils.MustInt(ctx.QueryParam("nid")) + parent := goutils.MustInt(ctx.QueryParam("parent")) + if nid == 0 && parent == 0 { + // 新增 + data["node"] = &model.TopicNode{ + ShowIndex: true, + } + } else if nid > 0 { + data["node"] = logic.DefaultNode.FindOne(nid) + } else if parent > 0 { + data["node"] = &model.TopicNode{ + ShowIndex: true, + } + } + data["parent"] = parent + + return render(ctx, "topic/node_modify.html", data) +} + +func (NodeController) ModifySeq(ctx echo.Context) error { + nid := goutils.MustInt(ctx.FormValue("nid")) + seq := goutils.MustInt(ctx.FormValue("seq")) + err := logic.DefaultNode.ModifySeq(context.EchoContext(ctx), nid, seq) + if err != nil { + return fail(ctx, 1, err.Error()) + } + return success(ctx, nil) + +} diff --git a/internal/http/controller/admin/project.go b/internal/http/controller/admin/project.go new file mode 100644 index 00000000..56960d31 --- /dev/null +++ b/internal/http/controller/admin/project.go @@ -0,0 +1,145 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type ProjectController struct{} + +// 注册路由 +func (self ProjectController) RegisterRoute(g *echo.Group) { + g.GET("/crawl/project/list", self.ProjectList) + g.POST("/crawl/project/query.html", self.ProjectQuery) + g.Match([]string{"GET", "POST"}, "/crawl/project/new", self.CrawlProject) + g.Match([]string{"GET", "POST"}, "/crawl/project/modify", self.Modify) +} + +// ProjectList 所有文章(分页) +func (ProjectController) ProjectList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), nil, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "article/list.html,article/query.html", data) +} + +// ProjectQuery +func (ProjectController) ProjectQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"id", "domain", "title"}) + + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), conds, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "article/query.html", data) +} + +// CrawlProject +func (ProjectController) CrawlProject(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + urls := strings.Split(ctx.FormValue("urls"), "\n") + + var errMsg string + for _, projectUrl := range urls { + err := logic.DefaultProject.ParseOneProject(strings.TrimSpace(projectUrl)) + if err != nil { + errMsg = err.Error() + } + } + + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + return render(ctx, "project/new.html", data) +} + +// Modify +func (self ProjectController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.ProjectList))) + } + + data["article"] = article + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "article/modify.html", data) + +} + +// /crawl/article/del +// func DelArticleHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// id := req.FormValue("id") + +// if _, err := strconv.Atoi(id); err != nil { +// data["ok"] = 0 +// data["error"] = "id不是整型" + +// filter.SetData(req, data) +// return +// } + +// if err := service.DelArticle(id); err != nil { +// data["ok"] = 0 +// data["error"] = "删除失败!" +// } else { +// data["ok"] = 1 +// data["msg"] = "删除成功!" +// } + +// filter.SetData(req, data) +// } diff --git a/internal/http/controller/admin/reading.go b/internal/http/controller/admin/reading.go new file mode 100644 index 00000000..c5b47fd1 --- /dev/null +++ b/internal/http/controller/admin/reading.go @@ -0,0 +1,93 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ReadingController struct{} + +// 注册路由 +func (self ReadingController) RegisterRoute(g *echo.Group) { + g.GET("/reading/list", self.ReadingList) + g.POST("/reading/query.html", self.ReadingQuery) + g.Match([]string{"GET", "POST"}, "/reading/publish", self.Publish) +} + +// ReadingList 所有晨读(分页) +func (ReadingController) ReadingList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + readings, total := logic.DefaultReading.FindReadingByPage(context.EchoContext(ctx), nil, curPage, limit) + if readings == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": readings, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "reading/list.html,reading/query.html", data) +} + +// ReadingQuery +func (ReadingController) ReadingQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"id", "rtype"}) + + readings, total := logic.DefaultReading.FindReadingByPage(context.EchoContext(ctx), conds, curPage, limit) + if readings == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": readings, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "reading/query.html", data) +} + +// Publish +func (ReadingController) Publish(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultReading.SaveReading(context.EchoContext(ctx), forms, user.Username) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + id := goutils.MustInt(ctx.QueryParam("id")) + if id != 0 { + reading := logic.DefaultReading.FindById(context.EchoContext(ctx), id) + if reading != nil { + data["reading"] = reading + } + } + + return render(ctx, "reading/modify.html", data) +} diff --git a/internal/http/controller/admin/routes.go b/internal/http/controller/admin/routes.go new file mode 100644 index 00000000..50a5701a --- /dev/null +++ b/internal/http/controller/admin/routes.go @@ -0,0 +1,24 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import echo "github.com/labstack/echo/v4" + +func RegisterRoutes(g *echo.Group) { + g.GET("", AdminIndex) + new(AuthorityController).RegisterRoute(g) + new(UserController).RegisterRoute(g) + new(TopicController).RegisterRoute(g) + new(NodeController).RegisterRoute(g) + new(ArticleController).RegisterRoute(g) + new(ProjectController).RegisterRoute(g) + new(RuleController).RegisterRoute(g) + new(ReadingController).RegisterRoute(g) + new(ToolController).RegisterRoute(g) + new(SettingController).RegisterRoute(g) + new(MetricsController).RegisterRoute(g) +} diff --git a/internal/http/controller/admin/rule.go b/internal/http/controller/admin/rule.go new file mode 100644 index 00000000..e3adf011 --- /dev/null +++ b/internal/http/controller/admin/rule.go @@ -0,0 +1,119 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type RuleController struct{} + +// 注册路由 +func (self RuleController) RegisterRoute(g *echo.Group) { + g.GET("/crawl/rule/list", self.RuleList) + g.POST("/crawl/rule/query.html", self.Query) + g.Match([]string{"GET", "POST"}, "/crawl/rule/new", self.New) + g.Match([]string{"GET", "POST"}, "/crawl/rule/modify", self.Modify) + g.POST("/crawl/rule/del", self.Del) +} + +// RuleList 所有规则(分页) +func (RuleController) RuleList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + rules, total := logic.DefaultRule.FindBy(context.EchoContext(ctx), nil, curPage, limit) + + if rules == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": rules, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "rule/list.html,rule/query.html", data) +} + +// Query +func (RuleController) Query(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"domain"}) + + rules, total := logic.DefaultRule.FindBy(context.EchoContext(ctx), conds, curPage, limit) + + if rules == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": rules, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + return render(ctx, "rule/query.html", data) +} + +// New 新建规则 +func (RuleController) New(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultRule.Save(context.EchoContext(ctx), forms, user.Username) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + return render(ctx, "rule/new.html", data) +} + +// Modify 编辑规则 +func (self RuleController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultRule.Save(context.EchoContext(ctx), forms, user.Username) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + rule := logic.DefaultRule.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if rule == nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.RuleList))) + } + + data["rule"] = rule + + return render(ctx, "rule/modify.html", data) +} + +func (RuleController) Del(ctx echo.Context) error { + err := logic.DefaultRule.Delete(context.EchoContext(ctx), ctx.FormValue("id")) + if err != nil { + return fail(ctx, 1, "删除失败") + } + return success(ctx, nil) +} diff --git a/internal/http/controller/admin/setting.go b/internal/http/controller/admin/setting.go new file mode 100644 index 00000000..2d9fb89c --- /dev/null +++ b/internal/http/controller/admin/setting.go @@ -0,0 +1,69 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type SettingController struct{} + +// 注册路由 +func (self SettingController) RegisterRoute(g *echo.Group) { + g.Match([]string{"GET", "POST"}, "/setting/genneral/modify", self.GenneralModify) + g.Match([]string{"GET", "POST"}, "/setting/nav/modify", self.NavModify) + g.Match([]string{"GET", "POST"}, "/setting/index_tab/children", self.IndexTabChildren) +} + +// GenneralModify 常规选项修改 +func (self SettingController) GenneralModify(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultSetting.Update(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) + } + + return render(ctx, "setting/genneral.html", nil) +} + +// NavModify 菜单、导航修改 +func (self SettingController) NavModify(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultSetting.Update(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) + } + return render(ctx, "setting/menu_nav.html", nil) +} + +func (self SettingController) IndexTabChildren(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultSetting.UpdateIndexTabChildren(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) + } + + tab := ctx.QueryParam("tab") + name := ctx.QueryParam("name") + + return render(ctx, "setting/index_tab.html", map[string]interface{}{"tab": tab, "name": name}) +} diff --git a/internal/http/controller/admin/tool.go b/internal/http/controller/admin/tool.go new file mode 100644 index 00000000..b7860740 --- /dev/null +++ b/internal/http/controller/admin/tool.go @@ -0,0 +1,26 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type ToolController struct{} + +// 注册路由 +func (self ToolController) RegisterRoute(g *echo.Group) { + g.GET("/tool/sitemap", self.GenSitemap) +} + +// GenSitemap +func (ToolController) GenSitemap(ctx echo.Context) error { + logic.GenSitemap() + return render(ctx, "tool/sitemap.html", nil) +} diff --git a/internal/http/controller/admin/topic.go b/internal/http/controller/admin/topic.go new file mode 100644 index 00000000..82a14140 --- /dev/null +++ b/internal/http/controller/admin/topic.go @@ -0,0 +1,93 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type TopicController struct{} + +// 注册路由 +func (self TopicController) RegisterRoute(g *echo.Group) { + g.GET("/community/topic/list", self.List) + g.POST("/community/topic/query.html", self.Query) + g.Match([]string{"GET", "POST"}, "/community/topic/modify", self.Modify) +} + +// List 所有主题(分页) +func (TopicController) List(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + topics, total := logic.DefaultTopic.FindByPage(context.EchoContext(ctx), nil, curPage, limit) + + if topics == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": topics, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "topic/list.html,topic/query.html", data) +} + +// Query +func (TopicController) Query(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"tid", "title", "uid"}) + + articles, total := logic.DefaultTopic.FindByPage(context.EchoContext(ctx), conds, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "topic/query.html", data) +} + +// Modify +func (self TopicController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.List))) + } + + data["article"] = article + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "topic/modify.html", data) +} diff --git a/internal/http/controller/admin/user.go b/internal/http/controller/admin/user.go new file mode 100644 index 00000000..5901b36d --- /dev/null +++ b/internal/http/controller/admin/user.go @@ -0,0 +1,105 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type UserController struct{} + +// 注册路由 +func (self UserController) RegisterRoute(g *echo.Group) { + g.GET("/user/user/list", self.UserList) + g.POST("/user/user/query.html", self.UserQuery) + g.GET("/user/user/detail", self.Detail) + g.POST("/user/user/modify", self.Modify) + g.POST("/user/user/add_black", self.AddBlack) +} + +// UserList 所有用户(分页) +func (UserController) UserList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + users, total := logic.DefaultUser.FindUserByPage(context.EchoContext(ctx), nil, curPage, limit) + + data := map[string]interface{}{ + "datalist": users, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "user/list.html,user/query.html", data) +} + +func (UserController) UserQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"uid", "username", "email"}) + + users, total := logic.DefaultUser.FindUserByPage(context.EchoContext(ctx), conds, curPage, limit) + + data := map[string]interface{}{ + "datalist": users, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "user/query.html", data) +} + +func (UserController) Detail(ctx echo.Context) error { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "uid", ctx.QueryParam("uid")) + + data := map[string]interface{}{ + "user": user, + } + + return render(ctx, "user/detail.html", data) +} + +func (UserController) Modify(ctx echo.Context) error { + uid := ctx.FormValue("uid") + + amount := goutils.MustInt(ctx.FormValue("amount")) + forms, _ := ctx.FormParams() + if amount > 0 { + logic.DefaultUserRich.Recharge(context.EchoContext(ctx), uid, forms) + } + logic.DefaultUser.AdminUpdateUser(context.EchoContext(ctx), uid, forms) + + return success(ctx, nil) +} + +func (UserController) AddBlack(ctx echo.Context) error { + uid := goutils.MustInt(ctx.FormValue("uid")) + err := logic.DefaultUser.UpdateUserStatus(context.EchoContext(ctx), uid, model.UserStatusOutage) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + // 将用户 IP 加入黑名单 + logic.DefaultRisk.AddBlackIPByUID(uid) + + truncate := goutils.MustBool(ctx.FormValue("truncate")) + if truncate { + err = logic.DefaultUser.DeleteUserContent(context.EchoContext(ctx), uid) + if err != nil { + return fail(ctx, 1, err.Error()) + } + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/app/article.go b/internal/http/controller/app/article.go new file mode 100644 index 00000000..1f22c385 --- /dev/null +++ b/internal/http/controller/app/article.go @@ -0,0 +1,80 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ArticleController struct{} + +// 注册路由 +func (this *ArticleController) RegisterRoute(g *echo.Group) { + g.GET("/articles", this.ReadList) + g.GET("/article/detail", this.Detail) +} + +// ReadList 网友文章列表页 +func (ArticleController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + // 置顶的 article + topArticles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "top=1") + + articles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "") + + total := logic.DefaultArticle.Count(context.EchoContext(ctx), "") + hasMore := paginator.SetTotal(total).HasMorePage() + + data := map[string]interface{}{ + "articles": append(topArticles, articles...), + "has_more": hasMore, + } + + return success(ctx, data) +} + +// Detail 文章详细页 +func (ArticleController) Detail(ctx echo.Context) error { + article, prevNext, err := logic.DefaultArticle.FindByIdAndPreNext(context.EchoContext(ctx), goutils.MustInt(ctx.QueryParam("id"))) + if err != nil { + return fail(ctx, err.Error()) + } + + if article == nil || article.Id == 0 || article.Status == model.ArticleStatusOffline { + return success(ctx, map[string]interface{}{"article": map[string]interface{}{"id": 0}}) + } + + logic.Views.Incr(Request(ctx), model.TypeArticle, article.Id) + + // 为了阅读数即时看到 + article.Viewnum++ + + // 回复信息(评论) + replies, _, lastReplyUser := logic.DefaultComment.FindObjComments(context.EchoContext(ctx), article.Id, model.TypeArticle, 0, article.Lastreplyuid) + // 有人回复 + if article.Lastreplyuid != 0 { + article.LastReplyUser = lastReplyUser + } + + article.Txt = "" + data := map[string]interface{}{ + "article": article, + "replies": replies, + } + + // TODO: 暂时不用 + _ = prevNext + return success(ctx, data) +} diff --git a/internal/http/controller/app/base.go b/internal/http/controller/app/base.go new file mode 100644 index 00000000..27720f3d --- /dev/null +++ b/internal/http/controller/app/base.go @@ -0,0 +1,76 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "encoding/json" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +const perPage = 12 + +func getLogger(ctx echo.Context) *logger.Logger { + return logic.GetLogger(context.EchoContext(ctx)) +} + +func success(ctx echo.Context, data interface{}) error { + result := map[string]interface{}{ + "code": 0, + "msg": "ok", + "data": data, + } + + b, err := json.Marshal(result) + if err != nil { + return err + } + + go func(b []byte) { + if cacheKey := ctx.Get(nosql.CacheKey); cacheKey != nil { + nosql.DefaultLRUCache.CompressAndAdd(cacheKey, b, nosql.NewCacheData()) + } + }(b) + + AccessControl(ctx) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + return ctx.JSONBlob(http.StatusOK, b) +} + +func fail(ctx echo.Context, msg string, codes ...int) error { + AccessControl(ctx) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + code := 1 + if len(codes) > 0 { + code = codes[0] + } + result := map[string]interface{}{ + "code": code, + "msg": msg, + } + + getLogger(ctx).Errorln("operate fail:", result) + + return ctx.JSON(http.StatusOK, result) +} diff --git a/internal/http/controller/app/comment.go b/internal/http/controller/app/comment.go new file mode 100644 index 00000000..2067d29e --- /dev/null +++ b/internal/http/controller/app/comment.go @@ -0,0 +1,41 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type CommentController struct{} + +func (self CommentController) RegisterRoute(g *echo.Group) { + g.POST("/comment/:objid", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.PublishNotice()) +} + +// Create 评论(或回复) +func (CommentController) Create(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + + // 入库 + objid := goutils.MustInt(ctx.Param("objid")) + if objid == 0 { + return fail(ctx, "参数有误,请刷新后重试!", 1) + } + forms, _ := ctx.FormParams() + comment, err := logic.DefaultComment.Publish(context.EchoContext(ctx), user.Uid, objid, forms) + if err != nil { + return fail(ctx, "服务器内部错误", 2) + } + + return success(ctx, map[string]interface{}{"comment": comment}) +} diff --git a/internal/http/controller/app/doc.go b/internal/http/controller/app/doc.go new file mode 100644 index 00000000..7c2cc682 --- /dev/null +++ b/internal/http/controller/app/doc.go @@ -0,0 +1,8 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +// app Go 语言中文网 APP 接口 +package app diff --git a/internal/http/controller/app/index.go b/internal/http/controller/app/index.go new file mode 100644 index 00000000..96e5f541 --- /dev/null +++ b/internal/http/controller/app/index.go @@ -0,0 +1,77 @@ +// Copyright 2018 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type IndexController struct{} + +// 注册路由 +func (self IndexController) RegisterRoute(g *echo.Group) { + g.GET("/home", self.Home) + g.GET("/stat/site", self.WebsiteStat) +} + +// Home 首页 +func (IndexController) Home(ctx echo.Context) error { + if len(logic.WebsiteSetting.IndexNavs) == 0 { + return success(ctx, nil) + } + + tab := ctx.QueryParam("tab") + if tab == "" { + tab = GetFromCookie(ctx, "INDEX_TAB") + } + + if tab == "" { + tab = logic.WebsiteSetting.IndexNavs[0].Tab + } + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + data := logic.DefaultIndex.FindData(context.EchoContext(ctx), tab, paginator) + + SetCookie(ctx, "INDEX_TAB", data["tab"].(string)) + + data["all_nodes"] = logic.GenNodes() + + if tab == "all" { + data["total"] = paginator.GetTotal() + + } + return success(ctx, data) +} + +// WebsiteStat 网站统计信息 +func (IndexController) WebsiteStat(ctx echo.Context) error { + articleTotal := logic.DefaultArticle.Total() + projectTotal := logic.DefaultProject.Total() + topicTotal := logic.DefaultTopic.Total() + cmtTotal := logic.DefaultComment.Total() + resourceTotal := logic.DefaultResource.Total() + bookTotal := logic.DefaultGoBook.Total() + userTotal := logic.DefaultUser.Total() + + data := map[string]interface{}{ + "article": articleTotal, + "project": projectTotal, + "topic": topicTotal, + "resource": resourceTotal, + "book": bookTotal, + "comment": cmtTotal, + "user": userTotal, + } + + return success(ctx, data) +} diff --git a/internal/http/controller/app/project.go b/internal/http/controller/app/project.go new file mode 100644 index 00000000..c3cf4849 --- /dev/null +++ b/internal/http/controller/app/project.go @@ -0,0 +1,69 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ProjectController struct{} + +// 注册路由 +func (self ProjectController) RegisterRoute(g *echo.Group) { + g.GET("/projects", self.ReadList) + g.GET("/project/detail", self.Detail) +} + +// ReadList 开源项目列表页 +func (ProjectController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + projects := logic.DefaultProject.FindAll(context.EchoContext(ctx), paginator, "id DESC", "") + + total := logic.DefaultProject.Count(context.EchoContext(ctx), "") + hasMore := paginator.SetTotal(total).HasMorePage() + + data := map[string]interface{}{ + "projects": projects, + "has_more": hasMore, + } + + return success(ctx, data) +} + +// Detail 项目详情 +func (ProjectController) Detail(ctx echo.Context) error { + id := goutils.MustInt(ctx.QueryParam("id")) + project := logic.DefaultProject.FindOne(context.EchoContext(ctx), id) + if project == nil || project.Id == 0 { + return fail(ctx, "获取失败或已下线") + } + + logic.Views.Incr(Request(ctx), model.TypeProject, project.Id) + + // 为了阅读数即时看到 + project.Viewnum++ + + // 回复信息(评论) + replies, _, lastReplyUser := logic.DefaultComment.FindObjComments(context.EchoContext(ctx), project.Id, model.TypeProject, 0, project.Lastreplyuid) + // 有人回复 + if project.Lastreplyuid != 0 { + project.LastReplyUser = lastReplyUser + } + + return success(ctx, map[string]interface{}{ + "project": project, + "replies": replies, + }) +} diff --git a/internal/http/controller/app/resource.go b/internal/http/controller/app/resource.go new file mode 100644 index 00000000..4861f7fd --- /dev/null +++ b/internal/http/controller/app/resource.go @@ -0,0 +1,54 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ResourceController struct{} + +// 注册路由 +func (self ResourceController) RegisterRoute(g *echo.Group) { + g.GET("/resources", self.ReadList) + g.GET("/resource/detail", self.Detail) +} + +// ReadList 资源索引页 +func (ResourceController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + resources, total := logic.DefaultResource.FindAll(context.EchoContext(ctx), paginator, "resource.mtime", "") + hasMore := paginator.SetTotal(total).HasMorePage() + + data := map[string]interface{}{ + "resources": resources, + "has_more": hasMore, + } + + return success(ctx, data) +} + +// Detail 某个资源详细页 +func (ResourceController) Detail(ctx echo.Context) error { + id := goutils.MustInt(ctx.QueryParam("id")) + resource, comments := logic.DefaultResource.FindById(context.EchoContext(ctx), id) + if len(resource) == 0 { + return fail(ctx, "获取失败") + } + + logic.Views.Incr(Request(ctx), model.TypeResource, id) + + return success(ctx, map[string]interface{}{"resource": resource, "comments": comments}) +} diff --git a/internal/http/controller/app/routes.go b/internal/http/controller/app/routes.go new file mode 100644 index 00000000..de79ead5 --- /dev/null +++ b/internal/http/controller/app/routes.go @@ -0,0 +1,20 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import echo "github.com/labstack/echo/v4" + +func RegisterRoutes(g *echo.Group) { + new(IndexController).RegisterRoute(g) + new(ArticleController).RegisterRoute(g) + new(TopicController).RegisterRoute(g) + new(ResourceController).RegisterRoute(g) + new(ProjectController).RegisterRoute(g) + new(UserController).RegisterRoute(g) + new(WechatController).RegisterRoute(g) + new(CommentController).RegisterRoute(g) +} diff --git a/internal/http/controller/app/topic.go b/internal/http/controller/app/topic.go new file mode 100644 index 00000000..afef2bd6 --- /dev/null +++ b/internal/http/controller/app/topic.go @@ -0,0 +1,185 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type TopicController struct{} + +// 注册路由 +func (self TopicController) RegisterRoute(g *echo.Group) { + g.GET("/topics", self.TopicList) + g.GET("/topics/no_reply", self.TopicsNoReply) + g.GET("/topics/last", self.TopicsLast) + g.GET("/topic/detail", self.Detail) + g.GET("/topics/node/:nid", self.NodeTopics) + + g.Match([]string{"GET", "POST"}, "/topics/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.PublishNotice()) + g.Match([]string{"GET", "POST"}, "/topics/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +func (self TopicController) TopicList(ctx echo.Context) error { + tab := ctx.QueryParam("tab") + if tab != "" && tab != "all" { + nid := logic.GetNidByEname(tab) + if nid > 0 { + return self.topicList(ctx, tab, "topics.mtime DESC", "nid=? AND top!=1", nid) + } + } + + return self.topicList(ctx, "all", "topics.mtime DESC", "top!=1") +} + +func (self TopicController) Topics(ctx echo.Context) error { + return self.topicList(ctx, "", "topics.mtime DESC", "") +} + +func (self TopicController) TopicsNoReply(ctx echo.Context) error { + return self.topicList(ctx, "no_reply", "topics.mtime DESC", "lastreplyuid=?", 0) +} + +func (self TopicController) TopicsLast(ctx echo.Context) error { + return self.topicList(ctx, "last", "ctime DESC", "") +} + +func (TopicController) topicList(ctx echo.Context, tab, orderBy, querystring string, args ...interface{}) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + // 置顶的topic + topTopics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "ctime DESC", "top=1") + + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, orderBy, querystring, args...) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, args...) + hasMore := paginator.SetTotal(total).HasMorePage() + + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "topics": append(topTopics, topics...), + "tab": tab, + "tab_list": hotNodes, + "has_more": hasMore, + } + + return success(ctx, data) +} + +// NodeTopics 某节点下的主题列表 +func (TopicController) NodeTopics(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring, nid := "nid=?", goutils.MustInt(ctx.Param("nid")) + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.mtime DESC", querystring, nid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, nid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + // 当前节点信息 + node := logic.GetNode(nid) + + return success(ctx, map[string]interface{}{"activeTopics": "active", "topics": topics, "page": template.HTML(pageHtml), "total": total, "node": node}) +} + +// Detail 社区主题详细页 +func (TopicController) Detail(ctx echo.Context) error { + tid := goutils.MustInt(ctx.QueryParam("tid")) + if tid == 0 { + return fail(ctx, "tid 非法") + } + + topic, replies, err := logic.DefaultTopic.FindByTid(context.EchoContext(ctx), tid) + if err != nil { + return fail(ctx, "服务器异常") + } + + me, ok := ctx.Get("user").(*model.Me) + + permission := topic["permission"].(int) + switch permission { + case model.PermissionLogin: + if !ok { + topic["content"] = "登录用户可见!" + } + case model.PermissionPay: + if !ok || !me.IsVip || !me.IsRoot { + topic["content"] = "付费用户可见!" + } + } + + logic.Views.Incr(Request(ctx), model.TypeTopic, tid) + + data := map[string]interface{}{ + "topic": topic, + "replies": replies, + } + + return success(ctx, data) +} + +// Create 新建主题 +func (TopicController) Create(ctx echo.Context) error { + nodes := logic.GenNodes() + + title := ctx.FormValue("title") + // 请求新建主题页面 + if title == "" || ctx.Request().Method != "POST" { + return success(ctx, map[string]interface{}{"nodes": nodes, "activeTopics": "active"}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + tid, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, "内部服务错误", 1) + } + + return success(ctx, map[string]interface{}{"tid": tid}) +} + +// Modify 修改主题 +func (TopicController) Modify(ctx echo.Context) error { + tid := goutils.MustInt(ctx.FormValue("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + nodes := logic.GenNodes() + + if ctx.Request().Method != "POST" { + topics := logic.DefaultTopic.FindByTids([]int{tid}) + if len(topics) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + return success(ctx, map[string]interface{}{"nodes": nodes, "topic": topics[0], "activeTopics": "active"}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + _, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return fail(ctx, "没有权限操作", 1) + } + + return fail(ctx, "服务错误,请稍后重试!", 2) + } + return success(ctx, map[string]interface{}{"tid": tid}) +} diff --git a/internal/http/controller/app/user.go b/internal/http/controller/app/user.go new file mode 100644 index 00000000..487c285b --- /dev/null +++ b/internal/http/controller/app/user.go @@ -0,0 +1,100 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + . "github.com/studygolang/studygolang/internal/http/internal/helper" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type UserController struct{} + +// 注册路由 +func (self UserController) RegisterRoute(g *echo.Group) { + g.GET("/user/center", self.Center) + g.GET("/user/me", self.Me) + g.POST("/user/modify", self.Modify) + g.POST("/user/login", self.Login) +} + +// Center 用户自己个人中心 +func (UserController) Center(ctx echo.Context) error { + if user, ok := ctx.Get("user").(*model.Me); ok { + data := map[string]interface{}{ + "user": user, + } + return success(ctx, data) + } + + return success(ctx, nil) +} + +// Me 用户信息 +func (UserController) Me(ctx echo.Context) error { + if me, ok := ctx.Get("user").(*model.Me); ok { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "uid", me.Uid) + return success(ctx, map[string]interface{}{ + "user": user, + "default_avatars": logic.DefaultAvatars, + }) + } + + return success(ctx, nil) +} + +func (UserController) Login(ctx echo.Context) error { + if _, ok := ctx.Get("user").(*model.Me); ok { + return success(ctx, nil) + } + + username := ctx.FormValue("username") + if username == "" { + return fail(ctx, "用户名为空") + } + + // 处理用户登录 + passwd := ctx.FormValue("passwd") + userLogin, err := logic.DefaultUser.Login(context.EchoContext(ctx), username, passwd) + if err != nil { + return fail(ctx, err.Error()) + } + + data := map[string]interface{}{ + "token": GenToken(userLogin.Uid), + "uid": userLogin.Uid, + "username": userLogin.Username, + } + return success(ctx, data) +} + +func (UserController) Modify(ctx echo.Context) error { + me, ok := ctx.Get("user").(*model.Me) + if !ok { + return fail(ctx, "请先登录", NeedReLoginCode) + } + + forms, _ := ctx.FormParams() + + // 更新信息 + errMsg, err := logic.DefaultUser.Update(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, errMsg) + } + + email := ctx.FormValue("email") + if me.Email != email { + isHttps := CheckIsHttps(ctx) + go logic.DefaultEmail.SendActivateMail(email, RegActivateCode.GenUUID(email), isHttps) + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/app/wechat.go b/internal/http/controller/app/wechat.go new file mode 100644 index 00000000..cd8b5920 --- /dev/null +++ b/internal/http/controller/app/wechat.go @@ -0,0 +1,120 @@ +// Copyright 2018 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "net/url" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type WechatController struct{} + +// RegisterRoute 注册路由 +func (self WechatController) RegisterRoute(g *echo.Group) { + g.GET("/wechat/check_session", self.CheckSession) + g.POST("/wechat/register", self.Register) + g.POST("/wechat/login", self.Login) +} + +// CheckSession 校验小程序 session +func (WechatController) CheckSession(ctx echo.Context) error { + code := ctx.QueryParam("code") + + wechatUser, err := logic.DefaultWechat.CheckSession(context.EchoContext(ctx), code) + if err != nil { + return fail(ctx, err.Error()) + } + + if wechatUser.Uid > 0 { + data := map[string]interface{}{ + "token": GenToken(wechatUser.Uid), + "uid": wechatUser.Uid, + "nickname": wechatUser.Nickname, + "avatar": wechatUser.Avatar, + } + + return success(ctx, data) + } + + data := map[string]interface{}{ + "unbind_token": GenToken(wechatUser.Id), + } + + return success(ctx, data) +} + +// Login 通过系统用户登录 +func (WechatController) Login(ctx echo.Context) error { + unbindToken := ctx.FormValue("unbind_token") + id, ok := ParseToken(unbindToken) + if !ok { + return fail(ctx, "无效请求!") + } + + username := ctx.FormValue("username") + if username == "" { + return fail(ctx, "用户名为空") + } + + // 处理用户登录 + passwd := ctx.FormValue("passwd") + userLogin, err := logic.DefaultUser.Login(context.EchoContext(ctx), username, passwd) + if err != nil { + return fail(ctx, err.Error()) + } + + userInfo := ctx.FormValue("userInfo") + + wechatUser, err := logic.DefaultWechat.Bind(context.EchoContext(ctx), id, userLogin.Uid, userInfo) + if err != nil { + return fail(ctx, err.Error()) + } + + data := map[string]interface{}{ + "token": GenToken(wechatUser.Uid), + "uid": wechatUser.Uid, + "nickname": wechatUser.Nickname, + "avatar": wechatUser.Avatar, + } + + return success(ctx, data) +} + +// Register 注册系统账号 +func (WechatController) Register(ctx echo.Context) error { + unbindToken := ctx.FormValue("unbind_token") + id, ok := ParseToken(unbindToken) + if !ok { + return fail(ctx, "无效请求!") + } + + passwd := ctx.FormValue("passwd") + pass2 := ctx.FormValue("pass2") + if passwd != pass2 { + return fail(ctx, "确认密码不一致", 1) + } + + fields := []string{"username", "email", "passwd", "userInfo"} + form := url.Values{} + for _, field := range fields { + form.Set(field, ctx.FormValue(field)) + } + form.Set("id", strconv.Itoa(id)) + + errMsg, err := logic.DefaultUser.CreateUser(context.EchoContext(ctx), form) + if err != nil { + return fail(ctx, errMsg, 2) + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/article.go b/internal/http/controller/article.go new file mode 100644 index 00000000..1558163f --- /dev/null +++ b/internal/http/controller/article.go @@ -0,0 +1,228 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + "strings" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/echoutils" + + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeArticle, logic.ArticleComment{}) + logic.RegisterLikeObject(model.TypeArticle, logic.ArticleLike{}) +} + +type ArticleController struct{} + +// 注册路由 +func (self ArticleController) RegisterRoute(g *echo.Group) { + g.GET("/articles", self.ReadList) + g.GET("/articles/crawl", self.Crawl) + + g.GET("/articles/:id", self.Detail) + + g.Match([]string{"GET", "POST"}, "/articles/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/articles/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +// ReadList 网友文章列表页 +func (ArticleController) ReadList(ctx echo.Context) error { + limit := 20 + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + paginator.SetPerPage(limit) + total := logic.DefaultArticle.Count(context.EchoContext(ctx), "") + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + pageInfo := template.HTML(pageHtml) + + // TODO: 参考的 topics 的处理方式,但是感觉不应该这样做 + topArticles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "top=1") + unTopArticles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "top!=1") + articles := append(topArticles, unTopArticles...) + if articles == nil { + logger.Errorln("article controller: find article error") + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + + num := len(articles) + if num == 0 { + return render(ctx, "articles/list.html", map[string]interface{}{"articles": articles, "activeArticles": "active"}) + } + + // 获取当前用户喜欢对象信息 + me, ok := ctx.Get("user").(*model.Me) + var topLikeFlags map[int]int + var unTopLikeFlags map[int]int + likeFlags := map[int]int{} + + if ok { + topArticlesNum := len(topArticles) + if topArticlesNum > 0 { + topLikeFlags, _ = logic.DefaultLike.FindUserLikeObjects(context.EchoContext(ctx), me.Uid, model.TypeArticle, topArticles[0].Id, topArticles[topArticlesNum-1].Id) + for k, v := range topLikeFlags { + likeFlags[k] = v + } + } + + unTopArticlesNum := len(unTopArticles) + if unTopArticlesNum > 0 { + unTopLikeFlags, _ = logic.DefaultLike.FindUserLikeObjects(context.EchoContext(ctx), me.Uid, model.TypeArticle, unTopArticles[0].Id, unTopArticles[unTopArticlesNum-1].Id) + for k, v := range unTopLikeFlags { + likeFlags[k] = v + } + } + } + + return render(ctx, "articles/list.html", map[string]interface{}{"articles": articles, "activeArticles": "active", "page": pageInfo, "likeflags": likeFlags}) +} + +// Detail 文章详细页 +func (ArticleController) Detail(ctx echo.Context) error { + article, prevNext, err := logic.DefaultArticle.FindByIdAndPreNext(context.EchoContext(ctx), goutils.MustInt(ctx.Param("id"))) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + + if article == nil || article.Id == 0 || article.Status == model.ArticleStatusOffline { + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + + articleGCTT := logic.DefaultArticle.FindArticleGCTT(context.EchoContext(ctx), article) + data := map[string]interface{}{ + "activeArticles": "active", + "article": article, + "article_gctt": articleGCTT, + "prev": prevNext[0], + "next": prevNext[1], + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, article.Id, model.TypeArticle) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, article.Id, model.TypeArticle) + + logic.Views.Incr(Request(ctx), model.TypeArticle, article.Id, me.Uid) + + if !article.IsSelf || me.Uid != article.User.Uid { + go logic.DefaultViewRecord.Record(article.Id, model.TypeArticle, me.Uid) + } + + if me.IsRoot || (article.IsSelf && me.Uid == article.User.Uid) { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), article.Id, model.TypeArticle) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), article.Id, model.TypeArticle) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeArticle, article.Id) + } + + // 为了阅读数即时看到 + article.Viewnum++ + + data["subjects"] = logic.DefaultSubject.FindArticleSubjects(context.EchoContext(ctx), article.Id) + + return render(ctx, "articles/detail.html,common/comment.html", data) +} + +// Create 发布新文章 +func (ArticleController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + if title == "" || ctx.Request().Method != "POST" { + data := map[string]interface{}{"activeArticles": "active"} + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + return render(ctx, "articles/new.html", data) + } + + if ctx.FormValue("content") == "" { + return fail(ctx, 1, "内容不能为空") + } + + forms, _ := ctx.FormParams() + id, err := logic.DefaultArticle.Publish(echoutils.WrapEchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 2, "内部服务错误") + } + + return success(ctx, map[string]interface{}{"id": id}) +} + +// Modify 修改文章 +func (ArticleController) Modify(ctx echo.Context) error { + id := ctx.FormValue("id") + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), id) + + if ctx.Request().Method != "POST" { + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles/"+id) + } + + return render(ctx, "articles/new.html", map[string]interface{}{ + "article": article, + "activeArticles": "active", + }) + } + + if id == "" || ctx.FormValue("content") == "" { + return fail(ctx, 1, "内容不能为空") + } + + if err != nil { + return fail(ctx, 2, "文章不存在") + } + + me := ctx.Get("user").(*model.Me) + if !logic.CanEdit(me, article) { + return fail(ctx, 3, "没有修改权限") + } + + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(echoutils.WrapEchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 4, errMsg) + } + + return success(ctx, map[string]interface{}{"id": article.Id}) +} + +func (ArticleController) Crawl(ctx echo.Context) error { + strUrl := ctx.QueryParam("url") + + var ( + errMsg string + err error + ) + strUrl = strings.TrimSpace(strUrl) + _, err = logic.DefaultArticle.ParseArticle(context.EchoContext(ctx), strUrl, false) + if err != nil { + errMsg = err.Error() + } + + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) +} diff --git a/internal/http/controller/balance.go b/internal/http/controller/balance.go new file mode 100644 index 00000000..dbc8dab1 --- /dev/null +++ b/internal/http/controller/balance.go @@ -0,0 +1,53 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/polaris1119/goutils" + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type UserRichController struct{} + +// 注册路由 +func (self UserRichController) RegisterRoute(g *echo.Group) { + g.GET("/balance", self.MyBalance, middleware.NeedLogin()) + g.GET("/balance/add", self.Add, middleware.NeedLogin()) +} + +func (UserRichController) MyBalance(ctx echo.Context) error { + p := goutils.MustInt(ctx.QueryParam("p"), 1) + me := ctx.Get("user").(*model.Me) + balanceDetails := logic.DefaultUserRich.FindBalanceDetail(context.EchoContext(ctx), me, p) + total := logic.DefaultUserRich.Total(context.EchoContext(ctx), me.Uid) + + data := map[string]interface{}{ + "details": balanceDetails, + "total": int(total), + "cur_p": p, + } + return render(ctx, "rich/balance.html", data) +} + +func (UserRichController) Add(ctx echo.Context) error { + p := goutils.MustInt(ctx.QueryParam("p"), 1) + me := ctx.Get("user").(*model.Me) + balanceDetails := logic.DefaultUserRich.FindBalanceDetail(context.EchoContext(ctx), me, p, model.MissionTypeAdd) + + rechargeAmount := logic.DefaultUserRich.FindRecharge(context.EchoContext(ctx), me) + + data := map[string]interface{}{ + "details": balanceDetails, + "recharge_amount": rechargeAmount, + } + return render(ctx, "rich/add.html", data) +} diff --git a/internal/http/controller/base.go b/internal/http/controller/base.go new file mode 100644 index 00000000..34a264c8 --- /dev/null +++ b/internal/http/controller/base.go @@ -0,0 +1,89 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +func getLogger(ctx echo.Context) *logger.Logger { + return logic.GetLogger(context.EchoContext(ctx)) +} + +// render html 输出 +func render(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + return Render(ctx, contentTpl, data) +} + +func success(ctx echo.Context, data interface{}) error { + result := map[string]interface{}{ + "ok": 1, + "msg": "操作成功", + "data": data, + } + + b, err := json.Marshal(result) + if err != nil { + return err + } + + oldETag := ctx.Request().Header.Get("If-None-Match") + if strings.HasPrefix(oldETag, "W/") { + oldETag = oldETag[2:] + } + newETag := goutils.Md5Buf(b) + if oldETag == newETag { + return ctx.NoContent(http.StatusNotModified) + } + + go func(b []byte) { + if cacheKey := ctx.Get(nosql.CacheKey); cacheKey != nil { + nosql.DefaultLRUCache.CompressAndAdd(cacheKey, b, nosql.NewCacheData()) + } + }(b) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + ctx.Response().Header().Add("ETag", newETag) + + callback := ctx.QueryParam("callback") + if callback != "" { + return ctx.JSONPBlob(http.StatusOK, callback, b) + } + + return ctx.JSONBlob(http.StatusOK, b) +} + +func fail(ctx echo.Context, code int, msg string) error { + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + result := map[string]interface{}{ + "ok": 0, + "error": msg, + } + + getLogger(ctx).Errorln("operate fail:", result) + + return ctx.JSON(http.StatusOK, result) +} diff --git a/internal/http/controller/book.go b/internal/http/controller/book.go new file mode 100644 index 00000000..5cfd36d8 --- /dev/null +++ b/internal/http/controller/book.go @@ -0,0 +1,116 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeBook, logic.BookComment{}) + logic.RegisterLikeObject(model.TypeBook, logic.BookLike{}) +} + +type BookController struct{} + +// 注册路由 +func (self BookController) RegisterRoute(g *echo.Group) { + g.GET("/books", self.ReadList) + + g.GET("/book/:id", self.Detail) + + g.Match([]string{"GET", "POST"}, "/book/new", self.Create, middleware.NeedLogin(), middleware.BalanceCheck(), middleware.PublishNotice()) +} + +// ReadList 图书列表页 +func (BookController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + books := logic.DefaultGoBook.FindAll(context.EchoContext(ctx), paginator, "likenum DESC,id DESC") + total := logic.DefaultGoBook.Count(context.EchoContext(ctx)) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + data := map[string]interface{}{ + "books": books, + "activeBooks": "active", + "page": template.HTML(pageHtml), + } + + return render(ctx, "books/list.html", data) +} + +// Create 发布新书 +func (BookController) Create(ctx echo.Context) error { + name := ctx.FormValue("name") + // 请求新建图书页面 + if name == "" || ctx.Request().Method != "POST" { + book := &model.Book{} + return render(ctx, "books/new.html", map[string]interface{}{"book": book, "activeBooks": "active"}) + } + + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultGoBook.Publish(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, nil) +} + +// Detail 图书详细页 +func (BookController) Detail(ctx echo.Context) error { + book, err := logic.DefaultGoBook.FindById(context.EchoContext(ctx), ctx.Param("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/books") + } + + if book == nil || book.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/books") + } + + data := map[string]interface{}{ + "activeBooks": "active", + "book": book, + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, book.Id, model.TypeBook) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, book.Id, model.TypeBook) + + logic.Views.Incr(Request(ctx), model.TypeBook, book.Id, me.Uid) + + if me.Uid != book.Uid { + go logic.DefaultViewRecord.Record(book.Id, model.TypeBook, me.Uid) + } + + if me.IsRoot || me.Uid == book.Uid { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), book.Id, model.TypeBook) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), book.Id, model.TypeBook) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeBook, book.Id) + } + + // 为了阅读数即时看到 + book.Viewnum++ + + return render(ctx, "books/detail.html,common/comment.html", data) +} diff --git a/internal/http/controller/captcha.go b/internal/http/controller/captcha.go new file mode 100644 index 00000000..119cec78 --- /dev/null +++ b/internal/http/controller/captcha.go @@ -0,0 +1,28 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + . "github.com/studygolang/studygolang/internal/http" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" +) + +var captchaHandler = captcha.Server(100, 40) + +// 验证码 +type CaptchaController struct{} + +func (self CaptchaController) RegisterRoute(g *echo.Group) { + g.GET("/captcha/*", self.Server) +} + +func (CaptchaController) Server(ctx echo.Context) error { + captchaHandler.ServeHTTP(ResponseWriter(ctx), Request(ctx)) + return nil +} diff --git a/internal/http/controller/comment.go b/internal/http/controller/comment.go new file mode 100644 index 00000000..a0d40168 --- /dev/null +++ b/internal/http/controller/comment.go @@ -0,0 +1,204 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "errors" + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" + "github.com/studygolang/studygolang/echoutils" +) + +// 在需要喜欢且要回调的地方注册喜欢对象 +func init() { + // 注册喜欢对象 + logic.RegisterLikeObject(model.TypeComment, logic.CommentLike{}) +} + +type CommentController struct{} + +func (self CommentController) RegisterRoute(g *echo.Group) { + g.GET("/at/users", self.AtUsers) + g.POST("/comment/:objid", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice()) + g.GET("/object/comments", self.CommentList) + g.POST("/object/comments/:cid", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + + g.GET("/topics/:objid/comment/:cid", self.TopicDetail) + g.GET("/articles/:objid/comment/:cid", self.ArticleDetail) +} + +// AtUsers 评论或回复 @ 某人 suggest +func (CommentController) AtUsers(ctx echo.Context) error { + term := ctx.QueryParam("term") + isHttps := CheckIsHttps(ctx) + users := logic.DefaultUser.GetUserMentions(term, 10, isHttps) + return ctx.JSON(http.StatusOK, users) +} + +// Create 评论(或回复) +func (CommentController) Create(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + + // 入库 + objid := goutils.MustInt(ctx.Param("objid")) + if objid == 0 { + return fail(ctx, 1, "参数有误,请刷新后重试!") + } + forms, _ := ctx.FormParams() + comment, err := logic.DefaultComment.Publish(context.EchoContext(ctx), user.Uid, objid, forms) + if err != nil { + return fail(ctx, 2, "服务器内部错误") + } + + return success(ctx, comment) +} + +// 修改评论 +func (CommentController) Modify(ctx echo.Context) error { + cid := goutils.MustInt(ctx.Param("cid")) + content := ctx.FormValue("content") + comment, err := logic.DefaultComment.FindById(cid) + + if err != nil { + return fail(ctx, 2, "评论不存在") + } + + if content == "" { + return fail(ctx, 1, "内容不能为空") + } + + me := ctx.Get("user").(*model.Me) + // CanEdit 已包含修改时间限制 + if !logic.CanEdit(me, comment) { + return fail(ctx, 3, "没有修改权限") + } + + errMsg, err := logic.DefaultComment.Modify(echoutils.WrapEchoContext(ctx), cid, content) + if err != nil { + return fail(ctx, 4, errMsg) + } + + return success(ctx, map[string]interface{}{"cid": cid}) +} + +// CommentList 获取某对象的评论信息 +func (CommentController) CommentList(ctx echo.Context) error { + objid := goutils.MustInt(ctx.QueryParam("objid")) + objtype := goutils.MustInt(ctx.QueryParam("objtype")) + p := goutils.MustInt(ctx.QueryParam("p")) + + commentList, replyComments, pageNum, err := logic.DefaultComment.FindObjectComments(context.EchoContext(ctx), objid, objtype, p) + if err != nil { + return fail(ctx, 1, "服务器内部错误") + } + + uids := slices.StructsIntSlice(commentList, "Uid") + if len(replyComments) > 0 { + replyUids := slices.StructsIntSlice(replyComments, "Uid") + uids = append(uids, replyUids...) + } + users := logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + + result := map[string]interface{}{ + "comments": commentList, + "page_num": pageNum, + } + + // json encode 不支持 map[int]... + for uid, user := range users { + result[strconv.Itoa(uid)] = user + } + + replyMap := make(map[string]interface{}) + for _, comment := range replyComments { + replyMap[strconv.Itoa(comment.Floor)] = comment + } + result["reply_comments"] = replyMap + + return success(ctx, result) +} + +func (self CommentController) TopicDetail(ctx echo.Context) error { + objid := goutils.MustInt(ctx.Param("objid")) + cid := goutils.MustInt(ctx.Param("cid")) + + topicMaps := logic.DefaultTopic.FindFullinfoByTids([]int{objid}) + if len(topicMaps) < 1 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + topic := topicMaps[0] + topic["node"] = logic.GetNode(topic["nid"].(int)) + + data := map[string]interface{}{ + "topic": topic, + } + data["appends"] = logic.DefaultTopic.FindAppend(context.EchoContext(ctx), objid) + + err := self.fillCommentAndUser(ctx, data, cid, objid, model.TypeTopic) + + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/topics/"+strconv.Itoa(objid)) + } + + return render(ctx, "topics/comment.html", data) +} + +func (self CommentController) ArticleDetail(ctx echo.Context) error { + objid := goutils.MustInt(ctx.Param("objid")) + cid := goutils.MustInt(ctx.Param("cid")) + + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), objid) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + articleGCTT := logic.DefaultArticle.FindArticleGCTT(context.EchoContext(ctx), article) + + data := map[string]interface{}{ + "article": article, + "article_gctt": articleGCTT, + } + + err = self.fillCommentAndUser(ctx, data, cid, objid, model.TypeArticle) + + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles/"+strconv.Itoa(objid)) + } + + return render(ctx, "articles/comment.html", data) +} + +func (CommentController) fillCommentAndUser(ctx echo.Context, data map[string]interface{}, cid, objid, objtype int) error { + comment, comments := logic.DefaultComment.FindComment(context.EchoContext(ctx), cid, objid, objtype) + + if comment.Cid == 0 { + return errors.New("comment not exists!") + } + + uids := make([]int, 1+len(comments)) + uids[0] = comment.Uid + for i, comment := range comments { + uids[i+1] = comment.Uid + } + users := logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + + data["comment"] = comment + data["comments"] = comments + data["users"] = users + + return nil +} diff --git a/internal/http/controller/download.go b/internal/http/controller/download.go new file mode 100644 index 00000000..ddab8e44 --- /dev/null +++ b/internal/http/controller/download.go @@ -0,0 +1,149 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" +) + +const GoStoragePrefix = "https://golang.google.cn/dl/" + +type DownloadController struct{} + +// 注册路由 +func (self DownloadController) RegisterRoute(g *echo.Group) { + g.GET("/dl", self.GoDl) + g.Match([]string{"GET", "HEAD"}, "/dl/golang/:filename", self.FetchGoInstallPackage) + g.GET("/dl/add_new_version", self.AddNewDownload) +} + +// GoDl Go 语言安装包下载 +func (DownloadController) GoDl(ctx echo.Context) error { + downloads := logic.DefaultDownload.FindAll(context.EchoContext(ctx)) + + featured := make([]*model.Download, 0, 5) + stables := make(map[string][]*model.Download) + stableVersions := make([]string, 0, 2) + unstables := make(map[string][]*model.Download) + archiveds := make(map[string][]*model.Download) + archivedVersions := make([]string, 0, 20) + + for _, download := range downloads { + version := download.Version + if download.Category == model.DLStable { + if _, ok := stables[version]; !ok { + stableVersions = append(stableVersions, version) + stables[version] = make([]*model.Download, 0, 15) + } + stables[version] = append(stables[version], download) + + if download.IsRecommend && len(featured) < 5 { + featured = append(featured, download) + } + } else if download.Category == model.DLUnstable { + if _, ok := unstables[version]; !ok { + unstables[version] = make([]*model.Download, 0, 15) + } + unstables[version] = append(unstables[version], download) + } else if download.Category == model.DLArchived { + if _, ok := archiveds[version]; !ok { + archivedVersions = append(archivedVersions, version) + archiveds[version] = make([]*model.Download, 0, 15) + } + archiveds[version] = append(archiveds[version], download) + } + } + + data := map[string]interface{}{ + "activeDl": "active", + "featured": featured, + "stables": stables, + "stable_versions": stableVersions, + "unstables": unstables, + "archiveds": archiveds, + "archived_versions": archivedVersions, + } + + return render(ctx, "download/go.html", data) +} + +var filenameReg = regexp.MustCompile(`\d+\.\d[a-z\.]*\d+`) + +func (self DownloadController) FetchGoInstallPackage(ctx echo.Context) error { + filename := ctx.Param("filename") + + go logic.DefaultDownload.RecordDLTimes(context.EchoContext(ctx), filename) + + officalUrl := GoStoragePrefix + filename + resp, err := self.headWithTimeout(officalUrl) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return ctx.Redirect(http.StatusSeeOther, officalUrl) + } + if err == nil { + resp.Body.Close() + } + + goVersion := filenameReg.FindString(filename) + filePath := fmt.Sprintf("go/%s/%s", goVersion, filename) + + dlUrls := strings.Split(config.ConfigFile.MustValue("download", "dl_urls"), ",") + for _, dlUrl := range dlUrls { + dlUrl += filePath + resp, err = self.headWithTimeout(dlUrl) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return ctx.Redirect(http.StatusSeeOther, dlUrl) + } + if err == nil { + resp.Body.Close() + } + } + + getLogger(ctx).Infoln("download:", filename, "from the site static directory") + + return ctx.Redirect(http.StatusSeeOther, "/static/"+filePath) +} + +func (DownloadController) AddNewDownload(ctx echo.Context) error { + version := ctx.QueryParam("version") + selector := ctx.QueryParam("selector") + + if version == "" { + return fail(ctx, 1, "version is empty") + } + + if selector == "" { + selector = ".toggleVisible" + } + + err := logic.DefaultDownload.AddNewDownload(context.EchoContext(ctx), version, selector) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +func (DownloadController) headWithTimeout(dlUrl string) (*http.Response, error) { + client := http.Client{ + Timeout: 5 * time.Second, + } + + return client.Head(dlUrl) +} diff --git a/internal/http/controller/favorite.go b/internal/http/controller/favorite.go new file mode 100644 index 00000000..01cdf577 --- /dev/null +++ b/internal/http/controller/favorite.go @@ -0,0 +1,93 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" +) + +type FavoriteController struct{} + +// 注册路由 +func (self FavoriteController) RegisterRoute(g *echo.Group) { + g.POST("/favorite/:objid", self.Create, middleware.NeedLogin()) + g.GET("/favorites/:username", self.ReadList) +} + +// Create 收藏(取消收藏) +func (FavoriteController) Create(ctx echo.Context) error { + objtype := goutils.MustInt(ctx.FormValue("objtype")) + objid := goutils.MustInt(ctx.Param("objid")) + collect := goutils.MustInt(ctx.FormValue("collect")) + + user := ctx.Get("user").(*model.Me) + + var err error + if collect == 1 { + err = logic.DefaultFavorite.Save(context.EchoContext(ctx), user.Uid, objid, objtype) + } else { + err = logic.DefaultFavorite.Cancel(context.EchoContext(ctx), user.Uid, objid, objtype) + } + + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +// ReadList 我的(某人的)收藏 +func (FavoriteController) ReadList(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + objtype := goutils.MustInt(ctx.QueryParam("objtype"), model.TypeArticle) + p := goutils.MustInt(ctx.QueryParam("p"), 1) + + data := map[string]interface{}{"objtype": objtype, "user": user} + + rows := goutils.MustInt(ctx.QueryParam("rows"), 20) + if rows > 20 { + rows = 20 + } + favorites, total := logic.DefaultFavorite.FindUserFavorites(context.EchoContext(ctx), user.Uid, objtype, (p-1)*rows, rows) + if total > 0 { + objids := slices.StructsIntSlice(favorites, "Objid") + + switch objtype { + case model.TypeTopic: + data["topics"] = logic.DefaultTopic.FindByTids(objids) + case model.TypeArticle: + data["articles"] = logic.DefaultArticle.FindByIds(objids) + case model.TypeResource: + data["resources"] = logic.DefaultResource.FindByIds(objids) + case model.TypeWiki: + // data["wikis"] = logic.DefaultWiki.FindWikisByIds(objids) + case model.TypeProject: + data["projects"] = logic.DefaultProject.FindByIds(objids) + } + } + + uri := fmt.Sprintf("/favorites/%s?objtype=%d&rows=%d&", user.Username, objtype, rows) + paginator := logic.NewPaginatorWithPerPage(p, rows) + data["pageHtml"] = paginator.SetTotal(total).GetPageHtml(uri) + + return render(ctx, "favorite.html", data) +} diff --git a/internal/http/controller/feed.go b/internal/http/controller/feed.go new file mode 100644 index 00000000..ea2e4e22 --- /dev/null +++ b/internal/http/controller/feed.go @@ -0,0 +1,101 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "net/http" + "time" + + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + "github.com/gorilla/feeds" + echo "github.com/labstack/echo/v4" +) + +type FeedController struct{} + +// 注册路由 +func (self FeedController) RegisterRoute(g *echo.Group) { + g.GET("/feed.html", self.Atom) + g.GET("/feed.xml", self.List) +} + +func (self FeedController) Atom(ctx echo.Context) error { + return Render(ctx, "atom.html", map[string]interface{}{}) +} + +func (self FeedController) List(ctx echo.Context) error { + link := logic.WebsiteSetting.Domain + if logic.WebsiteSetting.OnlyHttps { + link = "https://" + link + "/" + } else { + link = "http://" + link + "/" + } + + now := time.Now() + + feed := &feeds.Feed{ + Title: logic.WebsiteSetting.Name, + Link: &feeds.Link{Href: link}, + Description: logic.WebsiteSetting.Slogan, + Author: &feeds.Author{Name: "polaris", Email: "polaris@studygolang.com"}, + Created: now, + Updated: now, + } + + respBody, err := logic.DefaultSearcher.FindAtomFeeds(50) + if err != nil { + return err + } + + feed.Items = make([]*feeds.Item, len(respBody.Docs)) + + for i, doc := range respBody.Docs { + url := "" + + switch doc.Objtype { + case model.TypeTopic: + url = fmt.Sprintf("%stopics/%d", link, doc.Objid) + case model.TypeArticle: + url = fmt.Sprintf("%sarticles/%d", link, doc.Objid) + case model.TypeResource: + url = fmt.Sprintf("%sresources/%d", link, doc.Objid) + case model.TypeProject: + url = fmt.Sprintf("%sp/%d", link, doc.Objid) + case model.TypeWiki: + url = fmt.Sprintf("%swiki/%d", link, doc.Objid) + case model.TypeBook: + url = fmt.Sprintf("%sbook/%d", link, doc.Objid) + } + feed.Items[i] = &feeds.Item{ + Title: doc.Title, + Link: &feeds.Link{Href: url}, + Author: &feeds.Author{Name: doc.Author}, + Description: doc.Content, + Created: time.Time(doc.CreatedAt), + Updated: time.Time(doc.CreatedAt), + } + } + + atom, err := feed.ToAtom() + if err != nil { + return err + } + + return self.responseXML(ctx, atom) +} + +func (FeedController) responseXML(ctx echo.Context, data string) (err error) { + response := ctx.Response() + response.Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) + response.WriteHeader(http.StatusOK) + _, err = response.Write([]byte(data)) + return +} diff --git a/internal/http/controller/gctt.go b/internal/http/controller/gctt.go new file mode 100644 index 00000000..9af685e3 --- /dev/null +++ b/internal/http/controller/gctt.go @@ -0,0 +1,237 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "crypto/hmac" + "crypto/sha1" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/studygolang/studygolang/echoutils" +) + +type GCTTController struct{} + +// 注册路由 +func (self GCTTController) RegisterRoute(g *echo.Group) { + g.GET("/gctt", self.Index) + g.GET("/gctt-list", self.UserList) + g.GET("/gctt-issue", self.IssueList) + g.GET("/gctt/:username", self.User) + g.GET("/gctt-apply", self.Apply, middleware.NeedLogin()) + g.Match([]string{"GET", "POST"}, "/gctt-new", self.Create, middleware.NeedLogin()) + + g.POST("/gctt-webhook", self.Webhook) +} + +func (self GCTTController) Index(ctx echo.Context) error { + gcttTimeLines := logic.DefaultGCTT.FindTimeLines(context.EchoContext(ctx)) + gcttUsers := logic.DefaultGCTT.FindCoreUsers(context.EchoContext(ctx)) + gcttIssues := logic.DefaultGCTT.FindUnTranslateIssues(context.EchoContext(ctx), 10) + + return Render(ctx, "gctt/index.html", map[string]interface{}{ + "time_lines": gcttTimeLines, + "users": gcttUsers, + "issues": gcttIssues, + }) +} + +// Apply 申请成为译者 +func (GCTTController) Apply(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + gcttUser := logic.DefaultGCTT.FindTranslator(context.EchoContext(ctx), me) + if gcttUser.Id > 0 { + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + // 是否绑定了 github 账号 + var githubUser *model.BindUser + bindUsers := logic.DefaultUser.FindBindUsers(context.EchoContext(ctx), me.Uid) + for _, bindUser := range bindUsers { + if bindUser.Type == model.BindTypeGithub { + githubUser = bindUser + break + } + } + + // 如果已经绑定,查看是否之前已经是译者 + if githubUser != nil { + gcttUser = logic.DefaultGCTT.FindOne(context.EchoContext(ctx), githubUser.Username) + logic.DefaultGCTT.BindUser(context.EchoContext(ctx), gcttUser, me.Uid, githubUser) + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + return render(ctx, "gctt/apply.html", map[string]interface{}{ + "activeGCTT": "active", + "github_user": githubUser, + }) +} + +// Create 发布新译文 +func (GCTTController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + gcttUser := logic.DefaultGCTT.FindTranslator(context.EchoContext(ctx), me) + + title := ctx.FormValue("title") + if title == "" || ctx.Request().Method != "POST" { + return render(ctx, "gctt/new.html", map[string]interface{}{ + "activeGCTT": "active", + "gctt_user": gcttUser, + }) + } + + if ctx.FormValue("content") == "" { + return fail(ctx, 1, "内容不能为空") + } + + if gcttUser == nil { + return fail(ctx, 2, "不允许发布!") + } + + forms, _ := ctx.FormParams() + id, err := logic.DefaultArticle.Publish(echoutils.WrapEchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 3, "内部服务错误") + } + + return success(ctx, map[string]interface{}{"id": id}) +} + +func (GCTTController) User(ctx echo.Context) error { + username := ctx.Param("username") + if username == "" { + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + gcttUser := logic.DefaultGCTT.FindOne(context.EchoContext(ctx), username) + if gcttUser.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + joinDays := int(gcttUser.LastAt-gcttUser.JoinedAt)/86400 + 1 + avgDays := fmt.Sprintf("%.1f", float64(gcttUser.AvgTime)/86400.0) + + articles := logic.DefaultArticle.FindTaGCTTArticles(context.EchoContext(ctx), username) + + return render(ctx, "gctt/user-info.html", map[string]interface{}{ + "gctt_user": gcttUser, + "articles": articles, + "join_days": joinDays, + "avg_days": avgDays, + }) +} + +func (GCTTController) UserList(ctx echo.Context) error { + users := logic.DefaultGCTT.FindUsers(context.EchoContext(ctx)) + + num, words := 0, 0 + for _, user := range users { + num += user.Num + words += user.Words + } + + prs := logic.DefaultGCTT.FindNewestGit(context.EchoContext(ctx)) + + return render(ctx, "gctt/user-list.html", map[string]interface{}{ + "users": users, + "num": num, + "words": words, + "prs": prs, + }) +} + +func (GCTTController) IssueList(ctx echo.Context) error { + querystring, arg := "", "" + + label := ctx.QueryParam("label") + + translator := ctx.QueryParam("translator") + if translator != "" { + querystring = "translator=?" + arg = translator + } else { + if label == model.LabelUnClaim { + querystring = "label=?" + arg = label + } else if label == model.LabelClaimed { + querystring = "label=? AND state=" + strconv.Itoa(model.IssueOpened) + arg = label + } + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + issues := logic.DefaultGCTT.FindIssues(context.EchoContext(ctx), paginator, querystring, arg) + + total := logic.DefaultGCTT.IssueCount(context.EchoContext(ctx), querystring, arg) + pageHTML := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + prs := logic.DefaultGCTT.FindNewestGit(context.EchoContext(ctx)) + + return render(ctx, "gctt/issue-list.html", map[string]interface{}{ + "issues": issues, + "prs": prs, + "page": template.HTML(pageHTML), + "translator": translator, + "label": label, + "total": total, + }) +} + +func (GCTTController) Webhook(ctx echo.Context) error { + body, err := ioutil.ReadAll(Request(ctx).Body) + if err != nil { + logger.Errorln("GCTTController Webhook error:", err) + return err + } + + header := ctx.Request().Header + + tokenSecret := config.ConfigFile.MustValue("gctt", "token_secret") + ok := checkMAC(body, header.Get("X-Hub-Signature"), []byte(tokenSecret)) + if !ok { + logger.Errorln("GCTTController Webhook checkMAC error", string(body)) + return nil + } + + event := header.Get("X-GitHub-Event") + logger.Infoln("GCTTController Webhook event:", event) + switch event { + case "pull_request": + return logic.DefaultGithub.PullRequestEvent(context.EchoContext(ctx), body) + case "issue_comment": + return logic.DefaultGithub.IssueCommentEvent(context.EchoContext(ctx), body) + case "issues": + return logic.DefaultGithub.IssueEvent(context.EchoContext(ctx), body) + default: + fmt.Println("not deal event:", event) + } + + return nil +} + +func checkMAC(message []byte, messageMAC string, key []byte) bool { + mac := hmac.New(sha1.New, key) + mac.Write(message) + expectedMAC := fmt.Sprintf("%x", mac.Sum(nil)) + return messageMAC == "sha1="+expectedMAC +} diff --git a/internal/http/controller/gift.go b/internal/http/controller/gift.go new file mode 100644 index 00000000..68af83fc --- /dev/null +++ b/internal/http/controller/gift.go @@ -0,0 +1,66 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type GiftController struct{} + +// 注册路由 +func (self GiftController) RegisterRoute(g *echo.Group) { + g.GET("/gift", self.GiftList) + g.POST("/gift/exchange", self.Exchange, middleware.NeedLogin()) + g.GET("/gift/mine", self.MyGift, middleware.NeedLogin()) +} + +func (GiftController) GiftList(ctx echo.Context) error { + gifts := logic.DefaultGift.FindAllOnline(context.EchoContext(ctx)) + + if len(gifts) > 0 { + user, ok := ctx.Get("user").(*model.Me) + if ok { + logic.DefaultGift.UserCanExchange(context.EchoContext(ctx), user, gifts) + } + } + + data := map[string]interface{}{ + "gifts": gifts, + } + + return render(ctx, "gift/list.html", data) +} + +func (GiftController) Exchange(ctx echo.Context) error { + giftId := goutils.MustInt(ctx.FormValue("gift_id")) + me := ctx.Get("user").(*model.Me) + err := logic.DefaultGift.Exchange(context.EchoContext(ctx), me, giftId) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +func (GiftController) MyGift(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + exchangeRecords := logic.DefaultGift.FindExchangeRecords(context.EchoContext(ctx), me) + + data := map[string]interface{}{ + "records": exchangeRecords, + } + + return render(ctx, "gift/mine.html", data) +} diff --git a/internal/http/controller/image.go b/internal/http/controller/image.go new file mode 100644 index 00000000..6c05853a --- /dev/null +++ b/internal/http/controller/image.go @@ -0,0 +1,212 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "encoding/json" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/times" +) + +// 图片处理 +type ImageController struct{} + +func (self ImageController) RegisterRoute(g *echo.Group) { + // todo 这三个upload差不多啊 + g.POST("/image/upload", self.Upload) + g.POST("/image/paste_upload", self.PasteUpload) + g.POST("/image/quick_upload", self.QuickUpload) + g.Match([]string{"GET", "POST"}, "/image/transfer", self.Transfer) +} + +// PasteUpload jquery 粘贴上传图片 +func (self ImageController) PasteUpload(ctx echo.Context) error { + objLogger := getLogger(ctx) + + file, fileHeader, err := Request(ctx).FormFile("imageFile") + if err != nil { + objLogger.Errorln("upload error:", err) + return self.pasteUploadFail(ctx, err.Error()) + } + defer file.Close() + + // 如果是临时文件,存在硬盘中,则是 *os.File(大于32M),直接报错 + if _, ok := file.(*os.File); ok { + objLogger.Errorln("upload error:file too large!") + return self.pasteUploadFail(ctx, "文件太大!") + } + + buf, err := ioutil.ReadAll(file) + if err != nil { + return self.pasteUploadFail(ctx, "文件读取失败!") + } + if len(buf) > logic.MaxImageSize { + return self.pasteUploadFail(ctx, "文件太大!") + } + + imgDir := times.Format("ymd") + file.Seek(0, io.SeekStart) + path, err := logic.DefaultUploader.UploadImage(context.EchoContext(ctx), file, imgDir, buf, filepath.Ext(fileHeader.Filename)) + if err != nil { + return self.pasteUploadFail(ctx, "文件上传失败!") + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + data := map[string]interface{}{ + "success": 1, + "message": cdnDomain + path, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} + +// QuickUpload CKEditor 编辑器,上传图片,支持粘贴方式上传 +func (self ImageController) QuickUpload(ctx echo.Context) error { + objLogger := getLogger(ctx) + + file, fileHeader, err := Request(ctx).FormFile("upload") + if err != nil { + objLogger.Errorln("upload error:", err) + return self.quickUploadFail(ctx, err.Error()) + } + defer file.Close() + + // 如果是临时文件,存在硬盘中,则是 *os.File(大于32M),直接报错 + if _, ok := file.(*os.File); ok { + objLogger.Errorln("upload error:file too large!") + return self.quickUploadFail(ctx, "文件太大!") + } + + buf, err := ioutil.ReadAll(file) + if err != nil { + return self.quickUploadFail(ctx, "文件读取失败!") + } + if len(buf) > logic.MaxImageSize { + return self.quickUploadFail(ctx, "文件太大!") + } + + fileName := goutils.Md5Buf(buf) + filepath.Ext(fileHeader.Filename) + imgDir := times.Format("ymd") + file.Seek(0, io.SeekStart) + path, err := logic.DefaultUploader.UploadImage(context.EchoContext(ctx), file, imgDir, buf, filepath.Ext(fileHeader.Filename)) + if err != nil { + return self.quickUploadFail(ctx, "文件上传失败!") + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + data := map[string]interface{}{ + "uploaded": 1, + "fileName": fileName, + "url": cdnDomain + path, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} + +// Upload 上传图片 +func (ImageController) Upload(ctx echo.Context) error { + objLogger := getLogger(ctx) + + file, fileHeader, err := Request(ctx).FormFile("img") + if err != nil { + objLogger.Errorln("upload error:", err) + return fail(ctx, 1, "非法文件上传!") + } + defer file.Close() + + // 如果是临时文件,存在硬盘中,则是 *os.File(大于32M),直接报错 + if _, ok := file.(*os.File); ok { + objLogger.Errorln("upload error:file too large!") + return fail(ctx, 2, "文件太大!") + } + + buf, err := ioutil.ReadAll(file) + if err != nil { + return fail(ctx, 3, "文件读取失败!") + } + if len(buf) > logic.MaxImageSize { + return fail(ctx, 4, "文件太大!") + } + + imgDir := times.Format("ymd") + if ctx.FormValue("avatar") != "" { + imgDir = "avatar" + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + file.Seek(0, io.SeekStart) + path, err := logic.DefaultUploader.UploadImage(context.EchoContext(ctx), file, imgDir, buf, filepath.Ext(fileHeader.Filename)) + if err != nil { + return fail(ctx, 5, "文件上传失败!") + } + + return success(ctx, map[string]interface{}{"url": cdnDomain + path, "uri": path}) +} + +// Transfer 转换图片:通过 url 从远程下载图片然后转存到七牛 +func (ImageController) Transfer(ctx echo.Context) error { + origUrl := ctx.FormValue("url") + if origUrl == "" { + return fail(ctx, 1, "url不能为空!") + } + + path, err := logic.DefaultUploader.TransferUrl(context.EchoContext(ctx), origUrl) + if err != nil { + return fail(ctx, 2, "文件上传失败!") + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + return success(ctx, map[string]interface{}{"url": cdnDomain + path}) +} + +func (ImageController) quickUploadFail(ctx echo.Context, message string) error { + data := map[string]interface{}{ + "uploaded": 0, + "error": map[string]string{ + "message": message, + }, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} + +func (ImageController) pasteUploadFail(ctx echo.Context, message string) error { + data := map[string]interface{}{ + "success": 0, + "message": message, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} diff --git a/internal/http/controller/index.go b/internal/http/controller/index.go new file mode 100644 index 00000000..a37f7079 --- /dev/null +++ b/internal/http/controller/index.go @@ -0,0 +1,161 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "bytes" + "html/template" + "net/http" + "net/url" + "strings" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" +) + +type IndexController struct{} + +// 注册路由 +func (self IndexController) RegisterRoute(g *echo.Group) { + g.GET("/", self.Index) + g.GET("/wr", self.WrapUrl) + g.GET("/pkgdoc", self.Pkgdoc) + g.GET("/markdown", self.Markdown) + g.GET("/link", self.Link) +} + +func (IndexController) Index(ctx echo.Context) error { + if len(logic.WebsiteSetting.IndexNavs) == 0 { + return render(ctx, "index.html", nil) + } + + tab := ctx.QueryParam("tab") + if tab == "" { + tab = GetFromCookie(ctx, "INDEX_TAB") + } + + if tab == "" { + tab = logic.WebsiteSetting.IndexNavs[0].Tab + } + paginator := logic.NewPaginator(goutils.MustInt(ctx.QueryParam("p"), 1)) + + data := logic.DefaultIndex.FindData(context.EchoContext(ctx), tab, paginator) + + SetCookie(ctx, "INDEX_TAB", data["tab"].(string)) + + data["all_nodes"] = logic.GenNodes() + + if tab == model.TabAll || tab == model.TabRecommend { + pageHtml := paginator.SetTotal(logic.DefaultFeed.GetTotalCount(context.EchoContext(ctx))).GetPageHtml(ctx.Request().URL.Path) + + data["page"] = template.HTML(pageHtml) + + data["total"] = paginator.GetTotal() + } + + return render(ctx, "index.html", data) +} + +// WrapUrl 包装链接 +func (IndexController) WrapUrl(ctx echo.Context) error { + tUrl := ctx.QueryParam("u") + if tUrl == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + // 本站 + if strings.Contains(tUrl, logic.WebsiteSetting.Domain) { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + + if strings.Contains(tUrl, "?") { + tUrl += "&" + } else { + tUrl += "?" + } + tUrl += "utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com" + + if CheckIsHttps(ctx) { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + + var ( + pUrl *url.URL + err error + ) + + if pUrl, err = url.Parse(tUrl); err != nil { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + + iframeDeny := config.ConfigFile.MustValue("crawl", "iframe_deny") + // 检测是否禁止了 iframe 加载 + // 看是否在黑名单中 + for _, denyHost := range strings.Split(iframeDeny, ",") { + if strings.Contains(pUrl.Host, denyHost) { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + } + + // 检测会比较慢,进行异步检测,记录下来,以后分析再加黑名单 + go func() { + resp, err := http.Head(tUrl) + if err != nil { + logger.Errorln("[iframe] head url:", tUrl, "error:", err) + return + } + defer resp.Body.Close() + if resp.Header.Get("X-Frame-Options") != "" { + logger.Errorln("[iframe] deny:", tUrl) + return + } + }() + + return render(ctx, "wr.html", map[string]interface{}{"url": tUrl}) +} + +// PkgdocHandler Go 语言文档中文版 +func (IndexController) Pkgdoc(ctx echo.Context) error { + // return render(ctx, "pkgdoc.html", map[string]interface{}{"activeDoc": "active"}) + tpl, err := template.ParseFiles(config.TemplateDir + "pkgdoc.html") + if err != nil { + logger.Errorln("parse file error:", err) + return err + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, nil) + if err != nil { + logger.Errorln("execute template error:", err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} + +func (IndexController) Markdown(ctx echo.Context) error { + return render(ctx, "markdown.html", nil) +} + +// Link 用于重定向外部链接,比如广告链接 +func (IndexController) Link(ctx echo.Context) error { + tUrl := ctx.QueryParam("url") + if strings.Contains(tUrl, "?") { + tUrl += "&" + } else { + tUrl += "?" + } + tUrl += "utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com" + return ctx.Redirect(http.StatusSeeOther, tUrl) +} diff --git a/internal/http/controller/install.go b/internal/http/controller/install.go new file mode 100644 index 00000000..8d3da960 --- /dev/null +++ b/internal/http/controller/install.go @@ -0,0 +1,297 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "bytes" + "html/template" + "net/http" + "net/url" + "runtime" + "strconv" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" +) + +type InstallController struct{} + +// 注册路由 +func (self InstallController) RegisterRoute(g *echo.Group) { + g.GET("/install", self.SetupConfig) + g.Match([]string{"GET", "POST"}, "/install/setup-config", self.SetupConfig) + g.Match([]string{"GET", "POST"}, "/install/do", self.DoInstall) + g.Match([]string{"GET", "POST"}, "/install/options", self.SetupOptions) +} + +func (self InstallController) SetupConfig(ctx echo.Context) error { + // config/env.ini 存在 + if db.MasterDB != nil { + if logic.DefaultInstall.IsTableExist(context.EchoContext(ctx)) { + return ctx.Redirect(http.StatusSeeOther, "/") + } + return ctx.Redirect(http.StatusSeeOther, "/install/do") + } + + step := goutils.MustInt(ctx.QueryParam("step")) + if step == 2 { + err := self.genConfig(ctx) + if err != nil { + data := map[string]interface{}{ + "dbhost": ctx.FormValue("dbhost"), + "dbport": ctx.FormValue("dbport"), + "dbname": ctx.FormValue("dbname"), + "uname": ctx.FormValue("uname"), + "err_type": 1, + } + + if err == db.ConnectDBErr { + data["err_type"] = 1 + } else if err == db.UseDBErr { + data["err_type"] = 2 + } + + return renderInstall(ctx, "install/setup-err.html", data) + } + } + return renderInstall(ctx, "install/setup-config.html", map[string]interface{}{"step": step}) +} + +// DoInstall 执行安装,包括站点简单配置,安装数据库(创建数据库、表,填充基本数据)等 +func (self InstallController) DoInstall(ctx echo.Context) error { + if db.MasterDB == nil { + return ctx.Redirect(http.StatusSeeOther, "/install") + } + + if logic.DefaultInstall.IsTableExist(context.EchoContext(ctx)) { + if logic.DefaultInstall.HadRootUser(context.EchoContext(ctx)) { + return ctx.Redirect(http.StatusSeeOther, "/") + } + } + + step := goutils.MustInt(ctx.QueryParam("step"), 1) + data := map[string]interface{}{ + "user_name": "admin", + "admin_email": "", + "step": step, + } + + if step == 2 { + username := ctx.FormValue("user_name") + email := ctx.FormValue("admin_email") + password1 := ctx.FormValue("admin_password") + password2 := ctx.FormValue("admin_password2") + + if username == "" || email == "" { + data["err"] = "用户名和邮箱不能留空" + return renderInstall(ctx, "install/install.html", data) + } + + data["user_name"] = username + data["admin_email"] = email + + if password1 != password2 { + data["err"] = "两次输入的密码不一致" + return renderInstall(ctx, "install/install.html", data) + } + + err := logic.DefaultInstall.CreateTable(context.EchoContext(ctx)) + if err != nil { + data["err"] = "创建数据表失败!" + return renderInstall(ctx, "install/install.html", data) + } + + err = logic.DefaultInstall.InitTable(context.EchoContext(ctx)) + if err != nil { + data["err"] = "初始化数据表失败!" + return renderInstall(ctx, "install/install.html", data) + } + + if password1 == "" { + password1 = goutils.RandString(12) + data["passwd"] = password1 + } + + // 创建管理员 + form := url.Values{ + "username": {username}, + "email": {email}, + "passwd": {password1}, + "is_root": {"true"}, + "status": {strconv.Itoa(model.UserStatusAudit)}, + } + errMsg, err := logic.DefaultUser.CreateUser(context.EchoContext(ctx), form) + if err != nil { + data["err"] = errMsg + return renderInstall(ctx, "install/install.html", data) + } + + data["step"] = 3 + + data["os"] = runtime.GOOS + + // 为了保证程序正常,需要重启 + go self.reload() + } + return renderInstall(ctx, "install/install.html", data) +} + +func (InstallController) SetupOptions(ctx echo.Context) error { + var ( + noEmailConf = false + noQiniuConf = false + ) + + if config.ConfigFile.MustValue("email", "smtp_username") == "" { + noEmailConf = true + } + + if config.ConfigFile.MustValue("qiniu", "access_key") == "" { + noQiniuConf = true + } + + if !noEmailConf && !noQiniuConf { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + if ctx.Request().Method == "POST" { + config.ConfigFile.SetSectionComments("email", "用于注册发送激活码等") + emailFields := []string{"smtp_host", "smtp_port", "smtp_username", "smtp_password", "from_email"} + for _, field := range emailFields { + if field == "smtp_port" && ctx.FormValue("smtp_port") == "" { + config.ConfigFile.SetValue("email", field, "25") + } else { + config.ConfigFile.SetValue("email", field, ctx.FormValue(field)) + } + } + + config.ConfigFile.SetSectionComments("qiniu", "图片存储在七牛云,如果没有可以通过 https://portal.qiniu.com/signup?code=3lfz4at7pxfma 免费申请") + qiniuFields := []string{"access_key", "secret_key", "bucket_name", "http_domain", "https_domain"} + for _, field := range qiniuFields { + config.ConfigFile.SetValue("qiniu", field, ctx.FormValue(field)) + } + if ctx.FormValue("https_domain") == "" { + config.ConfigFile.SetValue("qiniu", "https_domain", ctx.FormValue("http_domain")) + } + + config.SaveConfigFile() + + return renderInstall(ctx, "install/setup-options.html", map[string]interface{}{"success": true}) + } + + data := map[string]interface{}{ + "no_email_conf": noEmailConf, + "no_qiniu_conf": noQiniuConf, + } + return renderInstall(ctx, "install/setup-options.html", data) +} + +func (InstallController) genConfig(ctx echo.Context) error { + env := ctx.FormValue("env") + + config.ConfigFile.SetSectionComments("global", "") + config.ConfigFile.SetValue("global", "env", env) + + var ( + logLevel = "DEBUG" + // domain = global.App.Host + ":" + global.App.Port + xormLogLevel = "0" + xormShowSql = "true" + ) + if env == "pro" { + logLevel = "INFO" + xormLogLevel = "1" + xormShowSql = "false" + } + + config.ConfigFile.SetValue("global", "log_level", logLevel) + config.ConfigFile.SetValue("global", "cookie_secret", goutils.RandString(10)) + config.ConfigFile.SetValue("global", "data_path", "data/max_online_num") + + config.ConfigFile.SetSectionComments("listen", "") + config.ConfigFile.SetValue("listen", "host", "") + config.ConfigFile.SetValue("listen", "port", global.App.Port) + + dbname := ctx.FormValue("dbname") + uname := ctx.FormValue("uname") + pwd := ctx.FormValue("pwd") + dbhost := ctx.FormValue("dbhost") + dbport := ctx.FormValue("dbport") + + config.ConfigFile.SetSectionComments("mysql", "") + config.ConfigFile.SetValue("mysql", "host", dbhost) + config.ConfigFile.SetValue("mysql", "port", dbport) + config.ConfigFile.SetValue("mysql", "user", uname) + config.ConfigFile.SetValue("mysql", "password", pwd) + config.ConfigFile.SetValue("mysql", "dbname", dbname) + config.ConfigFile.SetValue("mysql", "charset", "utf8") + config.ConfigFile.SetKeyComments("mysql", "max_idle", "最大空闲连接数") + config.ConfigFile.SetValue("mysql", "max_idle", "2") + config.ConfigFile.SetKeyComments("mysql", "max_conn", "最大打开连接数") + config.ConfigFile.SetValue("mysql", "max_conn", "10") + + config.ConfigFile.SetSectionComments("xorm", "") + config.ConfigFile.SetValue("xorm", "show_sql", xormShowSql) + config.ConfigFile.SetKeyComments("xorm", "log_level", "0-debug, 1-info, 2-warning, 3-error, 4-off, 5-unknow") + config.ConfigFile.SetValue("xorm", "log_level", xormLogLevel) + + config.ConfigFile.SetSectionComments("security", "") + config.ConfigFile.SetKeyComments("security", "unsubscribe_token_key", "退订邮件使用的 token key") + config.ConfigFile.SetValue("security", "unsubscribe_token_key", goutils.RandString(18)) + config.ConfigFile.SetKeyComments("security", "activate_sign_salt", "注册激活邮件使用的 sign salt") + config.ConfigFile.SetValue("security", "activate_sign_salt", goutils.RandString(18)) + + config.ConfigFile.SetSectionComments("sensitive", "过滤广告") + config.ConfigFile.SetKeyComments("sensitive", "title", "标题关键词") + config.ConfigFile.SetValue("sensitive", "title", "") + config.ConfigFile.SetKeyComments("sensitive", "content", "内容关键词") + config.ConfigFile.SetValue("sensitive", "content", "") + + config.ConfigFile.SetSectionComments("search", "搜索配置") + config.ConfigFile.SetValue("search", "engine_url", "") + + // 校验数据库配置是否正确有效 + if err := db.TestDB(); err != nil { + return err + } + + config.SaveConfigFile() + return nil +} + +func renderInstall(ctx echo.Context, filename string, data map[string]interface{}) error { + objLog := getLogger(ctx) + + if data == nil { + data = make(map[string]interface{}) + } + + filename = config.TemplateDir + filename + + requestURI := ctx.Request().RequestURI + tpl, err := template.ParseFiles(filename) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", requestURI, err) + return err + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, data) + if err != nil { + objLog.Errorf("执行模板出错(Execute):[%q] %s\n", requestURI, err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} diff --git a/internal/http/controller/install_unix.go b/internal/http/controller/install_unix.go new file mode 100644 index 00000000..5ed61aba --- /dev/null +++ b/internal/http/controller/install_unix.go @@ -0,0 +1,19 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +//go:build !windows && !plan9 +// +build !windows,!plan9 + +package controller + +import ( + "os" + "syscall" +) + +func (InstallController) reload() { + syscall.Kill(os.Getpid(), syscall.SIGUSR2) +} diff --git a/websites/code/studygolang/src/controller/common.go b/internal/http/controller/install_windows.go similarity index 50% rename from websites/code/studygolang/src/controller/common.go rename to internal/http/controller/install_windows.go index 8c4f814b..4c522906 100644 --- a/websites/code/studygolang/src/controller/common.go +++ b/internal/http/controller/install_windows.go @@ -1,13 +1,10 @@ -// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Copyright 2016 The StudyGolang Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // http://studygolang.com -// Author:polaris studygolang@gmail.com +// Author: polaris polaris@studygolang.com package controller -import ( - "config" -) - -var ROOT = config.ROOT +func (InstallController) reload() { +} diff --git a/internal/http/controller/interview.go b/internal/http/controller/interview.go new file mode 100644 index 00000000..904c4533 --- /dev/null +++ b/internal/http/controller/interview.go @@ -0,0 +1,106 @@ +// Copyright 2022 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "strconv" + "time" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeInterview, logic.InterviewComment{}) + logic.RegisterLikeObject(model.TypeInterview, logic.InterviewLike{}) +} + +type InterviewController struct{} + +// RegisterRoute 注册路由 +func (self InterviewController) RegisterRoute(g *echo.Group) { + g.GET("/interview/question", self.TodayQuestion) + g.GET("/interview/question/:show_sn", self.Find) + + g.Match([]string{"GET", "POST"}, "/interview/new", self.Create, middleware.NeedLogin(), middleware.AdminAuth()) +} + +func (InterviewController) Create(ctx echo.Context) error { + question := ctx.FormValue("question") + // 请求新建面试题页面 + if question == "" || ctx.Request().Method != "POST" { + interview := &model.InterviewQuestion{} + return render(ctx, "interview/new.html", map[string]interface{}{"interview": interview}) + } + + forms, _ := ctx.FormParams() + interview, err := logic.DefaultInterview.Publish(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, interview) +} + +// TodayQuestion 今日题目 +func (ic InterviewController) TodayQuestion(ctx echo.Context) error { + question := logic.DefaultInterview.TodayQuestion(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "title": "Go每日一题 今日(" + time.Now().Format("2006-01-02") + ")", + } + return ic.detail(ctx, question, data) +} + +// Find 某个题目的详情 +func (ic InterviewController) Find(ctx echo.Context) error { + showSn := ctx.Param("show_sn") + sn, err := strconv.ParseInt(showSn, 32, 64) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/interview/question?"+err.Error()) + } + + question, err := logic.DefaultInterview.FindOne(context.EchoContext(ctx), sn) + if err != nil || question.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/interview/question") + } + + data := map[string]interface{}{ + "title": "Go每日一题(" + strconv.Itoa(question.Id) + ")", + } + + return ic.detail(ctx, question, data) +} + +func (InterviewController) detail(ctx echo.Context, question *model.InterviewQuestion, data map[string]interface{}) error { + data["question"] = question + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, question.Id, model.TypeInterview) + // data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, question.Id, model.TypeInterview) + + logic.Views.Incr(Request(ctx), model.TypeInterview, question.Id, me.Uid) + + go logic.DefaultViewRecord.Record(question.Id, model.TypeInterview, me.Uid) + + if me.IsRoot { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), question.Id, model.TypeInterview) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), question.Id, model.TypeInterview) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeInterview, question.Id) + } + + return render(ctx, "interview/question.html,common/comment.html", data) +} diff --git a/internal/http/controller/like.go b/internal/http/controller/like.go new file mode 100644 index 00000000..fab3daa6 --- /dev/null +++ b/internal/http/controller/like.go @@ -0,0 +1,47 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +// 喜欢系统 + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type LikeController struct{} + +// 注册路由 +func (self LikeController) RegisterRoute(g *echo.Group) { + g.POST("/like/:objid", self.Like, middleware.NeedLogin()) +} + +// Like 喜欢(或取消喜欢) +func (LikeController) Like(ctx echo.Context) error { + form, _ := ctx.FormParams() + if !util.CheckInt(form, "objtype") || !util.CheckInt(form, "flag") { + return fail(ctx, 1, "参数错误") + } + + user := ctx.Get("user").(*model.Me) + objid := goutils.MustInt(ctx.Param("objid")) + objtype := goutils.MustInt(ctx.FormValue("objtype")) + likeFlag := goutils.MustInt(ctx.FormValue("flag")) + + err := logic.DefaultLike.LikeObject(context.EchoContext(ctx), user.Uid, objid, objtype, likeFlag) + if err != nil { + return fail(ctx, 2, "服务器内部错误") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/link.go b/internal/http/controller/link.go new file mode 100644 index 00000000..b92846d6 --- /dev/null +++ b/internal/http/controller/link.go @@ -0,0 +1,29 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type LinkController struct{} + +// 注册路由 +func (self LinkController) RegisterRoute(g *echo.Group) { + g.GET("/links", self.FindLinks) +} + +// FindLinks 友情链接 +func (LinkController) FindLinks(ctx echo.Context) error { + + friendLinks := logic.DefaultFriendLink.FindAll(context.EchoContext(ctx)) + + return render(ctx, "link.html", map[string]interface{}{"links": friendLinks}) +} diff --git a/internal/http/controller/message.go b/internal/http/controller/message.go new file mode 100644 index 00000000..3c2588c1 --- /dev/null +++ b/internal/http/controller/message.go @@ -0,0 +1,119 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type MessageController struct{} + +// 注册路由 +func (self MessageController) RegisterRoute(g *echo.Group) { + messageG := g.Group("/message/", middleware.NeedLogin()) + + messageG.GET(":msgtype", self.ReadList) + messageG.GET("system", self.ReadList) + messageG.Match([]string{"GET", "POST"}, "send", self.Send) + messageG.POST("delete", self.Delete) + + // g.GET("/message/:msgtype", self.ReadList, middleware.NeedLogin()) + // g.GET("/message/system", self.ReadList, middleware.NeedLogin()) + // g.Match([]string{"GET", "POST"}, "/message/send", self.Send, middleware.NeedLogin()) + // g.POST("/message/delete", self.Delete, middleware.NeedLogin()) +} + +// Send 发短消息 +func (MessageController) Send(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + content := ctx.FormValue("content") + // 请求发送消息页面 + if content == "" || ctx.Request().Method != "POST" { + username := ctx.FormValue("username") + if username == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + message := logic.DefaultMessage.FindMsgById(context.EchoContext(ctx), ctx.FormValue("id")) + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + + if message != nil { + if message.To != me.Uid || message.From != user.Uid { + message = nil + } + } + + return render(ctx, "messages/send.html", map[string]interface{}{ + "user": user, + "message": message, + }) + } + + to := goutils.MustInt(ctx.FormValue("to")) + ok := logic.DefaultMessage.SendMessageTo(context.EchoContext(ctx), me.Uid, to, content) + if !ok { + return fail(ctx, 1, "对不起,发送失败,请稍候再试!") + } + + return success(ctx, nil) +} + +// 消息列表 +func (MessageController) ReadList(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + msgtype := ctx.Param("msgtype") + if msgtype == "" { + msgtype = "system" + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + var ( + messages []map[string]interface{} + total int64 + ) + switch msgtype { + case "system": + messages = logic.DefaultMessage.FindSysMsgsByUid(context.EchoContext(ctx), user.Uid, paginator) + total = logic.DefaultMessage.SysMsgCount(context.EchoContext(ctx), user.Uid) + case "inbox": + messages = logic.DefaultMessage.FindToMsgsByUid(context.EchoContext(ctx), user.Uid, paginator) + total = logic.DefaultMessage.ToMsgCount(context.EchoContext(ctx), user.Uid) + case "outbox": + messages = logic.DefaultMessage.FindFromMsgsByUid(context.EchoContext(ctx), user.Uid, paginator) + total = logic.DefaultMessage.FromMsgCount(context.EchoContext(ctx), user.Uid) + default: + return ctx.Redirect(http.StatusSeeOther, "/") + } + + pageHtml := paginator.SetTotal(total).GetPageHtml(fmt.Sprintf("/message/%s", msgtype)) + + return render(ctx, "messages/list.html", map[string]interface{}{"messages": messages, "msgtype": msgtype, "page": template.HTML(pageHtml)}) +} + +// 删除消息 +func (MessageController) Delete(ctx echo.Context) error { + id := ctx.FormValue("id") + msgtype := ctx.FormValue("msgtype") + if !logic.DefaultMessage.DeleteMessage(context.EchoContext(ctx), id, msgtype) { + return fail(ctx, 1, "对不起,删除失败,请稍候再试!") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/mission.go b/internal/http/controller/mission.go new file mode 100644 index 00000000..19f30fd4 --- /dev/null +++ b/internal/http/controller/mission.go @@ -0,0 +1,64 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/times" +) + +type MissionController struct{} + +// 注册路由 +func (self MissionController) RegisterRoute(g *echo.Group) { + g.GET("/mission/daily", self.Daily, middleware.NeedLogin()) + g.GET("/mission/daily/redeem", self.DailyRedeem, middleware.NeedLogin()) + g.GET("/mission/complete/:id", self.Complete, middleware.NeedLogin()) +} + +func (MissionController) Daily(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + userLoginMission := logic.DefaultMission.FindLoginMission(context.EchoContext(ctx), me) + userLoginMission.Uid = me.Uid + + data := map[string]interface{}{"login_mission": userLoginMission} + + if userLoginMission != nil && times.Format("Ymd") == strconv.Itoa(userLoginMission.Date) { + data["had_redeem"] = true + } else { + data["had_redeem"] = false + } + + fr := ctx.QueryParam("fr") + if fr == "redeem" { + data["show_msg"] = true + } + return render(ctx, "mission/daily.html", data) +} + +func (MissionController) DailyRedeem(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + logic.DefaultMission.RedeemLoginAward(context.EchoContext(ctx), me) + + return ctx.Redirect(http.StatusSeeOther, "/mission/daily?fr=redeem") +} + +func (MissionController) Complete(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + id := ctx.Param("id") + logic.DefaultMission.Complete(context.EchoContext(ctx), me, id) + + return ctx.Redirect(http.StatusSeeOther, "/balance") +} diff --git a/internal/http/controller/oauth.go b/internal/http/controller/oauth.go new file mode 100644 index 00000000..7a073fe9 --- /dev/null +++ b/internal/http/controller/oauth.go @@ -0,0 +1,115 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type OAuthController struct{} + +// 注册路由 +func (self OAuthController) RegisterRoute(g *echo.Group) { + g.GET("/oauth/github/callback", self.GithubCallback) + g.GET("/oauth/github/login", self.GithubLogin) + + g.GET("/oauth/gitea/callback", self.GiteaCallback) + g.GET("/oauth/gitea/login", self.GiteaLogin) +} + +func (OAuthController) GithubLogin(ctx echo.Context) error { + uri := ctx.QueryParam("uri") + url := logic.DefaultThirdUser.GithubAuthCodeUrl(context.EchoContext(ctx), uri) + return ctx.Redirect(http.StatusSeeOther, url) +} + +func (OAuthController) GithubCallback(ctx echo.Context) error { + code := ctx.FormValue("code") + + me, ok := ctx.Get("user").(*model.Me) + if ok { + // 已登录用户,绑定 github + logic.DefaultThirdUser.BindGithub(context.EchoContext(ctx), code, me) + + redirectURL := ctx.QueryParam("redirect_url") + if redirectURL == "" { + redirectURL = "/account/edit#connection" + } + return ctx.Redirect(http.StatusSeeOther, redirectURL) + } + + user, err := logic.DefaultThirdUser.LoginFromGithub(context.EchoContext(ctx), code) + if err != nil || user.Uid == 0 { + var errMsg = "" + if err != nil { + errMsg = err.Error() + } else { + errMsg = "服务内部错误" + } + + return render(ctx, "login.html", map[string]interface{}{"error": errMsg}) + } + + // 登录成功,种cookie + SetLoginCookie(ctx, user.Username) + + if user.Balance == 0 { + return ctx.Redirect(http.StatusSeeOther, "/balance") + } + + return ctx.Redirect(http.StatusSeeOther, "/") +} + +func (OAuthController) GiteaLogin(ctx echo.Context) error { + uri := ctx.QueryParam("uri") + url := logic.DefaultThirdUser.GiteaAuthCodeUrl(context.EchoContext(ctx), uri) + return ctx.Redirect(http.StatusSeeOther, url) +} + +func (OAuthController) GiteaCallback(ctx echo.Context) error { + code := ctx.FormValue("code") + + me, ok := ctx.Get("user").(*model.Me) + if ok { + // 已登录用户,绑定 github + logic.DefaultThirdUser.BindGitea(context.EchoContext(ctx), code, me) + + redirectURL := ctx.QueryParam("redirect_url") + if redirectURL == "" { + redirectURL = "/account/edit#connection" + } + return ctx.Redirect(http.StatusSeeOther, redirectURL) + } + + user, err := logic.DefaultThirdUser.LoginFromGitea(context.EchoContext(ctx), code) + if err != nil || user.Uid == 0 { + var errMsg = "" + if err != nil { + errMsg = err.Error() + } else { + errMsg = "服务内部错误" + } + + return render(ctx, "login.html", map[string]interface{}{"error": errMsg}) + } + + // 登录成功,种cookie + SetLoginCookie(ctx, user.Username) + + if user.Balance == 0 { + return ctx.Redirect(http.StatusSeeOther, "/balance") + } + + return ctx.Redirect(http.StatusSeeOther, "/") +} diff --git a/internal/http/controller/other.go b/internal/http/controller/other.go new file mode 100644 index 00000000..fea59eaa --- /dev/null +++ b/internal/http/controller/other.go @@ -0,0 +1,36 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "path" + + "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + + "github.com/studygolang/studygolang/util" +) + +// OtherController 有些页面只是前端,因此通过这个页面统一控制 +// 只需要创建模板文件就可以访问到 +type OtherController struct{} + +// RegisterRoute 注册路由 +func (self OtherController) RegisterRoute(g *echo.Group) { + g.GET("/*", self.Any) +} + +func (OtherController) Any(ctx echo.Context) error { + uri := ctx.Request().RequestURI + tplFile := uri + ".html" + if util.Exist(path.Clean(config.TemplateDir + tplFile)) { + return render(ctx, tplFile, nil) + } + + return echo.NewHTTPError(http.StatusNotFound) +} diff --git a/internal/http/controller/project.go b/internal/http/controller/project.go new file mode 100644 index 00000000..18a90860 --- /dev/null +++ b/internal/http/controller/project.go @@ -0,0 +1,169 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeProject, logic.ProjectComment{}) + logic.RegisterLikeObject(model.TypeProject, logic.ProjectLike{}) +} + +type ProjectController struct{} + +// 注册路由 +func (self ProjectController) RegisterRoute(g *echo.Group) { + g.GET("/projects", self.ReadList) + g.Match([]string{"GET", "POST"}, "/project/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/project/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + g.GET("/p/:uri", self.Detail) + g.GET("/project/uri", self.CheckExist) +} + +// ReadList 开源项目列表页 +func (ProjectController) ReadList(ctx echo.Context) error { + limit := 20 + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + paginator.SetPerPage(limit) + total := logic.DefaultProject.Count(context.EchoContext(ctx), "") + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + pageInfo := template.HTML(pageHtml) + + projects := logic.DefaultProject.FindAll(context.EchoContext(ctx), paginator, "id DESC", "status IN(?,?)", model.ProjectStatusNew, model.ProjectStatusOnline) + + num := len(projects) + if num == 0 { + return render(ctx, "projects/list.html", map[string]interface{}{"projects": projects, "activeProjects": "active"}) + } + + // 获取当前用户喜欢对象信息 + me, ok := ctx.Get("user").(*model.Me) + var likeFlags map[int]int + if ok { + likeFlags, _ = logic.DefaultLike.FindUserLikeObjects(context.EchoContext(ctx), me.Uid, model.TypeProject, projects[0].Id, projects[num-1].Id) + } + + return render(ctx, "projects/list.html", map[string]interface{}{"projects": projects, "activeProjects": "active", "page": pageInfo, "likeflags": likeFlags}) +} + +// Create 新建项目 +func (ProjectController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + name := ctx.FormValue("name") + // 请求新建项目页面 + if name == "" || ctx.Request().Method != "POST" { + project := &model.OpenProject{} + + data := map[string]interface{}{"project": project, "activeProjects": "active"} + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + + return render(ctx, "projects/new.html", data) + } + + forms, _ := ctx.FormParams() + err := logic.DefaultProject.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, nil) +} + +// Modify 修改项目 +func (ProjectController) Modify(ctx echo.Context) error { + id := goutils.MustInt(ctx.FormValue("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/projects") + } + + // 请求编辑项目页面 + if ctx.Request().Method != "POST" { + project := logic.DefaultProject.FindOne(context.EchoContext(ctx), id) + return render(ctx, "projects/new.html", map[string]interface{}{"project": project, "activeProjects": "active"}) + } + + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultProject.Publish(context.EchoContext(ctx), user, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return ctx.String(http.StatusForbidden, "没有权限") + } + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, nil) +} + +// Detail 项目详情 +func (ProjectController) Detail(ctx echo.Context) error { + project := logic.DefaultProject.FindOne(context.EchoContext(ctx), ctx.Param("uri")) + if project == nil || project.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/projects") + } + + data := map[string]interface{}{ + "activeProjects": "active", + "project": project, + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, project.Id, model.TypeProject) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, project.Id, model.TypeProject) + + logic.Views.Incr(Request(ctx), model.TypeProject, project.Id, me.Uid) + + if me.Uid != project.User.Uid { + go logic.DefaultViewRecord.Record(project.Id, model.TypeProject, me.Uid) + } + + if me.IsRoot || me.Uid == project.User.Uid { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), project.Id, model.TypeProject) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), project.Id, model.TypeProject) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeProject, project.Id) + } + + // 为了阅读数即时看到 + project.Viewnum++ + + return render(ctx, "projects/detail.html,common/comment.html", data) +} + +// CheckExist 检测 uri 对应的项目是否存在(验证,true表示不存在;false表示存在) +func (ProjectController) CheckExist(ctx echo.Context) error { + uri := ctx.QueryParam("uri") + if uri == "" { + return ctx.JSON(http.StatusOK, `true`) + } + + if logic.DefaultProject.UriExists(context.EchoContext(ctx), uri) { + return ctx.JSON(http.StatusOK, `false`) + } + return ctx.JSON(http.StatusOK, `true`) +} diff --git a/internal/http/controller/reading.go b/internal/http/controller/reading.go new file mode 100644 index 00000000..754a6f6f --- /dev/null +++ b/internal/http/controller/reading.go @@ -0,0 +1,82 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ReadingController struct{} + +// 注册路由 +func (self ReadingController) RegisterRoute(g *echo.Group) { + g.GET("/readings", self.ReadingList) + g.GET("/readings/:id", self.IReading) +} + +// ReadingList 晨读列表页 +func (ReadingController) ReadingList(ctx echo.Context) error { + limit := 20 + lastId := goutils.MustInt(ctx.QueryParam("lastid")) + rtype := goutils.MustInt(ctx.QueryParam("rtype"), model.RtypeGo) + + readings := logic.DefaultReading.FindBy(context.EchoContext(ctx), limit+5, rtype, lastId) + num := len(readings) + if num == 0 { + if lastId == 0 { + return render(ctx, "readings/list.html", map[string]interface{}{"activeReadings": "active", "readings": readings, "rtype": rtype}) + } else { + return ctx.Redirect(http.StatusSeeOther, "/readings") + } + } + + var ( + hasPrev, hasNext bool + prevId, nextId int + ) + + if lastId > 0 { + prevId = lastId + + // 避免因为项目下线,导致判断错误(所以 > 5) + if prevId-readings[0].Id > 5 { + hasPrev = false + } else { + prevId += limit + hasPrev = true + } + } + + if num > limit { + hasNext = true + readings = readings[:limit] + nextId = readings[limit-1].Id + } else { + nextId = readings[num-1].Id + } + + pageInfo := map[string]interface{}{ + "has_prev": hasPrev, + "prev_id": prevId, + "has_next": hasNext, + "next_id": nextId, + } + return render(ctx, "readings/list.html", map[string]interface{}{"activeReadings": "active", "readings": readings, "page": pageInfo, "rtype": rtype}) +} + +// IReading 点击 【我要晨读】,记录点击数,跳转 +func (ReadingController) IReading(ctx echo.Context) error { + uri := logic.DefaultReading.IReading(context.EchoContext(ctx), goutils.MustInt(ctx.Param("id"))) + return ctx.Redirect(http.StatusSeeOther, uri) +} diff --git a/internal/http/controller/resource.go b/internal/http/controller/resource.go new file mode 100644 index 00000000..857e845c --- /dev/null +++ b/internal/http/controller/resource.go @@ -0,0 +1,162 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeResource, logic.ResourceComment{}) + logic.RegisterLikeObject(model.TypeResource, logic.ResourceLike{}) +} + +type ResourceController struct{} + +// 注册路由 +func (self ResourceController) RegisterRoute(g *echo.Group) { + g.GET("/resources", self.ReadList) + g.GET("/resources/cat/:catid", self.ReadCatResources) + g.GET("/resources/:id", self.Detail) + g.Match([]string{"GET", "POST"}, "/resources/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/resources/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +// ReadList 资源索引页 +func (ResourceController) ReadList(ctx echo.Context) error { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") +} + +// ReadCatResources 某个分类的资源列表 +func (ResourceController) ReadCatResources(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + catid := goutils.MustInt(ctx.Param("catid")) + + resources, total := logic.DefaultResource.FindByCatid(context.EchoContext(ctx), paginator, catid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "resources/index.html", map[string]interface{}{"activeResources": "active", "resources": resources, "categories": logic.AllCategory, "page": template.HTML(pageHtml), "curCatid": catid}) +} + +// Detail 某个资源详细页 +func (ResourceController) Detail(ctx echo.Context) error { + id := goutils.MustInt(ctx.Param("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") + } + resource, comments := logic.DefaultResource.FindById(context.EchoContext(ctx), id) + if len(resource) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") + } + + data := map[string]interface{}{ + "activeResources": "active", + "resource": resource, + "comments": comments, + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + id := resource["id"].(int) + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, id, model.TypeResource) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, id, model.TypeResource) + + logic.Views.Incr(Request(ctx), model.TypeResource, id, me.Uid) + + if me.Uid != resource["uid"].(int) { + go logic.DefaultViewRecord.Record(id, model.TypeResource, me.Uid) + } + + if me.IsRoot || me.Uid == resource["uid"].(int) { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), id, model.TypeResource) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), id, model.TypeResource) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeResource, id) + } + + return render(ctx, "resources/detail.html,common/comment.html", data) +} + +// Create 发布新资源 +func (ResourceController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + // 请求新建资源页面 + if title == "" || ctx.Request().Method != "POST" { + data := map[string]interface{}{"activeResources": "active", "categories": logic.AllCategory} + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + return render(ctx, "resources/new.html", data) + } + + errMsg := "" + resForm := ctx.FormValue("form") + if resForm == model.LinkForm { + if ctx.FormValue("url") == "" { + errMsg = "url不能为空" + } + } else { + if ctx.FormValue("content") == "" { + errMsg = "内容不能为空" + } + } + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + + forms, _ := ctx.FormParams() + err := logic.DefaultResource.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 2, "内部服务错误,请稍候再试!") + } + + return success(ctx, nil) +} + +// Modify 修改資源 +func (ResourceController) Modify(ctx echo.Context) error { + id := goutils.MustInt(ctx.FormValue("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") + } + + // 请求编辑資源页面 + if ctx.Request().Method != "POST" { + resource := logic.DefaultResource.FindResource(context.EchoContext(ctx), id) + return render(ctx, "resources/new.html", map[string]interface{}{"resource": resource, "activeResources": "active", "categories": logic.AllCategory}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultResource.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return ctx.String(http.StatusForbidden, "没有权限修改") + } + return fail(ctx, 2, "内部服务错误,请稍候再试!") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/routes.go b/internal/http/controller/routes.go new file mode 100644 index 00000000..f764698a --- /dev/null +++ b/internal/http/controller/routes.go @@ -0,0 +1,49 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import echo "github.com/labstack/echo/v4" + +func RegisterRoutes(g *echo.Group) { + new(IndexController).RegisterRoute(g) + new(AccountController).RegisterRoute(g) + new(TopicController).RegisterRoute(g) + new(ArticleController).RegisterRoute(g) + new(ProjectController).RegisterRoute(g) + new(ResourceController).RegisterRoute(g) + new(ReadingController).RegisterRoute(g) + new(WikiController).RegisterRoute(g) + new(UserController).RegisterRoute(g) + new(LikeController).RegisterRoute(g) + new(FavoriteController).RegisterRoute(g) + new(MessageController).RegisterRoute(g) + new(SidebarController).RegisterRoute(g) + new(CommentController).RegisterRoute(g) + new(SearchController).RegisterRoute(g) + new(WideController).RegisterRoute(g) + new(ImageController).RegisterRoute(g) + new(CaptchaController).RegisterRoute(g) + new(BookController).RegisterRoute(g) + new(MissionController).RegisterRoute(g) + new(UserRichController).RegisterRoute(g) + new(TopController).RegisterRoute(g) + new(GiftController).RegisterRoute(g) + new(OAuthController).RegisterRoute(g) + new(WebsocketController).RegisterRoute(g) + new(DownloadController).RegisterRoute(g) + new(LinkController).RegisterRoute(g) + new(SubjectController).RegisterRoute(g) + new(GCTTController).RegisterRoute(g) + new(FeedController).RegisterRoute(g) + new(InterviewController).RegisterRoute(g) + + new(WechatController).RegisterRoute(g) + + new(InstallController).RegisterRoute(g) + + new(OtherController).RegisterRoute(g) +} diff --git a/internal/http/controller/search.go b/internal/http/controller/search.go new file mode 100644 index 00000000..d72768dc --- /dev/null +++ b/internal/http/controller/search.go @@ -0,0 +1,87 @@ +package controller + +import ( + "html" + "net/http" + "net/url" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type SearchController struct{} + +// 注册路由 +func (self SearchController) RegisterRoute(g *echo.Group) { + g.GET("/search", self.Search) + g.GET("/tag/:name", self.TagList) +} + +// Search +func (SearchController) Search(ctx echo.Context) error { + q := ctx.QueryParam("q") + field := ctx.QueryParam("f") + p := goutils.MustInt(ctx.QueryParam("p"), 1) + + rows := 50 + + respBody, err := logic.DefaultSearcher.DoSearch(q, field, (p-1)*rows, rows) + + data := map[string]interface{}{ + "respBody": respBody, + "q": q, + "f": field, + } + if err != nil { + return render(ctx, "500.html", nil) + } + uri := "/search?q=" + html.EscapeString(q) + "&f=" + field + "&" + paginator := logic.NewPaginatorWithPerPage(p, rows) + data["pageHtml"] = paginator.SetTotal(int64(respBody.NumFound)).GetPageHtml(uri) + + return render(ctx, "search.html", data) +} + +// TagList +func (SearchController) TagList(ctx echo.Context) error { + field := "tag" + p := goutils.MustInt(ctx.QueryParam("p"), 1) + q := ctx.Param("name") + if q == "" { + return render(ctx, "notfound", nil) + } + + var err error + q, err = url.QueryUnescape(q) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + // 过滤非法 tag + if len(q) > 9 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + rows := 50 + + respBody, err := logic.DefaultSearcher.DoSearch(q, field, (p-1)*rows, rows) + users, nodes := logic.DefaultSearcher.FillNodeAndUser(context.EchoContext(ctx), respBody) + + data := map[string]interface{}{ + "respBody": respBody, + "name": q, + "users": users, + "nodes": nodes, + } + if err != nil { + return render(ctx, "500.html", nil) + } + uri := "/tag/" + q + "?" + paginator := logic.NewPaginatorWithPerPage(p, rows) + data["pageHtml"] = paginator.SetTotal(int64(respBody.NumFound)).GetPageHtml(uri) + + return render(ctx, "feed/tag.html", data) +} diff --git a/internal/http/controller/sidebar.go b/internal/http/controller/sidebar.go new file mode 100644 index 00000000..9e30552e --- /dev/null +++ b/internal/http/controller/sidebar.go @@ -0,0 +1,191 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "strconv" + "time" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" + "github.com/polaris1119/times" +) + +// 侧边栏的内容通过异步请求获取 +type SidebarController struct{} + +func (self SidebarController) RegisterRoute(g *echo.Group) { + g.GET("/readings/recent", self.RecentReading) + g.GET("/topics/:nid/others", self.OtherTopics) + g.GET("/websites/stat", self.WebsiteStat) + g.GET("/dynamics/recent", self.RecentDynamic) + g.GET("/topics/recent", self.RecentTopic) + g.GET("/articles/recent", self.RecentArticle) + g.GET("/projects/recent", self.RecentProject) + g.GET("/resources/recent", self.RecentResource) + g.GET("/comments/recent", self.RecentComment) + g.GET("/nodes/hot", self.HotNodes) + g.GET("/users/active", self.ActiveUser) + g.GET("/users/newest", self.NewestUser) + g.GET("/friend/links", self.FriendLinks) + g.GET("/rank/view", self.ViewRank) +} + +// RecentReading 技术晨读 +func (SidebarController) RecentReading(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 7) + readings := logic.DefaultReading.FindBy(context.EchoContext(ctx), limit, model.RtypeGo) + if len(readings) == 1 { + // 首页,三天内的晨读才显示 + if time.Time(readings[0].Ctime).Before(time.Now().Add(-3 * 24 * time.Hour)) { + readings = nil + } + } + return success(ctx, readings) +} + +// OtherTopics 某节点下其他帖子 +func (SidebarController) OtherTopics(ctx echo.Context) error { + topics := logic.DefaultTopic.FindByNid(context.EchoContext(ctx), ctx.Param("nid"), ctx.QueryParam("tid")) + topics = logic.DefaultTopic.JSEscape(topics) + return success(ctx, topics) +} + +// WebsiteStat 网站统计信息 +func (SidebarController) WebsiteStat(ctx echo.Context) error { + articleTotal := logic.DefaultArticle.Total() + projectTotal := logic.DefaultProject.Total() + topicTotal := logic.DefaultTopic.Total() + cmtTotal := logic.DefaultComment.Total() + resourceTotal := logic.DefaultResource.Total() + bookTotal := logic.DefaultGoBook.Total() + userTotal := logic.DefaultUser.Total() + + data := map[string]interface{}{ + "article": articleTotal, + "project": projectTotal, + "topic": topicTotal, + "resource": resourceTotal, + "book": bookTotal, + "comment": cmtTotal, + "user": userTotal, + } + + return success(ctx, data) +} + +// RecentDynamic 社区最新公告或go最新动态 +func (SidebarController) RecentDynamic(ctx echo.Context) error { + dynamics := logic.DefaultDynamic.FindBy(context.EchoContext(ctx), 0, 3) + return success(ctx, dynamics) +} + +// RecentTopic 最新帖子 +func (SidebarController) RecentTopic(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + topicList := logic.DefaultTopic.FindRecent(limit) + return success(ctx, topicList) +} + +// RecentArticle 最新博文 +func (SidebarController) RecentArticle(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentArticles := logic.DefaultArticle.FindBy(context.EchoContext(ctx), limit) + return success(ctx, recentArticles) +} + +// RecentProject 最新开源项目 +func (SidebarController) RecentProject(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentProjects := logic.DefaultProject.FindBy(context.EchoContext(ctx), limit) + return success(ctx, recentProjects) +} + +// RecentResource 最新资源 +func (SidebarController) RecentResource(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentResources := logic.DefaultResource.FindBy(context.EchoContext(ctx), limit) + return success(ctx, recentResources) +} + +// RecentComment 最新评论 +func (SidebarController) RecentComment(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentComments := logic.DefaultComment.FindRecent(context.EchoContext(ctx), 0, -1, limit) + + uids := slices.StructsIntSlice(recentComments, "Uid") + users := logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + + result := map[string]interface{}{ + "comments": recentComments, + } + + // json encode 不支持 map[int]... + for uid, user := range users { + result[strconv.Itoa(uid)] = user + } + + return success(ctx, result) +} + +// HotNodes 社区热门节点 +func (SidebarController) HotNodes(ctx echo.Context) error { + nodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + return success(ctx, nodes) +} + +// ActiveUser 活跃会员 +func (SidebarController) ActiveUser(ctx echo.Context) error { + // activeUsers := logic.DefaultUser.FindActiveUsers(ctx, 9) + // return success(ctx, activeUsers) + activeUsers := logic.DefaultRank.FindDAURank(context.EchoContext(ctx), 9) + return success(ctx, activeUsers) +} + +// NewestUser 新加入会员 +func (SidebarController) NewestUser(ctx echo.Context) error { + newestUsers := logic.DefaultUser.FindNewUsers(context.EchoContext(ctx), 9) + return success(ctx, newestUsers) +} + +// FriendLinks 友情链接 +func (SidebarController) FriendLinks(ctx echo.Context) error { + friendLinks := logic.DefaultFriendLink.FindAll(context.EchoContext(ctx), 10) + return success(ctx, friendLinks) +} + +// ViewRank 阅读排行榜 +func (SidebarController) ViewRank(ctx echo.Context) error { + objtype := goutils.MustInt(ctx.QueryParam("objtype")) + rankType := ctx.QueryParam("rank_type") + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + + var result = map[string]interface{}{ + "objtype": objtype, + "rank_type": rankType, + } + switch rankType { + case "today": + result["list"] = logic.DefaultRank.FindDayRank(context.EchoContext(ctx), objtype, times.Format("ymd"), limit) + case "yesterday": + yesterday := time.Now().Add(-1 * 24 * time.Hour) + result["list"] = logic.DefaultRank.FindDayRank(context.EchoContext(ctx), objtype, times.Format("ymd", yesterday), limit) + case "week": + result["list"] = logic.DefaultRank.FindWeekRank(context.EchoContext(ctx), objtype, limit) + case "month": + result["list"] = logic.DefaultRank.FindMonthRank(context.EchoContext(ctx), objtype, limit) + } + + result["path"] = model.PathUrlMap[objtype] + + return success(ctx, result) +} diff --git a/internal/http/controller/subject.go b/internal/http/controller/subject.go new file mode 100644 index 00000000..60b40bc8 --- /dev/null +++ b/internal/http/controller/subject.go @@ -0,0 +1,207 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type SubjectController struct{} + +// 注册路由 +func (self SubjectController) RegisterRoute(g *echo.Group) { + g.GET("/subject/:id", self.Index) + g.POST("/subject/follow", self.Follow, middleware.NeedLogin()) + g.GET("/subject/my_articles", self.MyArticles, middleware.NeedLogin()) + g.POST("/subject/contribute", self.Contribute, middleware.NeedLogin()) + g.POST("/subject/remove_contribute", self.RemoveContribute, middleware.NeedLogin()) + g.GET("/subject/mine", self.Mine, middleware.NeedLogin()) + + g.Match([]string{"GET", "POST"}, "/subject/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice()) + g.Match([]string{"GET", "POST"}, "/subject/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +func (SubjectController) Index(ctx echo.Context) error { + id := goutils.MustInt(ctx.Param("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + subject := logic.DefaultSubject.FindOne(context.EchoContext(ctx), id) + if subject.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + if subject.Cover != "" && !strings.HasPrefix(subject.Cover, "http") { + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + subject.Cover = cdnDomain + subject.Cover + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + orderBy := ctx.QueryParam("order_by") + articles := logic.DefaultSubject.FindArticles(context.EchoContext(ctx), id, paginator, orderBy) + if orderBy == "" { + orderBy = "added_at" + } + + articleNum := logic.DefaultSubject.FindArticleTotal(context.EchoContext(ctx), id) + + pageHtml := paginator.SetTotal(articleNum).GetPageHtml(ctx.Request().URL.Path) + + followers := logic.DefaultSubject.FindFollowers(context.EchoContext(ctx), id) + followerNum := logic.DefaultSubject.FindFollowerTotal(context.EchoContext(ctx), id) + + // 是否已关注 + followed := false + me, ok := ctx.Get("user").(*model.Me) + if ok { + followed = logic.DefaultSubject.HadFollow(context.EchoContext(ctx), id, me) + } + + data := map[string]interface{}{ + "subject": subject, + "articles": articles, + "article_num": articleNum, + "followers": followers, + "follower_num": followerNum, + "order_by": orderBy, + "followed": followed, + "page": pageHtml, + } + + return render(ctx, "subject/index.html", data) +} + +func (self SubjectController) Follow(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + + me := ctx.Get("user").(*model.Me) + err := logic.DefaultSubject.Follow(context.EchoContext(ctx), sid, me) + if err != nil { + return fail(ctx, 1, "关注失败!") + } + + return success(ctx, nil) +} + +func (self SubjectController) MyArticles(ctx echo.Context) error { + kw := ctx.QueryParam("kw") + sid := goutils.MustInt(ctx.FormValue("sid")) + + me := ctx.Get("user").(*model.Me) + + articles := logic.DefaultArticle.SearchMyArticles(context.EchoContext(ctx), me, sid, kw) + + return success(ctx, map[string]interface{}{ + "articles": articles, + }) +} + +// Contribute 投稿 +func (self SubjectController) Contribute(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + articleId := goutils.MustInt(ctx.FormValue("article_id")) + + me := ctx.Get("user").(*model.Me) + + err := logic.DefaultSubject.Contribute(context.EchoContext(ctx), me, sid, articleId) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +// RemoveContribute 删除投稿 +func (self SubjectController) RemoveContribute(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + articleId := goutils.MustInt(ctx.FormValue("article_id")) + + err := logic.DefaultSubject.RemoveContribute(context.EchoContext(ctx), sid, articleId) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +// Mine 我管理的专栏 +func (self SubjectController) Mine(ctx echo.Context) error { + kw := ctx.QueryParam("kw") + articleId := goutils.MustInt(ctx.FormValue("article_id")) + me := ctx.Get("user").(*model.Me) + + subjects := logic.DefaultSubject.FindMine(context.EchoContext(ctx), me, articleId, kw) + + return success(ctx, map[string]interface{}{"subjects": subjects}) +} + +// Create 新建专栏 +func (SubjectController) Create(ctx echo.Context) error { + + name := ctx.FormValue("name") + // 请求新建专栏页面 + if name == "" || ctx.Request().Method != "POST" { + data := map[string]interface{}{} + return render(ctx, "subject/new.html", data) + } + + exist := logic.DefaultSubject.ExistByName(name) + if exist { + return fail(ctx, 1, "专栏已经存在 : "+name) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + sid, err := logic.DefaultSubject.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误:"+err.Error()) + } + + return success(ctx, map[string]interface{}{"sid": sid}) +} + +// Modify 修改专栏 +func (SubjectController) Modify(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + if sid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/subjects") + } + + if ctx.Request().Method != "POST" { + subject := logic.DefaultSubject.FindOne(context.EchoContext(ctx), sid) + if subject == nil { + return ctx.Redirect(http.StatusSeeOther, "/subjects") + } + + data := map[string]interface{}{ + "subject": subject, + } + + return render(ctx, "subject/new.html", data) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + _, err := logic.DefaultSubject.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 2, "服务错误,请稍后重试!") + } + return success(ctx, map[string]interface{}{"sid": sid}) +} diff --git a/internal/http/controller/top.go b/internal/http/controller/top.go new file mode 100644 index 00000000..469615a0 --- /dev/null +++ b/internal/http/controller/top.go @@ -0,0 +1,42 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/times" +) + +type TopController struct{} + +// 注册路由 +func (self TopController) RegisterRoute(g *echo.Group) { + g.GET("/top/dau", self.TopDAU) + g.GET("/top/rich", self.TopRich) +} + +func (TopController) TopDAU(ctx echo.Context) error { + data := map[string]interface{}{ + "today": times.Format("Ymd"), + } + + data["users"] = logic.DefaultRank.FindDAURank(context.EchoContext(ctx), 10) + data["active_num"] = logic.DefaultRank.TotalDAUUser(context.EchoContext(ctx)) + + return render(ctx, "top/dau.html", data) +} + +func (TopController) TopRich(ctx echo.Context) error { + data := map[string]interface{}{ + "users": logic.DefaultRank.FindRichRank(context.EchoContext(ctx)), + } + + return render(ctx, "top/rich.html", data) +} diff --git a/internal/http/controller/topic.go b/internal/http/controller/topic.go new file mode 100644 index 00000000..4c8897dc --- /dev/null +++ b/internal/http/controller/topic.go @@ -0,0 +1,365 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/dchest/captcha" + "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeTopic, logic.TopicComment{}) + logic.RegisterLikeObject(model.TypeTopic, logic.TopicLike{}) +} + +type TopicController struct{} + +// 注册路由 +func (self TopicController) RegisterRoute(g *echo.Group) { + g.GET("/topics", self.TopicList) + g.GET("/topics/no_reply", self.TopicsNoReply) + g.GET("/topics/last", self.TopicsLast) + g.GET("/topics/:tid", self.Detail) + g.GET("/topics/node/:nid", self.NodeTopics) + g.GET("/go/:node", self.GoNodeTopics) + g.GET("/nodes", self.Nodes) + + g.Match([]string{"GET", "POST"}, "/topics/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/topics/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + + g.POST("/topics/set_top", self.SetTop, middleware.NeedLogin()) + + g.Match([]string{"GET", "POST"}, "/append/topic/:tid", self.Append, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck()) +} + +func (self TopicController) TopicList(ctx echo.Context) error { + tab := ctx.QueryParam("tab") + if tab == "" { + tab = GetFromCookie(ctx, "TOPIC_TAB") + } else { + SetCookie(ctx, "TOPIC_TAB", tab) + } + + if tab != "" && tab != "all" { + nid := logic.GetNidByEname(tab) + if nid > 0 { + return self.topicList(ctx, tab, "topics.mtime DESC", "nid=? AND top!=1", nid) + } + } + + return self.topicList(ctx, "all", "topics.mtime DESC", "top!=1") +} + +func (self TopicController) Topics(ctx echo.Context) error { + return self.topicList(ctx, "", "topics.mtime DESC", "") +} + +func (self TopicController) TopicsNoReply(ctx echo.Context) error { + return self.topicList(ctx, "no_reply", "topics.mtime DESC", "lastreplyuid=?", 0) +} + +func (self TopicController) TopicsLast(ctx echo.Context) error { + return self.topicList(ctx, "last", "ctime DESC", "") +} + +func (TopicController) topicList(ctx echo.Context, tab, orderBy, querystring string, args ...interface{}) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + // 置顶的topic + topTopics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "ctime DESC", "top=1") + + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, orderBy, querystring, args...) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, args...) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "topics": append(topTopics, topics...), + "activeTopics": "active", + "nodes": logic.GenNodes(), + "tab": tab, + "tab_list": hotNodes, + "page": template.HTML(pageHtml), + } + + return render(ctx, "topics/list.html", data) +} + +// NodeTopics 某节点下的主题列表 +func (TopicController) NodeTopics(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring, nid := "nid=?", goutils.MustInt(ctx.Param("nid")) + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.mtime DESC", querystring, nid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, nid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + // 当前节点信息 + node := logic.GetNode(nid) + + return render(ctx, "topics/node.html", map[string]interface{}{"activeTopics": "active", "topics": topics, "page": template.HTML(pageHtml), "total": total, "node": node}) +} + +// GoNodeTopics 某节点下的主题列表,uri: /go/golang +func (TopicController) GoNodeTopics(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + ename := ctx.Param("node") + node := logic.GetNodeByEname(ename) + if node == nil { + return render(ctx, "notfound.html", nil) + } + + querystring, nid := "nid=?", node["nid"].(int) + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.mtime DESC", querystring, nid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, nid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "topics/node.html", map[string]interface{}{"activeTopics": "active", "topics": topics, "page": template.HTML(pageHtml), "total": total, "node": node}) +} + +// Detail 社区主题详细页 +func (TopicController) Detail(ctx echo.Context) error { + tid := goutils.MustInt(ctx.Param("tid")) + if tid == 0 { + return render(ctx, "notfound.html", nil) + } + + topic, replies, err := logic.DefaultTopic.FindByTid(context.EchoContext(ctx), tid) + if err != nil { + return render(ctx, "notfound.html", nil) + } + + data := map[string]interface{}{ + "activeTopics": "active", + "topic": topic, + "replies": replies, + "appends": []*model.TopicAppend{}, + "can_view": true, + } + + me, ok := ctx.Get("user").(*model.Me) + if topic["permission"] == model.PermissionOnlyMe { + if !ok || (topic["uid"].(int) != me.Uid && !me.IsRoot) { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + } else if topic["permission"] == model.PermissionPay { + // 当前用户是否对付费内容可见 + if !ok || (!me.IsVip && !me.IsRoot && topic["uid"].(int) != me.Uid) { + data["can_view"] = false + } + } + + if topic["permission"] == model.PermissionPublic || + (topic["permission"] == model.PermissionLogin && ok) || + (topic["permission"] == model.PermissionPay && ok && (me.IsVip || me.IsRoot)) { + data["appends"] = logic.DefaultTopic.FindAppend(context.EchoContext(ctx), tid) + } + + if ok { + tid := topic["tid"].(int) + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, tid, model.TypeTopic) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, tid, model.TypeTopic) + + logic.Views.Incr(Request(ctx), model.TypeTopic, tid, me.Uid) + + if me.Uid != topic["uid"].(int) { + go logic.DefaultViewRecord.Record(tid, model.TypeTopic, me.Uid) + } + + if me.IsRoot || me.Uid == topic["uid"].(int) { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), tid, model.TypeTopic) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), tid, model.TypeTopic) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeTopic, tid) + } + + return render(ctx, "topics/detail.html,common/comment.html", data) +} + +// Create 新建主题 +func (TopicController) Create(ctx echo.Context) error { + nid := goutils.MustInt(ctx.FormValue("nid")) + + me := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + // 请求新建主题页面 + if title == "" || ctx.Request().Method != "POST" { + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "activeTopics": "active", + "nid": nid, + "tab_list": hotNodes, + } + + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + + hadRecommend := false + if len(logic.AllRecommendNodes) > 0 { + hadRecommend = true + + data["nodes"] = logic.DefaultNode.FindAll(context.EchoContext(ctx)) + } else { + data["nodes"] = logic.GenNodes() + } + + data["had_recommend"] = hadRecommend + + return render(ctx, "topics/new.html", data) + } + + if nid == 0 { + return fail(ctx, 1, "没有选择节点!") + } + + forms, _ := ctx.FormParams() + tid, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 3, "内部服务错误:"+err.Error()) + } + + return success(ctx, map[string]interface{}{"tid": tid}) +} + +// Modify 修改主题 +func (TopicController) Modify(ctx echo.Context) error { + tid := goutils.MustInt(ctx.FormValue("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + if ctx.Request().Method != "POST" { + topics := logic.DefaultTopic.FindByTids([]int{tid}) + if len(topics) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "topic": topics[0], + "activeTopics": "active", + "tab_list": hotNodes, + } + + hadRecommend := false + if len(logic.AllRecommendNodes) > 0 { + hadRecommend = true + + data["nodes"] = logic.DefaultNode.FindAll(context.EchoContext(ctx)) + } else { + data["nodes"] = logic.GenNodes() + } + + data["had_recommend"] = hadRecommend + + return render(ctx, "topics/new.html", data) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + _, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return fail(ctx, 1, "没有权限操作") + } + + return fail(ctx, 2, "服务错误,请稍后重试!") + } + return success(ctx, map[string]interface{}{"tid": tid}) +} + +func (TopicController) Append(ctx echo.Context) error { + tid := goutils.MustInt(ctx.Param("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + topics := logic.DefaultTopic.FindByTids([]int{tid}) + if len(topics) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + topic := topics[0] + me := ctx.Get("user").(*model.Me) + if topic.Uid != me.Uid { + return ctx.Redirect(http.StatusSeeOther, "/topics/"+strconv.Itoa(tid)) + } + + // 请求新建主题页面 + if ctx.Request().Method != http.MethodPost { + data := map[string]interface{}{ + "topic": topic, + "activeTopics": "active", + } + + return render(ctx, "topics/append.html", data) + } + + content := ctx.FormValue("content") + err := logic.DefaultTopic.Append(context.EchoContext(ctx), me.Uid, tid, content) + if err != nil { + return fail(ctx, 1, "出错了:"+err.Error()) + } + + return success(ctx, nil) +} + +// Nodes 所有节点 +func (TopicController) Nodes(ctx echo.Context) error { + data := make(map[string]interface{}) + + if len(logic.AllRecommendNodes) > 0 { + data["nodes"] = logic.DefaultNode.FindAll(context.EchoContext(ctx)) + } else { + data["nodes"] = logic.GenNodes() + } + + return render(ctx, "topics/nodes.html", data) +} + +func (TopicController) SetTop(ctx echo.Context) error { + tid := goutils.MustInt(ctx.FormValue("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + me := ctx.Get("user").(*model.Me) + err := logic.DefaultTopic.SetTop(context.EchoContext(ctx), me, tid) + if err != nil { + if err == logic.NotFoundErr { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + return fail(ctx, 1, "出错了:"+err.Error()) + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/user.go b/internal/http/controller/user.go new file mode 100644 index 00000000..595bc8ce --- /dev/null +++ b/internal/http/controller/user.go @@ -0,0 +1,253 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" +) + +type UserController struct{} + +// 注册路由 +func (self UserController) RegisterRoute(g *echo.Group) { + g.GET("/user/:username", self.Home) + g.GET("/user/:username/topics", self.Topics) + g.GET("/user/:username/articles", self.Articles) + g.GET("/user/:username/resources", self.Resources) + g.GET("/user/:username/projects", self.Projects) + g.GET("/user/:username/comments", self.Comments) + g.GET("/users", self.ReadList) + g.Match([]string{"GET", "POST"}, "/user/email/unsubscribe", self.EmailUnsub) +} + +// Home 用户个人首页 +func (UserController) Home(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 || user.Status == model.UserStatusOutage { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + user.Weight = logic.DefaultRank.UserDAURank(context.EchoContext(ctx), user.Uid) + + topics := logic.DefaultTopic.FindRecent(5, user.Uid) + + articles := logic.DefaultArticle.FindByUser(context.EchoContext(ctx), user.Username, 5) + + resources := logic.DefaultResource.FindRecent(context.EchoContext(ctx), user.Uid) + for _, resource := range resources { + resource.CatName = logic.GetCategoryName(resource.Catid) + } + + projects := logic.DefaultProject.FindRecent(context.EchoContext(ctx), user.Username) + comments := logic.DefaultComment.FindRecent(context.EchoContext(ctx), user.Uid, -1, 5) + + user.IsOnline = logic.Book.RegUserIsOnline(user.Uid) + + return render(ctx, "user/profile.html", map[string]interface{}{ + "activeUsers": "active", + "topics": topics, + "articles": articles, + "resources": resources, + "projects": projects, + "comments": comments, + "user": user, + }) +} + +// ReadList 会员列表 +func (UserController) ReadList(ctx echo.Context) error { + // 获取活跃会员 + // activeUsers := logic.DefaultUser.FindActiveUsers(ctx, 36) + activeUsers := logic.DefaultRank.FindDAURank(context.EchoContext(ctx), 36) + // 获取最新加入会员 + newUsers := logic.DefaultUser.FindNewUsers(context.EchoContext(ctx), 36) + // 获取会员总数 + total := logic.DefaultUser.Total() + + return render(ctx, "user/users.html", map[string]interface{}{"activeUsers": "active", "actives": activeUsers, "news": newUsers, "total": total}) +} + +// EmailUnsub 邮件订阅/退订页面 +func (UserController) EmailUnsub(ctx echo.Context) error { + token := ctx.FormValue("u") + if token == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + // 校验 token 的合法性 + email := ctx.FormValue("email") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "email", email) + if user.Email == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + realToken := logic.DefaultEmail.GenUnsubscribeToken(user) + if token != realToken { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + if ctx.Request().Method != "POST" { + data := map[string]interface{}{ + "email": email, + "token": token, + "unsubscribe": user.Unsubscribe, + } + + return render(ctx, "user/email_unsub.html", data) + } + + logic.DefaultUser.EmailSubscribe(context.EchoContext(ctx), user.Uid, goutils.MustInt(ctx.FormValue("unsubscribe"))) + + return success(ctx, nil) +} + +func (UserController) Topics(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "uid=?" + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.tid DESC", querystring, user.Uid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, user.Uid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/topics.html", map[string]interface{}{ + "user": user, + "activeTopics": "active", + "topics": topics, + "page": template.HTML(pageHtml), + "total": total, + }) +} + +func (UserController) Articles(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "author_txt=?" + articles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", querystring, user.Username) + total := logic.DefaultArticle.Count(context.EchoContext(ctx), querystring, user.Username) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/articles.html", map[string]interface{}{ + "user": user, + "activeArticles": "active", + "articles": articles, + "page": template.HTML(pageHtml), + "total": total, + }) +} + +func (UserController) Resources(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "uid=?" + resources, total := logic.DefaultResource.FindAll(context.EchoContext(ctx), paginator, "resource.id DESC", querystring, user.Uid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/resources.html", map[string]interface{}{ + "user": user, + "activeResources": "active", + "resources": resources, + "page": template.HTML(pageHtml), + "total": total, + }) +} + +func (UserController) Projects(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "username=?" + projects := logic.DefaultProject.FindAll(context.EchoContext(ctx), paginator, "id DESC", querystring, user.Username) + total := logic.DefaultProject.Count(context.EchoContext(ctx), querystring, user.Username) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/projects.html", map[string]interface{}{ + "user": user, + "activeProjects": "active", + "projects": projects, + "page": template.HTML(pageHtml), + "total": total, + }) +} +func (UserController) Comments(ctx echo.Context) error { + + username := ctx.Param("username") + + userid := 0 + querystring := "" + + if username != "0" { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + querystring = "uid=?" + userid = user.Uid + username = user.Username + } else { + username = "" + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + comments := logic.DefaultComment.FindAll(context.EchoContext(ctx), paginator, "cid DESC", querystring, userid) + + total := logic.DefaultComment.Count(context.EchoContext(ctx), querystring, userid) + + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + data := map[string]interface{}{ + "comments": comments, + "page": template.HTML(pageHtml), + "total": total, + } + + if username == "" { + uids := slices.StructsIntSlice(comments, "Uid") + data["users"] = logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + } + + return render(ctx, "user/comments.html", data) + +} diff --git a/internal/http/controller/websocket.go b/internal/http/controller/websocket.go new file mode 100644 index 00000000..cd3f59d5 --- /dev/null +++ b/internal/http/controller/websocket.go @@ -0,0 +1,86 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "sync/atomic" + "time" + + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "golang.org/x/net/websocket" +) + +type WebsocketController struct { + ServerId uint32 +} + +func (this *WebsocketController) RegisterRoute(g *echo.Group) { + g.GET("/ws", echo.WrapHandler(websocket.Handler(this.Ws))) +} + +// websocket,统计在线用户数 +// uri: /ws +func (this *WebsocketController) Ws(wsConn *websocket.Conn) { + defer wsConn.Close() + + serverId := int(atomic.AddUint32(&this.ServerId, 1)) + + isUid := true + req := wsConn.Request() + user := goutils.MustInt(req.FormValue("uid")) + if user == 0 { + user = int(goutils.Ip2long(goutils.RemoteIp(req))) + isUid = false + } + userData := logic.Book.AddUser(user, serverId, isUid) + // 给自己发送消息,告诉当前在线用户数、历史最高在线人数 + onlineInfo := map[string]int{"online": logic.Book.Len(), "maxonline": logic.MaxOnlineNum()} + message := logic.NewMessage(logic.WsMsgOnline, onlineInfo) + err := websocket.JSON.Send(wsConn, message) + if err != nil { + logger.Errorln("Sending onlineusers error:", err) + return + } + + messageChan := userData.MessageQueue(serverId) + + ticker := time.NewTicker(15e9) + defer ticker.Stop() + + var clientClosed = false + for { + select { + case message := <-messageChan: + if err := websocket.JSON.Send(wsConn, message); err != nil { + // logger.Errorln("Send message", message, " to user:", user, "server_id:", serverId, "error:", err) + clientClosed = true + } + // 心跳 + case <-ticker.C: + if err := websocket.JSON.Send(wsConn, ""); err != nil { + // logger.Errorln("Send heart message to user:", user, "server_id:", serverId, "error:", err) + clientClosed = true + } + } + if clientClosed { + logic.Book.DelUser(user, serverId, isUid) + logger.Infoln("user:", user, "client close") + break + } + } + // 用户退出时需要变更其他用户看到的在线用户数 + if !logic.Book.UserIsOnline(user) { + logger.Infoln("user:", user, "had leave") + + message := logic.NewMessage(logic.WsMsgOnline, map[string]int{"online": logic.Book.Len()}) + go logic.Book.BroadcastAllUsersMessage(message) + } +} diff --git a/internal/http/controller/wechat.go b/internal/http/controller/wechat.go new file mode 100644 index 00000000..4e80d6fa --- /dev/null +++ b/internal/http/controller/wechat.go @@ -0,0 +1,68 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "io/ioutil" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type WechatController struct{} + +// 注册路由 +func (self WechatController) RegisterRoute(g *echo.Group) { + g.Any("/wechat/autoreply", self.AutoReply) + g.POST("/wechat/bind", self.Bind) +} + +func (self WechatController) AutoReply(ctx echo.Context) error { + // 配置微信(不校验,直接返回成功) + if ctx.QueryParam("echostr") != "" { + return ctx.String(http.StatusOK, ctx.QueryParam("echostr")) + } + + body, err := ioutil.ReadAll(ctx.Request().Body) + if err != nil { + return ctx.String(http.StatusOK, "") + } + + if len(body) == 0 { + return ctx.String(http.StatusOK, "") + } + + wechatReply, err := logic.DefaultWechat.AutoReply(context.EchoContext(ctx), body) + if err != nil { + return ctx.String(http.StatusOK, "") + } + + return ctx.XML(http.StatusOK, wechatReply) +} + +func (self WechatController) Bind(ctx echo.Context) error { + captcha := ctx.FormValue("captcha") + if captcha == "" { + return fail(ctx, 1, "验证码是不能空") + } + + echoCtx := context.EchoContext(ctx) + me, ok := ctx.Get("user").(*model.Me) + if !ok { + return fail(ctx, 1, "必须先登录") + } + err := logic.DefaultWechat.CheckCaptchaAndBind(echoCtx, me, captcha) + if err != nil { + return fail(ctx, 2, "验证码错误,请确认获取了或没填错!") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/wide.go b/internal/http/controller/wide.go new file mode 100644 index 00000000..a669b29e --- /dev/null +++ b/internal/http/controller/wide.go @@ -0,0 +1,21 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import echo "github.com/labstack/echo/v4" + +type WideController struct{} + +// 注册路由 +func (self WideController) RegisterRoute(g *echo.Group) { + g.GET("/wide/playground", self.Playground) +} + +// Playground Wide 的内嵌 iframe 的 playground +func (WideController) Playground(ctx echo.Context) error { + return render(ctx, "wide/playground.html", nil) +} diff --git a/internal/http/controller/wiki.go b/internal/http/controller/wiki.go new file mode 100644 index 00000000..35c9d1dc --- /dev/null +++ b/internal/http/controller/wiki.go @@ -0,0 +1,165 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + // logic.RegisterCommentObject(model.TypeArticle, logic.ArticleComment{}) + // logic.RegisterLikeObject(model.TypeArticle, logic.ArticleLike{}) +} + +type WikiController struct{} + +// 注册路由 +func (self WikiController) RegisterRoute(g *echo.Group) { + g.Match([]string{"GET", "POST"}, "/wiki/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck()) + g.Match([]string{"GET", "POST"}, "/wiki/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + g.GET("/wiki", self.ReadList) + g.GET("/wiki/:uri", self.Detail) +} + +// Create 创建wiki页 +func (WikiController) Create(ctx echo.Context) error { + title := ctx.FormValue("title") + // 请求新建 wiki 页面 + if title == "" || ctx.Request().Method != "POST" { + return render(ctx, "wiki/new.html", map[string]interface{}{"activeWiki": "active"}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultWiki.Create(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误") + } + + return success(ctx, nil) +} + +// Modify 修改 Wiki 页 +func (WikiController) Modify(ctx echo.Context) error { + id := goutils.MustInt(ctx.FormValue("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + if ctx.Request().Method != "POST" { + wiki := logic.DefaultWiki.FindById(context.EchoContext(ctx), id) + if wiki.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + return render(ctx, "wiki/new.html", map[string]interface{}{"activeWiki": "active", "wiki": wiki}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultWiki.Modify(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误") + } + + return success(ctx, nil) +} + +// Detail 展示wiki页 +func (WikiController) Detail(ctx echo.Context) error { + wiki := logic.DefaultWiki.FindOne(context.EchoContext(ctx), ctx.Param("uri")) + if wiki == nil { + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + // likeFlag := 0 + me, ok := ctx.Get("user").(*model.Me) + if ok { + // likeFlag = logic.DefaultLike.HadLike(ctx, me.Uid, wiki.Id, model.TypeWiki) + logic.Views.Incr(Request(ctx), model.TypeWiki, wiki.Id, me.Uid) + } else { + logic.Views.Incr(Request(ctx), model.TypeWiki, wiki.Id) + } + + // 为了阅读数即时看到 + wiki.Viewnum++ + + return render(ctx, "wiki/content.html", map[string]interface{}{"activeWiki": "active", "wiki": wiki}) +} + +// ReadList 获得wiki列表 +func (WikiController) ReadList(ctx echo.Context) error { + limit := 20 + + lastId := goutils.MustInt(ctx.QueryParam("lastid")) + wikis := logic.DefaultWiki.FindBy(context.EchoContext(ctx), limit+5, lastId) + if wikis == nil { + logger.Errorln("wiki controller: find wikis error") + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + num := len(wikis) + if num == 0 { + if lastId == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + var ( + hasPrev, hasNext bool + prevId, nextId int + ) + + if lastId != 0 { + prevId = lastId + + // 避免因为wiki下线,导致判断错误(所以 > 5) + if prevId-wikis[0].Id > 5 { + hasPrev = false + } else { + prevId += limit + hasPrev = true + } + } + + if num > limit { + hasNext = true + wikis = wikis[:limit] + nextId = wikis[limit-1].Id + } else { + nextId = wikis[num-1].Id + } + + pageInfo := map[string]interface{}{ + "has_prev": hasPrev, + "prev_id": prevId, + "has_next": hasNext, + "next_id": nextId, + } + + // 获取当前用户喜欢对象信息 + // me, ok := ctx.Get("user").(*model.Me) + // var likeFlags map[int]int + // if ok { + // likeFlags, _ = logic.DefaultLike.FindUserLikeObjects(ctx, me.Uid, model.TypeWiki, wikis[0].Id, nextId) + // } + + return render(ctx, "wiki/list.html", map[string]interface{}{"wikis": wikis, "activeWiki": "active", "page": pageInfo}) +} diff --git a/internal/http/http.go b/internal/http/http.go new file mode 100644 index 00000000..0b17c568 --- /dev/null +++ b/internal/http/http.go @@ -0,0 +1,479 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package http + +import ( + "bytes" + "encoding/json" + "html/template" + "math" + "math/rand" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/gorilla/sessions" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/times" +) + +var Store = sessions.NewCookieStore([]byte(config.ConfigFile.MustValue("global", "cookie_secret"))) + +func SetLoginCookie(ctx echo.Context, username string) { + Store.Options.HttpOnly = true + + session := GetCookieSession(ctx) + if ctx.FormValue("remember_me") != "1" { + // 浏览器关闭,cookie删除,否则保存30天(github.com/gorilla/sessions 包的默认值) + session.Options = &sessions.Options{ + Path: "/", + HttpOnly: true, + } + } + session.Values["username"] = username + req := Request(ctx) + resp := ResponseWriter(ctx) + session.Save(req, resp) +} + +func SetCookie(ctx echo.Context, key, value string) { + Store.Options.HttpOnly = true + + session := GetCookieSession(ctx) + session.Values[key] = value + req := Request(ctx) + resp := ResponseWriter(ctx) + session.Save(req, resp) +} + +func GetFromCookie(ctx echo.Context, key string) string { + session := GetCookieSession(ctx) + val, ok := session.Values[key] + if ok { + return val.(string) + } + return "" +} + +// 必须是 http.Request +func GetCookieSession(ctx echo.Context) *sessions.Session { + session, _ := Store.Get(Request(ctx), "user") + return session +} + +func Request(ctx echo.Context) *http.Request { + return ctx.Request() +} + +func ResponseWriter(ctx echo.Context) http.ResponseWriter { + return ctx.Response() +} + +// 自定义模板函数 +var funcMap = template.FuncMap{ + // 获取gravatar头像 + "gravatar": util.Gravatar, + // 转为前端显示需要的时间格式 + "formatTime": func(i interface{}) string { + ctime, ok := i.(string) + if !ok { + return "" + } + t, _ := time.Parse("2006-01-02 15:04:05", ctime) + return t.Format(time.RFC3339) + "+08:00" + }, + "format": func(i interface{}, format string) string { + switch i.(type) { + case time.Time: + return (i.(time.Time)).Format(format) + case int64: + val := i.(int64) + return time.Unix(val, 0).Format(format) + } + + return "" + }, + "hasPrefix": func(s, prefix string) bool { + if strings.HasPrefix(s, prefix) { + return true + } + return false + }, + "substring": util.Substring, + "add": func(nums ...interface{}) int { + total := 0 + for _, num := range nums { + if n, ok := num.(int); ok { + total += n + } + } + return total + }, + "sub": func(num1, num2 int) int { + return num1 - num2 + }, + "mod": func(num1, num2 int) int { + if num1 == 0 { + num1 = rand.Intn(500) + } + + return num1 % num2 + }, + "divide": func(num1, num2 int) int { + return int(math.Ceil(float64(num1) / float64(num2))) + }, + "explode": func(s, sep string) []string { + return strings.Split(s, sep) + }, + "noescape": func(s string) template.HTML { + return template.HTML(s) + }, + "timestamp": func(ts ...time.Time) int64 { + if len(ts) > 0 { + return ts[0].Unix() + } + return time.Now().Unix() + }, + "distanceDay": func(i interface{}) int { + var ( + t time.Time + err error + ) + switch val := i.(type) { + case string: + t, err = time.ParseInLocation("2006-01-02 15:04:05", val, time.Local) + if err != nil { + return 0 + } + case time.Time: + t = val + case model.OftenTime: + t = time.Time(val) + } + + return int(time.Now().Sub(t).Hours() / 24) + }, + "canEdit": logic.CanEdit, + "canPublish": logic.CanPublish, + "parseJSON": func(str string) map[string]interface{} { + result := make(map[string]interface{}) + json.Unmarshal([]byte(str), &result) + return result + }, + "safeHtml": util.SafeHtml, + "imageUrl": func(uri string, isHttps bool) string { + if !strings.HasPrefix(uri, "http") { + cdnDomain := global.App.CanonicalCDN(isHttps) + return cdnDomain + uri + } + + return uri + }, + "genList": func(n int, steps ...int) []int { + step := 1 + if len(steps) > 0 { + step = steps[0] + } + num := int(math.Ceil(float64(n) / float64(step))) + nums := make([]int, num) + for i := 0; i < num; i++ { + nums[i] = i + 1 + } + + return nums + }, +} + +func tplInclude(file string, dot map[string]interface{}) template.HTML { + var buffer = &bytes.Buffer{} + tpl, err := template.New(filepath.Base(file)).Funcs(funcMap).ParseFiles(config.TemplateDir + file) + // tpl, err := template.ParseFiles(config.TemplateDir + file) + if err != nil { + logger.Errorf("parse template file(%s) error:%v\n", file, err) + return "" + } + err = tpl.Execute(buffer, dot) + if err != nil { + logger.Errorf("template file(%s) syntax error:%v", file, err) + return "" + } + return template.HTML(buffer.String()) +} + +const ( + LayoutTpl = "common/layout.html" + AdminLayoutTpl = "common.html" +) + +// 是否访问这些页面 +var filterPathes = map[string]struct{}{ + "/account/login": {}, + "/account/register": {}, + "/account/forgetpwd": {}, + "/account/resetpwd": {}, + "/topics/new": {}, + "/topics/modify": {}, + "/resources/new": {}, + "/resources/modify": {}, + "/articles/new": {}, + "/articles/modify": {}, + "/project/new": {}, + "/project/modify": {}, + "/book/new": {}, + "/wiki/new": {}, + "/wiki/modify": {}, +} + +// Render html 输出 +func Render(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + if data == nil { + data = map[string]interface{}{} + } + + objLog := logic.GetLogger(context.EchoContext(ctx)) + + contentTpl = LayoutTpl + "," + contentTpl + // 为了使用自定义的模板函数,首先New一个以第一个模板文件名为模板名。 + // 这样,在ParseFiles时,新返回的*Template便还是原来的模板实例 + htmlFiles := strings.Split(contentTpl, ",") + for i, contentTpl := range htmlFiles { + htmlFiles[i] = config.TemplateDir + contentTpl + } + tpl, err := template.New("layout.html").Funcs(funcMap). + Funcs(template.FuncMap{"include": tplInclude}).ParseFiles(htmlFiles...) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", Request(ctx).RequestURI, err) + return err + } + + if strings.Contains(ctx.Request().UserAgent(), "miniProgram") { + data["min_program"] = true + } else { + data["pos_ad"] = logic.DefaultAd.FindAll(context.EchoContext(ctx), ctx.Path()) + } + + data["cur_time"] = times.Format("Y-m-d H:i:s") + data["path"] = ctx.Path() + data["filter"] = false + if _, ok := filterPathes[ctx.Path()]; ok { + data["filter"] = true + } + + // TODO:每次查询有点影响性能 + hasLoginMisson := false + me, ok := ctx.Get("user").(*model.Me) + if ok { + // 每日登录奖励 + hasLoginMisson = logic.DefaultMission.HasLoginMission(context.EchoContext(ctx), me) + } + data["has_login_misson"] = hasLoginMisson + + return executeTpl(ctx, tpl, data) +} + +// RenderAdmin html 输出 +func RenderAdmin(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + if data == nil { + data = map[string]interface{}{} + } + + objLog := logic.GetLogger(context.EchoContext(ctx)) + + contentTpl = AdminLayoutTpl + "," + contentTpl + // 为了使用自定义的模板函数,首先New一个以第一个模板文件名为模板名。 + // 这样,在ParseFiles时,新返回的*Template便还是原来的模板实例 + htmlFiles := strings.Split(contentTpl, ",") + for i, contentTpl := range htmlFiles { + htmlFiles[i] = config.TemplateDir + "admin/" + contentTpl + } + + requestURI := Request(ctx).RequestURI + tpl, err := template.New("common.html").Funcs(funcMap).ParseFiles(htmlFiles...) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", requestURI, err) + return err + } + + // 当前用户信息 + curUser := ctx.Get("user").(*model.Me) + + if menu1, menu2, curMenu1 := logic.DefaultAuthority.GetUserMenu(context.EchoContext(ctx), curUser, requestURI); menu2 != nil { + data["menu1"] = menu1 + data["menu2"] = menu2 + data["uri"] = requestURI + data["cur_menu1"] = curMenu1 + } + + return executeTpl(ctx, tpl, data) +} + +// 后台 query 查询返回结果 +func RenderQuery(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + objLog := logic.GetLogger(context.EchoContext(ctx)) + + contentTpl = "common_query.html," + contentTpl + contentTpls := strings.Split(contentTpl, ",") + for i, contentTpl := range contentTpls { + contentTpls[i] = config.TemplateDir + "admin/" + strings.TrimSpace(contentTpl) + } + + requestURI := Request(ctx).RequestURI + tpl, err := template.New("common_query.html").Funcs(funcMap).ParseFiles(contentTpls...) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", requestURI, err) + return err + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, data) + if err != nil { + objLog.Errorf("执行模板出错(Execute):[%q] %s\n", requestURI, err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} + +func executeTpl(ctx echo.Context, tpl *template.Template, data map[string]interface{}) error { + objLog := logic.GetLogger(context.EchoContext(ctx)) + + // 如果没有定义css和js模板,则定义之 + if jsTpl := tpl.Lookup("js"); jsTpl == nil { + tpl.Parse(`{{define "js"}}{{end}}`) + } + if cssTpl := tpl.Lookup("css"); cssTpl == nil { + tpl.Parse(`{{define "css"}}{{end}}`) + } + // 如果没有 seo 模板,则定义之 + if seoTpl := tpl.Lookup("seo"); seoTpl == nil { + tpl.Parse(`{{define "seo"}} + + + {{end}}`) + } + + // 当前用户信息 + curUser, ok := ctx.Get("user").(*model.Me) + if ok { + data["me"] = curUser + } else { + data["me"] = &model.Me{} + } + + // websocket主机 + if global.OnlineEnv() { + data["wshost"] = global.App.Domain + } else { + data["wshost"] = global.App.Host + ":" + global.App.Port + } + global.App.SetUptime() + global.App.SetCopyright() + + isHttps := CheckIsHttps(ctx) + cdnDomain := global.App.CanonicalCDN(isHttps) + if isHttps { + global.App.BaseURL = "https://" + global.App.Domain + "/" + } else { + global.App.BaseURL = "http://" + global.App.Domain + "/" + } + + staticDomain := "" + if global.OnlineEnv() { + staticDomain = strings.TrimRight(cdnDomain, "/") + } + + data["app"] = global.App + data["is_https"] = isHttps + data["cdn_domain"] = cdnDomain + data["static_domain"] = staticDomain + data["is_pro"] = global.OnlineEnv() + + data["online_users"] = map[string]int{"online": logic.Book.Len(), "maxonline": logic.MaxOnlineNum()} + + data["setting"] = logic.WebsiteSetting + + // 评论每页显示多少个 + data["cmt_per_num"] = logic.CommentPerNum + + // 记录处理时间 + data["resp_time"] = time.Since(ctx.Get("req_start_time").(time.Time)) + + buf := new(bytes.Buffer) + err := tpl.Execute(buf, data) + if err != nil { + objLog.Errorln("excute template error:", err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} + +func CheckIsHttps(ctx echo.Context) bool { + isHttps := goutils.MustBool(ctx.Request().Header.Get("X-Https")) + if logic.WebsiteSetting.OnlyHttps { + isHttps = true + } + + return isHttps +} + +///////////////////////////////// APP 相关 ////////////////////////////// + +const ( + TokenSalt = "b3%JFOykZx_golang_polaris" + NeedReLoginCode = 600 +) + +func ParseToken(token string) (int, bool) { + if len(token) < 32 { + return 0, false + } + + pos := strings.LastIndex(token, "uid") + if pos == -1 { + return 0, false + } + return goutils.MustInt(token[pos+3:]), true +} + +func ValidateToken(token string) bool { + _, ok := ParseToken(token) + if !ok { + return false + } + + expireTime := time.Unix(goutils.MustInt64(token[:10]), 0) + if time.Now().Before(expireTime) { + return true + } + return false +} + +func GenToken(uid int) string { + expireTime := time.Now().Add(30 * 24 * time.Hour).Unix() + + buffer := goutils.NewBuffer().Append(expireTime).Append(uid).Append(TokenSalt) + + md5 := goutils.Md5(buffer.String()) + + buffer = goutils.NewBuffer().Append(expireTime).Append(md5).Append("uid").Append(uid) + return buffer.String() +} + +func AccessControl(ctx echo.Context) { + ctx.Response().Header().Add("Access-Control-Allow-Origin", "*") +} diff --git a/internal/http/internal/helper/account.go b/internal/http/internal/helper/account.go new file mode 100644 index 00000000..3a5d9ac7 --- /dev/null +++ b/internal/http/internal/helper/account.go @@ -0,0 +1,56 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package helper + +import ( + "sync" + + "github.com/polaris1119/logger" + guuid "github.com/twinj/uuid" +) + +// 保存uuid和email的对应关系(TODO:重启如何处理,有效期问题) +type regActivateCode struct { + data map[string]string + locker sync.RWMutex +} + +var RegActivateCode = ®ActivateCode{ + data: map[string]string{}, +} + +func (this *regActivateCode) GenUUID(email string) string { + this.locker.Lock() + defer this.locker.Unlock() + var uuid string + for { + uuid = guuid.NewV4().String() + if _, ok := this.data[uuid]; !ok { + this.data[uuid] = email + break + } + logger.Errorln("GenUUID 冲突....") + } + return uuid +} + +func (this *regActivateCode) GetEmail(uuid string) (email string, ok bool) { + this.locker.RLock() + defer this.locker.RUnlock() + + if email, ok = this.data[uuid]; ok { + return + } + return +} + +func (this *regActivateCode) DelUUID(uuid string) { + this.locker.Lock() + defer this.locker.Unlock() + + delete(this.data, uuid) +} diff --git a/internal/http/middleware/admin.go b/internal/http/middleware/admin.go new file mode 100644 index 00000000..5686c1da --- /dev/null +++ b/internal/http/middleware/admin.go @@ -0,0 +1,33 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +// AdminAuth 用于 echo 框架的判断用户是否有管理后台权限 +func AdminAuth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + if !user.IsAdmin { + return ctx.HTML(http.StatusForbidden, `403 Forbidden`) + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/balance_check.go b/internal/http/middleware/balance_check.go new file mode 100644 index 00000000..0d449ff2 --- /dev/null +++ b/internal/http/middleware/balance_check.go @@ -0,0 +1,49 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + echo "github.com/labstack/echo/v4" +) + +// BalanceCheck 用于 echo 框架,用户发布内容校验余额是否足够 +func BalanceCheck() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + + if util.IsAjax(ctx) { + + curUser := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + content := ctx.FormValue("content") + if ctx.Request().Method == "POST" && (title != "" || content != "") { + if ctx.Path() == "/comment/:objid" { + if curUser.Balance < 5 { + return ctx.String(http.StatusOK, `{"ok":0,"error":"对不起,您的账号余额不足,可以领取初始资本!"}`) + } + } else { + if curUser.Balance < 20 { + return ctx.String(http.StatusOK, `{"ok":0,"error":"对不起,您的账号余额不足,可以领取初始资本!"}`) + } + } + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/captcha.go b/internal/http/middleware/captcha.go new file mode 100644 index 00000000..a659ea89 --- /dev/null +++ b/internal/http/middleware/captcha.go @@ -0,0 +1,44 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" +) + +// CheckCaptcha 用于 echo 框架校验发布验证码 +func CheckCaptcha() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + + curUser := ctx.Get("user").(*model.Me) + + if ctx.Request().Method == "POST" { + if logic.NeedCaptcha(curUser) { + captchaId := ctx.FormValue("captchaid") + if !captcha.VerifyString(captchaId, ctx.FormValue("captchaSolution")) { + util.SetCaptcha(captchaId) + return ctx.String(http.StatusOK, `{"ok":0,"error":"验证码错误,记得刷新验证码!"}`) + } + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/http_error.go b/internal/http/middleware/http_error.go new file mode 100644 index 00000000..99dda9ab --- /dev/null +++ b/internal/http/middleware/http_error.go @@ -0,0 +1,51 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/util" + + echo "github.com/labstack/echo/v4" +) + +// HTTPError 用于 echo 框架的 HTTP 错误 +func HTTPError() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + if err := next(ctx); err != nil { + + if !ctx.Response().Committed { + if he, ok := err.(*echo.HTTPError); ok { + switch he.Code { + case http.StatusNotFound: + if util.IsAjax(ctx) { + return ctx.String(http.StatusOK, `{"ok":0,"error":"接口不存在"}`) + } + return Render(ctx, "404.html", nil) + case http.StatusForbidden: + if util.IsAjax(ctx) { + return ctx.String(http.StatusOK, `{"ok":0,"error":"没有权限访问"}`) + } + return Render(ctx, "403.html", map[string]interface{}{"msg": he.Message}) + case http.StatusInternalServerError: + if util.IsAjax(ctx) { + return ctx.String(http.StatusOK, `{"ok":0,"error":"接口服务器错误"}`) + } + return Render(ctx, "500.html", nil) + default: + return err + } + } + } + } + return nil + } + } +} diff --git a/internal/http/middleware/installed.go b/internal/http/middleware/installed.go new file mode 100644 index 00000000..6a48273a --- /dev/null +++ b/internal/http/middleware/installed.go @@ -0,0 +1,46 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/db" + + echo "github.com/labstack/echo/v4" +) + +// Installed 用于 echo 框架,判断是否已经安装了 +func Installed(filterPrefixs []string) echo.MiddlewareFunc { + filterPrefixs = append(filterPrefixs, "/install") + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + if db.MasterDB == nil { + shouldRedirect := true + + uri := ctx.Request().RequestURI + for _, prefix := range filterPrefixs { + if strings.HasPrefix(uri, prefix) { + shouldRedirect = false + break + } + } + + if shouldRedirect { + return ctx.Redirect(http.StatusSeeOther, "/install") + } + } + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/login.go b/internal/http/middleware/login.go new file mode 100644 index 00000000..c4b043c8 --- /dev/null +++ b/internal/http/middleware/login.go @@ -0,0 +1,158 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + "net/url" + "strconv" + "strings" + "time" + + mycontext "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/db" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/gorilla/context" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// AutoLogin 用于 echo 框架的自动登录和通过 cookie 获取用户信息 +func AutoLogin() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + // github.com/gorilla/sessions 要求必须 Clear + defer context.Clear(Request(ctx)) + + ctx.Set("req_start_time", time.Now()) + + var getCurrentUser = func(usernameOrId interface{}) { + ip := goutils.RemoteIp(Request(ctx)) + // IP 黑名单,不让登录 + if logic.DefaultRisk.IsBlackIP(ip) { + return + } + + if db.MasterDB != nil { + ctx.Set("ip", ip) + // TODO: 考虑缓存,或延迟查询,避免每次都查询 + user := logic.DefaultUser.FindCurrentUser(mycontext.EchoContext(ctx), usernameOrId) + if user.Uid != 0 { + ctx.Set("user", user) + + if !util.IsAjax(ctx) && ctx.Path() != "/ws" { + go logic.ViewObservable.NotifyObservers(user.Uid, 0, 0) + } + } + } + } + + session := GetCookieSession(ctx) + username, ok := session.Values["username"] + if ok { + getCurrentUser(username) + } else { + // App(手机) 登录 + uid, ok := ParseToken(ctx.FormValue("token")) + if ok { + getCurrentUser(uid) + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +// NeedLogin 用于 echo 框架的验证必须登录的请求 +func NeedLogin() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user, ok := ctx.Get("user").(*model.Me) + if !ok || user.Status != model.UserStatusAudit { + method := ctx.Request().Method + if util.IsAjax(ctx) { + if !strings.HasPrefix(ctx.Path(), "/account") { + return ctx.JSON(http.StatusForbidden, map[string]interface{}{"ok": 0, "error": "403 Forbidden"}) + } + } else { + if method == "POST" { + return ctx.HTML(http.StatusForbidden, `403 Forbidden`) + } + + if !ok { + reqURL := ctx.Request().URL + uri := reqURL.Path + if reqURL.RawQuery != "" { + uri += "?" + reqURL.RawQuery + } + return ctx.Redirect(http.StatusSeeOther, "/account/login?redirect_uri="+url.QueryEscape(uri)) + } else { + // 未激活可以查看账号信息 + if !strings.HasPrefix(ctx.Path(), "/account") { + return echo.NewHTTPError(http.StatusForbidden, `您的邮箱未激活,去激活`) + } + } + } + } else { + newUserWait := time.Duration(logic.UserSetting[model.KeyNewUserWait]) * time.Second + if newUserWait > 0 { + elapse := time.Now().Sub(user.CreatedAt) + if elapse <= newUserWait { + return echo.NewHTTPError(http.StatusForbidden, `您需要再等待`+(newUserWait-elapse).String()+"才能进行此操作") + } + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +// AppNeedLogin 用于 echo 框架的验证必须登录的请求(APP 专用) +func AppNeedLogin() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user, ok := ctx.Get("user").(*model.Me) + if ok { + // 校验 token 是否有效 + if !ValidateToken(ctx.QueryParam("token")) { + return outputAppJSON(ctx, NeedReLoginCode, "token无效,请重新登录!") + } + + if user.Status != model.UserStatusAudit { + return outputAppJSON(ctx, 1, "账号未审核通过、被冻结或被停号,请联系我们") + } + } else { + return outputAppJSON(ctx, NeedReLoginCode, "请先登录!") + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +func outputAppJSON(ctx echo.Context, code int, msg string) error { + AccessControl(ctx) + return ctx.JSON(http.StatusForbidden, map[string]interface{}{"code": strconv.Itoa(code), "msg": msg}) +} diff --git a/internal/http/middleware/notice.go b/internal/http/middleware/notice.go new file mode 100644 index 00000000..fabe6e07 --- /dev/null +++ b/internal/http/middleware/notice.go @@ -0,0 +1,50 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "fmt" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +// PublishNotice 用于 echo 框架,用户发布内容邮件通知站长 +func PublishNotice() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + if err := next(ctx); err != nil { + return err + } + + curUser := ctx.Get("user").(*model.Me) + if curUser.IsRoot { + return nil + } + + title := ctx.FormValue("title") + content := ctx.FormValue("content") + if ctx.Request().Method == "POST" && (title != "" || content != "") { + requestURI := ctx.Request().RequestURI + go func() { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "is_root", 1) + if user.Uid == 0 { + return + } + + content = fmt.Sprintf("URI:%s" + comment + "" + }) + + if strings.TrimSpace(content) == "" { + return errors.New("goquery reddit.com" + this.path + " self newdocument(" + resourceUrl + ") error: content is empty") + } + + resource.Content = content + + // reddit 本身的,当做其他资源 + resource.Catid = 4 + } else { + resource.Form = model.LinkForm + + // Github,是开源项目 + if contentSelection.Find(".title .domain a").Text() == "github.com" { + resource.Catid = 2 + } else { + resource.Catid = 1 + } + } + + resource.Title = title + resource.Url = resourceUrl + resource.Uid = goutils.MustInt(PresetUids[rand.Intn(len(PresetUids))]) + + ctime := time.Now() + datetime, ok := contentSelection.Find(".tagline time").Attr("datetime") + if ok { + dtime, err := time.ParseInLocation(time.RFC3339, datetime, time.UTC) + if err != nil { + logger.Errorln("parse ctime error:", err) + } else { + ctime = dtime.Local() + } + } + resource.Ctime = model.OftenTime(ctime) + + if resource.Id == 0 { + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err = session.Insert(resource) + if err != nil { + session.Rollback() + return errors.New("insert into Resource error:" + err.Error()) + } + + // 存扩展信息 + resourceEx := &model.ResourceEx{} + resourceEx.Id = resource.Id + if _, err = session.Insert(resourceEx); err != nil { + session.Rollback() + return errors.New("insert into ResourceEx error:" + err.Error()) + } + session.Commit() + + me := &model.Me{IsAdmin: true} + DefaultFeed.publish(resource, resourceEx, me) + } else { + if _, err = MasterDB.ID(resource.Id).Update(resource); err != nil { + return errors.New("update resource:" + strconv.Itoa(resource.Id) + " error:" + err.Error()) + } + } + + return nil +} diff --git a/internal/logic/resource.go b/internal/logic/resource.go new file mode 100644 index 00000000..2100b8b7 --- /dev/null +++ b/internal/logic/resource.go @@ -0,0 +1,466 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "net/url" + "strconv" + "time" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "github.com/fatih/structs" + "github.com/polaris1119/logger" + "github.com/polaris1119/set" + "golang.org/x/net/context" +) + +type ResourceLogic struct{} + +var DefaultResource = ResourceLogic{} + +// Publish 增加(修改)资源 +func (ResourceLogic) Publish(ctx context.Context, me *model.Me, form url.Values) (err error) { + objLog := GetLogger(ctx) + + uid := me.Uid + resource := &model.Resource{} + + if form.Get("id") != "" { + id := form.Get("id") + _, err = MasterDB.ID(id).Get(resource) + if err != nil { + logger.Errorln("ResourceLogic Publish find error:", err) + return + } + + if !CanEdit(me, resource) { + err = NotModifyAuthorityErr + return + } + + if form.Get("form") == model.LinkForm { + form.Set("content", "") + } else { + form.Set("url", "") + } + + err = schemaDecoder.Decode(resource, form) + if err != nil { + objLog.Errorln("ResourceLogic Publish decode error:", err) + return + } + _, err = MasterDB.ID(id).Update(resource) + if err != nil { + objLog.Errorf("更新资源 【%s】 信息失败:%s\n", id, err) + return + } + + go modifyObservable.NotifyObservers(uid, model.TypeResource, resource.Id) + + } else { + + err = schemaDecoder.Decode(resource, form) + if err != nil { + objLog.Errorln("ResourceLogic Publish decode error:", err) + return + } + + resource.Uid = uid + + session := MasterDB.NewSession() + defer session.Close() + + err = session.Begin() + if err != nil { + session.Rollback() + objLog.Errorln("Publish Resource begin tx error:", err) + return + } + + _, err = session.Insert(resource) + if err != nil { + session.Rollback() + objLog.Errorln("Publish Resource insert resource error:", err) + return + } + + resourceEx := &model.ResourceEx{ + Id: resource.Id, + } + _, err = session.Insert(resourceEx) + if err != nil { + session.Rollback() + objLog.Errorln("Publish Resource insert resource_ex error:", err) + return + } + + err = session.Commit() + if err != nil { + objLog.Errorln("Publish Resource commit error:", err) + return + } + + // 发布动态 + DefaultFeed.publish(resource, resourceEx, me) + + // 给 被@用户 发系统消息 + ext := map[string]interface{}{ + "objid": resource.Id, + "objtype": model.TypeResource, + "uid": uid, + "msgtype": model.MsgtypePublishAtMe, + } + go DefaultMessage.SendSysMsgAtUsernames(ctx, form.Get("usernames"), ext, 0) + + go publishObservable.NotifyObservers(uid, model.TypeResource, resource.Id) + } + + return +} + +// Total 资源总数 +func (ResourceLogic) Total() int64 { + total, err := MasterDB.Count(new(model.Resource)) + if err != nil { + logger.Errorln("CommentLogic Total error:", err) + } + return total +} + +// FindBy 获取资源列表(分页) +func (ResourceLogic) FindBy(ctx context.Context, limit int, lastIds ...int) []*model.Resource { + objLog := GetLogger(ctx) + + dbSession := MasterDB.OrderBy("id DESC").Limit(limit) + if len(lastIds) > 0 && lastIds[0] > 0 { + dbSession.Where("id", lastIds[0]) + } + + resourceList := make([]*model.Resource, 0) + err := dbSession.Find(&resourceList) + if err != nil { + objLog.Errorln("ResourceLogic FindBy Error:", err) + return nil + } + + return resourceList +} + +// FindAll 获得资源列表(完整信息),分页 +func (self ResourceLogic) FindAll(ctx context.Context, paginator *Paginator, orderBy, querystring string, args ...interface{}) (resources []map[string]interface{}, total int64) { + objLog := GetLogger(ctx) + + var ( + count = paginator.PerPage() + resourceInfos = make([]*model.ResourceInfo, 0) + ) + + session := MasterDB.Join("INNER", "resource_ex", "resource.id=resource_ex.id") + if querystring != "" { + session.Where(querystring, args...) + } + err := session.OrderBy(orderBy).Limit(count, paginator.Offset()).Find(&resourceInfos) + if err != nil { + objLog.Errorln("ResourceLogic FindAll error:", err) + return + } + + total = self.Count(ctx, querystring, args...) + + uidSet := set.New(set.NonThreadSafe) + for _, resourceInfo := range resourceInfos { + uidSet.Add(resourceInfo.Uid) + } + + usersMap := DefaultUser.FindUserInfos(ctx, set.IntSlice(uidSet)) + + resources = make([]map[string]interface{}, len(resourceInfos)) + + for i, resourceInfo := range resourceInfos { + dest := make(map[string]interface{}) + + structs.FillMap(resourceInfo.Resource, dest) + structs.FillMap(resourceInfo.ResourceEx, dest) + + dest["user"] = usersMap[resourceInfo.Uid] + + // 链接的host + if resourceInfo.Form == model.LinkForm { + urlObj, err := url.Parse(resourceInfo.Url) + if err == nil { + dest["host"] = urlObj.Host + } + } else { + dest["url"] = "/resources/" + strconv.Itoa(resourceInfo.Resource.Id) + } + + resources[i] = dest + } + + return +} + +func (ResourceLogic) Count(ctx context.Context, querystring string, args ...interface{}) int64 { + objLog := GetLogger(ctx) + + var ( + total int64 + err error + ) + if querystring == "" { + total, err = MasterDB.Count(new(model.Resource)) + } else { + total, err = MasterDB.Where(querystring, args...).Count(new(model.Resource)) + } + + if err != nil { + objLog.Errorln("ResourceLogic Count error:", err) + } + + return total +} + +// FindByCatid 获得某个分类的资源列表,分页 +func (ResourceLogic) FindByCatid(ctx context.Context, paginator *Paginator, catid int) (resources []map[string]interface{}, total int64) { + objLog := GetLogger(ctx) + + var ( + count = paginator.PerPage() + resourceInfos = make([]*model.ResourceInfo, 0) + ) + + err := MasterDB.Join("INNER", "resource_ex", "resource.id=resource_ex.id").Where("catid=?", catid). + Desc("resource.mtime").Limit(count, paginator.Offset()).Find(&resourceInfos) + if err != nil { + objLog.Errorln("ResourceLogic FindByCatid error:", err) + return + } + + total, err = MasterDB.Where("catid=?", catid).Count(new(model.Resource)) + if err != nil { + objLog.Errorln("ResourceLogic FindByCatid count error:", err) + return + } + + uidSet := set.New(set.NonThreadSafe) + for _, resourceInfo := range resourceInfos { + uidSet.Add(resourceInfo.Uid) + } + + usersMap := DefaultUser.FindUserInfos(ctx, set.IntSlice(uidSet)) + + resources = make([]map[string]interface{}, len(resourceInfos)) + + for i, resourceInfo := range resourceInfos { + dest := make(map[string]interface{}) + + structs.FillMap(resourceInfo.Resource, dest) + structs.FillMap(resourceInfo.ResourceEx, dest) + + dest["user"] = usersMap[resourceInfo.Uid] + + // 链接的host + if resourceInfo.Form == model.LinkForm { + urlObj, err := url.Parse(resourceInfo.Url) + if err == nil { + dest["host"] = urlObj.Host + } + } else { + dest["url"] = "/resources/" + strconv.Itoa(resourceInfo.Resource.Id) + } + + resources[i] = dest + } + + return +} + +// FindByIds 获取多个资源详细信息 +func (ResourceLogic) FindByIds(ids []int) []*model.Resource { + if len(ids) == 0 { + return nil + } + resources := make([]*model.Resource, 0) + err := MasterDB.In("id", ids).Find(&resources) + if err != nil { + logger.Errorln("ResourceLogic FindByIds error:", err) + return nil + } + return resources +} + +func (ResourceLogic) findById(id int) *model.Resource { + resource := &model.Resource{} + _, err := MasterDB.ID(id).Get(resource) + if err != nil { + logger.Errorln("ResourceLogic findById error:", err) + } + return resource +} + +// findByIds 获取多个资源详细信息 包内使用 +func (ResourceLogic) findByIds(ids []int) map[int]*model.Resource { + if len(ids) == 0 { + return nil + } + resources := make(map[int]*model.Resource) + err := MasterDB.In("id", ids).Find(&resources) + if err != nil { + logger.Errorln("ResourceLogic FindByIds error:", err) + return nil + } + return resources +} + +// 获得资源详细信息 +func (ResourceLogic) FindById(ctx context.Context, id int) (resourceMap map[string]interface{}, comments []map[string]interface{}) { + objLog := GetLogger(ctx) + + resourceInfo := &model.ResourceInfo{} + _, err := MasterDB.Join("INNER", "resource_ex", "resource.id=resource_ex.id").Where("resource.id=?", id).Get(resourceInfo) + if err != nil { + objLog.Errorln("ResourceLogic FindById error:", err) + return + } + + resource := &resourceInfo.Resource + if resource.Id == 0 { + objLog.Errorln("ResourceLogic FindById get error:", err) + return + } + + resourceMap = make(map[string]interface{}) + structs.FillMap(resource, resourceMap) + structs.FillMap(resourceInfo.ResourceEx, resourceMap) + + resourceMap["catname"] = GetCategoryName(resource.Catid) + // 链接的host + if resource.Form == model.LinkForm { + urlObj, err := url.Parse(resource.Url) + if err == nil { + resourceMap["host"] = urlObj.Host + } + } else { + resourceMap["url"] = "/resources/" + strconv.Itoa(resource.Id) + } + + // 评论信息 + comments, ownerUser, _ := DefaultComment.FindObjComments(ctx, id, model.TypeResource, resource.Uid, 0) + resourceMap["user"] = ownerUser + return +} + +// 获取单个 Resource 信息(用于编辑) +func (ResourceLogic) FindResource(ctx context.Context, id int) *model.Resource { + objLog := GetLogger(ctx) + + resource := &model.Resource{} + _, err := MasterDB.ID(id).Get(resource) + if err != nil { + objLog.Errorf("ResourceLogic FindResource [%d] error:%s\n", id, err) + } + + return resource +} + +// 获得某个用户最近的资源 +func (ResourceLogic) FindRecent(ctx context.Context, uid int) []*model.Resource { + resourceList := make([]*model.Resource, 0) + err := MasterDB.Where("uid=?", uid).Limit(5).OrderBy("id DESC").Find(&resourceList) + if err != nil { + logger.Errorln("resource logic FindRecent error:", err) + return nil + } + + return resourceList +} + +// getOwner 通过id获得资源的所有者 +func (ResourceLogic) getOwner(id int) int { + resource := &model.Resource{} + _, err := MasterDB.ID(id).Get(resource) + if err != nil { + logger.Errorln("resource logic getOwner Error:", err) + return 0 + } + return resource.Uid +} + +// 资源评论 +type ResourceComment struct{} + +// 更新该资源的评论信息 +// cid:评论id;objid:被评论对象id;uid:评论者;cmttime:评论时间 +func (self ResourceComment) UpdateComment(cid, objid, uid int, cmttime time.Time) { + session := MasterDB.NewSession() + defer session.Close() + + session.Begin() + + // 更新最后回复信息 + _, err := session.Table(new(model.Resource)).ID(objid).Update(map[string]interface{}{ + "lastreplyuid": uid, + "lastreplytime": cmttime, + }) + if err != nil { + logger.Errorln("更新最后回复人信息失败:", err) + session.Rollback() + return + } + + // 更新评论数(TODO:暂时每次都更新表) + _, err = session.ID(objid).Incr("cmtnum", 1).Update(new(model.ResourceEx)) + if err != nil { + logger.Errorln("更新资源评论数失败:", err) + session.Rollback() + return + } + + session.Commit() +} + +func (self ResourceComment) String() string { + return "resource" +} + +// 实现 CommentObjecter 接口 +func (self ResourceComment) SetObjinfo(ids []int, commentMap map[int][]*model.Comment) { + resources := DefaultResource.FindByIds(ids) + if len(resources) == 0 { + return + } + + for _, resource := range resources { + objinfo := make(map[string]interface{}) + objinfo["title"] = resource.Title + objinfo["uri"] = model.PathUrlMap[model.TypeResource] + objinfo["type_name"] = model.TypeNameMap[model.TypeResource] + + for _, comment := range commentMap[resource.Id] { + comment.Objinfo = objinfo + } + } +} + +// 资源喜欢 +type ResourceLike struct{} + +// 更新该主题的喜欢数 +// objid:被喜欢对象id;num: 喜欢数(负数表示取消喜欢) +func (self ResourceLike) UpdateLike(objid, num int) { + // 更新喜欢数(TODO:暂时每次都更新表) + _, err := MasterDB.Where("id=?", objid).Incr("likenum", num).Update(new(model.ResourceEx)) + if err != nil { + logger.Errorln("更新资源喜欢数失败:", err) + } +} + +func (self ResourceLike) String() string { + return "resource" +} diff --git a/internal/logic/risk.go b/internal/logic/risk.go new file mode 100644 index 00000000..662492c5 --- /dev/null +++ b/internal/logic/risk.go @@ -0,0 +1,56 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "github.com/polaris1119/nosql" +) + +type RiskLogic struct{} + +var DefaultRisk = RiskLogic{} + +// AddBlackIP 加入 IP 黑名单 +func (RiskLogic) AddBlackIP(ip string) error { + redisClient := nosql.NewRedisClient() + defer redisClient.Close() + + key := "black:ip" + return redisClient.HSET(key, ip, "1") +} + +// AddBlackIPByUID 通过用户 UID 将最后一次登录 IP 加入黑名单 +func (self RiskLogic) AddBlackIPByUID(uid int) error { + userLogin := &model.UserLogin{} + _, err := MasterDB.Where("uid=?", uid).Get(userLogin) + if err != nil { + return err + } + + if userLogin.LoginIp != "" { + return self.AddBlackIP(userLogin.LoginIp) + } + + return nil +} + +// IsBlackIP 是否是 IP 黑名单 +func (RiskLogic) IsBlackIP(ip string) bool { + redisClient := nosql.NewRedisClient() + defer redisClient.Close() + + key := "black:ip" + val, err := redisClient.HGET(key, ip) + if err != nil { + return false + } + + return val == "1" +} diff --git a/internal/logic/rule.go b/internal/logic/rule.go new file mode 100644 index 00000000..77e48b12 --- /dev/null +++ b/internal/logic/rule.go @@ -0,0 +1,99 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "net/url" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "golang.org/x/net/context" +) + +type RuleLogic struct{} + +var DefaultRule = RuleLogic{} + +// 获取抓取规则列表(分页) +func (RuleLogic) FindBy(ctx context.Context, conds map[string]string, curPage, limit int) ([]*model.CrawlRule, int) { + objLog := GetLogger(ctx) + + session := MasterDB.NewSession() + + for k, v := range conds { + session.And(k+"=?", v) + } + + totalSession := SessionClone(session) + + offset := (curPage - 1) * limit + ruleList := make([]*model.CrawlRule, 0) + err := session.OrderBy("id DESC").Limit(limit, offset).Find(&ruleList) + if err != nil { + objLog.Errorln("rule find error:", err) + return nil, 0 + } + + total, err := totalSession.Count(new(model.CrawlRule)) + if err != nil { + objLog.Errorln("rule find count error:", err) + return nil, 0 + } + + return ruleList, int(total) +} + +func (RuleLogic) FindById(ctx context.Context, id string) *model.CrawlRule { + objLog := GetLogger(ctx) + + rule := &model.CrawlRule{} + _, err := MasterDB.ID(id).Get(rule) + if err != nil { + objLog.Errorln("find rule error:", err) + return nil + } + + if rule.Id == 0 { + return nil + } + + return rule +} + +func (RuleLogic) Save(ctx context.Context, form url.Values, opUser string) (errMsg string, err error) { + objLog := GetLogger(ctx) + + rule := &model.CrawlRule{} + err = schemaDecoder.Decode(rule, form) + if err != nil { + objLog.Errorln("rule Decode error", err) + errMsg = err.Error() + return + } + + rule.OpUser = opUser + + if rule.Id != 0 { + _, err = MasterDB.ID(rule.Id).Update(rule) + } else { + _, err = MasterDB.Insert(rule) + } + + if err != nil { + errMsg = "内部服务器错误" + objLog.Errorln("rule save:", errMsg, ":", err) + return + } + + return +} + +func (RuleLogic) Delete(ctx context.Context, id string) error { + _, err := MasterDB.ID(id).Delete(new(model.CrawlRule)) + return err +} diff --git a/internal/logic/searcher.go b/internal/logic/searcher.go new file mode 100644 index 00000000..e53ee50b --- /dev/null +++ b/internal/logic/searcher.go @@ -0,0 +1,604 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package logic + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/url" + "strconv" + "time" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/util" + + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/set" + + "github.com/studygolang/studygolang/internal/model" +) + +type SearcherLogic struct { + maxRows int + + engineUrl string +} + +var DefaultSearcher = SearcherLogic{maxRows: 100, engineUrl: config.ConfigFile.MustValue("search", "engine_url")} + +// 准备索引数据,post 给 solr +// isAll: 是否全量 +func (self SearcherLogic) Indexing(isAll bool) { + go self.IndexingOpenProject(isAll) + go self.IndexingTopic(isAll) + go self.IndexingResource(isAll) + self.IndexingArticle(isAll) +} + +// IndexingArticle 索引博文 +func (self SearcherLogic) IndexingArticle(isAll bool) { + solrClient := NewSolrClient() + + var ( + articleList []*model.Article + err error + ) + + id := 0 + for { + articleList = make([]*model.Article, 0) + if isAll { + err = MasterDB.Where("id>?", id).Limit(self.maxRows).OrderBy("id ASC").Find(&articleList) + } else { + timeAgo := time.Now().Add(-2 * time.Minute).Format("2006-01-02 15:04:05") + err = MasterDB.Where("mtime>?", timeAgo).Find(&articleList) + } + if err != nil { + logger.Errorln("IndexingArticle error:", err) + break + } + + if len(articleList) == 0 { + break + } + + for _, article := range articleList { + logger.Infoln("deal article_id:", article.Id) + + if id < article.Id { + id = article.Id + } + + if article.Tags == "" { + // 自动生成 + article.Tags = model.AutoTag(article.Title, article.Txt, 4) + if article.Tags != "" { + MasterDB.ID(article.Id).Cols("tags").Update(article) + } + } + + document := model.NewDocument(article, nil) + if article.Status != model.ArticleStatusOffline { + solrClient.PushAdd(model.NewDefaultArgsAddCommand(document)) + } else { + solrClient.PushDel(model.NewDelCommand(document)) + } + } + + solrClient.Post() + + if !isAll { + break + } + } +} + +// 索引主题 +func (self SearcherLogic) IndexingTopic(isAll bool) { + solrClient := NewSolrClient() + + var ( + topicList []*model.Topic + topicExList map[int]*model.TopicUpEx + + err error + ) + + id := 0 + for { + topicList = make([]*model.Topic, 0) + topicExList = make(map[int]*model.TopicUpEx) + + if isAll { + err = MasterDB.Where("tid>?", id).OrderBy("tid ASC").Limit(self.maxRows).Find(&topicList) + } else { + timeAgo := time.Now().Add(-2 * time.Minute).Format("2006-01-02 15:04:05") + err = MasterDB.Where("mtime>?", timeAgo).Find(&topicList) + } + if err != nil { + logger.Errorln("IndexingTopic error:", err) + break + } + + if len(topicList) == 0 { + break + } + + tids := util.Models2Intslice(topicList, "Tid") + + err = MasterDB.In("tid", tids).Find(&topicExList) + if err != nil { + logger.Errorln("IndexingTopic error:", err) + break + } + + for _, topic := range topicList { + logger.Infoln("deal topic_id:", topic.Tid) + + if id < topic.Tid { + id = topic.Tid + } + + if topic.Tags == "" { + // 自动生成 + topic.Tags = model.AutoTag(topic.Title, topic.Content, 4) + if topic.Tags != "" { + MasterDB.ID(topic.Tid).Cols("tags").Update(topic) + } + } + + if topic.Permission == model.PermissionPay { + topic.Content = "付费用户可见!" + } + + topicEx := topicExList[topic.Tid] + + document := model.NewDocument(topic, topicEx) + addCommand := model.NewDefaultArgsAddCommand(document) + + solrClient.PushAdd(addCommand) + } + + solrClient.Post() + + if !isAll { + break + } + } +} + +// 索引资源 +func (self SearcherLogic) IndexingResource(isAll bool) { + solrClient := NewSolrClient() + + var ( + resourceList []*model.Resource + resourceExList map[int]*model.ResourceEx + err error + ) + + id := 0 + for { + resourceList = make([]*model.Resource, 0) + resourceExList = make(map[int]*model.ResourceEx) + + if isAll { + err = MasterDB.Where("id>?", id).OrderBy("id ASC").Limit(self.maxRows).Find(&resourceList) + } else { + timeAgo := time.Now().Add(-2 * time.Minute).Format("2006-01-02 15:04:05") + err = MasterDB.Where("mtime>?", timeAgo).Find(&resourceList) + } + if err != nil { + logger.Errorln("IndexingResource error:", err) + break + } + + if len(resourceList) == 0 { + break + } + + ids := util.Models2Intslice(resourceList, "Id") + + err = MasterDB.In("id", ids).Find(&resourceExList) + if err != nil { + logger.Errorln("IndexingResource error:", err) + break + } + + for _, resource := range resourceList { + logger.Infoln("deal resource_id:", resource.Id) + + if id < resource.Id { + id = resource.Id + } + + if resource.Tags == "" { + // 自动生成 + resource.Tags = model.AutoTag(resource.Title+resource.CatName, resource.Content, 4) + if resource.Tags != "" { + MasterDB.ID(resource.Id).Cols("tags").Update(resource) + } + } + + resourceEx := resourceExList[resource.Id] + + document := model.NewDocument(resource, resourceEx) + addCommand := model.NewDefaultArgsAddCommand(document) + + solrClient.PushAdd(addCommand) + } + + solrClient.Post() + + if !isAll { + break + } + } +} + +// IndexingOpenProject 索引博文 +func (self SearcherLogic) IndexingOpenProject(isAll bool) { + solrClient := NewSolrClient() + + var ( + projectList []*model.OpenProject + err error + ) + + id := 0 + for { + projectList = make([]*model.OpenProject, 0) + + if isAll { + err = MasterDB.Where("id>?", id).OrderBy("id ASC").Limit(self.maxRows).Find(&projectList) + } else { + timeAgo := time.Now().Add(-2 * time.Minute).Format("2006-01-02 15:04:05") + err = MasterDB.Where("mtime>?", timeAgo).Find(&projectList) + } + if err != nil { + logger.Errorln("IndexingArticle error:", err) + break + } + + if len(projectList) == 0 { + break + } + + for _, project := range projectList { + logger.Infoln("deal project_id:", project.Id) + + if id < project.Id { + id = project.Id + } + + if project.Tags == "" { + // 自动生成 + project.Tags = model.AutoTag(project.Name+project.Category, project.Desc, 4) + if project.Tags != "" { + MasterDB.ID(project.Id).Cols("tags").Update(project) + } + } + + document := model.NewDocument(project, nil) + if project.Status != model.ProjectStatusOffline { + solrClient.PushAdd(model.NewDefaultArgsAddCommand(document)) + } else { + solrClient.PushDel(model.NewDelCommand(document)) + } + } + + solrClient.Post() + + if !isAll { + break + } + } + +} + +const searchContentLen = 350 + +// DoSearch 搜索 +func (this *SearcherLogic) DoSearch(q, field string, start, rows int) (*model.ResponseBody, error) { + selectUrl := this.engineUrl + "/select?" + + var values = url.Values{ + "wt": []string{"json"}, + "hl": []string{"true"}, + "hl.fl": []string{"title,content"}, + "hl.simple.pre": []string{""}, + "hl.simple.post": []string{""}, + "hl.fragsize": []string{strconv.Itoa(searchContentLen)}, + "start": []string{strconv.Itoa(start)}, + "rows": []string{strconv.Itoa(rows)}, + } + + if q == "" { + values.Add("q", "*:*") + } else if field == "tag" { + values.Add("q", "*:*") + values.Add("fq", "tags:"+q) + values.Add("sort", "viewnum desc") + q = "" + field = "" + } else { + searchStat := &model.SearchStat{} + MasterDB.Where("keyword=?", q).Get(searchStat) + if searchStat.Id > 0 { + MasterDB.Where("keyword=?", q).Incr("times", 1).Update(new(model.SearchStat)) + } else { + searchStat.Keyword = q + searchStat.Times = 1 + _, err := MasterDB.Insert(searchStat) + if err != nil { + MasterDB.Where("keyword=?", q).Incr("times", 1).Update(new(model.SearchStat)) + } + } + } + + if field != "" { + values.Add("df", field) + if q != "" { + values.Add("q", q) + } + } else { + // 全文检索 + if q != "" { + values.Add("q", "title:"+q+"^2"+" OR content:"+q+"^0.2") + } + } + logger.Infoln(selectUrl + values.Encode()) + resp, err := http.Get(selectUrl + values.Encode()) + if err != nil { + logger.Errorln("search error:", err) + return &model.ResponseBody{}, err + } + + defer resp.Body.Close() + + var searchResponse model.SearchResponse + err = json.NewDecoder(resp.Body).Decode(&searchResponse) + if err != nil { + logger.Errorln("parse response error:", err) + return &model.ResponseBody{}, err + } + + if len(searchResponse.Highlight) > 0 { + for _, doc := range searchResponse.RespBody.Docs { + highlighting, ok := searchResponse.Highlight[doc.Id] + if ok { + if len(highlighting.Title) > 0 { + doc.HlTitle = highlighting.Title[0] + } + + if len(highlighting.Content) > 0 { + doc.HlContent = highlighting.Content[0] + } + } + + if doc.HlTitle == "" { + doc.HlTitle = doc.Title + } + + if doc.HlContent == "" && doc.Content != "" { + utf8string := util.NewString(doc.Content) + maxLen := utf8string.RuneCount() - 1 + if maxLen > searchContentLen { + maxLen = searchContentLen + } + doc.HlContent = util.NewString(doc.Content).Slice(0, maxLen) + } + + doc.HlContent += "..." + } + + } + + if searchResponse.RespBody == nil { + searchResponse.RespBody = &model.ResponseBody{} + } + + return searchResponse.RespBody, nil +} + +// DoSearch 搜索 +func (this *SearcherLogic) SearchByField(field, value string, start, rows int, sorts ...string) (*model.ResponseBody, error) { + selectUrl := this.engineUrl + "/select?" + + sort := "sort_time desc,cmtnum desc,viewnum desc" + if len(sorts) > 0 { + sort = sorts[0] + } + var values = url.Values{ + "wt": []string{"json"}, + "start": []string{strconv.Itoa(start)}, + "rows": []string{strconv.Itoa(rows)}, + "sort": []string{sort}, + "fl": []string{"objid,objtype,title,author,uid,pub_time,tags,viewnum,cmtnum,likenum,lastreplyuid,lastreplytime,updated_at,top,nid"}, + } + + values.Add("q", value) + values.Add("df", field) + + logger.Infoln(selectUrl + values.Encode()) + + resp, err := http.Get(selectUrl + values.Encode()) + if err != nil { + logger.Errorln("search error:", err) + return &model.ResponseBody{}, err + } + + defer resp.Body.Close() + + var searchResponse model.SearchResponse + err = json.NewDecoder(resp.Body).Decode(&searchResponse) + if err != nil { + logger.Errorln("parse response error:", err) + return &model.ResponseBody{}, err + } + + if searchResponse.RespBody == nil { + searchResponse.RespBody = &model.ResponseBody{} + } + + return searchResponse.RespBody, nil +} + +func (this *SearcherLogic) FindAtomFeeds(rows int) (*model.ResponseBody, error) { + selectUrl := this.engineUrl + "/select?" + + var values = url.Values{ + "q": []string{"*:*"}, + "sort": []string{"sort_time desc"}, + "wt": []string{"json"}, + "start": []string{"0"}, + "rows": []string{strconv.Itoa(rows)}, + } + + resp, err := http.Get(selectUrl + values.Encode()) + if err != nil { + logger.Errorln("search error:", err) + return &model.ResponseBody{}, err + } + + defer resp.Body.Close() + + var searchResponse model.SearchResponse + err = json.NewDecoder(resp.Body).Decode(&searchResponse) + if err != nil { + logger.Errorln("parse response error:", err) + return &model.ResponseBody{}, err + } + + if searchResponse.RespBody == nil { + searchResponse.RespBody = &model.ResponseBody{} + } + + return searchResponse.RespBody, nil +} + +func (this *SearcherLogic) FillNodeAndUser(ctx context.Context, respBody *model.ResponseBody) (map[int]*model.User, map[int]*model.TopicNode) { + if respBody.NumFound == 0 { + return nil, nil + } + + uidSet := set.New(set.NonThreadSafe) + nidSet := set.New(set.NonThreadSafe) + + for _, doc := range respBody.Docs { + if doc.Uid > 0 { + uidSet.Add(doc.Uid) + } + if doc.Lastreplyuid > 0 { + uidSet.Add(doc.Lastreplyuid) + } + if doc.Nid > 0 { + nidSet.Add(doc.Nid) + } + } + + users := DefaultUser.FindUserInfos(nil, set.IntSlice(uidSet)) + // 获取节点信息 + nodes := GetNodesByNids(set.IntSlice(nidSet)) + + return users, nodes +} + +type SolrClient struct { + addCommands []*model.AddCommand + delCommands []*model.DelCommand +} + +func NewSolrClient() *SolrClient { + return &SolrClient{ + addCommands: make([]*model.AddCommand, 0, 100), + delCommands: make([]*model.DelCommand, 0, 100), + } +} + +func (this *SolrClient) PushAdd(addCommand *model.AddCommand) { + this.addCommands = append(this.addCommands, addCommand) +} + +func (this *SolrClient) PushDel(delCommand *model.DelCommand) { + this.delCommands = append(this.delCommands, delCommand) +} + +func (this *SolrClient) Post() error { + stringBuilder := goutils.NewBuffer().Append("{") + + needComma := false + for _, addCommand := range this.addCommands { + commandJson, err := json.Marshal(addCommand) + if err != nil { + continue + } + + if stringBuilder.Len() == 1 { + needComma = false + } else { + needComma = true + } + + if needComma { + stringBuilder.Append(",") + } + + stringBuilder.Append(`"add":`).Append(commandJson) + } + + for _, delCommand := range this.delCommands { + commandJson, err := json.Marshal(delCommand) + if err != nil { + continue + } + + if stringBuilder.Len() == 1 { + needComma = false + } else { + needComma = true + } + + if needComma { + stringBuilder.Append(",") + } + + stringBuilder.Append(`"delete":`).Append(commandJson) + } + + if stringBuilder.Len() == 1 { + logger.Errorln("post docs:no right addcommand") + return errors.New("no right addcommand") + } + + stringBuilder.Append("}") + + logger.Infoln("start post data to solr...") + + resp, err := http.Post(config.ConfigFile.MustValue("search", "engine_url")+"/update?wt=json&commit=true", "application/json", stringBuilder) + if err != nil { + logger.Errorln("post error:", err) + return err + } + + defer resp.Body.Close() + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + logger.Errorln("parse response error:", err) + return err + } + + logger.Infoln("post data result:", result) + + return nil +} diff --git a/internal/logic/setting.go b/internal/logic/setting.go new file mode 100644 index 00000000..38f49922 --- /dev/null +++ b/internal/logic/setting.go @@ -0,0 +1,221 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "encoding/json" + "errors" + "net/url" + "strings" + + . "github.com/studygolang/studygolang/db" + + "github.com/studygolang/studygolang/internal/model" + + "github.com/polaris1119/goutils" + "golang.org/x/net/context" +) + +type SettingLogic struct{} + +var DefaultSetting = SettingLogic{} + +func (SettingLogic) Update(ctx context.Context, form url.Values) error { + objLog := GetLogger(ctx) + + name := form.Get("name") + if name != "" { + WebsiteSetting.Name = name + } + + domain := form.Get("domain") + if domain != "" { + WebsiteSetting.Domain = domain + } + + titleSuffix := form.Get("title_suffix") + if titleSuffix != "" { + WebsiteSetting.TitleSuffix = titleSuffix + } + + favicon := form.Get("favicon") + if favicon != "" { + WebsiteSetting.Favicon = favicon + } + + startYear := goutils.MustInt(form.Get("start_year")) + if startYear != 0 { + WebsiteSetting.StartYear = startYear + } + + logo := form.Get("logo") + if logo != "" { + WebsiteSetting.Logo = logo + } + + WebsiteSetting.BlogUrl = form.Get("blog_url") + + slogan := form.Get("slogan") + if slogan != "" { + WebsiteSetting.Slogan = slogan + } + + WebsiteSetting.Beian = form.Get("beian") + + WebsiteSetting.ReadingMenu = form.Get("reading_menu") + + if docNameSlice, ok := form["doc_name"]; ok { + docUrlSlice := form["doc_url"] + + docMenus := make([]*model.DocMenu, len(docNameSlice)) + for i, docName := range docNameSlice { + docMenus[i] = &model.DocMenu{ + Name: docName, + Url: docUrlSlice[i], + } + } + + docMenusBytes, err := json.Marshal(docMenus) + if err != nil { + objLog.Errorln("marshal doc menu error:", err) + return err + } + + WebsiteSetting.DocMenus = docMenus + WebsiteSetting.DocsMenu = string(docMenusBytes) + } + + if indexTabSlice, ok := form["index_tab"]; ok { + indexNameSlice := form["index_name"] + indexDataSourceSlice := form["index_data_source"] + + indexNavs := make([]*model.IndexNav, len(indexTabSlice)) + for i, indexTab := range indexTabSlice { + indexNavs[i] = &model.IndexNav{ + Tab: indexTab, + Name: indexNameSlice[i], + DataSource: indexDataSourceSlice[i], + } + + // 原来的子 tab 得保留 + oldIndexNav := GetCurIndexNav(indexTab) + if oldIndexNav != nil { + indexNavs[i].Children = oldIndexNav.Children + } + } + + indexNavsBytes, err := json.Marshal(indexNavs) + if err != nil { + objLog.Errorln("marshal index tab nav error:", err) + return err + } + + WebsiteSetting.IndexNavs = indexNavs + WebsiteSetting.IndexNav = string(indexNavsBytes) + } + + if navNameSlice, ok := form["nav_name"]; ok { + navUrlSlice := form["nav_url"] + + footerNavs := make([]*model.FooterNav, len(navNameSlice)) + for i, navName := range navNameSlice { + outerWeb := true + if strings.HasPrefix(navUrlSlice[i], "/") { + outerWeb = false + } + footerNavs[i] = &model.FooterNav{ + Name: navName, + Url: navUrlSlice[i], + OuterSite: outerWeb, + } + } + + footerNavsBytes, err := json.Marshal(footerNavs) + if err != nil { + objLog.Errorln("marshal footer nav error:", err) + return err + } + + WebsiteSetting.FooterNavs = footerNavs + WebsiteSetting.FooterNav = string(footerNavsBytes) + } + + if frLogoImageSlice, ok := form["fr_logo_image"]; ok { + frLogoUrlSlice := form["fr_logo_url"] + frWidthSlice := form["fr_logo_width"] + frHeightSlice := form["fr_logo_height"] + + friendLogos := make([]*model.FriendLogo, len(frLogoImageSlice)) + for i, frLogoImage := range frLogoImageSlice { + friendLogos[i] = &model.FriendLogo{ + Image: frLogoImage, + Url: frLogoUrlSlice[i], + Width: frWidthSlice[i], + Height: frHeightSlice[i], + } + } + + friendLogosBytes, err := json.Marshal(friendLogos) + if err != nil { + objLog.Errorln("marshal friend logo error:", err) + return err + } + + WebsiteSetting.FriendLogos = friendLogos + WebsiteSetting.FriendsLogo = string(friendLogosBytes) + } + + _, err := MasterDB.Update(WebsiteSetting) + if err != nil { + objLog.Errorln("Update setting error:", err) + return err + } + + return nil +} + +func (SettingLogic) UpdateIndexTabChildren(ctx context.Context, form url.Values) error { + objLog := GetLogger(ctx) + + if _, ok := form["tab"]; !ok { + return errors.New("父 tab 没有指定") + } + + for _, indexTab := range WebsiteSetting.IndexNavs { + if indexTab.Tab == form.Get("tab") { + + if indexUriSlice, ok := form["index_uri"]; ok { + indexTab.Children = make([]*model.IndexNavChild, len(indexUriSlice)) + + indexNameSlice := form["index_name"] + + for i, indexUri := range indexUriSlice { + indexTab.Children[i] = &model.IndexNavChild{ + Uri: indexUri, + Name: indexNameSlice[i], + } + } + } + } + } + + indexNavsBytes, err := json.Marshal(WebsiteSetting.IndexNavs) + if err != nil { + objLog.Errorln("marshal index child tab nav error:", err) + return err + } + + WebsiteSetting.IndexNav = string(indexNavsBytes) + + _, err = MasterDB.Update(WebsiteSetting) + if err != nil { + objLog.Errorln("Update index child tab error:", err) + return err + } + + return nil +} diff --git a/internal/logic/sitemap.go b/internal/logic/sitemap.go new file mode 100644 index 00000000..773daabf --- /dev/null +++ b/internal/logic/sitemap.go @@ -0,0 +1,291 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "os" + "strconv" + "text/template" + "time" + + "github.com/studygolang/studygolang/util" + + "github.com/polaris1119/config" + "github.com/polaris1119/logger" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" +) + +// 自定义模板函数 +var funcMap = template.FuncMap{ + "time_format": func(i interface{}) string { + if t, ok := i.(time.Time); ok { + return t.Format(time.RFC3339) + } else if t, ok := i.(model.OftenTime); ok { + return time.Time(t).Format(time.RFC3339) + } + return "" + }, +} + +var sitemapTpl = template.Must(template.New("sitemap.xml").Funcs(funcMap).ParseFiles(config.TemplateDir + "/sitemap.xml")) +var sitemapIndexTpl = template.Must(template.ParseFiles(config.TemplateDir + "/sitemapindex.xml")) + +var sitemapPath = config.ROOT + "/sitemap/" + +func init() { + if !util.Exist(sitemapPath) { + err := os.MkdirAll(sitemapPath, 0777) + if err != nil { + panic(err) + } + } +} + +func GenSitemap() { + sitemapFiles := []string{} + + loc := "http://" + WebsiteSetting.Domain + if WebsiteSetting.OnlyHttps { + loc = "https://" + WebsiteSetting.Domain + } + // 首页 + home := map[string]string{ + "loc": loc, + "lastmode": time.Now().Format(time.RFC3339), + } + + var ( + little = 1 + step = 4999 + large = little + step + ) + + // 文章 + var ( + articles = make([]*model.Article, 0) + err error + ) + for { + sitemapFile := "sitemap_article_" + strconv.Itoa(large) + ".xml" + + err = MasterDB.Where("id BETWEEN ? AND ? AND status!=?", little, large, model.ArticleStatusOffline).Select("id,mtime").Find(&articles) + little = large + 1 + large = little + step + + if err != nil { + continue + } + + if len(articles) == 0 { + break + } + + data := map[string]interface{}{ + "home": home, + "articles": articles, + } + + if err = output(sitemapFile, data); err == nil { + sitemapFiles = append(sitemapFiles, sitemapFile) + } + + articles = make([]*model.Article, 0) + } + + little = 1 + large = little + step + + // 主题(帖子) + topics := make([]*model.Topic, 0) + for { + sitemapFile := "sitemap_topic_" + strconv.Itoa(large) + ".xml" + + err = MasterDB.Where("tid BETWEEN ? AND ? AND flag IN(?,?)", little, large, 0, 1).Select("tid,mtime").Find(&topics) + little = large + 1 + large = little + step + + if err != nil { + continue + } + + if len(topics) == 0 { + break + } + + data := map[string]interface{}{ + "home": home, + "topics": topics, + } + + if err = output(sitemapFile, data); err == nil { + sitemapFiles = append(sitemapFiles, sitemapFile) + } + + topics = make([]*model.Topic, 0) + } + + little = 1 + large = little + step + + // 资源 + resources := make([]*model.Resource, 0) + for { + sitemapFile := "sitemap_resource_" + strconv.Itoa(large) + ".xml" + + err = MasterDB.Where("id BETWEEN ? AND ?", little, large).Select("id,mtime").Find(&resources) + little = large + 1 + large = little + step + + if err != nil { + logger.Errorln("sitemap resource find error:", err) + continue + } + + if len(resources) == 0 { + break + } + + data := map[string]interface{}{ + "home": home, + "resources": resources, + } + + if err = output(sitemapFile, data); err == nil { + sitemapFiles = append(sitemapFiles, sitemapFile) + } + + resources = make([]*model.Resource, 0) + } + + little = 1 + large = little + step + + // 项目 + projects := make([]*model.OpenProject, 0) + for { + sitemapFile := "sitemap_project_" + strconv.Itoa(large) + ".xml" + + err = MasterDB.Where("id BETWEEN ? AND ?", little, large).Select("id,uri,mtime").Find(&projects) + little = large + 1 + large = little + step + + if err != nil { + continue + } + + if len(projects) == 0 { + break + } + + data := map[string]interface{}{ + "home": home, + "projects": projects, + } + + if err = output(sitemapFile, data); err == nil { + sitemapFiles = append(sitemapFiles, sitemapFile) + } + + projects = make([]*model.OpenProject, 0) + } + + little = 1 + large = little + step + + // 图书 + books := make([]*model.Book, 0) + for { + sitemapFile := "sitemap_book_" + strconv.Itoa(large) + ".xml" + + err = MasterDB.Where("id BETWEEN ? AND ?", little, large).Select("id,updated_at").Find(&books) + little = large + 1 + large = little + step + + if err != nil { + continue + } + + if len(books) == 0 { + break + } + + data := map[string]interface{}{ + "home": home, + "books": books, + } + + if err = output(sitemapFile, data); err == nil { + sitemapFiles = append(sitemapFiles, sitemapFile) + } + + books = make([]*model.Book, 0) + } + + little = 1 + large = little + step + + // wiki + wikis := make([]*model.Wiki, 0) + for { + sitemapFile := "sitemap_wiki_" + strconv.Itoa(large) + ".xml" + + err = MasterDB.Where("id BETWEEN ? AND ?", little, large).Select("id,uri,mtime").Find(&wikis) + little = large + 1 + large = little + step + + if err != nil { + continue + } + + if len(wikis) == 0 { + break + } + + data := map[string]interface{}{ + "home": home, + "wikis": wikis, + } + + if err = output(sitemapFile, data); err == nil { + sitemapFiles = append(sitemapFiles, sitemapFile) + } + + wikis = make([]*model.Wiki, 0) + } + + file, err := os.Create(sitemapPath + "sitemapindex.xml") + if err != nil { + logger.Errorln("gen sitemap index file error:", err) + return + } + defer file.Close() + + err = sitemapIndexTpl.Execute(file, map[string]interface{}{ + "home": home, + "sitemapFiles": sitemapFiles, + }) + if err != nil { + logger.Errorln("execute sitemap index template error:", err) + } +} + +func output(filename string, data map[string]interface{}) (err error) { + var file *os.File + file, err = os.Create(sitemapPath + filename) + if err != nil { + logger.Errorln("open file error:", err) + return + } + defer file.Close() + if err = sitemapTpl.Execute(file, data); err != nil { + logger.Errorln("execute template error:", err) + } + + return +} diff --git a/internal/logic/subject.go b/internal/logic/subject.go new file mode 100644 index 00000000..7d2652af --- /dev/null +++ b/internal/logic/subject.go @@ -0,0 +1,486 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "errors" + "net/url" + "strings" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/polaris1119/goutils" + "github.com/polaris1119/set" + "github.com/polaris1119/slices" + "golang.org/x/net/context" +) + +type SubjectLogic struct{} + +var DefaultSubject = SubjectLogic{} + +func (self SubjectLogic) FindBy(ctx context.Context, paginator *Paginator) []*model.Subject { + objLog := GetLogger(ctx) + + subjects := make([]*model.Subject, 0) + err := MasterDB.OrderBy("article_num DESC").Limit(paginator.PerPage(), paginator.Offset()). + Find(&subjects) + if err != nil { + objLog.Errorln("SubjectLogic FindBy error:", err) + } + + if len(subjects) > 0 { + + uidSet := set.New(set.NonThreadSafe) + + for _, subject := range subjects { + uidSet.Add(subject.Uid) + } + + usersMap := DefaultUser.FindUserInfos(ctx, set.IntSlice(uidSet)) + + for _, subject := range subjects { + subject.User = usersMap[subject.Uid] + } + } + + return subjects +} + +func (self SubjectLogic) FindOne(ctx context.Context, sid int) *model.Subject { + objLog := GetLogger(ctx) + + subject := &model.Subject{} + _, err := MasterDB.ID(sid).Get(subject) + if err != nil { + objLog.Errorln("SubjectLogic FindOne get error:", err) + } + + if subject.Uid > 0 { + subject.User = DefaultUser.findUser(ctx, subject.Uid) + } + + return subject +} + +func (self SubjectLogic) findByIds(ids []int) map[int]*model.Subject { + if len(ids) == 0 { + return nil + } + + subjects := make(map[int]*model.Subject) + err := MasterDB.In("id", ids).Find(&subjects) + if err != nil { + return nil + } + + return subjects +} + +func (self SubjectLogic) FindArticles(ctx context.Context, sid int, paginator *Paginator, orderBy string) []*model.Article { + objLog := GetLogger(ctx) + + order := "subject_article.created_at DESC" + if orderBy == "commented_at" { + order = "articles.lastreplytime DESC" + } + + subjectArticles := make([]*model.SubjectArticles, 0) + err := MasterDB.Join("INNER", "subject_article", "subject_article.article_id = articles.id"). + Where("sid=? AND state=?", sid, model.ContributeStateOnline). + Limit(paginator.PerPage(), paginator.Offset()). + OrderBy(order).Find(&subjectArticles) + if err != nil { + objLog.Errorln("SubjectLogic FindArticles Find subject_article error:", err) + return nil + } + + articles := make([]*model.Article, 0, len(subjectArticles)) + for _, subjectArticle := range subjectArticles { + if subjectArticle.Status == model.ArticleStatusOffline { + continue + } + + articles = append(articles, &subjectArticle.Article) + } + + DefaultArticle.fillUser(articles) + return articles +} + +// FindArticleTotal 专栏收录的文章数 +func (self SubjectLogic) FindArticleTotal(ctx context.Context, sid int) int64 { + objLog := GetLogger(ctx) + + total, err := MasterDB.Where("sid=?", sid).Count(new(model.SubjectArticle)) + if err != nil { + objLog.Errorln("SubjectLogic FindArticleTotal error:", err) + } + + return total +} + +// FindFollowers 专栏关注的用户 +func (self SubjectLogic) FindFollowers(ctx context.Context, sid int) []*model.SubjectFollower { + objLog := GetLogger(ctx) + + followers := make([]*model.SubjectFollower, 0) + err := MasterDB.Where("sid=?", sid).OrderBy("id DESC").Limit(8).Find(&followers) + if err != nil { + objLog.Errorln("SubjectLogic FindFollowers error:", err) + } + + if len(followers) == 0 { + return followers + } + + uids := slices.StructsIntSlice(followers, "Uid") + usersMap := DefaultUser.FindUserInfos(ctx, uids) + for _, follower := range followers { + follower.User = usersMap[follower.Uid] + follower.TimeAgo = util.TimeAgo(follower.CreatedAt) + } + + return followers +} + +func (self SubjectLogic) findFollowersBySid(sid int) []*model.SubjectFollower { + followers := make([]*model.SubjectFollower, 0) + MasterDB.Where("sid=?", sid).Find(&followers) + return followers +} + +// FindFollowerTotal 专栏关注的用户数 +func (self SubjectLogic) FindFollowerTotal(ctx context.Context, sid int) int64 { + objLog := GetLogger(ctx) + + total, err := MasterDB.Where("sid=?", sid).Count(new(model.SubjectFollower)) + if err != nil { + objLog.Errorln("SubjectLogic FindFollowerTotal error:", err) + } + + return total +} + +// Follow 关注或取消关注 +func (self SubjectLogic) Follow(ctx context.Context, sid int, me *model.Me) (err error) { + objLog := GetLogger(ctx) + + follower := &model.SubjectFollower{} + _, err = MasterDB.Where("sid=? AND uid=?", sid, me.Uid).Get(follower) + if err != nil { + objLog.Errorln("SubjectLogic Follow Get error:", err) + } + + if follower.Id > 0 { + _, err = MasterDB.Where("sid=? AND uid=?", sid, me.Uid).Delete(new(model.SubjectFollower)) + if err != nil { + objLog.Errorln("SubjectLogic Follow Delete error:", err) + } + + return + } + + follower.Sid = sid + follower.Uid = me.Uid + _, err = MasterDB.Insert(follower) + if err != nil { + objLog.Errorln("SubjectLogic Follow insert error:", err) + } + return +} + +func (self SubjectLogic) HadFollow(ctx context.Context, sid int, me *model.Me) bool { + objLog := GetLogger(ctx) + + num, err := MasterDB.Where("sid=? AND uid=?", sid, me.Uid).Count(new(model.SubjectFollower)) + if err != nil { + objLog.Errorln("SubjectLogic Follow insert error:", err) + } + + return num > 0 +} + +// Contribute 投稿 +func (self SubjectLogic) Contribute(ctx context.Context, me *model.Me, sid, articleId int) error { + objLog := GetLogger(ctx) + + subject := self.FindOne(ctx, sid) + if subject.Id == 0 { + return errors.New("该专栏不存在") + } + + count, _ := MasterDB.Where("article_id=?", articleId).Count(new(model.SubjectArticle)) + if count >= 5 { + return errors.New("该文超过 5 次投稿") + } + + subjectArticle := &model.SubjectArticle{ + Sid: sid, + ArticleId: articleId, + State: model.ContributeStateNew, + } + + // TODO: 非创建管理员投稿不需要审核 + if subject.Uid == me.Uid { + subjectArticle.State = model.ContributeStateOnline + } else { + if !subject.Contribute { + return errors.New("不允许投稿") + } + + // 不需要审核 + if !subject.Audit { + subjectArticle.State = model.ContributeStateOnline + } + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err := session.Insert(subjectArticle) + if err != nil { + session.Rollback() + objLog.Errorln("SubjectLogic Contribute insert error:", err) + return errors.New("投稿失败:" + err.Error()) + } + + _, err = session.ID(sid).Incr("article_num", 1).Update(new(model.Subject)) + if err != nil { + session.Rollback() + objLog.Errorln("SubjectLogic Contribute update subject article num error:", err) + return errors.New("投稿失败:" + err.Error()) + } + + if err := session.Commit(); err == nil { + // 成功,发送站内系统消息给关注者 + go self.sendMsgForFollower(ctx, subject, sid, articleId) + } + + return nil +} + +// sendMsgForFollower 专栏投稿发送消息给关注者 +func (self SubjectLogic) sendMsgForFollower(ctx context.Context, subject *model.Subject, sid, articleId int) { + followers := self.findFollowersBySid(sid) + for _, f := range followers { + DefaultMessage.SendSystemMsgTo(ctx, f.Uid, model.MsgtypeSubjectContribute, map[string]interface{}{ + "uid": subject.Uid, + "objid": articleId, + "sid": sid, + }) + } +} + +// RemoveContribute 删除投稿 +func (self SubjectLogic) RemoveContribute(ctx context.Context, sid, articleId int) error { + objLog := GetLogger(ctx) + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err := session.Where("sid=? AND article_id=?", sid, articleId).Delete(new(model.SubjectArticle)) + if err != nil { + session.Rollback() + objLog.Errorln("SubjectLogic RemoveContribute delete error:", err) + return errors.New("删除投稿失败:" + err.Error()) + } + + _, err = session.ID(sid).Decr("article_num", 1).Update(new(model.Subject)) + if err != nil { + session.Rollback() + objLog.Errorln("SubjectLogic RemoveContribute update subject article num error:", err) + return errors.New("删除投稿失败:" + err.Error()) + } + + session.Commit() + + return nil +} + +func (self SubjectLogic) ExistByName(name string) bool { + exist, _ := MasterDB.Where("name=?", name).Exist(new(model.Subject)) + return exist +} + +// Publish 发布专栏。 +func (self SubjectLogic) Publish(ctx context.Context, me *model.Me, form url.Values) (sid int, err error) { + objLog := GetLogger(ctx) + + sid = goutils.MustInt(form.Get("sid")) + if sid != 0 { + subject := &model.Subject{} + _, err = MasterDB.ID(sid).Get(subject) + if err != nil { + objLog.Errorln("Publish Subject find error:", err) + return + } + + _, err = self.Modify(ctx, me, form) + if err != nil { + objLog.Errorln("Publish Subject modify error:", err) + return + } + + } else { + subject := &model.Subject{} + err = schemaDecoder.Decode(subject, form) + if err != nil { + objLog.Errorln("SubjectLogic Publish decode error:", err) + return + } + subject.Uid = me.Uid + + _, err = MasterDB.Insert(subject) + if err != nil { + objLog.Errorln("SubjectLogic Publish insert error:", err) + return + } + sid = subject.Id + } + return +} + +// Modify 修改专栏 +func (SubjectLogic) Modify(ctx context.Context, user *model.Me, form url.Values) (errMsg string, err error) { + objLog := GetLogger(ctx) + + change := map[string]interface{}{} + + fields := []string{"name", "description", "cover", "contribute", "audit"} + for _, field := range fields { + change[field] = form.Get(field) + } + + sid := form.Get("sid") + _, err = MasterDB.Table(new(model.Subject)).ID(sid).Update(change) + if err != nil { + objLog.Errorf("更新专栏 【%s】 信息失败:%s\n", sid, err) + errMsg = "对不起,服务器内部错误,请稍后再试!" + return + } + + return +} + +func (self SubjectLogic) FindArticleSubjects(ctx context.Context, articleId int) []*model.Subject { + objLog := GetLogger(ctx) + + subjectArticles := make([]*model.SubjectArticle, 0) + err := MasterDB.Where("article_id=?", articleId).Find(&subjectArticles) + if err != nil { + objLog.Errorln("SubjectLogic FindArticleSubjects find error:", err) + return nil + } + + subjectLen := len(subjectArticles) + if subjectLen == 0 { + return nil + } + + sids := make([]int, subjectLen) + for i, subjectArticle := range subjectArticles { + sids[i] = subjectArticle.Sid + } + + subjects := make([]*model.Subject, 0) + err = MasterDB.In("id", sids).Find(&subjects) + if err != nil { + objLog.Errorln("SubjectLogic FindArticleSubjects find subject error:", err) + return nil + } + + return subjects +} + +// FindMine 获取我管理的专栏列表 +func (self SubjectLogic) FindMine(ctx context.Context, me *model.Me, articleId int, kw string) []map[string]interface{} { + objLog := GetLogger(ctx) + + subjects := make([]*model.Subject, 0) + // 先是我创建的专栏 + session := MasterDB.Where("uid=?", me.Uid) + if kw != "" { + session.Where("name LIKE ?", "%"+kw+"%") + } + err := session.Find(&subjects) + if err != nil { + objLog.Errorln("SubjectLogic FindMine find subject error:", err) + return nil + } + + adminSubjects := make([]*model.Subject, 0) + // 获取我管理的专栏 + strSql := "SELECT s.* FROM subject s,subject_admin sa WHERE s.id=sa.sid AND sa.uid=?" + if kw != "" { + strSql += " AND s.name LIKE '%" + kw + "%'" + } + err = MasterDB.SQL(strSql, me.Uid).Find(&adminSubjects) + if err != nil { + objLog.Errorln("SubjectLogic FindMine find admin subject error:", err) + } + + subjectArticles := make([]*model.SubjectArticle, 0) + err = MasterDB.Where("article_id=?", articleId).Find(&subjectArticles) + if err != nil { + objLog.Errorln("SubjectLogic FindMine find subject article error:", err) + } + subjectArticleMap := make(map[int]struct{}) + for _, sa := range subjectArticles { + subjectArticleMap[sa.Sid] = struct{}{} + } + + uidSet := set.New(set.NonThreadSafe) + for _, subject := range subjects { + uidSet.Add(subject.Uid) + } + for _, subject := range adminSubjects { + uidSet.Add(subject.Uid) + } + usersMap := DefaultUser.FindUserInfos(ctx, set.IntSlice(uidSet)) + + subjectMapSlice := make([]map[string]interface{}, 0, len(subjects)+len(adminSubjects)) + + for _, subject := range subjects { + self.genSubjectMapSlice(subject, &subjectMapSlice, subjectArticleMap, usersMap) + } + + for _, subject := range adminSubjects { + self.genSubjectMapSlice(subject, &subjectMapSlice, subjectArticleMap, usersMap) + } + + return subjectMapSlice +} + +func (self SubjectLogic) genSubjectMapSlice(subject *model.Subject, subjectMapSlice *[]map[string]interface{}, subjectArticleMap map[int]struct{}, usersMap map[int]*model.User) { + hadAdd := 0 + if _, ok := subjectArticleMap[subject.Id]; ok { + hadAdd = 1 + } + + cover := subject.Cover + if cover == "" { + user := usersMap[subject.Uid] + cover = util.Gravatar(user.Avatar, user.Email, 48, true) + } else if !strings.HasPrefix(cover, "http") { + cdnDomain := global.App.CanonicalCDN(true) + cover = cdnDomain + subject.Cover + } + + *subjectMapSlice = append(*subjectMapSlice, map[string]interface{}{ + "id": subject.Id, + "name": subject.Name, + "cover": cover, + "username": usersMap[subject.Uid].Username, + "had_add": hadAdd, + }) +} diff --git a/internal/logic/subject_test.go b/internal/logic/subject_test.go new file mode 100644 index 00000000..d29b0738 --- /dev/null +++ b/internal/logic/subject_test.go @@ -0,0 +1,47 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic_test + +import ( + "reflect" + "testing" + + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + "golang.org/x/net/context" +) + +func TestFindArticles(t *testing.T) { + type args struct { + ctx context.Context + sid int + } + tests := []struct { + name string + self logic.SubjectLogic + args args + want []*model.Article + }{ + { + name: "subject1", + args: args{ + nil, + 1, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + self := logic.SubjectLogic{} + if got := self.FindArticles(tt.args.ctx, tt.args.sid, nil, ""); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SubjectLogic.FindArticles() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/logic/third_user.go b/internal/logic/third_user.go new file mode 100644 index 00000000..e2385c9e --- /dev/null +++ b/internal/logic/third_user.go @@ -0,0 +1,453 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "encoding/json" + "errors" + "io/ioutil" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "github.com/polaris1119/logger" + + "github.com/polaris1119/config" + "golang.org/x/net/context" + "golang.org/x/oauth2" +) + +var githubConf *oauth2.Config +var giteaConf *oauth2.Config + +const GithubAPIBaseUrl = "https://api.github.com" +const GiteaAPIBaseUrl = "https://gitea.com/api/v1" + +func init() { + githubConf = &oauth2.Config{ + ClientID: config.ConfigFile.MustValue("github", "client_id"), + ClientSecret: config.ConfigFile.MustValue("github", "client_secret"), + Scopes: []string{"user:email"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/access_token", + }, + } + + giteaConf = &oauth2.Config{ + ClientID: config.ConfigFile.MustValue("gitea", "client_id"), + ClientSecret: config.ConfigFile.MustValue("gitea", "client_secret"), + Endpoint: oauth2.Endpoint{ + AuthURL: "https://gitea.com/login/oauth/authorize", + TokenURL: "https://gitea.com/login/oauth/access_token", + }, + } +} + +type ThirdUserLogic struct{} + +var DefaultThirdUser = ThirdUserLogic{} + +func (ThirdUserLogic) GithubAuthCodeUrl(ctx context.Context, redirectURL string) string { + // Redirect user to consent page to ask for permission + // for the scopes specified above. + githubConf.RedirectURL = redirectURL + return githubConf.AuthCodeURL("state", oauth2.AccessTypeOffline) +} + +func (self ThirdUserLogic) LoginFromGithub(ctx context.Context, code string) (*model.User, error) { + objLog := GetLogger(ctx) + + githubUser, token, err := self.githubTokenAndUser(ctx, code) + if err != nil { + objLog.Errorln("LoginFromGithub githubTokenAndUser error:", err) + return nil, err + } + + bindUser := &model.BindUser{} + // 是否已经授权过了 + _, err = MasterDB.Where("username=? AND type=?", githubUser.Login, model.BindTypeGithub).Get(bindUser) + if err != nil { + objLog.Errorln("LoginFromGithub Get BindUser error:", err) + return nil, err + } + + if bindUser.Uid > 0 { + // 更新 token 信息 + change := map[string]interface{}{ + "access_token": token.AccessToken, + "refresh_token": token.RefreshToken, + } + if !token.Expiry.IsZero() { + change["expire"] = int(token.Expiry.Unix()) + } + _, err = MasterDB.Table(new(model.BindUser)).Where("uid=?", bindUser.Uid).Update(change) + if err != nil { + objLog.Errorln("LoginFromGithub update token error:", err) + return nil, err + } + + user := DefaultUser.FindOne(ctx, "uid", bindUser.Uid) + return user, nil + } + + exists := DefaultUser.EmailOrUsernameExists(ctx, githubUser.Email, githubUser.Login) + if exists { + // TODO: 考虑改进? + objLog.Errorln("LoginFromGithub Github 对应的用户信息被占用") + return nil, errors.New("Github 对应的用户信息被占用,可能你注册过本站,用户名密码登录试试!") + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + // 有可能获取不到 email?加上 @github.com做邮箱后缀 + if githubUser.Email == "" { + githubUser.Email = githubUser.Login + "@github.com" + } + // 生成本站用户 + user := &model.User{ + Email: githubUser.Email, + Username: githubUser.Login, + Name: githubUser.Name, + City: githubUser.Location, + Company: githubUser.Company, + Github: githubUser.Login, + Website: githubUser.Blog, + Avatar: githubUser.AvatarUrl, + IsThird: 1, + Status: model.UserStatusAudit, + } + err = DefaultUser.doCreateUser(ctx, session, user) + if err != nil { + session.Rollback() + objLog.Errorln("LoginFromGithub doCreateUser error:", err) + return nil, err + } + + bindUser = &model.BindUser{ + Uid: user.Uid, + Type: model.BindTypeGithub, + Email: user.Email, + Tuid: githubUser.Id, + Username: githubUser.Login, + Name: githubUser.Name, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + Avatar: githubUser.AvatarUrl, + } + if !token.Expiry.IsZero() { + bindUser.Expire = int(token.Expiry.Unix()) + } + _, err = session.Insert(bindUser) + if err != nil { + session.Rollback() + objLog.Errorln("LoginFromGithub bindUser error:", err) + return nil, err + } + + session.Commit() + + return user, nil +} + +func (self ThirdUserLogic) BindGithub(ctx context.Context, code string, me *model.Me) error { + objLog := GetLogger(ctx) + + githubUser, token, err := self.githubTokenAndUser(ctx, code) + if err != nil { + objLog.Errorln("LoginFromGithub githubTokenAndUser error:", err) + return err + } + + bindUser := &model.BindUser{} + // 是否已经授权过了 + _, err = MasterDB.Where("username=? AND type=?", githubUser.Login, model.BindTypeGithub).Get(bindUser) + if err != nil { + objLog.Errorln("LoginFromGithub Get BindUser error:", err) + return err + } + + if bindUser.Uid > 0 { + // 更新 token 信息 + bindUser.AccessToken = token.AccessToken + bindUser.RefreshToken = token.RefreshToken + if !token.Expiry.IsZero() { + bindUser.Expire = int(token.Expiry.Unix()) + } + _, err = MasterDB.Where("uid=?", bindUser.Uid).Update(bindUser) + if err != nil { + objLog.Errorln("LoginFromGithub update token error:", err) + return err + } + + return nil + } + + bindUser = &model.BindUser{ + Uid: me.Uid, + Type: model.BindTypeGithub, + Email: githubUser.Email, + Tuid: githubUser.Id, + Username: githubUser.Login, + Name: githubUser.Name, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + Avatar: githubUser.AvatarUrl, + } + if !token.Expiry.IsZero() { + bindUser.Expire = int(token.Expiry.Unix()) + } + _, err = MasterDB.Insert(bindUser) + if err != nil { + objLog.Errorln("LoginFromGithub insert bindUser error:", err) + return err + } + + return nil +} + +func (ThirdUserLogic) GiteaAuthCodeUrl(ctx context.Context, redirectURL string) string { + // Redirect user to consent page to ask for permission + // for the scopes specified above. + giteaConf.RedirectURL = redirectURL + return giteaConf.AuthCodeURL("state", oauth2.AccessTypeOffline) +} + +func (self ThirdUserLogic) LoginFromGitea(ctx context.Context, code string) (*model.User, error) { + objLog := GetLogger(ctx) + + giteaUser, token, err := self.giteaTokenAndUser(ctx, code) + if err != nil { + objLog.Errorln("LoginFromGithub githubTokenAndUser error:", err) + return nil, err + } + + bindUser := &model.BindUser{} + // 是否已经授权过了 + _, err = MasterDB.Where("username=? AND type=?", giteaUser.UserName, model.BindTypeGitea).Get(bindUser) + if err != nil { + objLog.Errorln("LoginFromGithub Get BindUser error:", err) + return nil, err + } + + if bindUser.Uid > 0 { + // 更新 token 信息 + change := map[string]interface{}{ + "access_token": token.AccessToken, + "refresh_token": token.RefreshToken, + } + if !token.Expiry.IsZero() { + change["expire"] = int(token.Expiry.Unix()) + } + _, err = MasterDB.Table(new(model.BindUser)).Where("uid=?", bindUser.Uid).Update(change) + if err != nil { + objLog.Errorln("LoginFromGithub update token error:", err) + return nil, err + } + + user := DefaultUser.FindOne(ctx, "uid", bindUser.Uid) + return user, nil + } + + exists := DefaultUser.EmailOrUsernameExists(ctx, giteaUser.Email, giteaUser.UserName) + if exists { + // TODO: 考虑改进? + objLog.Errorln("LoginFromGitea Gitea 对应的用户信息被占用") + return nil, errors.New("Gitea 对应的用户信息被占用,可能你注册过本站,用户名密码登录试试!") + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + // 有可能获取不到 email?加上 @gitea.com做邮箱后缀 + if giteaUser.Email == "" { + giteaUser.Email = giteaUser.UserName + "@gitea.com" + } + // 生成本站用户 + user := &model.User{ + Email: giteaUser.Email, + Username: giteaUser.UserName, + Name: model.DisplayName(giteaUser), + City: "", + Company: "", + Gitea: giteaUser.UserName, + Website: "", + Avatar: giteaUser.AvatarURL, + IsThird: 1, + Status: model.UserStatusAudit, + } + err = DefaultUser.doCreateUser(ctx, session, user) + if err != nil { + session.Rollback() + objLog.Errorln("LoginFromGithub doCreateUser error:", err) + return nil, err + } + + bindUser = &model.BindUser{ + Uid: user.Uid, + Type: model.BindTypeGithub, + Email: user.Email, + Tuid: int(giteaUser.ID), + Username: giteaUser.UserName, + Name: model.DisplayName(giteaUser), + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + Avatar: giteaUser.AvatarURL, + } + if !token.Expiry.IsZero() { + bindUser.Expire = int(token.Expiry.Unix()) + } + _, err = session.Insert(bindUser) + if err != nil { + session.Rollback() + objLog.Errorln("LoginFromGitea bindUser error:", err) + return nil, err + } + + session.Commit() + + return user, nil +} + +func (self ThirdUserLogic) BindGitea(ctx context.Context, code string, me *model.Me) error { + objLog := GetLogger(ctx) + + giteaUser, token, err := self.giteaTokenAndUser(ctx, code) + if err != nil { + objLog.Errorln("LoginFromGitea githubTokenAndUser error:", err) + return err + } + + bindUser := &model.BindUser{} + // 是否已经授权过了 + _, err = MasterDB.Where("username=? AND type=?", giteaUser.UserName, model.BindTypeGitea).Get(bindUser) + if err != nil { + objLog.Errorln("LoginFromGitea Get BindUser error:", err) + return err + } + + if bindUser.Uid > 0 { + // 更新 token 信息 + bindUser.AccessToken = token.AccessToken + bindUser.RefreshToken = token.RefreshToken + if !token.Expiry.IsZero() { + bindUser.Expire = int(token.Expiry.Unix()) + } + _, err = MasterDB.Where("uid=?", bindUser.Uid).Update(bindUser) + if err != nil { + objLog.Errorln("LoginFromGitea update token error:", err) + return err + } + + return nil + } + + bindUser = &model.BindUser{ + Uid: me.Uid, + Type: model.BindTypeGithub, + Email: giteaUser.Email, + Tuid: int(giteaUser.ID), + Username: giteaUser.UserName, + Name: model.DisplayName(giteaUser), + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + Avatar: giteaUser.AvatarURL, + } + if !token.Expiry.IsZero() { + bindUser.Expire = int(token.Expiry.Unix()) + } + _, err = MasterDB.Insert(bindUser) + if err != nil { + objLog.Errorln("LoginFromGitea insert bindUser error:", err) + return err + } + + return nil +} + +func (ThirdUserLogic) UnBindUser(ctx context.Context, bindId interface{}, me *model.Me) error { + if !DefaultUser.HasPasswd(ctx, me.Uid) { + return errors.New("请先设置密码!") + } + _, err := MasterDB.Where("id=? AND uid=?", bindId, me.Uid).Delete(new(model.BindUser)) + return err +} + +func (ThirdUserLogic) findUid(thirdUsername string, typ int) int { + bindUser := &model.BindUser{} + _, err := MasterDB.Where("username=? AND `type`=?", thirdUsername, typ).Get(bindUser) + if err != nil { + logger.Errorln("ThirdUserLogic findUid error:", err) + } + + return bindUser.Uid +} + +func (ThirdUserLogic) githubTokenAndUser(ctx context.Context, code string) (*model.GithubUser, *oauth2.Token, error) { + token, err := githubConf.Exchange(ctx, code) + if err != nil { + return nil, nil, err + } + + httpClient := githubConf.Client(ctx, token) + resp, err := httpClient.Get(GithubAPIBaseUrl + "/user") + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + + githubUser := &model.GithubUser{} + err = json.Unmarshal(respBytes, githubUser) + if err != nil { + return nil, nil, err + } + + if githubUser.Id == 0 { + return nil, nil, errors.New("get github user info error") + } + + return githubUser, token, nil +} + +func (ThirdUserLogic) giteaTokenAndUser(ctx context.Context, code string) (*model.GiteaUser, *oauth2.Token, error) { + token, err := giteaConf.Exchange(ctx, code) + if err != nil { + return nil, nil, err + } + + httpClient := giteaConf.Client(ctx, token) + resp, err := httpClient.Get(GiteaAPIBaseUrl + "/user") + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + + giteaUser := &model.GiteaUser{} + err = json.Unmarshal(respBytes, giteaUser) + if err != nil { + return nil, nil, err + } + + if giteaUser.ID == 0 { + return nil, nil, errors.New("get gitea user info error") + } + + return giteaUser, token, nil +} diff --git a/internal/logic/topic.go b/internal/logic/topic.go new file mode 100644 index 00000000..24494360 --- /dev/null +++ b/internal/logic/topic.go @@ -0,0 +1,754 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "errors" + "fmt" + "html/template" + "net/url" + "sync" + "time" + + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + . "github.com/studygolang/studygolang/db" + + "github.com/fatih/structs" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/set" + "golang.org/x/net/context" + "xorm.io/xorm" +) + +type TopicLogic struct{} + +var DefaultTopic = TopicLogic{} + +// Publish 发布主题。入topics和topics_ex库 +func (self TopicLogic) Publish(ctx context.Context, me *model.Me, form url.Values) (tid int, err error) { + objLog := GetLogger(ctx) + + tid = goutils.MustInt(form.Get("tid")) + if tid != 0 { + topic := &model.Topic{} + _, err = MasterDB.ID(tid).Get(topic) + if err != nil { + objLog.Errorln("Publish Topic find error:", err) + return + } + + if !CanEdit(me, topic) { + err = NotModifyAuthorityErr + return + } + + _, err = self.Modify(ctx, me, form) + if err != nil { + objLog.Errorln("Publish Topic modify error:", err) + return + } + + nid := goutils.MustInt(form.Get("nid")) + + go func() { + // 不是作者自己修改,且是调整节点,扣除铜币 + if topic.Uid != me.Uid && topic.Nid != nid { + node := DefaultNode.FindOne(nid) + award := -500 + if node.ShowIndex { + award = -30 + } + desc := fmt.Sprintf(`主题节点被管理员调整为 %s`, node.Ename, node.Name) + user := DefaultUser.FindOne(ctx, "uid", topic.Uid) + DefaultUserRich.IncrUserRich(user, model.MissionTypeModify, award, desc) + } + + if nid != topic.Nid { + DefaultFeed.modifyTopicNode(tid, nid) + } + }() + } else { + usernames := form.Get("usernames") + form.Del("usernames") + + topic := &model.Topic{} + err = schemaDecoder.Decode(topic, form) + if err != nil { + objLog.Errorln("TopicLogic Publish decode error:", err) + return + } + topic.Uid = me.Uid + topic.Lastreplytime = model.NewOftenTime() + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err = session.Insert(topic) + if err != nil { + session.Rollback() + objLog.Errorln("TopicLogic Publish insert error:", err) + return + } + + topicEx := &model.TopicEx{ + Tid: topic.Tid, + } + + _, err = session.Insert(topicEx) + if err != nil { + session.Rollback() + objLog.Errorln("TopicLogic Publish Insert TopicEx error:", err) + return + } + session.Commit() + + go func() { + // 同一个首页不显示的节点,一天发布主题数超过3个,扣 1 千铜币 + topicNum, err := MasterDB.Where("uid=? AND ctime>?", me.Uid, time.Now().Format("2006-01-02 00:00:00")).Count(new(model.Topic)) + if err != nil { + logger.Errorln("find today topic num error:", err) + return + } + + if topicNum > 3 { + node := DefaultNode.FindOne(topic.Nid) + if node.ShowIndex { + return + } + + award := -1000 + + desc := fmt.Sprintf(`一天发布推广过多或 Spam 扣除铜币 %d 个`, -award) + user := DefaultUser.FindOne(ctx, "uid", me.Uid) + DefaultUserRich.IncrUserRich(user, model.MissionTypeSpam, award, desc) + + DefaultRank.GenDAURank(me.Uid, -1000) + } + }() + + // 发布动态 + DefaultFeed.publish(topic, topicEx, me) + + // 给 被@用户 发系统消息 + ext := map[string]interface{}{ + "objid": topic.Tid, + "objtype": model.TypeTopic, + "uid": me.Uid, + "msgtype": model.MsgtypePublishAtMe, + } + go DefaultMessage.SendSysMsgAtUsernames(ctx, usernames, ext, 0) + + go publishObservable.NotifyObservers(me.Uid, model.TypeTopic, topic.Tid) + + tid = topic.Tid + } + + return +} + +// Modify 修改主题 +// user 修改人的(有可能是作者或管理员) +func (TopicLogic) Modify(ctx context.Context, user *model.Me, form url.Values) (errMsg string, err error) { + objLog := GetLogger(ctx) + + change := map[string]interface{}{ + "editor_uid": user.Uid, + } + + fields := []string{"title", "content", "nid", "permission"} + for _, field := range fields { + change[field] = form.Get(field) + } + + tid := form.Get("tid") + _, err = MasterDB.Table(new(model.Topic)).ID(tid).Update(change) + if err != nil { + objLog.Errorf("更新主题 【%s】 信息失败:%s\n", tid, err) + errMsg = "对不起,服务器内部错误,请稍后再试!" + return + } + + go modifyObservable.NotifyObservers(user.Uid, model.TypeTopic, goutils.MustInt(tid)) + + return +} + +// Append 主题附言 +func (self TopicLogic) Append(ctx context.Context, uid, tid int, content string) error { + objLog := GetLogger(ctx) + + // 当前已经附言了几条,最多 3 条 + num, err := MasterDB.Where("tid=?", tid).Count(new(model.TopicAppend)) + if err != nil { + objLog.Errorln("TopicLogic Append error:", err) + return err + } + + if num >= model.AppendMaxNum { + return errors.New("不允许再发附言!") + } + + topicAppend := &model.TopicAppend{ + Tid: tid, + Content: content, + } + _, err = MasterDB.Insert(topicAppend) + + if err != nil { + objLog.Errorln("TopicLogic Append insert error:", err) + return err + } + + go appendObservable.NotifyObservers(uid, model.TypeTopic, tid) + + return nil +} + +// SetTop 置顶 +func (self TopicLogic) SetTop(ctx context.Context, me *model.Me, tid int) error { + objLog := GetLogger(ctx) + + if !me.IsAdmin { + topic := self.findByTid(tid) + if topic.Tid == 0 || topic.Uid != me.Uid { + return NotFoundErr + } + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err := session.Table(new(model.Topic)).ID(tid).Update(map[string]interface{}{ + "top": 1, + "top_time": time.Now().Unix(), + }) + if err != nil { + objLog.Errorln("TopicLogic SetTop error:", err) + session.Rollback() + return err + } + + err = DefaultFeed.setTop(session, tid, model.TypeTopic, 1) + if err != nil { + objLog.Errorln("TopicLogic SetTop feed error:", err) + session.Rollback() + return err + } + + session.Commit() + + go topObservable.NotifyObservers(me.Uid, model.TypeTopic, tid) + + return nil +} + +// UnsetTop 取消置顶 +func (self TopicLogic) UnsetTop(ctx context.Context, tid int) error { + objLog := GetLogger(ctx) + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err := session.Table(new(model.Topic)).ID(tid).Update(map[string]interface{}{ + "top": 0, + }) + if err != nil { + objLog.Errorln("TopicLogic UnsetTop error:", err) + session.Rollback() + return err + } + + err = DefaultFeed.setTop(session, tid, model.TypeTopic, 0) + if err != nil { + objLog.Errorln("TopicLogic UnsetTop feed error:", err) + session.Rollback() + return err + } + + session.Commit() + + return nil +} + +// AutoUnsetTop 自动取消置顶 +func (self TopicLogic) AutoUnsetTop() error { + topics := make([]*model.Topic, 0) + err := MasterDB.Where("top=1").Find(&topics) + if err != nil { + logger.Errorln("TopicLogic AutoUnsetTop error:", err) + return err + } + + for _, topic := range topics { + if topic.TopTime == 0 || topic.TopTime+86400 > time.Now().Unix() { + continue + } + + self.UnsetTop(nil, topic.Tid) + } + + return nil +} + +// FindAll 支持多页翻看 +func (self TopicLogic) FindAll(ctx context.Context, paginator *Paginator, orderBy string, querystring string, args ...interface{}) []map[string]interface{} { + objLog := GetLogger(ctx) + + topicInfos := make([]*model.TopicInfo, 0) + + session := MasterDB.Join("INNER", "topics_ex", "topics.tid=topics_ex.tid") + if querystring != "" { + session.Where(querystring, args...) + } + self.addFlagWhere(session) + err := session.OrderBy(orderBy).Limit(paginator.PerPage(), paginator.Offset()).Find(&topicInfos) + if err != nil { + objLog.Errorln("TopicLogic FindAll error:", err) + return nil + } + + return self.fillDataForTopicInfo(topicInfos) +} + +func (TopicLogic) FindLastList(beginTime string, limit int) ([]*model.Topic, error) { + topics := make([]*model.Topic, 0) + err := MasterDB.Where("ctime>? AND flag IN(?,?)", beginTime, model.FlagNoAudit, model.FlagNormal). + OrderBy("tid DESC").Limit(limit).Find(&topics) + + return topics, err +} + +// FindRecent 获得最近的主题(uids[0],则获取某个用户最近的主题) +func (self TopicLogic) FindRecent(limit int, uids ...int) []*model.Topic { + dbSession := MasterDB.OrderBy("ctime DESC").Limit(limit) + if len(uids) > 0 { + dbSession.Where("uid=?", uids[0]) + } + self.addFlagWhere(dbSession) + + topics := make([]*model.Topic, 0) + if err := dbSession.Find(&topics); err != nil { + logger.Errorln("TopicLogic FindRecent error:", err) + } + for _, topic := range topics { + topic.Node = GetNodeName(topic.Nid) + } + return topics +} + +// FindByNid 获得某个节点下的主题列表(侧边栏推荐) +func (TopicLogic) FindByNid(ctx context.Context, nid, curTid string) []*model.Topic { + objLog := GetLogger(ctx) + + topics := make([]*model.Topic, 0) + err := MasterDB.Where("nid=? AND tid!=? AND flag", nid, curTid, model.FlagAuditDelete).Limit(10).Find(&topics) + if err != nil { + objLog.Errorln("TopicLogic FindByNid Error:", err) + } + + return topics +} + +// FindByTids 获取多个主题详细信息 +func (TopicLogic) FindByTids(tids []int) []*model.Topic { + if len(tids) == 0 { + return nil + } + + topics := make([]*model.Topic, 0) + err := MasterDB.In("tid", tids).Find(&topics) + if err != nil { + logger.Errorln("TopicLogic FindByTids error:", err) + return nil + } + return topics +} + +func (self TopicLogic) FindFullinfoByTids(tids []int) []map[string]interface{} { + topicInfoMap := make(map[int]*model.TopicInfo, 0) + + err := MasterDB.Join("INNER", "topics_ex", "topics.tid=topics_ex.tid").In("topics.tid", tids).Find(&topicInfoMap) + if err != nil { + logger.Errorln("TopicLogic FindFullinfoByTids error:", err) + return nil + } + + topicInfos := make([]*model.TopicInfo, 0, len(topicInfoMap)) + for _, tid := range tids { + if topicInfo, ok := topicInfoMap[tid]; ok { + if topicInfo.Flag > model.FlagNormal { + continue + } + topicInfos = append(topicInfos, topicInfo) + } + } + + return self.fillDataForTopicInfo(topicInfos) +} + +// FindByTid 获得主题详细信息(包括详细回复) +func (self TopicLogic) FindByTid(ctx context.Context, tid int) (topicMap map[string]interface{}, replies []map[string]interface{}, err error) { + objLog := GetLogger(ctx) + + topicInfo := &model.TopicInfo{} + _, err = MasterDB.Join("INNER", "topics_ex", "topics.tid=topics_ex.tid").Where("topics.tid=?", tid).Get(topicInfo) + if err != nil { + objLog.Errorln("TopicLogic FindByTid get error:", err) + return + } + + topic := &topicInfo.Topic + + if topic.Tid == 0 { + err = errors.New("The topic of tid is not exists") + objLog.Errorln("TopicLogic FindByTid get error:", err) + return + } + + if topic.Flag > model.FlagNormal { + err = errors.New("The topic of tid is not exists or delete") + return + } + + topicMap = make(map[string]interface{}) + structs.FillMap(topic, topicMap) + structs.FillMap(topicInfo.TopicEx, topicMap) + + // 解析内容中的 @ + topicMap["content"] = self.decodeTopicContent(ctx, topic) + + // 节点 + topicMap["node"] = GetNode(topic.Nid) + + // 回复信息(评论) + replies, owerUser, lastReplyUser := DefaultComment.FindObjComments(ctx, topic.Tid, model.TypeTopic, topic.Uid, topic.Lastreplyuid) + topicMap["user"] = owerUser + // 有人回复 + if topic.Lastreplyuid != 0 { + topicMap["lastreplyusername"] = lastReplyUser.Username + } + + if topic.EditorUid != 0 { + editorUser := DefaultUser.FindOne(ctx, "uid", topic.EditorUid) + topicMap["editor_username"] = editorUser.Username + } + + return +} + +// 获取列表(分页):后台用 +func (TopicLogic) FindByPage(ctx context.Context, conds map[string]string, curPage, limit int) ([]*model.Topic, int) { + objLog := GetLogger(ctx) + + session := MasterDB.NewSession() + + for k, v := range conds { + session.And(k+"=?", v) + } + + totalSession := SessionClone(session) + + offset := (curPage - 1) * limit + topicList := make([]*model.Topic, 0) + err := session.OrderBy("tid DESC").Limit(limit, offset).Find(&topicList) + if err != nil { + objLog.Errorln("find error:", err) + return nil, 0 + } + + total, err := totalSession.Count(new(model.Topic)) + if err != nil { + objLog.Errorln("find count error:", err) + return nil, 0 + } + + return topicList, int(total) +} + +func (TopicLogic) FindAppend(ctx context.Context, tid int) []*model.TopicAppend { + objLog := GetLogger(ctx) + + topicAppends := make([]*model.TopicAppend, 0) + err := MasterDB.Where("tid=?", tid).Find(&topicAppends) + if err != nil { + objLog.Errorln("TopicLogic FindAppend error:", err) + } + + return topicAppends +} + +func (TopicLogic) findByTid(tid int) *model.Topic { + topic := &model.Topic{} + _, err := MasterDB.Where("tid=?", tid).Get(topic) + if err != nil { + logger.Errorln("TopicLogic findByTid error:", err) + } + return topic +} + +// findByTids 获取多个主题详细信息 包内用 +func (TopicLogic) findByTids(tids []int) map[int]*model.Topic { + if len(tids) == 0 { + return nil + } + + topics := make(map[int]*model.Topic) + err := MasterDB.In("tid", tids).Find(&topics) + if err != nil { + logger.Errorln("TopicLogic findByTids error:", err) + return nil + } + return topics +} + +func (TopicLogic) fillDataForTopicInfo(topicInfos []*model.TopicInfo) []map[string]interface{} { + uidSet := set.New(set.NonThreadSafe) + nidSet := set.New(set.NonThreadSafe) + for _, topicInfo := range topicInfos { + uidSet.Add(topicInfo.Uid) + if topicInfo.Lastreplyuid != 0 { + uidSet.Add(topicInfo.Lastreplyuid) + } + nidSet.Add(topicInfo.Nid) + } + + usersMap := DefaultUser.FindUserInfos(nil, set.IntSlice(uidSet)) + // 获取节点信息 + nodes := GetNodesByNids(set.IntSlice(nidSet)) + + data := make([]map[string]interface{}, len(topicInfos)) + + for i, topicInfo := range topicInfos { + dest := make(map[string]interface{}) + + // 有人回复 + if topicInfo.Lastreplyuid != 0 { + if user, ok := usersMap[topicInfo.Lastreplyuid]; ok { + dest["lastreplyusername"] = user.Username + } + } + + structs.FillMap(topicInfo.Topic, dest) + structs.FillMap(topicInfo.TopicEx, dest) + + dest["user"] = usersMap[topicInfo.Uid] + dest["node"] = nodes[topicInfo.Nid] + + data[i] = dest + } + + return data +} + +var ( + hotNodesCache []map[string]interface{} + hotNodesBegin time.Time + hotNodesLocker sync.Mutex +) + +// FindHotNodes 获得热门节点 +func (TopicLogic) FindHotNodes(ctx context.Context) []map[string]interface{} { + hotNodesLocker.Lock() + defer hotNodesLocker.Unlock() + if !hotNodesBegin.IsZero() && hotNodesBegin.Add(1*time.Hour).Before(time.Now()) { + return hotNodesCache + } + + objLog := GetLogger(ctx) + + hotNum := 10 + + lastWeek := time.Now().Add(-7 * 24 * time.Hour).Format("2006-01-02 15:04:05") + strSql := fmt.Sprintf("SELECT nid, COUNT(1) AS topicnum FROM topics WHERE ctime>='%s' GROUP BY nid ORDER BY topicnum DESC LIMIT 15", lastWeek) + rows, err := MasterDB.DB().DB.Query(strSql) + if err != nil { + objLog.Errorln("TopicLogic FindHotNodes error:", err) + return nil + } + + nids := make([]int, 0, 15) + for rows.Next() { + var nid, topicnum int + err = rows.Scan(&nid, &topicnum) + if err != nil { + objLog.Errorln("rows.Scan error:", err) + continue + } + + nids = append(nids, nid) + } + + nodes := make([]map[string]interface{}, 0, hotNum) + + topicNodes := GetNodesByNids(nids) + for _, nid := range nids { + topicNode := topicNodes[nid] + if !topicNode.ShowIndex { + continue + } + + node := map[string]interface{}{ + "name": topicNode.Name, + "ename": topicNode.Ename, + "nid": topicNode.Nid, + } + nodes = append(nodes, node) + if len(nodes) == hotNum { + break + } + } + + hotNodesCache = nodes + hotNodesBegin = time.Now() + + return nodes +} + +// Total 话题总数 +func (TopicLogic) Total() int64 { + total, err := MasterDB.Count(new(model.Topic)) + if err != nil { + logger.Errorln("TopicLogic Total error:", err) + } + return total +} + +// JSEscape 安全过滤 +func (TopicLogic) JSEscape(topics []*model.Topic) []*model.Topic { + for i, topic := range topics { + topics[i].Title = template.JSEscapeString(topic.Title) + topics[i].Content = template.JSEscapeString(topic.Content) + } + return topics +} + +func (TopicLogic) Count(ctx context.Context, querystring string, args ...interface{}) int64 { + objLog := GetLogger(ctx) + + session := MasterDB.Where("flag", model.FlagAuditDelete) + var ( + total int64 + err error + ) + if querystring == "" { + total, err = session.Count(new(model.Topic)) + } else { + total, err = session.Where(querystring, args...).Count(new(model.Topic)) + } + + if err != nil { + objLog.Errorln("TopicLogic Count error:", err) + } + + return total +} + +// getOwner 通过tid获得话题的所有者 +func (TopicLogic) getOwner(tid int) int { + topic := &model.Topic{} + _, err := MasterDB.ID(tid).Get(topic) + if err != nil { + logger.Errorln("topic logic getOwner Error:", err) + return 0 + } + return topic.Uid +} + +func (TopicLogic) decodeTopicContent(ctx context.Context, topic *model.Topic) string { + // 允许内嵌 Wide iframe + content := util.EmbedWide(topic.Content) + + // @别人 + return parseAtUser(ctx, content) +} + +func (TopicLogic) addFlagWhere(session *xorm.Session) { + session.Where("flag", model.FlagAuditDelete) +} + +// 话题回复(评论) +type TopicComment struct{} + +// UpdateComment 更新该主题的回复信息 +// cid:评论id;objid:被评论对象id;uid:评论者;cmttime:评论时间 +func (self TopicComment) UpdateComment(cid, objid, uid int, cmttime time.Time) { + session := MasterDB.NewSession() + defer session.Close() + + session.Begin() + + // 更新最后回复信息 + _, err := session.Table(new(model.Topic)).ID(objid).Update(map[string]interface{}{ + "lastreplyuid": uid, + "lastreplytime": cmttime, + }) + if err != nil { + logger.Errorln("更新主题最后回复人信息失败:", err) + session.Rollback() + return + } + + // 更新回复数(TODO:暂时每次都更新表) + _, err = session.ID(objid).Incr("reply", 1).Update(new(model.TopicUpEx)) + if err != nil { + logger.Errorln("更新主题回复数失败:", err) + session.Rollback() + return + } + + session.Commit() +} + +func (self TopicComment) String() string { + return "topic" +} + +// 实现 CommentObjecter 接口 +func (self TopicComment) SetObjinfo(ids []int, commentMap map[int][]*model.Comment) { + + topics := DefaultTopic.FindByTids(ids) + if len(topics) == 0 { + return + } + + for _, topic := range topics { + if topic.Flag > model.FlagNormal { + continue + } + objinfo := make(map[string]interface{}) + objinfo["title"] = topic.Title + objinfo["uri"] = model.PathUrlMap[model.TypeTopic] + objinfo["type_name"] = model.TypeNameMap[model.TypeTopic] + + for _, comment := range commentMap[topic.Tid] { + comment.Objinfo = objinfo + } + } +} + +// 主题喜欢 +type TopicLike struct{} + +// 更新该主题的喜欢数 +// objid:被喜欢对象id;num: 喜欢数(负数表示取消喜欢) +func (self TopicLike) UpdateLike(objid, num int) { + // 更新喜欢数(TODO:暂时每次都更新表) + _, err := MasterDB.Where("tid=?", objid).Incr("like", num).Update(new(model.TopicUpEx)) + if err != nil { + logger.Errorln("更新主题喜欢数失败:", err) + } +} + +func (self TopicLike) String() string { + return "topic" +} diff --git a/internal/logic/topic_node.go b/internal/logic/topic_node.go new file mode 100644 index 00000000..a176d995 --- /dev/null +++ b/internal/logic/topic_node.go @@ -0,0 +1,148 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "context" + "net/url" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" +) + +type TopicNodeLogic struct{} + +var DefaultNode = TopicNodeLogic{} + +func (self TopicNodeLogic) FindOne(nid int) *model.TopicNode { + topicNode := &model.TopicNode{} + _, err := MasterDB.ID(nid).Get(topicNode) + if err != nil { + logger.Errorln("TopicNodeLogic FindOne error:", err, "nid:", nid) + } + + return topicNode +} + +func (self TopicNodeLogic) FindByEname(ename string) *model.TopicNode { + topicNode := &model.TopicNode{} + _, err := MasterDB.Where("ename=?", ename).Get(topicNode) + if err != nil { + logger.Errorln("TopicNodeLogic FindByEname error:", err, "ename:", ename) + } + + return topicNode +} + +func (self TopicNodeLogic) FindByNids(nids []int) map[int]*model.TopicNode { + nodeList := make(map[int]*model.TopicNode, 0) + err := MasterDB.In("nid", nids).Find(&nodeList) + if err != nil { + logger.Errorln("TopicNodeLogic FindByNids error:", err, "nids:", nids) + } + + return nodeList +} + +func (self TopicNodeLogic) FindByParent(pid, num int) []*model.TopicNode { + nodeList := make([]*model.TopicNode, 0) + err := MasterDB.Where("parent=?", pid).Limit(num).Find(&nodeList) + if err != nil { + logger.Errorln("TopicNodeLogic FindByParent error:", err, "parent:", pid) + } + + return nodeList +} + +func (self TopicNodeLogic) FindAll(ctx context.Context) []*model.TopicNode { + nodeList := make([]*model.TopicNode, 0) + err := MasterDB.Asc("seq").Find(&nodeList) + if err != nil { + logger.Errorln("TopicNodeLogic FindAll error:", err) + } + + return nodeList +} + +func (self TopicNodeLogic) Modify(ctx context.Context, form url.Values) error { + objLog := GetLogger(ctx) + + node := &model.TopicNode{} + err := schemaDecoder.Decode(node, form) + if err != nil { + objLog.Errorln("TopicNodeLogic Modify decode error:", err) + return err + } + + nid := goutils.MustInt(form.Get("nid")) + if nid == 0 { + // 新增 + _, err = MasterDB.Insert(node) + if err != nil { + objLog.Errorln("TopicNodeLogic Modify insert error:", err) + } + return err + } + + change := make(map[string]interface{}) + + fields := []string{"parent", "logo", "name", "ename", "intro", "seq", "show_index"} + for _, field := range fields { + change[field] = form.Get(field) + } + + _, err = MasterDB.Table(new(model.TopicNode)).ID(nid).Update(change) + if err != nil { + objLog.Errorln("TopicNodeLogic Modify update error:", err) + } + return err +} + +func (self TopicNodeLogic) ModifySeq(ctx context.Context, nid, seq int) error { + _, err := MasterDB.Table(new(model.TopicNode)).ID(nid).Update(map[string]interface{}{"seq": seq}) + return err +} + +func (self TopicNodeLogic) FindParallelTree(ctx context.Context) []*model.TopicNode { + nodeList := make([]*model.TopicNode, 0) + err := MasterDB.Asc("parent").Asc("seq").Find(&nodeList) + if err != nil { + logger.Errorln("TopicNodeLogic FindTreeList error:", err) + + return nil + } + + showNodeList := make([]*model.TopicNode, 0, len(nodeList)) + self.tileNodes(&showNodeList, nodeList, 0, 1, 3, 0) + + return showNodeList +} + +func (self TopicNodeLogic) tileNodes(showNodeList *[]*model.TopicNode, nodeList []*model.TopicNode, parentId, curLevel, showLevel, pos int) { + for num := len(nodeList); pos < num; pos++ { + node := nodeList[pos] + + if node.Parent == parentId { + *showNodeList = append(*showNodeList, node) + + if node.Level == 0 { + node.Level = curLevel + } + + if curLevel <= showLevel { + self.tileNodes(showNodeList, nodeList, node.Nid, curLevel+1, showLevel, pos+1) + } + } + + if node.Parent > parentId { + break + } + } +} diff --git a/internal/logic/topic_node_test.go b/internal/logic/topic_node_test.go new file mode 100644 index 00000000..dd3b7d39 --- /dev/null +++ b/internal/logic/topic_node_test.go @@ -0,0 +1,20 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package logic_test + +import ( + "testing" +) + +func TestFindParallelTree(t *testing.T) { + // treeNodes := logic.DefaultNode.FindParallelTree(nil) + // for _, node := range treeNodes { + // fmt.Printf("%+v\n", node) + // } + + // fmt.Println(len(treeNodes)) +} diff --git a/internal/logic/topic_test.go b/internal/logic/topic_test.go new file mode 100644 index 00000000..dd5ba443 --- /dev/null +++ b/internal/logic/topic_test.go @@ -0,0 +1,18 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package logic_test + +import "testing" + +func TestFindAll(t *testing.T) { + // paginator := logic.NewPaginator(2) + // topicsMap := logic.DefaultTopic.FindAll(nil, paginator, "", "") + // if len(topicsMap) == 0 { + // t.Fatal(topicsMap) + // } + // t.Fatal(topicsMap) +} diff --git a/internal/logic/uploader.go b/internal/logic/uploader.go new file mode 100644 index 00000000..78b34f63 --- /dev/null +++ b/internal/logic/uploader.go @@ -0,0 +1,283 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package logic + +import ( + "bytes" + "encoding/json" + "errors" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + gio "io" + "io/ioutil" + "mime" + "net/http" + "path/filepath" + "strings" + "sync" + "time" + + "golang.org/x/net/context" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/times" + "github.com/qiniu/api.v6/conf" + "github.com/qiniu/api.v6/io" + "github.com/qiniu/api.v6/rs" +) + +const ( + MaxImageSize = 5 << 20 // 5M +) + +// http://developer.qiniu.com/code/v6/sdk/go-sdk-6.html +type UploaderLogic struct { + bucketName string + + uptoken string + tokenTime time.Time + locker sync.RWMutex +} + +var DefaultUploader = &UploaderLogic{} + +func (this *UploaderLogic) InitQiniu() { + conf.ACCESS_KEY = config.ConfigFile.MustValue("qiniu", "access_key") + conf.SECRET_KEY = config.ConfigFile.MustValue("qiniu", "secret_key") + conf.UP_HOST = config.ConfigFile.MustValue("qiniu", "up_host", conf.UP_HOST) + this.bucketName = config.ConfigFile.MustValue("qiniu", "bucket_name") +} + +// 生成上传凭证 +func (this *UploaderLogic) genUpToken() { + // 避免服务器时间不同步,45分钟就更新 token + if this.uptoken != "" && this.tokenTime.Add(45*time.Minute).After(time.Now()) { + return + } + + putPolicy := rs.PutPolicy{ + Scope: this.bucketName, + // CallbackUrl: callbackUrl, + // CallbackBody: callbackBody, + // ReturnUrl: returnUrl, + // ReturnBody: returnBody, + // AsyncOps: asyncOps, + // EndUser: endUser, + // 指定上传凭证有效期(默认1小时) + // Expires: expires, + } + + this.locker.Lock() + this.uptoken = putPolicy.Token(nil) + this.locker.Unlock() + this.tokenTime = time.Now() +} + +func (this *UploaderLogic) uploadLocalFile(localFile, key string) (err error) { + this.genUpToken() + + var ret io.PutRet + var extra = &io.PutExtra{ + // Params: params, + // MimeType: mieType, + // Crc32: crc32, + // CheckCrc: CheckCrc, + } + + // ret 变量用于存取返回的信息,详情见 io.PutRet + // uptoken 为业务服务器生成的上传口令 + // key 为文件存储的标识(文件名) + // localFile 为本地文件名 + // extra 为上传文件的额外信息,详情见 io.PutExtra,可选 + err = io.PutFile(nil, &ret, this.uptoken, key, localFile, extra) + + if err != nil { + //上传产生错误 + logger.Errorln("io.PutFile failed:", err) + return + } + + //上传成功,处理返回值 + logger.Debugln(ret.Hash, ret.Key) + + return +} + +func (this *UploaderLogic) uploadMemoryFile(r gio.Reader, key string, size int) (err error) { + this.genUpToken() + + var ret io.PutRet + var extra = &io.PutExtra{ + // Params: params, + // MimeType: mieType, + // Crc32: crc32, + // CheckCrc: CheckCrc, + } + + // ret 变量用于存取返回的信息,详情见 io.PutRet + // uptoken 为业务服务器端生成的上传口令 + // key 为文件存储的标识 + // r 为io.Reader类型,用于从其读取数据 + // extra 为上传文件的额外信息,可为空, 详情见 io.PutExtra, 可选 + err = io.Put2(nil, &ret, this.uptoken, key, r, int64(size), extra) + + // 上传产生错误 + if err != nil { + logger.Errorln("io.Put failed:", err) + + errInfo := make(map[string]interface{}) + err = json.Unmarshal([]byte(err.Error()), &errInfo) + if err != nil { + logger.Errorln("io.Put Unmarshal failed:", err) + return + } + + code, ok := errInfo["code"] + if ok && code == 614 { + err = nil + } + + return + } + + // 上传成功,处理返回值 + logger.Debugln(ret.Hash, ret.Key) + + return +} + +func (this *UploaderLogic) UploadImage(ctx context.Context, reader gio.Reader, imgDir string, buf []byte, ext string) (string, error) { + objLogger := GetLogger(ctx) + + md5 := goutils.Md5Buf(buf) + objImage, err := this.findImage(md5) + if err != nil { + objLogger.Errorln("find image:", md5, "error:", err) + return "", err + } + + if objImage.Pid > 0 { + return objImage.Path, nil + } + + path := imgDir + "/" + md5 + ext + if err = this.uploadMemoryFile(reader, path, len(buf)); err != nil { + return "", err + } + + go this.saveImage(buf, path) + + return path, nil +} + +// TransferUrl 将外站图片URL转为本站,如果失败,返回原图 +func (this *UploaderLogic) TransferUrl(ctx context.Context, origUrl string, prefixs ...string) (string, error) { + if origUrl == "" || strings.Contains(origUrl, WebsiteSetting.Domain) { + return origUrl, errors.New("origin image is empty or is " + WebsiteSetting.Domain) + } + + if !strings.HasPrefix(origUrl, "http") { + origUrl = "https:" + origUrl + } + + resp, err := http.Get(origUrl) + if err != nil { + return origUrl, errors.New("获取图片失败") + } + defer resp.Body.Close() + + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + return origUrl, errors.New("获取图片内容失败") + } + + md5 := goutils.Md5Buf(buf) + objImage, err := this.findImage(md5) + if err != nil { + logger.Errorln("find image:", md5, "error:", err) + return origUrl, err + } + + if objImage.Pid > 0 { + return objImage.Path, nil + } + + ext := filepath.Ext(origUrl) + if ext == "" { + contentType := http.DetectContentType(buf) + exts, err := mime.ExtensionsByType(contentType) + if err != nil { + logger.Errorln("detect extension error:", err, "orig url:", origUrl) + } else if len(exts) > 0 { + ext = exts[0] + } + } + + if ext == "" && !strings.Contains("png,jpg,jpeg,gif,bmp", strings.ToLower(ext)) { + logger.Errorln("can't fetch extension, url:", origUrl) + return origUrl, errors.New("can't fetch extension") + } + + prefix := times.Format("ymd") + if len(prefixs) > 0 { + prefix = prefixs[0] + } + path := prefix + "/" + md5 + ext + reader := bytes.NewReader(buf) + + if len(buf) > MaxImageSize { + return origUrl, errors.New("文件太大") + } + + err = this.uploadMemoryFile(reader, path, len(buf)) + if err != nil { + return origUrl, err + } + + go this.saveImage(buf, path) + + return path, nil +} + +func (this *UploaderLogic) findImage(md5 string) (*model.Image, error) { + objImage := &model.Image{} + _, err := MasterDB.Where("md5=?", md5).Get(objImage) + if err != nil { + return nil, err + } + + return objImage, nil +} + +func (this *UploaderLogic) saveImage(buf []byte, path string) { + objImage := &model.Image{ + Path: path, + Md5: goutils.Md5Buf(buf), + Size: len(buf), + } + + reader := bytes.NewReader(buf) + img, _, err := image.Decode(reader) + if err != nil { + logger.Errorln("image decode err:", err) + } else { + objImage.Width = img.Bounds().Dx() + objImage.Height = img.Bounds().Dy() + } + + _, err = MasterDB.Insert(objImage) + if err != nil { + logger.Errorln("image insert err:", err) + } +} diff --git a/internal/logic/user.go b/internal/logic/user.go new file mode 100644 index 00000000..60f6da76 --- /dev/null +++ b/internal/logic/user.go @@ -0,0 +1,850 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "errors" + "fmt" + "math/rand" + "net/url" + "strconv" + "strings" + "time" + + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/polaris1119/times" + + "github.com/polaris1119/slices" + + "github.com/go-validator/validator" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "golang.org/x/net/context" + "xorm.io/xorm" + + . "github.com/studygolang/studygolang/db" +) + +type UserLogic struct{} + +var DefaultUser = UserLogic{} + +// CreateUser 创建用户 +func (self UserLogic) CreateUser(ctx context.Context, form url.Values) (errMsg string, err error) { + objLog := GetLogger(ctx) + + if self.UserExists(ctx, "email", form.Get("email")) { + errMsg = "该邮箱已注册过" + err = errors.New(errMsg) + return + } + if self.UserExists(ctx, "username", form.Get("username")) { + errMsg = "用户名已存在" + err = errors.New(errMsg) + return + } + + user := &model.User{} + err = schemaDecoder.Decode(user, form) + if err != nil { + objLog.Errorln("user schema Decode error:", err) + errMsg = err.Error() + return + } + + if err = validator.Validate(user); err != nil { + objLog.Errorf("validate user error:%#v", err) + + // TODO: 暂时简单处理 + if errMap, ok := err.(validator.ErrorMap); ok { + if _, ok = errMap["Username"]; ok { + errMsg = "用户名不合法!" + } + } else { + errMsg = err.Error() + } + return + } + + if config.ConfigFile.MustBool("account", "verify_email", true) { + if !user.IsRoot { + // 避免前端伪造,传递 status=1 + user.Status = model.UserStatusNoAudit + } + } else { + user.Status = model.UserStatusAudit + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + err = self.doCreateUser(ctx, session, user, form.Get("passwd")) + if err != nil { + errMsg = "内部服务错误!" + session.Rollback() + objLog.Errorln("create user error:", err) + return + } + + if form.Get("id") != "" { + id := goutils.MustInt(form.Get("id")) + _, err = DefaultWechat.Bind(ctx, id, user.Uid, form.Get("userInfo")) + if err != nil { + session.Rollback() + objLog.Errorln("bind wechat user error:", err) + errMsg = err.Error() + return + } + } + + session.Commit() + + return +} + +// Update 更新用户信息 +func (self UserLogic) Update(ctx context.Context, me *model.Me, form url.Values) (errMsg string, err error) { + objLog := GetLogger(ctx) + + if form.Get("open") != "1" { + form.Set("open", "0") + } + + user := &model.User{} + err = schemaDecoder.Decode(user, form) + if err != nil { + objLog.Errorln("userlogic update, schema decode error:", err) + errMsg = "服务内部错误" + return + } + + cols := "name,open,city,company,github,weibo,website,monlog,introduce" + // 变更了邮箱 + if user.Email != me.Email { + cols += ",email,status" + user.Status = model.UserStatusNoAudit + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + _, err = session.ID(me.Uid).Cols(cols).Update(user) + if err != nil { + session.Rollback() + + objLog.Errorf("更新用户 【%d】 信息失败:%s", me.Uid, err) + if strings.Contains(err.Error(), "Error 1062: Duplicate entry") { + // TODO:被恶意注册? + errMsg = "该邮箱地址被其他账号注册了" + } else { + errMsg = "对不起,服务器内部错误,请稍后再试!" + } + return + } + + _, err = session.Table(new(model.UserLogin)). + Where("uid=?", me.Uid).Update(map[string]interface{}{"email": me.Email}) + if err != nil { + session.Rollback() + objLog.Errorf("更新用户 【%d】 信息失败:%s", me.Uid, err) + errMsg = "对不起,服务器内部错误,请稍后再试!" + return + } + + session.Commit() + + // 修改用户资料,活跃度+1 + go self.IncrUserWeight("uid", me.Uid, 1) + + return +} + +// UpdateUserStatus 更新用户状态 +func (UserLogic) UpdateUserStatus(ctx context.Context, uid, status int) error { + objLog := GetLogger(ctx) + + _, err := MasterDB.Table(new(model.User)).ID(uid).Update(map[string]interface{}{"status": status}) + if err != nil { + objLog.Errorf("更新用户 【%d】 状态失败:%s", uid, err) + } + + return err +} + +// ChangeAvatar 更换头像 +func (UserLogic) ChangeAvatar(ctx context.Context, uid int, avatar string) (err error) { + changeData := map[string]interface{}{"avatar": avatar} + _, err = MasterDB.Table(new(model.User)).ID(uid).Update(changeData) + if err == nil { + _, err = MasterDB.Table(new(model.UserActive)).ID(uid).Update(changeData) + } + + return +} + +// UserExists 判断用户是否存在 +func (UserLogic) UserExists(ctx context.Context, field, val string) bool { + objLog := GetLogger(ctx) + + userLogin := &model.UserLogin{} + _, err := MasterDB.Where(field+"=?", val).Get(userLogin) + if err != nil || userLogin.Uid == 0 { + if err != nil { + objLog.Errorln("user logic UserExists error:", err) + } + return false + } + return true +} + +// EmailOrUsernameExists 判断指定的邮箱(email)或用户名是否存在 +func (UserLogic) EmailOrUsernameExists(ctx context.Context, email, username string) bool { + objLog := GetLogger(ctx) + + userLogin := &model.UserLogin{} + _, err := MasterDB.Where("email=?", email).Or("username=?", username).Get(userLogin) + if err != nil || userLogin.Uid == 0 { + if err != nil { + objLog.Errorln("user logic EmailOrUsernameExists error:", err) + } + return false + } + return true +} + +// FindUserInfos 获得用户信息,uniq 可能是 uid slice 或 username slice +func (self UserLogic) FindUserInfos(ctx context.Context, uniq interface{}) map[int]*model.User { + objLog := GetLogger(ctx) + + field := "uid" + if uids, ok := uniq.([]int); ok { + if len(uids) == 0 { + return nil + } + } else if usernames, ok := uniq.([]string); ok { + if len(usernames) == 0 { + return nil + } + field = "username" + } + + usersMap := make(map[int]*model.User) + if err := MasterDB.In(field, uniq).Find(&usersMap); err != nil { + objLog.Errorln("user logic FindUserInfos not record found:", err) + return nil + } + return usersMap +} + +func (self UserLogic) FindOne(ctx context.Context, field string, val interface{}) *model.User { + objLog := GetLogger(ctx) + + user := &model.User{} + _, err := MasterDB.Where(field+"=?", val).Get(user) + if err != nil { + objLog.Errorln("user logic FindOne error:", err) + } + + if user.Uid != 0 { + if user.IsRoot { + user.Roleids = []int{0} + user.Rolenames = []string{"站长"} + return user + } + + // 获取用户角色信息 + userRoleList := make([]*model.UserRole, 0) + err = MasterDB.Where("uid=?", user.Uid).OrderBy("roleid ASC").Find(&userRoleList) + if err != nil { + objLog.Errorf("获取用户 %s 角色 信息失败:%s", val, err) + return nil + } + + if roleNum := len(userRoleList); roleNum > 0 { + user.Roleids = make([]int, roleNum) + user.Rolenames = make([]string, roleNum) + + for i, userRole := range userRoleList { + user.Roleids[i] = userRole.Roleid + user.Rolenames[i] = Roles[userRole.Roleid-1].Name + } + } + } + return user +} + +// 获取当前登录用户信息(常用信息) +func (self UserLogic) FindCurrentUser(ctx context.Context, username interface{}) *model.Me { + objLog := GetLogger(ctx) + + user := &model.User{} + var err error + + if uid, ok := username.(int); ok { + _, err = MasterDB.Where("uid=? AND status<=?", uid, model.UserStatusAudit).Get(user) + } else { + _, err = MasterDB.Where("username=? AND status<=?", username, model.UserStatusAudit).Get(user) + } + + if err != nil { + objLog.Errorf("获取用户 %q 信息失败:%s", username, err) + return &model.Me{} + } + if user.Uid == 0 { + logger.Infof("用户 %q 不存在或状态不正常!", username) + return &model.Me{} + } + + isVip := user.IsVip + if user.VipExpire < goutils.MustInt(times.Format("Ymd")) { + isVip = false + } + + me := &model.Me{ + Uid: user.Uid, + Username: user.Username, + Name: user.Name, + Monlog: user.Monlog, + Email: user.Email, + Avatar: user.Avatar, + Status: user.Status, + IsRoot: user.IsRoot, + MsgNum: DefaultMessage.FindNotReadMsgNum(ctx, user.Uid), + DauAuth: user.DauAuth, + IsVip: isVip, + CreatedAt: time.Time(user.Ctime), + + Balance: user.Balance, + Gold: user.Gold, + Silver: user.Silver, + Copper: user.Copper, + + RoleIds: make([]int, 0, 2), + } + + // TODO: 先每次都记录登录时间 + ip := ctx.Value("ip") + go self.RecordLogin(user.Username, ip) + + if user.IsRoot { + me.IsAdmin = true + return me + } + + // 获取角色信息 + userRoleList := make([]*model.UserRole, 0) + err = MasterDB.Where("uid=?", user.Uid).Find(&userRoleList) + if err != nil { + logger.Errorf("获取用户 %q 角色 信息失败:%s", username, err) + return me + } + for _, userRole := range userRoleList { + me.RoleIds = append(me.RoleIds, userRole.Roleid) + + if userRole.Roleid <= model.AdminMinRoleId { + // 是管理员 + me.IsAdmin = true + } + } + + return me +} + +// findUsers 获得用户信息,包内使用。 +// s 是包含用户 UID 的二维数组 +func (self UserLogic) findUsers(ctx context.Context, s interface{}) []*model.User { + objLog := GetLogger(ctx) + + uids := slices.StructsIntSlice(s, "Uid") + + users := make([]*model.User, 0) + if err := MasterDB.In("uid", uids).Find(&users); err != nil { + objLog.Errorln("user logic findUsers not record found:", err) + return nil + } + return users +} + +func (self UserLogic) findUser(ctx context.Context, uid int) *model.User { + objLog := GetLogger(ctx) + + user := &model.User{} + _, err := MasterDB.ID(uid).Get(user) + if err != nil { + objLog.Errorln("user logic findUser not record found:", err) + } + + return user +} + +// 会员总数 +func (UserLogic) Total() int64 { + total, err := MasterDB.Count(new(model.User)) + if err != nil { + logger.Errorln("UserLogic Total error:", err) + } + return total +} + +func (UserLogic) IsAdmin(user *model.User) bool { + if user.IsRoot { + return true + } + + for _, roleId := range user.Roleids { + if roleId <= model.AdminMinRoleId { + return true + } + } + + return false +} + +var ( + ErrUsername = errors.New("用户名不存在") + ErrPasswd = errors.New("密码错误") +) + +// Login 登录;成功返回用户登录信息(user_login) +func (self UserLogic) Login(ctx context.Context, username, passwd string) (*model.UserLogin, error) { + objLog := GetLogger(ctx) + + userLogin := &model.UserLogin{} + _, err := MasterDB.Where("username=? OR email=?", username, username).Get(userLogin) + if err != nil { + objLog.Errorf("user %q login failure: %s", username, err) + return nil, errors.New("内部错误,请稍后再试!") + } + // 校验用户 + if userLogin.Uid == 0 { + objLog.Infof("user %q is not exists!", username) + return nil, ErrUsername + } + + // 检验用户状态是否正常(未激活的可以登录,但不能发布信息) + user := &model.User{} + MasterDB.ID(userLogin.Uid).Get(user) + if user.Status > model.UserStatusAudit { + objLog.Infof("用户 %q 的状态非审核通过, 用户的状态值:%d", username, user.Status) + var errMap = map[int]error{ + model.UserStatusRefuse: errors.New("您的账号审核拒绝"), + model.UserStatusFreeze: errors.New("您的账号因为非法发布信息已被冻结,请联系管理员!"), + model.UserStatusOutage: errors.New("您的账号因为非法发布信息已被停号,请联系管理员!"), + } + return nil, errMap[user.Status] + } + + md5Passwd := goutils.Md5(passwd + userLogin.Passcode) + if md5Passwd != userLogin.Passwd { + objLog.Infof("用户名 %q 填写的密码错误", username) + return nil, ErrPasswd + } + + go func() { + self.IncrUserWeight("uid", userLogin.Uid, 1) + ip := ctx.Value("ip") + self.RecordLogin(username, ip) + }() + + return userLogin, nil +} + +// UpdatePasswd 更新用户密码 +func (self UserLogic) UpdatePasswd(ctx context.Context, username, curPasswd, newPasswd string) (string, error) { + userLogin := &model.UserLogin{} + _, err := MasterDB.Where("username=?", username).Get(userLogin) + if err != nil { + return "用户不存在", err + } + + if userLogin.Passwd != "" { + _, err = self.Login(ctx, username, curPasswd) + if err != nil { + return "原密码填写错误", err + } + } + + userLogin = &model.UserLogin{ + Passwd: newPasswd, + } + err = userLogin.GenMd5Passwd() + if err != nil { + return err.Error(), err + } + + changeData := map[string]interface{}{ + "passwd": userLogin.Passwd, + "passcode": userLogin.Passcode, + } + _, err = MasterDB.Table(userLogin).Where("username=?", username).Update(changeData) + if err != nil { + logger.Errorf("用户 %s 更新密码错误:%s", username, err) + return "对不起,内部服务错误!", err + } + return "", nil +} + +func (UserLogic) HasPasswd(ctx context.Context, uid int) bool { + userLogin := &model.UserLogin{} + _, err := MasterDB.Where("uid=?", uid).Get(userLogin) + if err == nil && userLogin.Passwd != "" { + return true + } + + return false +} + +func (self UserLogic) ResetPasswd(ctx context.Context, email, passwd string) (string, error) { + objLog := GetLogger(ctx) + + userLogin := &model.UserLogin{ + Passwd: passwd, + } + err := userLogin.GenMd5Passwd() + if err != nil { + return err.Error(), err + } + + changeData := map[string]interface{}{ + "passwd": userLogin.Passwd, + "passcode": userLogin.Passcode, + } + _, err = MasterDB.Table(userLogin).Where("email=?", email).Update(changeData) + if err != nil { + objLog.Errorf("用户 %s 更新密码错误:%s", email, err) + return "对不起,内部服务错误!", err + } + return "", nil +} + +// Activate 用户激活 +func (self UserLogic) Activate(ctx context.Context, email, uuid string, timestamp int64, sign string) (*model.User, error) { + objLog := GetLogger(ctx) + + realSign := DefaultEmail.genActivateSign(email, uuid, timestamp) + if sign != realSign { + return nil, errors.New("签名非法!") + } + + user := self.FindOne(ctx, "email", email) + if user.Uid == 0 { + return nil, errors.New("邮箱非法") + } + + user.Status = model.UserStatusAudit + + _, err := MasterDB.ID(user.Uid).Update(user) + if err != nil { + objLog.Errorf("activate [%s] failure:%s", email, err) + return nil, err + } + + return user, nil +} + +// 增加或减少用户活跃度 +func (UserLogic) IncrUserWeight(field string, value interface{}, weight int) { + _, err := MasterDB.Where(field+"=?", value).Incr("weight", weight).Update(new(model.UserActive)) + if err != nil { + logger.Errorln("UserActive update Error:", err) + } +} + +func (UserLogic) DecrUserWeight(field string, value interface{}, divide int) { + if divide <= 0 { + return + } + + strSql := fmt.Sprintf("UPDATE user_active SET weight=weight/%d WHERE %s=?", divide, field) + if result, err := MasterDB.Exec(strSql, value); err != nil { + logger.Errorln("UserActive update Error:", err) + } else { + n, _ := result.RowsAffected() + logger.Debugln(strSql, "affected num:", n) + } +} + +// RecordLogin 记录用户最后登录时间和 IP +func (UserLogic) RecordLogin(username string, ipinter interface{}) error { + change := map[string]interface{}{ + "login_time": time.Now(), + } + if ip, ok := ipinter.(string); ok && ip != "" { + change["login_ip"] = ip + } + _, err := MasterDB.Table(new(model.UserLogin)).Where("username=?", username). + Update(change) + if err != nil { + logger.Errorf("记录用户 %q 登录错误:%s", username, err) + } + return err +} + +// FindActiveUsers 获得活跃用户 +func (UserLogic) FindActiveUsers(ctx context.Context, limit int, offset ...int) []*model.UserActive { + objLog := GetLogger(ctx) + + activeUsers := make([]*model.UserActive, 0) + err := MasterDB.OrderBy("weight DESC").Limit(limit, offset...).Find(&activeUsers) + if err != nil { + objLog.Errorln("UserLogic FindActiveUsers error:", err) + return nil + } + return activeUsers +} + +func (UserLogic) FindDAUUsers(ctx context.Context, uids []int) map[int]*model.User { + objLog := GetLogger(ctx) + + users := make(map[int]*model.User) + err := MasterDB.In("uid", uids).Find(&users) + if err != nil { + objLog.Errorln("UserLogic FindDAUUsers error:", err) + return nil + } + return users +} + +// FindNewUsers 最新加入会员 +func (UserLogic) FindNewUsers(ctx context.Context, limit int, offset ...int) []*model.User { + objLog := GetLogger(ctx) + + users := make([]*model.User, 0) + err := MasterDB.OrderBy("ctime DESC").Limit(limit, offset...).Find(&users) + if err != nil { + objLog.Errorln("UserLogic FindNewUsers error:", err) + return nil + } + return users +} + +// 获取用户列表(分页):后台用 +func (UserLogic) FindUserByPage(ctx context.Context, conds map[string]string, curPage, limit int) ([]*model.User, int) { + objLog := GetLogger(ctx) + + session := MasterDB.NewSession() + + for k, v := range conds { + session.And(k+"=?", v) + } + + totalSession := SessionClone(session) + + offset := (curPage - 1) * limit + userList := make([]*model.User, 0) + err := session.OrderBy("uid DESC").Limit(limit, offset).Find(&userList) + if err != nil { + objLog.Errorln("UserLogic find error:", err) + return nil, 0 + } + + total, err := totalSession.Count(new(model.User)) + if err != nil { + objLog.Errorln("UserLogic find count error:", err) + return nil, 0 + } + + return userList, int(total) +} + +func (self UserLogic) AdminUpdateUser(ctx context.Context, uid string, form url.Values) { + user := self.FindOne(ctx, "uid", uid) + user.DauAuth = 0 + + for k := range form { + switch k { + case "topic": + user.DauAuth |= model.DauAuthTopic + case "article": + user.DauAuth |= model.DauAuthArticle + case "resource": + user.DauAuth |= model.DauAuthResource + case "project": + user.DauAuth |= model.DauAuthProject + case "wiki": + user.DauAuth |= model.DauAuthWiki + case "book": + user.DauAuth |= model.DauAuthBook + case "comment": + user.DauAuth |= model.DauAuthComment + case "top": + user.DauAuth |= model.DauAuthTop + } + } + + user.IsVip = goutils.MustBool(form.Get("is_vip"), false) + user.VipExpire = goutils.MustInt(form.Get("vip_expire")) + + MasterDB.ID(user.Uid).UseBool("is_vip").Update(user) +} + +// GetUserMentions 获取 @ 的 suggest 列表 +func (UserLogic) GetUserMentions(term string, limit int, isHttps bool) []map[string]string { + userActives := make([]*model.UserActive, 0) + err := MasterDB.Where("username like ?", "%"+term+"%").Desc("mtime").Limit(limit).Find(&userActives) + if err != nil { + logger.Errorln("UserLogic GetUserMentions Error:", err) + return nil + } + + users := make([]map[string]string, len(userActives)) + for i, userActive := range userActives { + user := make(map[string]string, 2) + user["username"] = userActive.Username + user["avatar"] = util.Gravatar(userActive.Avatar, userActive.Email, 20, isHttps) + users[i] = user + } + + return users +} + +// 获取 loginTime 之前没有登录的用户 +func (UserLogic) FindNotLoginUsers(loginTime time.Time) (userList []*model.UserLogin, err error) { + userList = make([]*model.UserLogin, 0) + err = MasterDB.Where("login_time", loginTime).Find(&userList) + return +} + +// 邮件订阅或取消订阅 +func (UserLogic) EmailSubscribe(ctx context.Context, uid, unsubscribe int) { + _, err := MasterDB.Table(&model.User{}).ID(uid).Update(map[string]interface{}{"unsubscribe": unsubscribe}) + if err != nil { + logger.Errorln("user:", uid, "Email Subscribe Error:", err) + } +} + +func (UserLogic) FindBindUsers(ctx context.Context, uid int) []*model.BindUser { + bindUsers := make([]*model.BindUser, 0) + err := MasterDB.Where("uid=?", uid).Find(&bindUsers) + if err != nil { + logger.Errorln("user:", uid, "FindBindUsers Error:", err) + } + return bindUsers +} + +func (UserLogic) doCreateUser(ctx context.Context, session *xorm.Session, user *model.User, passwd ...string) error { + + if user.Avatar == "" && len(DefaultAvatars) > 0 { + // 随机给一个默认头像 + user.Avatar = DefaultAvatars[rand.Intn(len(DefaultAvatars))] + } + user.Open = 0 + + user.DauAuth = model.DefaultAuth + + _, err := session.Insert(user) + if err != nil { + return err + } + + // 存用户登录信息 + userLogin := &model.UserLogin{ + Email: user.Email, + Username: user.Username, + Uid: user.Uid, + } + if len(passwd) > 0 { + userLogin.Passwd = passwd[0] + err = userLogin.GenMd5Passwd() + if err != nil { + return err + } + } + + if _, err = session.Insert(userLogin); err != nil { + return err + } + + if !user.IsRoot { + // 存用户角色信息 + userRole := &model.UserRole{} + // 默认为初级会员 + userRole.Roleid = Roles[len(Roles)-1].Roleid + userRole.Uid = user.Uid + if _, err = session.Insert(userRole); err != nil { + return err + } + } + + // 存用户活跃信息,初始活跃+2 + userActive := &model.UserActive{ + Uid: user.Uid, + Username: user.Username, + Avatar: user.Avatar, + Email: user.Email, + Weight: 2, + } + if _, err = session.Insert(userActive); err != nil { + return err + } + + return nil +} + +func (UserLogic) DeleteUserContent(ctx context.Context, uid int) error { + user := &model.User{} + _, err := MasterDB.ID(uid).Get(user) + if err != nil || user.Username == "" { + return err + } + + feedResult, feedErr := MasterDB.Exec("DELETE FROM `feed` WHERE uid=?", uid) + topicResult, topicErr := MasterDB.Exec("DELETE t,tex FROM `topics` as t LEFT JOIN `topics_ex` as tex USING(tid) WHERE uid=?", uid) + resourceResult, resourceErr := MasterDB.Exec("DELETE r,rex FROM `resource` as r LEFT JOIN `resource_ex` as rex USING(id) WHERE uid=?", uid) + articleResult, articleErr := MasterDB.Exec("DELETE FROM `articles` WHERE author_txt=?", user.Username) + + if feedErr == nil { + affected, _ := feedResult.RowsAffected() + if affected > 0 { + feed := &model.Feed{} + MasterDB.Desc("id").Get(feed) + if feed.Id > 0 { + MasterDB.Exec(`ALTER TABLE feed auto_increment=` + strconv.Itoa(feed.Id+1)) + } + } + } + + if topicErr == nil { + affected, _ := topicResult.RowsAffected() + if affected > 0 { + topic := &model.Topic{} + MasterDB.Desc("tid").Get(topic) + if topic.Tid > 0 { + MasterDB.Exec(`ALTER TABLE topics auto_increment=` + strconv.Itoa(topic.Tid+1)) + } + } + } + + if resourceErr == nil { + affected, _ := resourceResult.RowsAffected() + if affected > 0 { + resource := &model.Resource{} + MasterDB.Desc("id").Get(resource) + if resource.Id > 0 { + MasterDB.Exec(`ALTER TABLE resource auto_increment=` + strconv.Itoa(resource.Id+1)) + } + } + } + + if articleErr == nil { + affected, _ := articleResult.RowsAffected() + if affected > 0 { + article := &model.Article{} + MasterDB.Desc("id").Get(article) + if article.Id > 0 { + MasterDB.Exec(`ALTER TABLE articles auto_increment=` + strconv.Itoa(article.Id+1)) + } + } + } + + return nil +} diff --git a/internal/logic/user_rich.go b/internal/logic/user_rich.go new file mode 100644 index 00000000..3535ca50 --- /dev/null +++ b/internal/logic/user_rich.go @@ -0,0 +1,258 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "errors" + "fmt" + "net/url" + "time" + + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + . "github.com/studygolang/studygolang/db" + + "github.com/garyburd/redigo/redis" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" + "github.com/polaris1119/times" + "golang.org/x/net/context" + "xorm.io/xorm" +) + +var ( + beginAwardWeight = 50 +) + +type UserRichLogic struct{} + +var DefaultUserRich = UserRichLogic{} + +func (self UserRichLogic) AwardCooper() { + redisClient := nosql.NewRedisClient() + defer redisClient.Close() + ymd := times.Format("ymd", time.Now().Add(-86400*time.Second)) + key := DefaultRank.getDAURankKey(ymd) + + var ( + cursor uint64 + err error + resultSlice []interface{} + count = 20 + ) + + for { + cursor, resultSlice, err = redisClient.ZSCAN(key, cursor, "COUNT", count) + if err != nil { + logger.Errorln("AwardCooper ZSCAN error:", err) + break + } + + for len(resultSlice) > 0 { + var ( + uid, weight int + err error + ) + resultSlice, err = redis.Scan(resultSlice, &uid, &weight) + if err != nil { + logger.Errorln("AwardCooper redis Scan error:", err) + continue + } + + if weight < beginAwardWeight { + continue + } + + award := util.Max((weight-500)*5, 0) + + util.UMin((weight-400), 100)*4 + + util.UMin((weight-300), 100)*3 + + util.UMin((weight-200), 100)*2 + + util.UMin((weight-100), 100) + + int(float64(util.UMin((weight-beginAwardWeight), beginAwardWeight))*0.5) + + userRank := redisClient.ZREVRANK(key, uid) + desc := fmt.Sprintf("%s 的活跃度为 %d,排名第 %d,奖励 %d 铜币", ymd, weight, userRank, award) + + user := DefaultUser.FindOne(nil, "uid", uid) + self.IncrUserRich(user, model.MissionTypeActive, award, desc) + } + + if cursor == 0 { + break + } + } +} + +// IncrUserRich 增加或减少用户财富 +func (self UserRichLogic) IncrUserRich(user *model.User, typ, award int, desc string) { + if award == 0 { + logger.Errorln("IncrUserRich, but award is empty!") + return + } + + var ( + total int64 = -1 + err error + ) + + if award > 0 && (typ == model.MissionTypeReplied || typ == model.MissionTypeActive) { + // 老用户,因为之前的主题被人回复而增加财富,自动帮其领取初始资本 + // 因为活跃奖励铜币,自动帮其领取初始资本 + total, err = MasterDB.Where("uid=?", user.Uid).Count(new(model.UserBalanceDetail)) + if err != nil { + logger.Errorln("IncrUserRich count error:", err) + return + } + } + + session := MasterDB.NewSession() + defer session.Close() + session.Begin() + + var initialAward int + if total == 0 { + initialAward, err = self.autoCompleteInitial(session, user) + if err != nil { + logger.Errorln("IncrUserRich autoCompleteInitial error:", err) + session.Rollback() + return + } + } + + user.Balance += initialAward + award + if user.Balance < 0 { + user.Balance = 0 + } + _, err = session.Where("uid=?", user.Uid).Cols("balance").Update(user) + if err != nil { + logger.Errorln("IncrUserRich update error:", err) + session.Rollback() + return + } + + balanceDetail := &model.UserBalanceDetail{ + Uid: user.Uid, + Type: typ, + Num: award, + Balance: user.Balance, + Desc: desc, + } + _, err = session.Insert(balanceDetail) + if err != nil { + logger.Errorln("IncrUserRich insert error:", err) + session.Rollback() + return + } + + session.Commit() +} + +func (UserRichLogic) FindBalanceDetail(ctx context.Context, me *model.Me, p int, types ...int) []*model.UserBalanceDetail { + objLog := GetLogger(ctx) + + balanceDetails := make([]*model.UserBalanceDetail, 0) + session := MasterDB.Where("uid=?", me.Uid) + if len(types) > 0 { + session.And("type=?", types[0]) + } + + err := session.Desc("id").Limit(CommentPerNum, (p-1)*CommentPerNum).Find(&balanceDetails) + if err != nil { + objLog.Errorln("UserRichLogic FindBalanceDetail error:", err) + return nil + } + + return balanceDetails +} + +func (UserRichLogic) Total(ctx context.Context, uid int) int64 { + total, err := MasterDB.Where("uid=?", uid).Count(new(model.UserBalanceDetail)) + if err != nil { + logger.Errorln("UserRichLogic Total error:", err) + } + return total +} + +func (self UserRichLogic) FindRecharge(ctx context.Context, me *model.Me) int { + objLog := GetLogger(ctx) + + total, err := MasterDB.Where("uid=?", me.Uid).SumInt(new(model.UserRecharge), "amount") + if err != nil { + objLog.Errorln("UserRichLogic FindRecharge error:", err) + return 0 + } + + return int(total) +} + +// Recharge 用户充值 +func (self UserRichLogic) Recharge(ctx context.Context, uid string, form url.Values) { + objLog := GetLogger(ctx) + + createdAt, _ := time.ParseInLocation("2006-01-02 15:04:05", form.Get("time"), time.Local) + userRecharge := &model.UserRecharge{ + Uid: goutils.MustInt(uid), + Amount: goutils.MustInt(form.Get("amount")), + Channel: form.Get("channel"), + CreatedAt: createdAt, + } + + session := MasterDB.NewSession() + session.Begin() + + _, err := session.Insert(userRecharge) + if err != nil { + session.Rollback() + objLog.Errorln("UserRichLogic Recharge error:", err) + return + } + + user := DefaultUser.FindOne(ctx, "uid", uid) + me := &model.Me{ + Uid: user.Uid, + Balance: user.Balance, + } + + award := goutils.MustInt(form.Get("copper")) + desc := fmt.Sprintf("%s 充值 ¥%d,获得 %d 个铜币", times.Format("Ymd"), userRecharge.Amount, award) + err = DefaultMission.changeUserBalance(session, me, model.MissionTypeAdd, award, desc) + if err != nil { + session.Rollback() + objLog.Errorln("UserRichLogic changeUserBalance error:", err) + return + } + session.Commit() +} + +func (UserRichLogic) add(session *xorm.Session, balanceDetail *model.UserBalanceDetail) error { + _, err := session.Insert(balanceDetail) + return err +} + +func (UserRichLogic) autoCompleteInitial(session *xorm.Session, user *model.User) (int, error) { + mission := &model.Mission{} + _, err := session.Where("id=?", model.InitialMissionId).Get(mission) + if err != nil { + return 0, err + } + if mission.Id == 0 { + return 0, errors.New("初始资本任务不存在!") + } + + balanceDetail := &model.UserBalanceDetail{ + Uid: user.Uid, + Type: model.MissionTypeInitial, + Num: mission.Fixed, + Balance: mission.Fixed, + Desc: fmt.Sprintf("获得%s %d 铜币", model.BalanceTypeMap[mission.Type], mission.Fixed), + } + _, err = session.Insert(balanceDetail) + + return mission.Fixed, err +} diff --git a/internal/logic/user_rich_test.go b/internal/logic/user_rich_test.go new file mode 100644 index 00000000..ab94e88a --- /dev/null +++ b/internal/logic/user_rich_test.go @@ -0,0 +1,7 @@ +package logic_test + +import "testing" + +func TestAwardCooper(t *testing.T) { + // logic.DefaultUserRich.AwardCooper() +} diff --git a/internal/logic/user_test.go b/internal/logic/user_test.go new file mode 100644 index 00000000..3556d3a4 --- /dev/null +++ b/internal/logic/user_test.go @@ -0,0 +1,18 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package logic_test + +import ( + "testing" +) + +func TestFindUserInfos(t *testing.T) { + // usersMap := DefaultUser.FindUserInfos(nil, []int{1, 2, 3}) + // if len(usersMap) == 0 { + // t.Fatal(usersMap) + // } +} diff --git a/internal/logic/view.go b/internal/logic/view.go new file mode 100644 index 00000000..a8685663 --- /dev/null +++ b/internal/logic/view.go @@ -0,0 +1,147 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "sync" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" +) + +// 话题/文章/资源/图书等的浏览数 +// 避免每次写库,同时避免刷屏 +type view struct { + objtype int // 对象类型(model/comment 中的 type 常量) + objid int // 对象id(相应的表中的id) + + num int // 当前浏览数 + locker sync.Mutex +} + +func newView(objtype, objid int) *view { + return &view{objtype: objtype, objid: objid} +} + +func (this *view) incr() { + this.locker.Lock() + defer this.locker.Unlock() + + this.num++ +} + +// flush 将浏览数刷入数据库中 +func (this *view) flush() { + this.locker.Lock() + defer this.locker.Unlock() + + session := MasterDB.ID(this.objid) + switch this.objtype { + case model.TypeTopic: + session.Incr("view", this.num).Update(new(model.TopicUpEx)) + case model.TypeArticle: + session.Incr("viewnum", this.num).Update(new(model.Article)) + case model.TypeResource: + session.Incr("viewnum", this.num).Update(new(model.ResourceEx)) + case model.TypeProject: + session.Incr("viewnum", this.num).Update(new(model.OpenProject)) + case model.TypeWiki: + session.Incr("viewnum", this.num).Update(new(model.Wiki)) + case model.TypeBook: + session.Incr("viewnum", this.num).Update(new(model.Book)) + case model.TypeInterview: + session.Incr("viewnum", this.num).Update(new(model.InterviewQuestion)) + } + + DefaultRank.GenDayRank(this.objtype, this.objid, this.num) + + this.num = 0 +} + +// 保存所有对象的浏览数 +type views struct { + data map[string]*view + // 记录用户是否已经看过(防止刷屏) + users map[string]bool + + locker sync.Mutex +} + +func newViews() *views { + return &views{data: make(map[string]*view), users: make(map[string]bool)} +} + +// TODO: 用户登录了,应该用用户标识,而不是IP +func (this *views) Incr(req *http.Request, objtype, objid int, uids ...int) { + ua := req.UserAgent() + spiders := config.ConfigFile.MustValueArray("global", "spider", ",") + for _, spider := range spiders { + if strings.Contains(ua, spider) { + return + } + } + + // 记录浏览来源 + go DefaultViewSource.Record(req, objtype, objid) + + key := strconv.Itoa(objtype) + strconv.Itoa(objid) + + var userKey string + + if len(uids) > 0 { + userKey = fmt.Sprintf("%s_uid_%d", key, uids[0]) + } else { + userKey = fmt.Sprintf("%s_ip_%d", key, goutils.Ip2long(goutils.RemoteIp(req))) + } + + this.locker.Lock() + defer this.locker.Unlock() + + if _, ok := this.users[userKey]; ok { + return + } else { + this.users[userKey] = true + } + + if _, ok := this.data[key]; !ok { + this.data[key] = newView(objtype, objid) + } + + this.data[key].incr() + + if len(uids) > 0 { + ViewObservable.NotifyObservers(uids[0], objtype, objid) + } else { + ViewObservable.NotifyObservers(0, objtype, objid) + } +} + +func (this *views) Flush() { + logger.Debugln("start views flush") + this.locker.Lock() + defer this.locker.Unlock() + + // TODO:量大时,考虑copy一份,然后异步 入库,以免堵塞 锁 太久 + for _, view := range this.data { + view.flush() + } + + this.data = make(map[string]*view) + this.users = make(map[string]bool) + + logger.Debugln("end views flush") +} + +var Views = newViews() diff --git a/internal/logic/view_record.go b/internal/logic/view_record.go new file mode 100644 index 00000000..26b3d9a5 --- /dev/null +++ b/internal/logic/view_record.go @@ -0,0 +1,57 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "github.com/studygolang/studygolang/internal/model" + + . "github.com/studygolang/studygolang/db" + + "github.com/polaris1119/logger" + "golang.org/x/net/context" +) + +type ViewRecordLogic struct{} + +var DefaultViewRecord = ViewRecordLogic{} + +func (ViewRecordLogic) Record(objid, objtype, uid int) { + + total, err := MasterDB.Where("objid=? AND objtype=? AND uid=?", objid, objtype, uid).Count(new(model.ViewRecord)) + if err != nil { + logger.Errorln("ViewRecord logic Record count error:", err) + return + } + + if total > 0 { + return + } + + viewRecord := &model.ViewRecord{ + Objid: objid, + Objtype: objtype, + Uid: uid, + } + + if _, err = MasterDB.Insert(viewRecord); err != nil { + logger.Errorln("ViewRecord logic Record insert Error:", err) + return + } + + return +} + +func (ViewRecordLogic) FindUserNum(ctx context.Context, objid, objtype int) int64 { + objLog := GetLogger(ctx) + + total, err := MasterDB.Where("objid=? AND objtype=?", objid, objtype).Count(new(model.ViewRecord)) + if err != nil { + objLog.Errorln("ViewRecordLogic FindUserNum error:", err) + } + + return total +} diff --git a/internal/logic/view_source.go b/internal/logic/view_source.go new file mode 100644 index 00000000..adad94e9 --- /dev/null +++ b/internal/logic/view_source.go @@ -0,0 +1,77 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/internal/model" + + . "github.com/studygolang/studygolang/db" + + "github.com/polaris1119/logger" + "golang.org/x/net/context" +) + +type ViewSourceLogic struct{} + +var DefaultViewSource = ViewSourceLogic{} + +// Record 记录浏览来源 +func (ViewSourceLogic) Record(req *http.Request, objtype, objid int) { + referer := req.Referer() + if referer == "" || strings.Contains(referer, WebsiteSetting.Domain) { + return + } + + viewSource := &model.ViewSource{} + _, err := MasterDB.Where("objid=? AND objtype=?", objid, objtype).Get(viewSource) + if err != nil { + logger.Errorln("ViewSourceLogic Record find error:", err) + return + } + + if viewSource.Id == 0 { + viewSource.Objid = objid + viewSource.Objtype = objtype + _, err = MasterDB.Insert(viewSource) + if err != nil { + logger.Errorln("ViewSourceLogic Record insert error:", err) + return + } + } + + field := "other" + referer = strings.ToLower(referer) + ses := []string{"google", "baidu", "bing", "sogou", "so"} + for _, se := range ses { + if strings.Contains(referer, se+".") { + field = se + break + } + } + + _, err = MasterDB.ID(viewSource.Id).Incr(field, 1).Update(new(model.ViewSource)) + if err != nil { + logger.Errorln("ViewSourceLogic Record update error:", err) + return + } +} + +// FindOne 获得浏览来源 +func (ViewSourceLogic) FindOne(ctx context.Context, objid, objtype int) *model.ViewSource { + objLog := GetLogger(ctx) + + viewSource := &model.ViewSource{} + _, err := MasterDB.Where("objid=? AND objtype=?", objid, objtype).Get(viewSource) + if err != nil { + objLog.Errorln("ViewSourceLogic FindOne error:", err) + } + + return viewSource +} diff --git a/internal/logic/wechat.go b/internal/logic/wechat.go new file mode 100644 index 00000000..a49816d7 --- /dev/null +++ b/internal/logic/wechat.go @@ -0,0 +1,508 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + . "github.com/studygolang/studygolang/db" + + "github.com/tidwall/gjson" + + "golang.org/x/net/context" + + "github.com/polaris1119/config" + "github.com/polaris1119/nosql" +) + +type WechatLogic struct{} + +var DefaultWechat = WechatLogic{} + +var jscodeRUL = "https://api.weixin.qq.com/sns/jscode2session" + +// CheckSession 微信小程序登录凭证校验 +func (self WechatLogic) CheckSession(ctx context.Context, code string) (*model.WechatUser, error) { + objLog := GetLogger(ctx) + + appid := config.ConfigFile.MustValue("wechat.xcx", "appid") + appsecret := config.ConfigFile.MustValue("wechat.xcx", "appsecret") + + checkLoginURL := fmt.Sprintf("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", + jscodeRUL, appid, appsecret, code) + + body, err := util.DoGet(checkLoginURL) + if err != nil { + return nil, err + } + + result := gjson.ParseBytes(body) + + openidResult := result.Get("openid") + if !openidResult.Exists() { + objLog.Errorln("WechatLogic WxLogin error:", result.Raw) + return nil, errors.New(result.Get("errmsg").String()) + } + + openid := openidResult.String() + wechatUser := &model.WechatUser{} + _, err = MasterDB.Where("openid=?", openid).Get(wechatUser) + if err != nil { + objLog.Errorln("WechatLogic WxLogin find wechat user error:", err) + return nil, err + } + + if wechatUser.Id == 0 { + wechatUser.Openid = openid + wechatUser.SessionKey = result.Get("session_key").String() + _, err = MasterDB.Insert(wechatUser) + if err != nil { + objLog.Errorln("WechatLogic WxLogin insert wechat user error:", err) + return nil, err + } + } + + return wechatUser, nil +} + +func (self WechatLogic) Bind(ctx context.Context, id, uid int, userInfo string) (*model.WechatUser, error) { + objLog := GetLogger(ctx) + + result := gjson.Parse(userInfo) + + wechatUser := &model.WechatUser{ + Uid: uid, + Nickname: result.Get("nickName").String(), + Avatar: result.Get("avatarUrl").String(), + OpenInfo: userInfo, + } + _, err := MasterDB.ID(id).Update(wechatUser) + if err != nil { + objLog.Errorln("WechatLogic Bind update error:", err) + return nil, err + } + + return wechatUser, nil +} + +func (self WechatLogic) FetchOrUpdateToken() (string, error) { + var result = struct { + AccessToken string + ExpiresTime time.Time + }{} + + filename := config.ROOT + "/data/wechat-token.json" + if util.Exist(filename) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return "", err + } + + err = json.Unmarshal(b, &result) + if err != nil { + return "", err + } + + if result.ExpiresTime.After(time.Now()) { + return result.AccessToken, nil + } + } + + appid := config.ConfigFile.MustValue("wechat", "appid") + appsecret := config.ConfigFile.MustValue("wechat", "appsecret") + strURL := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appid, appsecret) + + b, err := util.DoGet(strURL) + if err != nil { + return "", err + } + gresult := gjson.ParseBytes(b) + if gresult.Get("errmsg").Exists() { + return "", errors.New(gresult.Get("errmsg").String()) + } + + result.AccessToken = gresult.Get("access_token").String() + result.ExpiresTime = time.Now().Add(time.Duration(gresult.Get("expires_in").Int()-5) * time.Second) + + b, err = json.Marshal(result) + if err != nil { + return "", err + } + err = ioutil.WriteFile(filename, b, 0755) + if err != nil { + return "", err + } + + return result.AccessToken, nil +} + +func (self WechatLogic) AutoReply(ctx context.Context, reqData []byte) (*model.WechatReply, error) { + objLog := GetLogger(ctx) + + wechatMsg := &model.WechatMsg{} + err := xml.Unmarshal(reqData, wechatMsg) + if err != nil { + objLog.Errorln("wechat autoreply xml unmarshal error:", err) + return nil, err + } + + switch wechatMsg.MsgType { + case model.WeMsgTypeText: + if strings.Contains(wechatMsg.Content, "晨读") { + return self.readingContent(ctx, wechatMsg) + } else if strings.Contains(wechatMsg.Content, "主题") || strings.Contains(wechatMsg.Content, "帖子") { + return self.topicContent(ctx, wechatMsg) + } else if strings.Contains(wechatMsg.Content, "文章") { + return self.articleContent(ctx, wechatMsg) + } else if strings.Contains(wechatMsg.Content, "资源") { + return self.resourceContent(ctx, wechatMsg) + } else if strings.Contains(wechatMsg.Content, "项目") { + return self.projectContent(ctx, wechatMsg) + } else if strings.Contains(wechatMsg.Content, "图书") { + return self.bookContent(ctx, wechatMsg) + } else { + // 用户获取验证码用 + user := DefaultUser.FindOne(ctx, "username", wechatMsg.Content) + if user.Uid > 0 { + var content string + // 获取微信用户信息 + if err = self.checkAndSave(ctx, wechatMsg); err != nil { + content = err.Error() + } else { + content = self.genCaptcha(user.Username, wechatMsg.FromUserName) + } + return self.wechatResponse(ctx, content, wechatMsg) + } + + // 关键词回复 + autoReply := &model.WechatAutoReply{} + MasterDB.Where("word LIKE ?", "%"+wechatMsg.Content+"%").Get(autoReply) + if autoReply.Id != 0 { + wechatMsg.MsgType = autoReply.MsgType + return self.wechatResponse(ctx, autoReply.Content, wechatMsg) + } + + return self.searchContent(ctx, wechatMsg) + } + case model.WeMsgTypeEvent: + switch wechatMsg.Event { + case model.WeEventSubscribe: + wechatMsg.MsgType = model.WeMsgTypeText + welcomeText := strings.ReplaceAll(config.ConfigFile.MustValue("wechat", "subscribe"), "\\n", "\n") + + autoReply := &model.WechatAutoReply{} + _, err = MasterDB.Where("typ=?", model.AutoReplyTypSubscribe).Get(autoReply) + if err == nil { + welcomeText = autoReply.Content + } + + return self.wechatResponse(ctx, welcomeText, wechatMsg) + } + } + + return self.wechatResponse(ctx, "success", wechatMsg) +} + +func (self WechatLogic) genCaptcha(username, openid string) string { + num := rand.Intn(9000) + 1000 + redisClient := nosql.NewRedisClient() + defer redisClient.Close() + + captcha := strconv.Itoa(num) + redisClient.SET("wechat:captcha:$username:"+username, captcha+openid, 600) + + return captcha +} + +func (self WechatLogic) CheckCaptchaAndActivate(ctx context.Context, me *model.Me, captcha string) error { + openid, err := self.checkCaptchaAndFetch(ctx, me, captcha) + if err != nil { + return err + } + + session := MasterDB.NewSession() + defer session.Close() + + session.Begin() + _, err = session.Table(new(model.WechatUser)).Where("openid=?", openid).Update(map[string]interface{}{ + "uid": me.Uid, + }) + if err != nil { + session.Rollback() + return err + } + + _, err = session.Table(new(model.User)).ID(me.Uid).Update(map[string]interface{}{ + "status": model.UserStatusAudit, + "ctime": time.Now().Add(-5 * time.Hour), + }) + if err != nil { + session.Rollback() + return err + } + + session.Commit() + return nil +} + +func (self WechatLogic) CheckCaptchaAndBind(ctx context.Context, me *model.Me, captcha string) error { + openid, err := self.checkCaptchaAndFetch(ctx, me, captcha) + if err != nil { + return err + } + + session := MasterDB.NewSession() + defer session.Close() + + session.Begin() + _, err = session.Table(new(model.WechatUser)).Where("openid=?", openid).Update(map[string]interface{}{ + "uid": me.Uid, + }) + if err != nil { + session.Rollback() + return err + } + + _, err = session.Table(new(model.User)).ID(me.Uid).Update(map[string]interface{}{ + "ctime": time.Now().Add(-5 * time.Hour), + }) + if err != nil { + session.Rollback() + return err + } + + session.Commit() + return nil +} + +func (self WechatLogic) checkCaptchaAndFetch(ctx context.Context, me *model.Me, captcha string) (string, error) { + redisClient := nosql.NewRedisClient() + defer redisClient.Close() + + key := "wechat:captcha:$username:" + me.Username + store := redisClient.GET(key) + if store[:4] != captcha { + return "", errors.New("验证码错误") + } + + redisClient.DEL(key) + + return store[4:], nil +} + +func (self WechatLogic) checkAndSave(ctx context.Context, wechatMsg *model.WechatMsg) error { + accessToken, err := self.FetchOrUpdateToken() + if err != nil { + return err + } + + wechatUser := &model.WechatUser{} + _, err = MasterDB.Where("openid=?", wechatMsg.FromUserName).Get(wechatUser) + if err != nil { + return err + } + + strURL := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN", accessToken, wechatMsg.FromUserName) + b, err := util.DoGet(strURL) + if err != nil { + return err + } + + result := gjson.ParseBytes(b) + if result.Get("errmsg").Exists() { + return errors.New(result.Get("errmsg").String()) + } + + // 已经存在 + if wechatUser.Openid != "" { + wechatUser.Nickname = result.Get("nickname").String() + wechatUser.Avatar = result.Get("headimgurl").String() + wechatUser.OpenInfo = result.Raw + + _, err = MasterDB.ID(wechatUser.Id).Update(wechatUser) + } else { + wechatUser = &model.WechatUser{ + Openid: result.Get("openid").String(), + Nickname: result.Get("nickname").String(), + Avatar: result.Get("headimgurl").String(), + OpenInfo: result.Raw, + } + _, err = MasterDB.InsertOne(wechatUser) + } + + if wechatUser.Uid > 0 { + return errors.New("该微信绑定过其他账号") + } + + return err +} + +func (self WechatLogic) topicContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + + topics := DefaultTopic.FindRecent(5) + + respContentSlice := make([]string, len(topics)) + for i, topic := range topics { + respContentSlice[i] = fmt.Sprintf("%d.《%s》 %s/topics/%d", i+1, topic.Title, website(), topic.Tid) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n"), wechatMsg) +} + +func (self WechatLogic) articleContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + + articles := DefaultArticle.FindBy(ctx, 5) + + respContentSlice := make([]string, len(articles)) + for i, article := range articles { + respContentSlice[i] = fmt.Sprintf("%d.《%s》 %s/articles/%d", i+1, article.Title, website(), article.Id) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n"), wechatMsg) +} + +func (self WechatLogic) resourceContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + + resources := DefaultResource.FindBy(ctx, 5) + + respContentSlice := make([]string, len(resources)) + for i, resource := range resources { + respContentSlice[i] = fmt.Sprintf("%d.《%s》 %s/resources/%d", i+1, resource.Title, website(), resource.Id) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n"), wechatMsg) +} + +func (self WechatLogic) projectContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + + projects := DefaultProject.FindBy(ctx, 5) + + respContentSlice := make([]string, len(projects)) + for i, project := range projects { + respContentSlice[i] = fmt.Sprintf("%d.《%s%s》 %s/p/%d", i+1, project.Category, project.Name, website(), project.Id) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n"), wechatMsg) +} + +func (self WechatLogic) bookContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + + books := DefaultGoBook.FindBy(ctx, 5) + + respContentSlice := make([]string, len(books)) + for i, book := range books { + respContentSlice[i] = fmt.Sprintf("%d.《%s》 %s/book/%d", i+1, book.Name, website(), book.Id) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n"), wechatMsg) +} + +func (self WechatLogic) readingContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + + var formatContent = func(reading *model.MorningReading) string { + if reading.Inner == 0 { + return fmt.Sprintf("%s\n%s", reading.Content, reading.Url) + } + + return fmt.Sprintf("%s\n%s/articles/%d", reading.Content, website(), reading.Inner) + } + + var readings []*model.MorningReading + if wechatMsg.Content == "最新晨读" { + readings = DefaultReading.FindBy(ctx, 1, model.RtypeGo) + if len(readings) == 0 { + return self.wechatResponse(ctx, config.ConfigFile.MustValue("wechat", "not_found"), wechatMsg) + } + + return self.wechatResponse(ctx, formatContent(readings[0]), wechatMsg) + } + + readings = DefaultReading.FindBy(ctx, 3, model.RtypeGo) + + respContentSlice := make([]string, len(readings)) + for i, reading := range readings { + respContentSlice[i] = fmt.Sprintf("%d. %s", i+1, formatContent(reading)) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n\n"), wechatMsg) +} + +func (self WechatLogic) searchContent(ctx context.Context, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + objLog := GetLogger(ctx) + + respBody, err := DefaultSearcher.SearchByField("title", wechatMsg.Content, 0, 5) + if err != nil { + objLog.Errorln("wechat search by field error:", err) + return nil, err + } + + if respBody.NumFound == 0 { + return self.wechatResponse(ctx, config.ConfigFile.MustValue("wechat", "not_found"), wechatMsg) + } + + host := WebsiteSetting.Domain + if WebsiteSetting.OnlyHttps { + host = "https://" + host + } else { + host = "http://" + host + } + + respContentSlice := make([]string, len(respBody.Docs)) + for i, doc := range respBody.Docs { + url := "" + + switch doc.Objtype { + case model.TypeTopic: + url = fmt.Sprintf("%s/topics/%d", host, doc.Objid) + case model.TypeArticle: + url = fmt.Sprintf("%s/articles/%d", host, doc.Objid) + case model.TypeResource: + url = fmt.Sprintf("%s/resources/%d", host, doc.Objid) + case model.TypeProject: + url = fmt.Sprintf("%s/p/%d", host, doc.Objid) + case model.TypeWiki: + url = fmt.Sprintf("%s/wiki/%d", host, doc.Objid) + case model.TypeBook: + url = fmt.Sprintf("%s/book/%d", host, doc.Objid) + } + respContentSlice[i] = fmt.Sprintf("%d.《%s》 %s", i+1, doc.Title, url) + } + + return self.wechatResponse(ctx, strings.Join(respContentSlice, "\n"), wechatMsg) +} + +func (self WechatLogic) wechatResponse(ctx context.Context, respContent string, wechatMsg *model.WechatMsg) (*model.WechatReply, error) { + wechatReply := &model.WechatReply{ + ToUserName: &model.CData{Val: wechatMsg.FromUserName}, + FromUserName: &model.CData{Val: wechatMsg.ToUserName}, + MsgType: &model.CData{Val: wechatMsg.MsgType}, + CreateTime: time.Now().Unix(), + } + switch wechatMsg.MsgType { + case model.WeMsgTypeText: + wechatReply.Content = &model.CData{Val: respContent} + case model.WeMsgTypeImage: + wechatReply.Image = &model.WechatImage{ + MediaId: &model.CData{Val: respContent}, + } + default: + wechatReply.Content = &model.CData{Val: config.ConfigFile.MustValue("wechat", "not_found")} + } + + return wechatReply, nil +} diff --git a/internal/logic/wiki.go b/internal/logic/wiki.go new file mode 100644 index 00000000..d419fa2a --- /dev/null +++ b/internal/logic/wiki.go @@ -0,0 +1,195 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package logic + +import ( + "errors" + "net/url" + "strconv" + "strings" + + . "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/internal/model" + + "golang.org/x/net/context" + + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/set" +) + +type WikiLogic struct{} + +var DefaultWiki = WikiLogic{} + +// Create 创建一个wiki页面 +func (WikiLogic) Create(ctx context.Context, me *model.Me, form url.Values) error { + objLog := GetLogger(ctx) + + wiki := &model.Wiki{} + err := schemaDecoder.Decode(wiki, form) + if err != nil { + objLog.Errorln("Create Wiki schema decode error:", err) + return err + } + + wiki.Uid = me.Uid + if _, err = MasterDB.Insert(wiki); err != nil { + objLog.Errorln("Create Wiki error:", err) + return err + } + + go publishObservable.NotifyObservers(me.Uid, model.TypeWiki, wiki.Id) + + return nil +} + +func (self WikiLogic) Modify(ctx context.Context, me *model.Me, form url.Values) error { + objLog := GetLogger(ctx) + + id := goutils.MustInt(form.Get("id")) + wiki := self.FindById(ctx, id) + if !CanEdit(me, wiki) { + return errors.New("没有权限") + } + + if wiki.Uid != me.Uid { + hasExists := false + cuids := strings.Split(wiki.Cuid, ",") + for _, cuid := range cuids { + if me.Uid == goutils.MustInt(cuid) { + hasExists = true + break + } + } + + if !hasExists { + cuids = append(cuids, strconv.Itoa(me.Uid)) + wiki.Cuid = strings.Join(cuids, ",") + } + } + + wiki.Title = form.Get("title") + wiki.Content = form.Get("content") + + _, err := MasterDB.ID(id).Update(wiki) + if err != nil { + objLog.Errorf("更新wiki 【%d】 信息失败:%s\n", id, err) + return err + } + + go modifyObservable.NotifyObservers(me.Uid, model.TypeWiki, wiki.Id) + + return nil +} + +// FindBy 获取 wiki 列表(分页) +func (WikiLogic) FindBy(ctx context.Context, limit int, lastIds ...int) []*model.Wiki { + objLog := GetLogger(ctx) + + dbSession := MasterDB.OrderBy("id DESC") + + if len(lastIds) > 0 && lastIds[0] > 0 { + dbSession.Where("id", lastIds[0]) + } + + wikis := make([]*model.Wiki, 0) + err := dbSession.Limit(limit).Find(&wikis) + if err != nil { + objLog.Errorln("WikiLogic FindBy Error:", err) + return nil + } + + uidSet := set.New(set.NonThreadSafe) + for _, wiki := range wikis { + uidSet.Add(wiki.Uid) + } + usersMap := DefaultUser.FindUserInfos(ctx, set.IntSlice(uidSet)) + for _, wiki := range wikis { + wiki.Users = map[int]*model.User{wiki.Uid: usersMap[wiki.Uid]} + } + + return wikis +} + +// FindById 通过ID获取Wiki +func (WikiLogic) FindById(ctx context.Context, id int) *model.Wiki { + objLog := GetLogger(ctx) + + wiki := &model.Wiki{} + if _, err := MasterDB.Where("id=?", id).Get(wiki); err != nil { + objLog.Errorln("wiki logic FindById error:", err) + return nil + } + return wiki +} + +// FindOne 某个wiki页面详细信息 +func (WikiLogic) FindOne(ctx context.Context, uri string) *model.Wiki { + objLog := GetLogger(ctx) + + wiki := &model.Wiki{} + if _, err := MasterDB.Where("uri=?", uri).Get(wiki); err != nil { + objLog.Errorln("wiki logic FindOne error:", err) + return nil + } + + if wiki.Id == 0 { + return nil + } + + uidSet := set.New(set.NonThreadSafe) + uidSet.Add(wiki.Uid) + if wiki.Cuid != "" { + cuids := strings.Split(wiki.Cuid, ",") + for _, cuid := range cuids { + uidSet.Add(goutils.MustInt(cuid)) + } + } + wiki.Users = DefaultUser.FindUserInfos(ctx, set.IntSlice(uidSet)) + + return wiki +} + +// getOwner 通过id获得wiki的所有者 +func (WikiLogic) getOwner(id int) int { + wiki := &model.Wiki{} + _, err := MasterDB.ID(id).Get(wiki) + if err != nil { + logger.Errorln("wiki logic getOwner Error:", err) + return 0 + } + return wiki.Uid +} + +// FindByIds 获取多个wiki页面详细信息 +func (WikiLogic) FindByIds(ids []int) []*model.Wiki { + if len(ids) == 0 { + return nil + } + wikis := make([]*model.Wiki, 0) + err := MasterDB.In("id", ids).Find(&wikis) + if err != nil { + logger.Errorln("wiki logic FindByIds error:", err) + return nil + } + return wikis +} + +// findByIds 获取多个wiki页面详细信息 包内使用 +func (WikiLogic) findByIds(ids []int) map[int]*model.Wiki { + if len(ids) == 0 { + return nil + } + wikis := make(map[int]*model.Wiki) + err := MasterDB.In("id", ids).Find(&wikis) + if err != nil { + logger.Errorln("wiki logic FindByIds error:", err) + return nil + } + return wikis +} diff --git a/internal/model/ad.go b/internal/model/ad.go new file mode 100644 index 00000000..93257242 --- /dev/null +++ b/internal/model/ad.go @@ -0,0 +1,28 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +type Advertisement struct { + Id int `json:"id" xorm:"pk autoincr"` + Name string `json:"name"` + AdType int `json:"ad_type"` + Code string `json:"code"` + Source string `json:"source"` + IsOnline bool `json:"is_online"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} + +type PageAd struct { + Id int `json:"id" xorm:"pk autoincr"` + Path string `json:"path"` + AdId int `json:"ad_id"` + Position string `json:"position"` + IsOnline bool `json:"is_online"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} diff --git a/internal/model/article.go b/internal/model/article.go new file mode 100644 index 00000000..d1da24e7 --- /dev/null +++ b/internal/model/article.go @@ -0,0 +1,185 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "encoding/json" + "strconv" + "strings" + "time" + + "github.com/polaris1119/logger" + "xorm.io/xorm" +) + +const ( + ArticleStatusNew = iota + ArticleStatusOnline + ArticleStatusOffline +) + +var LangSlice = []string{"中文", "英文"} +var ArticleStatusSlice = []string{"未上线", "已上线", "已下线"} + +// 抓取的文章信息 +type Article struct { + Id int `json:"id" xorm:"pk autoincr"` + Domain string `json:"domain"` + Name string `json:"name"` + Title string `json:"title"` + Cover string `json:"cover"` + Author string `json:"author"` + AuthorTxt string `json:"author_txt"` + Lang int `json:"lang"` + PubDate string `json:"pub_date"` + Url string `json:"url"` + Content string `json:"content"` + Txt string `json:"txt"` + Tags string `json:"tags"` + Css string `json:"css"` + Viewnum int `json:"viewnum"` + Cmtnum int `json:"cmtnum"` + Likenum int `json:"likenum"` + Lastreplyuid int `json:"lastreplyuid"` + Lastreplytime OftenTime `json:"lastreplytime"` + Top uint8 `json:"top"` + Markdown bool `json:"markdown"` + GCTT bool `json:"gctt" xorm:"gctt"` + CloseReply bool `json:"close_reply"` + Status int `json:"status"` + OpUser string `json:"op_user"` + Ctime OftenTime `json:"ctime" xorm:"created"` + Mtime OftenTime `json:"mtime" xorm:"<-"` + + IsSelf bool `json:"is_self" xorm:"-"` + User *User `json:"-" xorm:"-"` + // 排行榜阅读量 + RankView int `json:"rank_view" xorm:"-"` + LastReplyUser *User `json:"last_reply_user" xorm:"-"` +} + +func (this *Article) AfterSet(name string, cell xorm.Cell) { + if name == "id" { + this.IsSelf = strconv.Itoa(this.Id) == this.Url + } +} + +func (this *Article) BeforeInsert() { + if this.Tags == "" { + this.Tags = AutoTag(this.Title, this.Txt, 4) + } + this.Lastreplytime = NewOftenTime() +} + +func (this *Article) AfterInsert() { + go func() { + // AfterInsert 时,自增 ID 还未赋值,这里 sleep 一会,确保自增 ID 有值 + for { + if this.Id > 0 { + PublishFeed(this, nil, nil) + return + } + time.Sleep(100 * time.Millisecond) + } + }() +} + +func (*Article) TableName() string { + return "articles" +} + +type ArticleGCTT struct { + ArticleID int `xorm:"article_id pk"` + Author string + AuthorURL string `xorm:"author_url"` + Translator string + Checker string + URL string `xorm:"url"` + + Avatar string `xorm:"-"` + Checkers []string `xorm:"-"` +} + +func (*ArticleGCTT) TableName() string { + return "article_gctt" +} + +func (this *ArticleGCTT) AfterSet(name string, cell xorm.Cell) { + if name == "checker" { + this.Checkers = strings.Split(this.Checker, ",") + } +} + +// 抓取网站文章的规则 +type CrawlRule struct { + Id int `json:"id" xorm:"pk autoincr"` + Domain string `json:"domain"` + Subpath string `json:"subpath"` + Lang int `json:"lang"` + Name string `json:"name"` + Title string `json:"title"` + Author string `json:"author"` + InUrl bool `json:"in_url"` + PubDate string `json:"pub_date"` + Content string `json:"content"` + Ext string `json:"ext"` + OpUser string `json:"op_user"` + Ctime string `json:"ctime" xorm:"<-"` +} + +func (this *CrawlRule) ParseExt() map[string]string { + if this.Ext == "" { + return nil + } + + extMap := make(map[string]string) + err := json.Unmarshal([]byte(this.Ext), &extMap) + if err != nil { + logger.Errorln("parse crawl rule ext error:", err) + return nil + } + + return extMap +} + +const ( + AutoCrawlOn = 0 + AutoCrawOff = 1 +) + +// 网站自动抓取规则 +type AutoCrawlRule struct { + Id int `json:"id" xorm:"pk autoincr"` + Website string `json:"website"` + AllUrl string `json:"all_url"` + IncrUrl string `json:"incr_url"` + Keywords string `json:"keywords"` + ListSelector string `json:"list_selector"` + ResultSelector string `json:"result_selector"` + PageField string `json:"page_field"` + MaxPage int `json:"max_page"` + Ext string `json:"ext"` + OpUser string `json:"op_user"` + Mtime string `json:"mtime" xorm:"<-"` + + ExtMap map[string]string `json:"-" xorm:"-"` +} + +func (this *AutoCrawlRule) AfterSet(name string, cell xorm.Cell) { + if name == "ext" { + if this.Ext == "" { + return + } + + this.ExtMap = make(map[string]string) + err := json.Unmarshal([]byte(this.Ext), &this.ExtMap) + if err != nil { + logger.Errorln("parse auto crawl rule ext error:", err) + return + } + } +} diff --git a/internal/model/authority.go b/internal/model/authority.go new file mode 100644 index 00000000..e4be9eaa --- /dev/null +++ b/internal/model/authority.go @@ -0,0 +1,19 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +// 权限信息 +type Authority struct { + Aid int `json:"aid" xorm:"pk autoincr"` + Name string `json:"name"` + Menu1 int `json:"menu1"` + Menu2 int `json:"menu2"` + Route string `json:"route"` + OpUser string `json:"op_user"` + Ctime OftenTime `json:"ctime" xorm:"created"` + Mtime OftenTime `json:"mtime" xorm:"<-"` +} diff --git a/internal/model/auto_tag.go b/internal/model/auto_tag.go new file mode 100644 index 00000000..ec353981 --- /dev/null +++ b/internal/model/auto_tag.go @@ -0,0 +1,21 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author:polaris polaris@studygolang.com + +package model + +import ( + "strings" + + "github.com/polaris1119/keyword" +) + +// AutoTag 自动生成 tag +func AutoTag(title, content string, num int) string { + defer func() { + recover() + }() + return strings.Join(keyword.ExtractWithTitle(title, content, num), ",") +} diff --git a/internal/model/book.go b/internal/model/book.go new file mode 100644 index 00000000..b0c5e7b3 --- /dev/null +++ b/internal/model/book.go @@ -0,0 +1,57 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + IsFreeFalse = iota + IsFreeTrue +) + +type Book struct { + Id int `json:"id" xorm:"pk autoincr"` + Name string `json:"name"` + Ename string `json:"ename"` + Cover string `json:"cover"` + Author string `json:"author"` + Translator string `json:"translator"` + Lang int `json:"lang"` + PubDate string `json:"pub_date"` + Desc string `json:"desc"` + Tags string `json:"tags"` + Catalogue string `json:"catalogue"` + IsFree bool `json:"is_free"` + OnlineUrl string `json:"online_url"` + DownloadUrl string `json:"download_url"` + BuyUrl string `json:"buy_url"` + Price float32 `json:"price"` + Lastreplyuid int `json:"lastreplyuid"` + Lastreplytime OftenTime `json:"lastreplytime"` + Viewnum int `json:"viewnum"` + Cmtnum int `json:"cmtnum"` + Likenum int `json:"likenum"` + Uid int `json:"uid"` + CreatedAt OftenTime `json:"created_at" xorm:"created"` + UpdatedAt OftenTime `json:"updated_at" xorm:"<-"` + + // 排行榜阅读量 + RankView int `json:"rank_view" xorm:"-"` +} + +func (this *Book) AfterInsert() { + go func() { + // AfterInsert 时,自增 ID 还未赋值,这里 sleep 一会,确保自增 ID 有值 + for { + if this.Id > 0 { + PublishFeed(this, nil, nil) + return + } + time.Sleep(100 * time.Millisecond) + } + }() +} diff --git a/internal/model/comment.go b/internal/model/comment.go new file mode 100644 index 00000000..016b0040 --- /dev/null +++ b/internal/model/comment.go @@ -0,0 +1,72 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +// 不要修改常量的顺序 +const ( + TypeTopic = iota // 主题 + TypeArticle // 博文 + TypeResource // 资源 + TypeWiki // WIKI + TypeProject // 开源项目 + TypeBook // 图书 + TypeInterview // 面试题 +) + +const ( + TypeComment = 100 + // 置顶 + TypeTop = 101 +) + +const ( + TopicURI = "topics" + ArticleURI = "articles" + ResourceURI = "resources" + WikiURI = "wiki" + ProjectURI = "p" + BookURI = "book" +) + +var PathUrlMap = map[int]string{ + TypeTopic: "/topics/", + TypeArticle: "/articles/", + TypeResource: "/resources/", + TypeWiki: "/wiki/", + TypeProject: "/p/", + TypeBook: "/book/", + TypeInterview: "/interview/", +} + +var TypeNameMap = map[int]string{ + TypeTopic: "主题", + TypeArticle: "博文", + TypeResource: "资源", + TypeWiki: "Wiki", + TypeProject: "项目", + TypeBook: "图书", + TypeInterview: "面试题", +} + +// 评论信息(通用) +type Comment struct { + Cid int `json:"cid" xorm:"pk autoincr"` + Objid int `json:"objid"` + Objtype int `json:"objtype"` + Content string `json:"content"` + Uid int `json:"uid"` + Floor int `json:"floor"` + Flag int `json:"flag"` + Ctime OftenTime `json:"ctime" xorm:"created"` + + Objinfo map[string]interface{} `json:"objinfo" xorm:"-"` + ReplyFloor int `json:"reply_floor" xorm:"-"` // 回复某一楼层 +} + +func (*Comment) TableName() string { + return "comments" +} diff --git a/internal/model/default_avatar.go b/internal/model/default_avatar.go new file mode 100644 index 00000000..466cd5bc --- /dev/null +++ b/internal/model/default_avatar.go @@ -0,0 +1,15 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +type DefaultAvatar struct { + Id int `json:"-" xorm:"pk autoincr"` + Filename string + CreatedAt time.Time `json:"-" xorm:"<-"` +} diff --git a/internal/model/document.go b/internal/model/document.go new file mode 100644 index 00000000..0f9cd19c --- /dev/null +++ b/internal/model/document.go @@ -0,0 +1,260 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "fmt" + "html/template" + "regexp" + "strings" + "time" + + "github.com/studygolang/studygolang/db" +) + +// 文档对象(供solr使用) +type Document struct { + Id string `json:"id"` + Objid int `json:"objid"` + Objtype int `json:"objtype"` + Title string `json:"title"` + Author string `json:"author"` + Uid int `json:"uid"` + PubTime string `json:"pub_time"` + Content string `json:"content"` + Tags string `json:"tags"` + Viewnum int `json:"viewnum"` + Cmtnum int `json:"cmtnum"` + Likenum int `json:"likenum"` + + Lastreplyuid int `json:"lastreplyuid"` + Lastreplytime OftenTime `json:"lastreplytime"` + + CreatedAt OftenTime `json:"created_at"` + UpdatedAt OftenTime `json:"updated_at"` + + // 排序用的时间 + SortTime OftenTime `json:"sort_time"` + + Top uint8 `json:"top"` + + Nid int `json:"nid"` + + HlTitle string `json:",omitempty"` // 高亮的标题 + HlContent string `json:",omitempty"` // 高亮的内容 +} + +func NewDocument(object interface{}, objectExt interface{}) *Document { + var document *Document + switch objdoc := object.(type) { + case *Topic: + viewnum, cmtnum, likenum := 0, 0, 0 + if objectExt != nil { + // 传递过来的是一个 *TopicEx 对象,类型是有的,即使值是 nil,这里也和 nil 是不等 + topicEx := objectExt.(*TopicUpEx) + if topicEx != nil { + viewnum = topicEx.View + cmtnum = topicEx.Reply + likenum = topicEx.Like + } + } + + var sortTime = objdoc.Ctime + if objdoc.Lastreplyuid != 0 && time.Since(time.Time(sortTime)) < 120*24*time.Hour { + sortTime = objdoc.Lastreplytime + } + + userLogin := &UserLogin{} + db.MasterDB.ID(objdoc.Uid).Get(userLogin) + document = &Document{ + Id: fmt.Sprintf("%d%d", TypeTopic, objdoc.Tid), + Objid: objdoc.Tid, + Objtype: TypeTopic, + Title: objdoc.Title, + Author: userLogin.Username, + Uid: userLogin.Uid, + PubTime: objdoc.Ctime.String(), + Content: objdoc.Content, + Tags: objdoc.Tags, + Viewnum: viewnum, + Cmtnum: cmtnum, + Likenum: likenum, + + Nid: objdoc.Nid, + + Top: objdoc.Top, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + CreatedAt: objdoc.Ctime, + UpdatedAt: objdoc.Mtime, + SortTime: sortTime, + } + case *Article: + var uid int + if objdoc.IsSelf { + userLogin := &UserLogin{} + db.MasterDB.Where("username=?", objdoc.AuthorTxt).Get(userLogin) + uid = userLogin.Uid + } + + var sortTime = objdoc.Ctime + if objdoc.Lastreplyuid != 0 && time.Since(time.Time(sortTime)) < 120*24*time.Hour { + sortTime = objdoc.Lastreplytime + } + + document = &Document{ + Id: fmt.Sprintf("%d%d", TypeArticle, objdoc.Id), + Objid: objdoc.Id, + Objtype: TypeArticle, + Title: FilterTxt(objdoc.Title), + Author: objdoc.AuthorTxt, + Uid: uid, + PubTime: objdoc.Ctime.String(), + Content: FilterTxt(objdoc.Txt), + Tags: objdoc.Tags, + Viewnum: objdoc.Viewnum, + Cmtnum: objdoc.Cmtnum, + Likenum: objdoc.Likenum, + + Top: objdoc.Top, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + CreatedAt: objdoc.Ctime, + UpdatedAt: objdoc.Mtime, + SortTime: sortTime, + } + case *Resource: + viewnum, cmtnum, likenum := 0, 0, 0 + if objectExt != nil { + resourceEx := objectExt.(*ResourceEx) + if resourceEx != nil { + viewnum = resourceEx.Viewnum + cmtnum = resourceEx.Cmtnum + } + } + + var sortTime = objdoc.Ctime + if objdoc.Lastreplyuid != 0 && time.Since(time.Time(sortTime)) < 120*24*time.Hour { + sortTime = objdoc.Lastreplytime + } + + userLogin := &UserLogin{} + db.MasterDB.ID(objdoc.Uid).Get(userLogin) + document = &Document{ + Id: fmt.Sprintf("%d%d", TypeResource, objdoc.Id), + Objid: objdoc.Id, + Objtype: TypeResource, + Title: objdoc.Title, + Author: userLogin.Username, + Uid: objdoc.Uid, + PubTime: objdoc.Ctime.String(), + Content: template.HTMLEscapeString(objdoc.Content), + Tags: objdoc.Tags, + Viewnum: viewnum, + Cmtnum: cmtnum, + Likenum: likenum, + + Top: 0, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + CreatedAt: objdoc.Ctime, + UpdatedAt: objdoc.Mtime, + SortTime: sortTime, + } + case *OpenProject: + userLogin := &UserLogin{} + db.MasterDB.Where("username=?", objdoc.Username).Get(userLogin) + + var sortTime = objdoc.Ctime + if objdoc.Lastreplyuid != 0 && time.Since(time.Time(sortTime)) < 120*24*time.Hour { + sortTime = objdoc.Lastreplytime + } + + document = &Document{ + Id: fmt.Sprintf("%d%d", TypeProject, objdoc.Id), + Objid: objdoc.Id, + Objtype: TypeProject, + Title: objdoc.Category + objdoc.Name, + Author: objdoc.Author, + Uid: userLogin.Uid, + PubTime: objdoc.Ctime.String(), + Content: objdoc.Desc, + Tags: objdoc.Tags, + Viewnum: objdoc.Viewnum, + Cmtnum: objdoc.Cmtnum, + Likenum: objdoc.Likenum, + + Top: 0, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + CreatedAt: objdoc.Ctime, + UpdatedAt: objdoc.Mtime, + SortTime: sortTime, + } + } + + return document +} + +var docRe = regexp.MustCompile("[\r \n \t\v]+") +var docSpaceRe = regexp.MustCompile("[ ]+") + +// 文本过滤(预处理) +func FilterTxt(txt string) string { + txt = strings.TrimSpace(strings.TrimPrefix(txt, "原")) + txt = strings.TrimSpace(strings.TrimPrefix(txt, "荐")) + txt = strings.TrimSpace(strings.TrimPrefix(txt, "顶")) + txt = strings.TrimSpace(strings.TrimPrefix(txt, "转")) + + txt = docRe.ReplaceAllLiteralString(txt, " ") + return docSpaceRe.ReplaceAllLiteralString(txt, " ") +} + +type AddCommand struct { + Doc *Document `json:"doc"` + Boost float64 `json:"boost,omitempty"` + Overwrite bool `json:"overwrite"` + CommitWithin int `json:"commitWithin,omitempty"` +} + +func NewDefaultArgsAddCommand(doc *Document) *AddCommand { + return NewAddCommand(doc, 0.0, true, 0) +} + +func NewAddCommand(doc *Document, boost float64, overwrite bool, commitWithin int) *AddCommand { + return &AddCommand{ + Doc: doc, + Boost: boost, + Overwrite: overwrite, + CommitWithin: commitWithin, + } +} + +type DelCommand struct { + Id string `json:"id"` +} + +func NewDelCommand(doc *Document) *DelCommand { + return &DelCommand{Id: doc.Id} +} + +type ResponseBody struct { + NumFound int `json:"numFound"` + Start int `json:"start"` + Docs []*Document `json:"docs"` +} + +type Highlighting struct { + Title []string `json:"title"` + Content []string `json:"content"` +} + +type SearchResponse struct { + RespHeader map[string]interface{} `json:"responseHeader"` + RespBody *ResponseBody `json:"response"` + Highlight map[string]*Highlighting `json:"highlighting"` +} diff --git a/internal/model/download.go b/internal/model/download.go new file mode 100644 index 00000000..7ae9f874 --- /dev/null +++ b/internal/model/download.go @@ -0,0 +1,32 @@ +// Copyright 2018 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + DLArchived = iota + DLStable + DLFeatured + DLUnstable +) + +// Download go 下载 +type Download struct { + Id int `xorm:"pk autoincr"` + Version string + Filename string + Kind string + OS string `xorm:"os"` + Arch string + Size int + Checksum string + Category int + IsRecommend bool + Seq int + CreatedAt time.Time `xorm:"created"` +} diff --git a/internal/model/dynamic.go b/internal/model/dynamic.go new file mode 100644 index 00000000..f8fa8b7c --- /dev/null +++ b/internal/model/dynamic.go @@ -0,0 +1,19 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +// 动态(go动态;本站动态等) +type Dynamic struct { + Id int `json:"id" xorm:"pk autoincr"` + Content string `json:"content"` + Dmtype int `json:"dmtype"` + Url string `json:"url"` + Seq int `json:"seq"` + Ctime time.Time `json:"ctime" xorm:"created"` +} diff --git a/internal/model/favorite.go b/internal/model/favorite.go new file mode 100644 index 00000000..0300c9a2 --- /dev/null +++ b/internal/model/favorite.go @@ -0,0 +1,19 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +// 用户收藏(用户可以收藏文章、话题、资源等) +type Favorite struct { + Uid int `json:"uid"` + Objtype int `json:"objtype"` + Objid int `json:"objid"` + Ctime string `json:"ctime" xorm:"<-"` +} + +func (*Favorite) TableName() string { + return "favorites" +} diff --git a/internal/model/feed.go b/internal/model/feed.go new file mode 100644 index 00000000..99104028 --- /dev/null +++ b/internal/model/feed.go @@ -0,0 +1,164 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "github.com/polaris1119/config" + + "github.com/studygolang/studygolang/db" + + "github.com/polaris1119/logger" +) + +const FeedOffline = 1 + +type Feed struct { + Id int `xorm:"pk autoincr"` + Title string + Objid int + Objtype int + Uid int + Author string + Nid int + Lastreplyuid int + Lastreplytime OftenTime + Tags string + Cmtnum int + Likenum int + Top uint8 + Seq int + State int + CreatedAt OftenTime `xorm:"created"` + UpdatedAt OftenTime `json:"updated_at" xorm:"<-"` + + User *User `xorm:"-"` + Lastreplyuser *User `xorm:"-"` + Node map[string]interface{} `xorm:"-"` + Uri string `xorm:"-"` +} + +// PublishFeed 发布动态 +func PublishFeed(object interface{}, objectExt interface{}, me *Me) { + var feed *Feed + switch objdoc := object.(type) { + case *Topic: + node := &TopicNode{} + _, err := db.MasterDB.ID(objdoc.Nid).Get(node) + if err == nil && !node.ShowIndex { + return + } + + cmtnum := 0 + if objectExt != nil { + // 传递过来的是一个 *TopicEx 对象,类型是有的,即时值是 nil,这里也和 nil 是不等 + topicEx := objectExt.(*TopicEx) + if topicEx != nil { + cmtnum = topicEx.Reply + } + } + + feed = &Feed{ + Objid: objdoc.Tid, + Objtype: TypeTopic, + Title: objdoc.Title, + Uid: objdoc.Uid, + Tags: objdoc.Tags, + Cmtnum: cmtnum, + Nid: objdoc.Nid, + Top: objdoc.Top, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + UpdatedAt: objdoc.Mtime, + } + case *Article: + var uid int + if objdoc.Domain == WebsiteSetting.Domain { + userLogin := &UserLogin{} + db.MasterDB.Where("username=?", objdoc.AuthorTxt).Get(userLogin) + uid = userLogin.Uid + } + + feed = &Feed{ + Objid: objdoc.Id, + Objtype: TypeArticle, + Title: FilterTxt(objdoc.Title), + Author: objdoc.AuthorTxt, + Uid: uid, + Tags: objdoc.Tags, + Cmtnum: objdoc.Cmtnum, + Top: objdoc.Top, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + UpdatedAt: objdoc.Mtime, + } + case *Resource: + cmtnum := 0 + if objectExt != nil { + resourceEx := objectExt.(*ResourceEx) + if resourceEx != nil { + cmtnum = resourceEx.Cmtnum + } + } + + feed = &Feed{ + Objid: objdoc.Id, + Objtype: TypeResource, + Title: objdoc.Title, + Uid: objdoc.Uid, + Tags: objdoc.Tags, + Cmtnum: cmtnum, + Nid: objdoc.Catid, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + UpdatedAt: objdoc.Mtime, + } + case *OpenProject: + userLogin := &UserLogin{} + db.MasterDB.Where("username=?", objdoc.Username).Get(userLogin) + feed = &Feed{ + Objid: objdoc.Id, + Objtype: TypeProject, + Title: objdoc.Category + " " + objdoc.Name, + Author: objdoc.Author, + Uid: userLogin.Uid, + Tags: objdoc.Tags, + Cmtnum: objdoc.Cmtnum, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + UpdatedAt: objdoc.Mtime, + } + case *Book: + feed = &Feed{ + Objid: objdoc.Id, + Objtype: TypeBook, + Title: "分享一本图书《" + objdoc.Name + "》", + Uid: objdoc.Uid, + Tags: objdoc.Tags, + Cmtnum: objdoc.Cmtnum, + Lastreplyuid: objdoc.Lastreplyuid, + Lastreplytime: objdoc.Lastreplytime, + UpdatedAt: objdoc.UpdatedAt, + } + + if me == nil { + me = &Me{ + IsAdmin: true, + } + } + } + + feedDay := config.ConfigFile.MustInt("feed", "day", 3) + feed.Seq = feedDay * 24 + if me != nil && me.IsAdmin { + feed.Seq += 100000 + } + + _, err := db.MasterDB.Insert(feed) + if err != nil { + logger.Errorln("publish feed:", object, " error:", err) + } +} diff --git a/internal/model/friend_link.go b/internal/model/friend_link.go new file mode 100644 index 00000000..3e8bd55e --- /dev/null +++ b/internal/model/friend_link.go @@ -0,0 +1,18 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +type FriendLink struct { + Id int `json:"-" xorm:"pk autoincr"` + Name string `json:"name"` + Url string `json:"url"` + Logo string `json:"logo"` + Seq int `json:"-"` + CreatedAt time.Time `json:"-" xorm:"created"` +} diff --git a/internal/model/gctt.go b/internal/model/gctt.go new file mode 100644 index 00000000..7679ebdf --- /dev/null +++ b/internal/model/gctt.go @@ -0,0 +1,118 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "time" + + "xorm.io/xorm" +) + +const ( + GCTTRoleTranslator = iota + GCTTRoleLeader + GCTTRoleSelecter // 选题 + GCTTRoleChecker // 校对 + GCTTRoleCore // 核心成员 +) + +const ( + IssueOpened = iota + IssueClosed +) + +const ( + LabelUnClaim = "待认领" + LabelClaimed = "已认领" +) + +var roleMap = map[int]string{ + GCTTRoleTranslator: "译者", + GCTTRoleLeader: "组长", + GCTTRoleSelecter: "选题", + GCTTRoleChecker: "校对", + GCTTRoleCore: "核心成员", +} + +var faMap = map[int]string{ + GCTTRoleTranslator: "fa-user", + GCTTRoleLeader: "fa-graduation-cap", + GCTTRoleSelecter: "fa-user-circle", + GCTTRoleChecker: "fa-user-secret", + GCTTRoleCore: "fa-heart", +} + +type GCTTUser struct { + Id int `xorm:"pk autoincr"` + Username string + Avatar string + Uid int + JoinedAt int64 + LastAt int64 + Num int + Words int + AvgTime int + Role int + CreatedAt time.Time `xorm:"<-"` + + RoleName string `xorm:"-"` + Fa string `xorm:"-"` +} + +func (this *GCTTUser) AfterSet(name string, cell xorm.Cell) { + if name == "role" { + this.RoleName = roleMap[this.Role] + this.Fa = faMap[this.Role] + } +} + +func (*GCTTUser) TableName() string { + return "gctt_user" +} + +type GCTTGit struct { + Id int `xorm:"pk autoincr"` + Username string + Md5 string + Title string + PR int `xorm:"pr"` + TranslatingAt int64 + TranslatedAt int64 + Words int + ArticleId int + CreatedAt time.Time `xorm:"<-"` +} + +func (*GCTTGit) TableName() string { + return "gctt_git" +} + +type GCTTIssue struct { + Id int `xorm:"pk autoincr"` + Translator string + Email string + Title string + TranslatingAt int64 + TranslatedAt int64 + Label string + State uint8 + CreatedAt time.Time `xorm:"<-"` +} + +func (*GCTTIssue) TableName() string { + return "gctt_issue" +} + +type GCTTTimeLine struct { + Id int `xorm:"pk autoincr"` + Content string + CreatedAt time.Time +} + +func (*GCTTTimeLine) TableName() string { + return "gctt_timeline" +} diff --git a/internal/model/gift.go b/internal/model/gift.go new file mode 100644 index 00000000..e35f0df0 --- /dev/null +++ b/internal/model/gift.go @@ -0,0 +1,67 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "time" + + "xorm.io/xorm" +) + +const ( + GiftStateOnline = 1 + GiftStateExpired = 3 + + GiftTypRedeem = 0 + GiftTypDiscount = 1 +) + +var GiftTypeMap = map[int]string{ + GiftTypRedeem: "兑换码", + GiftTypDiscount: "折扣", +} + +type Gift struct { + Id int `json:"id" xorm:"pk autoincr"` + Name string + Description string + Price int + TotalNum int + RemainNum int + ExpireTime time.Time `xorm:"int"` + Supplier string + BuyLimit int + Typ int + State int + CreatedAt OftenTime `xorm:"<-"` + + TypShow string `xorm:"-"` +} + +func (this *Gift) AfterSet(name string, cell xorm.Cell) { + if name == "typ" { + this.TypShow = GiftTypeMap[this.Typ] + } +} + +type GiftRedeem struct { + Id int `json:"id" xorm:"pk autoincr"` + GiftId int + Code string + Exchange int + Uid int + UpdatedAt OftenTime `xorm:"<-"` +} + +type UserExchangeRecord struct { + Id int `json:"id" xorm:"pk autoincr"` + GiftId int + Uid int + Remark string + ExpireTime time.Time `xorm:"int"` + CreatedAt OftenTime `xorm:"<-"` +} diff --git a/internal/model/github_user.go b/internal/model/github_user.go new file mode 100644 index 00000000..f54749ef --- /dev/null +++ b/internal/model/github_user.go @@ -0,0 +1,31 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "code.gitea.io/sdk/gitea" +) + +type GithubUser struct { + Id int `json:"id"` + Login string `json:"login"` + AvatarUrl string `json:"avatar_url"` + Email string `json:"email"` + Name string `json:"name"` + Company string `json:"company"` + Blog string `json:"blog"` + Location string `json:"location"` +} + +type GiteaUser = gitea.User + +func DisplayName(g *GiteaUser) string { + if g.FullName == "" { + return g.UserName + } + return g.FullName +} diff --git a/internal/model/image.go b/internal/model/image.go new file mode 100644 index 00000000..91f4efca --- /dev/null +++ b/internal/model/image.go @@ -0,0 +1,17 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +// 图片 +type Image struct { + Pid int `xorm:"pk autoincr"` + Md5 string + Path string + Size int + Width int + Height int +} diff --git a/internal/model/interview_question.go b/internal/model/interview_question.go new file mode 100644 index 00000000..0284d6a7 --- /dev/null +++ b/internal/model/interview_question.go @@ -0,0 +1,39 @@ +// Copyright 2022 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "strconv" + "time" + + "xorm.io/xorm" +) + +// Go 面试题 +type InterviewQuestion struct { + Id int `json:"id" xorm:"pk autoincr"` + Sn int64 `json:"sn"` + ShowSn string `json:"show_sn" xorm:"-"` + Question string `json:"question"` + Answer string `json:"answer"` + Level int `json:"level"` + Viewnum int `json:"viewnum"` + Cmtnum int `json:"cmtnum"` + Likenum int `json:"likenum"` + Source string `json:"source"` + CreatedAt time.Time `json:"created_at" xorm:"created"` +} + +func (iq *InterviewQuestion) AfterSet(name string, cell xorm.Cell) { + if name == "sn" { + iq.ShowSn = strconv.FormatInt(iq.Sn, 32) + } +} + +func (iq *InterviewQuestion) AfterInsert() { + iq.ShowSn = strconv.FormatInt(iq.Sn, 32) +} diff --git a/internal/model/learning_material.go b/internal/model/learning_material.go new file mode 100644 index 00000000..e8f81845 --- /dev/null +++ b/internal/model/learning_material.go @@ -0,0 +1,19 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +type LearningMaterial struct { + Id int `json:"-" xorm:"pk autoincr"` + Title string `json:"title"` + Url string `json:"url"` + Type int `json:"type"` + Seq int `json:"-"` + FirstUrl string `json:"first_url"` + CreatedAt time.Time `json:"-" xorm:"created"` +} diff --git a/internal/model/like.go b/internal/model/like.go new file mode 100644 index 00000000..e9baa46a --- /dev/null +++ b/internal/model/like.go @@ -0,0 +1,28 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + FlagCancel = iota + FlagLike // 喜欢 + FlagUnlike // 不喜欢(暂时不支持) +) + +// 赞(喜欢) +type Like struct { + Uid int `json:"uid"` + Objid int `json:"objid"` + Objtype int `json:"objtype"` + Flag int `json:"flag"` + Ctime time.Time `json:"ctime" xorm:"<-"` +} + +func (*Like) TableName() string { + return "likes" +} diff --git a/internal/model/message.go b/internal/model/message.go new file mode 100644 index 00000000..b319bab1 --- /dev/null +++ b/internal/model/message.go @@ -0,0 +1,79 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "encoding/json" + + "github.com/polaris1119/logger" +) + +const ( + FdelNotDel = "未删" + FdelHasDel = "已删" + + TdelNotDel = "未删" + TdelHasDel = "已删" + + HasRead = "已读" + NotRead = "未读" +) + +// 短消息 +type Message struct { + Id int `json:"id" xorm:"pk autoincr"` + Content string `json:"content"` + Hasread string `json:"hasread"` + From int `json:"from"` + Fdel string `json:"fdel"` + To int `json:"to"` + Tdel string `json:"tdel"` + Ctime OftenTime `json:"ctime" xorm:"created"` +} + +const ( + // 和comment中objtype保持一致(除了@) + MsgtypeTopicReply = iota // 回复我的主题 + MsgtypeArticleComment // 评论我的博文 + MsgtypeResourceComment // 评论我的资源 + MsgtypeWikiComment // 评论我的Wiki页 + MsgtypeProjectComment // 评论我的项目 + + MsgtypeAtMe = 10 // 评论 @提到我 + MsgtypePublishAtMe = 11 // 发布时提到我 + + MsgtypeSubjectContribute = 12 //专栏投稿 +) + +// 系统消息 +type SystemMessage struct { + Id int `json:"id" xorm:"pk autoincr"` + Msgtype int `json:"msgtype"` + Hasread string `json:"hasread"` + To int `json:"to"` + Ctime OftenTime `json:"ctime" xorm:"created"` + + // 扩展信息,json格式 + Ext string +} + +func (this *SystemMessage) GetExt() map[string]interface{} { + result := make(map[string]interface{}) + if err := json.Unmarshal([]byte(this.Ext), &result); err != nil { + logger.Errorln("SystemMessage Ext JsonUnmarshal Error:", err) + return nil + } + return result +} + +func (this *SystemMessage) SetExt(ext map[string]interface{}) { + if extBytes, err := json.Marshal(ext); err != nil { + logger.Errorln("SystemMessage SetExt JsonMarshal Error:", err) + } else { + this.Ext = string(extBytes) + } +} diff --git a/internal/model/mission.go b/internal/model/mission.go new file mode 100644 index 00000000..f2750edf --- /dev/null +++ b/internal/model/mission.go @@ -0,0 +1,71 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + MissionTypeLogin = 1 + MissionTypeInitial = 2 + MissionTypeShare = 3 + MissionTypeAdd = 4 + + // 回复 + MissionTypeReply = 51 + // 创建 + MissionTypeTopic = 52 + MissionTypeArticle = 53 + MissionTypeResource = 54 + MissionTypeWiki = 55 + MissionTypeProject = 56 + MissionTypeBook = 57 + + MissionTypeAppend = 60 + // 置顶 + MissionTypeTop = 61 + + MissionTypeModify = 65 + // 被回复 + MissionTypeReplied = 70 + // 额外赠予 + MissionTypeAward = 80 + // 活跃奖励 + MissionTypeActive = 81 + + // 物品兑换 + MissionTypeGift = 100 + + // 管理员操作后处罚 + MissionTypePunish = 120 + // 水 + MissionTypeSpam = 127 +) + +const ( + InitialMissionId = 1 +) + +type Mission struct { + Id int `json:"id" xorm:"pk autoincr"` + Name string `json:"name"` + Type int `json:"type"` + Fixed int `json:"fixed"` + Min int `json:"min"` + Max int `json:"max"` + Incr int `json:"incr"` + State int `json:"state"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} + +type UserLoginMission struct { + Uid int `json:"uid" xorm:"pk"` + Date int `json:"date"` + Award int `json:"award"` + Days int `json:"days"` + TotalDays int `json:"total_days"` + UpdatedAt time.Time `json:"updated_at" xorm:"<-"` +} diff --git a/internal/model/morning_reading.go b/internal/model/morning_reading.go new file mode 100644 index 00000000..a9902f95 --- /dev/null +++ b/internal/model/morning_reading.go @@ -0,0 +1,48 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "strings" + "time" + + "xorm.io/xorm" +) + +const ( + RtypeGo = iota // Go技术晨读 + RtypeComp // 综合技术晨读 +) + +// 技术晨读 +type MorningReading struct { + Id int `json:"id" xorm:"pk autoincr"` + Content string `json:"content"` + Rtype int `json:"rtype"` + Inner int `json:"inner"` + Url string `json:"url"` + Moreurls string `json:"moreurls"` + Username string `json:"username"` + Clicknum int `json:"clicknum,omitempty"` + Ctime OftenTime `json:"ctime" xorm:"<-"` + + // 晨读日期,从 ctime 中提取 + Rdate string `json:"rdate,omitempty" xorm:"-"` + + Urls []string `json:"urls" xorm:"-"` +} + +func (this *MorningReading) AfterSet(name string, cell xorm.Cell) { + switch name { + case "ctime": + this.Rdate = time.Time(this.Ctime).Format("2006-01-02") + case "moreurls": + if this.Moreurls != "" { + this.Urls = strings.Split(this.Moreurls, ",") + } + } +} diff --git a/internal/model/openproject.go b/internal/model/openproject.go new file mode 100644 index 00000000..201debda --- /dev/null +++ b/internal/model/openproject.go @@ -0,0 +1,85 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "net/url" + "time" + + "xorm.io/xorm" +) + +const ( + ProjectStatusNew = 0 + ProjectStatusOnline = 1 + ProjectStatusOffline = 2 +) + +// 开源项目信息 +type OpenProject struct { + Id int `json:"id" xorm:"pk autoincr"` + Name string `json:"name"` + Category string `json:"category"` + Uri string `json:"uri"` + Home string `json:"home"` + Doc string `json:"doc"` + Download string `json:"download"` + Src string `json:"src"` + Logo string `json:"logo"` + Desc string `json:"desc"` + Repo string `json:"repo"` + Author string `json:"author"` + Licence string `json:"licence"` + Lang string `json:"lang"` + Os string `json:"os"` + Tags string `json:"tags"` + Username string `json:"username,omitempty"` + Viewnum int `json:"viewnum,omitempty"` + Cmtnum int `json:"cmtnum,omitempty"` + Likenum int `json:"likenum,omitempty"` + Lastreplyuid int `json:"lastreplyuid"` + Lastreplytime OftenTime `json:"lastreplytime"` + Status int `json:"status"` + Ctime OftenTime `json:"ctime,omitempty" xorm:"created"` + Mtime OftenTime `json:"mtime,omitempty" xorm:"<-"` + + User *User `json:"user" xorm:"-"` + // 排行榜阅读量 + RankView int `json:"rank_view" xorm:"-"` + LastReplyUser *User `json:"last_reply_user" xorm:"-"` +} + +func (this *OpenProject) BeforeInsert() { + if this.Tags == "" { + this.Tags = AutoTag(this.Name+this.Category, this.Desc, 4) + } + + this.Lastreplytime = NewOftenTime() +} + +func (this *OpenProject) AfterInsert() { + go func() { + // AfterInsert 时,自增 ID 还未赋值,这里 sleep 一会,确保自增 ID 有值 + for { + if this.Id > 0 { + PublishFeed(this, nil, nil) + return + } + time.Sleep(100 * time.Millisecond) + } + }() +} + +func (this *OpenProject) AfterSet(name string, cell xorm.Cell) { + if name == "logo" && this.Logo == "" { + this.Logo = WebsiteSetting.ProjectDfLogo + } +} + +func (this *OpenProject) AfterLoad() { + this.Uri = url.QueryEscape(this.Uri) +} diff --git a/internal/model/resource.go b/internal/model/resource.go new file mode 100644 index 00000000..a9cd3819 --- /dev/null +++ b/internal/model/resource.go @@ -0,0 +1,72 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + LinkForm = "只是链接" + ContentForm = "包括内容" +) + +// 资源信息 +type Resource struct { + Id int `json:"id" xorm:"pk autoincr"` + Title string `json:"title"` + Form string `json:"form"` + Content string `json:"content"` + Url string `json:"url"` + Uid int `json:"uid"` + Catid int `json:"catid"` + CatName string `json:"-" xorm:"-"` + Lastreplyuid int `json:"lastreplyuid"` + Lastreplytime OftenTime `json:"lastreplytime"` + Tags string `json:"tags"` + Ctime OftenTime `json:"ctime" xorm:"created"` + Mtime OftenTime `json:"mtime" xorm:"<-"` + + // 排行榜阅读量 + RankView int `json:"rank_view" xorm:"-"` +} + +func (this *Resource) BeforeInsert() { + if this.Tags == "" { + this.Tags = AutoTag(this.Title+this.CatName, this.Content, 4) + } + + this.Lastreplytime = NewOftenTime() +} + +// 资源扩展(计数)信息 +type ResourceEx struct { + Id int `json:"-" xorm:"pk"` + Viewnum int `json:"viewnum"` + Cmtnum int `json:"cmtnum"` + Likenum int `json:"likenum"` + Mtime time.Time `json:"mtime" xorm:"<-"` +} + +type ResourceInfo struct { + Resource `xorm:"extends"` + ResourceEx `xorm:"extends"` +} + +func (*ResourceInfo) TableName() string { + return "resource" +} + +// 资源分类信息 +type ResourceCat struct { + Catid int `json:"catid" xorm:"pk autoincr"` + Name string `json:"name"` + Intro string `json:"intro"` + Ctime string `json:"ctime" xorm:"<-"` +} + +func (*ResourceCat) TableName() string { + return "resource_category" +} diff --git a/internal/model/role.go b/internal/model/role.go new file mode 100644 index 00000000..e2e84c8c --- /dev/null +++ b/internal/model/role.go @@ -0,0 +1,38 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +// 角色分界点:roleid 大于该值,则没有管理权限 +const AdminMinRoleId = 7 // 晨读管理员 + +const ( + // Master 站长 + Master = iota + 1 + AssistantMaster + Administrator + TopicAdmin + ResourceAdmin + ArticleAdmin + ReadingAdmin +) + +// 角色信息 +type Role struct { + Roleid int `json:"roleid" xorm:"pk autoincr"` + Name string `json:"name"` + OpUser string `json:"op_user"` + Ctime string `json:"ctime,omitempty" xorm:"created"` + Mtime string `json:"mtime,omitempty" xorm:"<-"` +} + +// 角色权限信息 +type RoleAuthority struct { + Roleid int `json:"roleid" xorm:"pk autoincr"` + Aid int `json:"aid"` + OpUser string `json:"op_user"` + Ctime string `json:"ctime" xorm:"<-"` +} diff --git a/internal/model/search_stat.go b/internal/model/search_stat.go new file mode 100644 index 00000000..d6d5b47b --- /dev/null +++ b/internal/model/search_stat.go @@ -0,0 +1,17 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +// 搜索词统计 +type SearchStat struct { + Id int `json:"id" xorm:"pk autoincr"` + Keyword string `json:"keyword"` + Times int `json:"times"` + Ctime time.Time `json:"ctime" xorm:"<-"` +} diff --git a/internal/model/subject.go b/internal/model/subject.go new file mode 100644 index 00000000..10f8a274 --- /dev/null +++ b/internal/model/subject.go @@ -0,0 +1,72 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "time" +) + +// Subject 专栏 +type Subject struct { + Id int `xorm:"pk autoincr" json:"id"` + Name string `json:"name"` + Cover string `json:"cover"` + Description string `json:"description"` + Uid int `json:"uid"` + Contribute bool `json:"contribute"` + Audit bool `json:"audit"` + ArticleNum int `json:"article_num"` + CreatedAt OftenTime `json:"created_at" xorm:"created"` + UpdatedAt OftenTime `json:"updated_at" xorm:"<-"` + + User *User `json:"user" xorm:"-"` +} + +// SubjectAdmin 专栏管理员 +type SubjectAdmin struct { + Id int `xorm:"pk autoincr" json:"id"` + Sid int `json:"sid"` + Uid int `json:"uid"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} + +const ( + ContributeStateNew = iota + ContributeStateOnline + ContributeStateOffline +) + +// SubjectArticle 专栏文章 +type SubjectArticle struct { + Id int `xorm:"pk autoincr" json:"id"` + Sid int `json:"sid"` + ArticleId int `json:"article_id"` + State int `json:"state"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} + +// SubjectArticles xorm join 需要 +type SubjectArticles struct { + Article `xorm:"extends"` + Sid int + CreatedAt time.Time +} + +func (*SubjectArticles) TableName() string { + return "articles" +} + +// SubjectFollower 专栏关注者 +type SubjectFollower struct { + Id int `xorm:"pk autoincr" json:"id"` + Sid int `json:"sid"` + Uid int `json:"uid"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` + + User *User `xorm:"-"` + TimeAgo string `xorm:"-"` +} diff --git a/internal/model/topic.go b/internal/model/topic.go new file mode 100644 index 00000000..e45d89d5 --- /dev/null +++ b/internal/model/topic.go @@ -0,0 +1,144 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + FlagNoAudit = iota + FlagNormal + FlagAuditDelete + FlagUserDelete +) + +const ( + // 最多附言条数 + AppendMaxNum = 3 +) + +const ( + PermissionPublic = iota // 公开 + PermissionLogin // 登录可见 + PermissionFollow // 关注可见(暂未实现) + PermissionPay // 知识星球或其他方式付费可见 + PermissionOnlyMe // 自己可见 +) + +// 社区主题信息 +type Topic struct { + Tid int `xorm:"pk autoincr" json:"tid"` + Title string `json:"title"` + Content string `json:"content"` + Nid int `json:"nid"` + Uid int `json:"uid"` + Flag uint8 `json:"flag"` + Lastreplyuid int `json:"lastreplyuid"` + Lastreplytime OftenTime `json:"lastreplytime"` + EditorUid int `json:"editor_uid"` + Top uint8 `json:"top"` + TopTime int64 `json:"top_time"` + Tags string `json:"tags"` + Permission int `json:"permission"` + CloseReply bool `json:"close_reply"` + Ctime OftenTime `json:"ctime" xorm:"created"` + Mtime OftenTime `json:"mtime" xorm:"<-"` + + // 为了方便,加上Node(节点名称,数据表没有) + Node string `xorm:"-"` + // 排行榜阅读量 + RankView int `json:"rank_view" xorm:"-"` +} + +func (*Topic) TableName() string { + return "topics" +} + +func (this *Topic) BeforeInsert() { + if this.Tags == "" { + this.Tags = AutoTag(this.Title, this.Content, 4) + } +} + +// 社区主题扩展(计数)信息 +type TopicEx struct { + Tid int `json:"-"` + View int `json:"view"` + Reply int `json:"reply"` + Like int `json:"like"` + Mtime time.Time `json:"mtime" xorm:"<-"` +} + +func (*TopicEx) TableName() string { + return "topics_ex" +} + +// 社区主题扩展(计数)信息,用于 incr 更新 +type TopicUpEx struct { + Tid int `json:"-" xorm:"pk"` + View int `json:"view"` + Reply int `json:"reply"` + Like int `json:"like"` + Mtime time.Time `json:"mtime" xorm:"<-"` +} + +func (*TopicUpEx) TableName() string { + return "topics_ex" +} + +type TopicInfo struct { + Topic `xorm:"extends"` + TopicEx `xorm:"extends"` +} + +func (*TopicInfo) TableName() string { + return "topics" +} + +type TopicAppend struct { + Id int `xorm:"pk autoincr"` + Tid int + Content string + CreatedAt OftenTime `xorm:"<-"` +} + +// 社区主题节点信息 +type TopicNode struct { + Nid int `json:"nid" xorm:"pk autoincr"` + Parent int `json:"parent"` + Logo string `json:"logo"` + Name string `json:"name"` + Ename string `json:"ename"` + Seq int `json:"seq"` + Intro string `json:"intro"` + ShowIndex bool `json:"show_index"` + Ctime time.Time `json:"ctime" xorm:"<-"` + + Level int `json:"-" xorm:"-"` +} + +func (*TopicNode) TableName() string { + return "topics_node" +} + +// 推荐节点 +type RecommendNode struct { + Id int `json:"id" xorm:"pk autoincr"` + Name string `json:"name"` + Parent int `json:"parent"` + Nid int `json:"nid"` + Seq int `json:"seq"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} + +type NodeInfo struct { + RecommendNode `xorm:"extends"` + TopicNode `xorm:"extends"` +} + +func (*NodeInfo) TableName() string { + return "recommend_node" +} diff --git a/internal/model/type.go b/internal/model/type.go new file mode 100644 index 00000000..11a0aa8d --- /dev/null +++ b/internal/model/type.go @@ -0,0 +1,76 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "errors" + "time" +) + +type OftenTime time.Time + +func NewOftenTime() OftenTime { + t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2000-01-01 00:00:00", time.Local) + return OftenTime(t) +} + +func (self OftenTime) String() string { + t := time.Time(self) + if t.IsZero() { + return "0000-00-00 00:00:00" + } + return t.Format("2006-01-02 15:04:05") +} + +func (self OftenTime) MarshalBinary() ([]byte, error) { + return time.Time(self).MarshalBinary() +} + +func (self OftenTime) MarshalJSON() ([]byte, error) { + t := time.Time(self) + if y := t.Year(); y < 0 || y >= 10000 { + if y < 2000 { + return []byte(`"2000-01-01 00:00:00"`), nil + } + return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") + } + return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil +} + +func (self OftenTime) MarshalText() ([]byte, error) { + return time.Time(self).MarshalText() +} + +func (this *OftenTime) UnmarshalBinary(data []byte) error { + t := time.Time(*this) + return t.UnmarshalBinary(data) +} + +func (this *OftenTime) UnmarshalJSON(data []byte) (err error) { + str := string(data) + if str == "null" { + return nil + } + + if str == `"0001-01-01 08:00:00"` { + + ft := NewOftenTime() + this = &ft + return nil + } + + var t time.Time + t, err = time.ParseInLocation(`"2006-01-02 15:04:05"`, str, time.Local) + *this = OftenTime(t) + + return +} + +func (this *OftenTime) UnmarshalText(data []byte) (err error) { + t := time.Time(*this) + return t.UnmarshalText(data) +} diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 00000000..abea7b60 --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,198 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "errors" + "fmt" + "math/rand" + "time" + + "github.com/polaris1119/goutils" + "xorm.io/xorm" +) + +// 用户登录信息 +type UserLogin struct { + Uid int `json:"uid" xorm:"pk"` + Username string `json:"username"` + Passcode string `json:"passcode"` // 加密随机串 + Passwd string `json:"passwd"` + Email string `json:"email"` + LoginIp string `json:"login_ip"` + LoginTime time.Time `json:"login_time" xorm:"<-"` +} + +func (this *UserLogin) TableName() string { + return "user_login" +} + +// 生成加密密码 +func (this *UserLogin) GenMd5Passwd() error { + if this.Passwd == "" { + return errors.New("password is empty!") + } + this.Passcode = fmt.Sprintf("%x", rand.Int31()) + // 密码经过md5(passwd+passcode)加密保存 + this.Passwd = goutils.Md5(this.Passwd + this.Passcode) + return nil +} + +const ( + UserStatusNoAudit = iota + UserStatusAudit // 已激活 + UserStatusRefuse + UserStatusFreeze // 冻结 + UserStatusOutage // 停用 +) + +const ( + // 用户拥有的权限设置 + DauAuthTopic = 1 << iota + DauAuthArticle + DauAuthResource + DauAuthWiki + DauAuthProject + DauAuthBook + DauAuthComment // 评论 + DauAuthTop // 置顶 +) + +const DefaultAuth = DauAuthTopic | DauAuthArticle | DauAuthResource | DauAuthProject | DauAuthComment + +// 用户基本信息 +type User struct { + Uid int `json:"uid" xorm:"pk autoincr"` + Username string `json:"username" validate:"min=4,max=20,regexp=^[a-zA-Z0-9_]*$"` + Email string `json:"email"` + Open int `json:"open"` + Name string `json:"name"` + Avatar string `json:"avatar"` + City string `json:"city"` + Company string `json:"company"` + Github string `json:"github"` + Gitea string `json:"gitea"` + Weibo string `json:"weibo"` + Website string `json:"website"` + Monlog string `json:"monlog"` + Introduce string `json:"introduce"` + Unsubscribe int `json:"unsubscribe"` + Balance int `json:"balance"` + IsThird int `json:"is_third"` + DauAuth int `json:"dau_auth"` + IsVip bool `json:"is_vip"` + VipExpire int `json:"vip_expire"` + Status int `json:"status"` + IsRoot bool `json:"is_root"` + Ctime OftenTime `json:"ctime" xorm:"created"` + Mtime time.Time `json:"mtime" xorm:"<-"` + + // 非用户表中的信息,为了方便放在这里 + Roleids []int `xorm:"-"` + Rolenames []string `xorm:"-"` + + // 活跃度 + Weight int `json:"weight" xorm:"-"` + Gold int `json:"gold" xorm:"-"` + Silver int `json:"silver" xorm:"-"` + Copper int `json:"copper" xorm:"-"` + + IsOnline bool `json:"is_online" xorm:"-"` +} + +func (this *User) TableName() string { + return "user_info" +} + +func (this *User) String() string { + buffer := goutils.NewBuffer() + buffer.Append(this.Username).Append(" "). + Append(this.Email).Append(" "). + Append(this.Uid).Append(" "). + Append(this.Mtime) + + return buffer.String() +} + +func (this *User) AfterSet(name string, cell xorm.Cell) { + if name == "balance" { + this.Gold = this.Balance / 10000 + balance := this.Balance % 10000 + + this.Silver = balance / 100 + this.Copper = balance % 100 + } +} + +// Me 代表当前用户 +type Me struct { + Uid int `json:"uid"` + Username string `json:"username"` + Name string `json:"name"` + Monlog string `json:"monlog"` + Email string `json:"email"` + Avatar string `json:"avatar"` + Status int `json:"status"` + MsgNum int `json:"msgnum"` + IsAdmin bool `json:"isadmin"` + IsRoot bool `json:"is_root"` + DauAuth int `json:"dau_auth"` + IsVip bool `json:"is_vip"` + CreatedAt time.Time `json:"created_at"` + + Balance int `json:"balance"` + Gold int `json:"gold"` + Silver int `json:"silver"` + Copper int `json:"copper"` + + RoleIds []int `json:"-"` +} + +// 活跃用户信息 +// 活跃度规则: +// +// 1、注册成功后 +2 +// 2、登录一次 +1 +// 3、修改资料 +1 +// 4、发帖子 + 10 +// 5、评论 +5 +// 6、创建Wiki页 +10 +type UserActive struct { + Uid int `json:"uid" xorm:"pk"` + Username string `json:"username"` + Email string `json:"email"` + Avatar string `json:"avatar"` + Weight int `json:"weight"` + Mtime time.Time `json:"mtime" xorm:"<-"` +} + +// 用户角色信息 +type UserRole struct { + Uid int `json:"uid"` + Roleid int `json:"roleid"` + ctime string `xorm:"-"` +} + +const ( + BindTypeGithub = iota + BindTypeGitea +) + +type BindUser struct { + Id int `json:"id" xorm:"pk autoincr"` + Uid int `json:"uid"` + Type int `json:"type"` + Email string `json:"email"` + Tuid int `json:"tuid"` + Username string `json:"username"` + Name string `json:"name"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Expire int `json:"expire"` + Avatar string `json:"avatar"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` +} diff --git a/internal/model/user_rich.go b/internal/model/user_rich.go new file mode 100644 index 00000000..010022a5 --- /dev/null +++ b/internal/model/user_rich.go @@ -0,0 +1,63 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "time" + + "xorm.io/xorm" +) + +var BalanceTypeMap = map[int]string{ + MissionTypeLogin: "每日登录奖励", + MissionTypeInitial: "初始资本", + MissionTypeShare: "分享获得", + MissionTypeAdd: "充值获得", + MissionTypeReply: "创建回复", + MissionTypeTopic: "创建主题", + MissionTypeArticle: "发表文章", + MissionTypeResource: "分享资源", + MissionTypeWiki: "创建WIKI", + MissionTypeProject: "发布项目", + MissionTypeBook: "分享图书", + MissionTypeAppend: "增加附言", + MissionTypeTop: "置顶", + MissionTypeModify: "修改", + MissionTypeReplied: "回复收益", + MissionTypeAward: "额外赠予", + MissionTypeActive: "活跃奖励", + MissionTypeGift: "兑换物品", + MissionTypePunish: "处罚", + MissionTypeSpam: "Spam", +} + +type UserBalanceDetail struct { + Id int `json:"id" xorm:"pk autoincr"` + Uid int `json:"uid"` + Type int `json:"type"` + Num int `json:"num"` + Balance int `json:"balance"` + Desc string `json:"desc"` + CreatedAt time.Time `json:"created_at" xorm:"<-"` + + TypeShow string `json:"type_show" xorm:"-"` +} + +func (this *UserBalanceDetail) AfterSet(name string, cell xorm.Cell) { + if name == "type" { + this.TypeShow = BalanceTypeMap[this.Type] + } +} + +type UserRecharge struct { + Id int `json:"id" xorm:"pk autoincr"` + Uid int `json:"uid"` + Amount int `json:"amount"` + Channel string `json:"channel"` + Remark string `json:"remark"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/model/user_setting.go b/internal/model/user_setting.go new file mode 100644 index 00000000..2727f7c6 --- /dev/null +++ b/internal/model/user_setting.go @@ -0,0 +1,23 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +const ( + KeyNewUserWait = "new_user_wait" // 新用户注册多久才能发布帖子,单位秒,0表示没限制 + KeyCanEditTime = "can_edit_time" // 发布后多久内能够编辑,单位秒 + KeyPublishTimes = "publish_times" // 一天发布次数大于该值,需要验证码 + KeyPublishInterval = "publish_interval" // 发布时间间隔在该值内,需要验证码,单位秒 +) + +type UserSetting struct { + Id int `xorm:"pk autoincr"` + Key string + Value int + CreatedAt time.Time `xorm:"created"` +} diff --git a/internal/model/view_record.go b/internal/model/view_record.go new file mode 100644 index 00000000..243ad5be --- /dev/null +++ b/internal/model/view_record.go @@ -0,0 +1,15 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +type ViewRecord struct { + Id int `json:"id" xorm:"pk autoincr"` + Objid int `json:"objid"` + Objtype int `json:"objtype"` + Uid int `json:"uid"` + CreatedAt OftenTime `json:"created_at" xorm:"<-"` +} diff --git a/internal/model/view_source.go b/internal/model/view_source.go new file mode 100644 index 00000000..b43b2806 --- /dev/null +++ b/internal/model/view_source.go @@ -0,0 +1,20 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +type ViewSource struct { + Id int `xorm:"pk autoincr"` + Objid int + Objtype int + Google int + Baidu int + Bing int + Sogou int + So int + Other int + UpdatedAt OftenTime `xorm:"<-"` +} diff --git a/internal/model/website_setting.go b/internal/model/website_setting.go new file mode 100644 index 00000000..03f23e4d --- /dev/null +++ b/internal/model/website_setting.go @@ -0,0 +1,159 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "xorm.io/xorm" +) + +type DocMenu struct { + Name string `json:"name"` + Url string `json:"url"` +} + +type FriendLogo struct { + Image string `json:"image"` + Url string `json:"url"` + Name string `json:"name"` + Width string `json:"width"` + Height string `json:"height"` +} + +type FooterNav struct { + Name string `json:"name"` + Url string `json:"url"` + OuterSite bool `json:"outer_site"` +} + +const ( + TabRecommend = "recommend" + TabAll = "all" +) + +type IndexNav struct { + Tab string `json:"tab"` + Name string `json:"name"` + DataSource string `json:"data_source"` + Children []*IndexNavChild `json:"children"` +} + +type IndexNavChild struct { + Uri string `json:"uri"` + Name string `json:"name"` +} + +type websiteSetting struct { + Id int `xorm:"pk autoincr"` + Name string + Domain string + OnlyHttps bool + TitleSuffix string + Favicon string + Logo string + StartYear int + BlogUrl string + ReadingMenu string + DocsMenu string + Slogan string + Beian string + FooterNav string + FriendsLogo string + ProjectDfLogo string + SeoKeywords string + SeoDescription string + IndexNav string + CreatedAt time.Time `xorm:"created"` + UpdatedAt time.Time `xorm:"<-"` + + DocMenus []*DocMenu `xorm:"-"` + FriendLogos []*FriendLogo `xorm:"-"` + FooterNavs []*FooterNav `xorm:"-"` + IndexNavs []*IndexNav `xorm:"-"` +} + +var WebsiteSetting = &websiteSetting{} + +func (self websiteSetting) TableName() string { + return "website_setting" +} + +func (this *websiteSetting) AfterSet(name string, cell xorm.Cell) { + if name == "docs_menu" { + this.DocMenus = this.unmarshalDocsMenu() + } else if name == "friends_logo" { + this.FriendLogos = this.unmarshalFriendsLogo() + } else if name == "footer_nav" { + this.FooterNavs = this.unmarshalFooterNav() + } else if name == "index_nav" { + this.IndexNavs = this.unmarshalIndexNav() + } +} + +func (this *websiteSetting) unmarshalDocsMenu() []*DocMenu { + if this.DocsMenu == "" { + return nil + } + + var docMenus = []*DocMenu{} + err := json.Unmarshal([]byte(this.DocsMenu), &docMenus) + if err != nil { + fmt.Println("unmarshal docs menu error:", err) + return nil + } + + return docMenus +} + +func (this *websiteSetting) unmarshalFriendsLogo() []*FriendLogo { + if this.FriendsLogo == "" { + return nil + } + + var friendLogos = []*FriendLogo{} + err := json.Unmarshal([]byte(this.FriendsLogo), &friendLogos) + if err != nil { + fmt.Println("unmarshal friends logo error:", err) + return nil + } + + return friendLogos +} + +func (this *websiteSetting) unmarshalFooterNav() []*FooterNav { + var footerNavs = []*FooterNav{} + err := json.Unmarshal([]byte(this.FooterNav), &footerNavs) + if err != nil { + fmt.Println("unmarshal footer nav error:", err) + return nil + } + + for _, footerNav := range footerNavs { + if strings.HasPrefix(footerNav.Url, "/") { + footerNav.OuterSite = false + } else { + footerNav.OuterSite = true + } + } + + return footerNavs +} + +func (this *websiteSetting) unmarshalIndexNav() []*IndexNav { + var indexNavs = []*IndexNav{} + err := json.Unmarshal([]byte(this.IndexNav), &indexNavs) + if err != nil { + fmt.Println("unmarshal index nav error:", err) + return nil + } + + return indexNavs +} diff --git a/internal/model/wechat.go b/internal/model/wechat.go new file mode 100644 index 00000000..4c65a605 --- /dev/null +++ b/internal/model/wechat.go @@ -0,0 +1,41 @@ +// Copyright 2018 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import ( + "time" +) + +// 微信绑定用户信息 +type WechatUser struct { + Id int `xorm:"pk autoincr"` + Openid string + Nickname string + Avatar string + SessionKey string + OpenInfo string + Uid int + CreatedAt time.Time `xorm:"created"` + UpdatedAt time.Time `xorm:"<-"` +} + +const ( + AutoReplyTypWord = iota // 关键词回复 + AutoReplyTypNotFound // 收到消息(未命中关键词且未搜索到) + AutoReplyTypSubscribe // 被关注回复 +) + +// WechatAutoReply 微信自动回复 +type WechatAutoReply struct { + Id int `xorm:"pk autoincr"` + Typ uint8 + Word string + MsgType string + Content string + CreatedAt time.Time `xorm:"created"` + UpdatedAt time.Time `xorm:"<-"` +} diff --git a/internal/model/wechat_msg.go b/internal/model/wechat_msg.go new file mode 100644 index 00000000..86fcca58 --- /dev/null +++ b/internal/model/wechat_msg.go @@ -0,0 +1,74 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "encoding/xml" + +const ( + WeMsgTypeText = "text" + WeMsgTypeImage = "image" + WeMsgTypeVoice = "voice" + WeMsgTypeVideo = "video" + WeMsgTypeShortVideo = "shortvideo" + WeMsgTypeLocation = "location" + WeMsgTypeLink = "link" + WeMsgTypeEvent = "event" + + WeEventSubscribe = "subscribe" + WeEventUnsubscribe = "unsubscribe" +) + +type WechatMsg struct { + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + Content string + MsgId int64 + + // 图片消息 + PicUrl string + MediaId string + + // 音频消息 + Format string + + // 视频或短视频 + ThumbMediaId string + + // 地理位置消息 + Location_X float64 + Location_Y float64 + Scale int + Label string + + // 链接消息 + Title string + Description string + Url string + + // 事件 + Event string +} + +type CData struct { + Val string `xml:",cdata"` +} + +type WechatReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName *CData + FromUserName *CData + CreateTime int64 + MsgType *CData + Content *CData `xml:",omitempty"` + Image *WechatImage `xml:",omitempty"` +} + +type WechatImage struct { + MediaId *CData +} diff --git a/internal/model/wiki.go b/internal/model/wiki.go new file mode 100644 index 00000000..f649f796 --- /dev/null +++ b/internal/model/wiki.go @@ -0,0 +1,30 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package model + +import "time" + +type Wiki struct { + Id int `json:"id" xorm:"pk autoincr"` + Title string `json:"title"` + Content string `json:"content"` + Uri string `json:"uri"` + Uid int `json:"uid"` + Cuid string `json:"cuid"` + Viewnum int `json:"viewnum"` + Tags string `json:"tags"` + Ctime OftenTime `json:"ctime" xorm:"created"` + Mtime time.Time `json:"mtime" xorm:"<-"` + + Users map[int]*User `xorm:"-"` +} + +func (this *Wiki) BeforeInsert() { + if this.Tags == "" { + this.Tags = AutoTag(this.Title, this.Content, 4) + } +} diff --git a/middleware/README.md b/middleware/README.md new file mode 100644 index 00000000..592a2375 --- /dev/null +++ b/middleware/README.md @@ -0,0 +1,2 @@ +# middleware +web中间件 diff --git a/middleware/async.go b/middleware/async.go new file mode 100644 index 00000000..e9d626c3 --- /dev/null +++ b/middleware/async.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "net/http" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// EchoAsync 用于 echo 框架的异步处理中间件 +func EchoAsync() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + req := ctx.Request() + + if req.Method != "GET" { + // 是否异步执行 + async := goutils.MustBool(ctx.FormValue("async"), false) + if async { + go next(ctx) + + result := map[string]interface{}{ + "code": 0, + "msg": "ok", + "data": nil, + } + return ctx.JSON(http.StatusOK, result) + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 00000000..de6b12f2 --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "net/http" + "net/url" + + echo "github.com/labstack/echo/v4" +) + +type AuthConfig struct { + signature func(url.Values, string) string + secretKey string +} + +func NewAuthConfig(signature func(url.Values, string) string, secretKey string) *AuthConfig { + return &AuthConfig{ + signature: signature, + secretKey: secretKey, + } +} + +var DefaultAuthConfig = &AuthConfig{} + +func EchoAuth() echo.MiddlewareFunc { + return EchoAuthWithConfig(DefaultAuthConfig) +} + +// EchoAuth 用于 echo 框架的签名校验中间件 +func EchoAuthWithConfig(authConfig *AuthConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + formParams, err := ctx.FormParams() + if err != nil { + return ctx.String(http.StatusBadRequest, `400 Bad Request`) + } + sign := authConfig.signature(formParams, authConfig.secretKey) + if sign != ctx.FormValue("sign") { + return ctx.String(http.StatusBadRequest, `400 Bad Request`) + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/middleware/cache.go b/middleware/cache.go new file mode 100644 index 00000000..48f6d3e5 --- /dev/null +++ b/middleware/cache.go @@ -0,0 +1,115 @@ +package middleware + +import ( + "net/http" + "sort" + "time" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +type CacheKeyAlgorithm interface { + GenCacheKey(echo.Context) string +} + +type CacheKeyFunc func(echo.Context) string + +func (self CacheKeyFunc) GenCacheKey(ctx echo.Context) string { + return self(ctx) +} + +var CacheKeyAlgorithmMap = make(map[string]CacheKeyAlgorithm) + +var LruCache = nosql.DefaultLRUCache + +// EchoCache 用于 echo 框架的缓存中间件。支持自定义 cache 数量 +func EchoCache(cacheMaxEntryNum ...int) echo.MiddlewareFunc { + + if len(cacheMaxEntryNum) > 0 { + LruCache = nosql.NewLRUCache(cacheMaxEntryNum[0]) + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + req := ctx.Request() + + if req.Method == "GET" { + cacheKey := getCacheKey(ctx) + + if cacheKey != "" { + ctx.Set(nosql.CacheKey, cacheKey) + + value, compressor, ok := LruCache.GetAndUnCompress(cacheKey) + if ok { + cacheData, ok := compressor.(*nosql.CacheData) + if ok { + + // 1分钟更新一次 + if time.Now().Sub(cacheData.StoreTime) >= time.Minute { + // TODO:雪崩问题处理 + goto NEXT + } + + logger.Debugln("cache hit:", cacheData.StoreTime, "now:", time.Now()) + return ctx.JSONBlob(http.StatusOK, value) + } + } + } + } + + NEXT: + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +func getCacheKey(ctx echo.Context) string { + cacheKey := "" + if cacheKeyAlgorithm, ok := CacheKeyAlgorithmMap[ctx.Path()]; ok { + // nil 表示不缓存 + if cacheKeyAlgorithm != nil { + cacheKey = cacheKeyAlgorithm.GenCacheKey(ctx) + } + } else { + cacheKey = defaultCacheKeyAlgorithm(ctx) + } + + return cacheKey +} + +func defaultCacheKeyAlgorithm(ctx echo.Context) string { + filter := map[string]bool{ + "from": true, + "sign": true, + "nonce": true, + "timestamp": true, + } + form, err := ctx.FormParams() + if err != nil { + return "" + } + + var keys = make([]string, 0, len(form)) + for key := range form { + if _, ok := filter[key]; !ok { + keys = append(keys, key) + } + } + + sort.Sort(sort.StringSlice(keys)) + + buffer := goutils.NewBuffer() + for _, k := range keys { + buffer.Append(k).Append("=").Append(ctx.FormValue(k)) + } + + req := ctx.Request() + return goutils.Md5(req.Method + req.URL.Path + buffer.String()) +} diff --git a/middleware/logger.go b/middleware/logger.go new file mode 100644 index 00000000..b6448794 --- /dev/null +++ b/middleware/logger.go @@ -0,0 +1,95 @@ +package middleware + +import ( + "context" + "fmt" + "time" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/logger" + "github.com/twinj/uuid" +) + +const HeaderKey = "X-Request-Id" + +type LoggerConfig struct { + // 是否输出 POST 参数,默认不输出 + OutputPost bool + // 当 OutputPost 为 true 时,排除这些 path,避免包含敏感信息输出 + Excludes map[string]struct{} +} + +var DefaultLoggerConfig = &LoggerConfig{} + +func EchoLogger() echo.MiddlewareFunc { + return EchoLoggerWitchConfig(DefaultLoggerConfig) +} + +// EchoLoggerWitchConfig 用于 echo 框架的日志中间件 +func EchoLoggerWitchConfig(loggerConfig *LoggerConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + start := time.Now() + + req := ctx.Request() + resp := ctx.Response() + + objLogger := logger.GetLogger() + ctx.Set("logger", objLogger) + + var params map[string][]string + if loggerConfig.OutputPost { + params, _ = ctx.FormParams() + if len(loggerConfig.Excludes) > 0 { + _, ok := loggerConfig.Excludes[req.URL.Path] + if ok { + params = ctx.QueryParams() + } + } + } else { + params = ctx.QueryParams() + } + objLogger.Infoln("request params:", params) + + remoteAddr := ctx.RealIP() + + id := func(ctx echo.Context) string { + id := req.Header.Get(HeaderKey) + if id == "" { + id = ctx.FormValue("request_id") + if id == "" { + id = uuid.NewV4().String() + } + } + + ctx.Set("request_id", id) + + return id + }(ctx) + + resp.Header().Set(HeaderKey, id) + + defer func() { + method := req.Method + path := req.URL.Path + if path == "" { + path = "/" + } + size := resp.Size + code := resp.Status + + stop := time.Now() + // [remoteAddr method path request_id "UA" code time size] + uri := fmt.Sprintf(`[%s %s %s %s "%s" %d %s %d]`, remoteAddr, method, path, id, req.UserAgent(), code, stop.Sub(start), size) + objLogger.SetContext(context.WithValue(context.Background(), "uri", uri)) + objLogger.Flush() + logger.PutLogger(objLogger) + }() + + if err := next(ctx); err != nil { + return err + } + return nil + } + } +} diff --git a/middleware/stats.go b/middleware/stats.go new file mode 100644 index 00000000..54118cf0 --- /dev/null +++ b/middleware/stats.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "net/http" + "strconv" + "sync" + "time" + + echo "github.com/labstack/echo/v4" +) + +type Stats struct { + Uptime time.Time `json:"uptime"` + RequestCount uint64 `json:"request_count"` + Statuses map[string]int `json:"statuses"` + mutex sync.RWMutex +} + +func NewStats() *Stats { + return &Stats{ + Uptime: time.Now(), + Statuses: make(map[string]int), + } +} + +func (s *Stats) Process() echo.MiddlewareFunc { + return s.process +} + +// Process is the middleware function. +func (s *Stats) process(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + defer func() { + s.mutex.Lock() + defer s.mutex.Unlock() + s.RequestCount++ + status := strconv.Itoa(ctx.Response().Status) + s.Statuses[status]++ + }() + + if err := next(ctx); err != nil { + return err + } + + return nil + } +} + +// Handle is the endpoint to get stats. +func (s *Stats) Handle(c echo.Context) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + return c.JSON(http.StatusOK, s) +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..daa8cd84 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "studygolang", + "version": "1.0.0", + "description": "studygolang =========== [](https://travis-ci.org/studygolang/studygolang)", + "main": "gulpfile.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/studygolang/studygolang.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/studygolang/studygolang/issues" + }, + "homepage": "https://github.com/studygolang/studygolang#readme", + "devDependencies": { + "gulp": "^3.9.1", + "gulp-concat": "^2.6.1", + "gulp-jshint": "^2.1.0", + "gulp-minify-css": "^1.2.4", + "gulp-notify": "^3.2.0", + "gulp-rename": "^1.4.0", + "gulp-rev-ayou": "^1.0.1", + "gulp-rev-collector-ayou": "^1.0.0", + "gulp-uglify": "^3.0.2", + "jshint": "^2.10.2" + } +} diff --git a/reload.bat b/reload.bat new file mode 100644 index 00000000..dfc25eaf --- /dev/null +++ b/reload.bat @@ -0,0 +1,20 @@ +@echo off + +setlocal + +if exist reload.bat goto ok +echo reload.bat must be run from its folder +goto end + +:ok + +:: stop +taskkill /im studygolang.exe /f +del /q /f /a pid + +:: start +start /b bin\studygolang >> log\panic.log 2>&1 & + +echo reload successfully + +:end \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..645fb26b --- /dev/null +++ b/robots.txt @@ -0,0 +1,7 @@ +User-agent: * +Allow: / +Sitemap: +Disallow:/dl/golang/ +Disallow:/search +Disallow:/wr +Disallow:/ws diff --git a/sg.service b/sg.service new file mode 100644 index 00000000..fcb57bcd --- /dev/null +++ b/sg.service @@ -0,0 +1,13 @@ +[Unit] +Description=studygolang + +[Service] +ExecStart=/data/www/studygolang/bin/studygolang +ExecReload=/bin/kill -USR2 $MAINPID +PIDFile=/data/www/studygolang/pid/studygolang.pid +Restart=always +User=xuxinhua +Group=xuxinhua + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/start-docker.sh b/start-docker.sh new file mode 100755 index 00000000..ad95de4f --- /dev/null +++ b/start-docker.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# *************************************************************************** +# * +# * @author:jockerxu +# * @date:2017-11-14 22:20 +# * @version 1.0 +# * @description: Shell script +#* +#**************************************************************************/ + +#---------tool function--------------- +echo_COLOR_GREEN=$( echo -e "\e[32;49m") +echo_COLOR_RESET=$( echo -e "\e[0m") +function echo-info() +{ + echo -e "${echo_COLOR_GREEN}[$(date "+%F %T")]\t$*${echo_COLOR_RESET}"; +} +#---------end tool function----------- +if [[ $USER != "root" ]]; then + echo "you must be root!!!!!" + exit 1 +fi + +if [[ $1 == "" ]]; then + echo "Usage start-docker.sh [local | remote]" + exit 1 +fi + +STUDYGOLANG_IMG= + +if [[ $1 == "local" ]]; then + STUDYGOLANG_IMG=studygolang + docker images ${STUDYGOLANG_IMG} | grep -q ${STUDYGOLANG_IMG} || { + docker build -f Dockerfile.web -t $STUDYGOLANG_IMG . + } +elif [[ $1 == "remote" ]]; then + STUDYGOLANG_IMG="jockerxu/studygolang" +else + exit 1 +fi + +docker ps -a | grep -q mysqlDB || { + docker run --name mysqlDB -e MYSQL_ROOT_PASSWORD=123456 -d mysql +} +docker ps -a | grep -q studygolang-web && { + docker rm -f studygolang-web +} +docker run -d --name studygolang-web -v `pwd`:/studygolang -p 8090:8088 --link mysqlDB:db.localhost $STUDYGOLANG_IMG ./docker-entrypoint.sh + +if [[ $? == 0 ]]; then + echo-info "studygolang-web start, waiting several seconds to install..." + sleep 5 + echo-info "open browser: http://localhost:8090" + echo-info "mysql-host is: db.localhost " + echo-info "mysql-password is: 123456" +fi diff --git a/websites/code/studygolang/start.bat b/start.bat similarity index 56% rename from websites/code/studygolang/start.bat rename to start.bat index dcd7ea4d..2487b232 100644 --- a/websites/code/studygolang/start.bat +++ b/start.bat @@ -8,7 +8,8 @@ goto end :ok -START /b bin\studygolang >> log\panic.log 2>&1 & +start /b bin\studygolang.exe >> log\panic.log 2>&1 & -:end -echo finished \ No newline at end of file +echo start successfully + +:end \ No newline at end of file diff --git a/static/.gitignore b/static/.gitignore new file mode 100644 index 00000000..5f23dcbd --- /dev/null +++ b/static/.gitignore @@ -0,0 +1 @@ +pkgdoc \ No newline at end of file diff --git a/static/ckeditor/article.js b/static/ckeditor/article.js new file mode 100644 index 00000000..8c3f31ae --- /dev/null +++ b/static/ckeditor/article.js @@ -0,0 +1,75 @@ +$(function(){ + CKEDITOR.plugins.addExternal('prism', '/static/ckeditor/plugins/prism/', 'plugin.js'); + $('#edit').on('click', function(){ + var txt = $(this).text(); + if (txt == '编辑') { + $('#myeditor').attr('contenteditable', true); + $('#myeditor').html($('#content_tpl').html()); + if (!CKEDITOR.instances.myeditor) { + MyEditorConfig.extraPlugins = MyEditorExtraPlugins+',prism,sourcedialog'; + MyEditorConfig.toolbarGroups = [ + { name: 'undo' }, + { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }, + { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align' ] }, + { name: 'links' }, + { name: 'insert' }, + { name: 'styles' }, + { name: 'document', groups: [ 'mode', 'document' ] } + ]; + MyEditorConfig.removeButtons = 'Anchor,SpecialChar,HorizontalRule,Table,Styles,Subscript,Superscript'; + CKEDITOR.inline( 'myeditor', MyEditorConfig ); + } + + $(this).text('完成'); + } else { + if (CKEDITOR.instances.myeditor) { + var content = CKEDITOR.instances.myeditor.getData(); + modify(content); + + CKEDITOR.instances.myeditor.destroy(); + + Prism.highlightAll(); + } + + $('#myeditor').attr('contenteditable', false); + $(this).text('编辑'); + } + }); + + CKEDITOR.on('instanceReady', function(evt, editor) { + $('#myeditor').find('.cke_widget_element').each(function(){ + $(this).addClass('line-numbers').css('background-color', '#000'); + }); + }); + + function modify(content) + { + var url = '/articles/modify', + data = { id: $('#title').data('id'), content:content }; + + $.ajax({ + type: "post", + url: url, + data: data, + dataType: 'json', + success: function(data){ + if(data.ok){ + if (typeof data.msg != "undefined") { + comTip(data.msg); + } else { + comTip("修改成功!"); + } + }else{ + comTip(data.error); + } + }, + complete:function(xmlReq, textStatus){ + }, + error:function(xmlReq, textStatus, errorThrown){ + if (xmlReq.status == 403) { + comTip("没有修改权限"); + } + } + }); + } +}); \ No newline at end of file diff --git a/static/ckeditor/config.js b/static/ckeditor/config.js new file mode 100644 index 00000000..de9c4860 --- /dev/null +++ b/static/ckeditor/config.js @@ -0,0 +1,66 @@ +var MyEditorExtraPlugins = 'codesnippet,image2,uploadimage,notification,widget,lineutils,justify,autolink'; + +var MyEditorConfig = { + title: 'Go语言中文网富文本编辑器', + // Define the toolbar: http://docs.ckeditor.com/#!/guide/dev_toolbar + // The standard preset from CDN which we used as a base provides more features than we need. + // Also by default it comes with a 2-line toolbar. Here we put all buttons in a single row. + // toolbar: [ + // { name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'RemoveFormat' ] }, + // { name: 'paragraph', items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight' ] }, + // { name: 'clipboard', items: [ 'Undo', 'Redo' ] }, + // { name: 'links', items: [ 'Link', 'Unlink' ] }, + // { name: 'insert', items: [ 'CodeSnippet', 'Image' ] }, + // { name: 'styles', items: [ 'Format' ] }, + // { name: 'document', items: [ 'Source', 'Preview' ] }, + // { name: 'tools', items: [ 'Maximize' ] } + // ], + startupFocus: true, + // Since we define all configuration options here, let's instruct CKEditor to not load config.js which it does by default. + // One HTTP request less will result in a faster startup time. + // For more information check http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-customConfig + customConfig: '', + // Enabling extra plugins, available in the standard-all preset: http://ckeditor.com/presets-all + // extraPlugins: 'sourcedialog,preview,codesnippet,image2,uploadimage,notification,prism,widget,lineutils,justify,autolink', + // Remove the default image plugin because image2, which offers captions for images, was enabled above. + removePlugins: 'image', + filebrowserImageUploadUrl: '/image/quick_upload?command=QuickUpload&type=Images', + + // See http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-codeSnippet_theme + codeSnippet_theme: 'monokai_sublime',//'ir_black', + codeSnippet_languages: { + go: 'Go', + php: 'PHP', + bash: 'Bash', + cpp: 'C/C++', + json: 'JSON', + html: 'HTML', + http: 'HTTP', + ini: 'INI', + java: 'Java', + javascript: 'JavaScript', + markdown: 'Markdown', + nginx: 'Nginx', + sql: 'SQL', + yaml: 'YAML', + armasm: 'ARM Assembly' + }, + /*********************** File management support ***********************/ + // In order to turn on support for file uploads, CKEditor has to be configured to use some server side + // solution with file upload/management capabilities, like for example CKFinder. + // For more information see http://docs.ckeditor.com/#!/guide/dev_ckfinder_integration + // Uncomment and correct these lines after you setup your local CKFinder instance. + // filebrowserBrowseUrl: 'http://example.com/ckfinder/ckfinder.html', + // filebrowserUploadUrl: 'http://example.com/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files', + /*********************** File management support ***********************/ + // Make the editing area bigger than default. + height: 361, + width: '98%', + // An array of stylesheets to style the WYSIWYG area. + // Note: it is recommended to keep your own styles in a separate file in order to make future updates painless. + // contentsCss: [ 'https://cdn.ckeditor.com/4.6.2/standard-all/contents.css', 'mystyles.css' ], + // Reduce the list of block elements listed in the Format dropdown to the most commonly used. + format_tags: 'p;h1;h2;h3;h4;pre', + // Simplify the Image and Link dialog windows. The "Advanced" tab is not needed in most cases. + removeDialogTabs: 'image:advanced;link:advanced;link:target' +} \ No newline at end of file diff --git a/static/ckeditor/plugins/autosave/css/autosave.min.css b/static/ckeditor/plugins/autosave/css/autosave.min.css new file mode 100755 index 00000000..b0efbb73 --- /dev/null +++ b/static/ckeditor/plugins/autosave/css/autosave.min.css @@ -0,0 +1 @@ +.diffContent{height:300px;overflow:auto}.diff *{white-space:pre-wrap!important}table.diff{border-collapse:collapse;border:1px solid #a9a9a9}table.diff tbody{font-family:Courier,monospace}table.diff tbody th{font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;background:#eed;font-size:11px;font-weight:normal;border:1px solid #bbc;color:#886;padding:.3em .5em .1em 2em;text-align:right;vertical-align:top}table.diff thead{border-bottom:1px solid #bbc;background:#efefef;font-family:Verdana}table.diff thead th.texttitle{text-align:left}table.diff tbody td{padding:0 .4em;vertical-align:top}table.diff .empty{background-color:#ddd}table.diff .replace{background-color:#ffc}table.diff .delete{background-color:#fcc;width:380px;-ms-word-break:break-word;word-break:break-word}table.diff .skip{background-color:#efefef;border:1px solid #aaa;border-right:1px solid #bbc}table.diff .insert{background-color:#cfc;width:380px;-ms-word-break:break-word;word-break:break-word}table.diff th.author{text-align:right;border-top:1px solid #bbc;background:#efefef}del{background-color:#e99 !important;text-decoration:underline !important}ins{background-color:#9e9 !important;text-decoration:underline !important}a.cke_dialog_autosave_ok span{width:auto!important}div.autoSaveMessage div{left:42%;position:absolute;padding:2px;top:4px;font-weight:bold}.hidden{opacity:0;visibility:hidden}.show{opacity:1;visibility:visible;-moz-transition:visibility .2s linear,opacity .2s linear;-o-transition:visibility .2s linear,opacity .2s linear;-webkit-transition:visibility .2s linear,opacity .2s linear;transition:visibility .2s linear,opacity .2s linear} \ No newline at end of file diff --git a/static/ckeditor/plugins/autosave/js/extensions.min.js b/static/ckeditor/plugins/autosave/js/extensions.min.js new file mode 100755 index 00000000..7d8bc563 --- /dev/null +++ b/static/ckeditor/plugins/autosave/js/extensions.min.js @@ -0,0 +1,7 @@ +function escape_jsdiff(n){var t=n;return t=t.replace(/&/g,"&"),t=t.replace(//g,">"),t.replace(/"/g,""")}function diffString(n,t){var f,r,o;n=n.replace(/\s+$/,"");t=t.replace(/\s+$/,"");var i=diff(n==""?[]:n.split(/\s+/),t==""?[]:t.split(/\s+/)),e="",u=n.match(/\s+/g);if(u==null?u=["\n"]:u.push("\n"),f=t.match(/\s+/g),f==null?f=["\n"]:f.push("\n"),i.n.length==0)for(r=0;r
jsdifflib<\/a> ";d+="and John Resig's diff<\/a> ";d+="by Richard Bondi<\/a>";h.push(t=e("th","author",d));t.setAttribute("colspan",a?3:4);o.push(t=document.createElement("tbody"));for(i in h)h.hasOwnProperty(i)&&t.appendChild(h[i]);t=ut("table","diff"+(a?" inlinediff":""));for(i in o)o.hasOwnProperty(i)&&t.appendChild(o[i]);return t}};
+//! moment.js
+//! version : 2.15.2
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+(function(n,t){typeof exports=="object"&&typeof module!="undefined"?module.exports=t():typeof define=="function"&&define.amd?define(t):n.moment=t()})(this,function(){"use strict";function i(){return lo.apply(null,arguments)}function pl(n){lo=n}function ei(n){return n instanceof Array||Object.prototype.toString.call(n)==="[object Array]"}function ou(n){return n!=null&&Object.prototype.toString.call(n)==="[object Object]"}function wl(n){for(var t in n)return!1;return!0}function su(n){return n instanceof Date||Object.prototype.toString.call(n)==="[object Date]"}function ao(n,t){for(var r=[],i=0;i
+ This is a XML Sitemap which is supposed to be processed by search engines like Google, Baidu, MSN Search and YAHOO.>1}c--;c==0&&(c=Math.pow(2,s),s++);delete v[o]}else for(f=l[o],e=0;e>1;c--;c==0&&(c=Math.pow(2,s),s++);l[y]=w++;o=String(a)}if(o!==""){if(Object.prototype.hasOwnProperty.call(v,o)){if(o.charCodeAt(0)<256){for(e=0;e>1}else{for(f=1,e=0;e>1}c--;c==0&&(c=Math.pow(2,s),s++);delete v[o]}else for(f=l[o],e=0;e>1;c--;c==0&&(c=Math.pow(2,s),s++)}for(f=2,e=0;e>1;for(;;)if(r=r<<1,u==t-1){h.push(i(r));break}else u++;return h.join("")},decompress:function(t){return t==null?"":t==""?null:n._decompress(t.length,32768,function(n){return t.charCodeAt(n)})},_decompress:function(n,i,r){for(var c=[],k,l=4,a=4,v=3,y="",b=[],w,e,o,s,f,h,u={val:r(0),position:i,index:1},p=0;p<3;p+=1)c[p]=p;for(e=0,s=Math.pow(2,2),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;switch(k=e){case 0:for(e=0,s=Math.pow(2,8),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;h=t(e);break;case 1:for(e=0,s=Math.pow(2,16),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;h=t(e);break;case 2:return""}for(c[3]=h,w=h,b.push(h);;){if(u.index>n)return"";for(e=0,s=Math.pow(2,v),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;switch(h=e){case 0:for(e=0,s=Math.pow(2,8),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;c[a++]=t(e);h=a-1;l--;break;case 1:for(e=0,s=Math.pow(2,16),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;c[a++]=t(e);h=a-1;l--;break;case 2:return b.join("")}if(l==0&&(l=Math.pow(2,v),v++),c[h])y=c[h];else if(h===a)y=w+w.charAt(0);else return null;b.push(y);c[a++]=w+y.charAt(0);l--;w=y;l==0&&(l=Math.pow(2,v),v++)}}};return n}();typeof define=="function"&&define.amd?define(function(){return LZString}):typeof module!="undefined"&&module!=null&&(module.exports=LZString);
\ No newline at end of file
diff --git a/static/ckeditor/plugins/autosave/lang/en.js b/static/ckeditor/plugins/autosave/lang/en.js
new file mode 100755
index 00000000..9472a0c4
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/lang/en.js
@@ -0,0 +1,18 @@
+/*
+Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.plugins.setLang('autosave', 'en', {
+ dateFormat: 'LLL',
+ autoSaveMessage: 'Auto Saved',
+ loadSavedContent: 'An auto-saved version of this content from "{0}" has been found. Would you like to compare content versions and choose which one to load? Clicking Cancel will remove previously auto-saved content.',
+ title: 'Compare auto-saved content with that loaded from the website',
+ loadedContent: 'Loaded content',
+ localStorageFull: 'Browser localStorage is full, clear your storage or Increase database size',
+ autoSavedContent: 'Auto-saved content from: \'',
+ ok: 'Yes, load auto-saved content',
+ no: 'No',
+ diffType: 'Choose view type:',
+ sideBySide: 'Side by side view',
+ inline: 'Inline view'
+});
diff --git a/static/ckeditor/plugins/autosave/lang/zh-cn.js b/static/ckeditor/plugins/autosave/lang/zh-cn.js
new file mode 100755
index 00000000..a19eeb86
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/lang/zh-cn.js
@@ -0,0 +1,18 @@
+/*
+Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.plugins.setLang('autosave', 'zh-cn', {
+ dateFormat: 'LLL',
+ autoSaveMessage: '自动保存',
+ loadSavedContent: '已经存在这个内容之前保存的版本 "{0}". 你希望比较两个不同的版本并从中选择你想要加载的那个么?',
+ title: '比较自动保存的版本',
+ loadedContent: '已经加载的内容',
+ localStorageFull: 'Browser localStorage is full, clear your storage or Increase database size',
+ autoSavedContent: '自动保存的内容: \'',
+ ok: '我确认,加载自动保存的内容',
+ no: '取消',
+ diffType: '选择视图类型:',
+ sideBySide: '并排视图',
+ inline: '内嵌视图'
+});
diff --git a/static/ckeditor/plugins/autosave/lang/zh.js b/static/ckeditor/plugins/autosave/lang/zh.js
new file mode 100755
index 00000000..143450ce
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/lang/zh.js
@@ -0,0 +1,17 @@
+/*
+Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.plugins.setLang('autosave', 'zh', {
+ dateFormat: 'LLL',
+ autoSaveMessage: '自動儲存 :)',
+ loadSavedContent: '你在 “{0}” 有一個自動儲存的版本。你想要比較新舊內容,並選擇使用哪一個嗎?點取消按鈕將會移除先前的自動儲存紀錄。',
+ title: '新舊內容比較',
+ loadedContent: '目前內容',
+ autoSavedContent: '自動儲存的內容: \'',
+ ok: '是,我要讀取自動儲存的內容',
+ no: '否',
+ diffType: '選擇比較方式:',
+ sideBySide: '雙欄式',
+ inline: '條列式'
+});
diff --git a/static/ckeditor/plugins/autosave/plugin.js b/static/ckeditor/plugins/autosave/plugin.js
new file mode 100755
index 00000000..86f0c45c
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/plugin.js
@@ -0,0 +1,334 @@
+/**
+ * @license Copyright (c) CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.html or http://ckeditor.com/license
+ */
+
+(function() {
+ if (!supportsLocalStorage()) {
+ CKEDITOR.plugins.add("autosave", {}); //register a dummy plugin to pass CKEditor plugin initialization process
+ return;
+ }
+
+ CKEDITOR.plugins.add("autosave", {
+ lang: 'ca,cs,de,en,es,fr,ja,nl,pl,pt-br,ru,sk,sv,zh,zh-cn', // %REMOVE_LINE_CORE%
+ requires: 'notification',
+ version: 0.17,
+ init: function (editor) {
+ // Default Config
+ var defaultConfig = {
+ delay: 10,
+ messageType: "notification",
+ saveDetectionSelectors: "a[href^='javascript:__doPostBack'][id*='Save'],a[id*='Cancel']",
+ saveOnDestroy: false,
+ NotOlderThen: 1440,
+ SaveKey: "autosaveKey",
+ diffType: "sideBySide"
+ };
+
+ // Get Config & Lang
+ var config = CKEDITOR.tools.extend(defaultConfig, editor.config.autosave || {}, true);
+
+ if (editor.plugins.textselection && config.messageType == "statusbar") {
+ config.messageType = "notification";
+ }
+
+ CKEDITOR.document.appendStyleSheet(CKEDITOR.getUrl(CKEDITOR.plugins.getPath('autosave') + 'css/autosave.min.css'));
+
+ editor.on('uiSpace', function(event) {
+ if (event.data.space == 'bottom' && config.messageType != null && config.messageType == "statusbar") {
+
+ event.data.html += '
tags/blocks
+ in your webpages. The ...
is one of the main basis for the styling/syntax highlighting.
+ Yet, for some systems like Drupal,
tag is blacklisted by default
+ when filtering/rendering page contents (actually
will become
when rendered).
+ In Drupal,
is allowed by default. Hence, you must allow/whitelist
tag in order for the syntax highlighter to work as intended. In Drupal, you should add the "
"" tag
+ as one of the whitelisted tags in the
+ 'Allowed HTML tags' section in 'Limit allowed HTML tags' filter/tab in
+ "admin/config/content/formats/filtered_html" page.
+
+ B. VIEWING THE PAGE
+
+ This syntax highlighter utilizes the Prism JS/CSS files in order to work as intended.
+
+ Typically, there are 2 view modes for every page in CMS: CKEditor Mode and Page Mode.
+ In CKEditor Mode (when editing the page), the Prism Highlighter handles the auto-loading
+ of Prism JS and CSS files. In Page Mode (when rendering/viewing the actual page),
+ Prism Highlighiter is not loaded anymore (since it depends on CKEditor.)
+
+ In effect, you have to load the Prism JS and CSS files when displaying the page.
+ You should add these link and script tags in your HTML :
+
+ ...
+
+
+
+
+
+ For example, in my personal site I have these values:
+
+ ...
+
+
+
+
+
+ Note that even if you're not using Prism Syntax Highlighter, and just using
+ the Code Snippet plugin, you'll have to do the same steps:
+ http://docs.ckeditor.com/#!/guide/dev_codesnippet.
+
+ In Drupal, you could easily add the script using JS Injector module
+ https://www.drupal.org/project/js_injector
+
+ and for adding the stylesheet, you could use the CSS Injector module
+ https://www.drupal.org/project/css_injector
+
+ If you're a Drupal developer, you could do the JS/CSS insertions using the
+ drupal_add_js() and drupal_add_css() functions in a custom module.
diff --git a/static/ckeditor/plugins/prism/LICENSE.txt b/static/ckeditor/plugins/prism/LICENSE.txt
new file mode 100644
index 00000000..4362b491
--- /dev/null
+++ b/static/ckeditor/plugins/prism/LICENSE.txt
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+Prism Highlighter is a plugin for CKEditor for inserting beautiful formatted text, markdown, or code snippets in your blog or website. Prism is the chosen renderer/highlighter/colorizer since it's pretty, lightweight, and extendable. I also use this plugin in my [personal website](http://www.ranelpadon.com/content/practical-regex-part-12-common-operators).
+
+This plugin utilizes the following libraries:
+
+
+
+By default, Prism has no line numbering mechanism, so the **Line Number** add-on has been added. Also, in order for the line numbers to work smoothly in CKEditor and when rendering the actual page, I did some minor patching in **prism.js** and **prism.css**.
+
+LIVE DEMO PAGE:
+Demo page could be found here.
+
+INSTALLATION:
+Kindly refer to Installation Guide.
+
+HOW TO USE:
+Kindly refer to How to Create and Edit Prism Snippets.
+
+LICENSE and CREDITS:
+License: LGPLv2.1 or later should apply. Note that LGPLv2.1+ is also compatible with GPLv2.
+Copyright 2015 by [Engr. Ranel O. Padon](http://www.ranelpadon.com)
+
+Thanks to CKEditor, Prism, and other great open-source softwares and their unsung developers.
+
+=======================================================
+
+
+
+
+
+
+
diff --git a/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.css b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.css
new file mode 100644
index 00000000..a8a001fe
--- /dev/null
+++ b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.css
@@ -0,0 +1 @@
+code[class*="language-"],pre{color:white;text-shadow:0 -.1em .2em black;font-family:Monaco,Consolas,'Andale Mono','Ubuntu Mono',monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*="language-"],pre{text-shadow:none}}pre,:not(pre)>code[class*="language-"]{background:hsl(30,20%,25%)}pre{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid hsl(30,20%,40%);border-radius:.5em;box-shadow:1px 1px .5em black inset}:not(pre)>code[class*="language-"]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid hsl(30,20%,40%);box-shadow:1px 1px .3em -.1em black inset;white-space:normal}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:hsl(30,20%,50%)}.token.punctuation{opacity:.7}.namespace{opacity:.7}.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol{color:hsl(350,40%,70%)}.token.selector,.token.attr-name,.token.string,.token.char,.token.builtin,.token.inserted{color:hsl(75,70%,60%)}.token.operator,.token.entity,.token.url,.language-css .token.string,.style .token.string,.token.variable{color:hsl(40,90%,60%)}.token.atrule,.token.attr-value,.token.keyword{color:hsl(350,40%,70%)}.token.regex,.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red}pre{position:relative;padding-left:3.8em;counter-reset:linenumber}pre>code{position:relative}pre>code .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{pointer-events:none;display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
diff --git a/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.js b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.js
new file mode 100644
index 00000000..c81aa615
--- /dev/null
+++ b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.js
@@ -0,0 +1,4 @@
+var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=_self.Prism={util:{encode:function(e){return e instanceof i?new i(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(f instanceof a)){d.lastIndex=0;var b=d.exec(f);if(b){u&&(m=b[1].length);var h=b.index-1+m,b=b[0].slice(m),S=b.length,T=h+S,A=f.slice(0,h+1),N=f.slice(T+1),I=[g,1];A&&I.push(A);var L=new a(o,p?t.tokenize(b,p):b,E);I.push(L),N&&I.push(N),Array.prototype.splice.apply(r,I)}}}}}return r},hooks:{all:{},add:function(e,i){var n=t.hooks.all;n[e]=n[e]||[],n[e].push(i)},run:function(e,i){var n=t.hooks.all[e];if(n&&n.length)for(var a,r=0;a=n[r++];)a(i)}}},i=t.Token=function(e,t,i){this.type=e,this.content=t,this.alias=i};if(i.stringify=function(e,n,a){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return i.stringify(t,n,e)}).join("");var r={type:e.type,content:i.stringify(e.content,n,a),tag:"span",classes:["token",e.type],attributes:{},language:n,parent:a};if("comment"==r.type&&(r.attributes.spellcheck="true"),e.alias){var s="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(r.classes,s)}t.hooks.run("wrap",r);var o="";for(var l in r.attributes)o+=(o?" ":"")+l+'="'+(r.attributes[l]||"")+'"';return"<"+r.tag+' class="'+r.classes.join(" ")+'" '+o+">"+r.content+""+r.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var i=JSON.parse(e.data),n=i.language,a=i.code,r=i.immediateClose;_self.postMessage(t.highlight(a,t.languages[n],n)),r&&_self.close()},!1),_self.Prism):_self.Prism;var n=document.getElementsByTagName("script");return n=n[n.length-1],n&&(t.filename=n.src,document.addEventListener&&!n.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=.$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/(
+
+
+ XML Sitemap
+
+
+ You can find more information about XML sitemaps on sitemaps.org and Google's list of sitemap programs.
+
+
+
+
+ URL
+ Priority
+ Change Frequency
+ LastChange (GMT)
+
+
+
+
+
+
+
+
+
+
+