369 lines
10 KiB
Go
369 lines
10 KiB
Go
|
|
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
|
||
|
|
}
|