init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
/*
The article module containing the article CRUD operation and relationship CRUD.
model.go: definition of orm based data model
routers.go: router binding and core logic
serializers.go: definition the schema of return data
validators.go: definition the validator of form data
*/
package articles

View File

@@ -0,0 +1,368 @@
package articles
import (
"strconv"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/users"
"gorm.io/gorm"
)
type ArticleModel struct {
gorm.Model
Slug string `gorm:"uniqueIndex"`
Title string
Description string `gorm:"size:2048"`
Body string `gorm:"size:2048"`
Author ArticleUserModel
AuthorID uint
Tags []TagModel `gorm:"many2many:article_tags;"`
Comments []CommentModel `gorm:"ForeignKey:ArticleID"`
}
type ArticleUserModel struct {
gorm.Model
UserModel users.UserModel
UserModelID uint
ArticleModels []ArticleModel `gorm:"ForeignKey:AuthorID"`
FavoriteModels []FavoriteModel `gorm:"ForeignKey:FavoriteByID"`
}
type FavoriteModel struct {
gorm.Model
Favorite ArticleModel
FavoriteID uint
FavoriteBy ArticleUserModel
FavoriteByID uint
}
type TagModel struct {
gorm.Model
Tag string `gorm:"uniqueIndex"`
ArticleModels []ArticleModel `gorm:"many2many:article_tags;"`
}
type CommentModel struct {
gorm.Model
Article ArticleModel
ArticleID uint
Author ArticleUserModel
AuthorID uint
Body string `gorm:"size:2048"`
}
func GetArticleUserModel(userModel users.UserModel) ArticleUserModel {
var articleUserModel ArticleUserModel
if userModel.ID == 0 {
return articleUserModel
}
db := common.GetDB()
db.Where(&ArticleUserModel{
UserModelID: userModel.ID,
}).FirstOrCreate(&articleUserModel)
articleUserModel.UserModel = userModel
return articleUserModel
}
func (article ArticleModel) favoritesCount() uint {
db := common.GetDB()
var count int64
db.Model(&FavoriteModel{}).Where(FavoriteModel{
FavoriteID: article.ID,
}).Count(&count)
return uint(count)
}
func (article ArticleModel) isFavoriteBy(user ArticleUserModel) bool {
db := common.GetDB()
var favorite FavoriteModel
db.Where(FavoriteModel{
FavoriteID: article.ID,
FavoriteByID: user.ID,
}).First(&favorite)
return favorite.ID != 0
}
// BatchGetFavoriteCounts returns a map of article ID to favorite count
func BatchGetFavoriteCounts(articleIDs []uint) map[uint]uint {
if len(articleIDs) == 0 {
return make(map[uint]uint)
}
db := common.GetDB()
type result struct {
FavoriteID uint
Count uint
}
var results []result
db.Model(&FavoriteModel{}).
Select("favorite_id, COUNT(*) as count").
Where("favorite_id IN ?", articleIDs).
Group("favorite_id").
Find(&results)
countMap := make(map[uint]uint)
for _, r := range results {
countMap[r.FavoriteID] = r.Count
}
return countMap
}
// BatchGetFavoriteStatus returns a map of article ID to whether the user favorited it
func BatchGetFavoriteStatus(articleIDs []uint, userID uint) map[uint]bool {
if len(articleIDs) == 0 || userID == 0 {
return make(map[uint]bool)
}
db := common.GetDB()
var favorites []FavoriteModel
db.Where("favorite_id IN ? AND favorite_by_id = ?", articleIDs, userID).Find(&favorites)
statusMap := make(map[uint]bool)
for _, f := range favorites {
statusMap[f.FavoriteID] = true
}
return statusMap
}
func (article ArticleModel) favoriteBy(user ArticleUserModel) error {
db := common.GetDB()
var favorite FavoriteModel
err := db.FirstOrCreate(&favorite, &FavoriteModel{
FavoriteID: article.ID,
FavoriteByID: user.ID,
}).Error
return err
}
func (article ArticleModel) unFavoriteBy(user ArticleUserModel) error {
db := common.GetDB()
err := db.Where("favorite_id = ? AND favorite_by_id = ?", article.ID, user.ID).Delete(&FavoriteModel{}).Error
return err
}
func SaveOne(data interface{}) error {
db := common.GetDB()
err := db.Save(data).Error
return err
}
func FindOneArticle(condition interface{}) (ArticleModel, error) {
db := common.GetDB()
var model ArticleModel
err := db.Preload("Author.UserModel").Preload("Tags").Where(condition).First(&model).Error
return model, err
}
func FindOneComment(condition *CommentModel) (CommentModel, error) {
db := common.GetDB()
var model CommentModel
err := db.Preload("Author.UserModel").Preload("Article").Where(condition).First(&model).Error
return model, err
}
func (self *ArticleModel) getComments() error {
db := common.GetDB()
err := db.Preload("Author.UserModel").Model(self).Association("Comments").Find(&self.Comments)
return err
}
func getAllTags() ([]TagModel, error) {
db := common.GetDB()
var models []TagModel
err := db.Find(&models).Error
return models, err
}
func FindManyArticle(tag, author, limit, offset, favorited string) ([]ArticleModel, int, error) {
db := common.GetDB()
var models []ArticleModel
var count int
offset_int, errOffset := strconv.Atoi(offset)
if errOffset != nil {
offset_int = 0
}
limit_int, errLimit := strconv.Atoi(limit)
if errLimit != nil {
limit_int = 20
}
tx := db.Begin()
if tag != "" {
var tagModel TagModel
tx.Where(TagModel{Tag: tag}).First(&tagModel)
if tagModel.ID != 0 {
// Get article IDs via association
var tempModels []ArticleModel
if err := tx.Model(&tagModel).Offset(offset_int).Limit(limit_int).Association("ArticleModels").Find(&tempModels); err != nil {
tx.Rollback()
return models, count, err
}
count = int(tx.Model(&tagModel).Association("ArticleModels").Count())
// Fetch articles with preloaded associations in single query, ordered by updated_at desc
if len(tempModels) > 0 {
var ids []uint
for _, m := range tempModels {
ids = append(ids, m.ID)
}
tx.Preload("Author.UserModel").Preload("Tags").Where("id IN ?", ids).Order("updated_at desc").Find(&models)
}
}
} else if author != "" {
var userModel users.UserModel
tx.Where(users.UserModel{Username: author}).First(&userModel)
articleUserModel := GetArticleUserModel(userModel)
if articleUserModel.ID != 0 {
count = int(tx.Model(&articleUserModel).Association("ArticleModels").Count())
// Get article IDs via association
var tempModels []ArticleModel
if err := tx.Model(&articleUserModel).Offset(offset_int).Limit(limit_int).Association("ArticleModels").Find(&tempModels); err != nil {
tx.Rollback()
return models, count, err
}
// Fetch articles with preloaded associations in single query, ordered by updated_at desc
if len(tempModels) > 0 {
var ids []uint
for _, m := range tempModels {
ids = append(ids, m.ID)
}
tx.Preload("Author.UserModel").Preload("Tags").Where("id IN ?", ids).Order("updated_at desc").Find(&models)
}
}
} else if favorited != "" {
var userModel users.UserModel
tx.Where(users.UserModel{Username: favorited}).First(&userModel)
articleUserModel := GetArticleUserModel(userModel)
if articleUserModel.ID != 0 {
var favoriteModels []FavoriteModel
tx.Where(FavoriteModel{
FavoriteByID: articleUserModel.ID,
}).Offset(offset_int).Limit(limit_int).Find(&favoriteModels)
count = int(tx.Model(&articleUserModel).Association("FavoriteModels").Count())
// Batch fetch articles to avoid N+1 query
if len(favoriteModels) > 0 {
var ids []uint
for _, favorite := range favoriteModels {
ids = append(ids, favorite.FavoriteID)
}
tx.Preload("Author.UserModel").Preload("Tags").Where("id IN ?", ids).Order("updated_at desc").Find(&models)
}
}
} else {
var count64 int64
tx.Model(&ArticleModel{}).Count(&count64)
count = int(count64)
tx.Offset(offset_int).Limit(limit_int).Preload("Author.UserModel").Preload("Tags").Find(&models)
}
err := tx.Commit().Error
return models, count, err
}
func (self *ArticleUserModel) GetArticleFeed(limit, offset string) ([]ArticleModel, int, error) {
db := common.GetDB()
models := make([]ArticleModel, 0)
var count int
offset_int, errOffset := strconv.Atoi(offset)
if errOffset != nil {
offset_int = 0
}
limit_int, errLimit := strconv.Atoi(limit)
if errLimit != nil {
limit_int = 20
}
tx := db.Begin()
followings := self.UserModel.GetFollowings()
// Batch get ArticleUserModel IDs to avoid N+1 query
if len(followings) > 0 {
var followingUserIDs []uint
for _, following := range followings {
followingUserIDs = append(followingUserIDs, following.ID)
}
var articleUserModels []ArticleUserModel
tx.Where("user_model_id IN ?", followingUserIDs).Find(&articleUserModels)
var authorIDs []uint
for _, aum := range articleUserModels {
authorIDs = append(authorIDs, aum.ID)
}
if len(authorIDs) > 0 {
var count64 int64
tx.Model(&ArticleModel{}).Where("author_id IN ?", authorIDs).Count(&count64)
count = int(count64)
tx.Preload("Author.UserModel").Preload("Tags").Where("author_id IN ?", authorIDs).Order("updated_at desc").Offset(offset_int).Limit(limit_int).Find(&models)
}
}
err := tx.Commit().Error
return models, count, err
}
func (model *ArticleModel) setTags(tags []string) error {
if len(tags) == 0 {
model.Tags = []TagModel{}
return nil
}
db := common.GetDB()
// Batch fetch existing tags
var existingTags []TagModel
db.Where("tag IN ?", tags).Find(&existingTags)
// Create a map for quick lookup
existingTagMap := make(map[string]TagModel)
for _, t := range existingTags {
existingTagMap[t.Tag] = t
}
// Create missing tags and build final list
var tagList []TagModel
for _, tag := range tags {
if existing, ok := existingTagMap[tag]; ok {
tagList = append(tagList, existing)
} else {
// Create new tag with race condition handling
newTag := TagModel{Tag: tag}
if err := db.Create(&newTag).Error; err != nil {
// If creation failed (e.g., concurrent insert), try to fetch existing
var existing TagModel
if err2 := db.Where("tag = ?", tag).First(&existing).Error; err2 == nil {
tagList = append(tagList, existing)
continue
}
return err
}
tagList = append(tagList, newTag)
}
}
model.Tags = tagList
return nil
}
func (model *ArticleModel) Update(data interface{}) error {
db := common.GetDB()
err := db.Model(model).Updates(data).Error
return err
}
func DeleteArticleModel(condition interface{}) error {
db := common.GetDB()
err := db.Where(condition).Delete(&ArticleModel{}).Error
return err
}
func DeleteCommentModel(condition interface{}) error {
db := common.GetDB()
err := db.Where(condition).Delete(&CommentModel{}).Error
return err
}

