仿知乎网页版(使用Go开发)

117 阅读19分钟

仿知乎网页版(使用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)
    //Add routes
    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: "/"} //Also set Secure: true if using SSL, you should though
    //engine.Use(sessions.Session("gin-session", store))
}

router/router.go

package router

import (
    "github.com/gin-gonic/gin"
    "zhihu/controllers"
    "zhihu/middleware"
)

func Route(router *gin.Engine) {
    router.LoadHTMLGlob("views/**/*")
    // router.LoadHTMLFiles("views/index.html")

    router.Static("/static", "./static")

    //foobar
    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)
    //question

    router.GET("/question/:qid", controllers.QuestionGet)
    //answer
    //-----看到这里了
    router.GET("/question/:qid/answer/:aid", middleware.RefreshSession(), controllers.AnswerGet)
    //
    router.GET("/topic/autocomplete", controllers.SearchTopics)
    //api
    api := router.Group("/api")
    {
       api.GET("/answers/:id/voters", controllers.AnswerVoters)
       //    api.GET("answers/:id/comments", controllers.AnswerComments)
       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.DELETE("/questions/:id", controllers.DeleteQuestion)

       api.POST("/answers/:id/voters", controllers.VoteAnswer)

       //    api.POST("/answers/:id/comments", controllers.PostAnswerComment)
       //    api.DELETE("/answers/:id/comments", controllers.DeleteAnswerComment)

       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) //qid not avaliable

       api.POST("/questions/:id/followers", controllers.FollowQuestion)
       api.DELETE("/questions/:id/followers", controllers.UnfollowQuestion) //204NoContent

       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
)

// ValidateFullName 验证全名
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})$`) //Email
    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() //yesterday

    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")
    //include := c.Request.FormValue("include")
    if urlToken == "" { //|| include == "" {
       c.JSON(http.StatusNotFound, nil)
       return
    }

    member := models.GetUserByURLToken(urlToken, uid)
    if member == nil {
       c.JSON(http.StatusNotFound, nil)
       return
    }

    // s := strings.Split(incldue,",")

    /* t := reflect.TypeOf(member).Elem()
       v := reflect.ValueOf(member).Elem()
       for i = 0; i < t.NumField(); i++ {
          _, ok := info[t.Field(i).Tag.Get("json")]
          if !ok {
             continue
          }
          name := t.Field(i).Name
          v.FieldByName(name)
       }*/

    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)
    // qid, _ := strconv.ParseUint(c.Param("qid"), 10, 0) //question ID
    aid := c.Param("aid") //answer ID
    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") //question ID
    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") //c.PostForm()
    password := c.Request.FormValue("password")
    // nextPath := c.PostForm("next")
    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)
    // c.Redirect(http.StatusSeeOther, nextPath)
}

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)
    //c.Redirect(http.StatusSeeOther, "/signin")
}

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()
}

//Deprecated: Initialize the Pool directory as shown in the example

models/models.go

package models

import (
    "fmt"
    "html/template"
    "log"
    "time"
)

type User struct {
    ID        uint   `json:"id"`
    Email     string `json:"-"` //username
    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"` //followed by user who sent request
    Following bool `json:"is_following"`
    Anonymous bool `json:"is_anonymous"`
}

type Member struct {
    User
    MarkedCount    uint `json:"marked_count"`
    FollowingCount uint `json:"following_count"`
    // Educations []*Education
    PrivacyProtected bool `json:"is_privacy_protected"`
    VoteUpCount      uint `json:"voteup_count"`
    //Blocked bool `json:"is_blocked"`
    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
    // Comments  []*QuestionComment
    // Followers []*User

    Followed             bool `json:"is_followed"` //followed by user who sent request
    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
    // UpVotes  []*User

    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 {
    // *User
    Paging
    sessions.Session
}

type Paging struct {
    IsEnd bool `json:"is_end"`
    // Totals   uint
    // Previous string
    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)
    //determine whether user voted this answer
    answer.QueryRelation(uid)

    return answer
}

/*
func (page *Page) GetAnswerComments(aid uint) (comments []*AnswerComment) {
    rows, err := db.Query("SELECT * FROM answer_comments WHERE answer_id=?", aid)
    if err != nil {
       log.Printf("answer_comments %d: %v", aid, err)
       return
    }
    defer rows.Close()
    for rows.Next() {
       comment := new(AnswerComment)
       comment.User = new(User)
       comment.Answer = NewAnswer()
       if err := rows.Scan(
          &comment.Comment.ID, &comment.User.ID,
          &comment.Answer.ID, &comment.Comment.DateCreated,
          &comment.Comment.Content); err != nil {
          log.Printf("answer_comments %d: %v", aid, err)
          return
       }
       comments = append(comments, comment)
    }
    return
}*/

func GetQuestionWithAnswers(qid string, uid uint) *Question {
    question := GetQuestion(qid, uid)
    if question == nil {
       return nil
    }
    question.GetAnswers(uid)

    return question
}

// GetQuestion 由问题id从db中查询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
    //query whether question's followed
    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
    }
    //query whether question's answered
    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 visit count + 1
    question.UpdateVisitCount()

    return question
}

/*func GetQuestionComments(qid uint) (comments []*QuestionComment) {
    rows, err := db.Query("SELECT * FROM question_comments WHERE question_id=?", qid)
    if err != nil {
       log.Printf("question_comments %d: %v", qid, err)
       return
    }
    defer rows.Close()
    for rows.Next() {
       comment := new(QuestionComment)
       comment.Comment.User = new(User)
       comment.Question = NewQuestion()
       if err := rows.Scan(&comment.Comment.ID, &comment.Comment.User.ID, &comment.Question.ID, &comment.Comment.DateCreated, &comment.Comment.Content); err != nil {
          log.Printf("question_comments %d: %v", qid, err)
          return
       }
       comments = append(comments, comment)
    }
    return
}*/

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指针
       answer.QueryRelation(uid) //修改answer指针

       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"
    //"time"
    "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) //XXX: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
       //    comment.QueryRelationWithVisitor(uid)
       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 { //deleted by oneself
       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 //XXX:
       }
    }
    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 { //urlToken:followed, uid:to follow
    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
}

基本上完了