仿知乎网页版(使用Go开发)
config/config.go
package config
import (
"encoding/json"
"fmt"
"log"
"os"
)
type config struct {
Server serverConfig
Database databaseConfig
Redis redisConfig
}
type serverConfig struct {
Addr string `json:"addr"`
SessionSecret string `json:"session_secret"`
SessionKey string `json:"session_key"`
Salt string `json:"salt"`
}
type databaseConfig struct {
DriverName string `json:"driver_name"`
User string `json:"user"`
Password string `json:"password"`
Host string `json:"host"`
Port string `json:"port"`
Database string `json:"database"`
DSN string
}
type redisConfig struct {
Host string
Port string
Password string
MaxIdle int `json:"max_idle"`
Addr string
}
var (
Config config
Server *serverConfig
Database *databaseConfig
Redis *redisConfig
)
func initJson() {
data, err := os.ReadFile("config/config.json")
if err != nil {
log.Println(err)
return
}
if err := json.Unmarshal(data, &Config); err != nil {
log.Fatal(err)
}
}
func initServer() {
Server = &Config.Server
}
func initDatabase() {
Database = &Config.Database
switch Database.DriverName {
case "mysql":
Database.DSN = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s",
Database.User,
Database.Password,
Database.Host,
Database.Port,
Database.Database,
)
}
}
func initRedis() {
Redis = &Config.Redis
Redis.Addr = Redis.Host + ":" + Redis.Port
}
func init() {
initJson()
initServer()
initDatabase()
initRedis()
}
config/config.json
{
"server": {
"addr": ":8080",
"session_key": "user_id",
"session_secret": "zhihu",
"salt": "^&*(kk)"
},
"database": {
"driver_name": "mysql",
"user": "root",
"password": "你猜",
"host": "localhost",
"port": "3306",
"database": "zhihu"
},
"redis": {
"host": "127.0.0.1",
"max_idle": 3,
"password": "",
"port": "6379"
}
}
main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"zhihu/config"
"zhihu/router"
)
func main() {
app := gin.Default()
Init(app)
_ = app.Run(config.Server.Addr)
}
func Init(engine *gin.Engine) {
setSession(engine)
router.Route(engine)
}
func setSession(engine *gin.Engine) {
store := sessions.NewCookieStore([]byte(config.Server.SessionSecret))
store.Options = &sessions.Options{HttpOnly: true, MaxAge: 7 * 86400, Path: "/"}
}
router/router.go
package router
import (
"github.com/gin-gonic/gin"
"zhihu/controllers"
"zhihu/middleware"
)
func Route(router *gin.Engine) {
router.LoadHTMLGlob("views/**/*")
router.Static("/static", "./static")
router.GET("/foo", handleFoo)
router.NoRoute(controllers.Handle404)
router.GET("/", controllers.IndexGet)
router.GET("/signup", controllers.SignupGet)
router.POST("/signup", controllers.SignupPost)
router.GET("/signin", controllers.SigninGet)
router.POST("/signin", controllers.SigninPost)
router.GET("/logout", controllers.LogoutGet)
router.GET("/question/:qid", controllers.QuestionGet)
router.GET("/question/:qid/answer/:aid", middleware.RefreshSession(), controllers.AnswerGet)
router.GET("/topic/autocomplete", controllers.SearchTopics)
api := router.Group("/api")
{
api.GET("/answers/:id/voters", controllers.AnswerVoters)
api.GET("questions/:id/comments", controllers.QuestionComments)
api.GET("questions/:id/followers", controllers.QuestionFollowers)
api.GET("/members/:url_token", controllers.MemberInfo)
api.Use(middleware.SigninRequired())
api.POST("/questions", controllers.PostQuestion)
api.POST("/answers/:id/voters", controllers.VoteAnswer)
api.POST("/questions/:id/answers", controllers.PostAnswer)
api.DELETE("/answers/:id", controllers.DeleteAnswer)
api.POST("/answers/:id/actions/restore", controllers.RestoreAnswer)
api.POST("/questions/:id/comments", controllers.PostQuestionComment)
api.DELETE("/questions/:id/comments/:cid", controllers.DeleteQuestionComment)
api.POST("/questions/:id/followers", controllers.FollowQuestion)
api.DELETE("/questions/:id/followers", controllers.UnfollowQuestion)
api.POST("/questions/:id/comments/:cid/actions/like", controllers.LikeQuestionComment)
api.DELETE("/questions/:id/comments/:cid/actions/like", controllers.UndoLikeQuestionComment)
api.POST("/members/:url_token/followers", controllers.FollowMember)
api.DELETE("/members/:url_token/followers", controllers.UnfollowMember)
api.POST("/topics", controllers.PostTopic)
}
}
func handleFoo(c *gin.Context) {
c.HTML(200, "foo.html", nil)
}
utils/utils.go
package utils
import (
"crypto/md5"
"fmt"
"io"
"regexp"
"time"
"zhihu/config"
)
type Err struct {
Message string
Code int
}
func (err *Err) Error() string {
return err.Message
}
const (
ErrAccountNotFound = 100000 + iota
ErrIncorrectPassword
ErrDuplicatedEmail
ErrBadFullNameFormat
ErrBadEmailFormat
ErrBadPasswordFormat
)
func ValidateFullName(fullName string) *Err {
reg := regexp.MustCompile(`^[\p{Han}\w]+([\p{Han}\w\s.-]*)$`)
println(fullName)
if !reg.MatchString(fullName) {
err := &Err{
Message: "名字中含有特殊字符",
Code: ErrBadFullNameFormat,
}
return err
}
return nil
}
func ValidateUsername(username string) *Err {
reg := regexp.MustCompile(`^[a-zA-Z0-9]+@(\w+).(\w{2,5})$`)
if !reg.MatchString(username) {
err := &Err{
Message: "请输入正确的邮箱",
Code: ErrBadEmailFormat,
}
return err
}
return nil
}
func ValidatePassword(password string) *Err {
reg := regexp.MustCompile(`^(\w+[\w[:graph:]]*){6,}$`)
if !reg.MatchString(password) {
err := &Err{
Message: "密码格式不正确",
Code: ErrBadPasswordFormat,
}
return err
}
return nil
}
func FormatUnixTime(dt int64) string {
var res string
now := time.Now()
datetime := time.Unix(dt, 0)
year, month, day := datetime.Date()
nowYear, nowMonth, nowDay := now.Date()
ydaYear, ydaMonth, ydaDay := time.Unix(now.Unix()-24*60*60, 0).Date()
if nowYear == year && nowMonth == month && nowDay == day {
res = datetime.Format("15:04")
} else if ydaYear == year && ydaMonth == month && ydaDay == day {
res = "昨天 " + datetime.Format("15:04")
} else {
res = datetime.Format(time.DateOnly)
}
return res
}
const (
Minute = 60
Hour = Minute * 60
Day = Hour * 24
Month = Day * 30
Year = Month * 12
)
func FormatBeforeUnixTime(dt int64) string {
var res string
now := time.Now().Unix()
diff := now - dt
switch {
case diff < Minute:
res = "刚刚"
case diff < Hour && diff >= Minute:
res = fmt.Sprintf("%d分钟前", diff/Minute)
case diff < Day && diff >= Hour:
res = fmt.Sprintf("%d小时前", diff/Hour)
case diff < Month && diff >= Day:
res = fmt.Sprintf("%d天前", diff/Day)
case diff < Year && diff >= Month:
res = fmt.Sprintf("%d个月前", diff/Month)
case diff >= Year:
res = fmt.Sprintf("%d年前", diff/Year)
}
return res
}
func EncryptPassword(username, password string) string {
h := md5.New()
_, _ = io.WriteString(h, password)
pwdMD5 := fmt.Sprintf("%x", h.Sum(nil))
_, _ = io.WriteString(h, config.Server.Salt)
_, _ = io.WriteString(h, username)
_, _ = io.WriteString(h, pwdMD5)
return fmt.Sprintf("%x", h.Sum(nil))
}
func URLToken(urlToken *string, urlTokenCode int) {
if urlTokenCode != 0 {
*urlToken = fmt.Sprintf("%s-%d", *urlToken, urlTokenCode)
}
}
middleware/middleware.go
package middleware
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"zhihu/config"
)
func SigninRequired() gin.HandlerFunc {
return func(c *gin.Context) {
sess := sessions.Default(c)
userID := sess.Get(config.Server.SessionKey)
if userID == nil {
c.JSON(http.StatusForbidden, gin.H{
"message": "not authorized",
})
c.Abort()
return
}
c.Next()
}
}
func RefreshSession() gin.HandlerFunc {
return func(c *gin.Context) {
sess := sessions.Default(c)
userID := sess.Get(config.Server.SessionKey)
sess.Clear()
if userID != nil {
sess.Set(config.Server.SessionKey, userID.(uint))
}
_ = sess.Save()
c.Next()
}
}
controllers/404.go
package controllers
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Handle404(c *gin.Context) {
c.HTML(http.StatusNotFound, "404.html", nil)
}
controllers/answer.go
package controllers
import (
"encoding/json"
"github.com/gin-gonic/gin"
"log"
"net/http"
"zhihu/models"
)
func PostAnswer(c *gin.Context) {
qid := c.Param("id")
if qid == "" {
c.JSON(http.StatusNotFound, nil)
log.Println("controllers.PostAnswer(): no question id")
return
}
uid := VisitorID(c)
content := struct {
Content string `json:"content"`
}{}
decoder := json.NewDecoder(c.Request.Body)
if err := decoder.Decode(&content); err != nil {
log.Println("controller.PostAnswer(): ", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
aid, err := models.InsertAnswer(qid, content.Content, uid)
if err != nil {
c.JSON(http.StatusInternalServerError, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"id": aid,
})
}
func DeleteAnswer(c *gin.Context) {
aid := c.Param("id")
if aid == "" {
c.JSON(http.StatusNotFound, nil)
log.Println("controllers.DeleteAnswer(): no answer id")
return
}
uid := VisitorID(c)
err := models.DeleteAnswer(aid, uid)
if err != nil {
c.JSON(http.StatusInternalServerError, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}
func RestoreAnswer(c *gin.Context) {
aid := c.Param("id")
if aid == "" {
c.JSON(http.StatusNotFound, nil)
log.Println("controllers.RestoreAnswer(): no answer id")
return
}
uid := VisitorID(c)
err := models.RestoreAnswer(aid, uid)
if err != nil {
c.JSON(http.StatusInternalServerError, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}
controllers/controllers.go
package controllers
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"zhihu/config"
"zhihu/models"
)
func Visitor(c *gin.Context) (*models.User, uint) {
sess := sessions.Default(c)
uid := sess.Get(config.Server.SessionKey)
if uid == nil {
return nil, 0
}
user := models.GetUserByID(uid.(uint))
if user == nil {
return nil, 0
}
return user, uid.(uint)
}
func VisitorID(c *gin.Context) uint {
sess := sessions.Default(c)
uid := sess.Get(config.Server.SessionKey)
if uid == nil {
return 0
}
return uid.(uint)
}
controllers/index.go
package controllers
import (
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
"zhihu/models"
)
func IndexGet(c *gin.Context) {
user, uid := Visitor(c)
topStory := models.HomeTimeline(uid)
v := DataState{
Page: "index",
TopStory: topStory,
}
dataState, _ := json.Marshal(&v)
c.HTML(http.StatusOK, "index.html", gin.H{
"user": user,
"topStory": topStory,
"dataState": string(dataState),
})
return
}
controllers/member.go
package controllers
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
"zhihu/models"
)
func MemberInfo(c *gin.Context) {
uid := VisitorID(c)
urlToken := c.Param("url_token")
if urlToken == "" {
c.JSON(http.StatusNotFound, nil)
return
}
member := models.GetUserByURLToken(urlToken, uid)
if member == nil {
c.JSON(http.StatusNotFound, nil)
return
}
c.JSON(http.StatusOK, member)
}
func FollowMember(c *gin.Context) {
urlToken := c.Param("url_token")
if urlToken == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
succeed := true
if err := models.FollowMember(urlToken, uid); err != nil {
succeed = false
log.Println("controllers.FollowMember(): ", err)
}
c.JSON(http.StatusOK, gin.H{
"succeed": succeed,
})
}
func UnfollowMember(c *gin.Context) {
urlToken := c.Param("url_token")
if urlToken == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
succeed := true
if err := models.UnfollowMember(urlToken, uid); err != nil {
succeed = false
log.Println("controllers.UnfollowMember(): ", err)
}
c.JSON(http.StatusOK, gin.H{
"succeed": succeed,
})
}
controllers/page.go
package controllers
import (
"encoding/json"
"github.com/gin-gonic/gin"
"log"
"net/http"
"zhihu/models"
)
type DataState struct {
Page string `json:"page"`
Answers []*models.Answer `json:"answers"`
Question *models.Question `json:"question"`
TopStory []*models.Action `json:"topStory"`
}
func AnswerGet(c *gin.Context) {
user, uid := Visitor(c)
aid := c.Param("aid")
answer := models.GetAnswer(aid, uid)
if answer == nil {
c.HTML(http.StatusNotFound, "404.html", nil)
return
}
v := DataState{
Page: "answer",
Question: answer.Question,
}
v.Answers = append(v.Answers, answer)
dataState, _ := json.Marshal(&v)
c.HTML(http.StatusOK, "answer.html", gin.H{
"answer": answer,
"question": answer.Question,
"user": user,
"dataState": string(dataState),
})
}
func QuestionGet(c *gin.Context) {
user, uid := Visitor(c)
qid := c.Param("qid")
if qid == "" {
c.HTML(http.StatusNotFound, "404.html", nil)
log.Println("controllers.QuestionGet(): no question id")
return
}
question := models.GetQuestionWithAnswers(qid, uid)
if question == nil {
c.HTML(http.StatusNotFound, "404.html", nil)
return
}
v := DataState{
Page: "question",
Question: question,
}
for _, answer := range question.Answers {
v.Answers = append(v.Answers, answer)
}
dataState, _ := json.Marshal(&v)
c.HTML(http.StatusOK, "question.html", gin.H{
"question": question,
"user": user,
"dataState": string(dataState),
})
}
controllers/question.go
package controllers
import (
"encoding/json"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"log"
"net/http"
"strconv"
"zhihu/models"
)
func PostQuestion(c *gin.Context) {
decoder := json.NewDecoder(c.Request.Body)
var question models.Question
if err := decoder.Decode(&question); err != nil {
log.Println("controllers.PostQuestion(): ", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
if question.Title == "" || len(question.TopicURLTokens) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
})
return
}
uid := VisitorID(c)
if uid == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
})
return
}
if err := models.InsertQuestion(&question, uid); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"questionId": question.ID,
})
}
func FollowQuestion(c *gin.Context) {
qid := c.Param("id")
if qid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
if err := models.FollowQuestion(qid, uid); err != nil {
c.JSON(http.StatusNotFound, nil)
log.Println("controllers.FollowQuestion(): ", err)
return
}
c.JSON(http.StatusOK, gin.H{
"succeed": true,
})
}
func UnfollowQuestion(c *gin.Context) {
qid := c.Param("id")
if qid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
if err := models.UnfollowQuestion(qid, uid); err != nil {
c.JSON(http.StatusNotFound, nil)
log.Println("controllers.UnfollowQuestion(): ", err)
return
}
c.JSON(http.StatusOK, gin.H{
"succeed": true,
})
}
func QuestionFollowers(c *gin.Context) {
qid := c.Param("id")
if qid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
page := &models.Page{
Session: sessions.Default(c),
}
offset, _ := strconv.Atoi(c.Request.FormValue("offset"))
followers := page.QuestionFollowers(qid, offset, uid)
if followers == nil {
c.JSON(http.StatusNotFound, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"paging": page.Paging,
"data": followers,
})
}
func QuestionComments(c *gin.Context) {
qid := c.Param("id")
if qid == "" {
log.Println("controlloers:QuestionComments(): no qestion id")
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
page := &models.Page{
Session: sessions.Default(c),
}
offset, _ := strconv.Atoi(c.Request.FormValue("offset"))
comments := page.QuestionComments(qid, offset, uid)
if comments == nil {
c.JSON(http.StatusNotFound, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"paging": page.Paging,
"data": comments,
})
}
func PostQuestionComment(c *gin.Context) {
qid := c.Param("id")
if qid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
content := struct {
Content string `json:"content"`
}{}
decoder := json.NewDecoder(c.Request.Body)
if err := decoder.Decode(&content); err != nil {
log.Println("controllers.PostQuestionComment(): ", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
comment, err := models.InsertQuestionComment(qid, content.Content, uid)
if err != nil {
log.Println("controllers.PostQuestionComment(): ", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
c.JSON(http.StatusOK, comment)
}
func DeleteQuestionComment(c *gin.Context) {
qid := c.Param("id")
cid := c.Param("cid")
if qid == "" || cid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
if err := models.DeleteQuestionComment(qid, cid, uid); err != nil {
log.Println("controllers.DeleteQuestionComment(): ", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
c.JSON(http.StatusNoContent, nil)
}
func LikeQuestionComment(c *gin.Context) {
cid := c.Param("cid")
if cid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
if err := models.LikeQuestionComment(cid, uid); err != nil {
log.Println("controllers.LikeQuestionComment(): ", err)
if err.Error() == "reply is zero" {
c.JSON(http.StatusBadRequest, nil)
} else {
c.JSON(http.StatusInternalServerError, nil)
}
return
}
c.JSON(http.StatusOK, nil)
}
func UndoLikeQuestionComment(c *gin.Context) {
cid := c.Param("cid")
if cid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
if err := models.UndoLikeQuestionComment(cid, uid); err != nil {
log.Println("controllers.UndoLikeQuestionComment(): ", err)
if err.Error() == "reply is zero" {
c.JSON(http.StatusBadRequest, nil)
} else {
c.JSON(http.StatusInternalServerError, nil)
}
return
}
c.JSON(http.StatusOK, nil)
}
controllers/topic.go
package controllers
import (
"encoding/json"
"github.com/gin-gonic/gin"
"log"
"net/http"
"zhihu/models"
)
func SearchTopics(c *gin.Context) {
var topics []models.Topic
token := c.Request.FormValue("token")
if token == "" {
c.JSON(http.StatusOK, topics)
return
}
topics = models.SearchTopics(token)
c.JSON(http.StatusOK, topics)
}
func PostTopic(c *gin.Context) {
topic := &models.Topic{}
decoder := json.NewDecoder(c.Request.Body)
if err := decoder.Decode(topic); err != nil {
log.Println("controllers.PostTopic(): ", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
if err := models.UpdateTopic(topic); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"topic": topic,
})
}
controllers/user.go
package controllers
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"log"
"net/http"
"zhihu/config"
"zhihu/models"
"zhihu/utils"
)
func SigninGet(c *gin.Context) {
uid := VisitorID(c)
if uid != 0 {
c.Redirect(http.StatusSeeOther, "/")
return
}
c.HTML(http.StatusOK, "signin.html", nil)
}
func SigninPost(c *gin.Context) {
username := c.Request.FormValue("username")
password := c.Request.FormValue("password")
if err := utils.ValidateUsername(username); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Message,
"code": err.Code,
})
return
}
if len(password) < 6 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "请输入6位及以上的密码",
"code": utils.ErrBadPasswordFormat,
})
return
}
pwdEncrypted := utils.EncryptPassword(username, password)
user := models.GetUserByUsername(username)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{
"message": "该邮箱号尚未注册知乎",
"code": utils.ErrAccountNotFound,
})
return
}
if user.Password != pwdEncrypted {
c.JSON(http.StatusUnauthorized, gin.H{
"message": "帐号或密码错误",
"code": utils.ErrIncorrectPassword,
})
return
}
sess := sessions.Default(c)
sess.Clear()
sess.Set(config.Server.SessionKey, user.ID)
_ = sess.Save()
c.JSON(http.StatusCreated, nil)
}
func SignupGet(c *gin.Context) {
uid := VisitorID(c)
if uid != 0 {
c.Redirect(http.StatusSeeOther, "/")
return
}
c.HTML(http.StatusOK, "signup.html", nil)
}
func SignupPost(c *gin.Context) {
fullName := c.Request.FormValue("fullName")
username := c.Request.FormValue("username")
password := c.Request.FormValue("password")
if err := utils.ValidateFullName(fullName); err != nil {
log.Println("fullName", err)
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Message,
"code": err.Code,
})
return
}
if err := utils.ValidateUsername(username); err != nil {
log.Println("username", err)
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Message,
"code": err.Code,
})
return
}
if err := utils.ValidatePassword(password); err != nil {
log.Println("password", err)
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Message,
"code": err.Code,
})
return
}
pwdEncrypted := utils.EncryptPassword(username, password)
user := &models.User{
Email: username,
Name: fullName,
Password: pwdEncrypted,
}
uid, err := models.InsertUser(user)
println(uid)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该邮箱已注册,请直接登录",
"code": utils.ErrDuplicatedEmail,
})
return
}
sess := sessions.Default(c)
sess.Clear()
sess.Set(config.Server.SessionKey, uid)
_ = sess.Save()
c.JSON(http.StatusCreated, nil)
}
func LogoutGet(c *gin.Context) {
sess := sessions.Default(c)
sess.Clear()
_ = sess.Save()
nextPath := c.GetHeader("referer")
c.Redirect(http.StatusSeeOther, nextPath)
}
controllers/vote.go
package controllers
import (
"encoding/json"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"log"
"net/http"
"strconv"
"zhihu/models"
)
type VoteType struct {
Type string
}
func VoteAnswer(c *gin.Context) {
var succeed bool
aid := c.Param("id")
if aid == "" {
c.JSON(http.StatusOK, gin.H{
"succeed": succeed,
})
return
}
user, _ := Visitor(c)
if user == nil {
c.JSON(http.StatusForbidden, gin.H{
"success": succeed,
})
return
}
var voteType VoteType
decoder := json.NewDecoder(c.Request.Body)
if err := decoder.Decode(&voteType); err != nil {
log.Println(err)
c.JSON(http.StatusOK, gin.H{
"success": succeed,
})
return
}
switch voteType.Type {
case "up":
succeed = user.UpVote(aid)
case "down":
succeed = user.DownVote(aid)
case "neutral":
succeed = user.Neutral(aid)
default:
}
c.JSON(http.StatusOK, gin.H{
"success": succeed,
})
}
func AnswerVoters(c *gin.Context) {
aid := c.Param("id")
if aid == "" {
c.JSON(http.StatusNotFound, nil)
return
}
uid := VisitorID(c)
page := &models.Page{
Session: sessions.Default(c),
}
offset, _ := strconv.Atoi(c.Request.FormValue("offset"))
voters := page.AnswerVoters(aid, offset, uid)
c.JSON(http.StatusOK, gin.H{
"paging": page.Paging,
"data": voters,
})
}
models/answer.go
package models
import (
"database/sql"
"github.com/garyburd/redigo/redis"
"log"
"strconv"
"time"
)
func InsertAnswer(qid, content string, uid uint) (string, error) {
var err error
defer func() {
if err != nil {
log.Println("models.InsertAnswer(): ", err)
}
}()
tx, err := db.Begin()
if err != nil {
return "", err
}
defer func(tx *sql.Tx) {
_ = tx.Rollback()
}(tx)
now := time.Now()
res, err := tx.Exec("INSERT answers SET content=?, user_id=?, question_id=?, created_at=?", content, uid, qid, now)
if err != nil {
return "", err
}
id, err := res.LastInsertId()
if err != nil {
return "", err
}
aid := strconv.FormatInt(id, 10)
_, err = tx.Exec("UPDATE questions SET answer_count=answer_count+1 WHERE id=?", qid)
if err != nil {
return "", err
}
_, err = tx.Exec("UPDATE users SET answer_count=answer_count+1 WHERE id=?", uid)
if err != nil {
return "", err
}
conn := redisPool.Get()
defer conn.Close()
if err := UpdateRank(conn, aid, now.Unix()); err != nil {
return "", err
}
err = tx.Commit()
if err != nil {
return "", err
}
go HandleNewAction(uid, AnswerQuestionAction, aid)
return aid, err
}
func DeleteAnswer(aid string, uid uint) error {
var err error
defer func() {
if err != nil {
log.Println("models.InsertAnswer(): ", err)
}
}()
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var qid string
err = tx.QueryRow("SELECT question_id FROM answers WHERE id=?", aid).Scan(&qid)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE answers SET is_deleted=1 WHERE id=?", aid)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE questions SET answer_count=answer_count-1 WHERE id=?", qid)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE users SET answer_count=answer_count-1 WHERE id=?", uid)
if err != nil {
return err
}
conn := redisPool.Get()
defer conn.Close()
if err = RemoveFromRank(conn, aid); err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
go RemoveAction(uid, AskQuestionAction, aid)
return err
}
func RestoreAnswer(aid string, uid uint) error {
var err error
defer func() {
if err != nil {
log.Println("models.RestoreAnswer(): ", err)
}
}()
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var qid string
var time int
err = tx.QueryRow("SELECT UNIX_TIMESTAMP(created_at), question_id FROM answers WHERE id=?", aid).Scan(&time, &qid)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE answers SET is_deleted=0 WHERE id=?", aid)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE questions SET answer_count=answer_count+1 WHERE id=?", qid)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE users SET answer_count=answer_count+1 WHERE id=?", uid)
if err != nil {
return err
}
conn := redisPool.Get()
defer conn.Close()
n, err := redis.Int(conn.Do("SCARD", "upvoted:"+aid))
score := int64(time + n*432)
if err := UpdateRank(conn, aid, score); err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return err
}
models/db.go
package models
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/gomodule/redigo/redis"
"log"
"zhihu/config"
)
var (
db *sql.DB
redisPool *redis.Pool
)
func initDB() {
var err error
db, err = sql.Open("mysql", config.Database.DSN)
if err != nil {
log.Fatal(err)
}
err = db.Ping()
if err != nil {
log.Fatal(err)
}
}
func initRedis() {
redisPool = redis.NewPool(func() (redis.Conn, error) {
return redis.Dial("tcp", config.Redis.Addr)
}, config.Redis.MaxIdle)
}
func init() {
initDB()
initRedis()
}
models/models.go
package models
import (
"fmt"
"html/template"
"log"
"time"
)
type User struct {
ID uint `json:"id"`
Email string `json:"-"`
Name string `json:"fullname"`
Password string `json:"-"`
URLToken string `json:"url_token"`
Gender int `json:"gender"`
Headline string `json:"headline"`
AvatarURL string `json:"avatar_url"`
Posts []*Post
AnswerCount uint `json:"answer_count"`
FollowerCount uint `json:"follower_count"`
Followed bool `json:"is_followed"`
Following bool `json:"is_following"`
Anonymous bool `json:"is_anonymous"`
}
type Member struct {
User
MarkedCount uint `json:"marked_count"`
FollowingCount uint `json:"following_count"`
PrivacyProtected bool `json:"is_privacy_protected"`
VoteUpCount uint `json:"voteup_count"`
Description string `json:"description"`
FollowingTopicCount uint `json:"following_topic_count"`
ThankedCount uint `json:"thanked_count"`
}
const (
AskQuestionAction = iota
AnswerQuestionAction
FollowQuestionAction
VoteUpAnswerAction
OtherAction
)
type Action struct {
*User `json:"user"`
Type int `json:"type"`
*Question `json:"question"`
*Answer `json:"answer"`
DateCreated string `json:"created_at"`
}
type Question struct {
ID string `json:"id"`
User *User `json:"user"`
Title string `json:"title"`
Detail string `json:"detail"`
DateCreated string `json:"date_created"`
DateModified string `json:"date_modified"`
VisitCount uint `json:"visit_count"`
AnswerCount uint `json:"answer_count"`
CommentCount uint `json:"comment_count"`
FollowerCount uint `json:"follower_count"`
Topics []*Topic
TopicURLTokens []string `json:"topic_url_tokens"`
Answers []*Answer
Followed bool `json:"is_followed"`
Answered bool `json:"is_answered"`
VisitorAnswerID uint `json:"visitor_answer_id"`
VisitorAnswerDeleted bool `json:"visitor_answer_deleted"`
Anonymous bool `json:"is_anonymous"`
}
type Topic struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type Answer struct {
ID string `json:"id"`
*Question
Author *User `json:"author"`
Content template.HTML
DateCreated string
DateModified string
MarkedCount uint
UpvoteCount uint
DownVoteCount uint
CommentCount uint
Comments []*AnswerComment
UpVoted bool
DownVoted bool
IsAuthor bool
Deleted bool
}
type Post struct{}
type Comment struct {
ID uint `json:"id"`
Author *User `json:"author"`
DateCreated string `json:"date_created"`
UpvoteCount uint `json:"upvote_count"`
DownVoteCount uint `json:"downvote_count"`
Content string `json:"content"`
LikeCount uint `json:"like_count"`
Liked bool `json:"is_liked"`
}
type AnswerComment struct {
*Answer
Comment
}
type QuestionComment struct {
*Question
Comment
}
func NewQuestion() *Question {
question := new(Question)
question.User = new(User)
return question
}
func NewAnswer() *Answer {
answer := new(Answer)
answer.Question = NewQuestion()
answer.Author = new(User)
return answer
}
func HandleNewAction(user uint, action int, id string) {
var err error
defer func() {
if err != nil {
log.Println("models.HandleNewAction(): ", err)
}
}()
conn := redisPool.Get()
rows, err := db.Query("SELECT follower_id FROM member_followers WHERE member_id=?", user)
if err != nil {
return
}
now := time.Now().Unix()
key := fmt.Sprintf("profile:%d", user)
field := fmt.Sprintf("%d:%s", action, id)
_, _ = conn.Do("ZADD", key, now, field)
var fid string
for rows.Next() {
if err = rows.Scan(&fid); err != nil {
continue
}
key := "home:" + fid
field := fmt.Sprintf("%d:%d:%s", user, action, id)
_, _ = conn.Do("ZADD", key, now, field)
}
if action == VoteUpAnswerAction {
n, err := conn.Do("SCARD", "upvoted:"+id)
if err != nil {
return
}
score := 86400 + n.(int64)*432
println(id, score)
_, err = conn.Do("ZADD", "rank", score, id)
if err != nil {
return
}
}
}
func RemoveAction(user uint, action int, id string) {
var err error
defer func() {
if err != nil {
log.Println("models.DeleteAction(): ", err)
}
}()
conn := redisPool.Get()
rows, err := db.Query("SELECT follower_id FROM member_followers WHERE member_id=?", user)
if err != nil {
return
}
key := fmt.Sprintf("profile:%d", user)
member := fmt.Sprintf("%d:%s", action, id)
_, _ = conn.Do("ZREM", key, member)
var fid string
for rows.Next() {
if err = rows.Scan(&fid); err != nil {
continue
}
key := "home:" + fid
member := fmt.Sprintf("%d:%d:%s", user, action, id)
_, _ = conn.Do("ZREM", key, member)
}
}
models/page.go
package models
import (
"database/sql"
"errors"
"fmt"
"github.com/garyburd/redigo/redis"
"github.com/gin-contrib/sessions"
_ "github.com/go-sql-driver/mysql"
"log"
"strconv"
"strings"
"zhihu/utils"
)
type Page struct {
Paging
sessions.Session
}
type Paging struct {
IsEnd bool `json:"is_end"`
IsStart bool `json:"is_start"`
Next string
}
func HomeTimeline(uid uint) []*Action {
if uid == 0 {
return TopStory(uid)
}
key := fmt.Sprintf("home:%d", uid)
conn := redisPool.Get()
defer conn.Close()
zrevrange := strings.ToUpper("zrevrange")
res, err := redis.Strings(conn.Do(zrevrange, key, 0, 9))
if err != nil {
log.Println("models.TopContent(): ", err)
return nil
}
var actions []*Action
for _, member := range res {
action := new(Action)
s := strings.SplitN(member, ":", 3)
act, err := strconv.Atoi(s[1])
id := s[2]
if err != nil {
continue
}
if id != "" {
who, err := strconv.Atoi(s[0])
if err != nil {
continue
}
action.User = GetUserByID(who)
}
zscore := strings.ToUpper("zscore")
time, err := redis.Int64(conn.Do(zscore, key, member))
if err != nil {
log.Println("models.TopContent(): ", err)
continue
}
action.DateCreated = utils.FormatBeforeUnixTime(time)
switch act {
case AskQuestionAction:
action.Type = AskQuestionAction
action.Question = GetQuestion(id, uid)
case AnswerQuestionAction:
action.Type = AnswerQuestionAction
action.Answer = GetAnswer(id, uid, "before")
case FollowQuestionAction:
action.Type = FollowQuestionAction
action.Question = GetQuestion(id, uid)
case VoteUpAnswerAction:
action.Type = VoteUpAnswerAction
action.Answer = GetAnswer(id, uid, "before")
default:
action.Type = OtherAction
}
actions = append(actions, action)
}
if len(res) == 0 {
return TopStory(uid)
}
return actions
}
func TopStory(uid uint) []*Action {
conn := redisPool.Get()
defer conn.Close()
var actions []*Action
answers, err := redis.Strings(conn.Do("ZREVRANGE", "rank", 0, 9))
if err != nil {
log.Println("models.TopContent(): ", err)
return actions
}
for _, aid := range answers {
action := new(Action)
action.Type = OtherAction
action.Answer = GetAnswer(aid, uid)
actions = append(actions, action)
}
return actions
}
func GetAnswer(aid string, uid uint, options ...string) *Answer {
answer := NewAnswer()
var dateCreated, dateModified int64
err := db.QueryRow("SELECT id, question_id, user_id, content, unix_timestamp(created_at), unix_timestamp(modified_at), "+
"marked_count, comment_count, is_deleted FROM answers WHERE id=?", aid).Scan(
&answer.ID, &answer.Question.ID, &answer.Author.ID,
&answer.Content, &dateCreated, &dateModified,
&answer.MarkedCount, &answer.CommentCount, &answer.Deleted,
)
if err != nil {
if err != sql.ErrNoRows {
log.Println("models.AnswerPage(): answer", aid, err)
}
return nil
}
if len(options) > 0 && options[0] == "before" {
answer.DateCreated = utils.FormatBeforeUnixTime(dateCreated)
answer.DateModified = utils.FormatBeforeUnixTime(dateModified)
} else {
answer.DateCreated = utils.FormatUnixTime(dateCreated)
answer.DateModified = utils.FormatUnixTime(dateModified)
}
answer.Question = GetQuestion(answer.Question.ID, uid)
answer.GetAuthorInfo(uid)
answer.QueryRelation(uid)
return answer
}
func GetQuestionWithAnswers(qid string, uid uint) *Question {
question := GetQuestion(qid, uid)
if question == nil {
return nil
}
question.GetAnswers(uid)
return question
}
func GetQuestion(qid string, uid uint) *Question {
question := NewQuestion()
if err := db.QueryRow("SELECT id, user_id, title, detail, created_at, modified_at, "+
"answer_count, follower_count, comment_count FROM questions WHERE id=?", qid).Scan(
&question.ID,
&question.User.ID,
&question.Title,
&question.Detail,
&question.DateCreated,
&question.DateModified,
&question.AnswerCount,
&question.FollowerCount,
&question.CommentCount,
); err != nil {
log.Printf("models.GetQuestion %s: %v", qid, err)
return nil
}
question.GetTopics()
var temp int
question.Followed = true
if err := db.QueryRow("SELECT 1 FROM question_followers WHERE question_id=? AND user_id=?", qid, uid).Scan(&temp); err != nil {
if !errors.Is(err, sql.ErrNoRows) {
log.Println(err)
}
question.Followed = false
}
var deleted int
question.Answered = true
if err := db.QueryRow("SELECT id, is_deleted FROM answers WHERE question_id=? AND user_id=?", qid, uid).Scan(&question.VisitorAnswerID, &deleted); err != nil {
if !errors.Is(err, sql.ErrNoRows) {
log.Println(err)
}
question.Answered = false
}
if deleted == 1 {
question.VisitorAnswerDeleted = true
}
question.UpdateVisitCount()
return question
}
func (q *Question) GetTopics() {
rows, err := db.Query("SELECT topic_id, topics.name FROM topics, question_topics WHERE topic_id=topics.id AND question_id=?", q.ID)
if err != nil {
log.Println(err)
}
defer rows.Close()
var topics []*Topic
for rows.Next() {
topic := new(Topic)
if err := rows.Scan(&topic.ID, &topic.Name); err != nil {
log.Println("*Question.GetTopics(): ", err)
continue
}
topics = append(topics, topic)
}
q.Topics = topics
}
func (q *Question) GetAnswers(uid uint) {
rows, err := db.Query("SELECT id, question_id, user_id, content, UNIX_TIMESTAMP(created_at),"+
" UNIX_TIMESTAMP(modified_at), marked_count, comment_count FROM answers WHERE question_id=? AND is_deleted=0", q.ID)
if err != nil {
log.Println(err)
}
defer rows.Close()
var answers []*Answer
var dateCreated, dateModified int64
for rows.Next() {
answer := NewAnswer()
err := rows.Scan(
&answer.ID,
&answer.Question.ID,
&answer.Author.ID,
&answer.Content,
&dateCreated,
&dateModified,
&answer.MarkedCount,
&answer.CommentCount,
)
if err != nil {
log.Printf("answer %v: %v", answer.ID, err)
}
answer.DateCreated = utils.FormatUnixTime(dateCreated)
answer.DateModified = utils.FormatUnixTime(dateModified)
answer.GetAuthorInfo(uid)
answer.QueryRelation(uid)
answers = append(answers, answer)
}
q.Answers = answers
}
func (answer *Answer) QueryRelation(uid uint) {
conn := redisPool.Get()
defer conn.Close()
v, err := conn.Do("SCARD", "upvoted:"+answer.ID)
if err != nil {
log.Println(err)
}
answer.UpvoteCount = uint(v.(int64))
if uid == 0 {
return
}
userID := strconv.FormatUint(uint64(uid), 10)
{
v, err := conn.Do("SISMEMBER", "upvoted:"+answer.ID, userID)
if err != nil {
log.Println(err)
}
if v.(int64) == 1 {
answer.UpVoted = true
}
}
if answer.UpVoted != true {
v, err := conn.Do("SISMEMBER", "downvoted:"+answer.ID, userID)
if err != nil {
log.Println(err)
}
if v.(int64) == 1 {
answer.DownVoted = true
}
}
if uid == answer.Author.ID {
answer.Answered = true
}
}
func (question *Question) UpdateVisitCount() {
conn := redisPool.Get()
defer conn.Close()
v, err := conn.Do("INCR", "visited:"+question.ID)
if err != nil {
log.Println("*Question.UpdateVisitCount(): ", question.ID, err)
}
question.VisitCount = uint(v.(int64))
}
models/question.go
package models
import (
"fmt"
"log"
"strconv"
"zhihu/utils"
)
func InsertQuestion(question *Question, uid uint) error {
var err error
defer func() {
if err != nil {
log.Println("models.InsertQuestion(): ", err)
}
}()
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
row, err := tx.Exec("INSERT questions SET user_id=?, title=?, detail=?",
uid, question.Title, question.Detail)
if err != nil {
return err
}
id, err := row.LastInsertId()
if err != nil {
return err
}
qid := strconv.FormatInt(id, 10)
question.ID = qid
for _, topic := range question.TopicURLTokens {
if _, err = tx.Exec("INSERT question_topics SET question_id=?, topic_id=?",
qid, topic); err != nil {
return err
}
}
if _, err = tx.Exec("UPDATE users SET question_count=question_count+1 WHERE id=?",
uid); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
go HandleNewAction(uid, AskQuestionAction, qid)
return nil
}
func FollowQuestion(qid string, uid uint) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec("INSERT question_followers SET question_id=?, user_id=?",
qid, uid); err != nil {
return err
}
if _, err := tx.Exec("UPDATE questions SET follower_count=follower_count+1 WHERE id=?",
qid); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
go HandleNewAction(uid, FollowQuestionAction, qid)
return nil
}
func UnfollowQuestion(qid string, uid uint) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec("DELETE FROM question_followers WHERE question_id=? AND user_id=?",
qid, uid); err != nil {
return err
}
if _, err := tx.Exec("UPDATE questions SET follower_count=follower_count-1 WHERE id=?",
qid); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
go RemoveAction(uid, FollowQuestionAction, qid)
return nil
}
func (page *Page) QuestionFollowers(qid string, offset int, uid uint) []User {
var err error
defer func() {
if err != nil {
log.Println("*Page.QestionUsers(): ", err)
}
}()
start := page.Session.Get("question_followers_start" + qid)
if start == nil {
var newStart string
err = db.QueryRow("SELECT question_followers.created_at FROM users, question_followers WHERE users.id=question_followers.user_id "+
"AND question_id=? ORDER BY question_followers.created_at DESC LIMIT 1", qid).Scan(&newStart)
if err != nil {
return nil
}
page.Session.Set("question_followers_start"+qid, newStart)
page.Session.Save()
start = newStart
offset = 0
page.Paging.IsStart = true
}
limit := fmt.Sprintf("limit %d,%d", offset, 10)
rows, err := db.Query("SELECT users.id, users.fullname, users.gender, users.headline, users.avatar_url, "+
"users.url_token, users.url_token_code, users.answer_count, users.follower_count FROM users, question_followers "+
"WHERE users.id=question_followers.user_id AND question_id=? AND question_followers.created_at<=? ORDER BY question_followers.created_at DESC "+limit,
qid, start.(string))
if err != nil {
return nil
}
defer rows.Close()
var followers []User
var i int
for ; rows.Next(); i++ {
follower := User{}
urlTokenCode := 0
err = rows.Scan(&follower.ID, &follower.Name, &follower.Gender, &follower.Headline,
&follower.AvatarURL, &follower.URLToken, &urlTokenCode,
&follower.AnswerCount, &follower.FollowerCount)
if err != nil {
continue
}
utils.URLToken(&follower.URLToken, urlTokenCode)
follower.QueryRelationWithVisitor(uid)
followers = append(followers, follower)
}
if i < 10 {
page.Paging.IsEnd = true
} else {
page.Paging.Next = fmt.Sprintf("/api/questions/%s/followers?offset=%d", qid, offset+i)
}
return followers
}
func (page *Page) QuestionComments(qid string, offset int, uid uint) []Comment {
var err error
defer func() {
if err != nil {
log.Println("*Page.QestionComments(): ", err)
}
}()
start := page.Session.Get("question_comments_start" + qid)
if start == nil {
var newStart string
if err = db.QueryRow("SELECT created_at FROM question_comments WHERE "+
"question_id=? ORDER BY created_at DESC LIMIT 1", qid).Scan(&newStart); err != nil {
return nil
}
page.Session.Set("question_comments_start"+qid, newStart)
page.Session.Save()
start = newStart
offset = 0
page.Paging.IsStart = true
}
limit := fmt.Sprintf("limit %d,%d", offset, 10)
rows, err := db.Query("SELECT users.id, users.fullname, users.gender, users.headline, users.avatar_url, "+
"users.url_token, users.url_token_code, users.answer_count, users.follower_count, question_comments.id, "+
"question_comments.content, unix_timestamp(question_comments.created_at) FROM users, question_comments "+
"WHERE users.id=question_comments.user_id AND question_id=? AND question_comments.created_at<=? ORDER BY question_comments.created_at DESC "+limit,
qid, start.(string))
if err != nil {
return nil
}
defer rows.Close()
conn := redisPool.Get()
defer conn.Close()
var comments []Comment
var i int
for ; rows.Next(); i++ {
var dateCreated int64
var comment Comment
var author User
var urlTokenCode int
if err = rows.Scan(&author.ID, &author.Name, &author.Gender,
&author.Headline, &author.AvatarURL, &author.URLToken,
&urlTokenCode, &author.AnswerCount, &author.FollowerCount,
&comment.ID, &comment.Content, &dateCreated); err != nil {
log.Println("*Page.QestionComments(): ui", err)
continue
}
key := fmt.Sprintf("question_comment liked:%d", comment.ID)
v, err := conn.Do("SCARD", key)
if err != nil {
log.Println("*Page.QestionComments(): ", err)
continue
}
comment.LikeCount = uint(v.(int64))
v, err = conn.Do("SISMEMBER", key, uid)
if err != nil {
log.Println("*Page.QestionComments(): ", err)
continue
}
if v.(int64) == 1 {
comment.Liked = true
}
utils.URLToken(&author.URLToken, urlTokenCode)
comment.DateCreated = utils.FormatBeforeUnixTime(dateCreated)
comment.Author = &author
comments = append(comments, comment)
}
if i < 10 {
page.Paging.IsEnd = true
} else {
page.Paging.Next = fmt.Sprintf("/api/questions/%s/comments?offset=%d", qid, offset+i)
}
return comments
}
func InsertQuestionComment(qid, content string, uid uint) (*Comment, error) {
var err error
defer func() {
if err != nil {
log.Println("*Page.InsertQestionComment(): ", err)
}
}()
conn := redisPool.Get()
defer conn.Close()
comment := new(Comment)
var author User
var dateCreated int64
var urlTokenCode int
tx, err := db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
if _, err = tx.Exec("INSERT question_comments SET question_id=?, user_id=?, content=?", qid, uid, content); err != nil {
return nil, err
}
if _, err = tx.Exec("UPDATE questions SET comment_count=comment_count+1 WHERE id=?", qid); err != nil {
return nil, err
}
if err = tx.QueryRow("SELECT users.id, users.fullname, users.gender, users.headline, "+
"users.avatar_url, users.url_token, users.url_token_code, users.answer_count, users.follower_count, "+
"question_comments.id, question_comments.content, unix_timestamp(question_comments.created_at) FROM users, question_comments "+
"WHERE users.id=question_comments.user_id AND question_comments.id=LAST_INSERT_ID() AND question_comments.user_id=?", uid).Scan(
&author.ID, &author.Name, &author.Gender, &author.Headline, &author.AvatarURL,
&author.URLToken, &urlTokenCode, &author.AnswerCount, &author.FollowerCount,
&comment.ID, &comment.Content, &dateCreated); err != nil {
return nil, err
}
key := fmt.Sprintf("question_comment liked:%d", comment.ID)
v, err := conn.Do("SCARD", key)
if err != nil {
log.Println("*Page.QestionComments(): ", err)
return nil, err
}
utils.URLToken(&author.URLToken, urlTokenCode)
comment.LikeCount = uint(v.(int64))
comment.DateCreated = utils.FormatBeforeUnixTime(dateCreated)
comment.Author = &author
if err = tx.Commit(); err != nil {
return nil, err
}
return comment, nil
}
func DeleteQuestionComment(qid, cid string, uid uint) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec("DELETE FROM question_comments WHERE id=? AND user_id=?", cid, uid); err != nil {
return err
}
if _, err := tx.Exec("UPDATE questions SET comment_count=comment_count-1 WHERE id=?", qid); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
conn := redisPool.Get()
defer conn.Close()
conn.Do("DEL", "question_comment liked:"+cid)
return nil
}
func LikeQuestionComment(cid string, uid uint) error {
conn := redisPool.Get()
defer conn.Close()
v, err := conn.Do("SADD", "question_comment liked:"+cid, uid)
if err != nil {
return err
}
if v.(int64) == 0 {
return fmt.Errorf("reply is zero")
}
return nil
}
func UndoLikeQuestionComment(cid string, uid uint) error {
conn := redisPool.Get()
defer conn.Close()
v, err := conn.Do("SREM", "question_comment liked:"+cid, uid)
if err != nil {
return err
}
if v.(int64) == 0 {
return fmt.Errorf("reply is zero")
}
return nil
}
models/topic.go
package models
import (
"database/sql"
"fmt"
"github.com/garyburd/redigo/redis"
"log"
"strings"
"unicode/utf8"
)
func SearchTopics(token string) []Topic {
topics := []Topic{}
conn := redisPool.Get()
defer conn.Close()
words := strings.Split(token, " ")
var keys []any
for _, t := range words {
keys = append(keys, "ind:"+t)
}
keys = append(keys, "ind:"+token)
for i, v := range token {
if utf8.RuneLen(v) == 1 {
continue
}
if i == len([]rune(token))-1 && v == []rune(" ")[0] {
break
}
keys = append(keys, fmt.Sprintf("ind:%c", v))
}
res, err := redis.Int64s(conn.Do("SUNION", keys...))
if err != nil {
log.Println("models.SearchTopics(): ", err)
return nil
}
for _, id := range res {
var name string
if err := db.QueryRow("SELECT name FROM topics WHERE id=?", id).Scan(&name); err != nil {
log.Println("modles.SearchTopics(): ", err)
continue
}
topics = append(topics, Topic{
Name: name,
ID: uint(id),
})
}
return topics
}
func UpdateTopic(topic *Topic) error {
var id int64
var err error
defer func() {
if err != nil {
log.Println("models.InsertTopic(): ", err)
}
}()
tx, err := db.Begin()
if err != nil {
return nil
}
defer tx.Rollback()
if err = tx.QueryRow("SELECT id FROM topics WHERE name=?", topic.Name).Scan(&id); err == nil {
if err = tx.Commit(); err != nil {
return err
}
topic.ID = uint(id)
return nil
}
if err != sql.ErrNoRows {
return err
}
row, err := tx.Exec("INSERT topics SET name=?", topic.Name)
if err != nil {
return err
}
id, err = row.LastInsertId()
if err = tx.Commit(); err != nil {
return err
}
topic.ID = uint(id)
if err = createTopicIndex(topic); err != nil {
return err
}
return nil
}
func createTopicIndex(topic *Topic) error {
conn := redisPool.Get()
defer conn.Close()
words := strings.Split(topic.Name, " ")
for _, word := range words {
key := fmt.Sprintf("ind:%s", word)
_, err := conn.Do("SADD", key, topic.ID)
if err != nil {
log.Println("models.createTopicIndex() :", err)
return err
}
}
letters := []rune(topic.Name)
for i := 0; i < len(letters); i++ {
key1 := fmt.Sprintf("ind:%s", string(letters[:i+1]))
_, err := conn.Do("SADD", key1, topic.ID)
if err != nil {
log.Println("models.createTopicIndex() :", err)
return err
}
if utf8.RuneLen(letters[i]) == 1 {
continue
}
if i == len(letters)-1 && letters[i] == []rune(" ")[0] {
break
}
key2 := fmt.Sprintf("ind:%c", letters[i])
_, err = conn.Do("SADD", key2, topic.ID)
if err != nil {
log.Println("models.createTopicIndex() :", err)
return err
}
}
return nil
}
models/user.go
package models
import (
"database/sql"
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/henrylee2cn/pholcus/common/pinyin"
"log"
"strings"
"unicode"
"zhihu/utils"
)
func InsertUser(user *User) (uid uint, err error) {
defer func() {
if err != nil {
log.Println("models.InsertUser: ", err)
}
}()
tx, err := db.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
urlToken, urlTokenCode, err := CreateURLToken(tx, user.Name)
if err != nil {
return 0, err
}
res, err := tx.Exec("INSERT users SET email=?, fullname=?, password=?, url_token=?, url_token_code=?",
user.Email,
user.Name,
user.Password,
urlToken,
urlTokenCode,
)
if err != nil {
return 0, err
}
id, err := res.LastInsertId()
if err != nil {
return 0, err
}
uid = uint(id)
err = tx.Commit()
if err != nil {
return 0, err
}
return uid, nil
}
func CreateURLToken(tx *sql.Tx, name string) (string, int, error) {
s := []rune(name)
var res []string
for i := len(s) - 1; i >= 0; i-- {
r := s[i]
if unicode.IsDigit(r) || unicode.IsLower(r) || unicode.IsUpper(r) {
c := fmt.Sprintf("%c", r)
if res == nil {
res = append(res, c)
} else {
res[len(res)-1] = c + res[len(res)-1]
}
} else {
res = append(res, pinyin.SinglePinyin(r, pinyin.NewArgs())[0])
}
}
for to, from := 0, len(res)-1; to < from; to, from = to+1, from-1 {
res[to], res[from] = res[from], res[to]
}
urlToken := strings.Join(res, "-")
urlTokenCode := 0
if err := tx.QueryRow("SELECT url_token_code FROM users WHERE url_token=? ORDER BY id DESC limit 1",
urlToken).Scan(&urlTokenCode); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return urlToken, urlTokenCode, nil
}
return "", 0, err
}
return urlToken, urlTokenCode + 1, nil
}
func GetUserByUsername(username string) *User {
stmt, err := db.Prepare("SELECT id, email, password FROM users WHERE email=?")
if err != nil {
log.Println(err)
return nil
}
defer stmt.Close()
user := new(User)
if err := stmt.QueryRow(username).Scan(&user.ID, &user.Email, &user.Password); err != nil {
log.Printf("user %s: %v", username, err)
return nil
}
return user
}
func GetUserByID(uid any) *User {
var err error
defer func() {
if err != nil {
log.Println("models.GetUserByID(): uid =", uid, err)
}
}()
user := new(User)
stmt, err := db.Prepare("SELECT id, fullname, gender, headline, url_token, " +
"url_token_code, avatar_url, answer_count, follower_count FROM users WHERE id=?")
if err != nil {
return nil
}
defer stmt.Close()
urlTokenCode := 0
err = stmt.QueryRow(uid).Scan(
&user.ID,
&user.Name,
&user.Gender,
&user.Headline,
&user.URLToken,
&urlTokenCode,
&user.AvatarURL,
&user.AnswerCount,
&user.FollowerCount,
)
if err != nil {
return nil
}
utils.URLToken(&user.URLToken, urlTokenCode)
return user
}
func (answer *Answer) GetAuthorInfo(uid uint) {
author := GetUserByID(answer.Author.ID)
_ = author.QueryRelationWithVisitor(uid)
answer.Author = author
}
func (user *User) QueryRelationWithVisitor(uid uint) error {
var temp int
if err := db.QueryRow("SELECT 1 FROM member_followers WHERE member_id=? AND follower_id=?",
user.ID, uid).Scan(&temp); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
}
user.Followed = true
return nil
}
func GetUserByURLToken(urlToken string, uid uint) *User {
user := new(User)
if err := db.QueryRow("SELECT id, fullname, gender, headline, avatar_url, "+
"answer_count, follower_count FROM users WHERE url_token=? AND url_token_code=0", urlToken).Scan(
&user.ID, &user.Name, &user.Gender,
&user.Headline, &user.AvatarURL,
&user.AnswerCount, &user.FollowerCount,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
s := strings.Split(urlToken, "-")
if len(s) == 1 {
return nil
}
code := s[len(s)-1]
pre := strings.TrimSuffix(urlToken, "-"+code)
if err := db.QueryRow("SELECT id, fullname, gender, headline, avatar_url, "+
"answer_count, follower_count FROM users WHERE url_token=? AND url_token_code=?", pre, code).Scan(
&user.ID, &user.Name, &user.Gender,
&user.Headline, &user.AvatarURL,
&user.AnswerCount, &user.FollowerCount,
); err != nil {
return nil
}
} else {
return nil
}
}
user.URLToken = urlToken
_ = user.QueryRelationWithVisitor(uid)
return user
}
func FollowMember(urlToken string, uid uint) error {
var memberID uint
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.QueryRow("SELECT id FROM users WHERE url_token=?", urlToken).Scan(&memberID); err != nil {
return err
}
if _, err := tx.Exec("INSERT member_followers SET member_id=?, follower_id=?", memberID, uid); err != nil {
return err
}
if _, err := tx.Exec("UPDATE users SET follower_count=follower_count+1 WHERE id=?", memberID); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func UnfollowMember(urlToken string, uid uint) error {
var memberID uint
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.QueryRow("SELECT id FROM users WHERE url_token=?", urlToken).Scan(&memberID); err != nil {
return err
}
if _, err := tx.Exec("DELETE FROM member_followers WHERE member_id=? AND follower_id=?", memberID, uid); err != nil {
return err
}
if _, err := tx.Exec("UPDATE users SET follower_count=follower_count-1 WHERE id=?", memberID); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
models/vote.go
package models
import (
"fmt"
"github.com/garyburd/redigo/redis"
"log"
)
func (user *User) UpVote(aid string) bool {
conn := redisPool.Get()
conn.Send("SADD", "upvoted:"+aid, user.ID)
conn.Send("SREM", "downvoted:"+aid, user.ID)
conn.Flush()
upvoteAddedCount, err := conn.Receive()
if err != nil {
return false
}
if _, err := conn.Receive(); err != nil {
return false
}
UpdateRank(conn, aid, 432)
if upvoteAddedCount.(int64) == 1 {
go func() {
_, err := db.Exec("INSERT answer_voters SET answer_id=?, user_id=?", aid, user.ID)
if err != nil {
log.Println("*models.User.UpVote: ", err)
}
HandleNewAction(user.ID, VoteUpAnswerAction, aid)
}()
}
return true
}
func UpdateRank(conn redis.Conn, aid string, increment int64) error {
_, err := conn.Do("ZINCRBY", "rank", increment, aid)
if err != nil {
log.Println("models.UpdateRank(): ", err)
return err
}
return nil
}
func RemoveFromRank(conn redis.Conn, aid string) error {
_, err := conn.Do("ZREM", "rank", aid)
return err
}
func (user *User) DownVote(aid string) bool {
conn := redisPool.Get()
conn.Send("SADD", "downvoted:"+aid, user.ID)
conn.Send("SREM", "upvoted:"+aid, user.ID)
conn.Flush()
if v, err := conn.Receive(); err != nil || v == 0 {
log.Println(err, v.(int64))
return false
}
upvoteRemovedCount, err := conn.Receive()
if err != nil {
return false
}
UpdateRank(conn, aid, -432)
if upvoteRemovedCount.(int64) == 1 {
go func() {
_, err := db.Exec("DELETE FROM answer_voters WHERE answer_id=? AND user_id=?", aid, user.ID)
if err != nil {
log.Println("*User.DownVote: ", err)
}
}()
}
return true
}
func (user *User) Neutral(aid string) bool {
conn := redisPool.Get()
conn.Send("SREM", "upvoted:"+aid, user.ID)
conn.Send("SREM", "downvoted:"+aid, user.ID)
conn.Flush()
upvoteRemovedCount, err := conn.Receive()
if err != nil {
return false
}
if _, err := conn.Receive(); err != nil {
return false
}
UpdateRank(conn, aid, -432)
if upvoteRemovedCount == 1 {
go func() {
_, err := db.Exec("DELETE FROM answer_voters WHERE answer_id=? AND user_id=?", aid, user.ID)
if err != nil {
log.Println("*User.DownVote: ", err)
}
}()
}
return true
}
func (page *Page) AnswerVoters(aid string, offset int, uid uint) []User {
var voters = make([]User, 0)
start := page.Session.Get("start" + aid)
if start == nil {
var newStart int
if err := db.QueryRow("SELECT answer_voters.id FROM users, answer_voters WHERE users.id=answer_voters.user_id "+
"AND answer_id=? ORDER BY answer_voters.id DESC LIMIT 1", aid).Scan(&newStart); err != nil {
log.Println("*Page.AnswerVoters(): ", err)
return voters
}
page.Session.Set("start"+aid, newStart)
page.Session.Save()
start = newStart
offset = 0
page.Paging.IsStart = true
}
limit := fmt.Sprintf("limit %d,%d", offset, 10)
rows, err := db.Query("SELECT users.id, users.fullname, users.gender, users.headline, "+
"users.avatar_url, users.url_token, users.answer_count, users.follower_count FROM users, answer_voters "+
"WHERE users.id=answer_voters.user_id AND answer_id=? AND answer_voters.id<=? ORDER BY answer_voters.id DESC "+limit,
aid, start.(int))
if err != nil {
log.Println("*Page.AnswerVoters(): ", err)
return voters
}
defer rows.Close()
var i int
for ; rows.Next(); i++ {
var voter User
if err := rows.Scan(&voter.ID, &voter.Name, &voter.Gender,
&voter.Headline, &voter.AvatarURL, &voter.URLToken,
&voter.AnswerCount, &voter.FollowerCount); err != nil {
log.Println("*Page.AnswerVoters(): ", err)
continue
}
voter.QueryRelationWithVisitor(uid)
voters = append(voters, voter)
}
if i < 10 {
page.Paging.IsEnd = true
} else {
page.Paging.Next = fmt.Sprintf("/api/answers/%s/voters?offset=%d", aid, offset+i)
}
return voters
}
基本上完了