View File

@@ -0,0 +1,251 @@
package articles
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/users"
"gorm.io/gorm"
"net/http"
"strconv"
)
func ArticlesRegister(router *gin.RouterGroup) {
router.GET("/feed", ArticleFeed)
router.POST("", ArticleCreate)
router.POST("/", ArticleCreate)
router.PUT("/:slug", ArticleUpdate)
router.PUT("/:slug/", ArticleUpdate)
router.DELETE("/:slug", ArticleDelete)
router.POST("/:slug/favorite", ArticleFavorite)
router.DELETE("/:slug/favorite", ArticleUnfavorite)
router.POST("/:slug/comments", ArticleCommentCreate)
router.DELETE("/:slug/comments/:id", ArticleCommentDelete)
}
func ArticlesAnonymousRegister(router *gin.RouterGroup) {
router.GET("", ArticleList)
router.GET("/", ArticleList)
router.GET("/:slug", ArticleRetrieve)
router.GET("/:slug/comments", ArticleCommentList)
}
func TagsAnonymousRegister(router *gin.RouterGroup) {
router.GET("", TagList)
router.GET("/", TagList)
}
func ArticleCreate(c *gin.Context) {
articleModelValidator := NewArticleModelValidator()
if err := articleModelValidator.Bind(c); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
//fmt.Println(articleModelValidator.articleModel.Author.UserModel)
if err := SaveOne(&articleModelValidator.articleModel); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := ArticleSerializer{c, articleModelValidator.articleModel}
c.JSON(http.StatusCreated, gin.H{"article": serializer.Response()})
}
func ArticleList(c *gin.Context) {
//condition := ArticleModel{}
tag := c.Query("tag")
author := c.Query("author")
favorited := c.Query("favorited")
limit := c.Query("limit")
offset := c.Query("offset")
articleModels, modelCount, err := FindManyArticle(tag, author, limit, offset, favorited)
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid param")))
return
}
serializer := ArticlesSerializer{c, articleModels}
c.JSON(http.StatusOK, gin.H{"articles": serializer.Response(), "articlesCount": modelCount})
}
func ArticleFeed(c *gin.Context) {
limit := c.Query("limit")
offset := c.Query("offset")
myUserModel := c.MustGet("my_user_model").(users.UserModel)
if myUserModel.ID == 0 {
c.AbortWithError(http.StatusUnauthorized, errors.New("{error : \"Require auth!\"}"))
return
}
articleUserModel := GetArticleUserModel(myUserModel)
articleModels, modelCount, err := articleUserModel.GetArticleFeed(limit, offset)
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid param")))
return
}
serializer := ArticlesSerializer{c, articleModels}
c.JSON(http.StatusOK, gin.H{"articles": serializer.Response(), "articlesCount": modelCount})
}
func ArticleRetrieve(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid slug")))
return
}
serializer := ArticleSerializer{c, articleModel}
c.JSON(http.StatusOK, gin.H{"article": serializer.Response()})
}
func ArticleUpdate(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid slug")))
return
}
// Check if current user is the author
myUserModel := c.MustGet("my_user_model").(users.UserModel)
articleUserModel := GetArticleUserModel(myUserModel)
if articleModel.AuthorID != articleUserModel.ID {
c.JSON(http.StatusForbidden, common.NewError("article", errors.New("you are not the author")))
return
}
articleModelValidator := NewArticleModelValidatorFillWith(articleModel)
if err := articleModelValidator.Bind(c); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
articleModelValidator.articleModel.ID = articleModel.ID
if err := articleModel.Update(articleModelValidator.articleModel); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := ArticleSerializer{c, articleModel}
c.JSON(http.StatusOK, gin.H{"article": serializer.Response()})
}
func ArticleDelete(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err == nil {
// Article exists, check authorization
myUserModel := c.MustGet("my_user_model").(users.UserModel)
articleUserModel := GetArticleUserModel(myUserModel)
if articleModel.AuthorID != articleUserModel.ID {
c.JSON(http.StatusForbidden, common.NewError("article", errors.New("you are not the author")))
return
}
}
// Delete regardless of existence (idempotent)
if err := DeleteArticleModel(&ArticleModel{Slug: slug}); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
c.JSON(http.StatusOK, gin.H{"article": "delete success"})
}
func ArticleFavorite(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid slug")))
return
}
myUserModel := c.MustGet("my_user_model").(users.UserModel)
if err = articleModel.favoriteBy(GetArticleUserModel(myUserModel)); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := ArticleSerializer{c, articleModel}
c.JSON(http.StatusOK, gin.H{"article": serializer.Response()})
}
func ArticleUnfavorite(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid slug")))
return
}
myUserModel := c.MustGet("my_user_model").(users.UserModel)
if err = articleModel.unFavoriteBy(GetArticleUserModel(myUserModel)); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := ArticleSerializer{c, articleModel}
c.JSON(http.StatusOK, gin.H{"article": serializer.Response()})
}
func ArticleCommentCreate(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("comment", errors.New("Invalid slug")))
return
}
commentModelValidator := NewCommentModelValidator()
if err := commentModelValidator.Bind(c); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
commentModelValidator.commentModel.Article = articleModel
if err := SaveOne(&commentModelValidator.commentModel); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := CommentSerializer{c, commentModelValidator.commentModel}
c.JSON(http.StatusCreated, gin.H{"comment": serializer.Response()})
}
func ArticleCommentDelete(c *gin.Context) {
id64, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("comment", errors.New("Invalid id")))
return
}
id := uint(id64)
commentModel, err := FindOneComment(&CommentModel{Model: gorm.Model{ID: id}})
if err == nil {
// Comment exists, check authorization
myUserModel := c.MustGet("my_user_model").(users.UserModel)
articleUserModel := GetArticleUserModel(myUserModel)
if commentModel.AuthorID != articleUserModel.ID {
c.JSON(http.StatusForbidden, common.NewError("comment", errors.New("you are not the author")))
return
}
}
// Delete regardless of existence (idempotent)
if err := DeleteCommentModel([]uint{id}); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
c.JSON(http.StatusOK, gin.H{"comment": "delete success"})
}
func ArticleCommentList(c *gin.Context) {
slug := c.Param("slug")
articleModel, err := FindOneArticle(&ArticleModel{Slug: slug})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("comments", errors.New("Invalid slug")))
return
}
err = articleModel.getComments()
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("comments", errors.New("Database error")))
return
}
serializer := CommentsSerializer{c, articleModel.Comments}
c.JSON(http.StatusOK, gin.H{"comments": serializer.Response()})
}
func TagList(c *gin.Context) {
tagModels, err := getAllTags()
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("articles", errors.New("Invalid param")))
return
}
serializer := TagsSerializer{c, tagModels}
c.JSON(http.StatusOK, gin.H{"tags": serializer.Response()})
}

