package service import ( "context" "sort" "go-common/app/interface/openplatform/article/dao" "go-common/app/interface/openplatform/article/model" "go-common/library/database/sql" "go-common/library/ecode" "go-common/library/log" "go-common/library/sync/errgroup" ) const ( _sortDefault = 0 _sortByCtime = 1 _sortByLike = 2 _sortByReply = 3 _sortByView = 4 _sortByFavorite = 5 _sortByCoin = 6 //_stateCancel 取消活动 // _stateCancel = -1 //_stateJoin 参加活动 _stateJoin = 0 _editTimes = 1 ) // checkPrivilege check that whether user has permission to write article. func (s *Service) checkPrivilege(c context.Context, mid int64) (err error) { if res, _, e := s.IsAuthor(c, mid); !res { err = ecode.ArtCreationNoPrivilege if e != nil { dao.PromError("creation:检查作者权限") } } return } func (s *Service) checkArtAuthor(c context.Context, aid, mid int64) (am *model.Meta, err error) { if am, err = s.creationArticleMeta(c, aid); err != nil { return } else if am == nil { err = ecode.NothingFound return } if am.Author.Mid != mid { err = ecode.ArtCreationMIDErr } return } // CreationArticle get creation article func (s *Service) creationArticleMeta(c context.Context, aid int64) (am *model.Meta, err error) { if am, err = s.dao.CreationArticleMeta(c, aid); err != nil { dao.PromError("creation:获取文章meta") return } else if am == nil { return } if s.categoriesMap[am.Category.ID] != nil { am.Category = s.categoriesMap[am.Category.ID] } var author *model.Author if author, _ = s.author(c, am.Author.Mid); author != nil { am.Author = author } return } // CreationArticle . func (s *Service) CreationArticle(c context.Context, aid, mid int64) (a *model.Article, err error) { if err = s.checkPrivilege(c, mid); err != nil { return } var ( content string am *model.Meta ) a = &model.Article{} if am, err = s.checkArtAuthor(c, aid, mid); err != nil { return } if am.State == model.StateRePending || am.State == model.StateReReject { if a, err = s.ArticleVersion(c, aid); err != nil { return } a.Author = am.Author a.Category = s.categoriesMap[a.Category.ID] a.List, _ = s.dao.ArtList(c, aid) a.Stats, _ = s.stat(c, aid) return } group, errCtx := errgroup.WithContext(c) group.Go(func() (err error) { content, err = s.dao.CreationArticleContent(c, aid) return }) group.Go(func() (err error) { am.Stats, _ = s.stat(errCtx, aid) return }) group.Go(func() (err error) { am.Tags, _ = s.Tags(errCtx, aid, true) return }) group.Go(func() (err error) { am.List, _ = s.dao.ArtList(errCtx, aid) return }) if err = group.Wait(); err != nil { return } a.Meta = am a.Content = content log.Info("s.CreationArticle() aid(%d) title(%s) content length(%d)", a.ID, a.Title, len(a.Content)) return } // AddArticle adds article. func (s *Service) AddArticle(c context.Context, a *model.Article, actID, listID int64, ip string) (id int64, err error) { log.Info("s.AddArticle() aid(%d) title(%s) actID(%d) content length(%d)", a.ID, a.Title, actID, len(a.Content)) defer func() { if err != nil && err != ecode.CreativeArticleCanNotRepeat { s.dao.DelSubmitCache(c, a.Author.Mid, a.Title) } }() if err = s.checkPrivilege(c, a.Author.Mid); err != nil { return } a.Content = xssFilter(a.Content) if err = s.preArticleCheck(c, a); err != nil { return } var num int if num, err = s.ArticleRemainCount(c, a.Author.Mid); err != nil { return } else if num <= 0 { err = ecode.ArtCreationArticleFull return } if s.checkList(c, a.Author.Mid, listID); err != nil { return } a.State = model.StatePending var tx *sql.Tx if tx, err = s.dao.BeginTran(c); err != nil { log.Error("tx.BeginTran() error(%+v)", err) return } defer func() { if err != nil { if err1 := tx.Rollback(); err1 != nil { log.Error("tx.Rollback() error(%+v)", err1) } return } if err = tx.Commit(); err != nil { dao.PromError("creation:添加文章") log.Error("tx.Commit() error(%+v)", err) return } // id is article id if e1 := s.creativeAddArticleList(c, a.Meta.Author.Mid, listID, id, false); e1 != nil { dao.PromError("creation:添加文章绑定文集") log.Errorv(c, log.KV("log", "creativeAddArticleList"), log.KV("mid", a.Meta.Author.Mid), log.KV("listID", listID), log.KV("article_id", id), log.KV("error", err)) } }() // del draft if err = s.dao.TxDeleteArticleDraft(c, tx, a.Author.Mid, a.ID); err != nil { dao.PromError("creation:删除草稿") return } if id, err = s.dao.TxAddArticleMeta(c, tx, a.Meta, actID); err != nil { dao.PromError("creation:删除文章meta") return } keywords, _ := s.Segment(c, int32(id), a.Content, 1, "article") if err = s.dao.TxAddArticleContent(c, tx, id, a.Content, keywords); err != nil { dao.PromError("creation:添加文章content") return } // add version if err = s.dao.TxAddArticleVersion(c, tx, id, a, actID); err != nil { dao.PromError("creation:添加历史记录") return } if actID > 0 { if e := s.dao.HandleActivity(c, a.Author.Mid, id, actID, _stateJoin, ip); e != nil { log.Error("creation: s.act.HandleActivity mid(%d) aid(%d) actID(%d) ip(%s) error(%+v)", a.Author.Mid, id, actID, ip, e) } } var tags []string for _, t := range a.Tags { tags = append(tags, t.Name) } if err = s.BindTags(c, a.Meta.Author.Mid, id, tags, ip, actID); err != nil { dao.PromError("creation:发布文章绑定tag") } return } // UpdateArticle update article. func (s *Service) UpdateArticle(c context.Context, a *model.Article, actID, listID int64, ip string) (err error) { log.Info("s.UpdateArticle() aid(%d) title(%s) content length(%d)", a.ID, a.Title, len(a.Content)) if err = s.checkPrivilege(c, a.Author.Mid); err != nil { return } a.Content = xssFilter(a.Content) if err = s.preArticleCheck(c, a); err != nil { return } a.State = model.StatePending if a.ID <= 0 { err = ecode.ArtCreationIDErr return } if _, err = s.checkArtAuthor(c, a.ID, a.Author.Mid); err != nil { return } //这里转换成 -2 2 几种状态 if err = s.convertState(c, a); err != nil { return } if a.State == model.StateRePending { err = s.updateArticleVersion(c, a, actID) return } if err = s.updateArticleDB(c, a); err != nil { return } var tags []string for _, t := range a.Tags { tags = append(tags, t.Name) } s.BindTags(c, a.Meta.Author.Mid, a.Meta.ID, tags, ip, actID) s.CreativeUpdateArticleList(c, a.Meta.Author.Mid, a.ID, listID, false) return } func (s *Service) updateArticleVersion(c context.Context, a *model.Article, actID int64) (err error) { var tx *sql.Tx if tx, err = s.dao.BeginTran(c); err != nil { log.Error("updateArticleVersion.tx.BeginTran() error(%+v)", err) return } defer func() { if err != nil { if err1 := tx.Rollback(); err1 != nil { log.Error("updateArticleVersion.tx.Rollback() error(%+v)", err1) } return } if err = tx.Commit(); err != nil { log.Error("updateArticleVersion.tx.Commit() error(%+v)", err) return } }() if err = s.dao.TxUpdateArticleStateApplyTime(c, tx, a.ID, a.State); err != nil { dao.PromError("creation:修改文章状态") return } if x, e := s.ArticleVersion(c, a.ID); e == nil && x.ID != 0 { if err = s.dao.TxUpdateArticleVersion(c, tx, a.ID, a, actID); err != nil { dao.PromError("creation:更新版本") } } else { if err = s.dao.TxAddArticleVersion(c, tx, a.ID, a, actID); err != nil { dao.PromError("creation:创建版本") } } // if err = s.dao.TxUpdateArticleVersion(c, tx, a.ID, a, actID); err != nil { // dao.PromError("creation:更新版本") // } return } func (s *Service) updateArticleDB(c context.Context, a *model.Article) (err error) { var tx *sql.Tx if tx, err = s.dao.BeginTran(c); err != nil { log.Error("tx.BeginTran() error(%+v)", err) return } if err = s.dao.TxUpdateArticleMeta(c, tx, a.Meta); err != nil { if err1 := tx.Rollback(); err1 != nil { dao.PromError("creation:更新文章meta") log.Error("tx.Rollback() error(%+v)", err1) } return } keywords, _ := s.Segment(c, int32(a.ID), a.Content, 1, "article") if err = s.dao.TxUpdateArticleContent(c, tx, a.ID, a.Content, keywords); err != nil { if err1 := tx.Rollback(); err1 != nil { dao.PromError("creation:更新文章content") log.Error("tx.Rollback() error(%+v)", err1) } return } // add version if err = s.dao.TxUpdateArticleVersion(c, tx, a.ID, a, 0); err != nil { dao.PromError("creation:添加历史记录") return } if err = tx.Commit(); err != nil { dao.PromError("creation:更新文章") log.Error("tx.Commit() error(%+v)", err) } return } // DelArticle drops article. func (s *Service) DelArticle(c context.Context, aid, mid int64) (err error) { if aid <= 0 { err = ecode.ArtCreationIDErr return } if err = s.checkPrivilege(c, mid); err != nil { return } var a *model.Meta if a, err = s.checkArtAuthor(c, aid, mid); err != nil { return } // can not delelte article which state is pending. if a.State == model.StatePending || a.State == model.StateOpenPending { err = ecode.ArtCreationDelPendingErr return } lists, _ := s.dao.RawArtsListID(c, []int64{aid}) if err = s.delArticle(c, aid); err != nil { return } s.dao.DelActivity(c, aid, "") cache.Save(func() { c := context.TODO() s.dao.DelRecommend(c, aid) s.DelRecommendArtCache(c, aid, a.Category.ID) s.DelArticleCache(c, mid, aid) s.deleteArtsListCache(c, aid) if lists[aid] > 0 { s.updateListInfo(c, lists[aid]) s.RebuildListCache(c, lists[aid]) } }) return } func (s *Service) delArticle(c context.Context, aid int64) (err error) { var tx *sql.Tx if tx, err = s.dao.BeginTran(c); err != nil { log.Error("tx.BeginTran() error(%+v)", err) return } defer func() { if err != nil { if err1 := tx.Rollback(); err1 != nil { log.Error("tx.Rollback() error(%+v)", err1) } return } if err = tx.Commit(); err != nil { dao.PromError("creation:删除文章") log.Error("tx.Commit() error(%+v)", err) } }() if err = s.dao.TxDeleteArticleMeta(c, tx, aid); err != nil { dao.PromError("creation:删除文章meta") return } if err = s.dao.TxDeleteArticleContent(c, tx, aid); err != nil { dao.PromError("creation:delete文章content") return } if err = s.dao.TxDelFilteredArtMeta(c, tx, aid); err != nil { dao.PromError("creation:删除过滤文章meta") return } if err = s.dao.TxDelFilteredArtContent(c, tx, aid); err != nil { dao.PromError("creation:删除过滤文章content") return } if err = s.dao.TxDelArticleList(tx, aid); err != nil { return } if err = s.dao.TxDelArticleVersion(c, tx, aid); err != nil { return } return } // CreationUpperArticlesMeta gets article list by mid. func (s *Service) CreationUpperArticlesMeta(c context.Context, mid int64, group, category, sortType, pn, ps int, ip string) (res *model.CreationArts, err error) { res = &model.CreationArts{} if err = s.checkPrivilege(c, mid); err != nil { return } var ( aids []int64 ams, as []*model.Meta stats = make(map[int64]*model.Stats) start = (pn - 1) * ps end = start + ps - 1 total int ) res.Page = &model.ArtPage{ Pn: pn, Ps: ps, } eg, errCtx := errgroup.WithContext(c) eg.Go(func() (err error) { res.Type, _ = s.dao.UpperArticlesTypeCount(errCtx, mid) return }) eg.Go(func() (err error) { if ams, err = s.dao.UpperArticlesMeta(errCtx, mid, group, category); err != nil { return } total = len(ams) res.Page.Total = total return }) if err = eg.Wait(); err != nil { log.Error("eg.Wait() error(%+v)", err) return } if total == 0 || total < start { return } for _, v := range ams { aids = append(aids, v.ID) } if stats, err = s.stats(c, aids); err != nil { dao.PromError("creation:获取计数信息") log.Error("s.stats(%v) err: %+v", aids, err) err = nil } for _, v := range ams { // stats if st := stats[v.ID]; st != nil { v.Stats = st } else { v.Stats = new(model.Stats) } } switch sortType { case _sortDefault, _sortByCtime: sort.Slice(ams, func(i, j int) bool { return ams[i].Ctime > ams[j].Ctime }) case _sortByLike: sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Like > ams[j].Stats.Like }) case _sortByReply: sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Reply > ams[j].Stats.Reply }) case _sortByView: sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.View > ams[j].Stats.View }) case _sortByFavorite: sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Favorite > ams[j].Stats.Favorite }) case _sortByCoin: sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Coin > ams[j].Stats.Coin }) } if total > end { as = ams[start : end+1] } else { as = ams[start:] } for _, v := range as { var pid int64 if pid, err = s.CategoryToRoot(v.Category.ID); err != nil { dao.PromError("creation:获取根分区") log.Error("s.CategoryToRoot(%d) error(%+v)", v.Category.ID, err) continue } v.Category = s.categoriesMap[pid] v.List, _ = s.dao.ArtList(c, v.ID) } res.Articles = as return } // convertState converts -1 to 1, -2 to 2 if the article had been published. func (s *Service) convertState(c context.Context, a *model.Article) (err error) { var am *model.Meta if am, err = s.dao.CreationArticleMeta(c, a.ID); err != nil { return } if am.State == model.StateOpen || am.State == model.StateRePass || am.State == model.StateReReject { if s.EditTimes(c, a.ID) <= 0 { err = ecode.ArtUpdateFullErr return } a.State = model.StateRePending return } if (am.State != model.StateReject) && (am.State != model.StateOpenReject) { err = ecode.ArtCannotEditErr return } if am.State > 0 && a.State < 0 { a.State = -a.State } return } // CreationWithdrawArticle recall the article and add it to draft. func (s *Service) CreationWithdrawArticle(c context.Context, mid, aid int64) (err error) { if err = s.checkPrivilege(c, mid); err != nil { return } var am *model.Meta if am, err = s.checkArtAuthor(c, aid, mid); err != nil { return } // 只有初次提交待审的文章才可以撤回,发布后的修改待审不允许撤回 if am.State != model.StatePending { err = ecode.ArtCreationStateErr return } var content string if content, err = s.dao.CreationArticleContent(c, aid); err != nil { return } // add draft var tags []string if ts, e := s.Tags(c, aid, false); e == nil && len(ts) > 0 { for _, v := range ts { tags = append(tags, v.Name) } } am.ID = 0 draft := &model.Draft{Article: &model.Article{Meta: am, Content: content}, Tags: tags} if _, err = s.AddArtDraft(c, draft); err != nil { return } // delete article err = s.delArticle(c, aid) // err = s.dao.UpdateArticleState(c, aid, model.StateOpen) return } // UpStat up stat func (s *Service) UpStat(c context.Context, mid int64) (res model.UpStat, err error) { return s.dao.UpStat(c, mid) } // UpThirtyDayStat for 30 days stat. func (s *Service) UpThirtyDayStat(c context.Context, mid int64) (res []*model.ThirtyDayArticle, err error) { res, err = s.dao.ThirtyDayArticle(c, mid) return } // ArticleUpCover article upload cover. func (s *Service) ArticleUpCover(c context.Context, fileType string, body []byte) (url string, err error) { if len(body) == 0 { err = ecode.FileNotExists return } if len(body) > s.c.BFS.MaxFileSize { err = ecode.FileTooLarge return } url, err = s.dao.UploadImage(c, fileType, body) if err != nil { log.Error("creation: s.bfs.Upload error(%v)", err) } return } // ArticleVersion . func (s *Service) ArticleVersion(c context.Context, aid int64) (a *model.Article, err error) { if a, err = s.dao.ArticleVersion(c, aid); err != nil { return } a.Category = s.categoriesMap[a.Category.ID] return } // EditTimes . func (s *Service) EditTimes(c context.Context, id int64) (res int) { var ( et = _editTimes count = et err error ) if s.c.Article.EditTimes != 0 { et = s.c.Article.EditTimes } if count, err = s.dao.EditTimes(c, id); err != nil { return } res = et - count if res < 0 { res = 0 } return } func (s *Service) lastReason(c context.Context, id int64, state int32) (res string, err error) { return s.dao.LastReason(c, id, state) }