diff --git a/internal/core/storage.go b/internal/core/storage.go index f1aa7ce7..e158c675 100644 --- a/internal/core/storage.go +++ b/internal/core/storage.go @@ -7,6 +7,9 @@ import ( // ObjectStorageService storage service interface that implement base AliOSS、MINIO or other type ObjectStorageService interface { PutObject(objectKey string, reader io.Reader, objectSize int64, contentType string) (string, error) + DeleteObject(objectKey string) error + DeleteObjcets(objectKeys []string) error + IsObjectExist(objectKey string) (bool, error) SignURL(objectKey string, expiredInSec int64) (string, error) ObjectURL(objetKey string) string ObjectKey(cUrl string) string diff --git a/internal/core/tweets.go b/internal/core/tweets.go index 356ebef0..1b454c6d 100644 --- a/internal/core/tweets.go +++ b/internal/core/tweets.go @@ -25,7 +25,7 @@ type TweetService interface { type TweetManageService interface { CreateAttachment(attachment *model.Attachment) (*model.Attachment, error) CreatePost(post *model.Post) (*model.Post, error) - DeletePost(post *model.Post) error + DeletePost(post *model.Post) ([]string, error) LockPost(post *model.Post) error StickPost(post *model.Post) error VisiblePost(post *model.Post, visibility model.PostVisibleT) error diff --git a/internal/dao/jinzhu/tweets.go b/internal/dao/jinzhu/tweets.go index da8be500..e26da66c 100644 --- a/internal/dao/jinzhu/tweets.go +++ b/internal/dao/jinzhu/tweets.go @@ -170,12 +170,85 @@ func (s *tweetManageServant) CreatePost(post *model.Post) (*model.Post, error) { return p, nil } -func (s *tweetManageServant) DeletePost(post *model.Post) error { - if err := post.Delete(s.db); err != nil { - return err +func (s *tweetManageServant) DeletePost(post *model.Post) ([]string, error) { + var mediaContents []string + + postId := post.ID + postContent := &model.PostContent{} + err := s.db.Transaction( + func(tx *gorm.DB) error { + if contents, err := postContent.MediaContentsByPostId(tx, postId); err == nil { + mediaContents = contents + } else { + return err + } + + // 删推文 + if err := (post).Delete(tx); err != nil { + return err + } + + // 删内容 + if err := postContent.DeleteByPostId(tx, postId); err != nil { + return err + } + + // 删评论 + if contents, err := s.deleteCommentByPostId(tx, postId); err == nil { + mediaContents = append(mediaContents, contents...) + } else { + return err + } + + if tags := strings.Split(post.Tags, ","); len(tags) > 0 { + // 删tag,宽松处理错误,有错误不会回滚 + deleteTags(tx, tags) + } + + return nil + }, + ) + + if err != nil { + return nil, err } + s.cacheIndex.SendAction(core.IdxActDeletePost, post) - return nil + return mediaContents, nil +} + +func (s *tweetManageServant) deleteCommentByPostId(db *gorm.DB, postId int64) ([]string, error) { + comment := &model.Comment{} + commentContent := &model.CommentContent{} + + // 获取推文的所有评论id + commentIds, err := comment.CommentIdsByPostId(db, postId) + if err != nil { + return nil, err + } + + // 获取评论的媒体内容 + mediaContents, err := commentContent.MediaContentsByCommentId(db, commentIds) + if err != nil { + return nil, err + } + + // 删评论 + if err = comment.DeleteByPostId(db, postId); err != nil { + return nil, err + } + + // 删评论内容 + if err = commentContent.DeleteByCommentIds(db, commentIds); err != nil { + return nil, err + } + + // 删评论的评论 + if err = (&model.CommentReply{}).DeleteByCommentIds(db, commentIds); err != nil { + return nil, err + } + + return mediaContents, nil } func (s *tweetManageServant) LockPost(post *model.Post) error { diff --git a/internal/dao/jinzhu/utils.go b/internal/dao/jinzhu/utils.go index b4b211bf..8c866027 100644 --- a/internal/dao/jinzhu/utils.go +++ b/internal/dao/jinzhu/utils.go @@ -32,6 +32,24 @@ func deleteTag(db *gorm.DB, tag *model.Tag) error { return tag.Update(db) } +func deleteTags(db *gorm.DB, tags []string) error { + allTags, err := (&model.Tag{}).TagsFrom(db, tags) + if err != nil { + return err + } + for _, tag := range allTags { + tag.QuoteNum-- + if tag.QuoteNum < 0 { + tag.QuoteNum = 0 + } + // 宽松处理错误, 尽可能更新tag记录, 只记录最后一次错误 + if e := tag.Update(db); e != nil { + err = e + } + } + return err +} + // 根据IDs获取用户列表 func getUsersByIDs(db *gorm.DB, ids []int64) ([]*model.User, error) { user := &model.User{} diff --git a/internal/dao/storage/alioss.go b/internal/dao/storage/alioss.go index c9c9b754..48cdcec0 100644 --- a/internal/dao/storage/alioss.go +++ b/internal/dao/storage/alioss.go @@ -37,6 +37,19 @@ func (s *aliossServant) PutObject(objectKey string, reader io.Reader, objectSize return s.domain + objectKey, nil } +func (s *aliossServant) DeleteObject(objectKey string) error { + return s.bucket.DeleteObject(objectKey) +} + +func (s *aliossServant) DeleteObjcets(objectKeys []string) error { + _, err := s.bucket.DeleteObjects(objectKeys) + return err +} + +func (s *aliossServant) IsObjectExist(objectKey string) (bool, error) { + return s.bucket.IsObjectExist(objectKey) +} + func (s *aliossServant) SignURL(objectKey string, expiredInSec int64) (string, error) { signedURL, err := s.bucket.SignURL(objectKey, oss.HTTPGet, expiredInSec) if err != nil { diff --git a/internal/dao/storage/localoss.go b/internal/dao/storage/localoss.go index 09503b3a..81ab2435 100644 --- a/internal/dao/storage/localoss.go +++ b/internal/dao/storage/localoss.go @@ -57,6 +57,25 @@ func (s *localossServant) PutObject(objectKey string, reader io.Reader, objectSi return s.domain + objectKey, nil } +func (s *localossServant) DeleteObject(objectKey string) error { + return os.Remove(s.savePath + objectKey) +} + +func (s *localossServant) DeleteObjcets(objectKeys []string) (err error) { + // 宽松处理删除动作,尽可能删除所有objectKey,如果出错,只返回最后一个错误 + for _, objectKey := range objectKeys { + if e := os.Remove(s.savePath + objectKey); e != nil { + err = e + } + } + return +} + +func (s *localossServant) IsObjectExist(objectKey string) (bool, error) { + // TODO + return false, nil +} + func (s *localossServant) SignURL(objectKey string, expiredInSec int64) (string, error) { if expiredInSec < 0 { return "", fmt.Errorf("invalid expires: %d, expires must bigger than 0", expiredInSec) diff --git a/internal/dao/storage/minio.go b/internal/dao/storage/minio.go index 02f6abe3..e767a356 100644 --- a/internal/dao/storage/minio.go +++ b/internal/dao/storage/minio.go @@ -43,6 +43,37 @@ func (s *minioServant) PutObject(objectKey string, reader io.Reader, objectSize return s.domain + objectKey, nil } +func (s *minioServant) DeleteObject(objectKey string) error { + return s.client.RemoveObject(context.Background(), s.bucket, objectKey, minio.RemoveObjectOptions{ForceDelete: true}) +} + +func (s *minioServant) DeleteObjcets(objectKeys []string) (err error) { + objectsCh := make(chan minio.ObjectInfo, len(objectKeys)) + + resCh := s.client.RemoveObjects(context.Background(), s.bucket, objectsCh, minio.RemoveObjectsOptions{}) + for _, objectKey := range objectKeys { + objectsCh <- minio.ObjectInfo{ + Key: objectKey, + } + } + + // 宽松处理所有错误,只记录最后一次发生的错误 + for result := range resCh { + if result.Err != nil { + err = result.Err + } + } + return +} + +func (s *minioServant) IsObjectExist(objectKey string) (bool, error) { + _, err := s.client.StatObject(context.Background(), s.bucket, objectKey, minio.StatObjectOptions{}) + if err != nil { + return false, err + } + return true, nil +} + func (s *minioServant) SignURL(objectKey string, expiredInSec int64) (string, error) { // TODO: Set request parameters for content-disposition. reqParams := make(url.Values) diff --git a/internal/model/comment.go b/internal/model/comment.go index 35cfbe74..8b54798a 100644 --- a/internal/model/comment.go +++ b/internal/model/comment.go @@ -108,7 +108,19 @@ func (c *Comment) Create(db *gorm.DB) (*Comment, error) { } func (c *Comment) Delete(db *gorm.DB) error { - return db.Model(&Comment{}).Where("id = ? AND is_del = ?", c.Model.ID, 0).Updates(map[string]interface{}{ + return db.Model(c).Where("id = ?", c.Model.ID).Updates(map[string]interface{}{ + "deleted_on": time.Now().Unix(), + "is_del": 1, + }).Error +} + +func (c *Comment) CommentIdsByPostId(db *gorm.DB, postId int64) (ids []int64, err error) { + err = db.Model(c).Where("post_id = ?", postId).Select("id").Find(&ids).Error + return +} + +func (c *Comment) DeleteByPostId(db *gorm.DB, postId int64) error { + return db.Model(c).Where("post_id = ?", postId).Updates(map[string]interface{}{ "deleted_on": time.Now().Unix(), "is_del": 1, }).Error diff --git a/internal/model/comment_content.go b/internal/model/comment_content.go index 65e3f654..543cacab 100644 --- a/internal/model/comment_content.go +++ b/internal/model/comment_content.go @@ -1,6 +1,10 @@ package model -import "gorm.io/gorm" +import ( + "time" + + "gorm.io/gorm" +) type CommentContent struct { *Model @@ -41,3 +45,15 @@ func (c *CommentContent) Create(db *gorm.DB) (*CommentContent, error) { return c, err } + +func (c *CommentContent) MediaContentsByCommentId(db *gorm.DB, commentIds []int64) (contents []string, err error) { + err = db.Model(c).Where("comment_id IN ? AND type = ?", commentIds, CONTENT_TYPE_IMAGE).Select("content").Find(&contents).Error + return +} + +func (c *CommentContent) DeleteByCommentIds(db *gorm.DB, commentIds []int64) error { + return db.Model(c).Where("comment_id IN ?", commentIds).Updates(map[string]interface{}{ + "deleted_on": time.Now().Unix(), + "is_del": 1, + }).Error +} diff --git a/internal/model/comment_reply.go b/internal/model/comment_reply.go index 5bc5fde5..d144e6c9 100644 --- a/internal/model/comment_reply.go +++ b/internal/model/comment_reply.go @@ -101,3 +101,10 @@ func (c *CommentReply) Delete(db *gorm.DB) error { "is_del": 1, }).Error } + +func (c *CommentReply) DeleteByCommentIds(db *gorm.DB, commentIds []int64) error { + return db.Model(c).Where("comment_id IN ?", commentIds).Updates(map[string]interface{}{ + "deleted_on": time.Now().Unix(), + "is_del": 1, + }).Error +} diff --git a/internal/model/post.go b/internal/model/post.go index 7b19350f..1aaedaf7 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -91,7 +91,7 @@ func (p *Post) Create(db *gorm.DB) (*Post, error) { } func (s *Post) Delete(db *gorm.DB) error { - return db.Model(&Post{}).Where("id = ? AND is_del = ?", s.Model.ID, 0).Updates(map[string]interface{}{ + return db.Model(s).Where("id = ?", s.Model.ID).Updates(map[string]interface{}{ "deleted_on": time.Now().Unix(), "is_del": 1, }).Error diff --git a/internal/model/post_content.go b/internal/model/post_content.go index f00b769b..ed7544ce 100644 --- a/internal/model/post_content.go +++ b/internal/model/post_content.go @@ -1,6 +1,10 @@ package model -import "gorm.io/gorm" +import ( + "time" + + "gorm.io/gorm" +) // 类型,1标题,2文字段落,3图片地址,4视频地址,5语音地址,6链接地址,7附件资源 @@ -17,6 +21,14 @@ const ( CONTENT_TYPE_CHARGE_ATTACHMENT ) +var mediaContentType = []PostContentT{ + CONTENT_TYPE_IMAGE, + CONTENT_TYPE_VIDEO, + CONTENT_TYPE_AUDIO, + CONTENT_TYPE_ATTACHMENT, + CONTENT_TYPE_CHARGE_ATTACHMENT, +} + type PostContent struct { *Model PostID int64 `json:"post_id"` @@ -33,6 +45,18 @@ type PostContentFormated struct { Sort int64 `json:"sort"` } +func (p *PostContent) DeleteByPostId(db *gorm.DB, postId int64) error { + return db.Model(p).Where("post_id = ?", postId).Updates(map[string]interface{}{ + "deleted_on": time.Now().Unix(), + "is_del": 1, + }).Error +} + +func (p *PostContent) MediaContentsByPostId(db *gorm.DB, postId int64) (contents []string, err error) { + err = db.Model(p).Where("post_id = ? AND type IN ?", postId, mediaContentType).Select("content").Find(&contents).Error + return +} + func (p *PostContent) Create(db *gorm.DB) (*PostContent, error) { err := db.Create(&p).Error diff --git a/internal/model/tag.go b/internal/model/tag.go index 59e03bde..4389c075 100644 --- a/internal/model/tag.go +++ b/internal/model/tag.go @@ -1,6 +1,10 @@ package model -import "gorm.io/gorm" +import ( + "time" + + "gorm.io/gorm" +) type Tag struct { *Model @@ -56,6 +60,13 @@ func (t *Tag) Update(db *gorm.DB) error { return db.Model(&Tag{}).Where("id = ? AND is_del = ?", t.Model.ID, 0).Save(t).Error } +func (t *Tag) Delete(db *gorm.DB) error { + return db.Model(t).Where("id = ?", t.Model.ID).Updates(map[string]interface{}{ + "deleted_on": time.Now().Unix(), + "is_del": 1, + }).Error +} + func (t *Tag) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*Tag, error) { var tags []*Tag var err error @@ -79,3 +90,8 @@ func (t *Tag) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]* return tags, nil } + +func (t *Tag) TagsFrom(db *gorm.DB, tags []string) (res []*Tag, err error) { + err = db.Where("tag IN ?", tags).Find(&res).Error + return +} diff --git a/internal/routers/api/post.go b/internal/routers/api/post.go index 7f70a046..900f04e2 100644 --- a/internal/routers/api/post.go +++ b/internal/routers/api/post.go @@ -92,22 +92,13 @@ func DeletePost(c *gin.Context) { return } - user, _ := c.Get("USER") - - // 获取Post - postFormated, err := service.GetPost(param.ID) - if err != nil { - logrus.Errorf("service.GetPost err: %v\n", err) - response.ToErrorResponse(errcode.GetPostFailed) - return - } - - if postFormated.UserID != user.(*model.User).ID && !user.(*model.User).IsAdmin { + user, exist := userFrom(c) + if !exist { response.ToErrorResponse(errcode.NoPermission) return } - err = service.DeletePost(param.ID) + err := service.DeletePost(user, param.ID) if err != nil { logrus.Errorf("service.DeletePost err: %v\n", err) response.ToErrorResponse(errcode.DeletePostFailed) diff --git a/internal/service/post.go b/internal/service/post.go index dcdf90bf..678f27c6 100644 --- a/internal/service/post.go +++ b/internal/service/post.go @@ -180,23 +180,36 @@ func CreatePost(c *gin.Context, userID int64, param PostCreationReq) (*model.Pos return post, nil } -func DeletePost(id int64) error { - post, _ := ds.GetPostByID(id) - - // tag删除 - tags := strings.Split(post.Tags, ",") - for _, t := range tags { - - tag := &model.Tag{ - Tag: t, - } - ds.DeleteTag(tag) +func DeletePost(user *model.User, id int64) *errcode.Error { + if user == nil { + return errcode.NoPermission } - err := ds.DeletePost(post) + post, err := ds.GetPostByID(id) + if err != nil { + return errcode.GetPostFailed + } + if post.UserID != user.ID && !user.IsAdmin { + return errcode.NoPermission + } + mediaContents, err := ds.DeletePost(post) if err != nil { - return err + logrus.Errorf("service.DeletePost delete post failed: %s", err) + return errcode.DeletePostFailed + } + + // 删除推文的媒体内容, 宽松处理错误(就是不处理) + mediaContentsSize := len(mediaContents) + if mediaContentsSize > 1 { + objectKeys := make([]string, 0, mediaContentsSize) + for _, cUrl := range mediaContents { + objectKeys = append(objectKeys, oss.ObjectKey(cUrl)) + } + // TODO: 优化处理尽量使用channel传递objectKeys使用可控数量的Goroutine集中处理object删除动作,后续完善 + go oss.DeleteObjcets(objectKeys) + } else if mediaContentsSize == 1 { + oss.DeleteObject(oss.ObjectKey(mediaContents[0])) } // 删除索引 diff --git a/internal/service/service.go b/internal/service/service.go index 268a2558..7a1ad7ce 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -9,11 +9,13 @@ import ( var ( ds core.DataService ts core.TweetSearchService + oss core.ObjectStorageService DisablePhoneVerify bool ) func Initialize() { ds = dao.NewDataService() ts = dao.NewTweetSearchService() + oss = dao.NewObjectStorageService() DisablePhoneVerify = !conf.CfgIf("Sms") }