View File

@@ -0,0 +1,180 @@
package articles
import (
"sort"
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/users"
)
type TagSerializer struct {
C *gin.Context
TagModel
}
type TagsSerializer struct {
C *gin.Context
Tags []TagModel
}
func (s *TagSerializer) Response() string {
return s.TagModel.Tag
}
func (s *TagsSerializer) Response() []string {
response := []string{}
for _, tag := range s.Tags {
serializer := TagSerializer{C: s.C, TagModel: tag}
response = append(response, serializer.Response())
}
return response
}
type ArticleUserSerializer struct {
C *gin.Context
ArticleUserModel
}
func (s *ArticleUserSerializer) Response() users.ProfileResponse {
response := users.ProfileSerializer{C: s.C, UserModel: s.ArticleUserModel.UserModel}
return response.Response()
}
type ArticleSerializer struct {
C *gin.Context
ArticleModel
}
type ArticleResponse struct {
ID uint `json:"-"`
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
Body string `json:"body"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Author users.ProfileResponse `json:"author"`
Tags []string `json:"tagList"`
Favorite bool `json:"favorited"`
FavoritesCount uint `json:"favoritesCount"`
}
type ArticlesSerializer struct {
C *gin.Context
Articles []ArticleModel
}
func (s *ArticleSerializer) Response() ArticleResponse {
myUserModel := s.C.MustGet("my_user_model").(users.UserModel)
authorSerializer := ArticleUserSerializer{C: s.C, ArticleUserModel: s.Author}
response := ArticleResponse{
ID: s.ID,
Slug: s.Slug,
Title: s.Title,
Description: s.Description,
Body: s.Body,
CreatedAt: s.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z"),
//UpdatedAt: s.UpdatedAt.UTC().Format(time.RFC3339Nano),
UpdatedAt: s.UpdatedAt.UTC().Format("2006-01-02T15:04:05.999Z"),
Author: authorSerializer.Response(),
Favorite: s.isFavoriteBy(GetArticleUserModel(myUserModel)),
FavoritesCount: s.favoritesCount(),
}
response.Tags = make([]string, 0)
for _, tag := range s.Tags {
serializer := TagSerializer{C: s.C, TagModel: tag}
response.Tags = append(response.Tags, serializer.Response())
}
sort.Strings(response.Tags)
return response
}
// ResponseWithPreloaded creates response using preloaded favorite data to avoid N+1 queries
func (s *ArticleSerializer) ResponseWithPreloaded(favorited bool, favoritesCount uint) ArticleResponse {
authorSerializer := ArticleUserSerializer{C: s.C, ArticleUserModel: s.Author}
response := ArticleResponse{
ID: s.ID,
Slug: s.Slug,
Title: s.Title,
Description: s.Description,
Body: s.Body,
CreatedAt: s.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z"),
UpdatedAt: s.UpdatedAt.UTC().Format("2006-01-02T15:04:05.999Z"),
Author: authorSerializer.Response(),
Favorite: favorited,
FavoritesCount: favoritesCount,
}
response.Tags = make([]string, 0)
for _, tag := range s.Tags {
serializer := TagSerializer{C: s.C, TagModel: tag}
response.Tags = append(response.Tags, serializer.Response())
}
sort.Strings(response.Tags)
return response
}
func (s *ArticlesSerializer) Response() []ArticleResponse {
response := []ArticleResponse{}
if len(s.Articles) == 0 {
return response
}
// Batch fetch favorite counts and status
var articleIDs []uint
for _, article := range s.Articles {
articleIDs = append(articleIDs, article.ID)
}
favoriteCounts := BatchGetFavoriteCounts(articleIDs)
myUserModel := s.C.MustGet("my_user_model").(users.UserModel)
articleUserModel := GetArticleUserModel(myUserModel)
favoriteStatus := BatchGetFavoriteStatus(articleIDs, articleUserModel.ID)
for _, article := range s.Articles {
serializer := ArticleSerializer{C: s.C, ArticleModel: article}
favorited := favoriteStatus[article.ID]
count := favoriteCounts[article.ID]
response = append(response, serializer.ResponseWithPreloaded(favorited, count))
}
return response
}
type CommentSerializer struct {
C *gin.Context
CommentModel
}
type CommentsSerializer struct {
C *gin.Context
Comments []CommentModel
}
type CommentResponse struct {
ID uint `json:"id"`
Body string `json:"body"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Author users.ProfileResponse `json:"author"`
}
func (s *CommentSerializer) Response() CommentResponse {
authorSerializer := ArticleUserSerializer{C: s.C, ArticleUserModel: s.Author}
response := CommentResponse{
ID: s.ID,
Body: s.Body,
CreatedAt: s.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z"),
UpdatedAt: s.UpdatedAt.UTC().Format("2006-01-02T15:04:05.999Z"),
Author: authorSerializer.Response(),
}
return response
}
func (s *CommentsSerializer) Response() []CommentResponse {
response := []CommentResponse{}
for _, comment := range s.Comments {
serializer := CommentSerializer{C: s.C, CommentModel: comment}
response = append(response, serializer.Response())
}
return response
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
package articles
import (
"github.com/gin-gonic/gin"
"github.com/gosimple/slug"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/users"
)
type ArticleModelValidator struct {
Article struct {
Title string `form:"title" json:"title" binding:"required,min=4"`
Description string `form:"description" json:"description" binding:"required,max=2048"`
Body string `form:"body" json:"body" binding:"required,max=2048"`
Tags []string `form:"tagList" json:"tagList"`
} `json:"article"`
articleModel ArticleModel `json:"-"`
}
func NewArticleModelValidator() ArticleModelValidator {
return ArticleModelValidator{}
}
func NewArticleModelValidatorFillWith(articleModel ArticleModel) ArticleModelValidator {
articleModelValidator := NewArticleModelValidator()
articleModelValidator.Article.Title = articleModel.Title
articleModelValidator.Article.Description = articleModel.Description
articleModelValidator.Article.Body = articleModel.Body
for _, tagModel := range articleModel.Tags {
articleModelValidator.Article.Tags = append(articleModelValidator.Article.Tags, tagModel.Tag)
}
return articleModelValidator
}
func (s *ArticleModelValidator) Bind(c *gin.Context) error {
myUserModel := c.MustGet("my_user_model").(users.UserModel)
err := common.Bind(c, s)
if err != nil {
return err
}
s.articleModel.Slug = slug.Make(s.Article.Title)
s.articleModel.Title = s.Article.Title
s.articleModel.Description = s.Article.Description
s.articleModel.Body = s.Article.Body
s.articleModel.Author = GetArticleUserModel(myUserModel)
s.articleModel.setTags(s.Article.Tags)
return nil
}
type CommentModelValidator struct {
Comment struct {
Body string `form:"body" json:"body" binding:"required,max=2048"`
} `json:"comment"`
commentModel CommentModel `json:"-"`
}
func NewCommentModelValidator() CommentModelValidator {
return CommentModelValidator{}
}
func (s *CommentModelValidator) Bind(c *gin.Context) error {
myUserModel := c.MustGet("my_user_model").(users.UserModel)
err := common.Bind(c, s)
if err != nil {
return err
}
s.commentModel.Body = s.Comment.Body
s.commentModel.Author = GetArticleUserModel(myUserModel)
return nil
}