sqlx: prepare implement tweets data logic for sqlx feature

pull/351/head
Michael Li 2 years ago
parent 9ab97d7950
commit 914889994f
No known key found for this signature in database

@ -0,0 +1,7 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// Package cs contain core data service interface type
// model define
package cs

@ -0,0 +1,38 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package cs
const (
// 搜索查询类型
TsQueryTypeDefault TsQueryType = "search"
TsQueryTypeTag TsQueryType = "tag"
)
type (
// TsQueryType 搜索查询类型
TsQueryType string
// TsDocList 索引条陈列表
TsDocList []TsDocItem
)
// TsQueryReq 搜索查询请求
type TsQueryReq struct {
Query string
Visibility []TweetVisibleType
Type TsQueryType
}
// TsQueryResp 搜索查询响应
type TsQueryResp struct {
Items TweetList
Total int64
}
// TsDocItem 索引条陈
type TsDocItem struct {
Post *TweetInfo
Content string
}

@ -0,0 +1,11 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package cs
// TweetBox 推文列表盒子,包含其他一些关于推文列表的信息
type TweetBox struct {
Tweets TweetList
Total int64
}

@ -0,0 +1,49 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package cs
const (
// 标签类型
TagTypeHot TagType = "hot"
TagTypeNew TagType = "new"
)
type (
// TagType 标签类型
TagType string
// TagInfoList 标签信息列表
TagInfoList []*TagInfo
// TagList 标签列表
TagList []*TagItem
)
// TagInfo 标签信息
type TagInfo struct {
ID int64 `json:"id" db:"id"`
UserID int64 `json:"user_id" db:"user_id"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num" db:"quote_num"`
}
// TagItem 标签信息条陈
type TagItem struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserInfo `json:"user" db:"u"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
}
func (t *TagInfo) Format() *TagItem {
return &TagItem{
ID: t.ID,
UserID: t.UserID,
User: &UserInfo{},
Tag: t.Tag,
QuoteNum: t.QuoteNum,
}
}

@ -0,0 +1,12 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package cs
type AttachmentBill struct {
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
PaidAmount int64 `json:"paid_amount"`
}

@ -0,0 +1,141 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package cs
const (
// 推文内容分快类型
TweetBlockTitle TweetBlockType = iota + 1
TweetBlockText
TweetBlockImage
TweetBlockVideo
TweetBlockAudio
TweetBlockLink
TweetBlockAttachment
TweetBlockChargeAttachment
// 推文可见性
TweetVisitPublic TweetVisibleType = iota
TweetVisitPrivate
TweetVisitFriend
TweetVisitInvalid
// 附件类型
AttachmentTypeImage AttachmentType = iota + 1
AttachmentTypeVideo
AttachmentTypeOther
)
type (
// TweetBlockType 推文内容分块类型1标题2文字段落3图片地址4视频地址5语音地址6链接地址7附件资源
// TODO: 优化一下类型为 uint8 需要底层数据库同步修改
TweetBlockType int
// TweetVisibleType 推文可见性0公开1私密2好友
TweetVisibleType uint8
// AttachmentType 附件类型, 1图片 2视频 3其他
// TODO: 优化一下类型为 uint8 需要底层数据库同步修改
AttachmentType int
// TweetList 推文列表
TweetList []*TweetItem
// TweetInfoList 推文信息列表
TweetInfoList []*TweetInfo
// FavoriteList 收藏列表
FavoriteList []*FavoriteItem
// ReactionList 点赞列表
ReactionList []*ReactionItem
// TweetBlockList 推文分块列表
TweetBlockList []*TweetBlock
)
// TweetBlock 推文分块
type TweetBlock struct {
ID int64 `json:"id" binding:"-"`
PostID int64 `json:"post_id" binding:"-"`
Content string `json:"content" binding:"required"`
Type TweetBlockType `json:"type" binding:"required"`
Sort int64 `json:"sort" binding:"required"`
}
// TweetInfo 推文信息
type TweetInfo struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
UpvoteCount int64 `json:"upvote_count"`
Visibility TweetVisibleType `json:"visibility"`
IsTop int `json:"is_top"`
IsEssence int `json:"is_essence"`
IsLock int `json:"is_lock"`
LatestRepliedOn int64 `json:"latest_replied_on"`
Tags string `json:"tags"`
AttachmentPrice int64 `json:"attachment_price"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
}
// TweetItem 一条推文信息
type TweetItem struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserInfo `json:"user"`
Contents []*TweetBlock `json:"contents"`
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
UpvoteCount int64 `json:"upvote_count"`
Visibility TweetVisibleType `json:"visibility"`
IsTop int `json:"is_top"`
IsEssence int `json:"is_essence"`
IsLock int `json:"is_lock"`
LatestRepliedOn int64 `json:"latest_replied_on"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
Tags map[string]int8 `json:"tags"`
AttachmentPrice int64 `json:"attachment_price"`
IPLoc string `json:"ip_loc"`
}
type Attachment struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FileSize int64 `json:"file_size"`
ImgWidth int `json:"img_width"`
ImgHeight int `json:"img_height"`
Type AttachmentType `json:"type"`
Content string `json:"content"`
}
// Favorite 收藏
type FavoriteItem struct {
ID int64 `json:"id"`
Tweet *TweetInfo `json:"-"`
TweetID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
}
// Reaction 反应、表情符号, 点赞、喜欢等
type ReactionItem struct {
ID int64 `json:"id"`
Tweet *TweetInfo `json:"-"`
TweetID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
}
type NewTweetReq struct {
Contents TweetBlockList `json:"contents" binding:"required"`
Tags []string `json:"tags" binding:"required"`
Users []string `json:"users" binding:"required"`
AttachmentPrice int64 `json:"attachment_price"`
Visibility TweetVisibleType `json:"visibility"`
ClientIP string `json:"-" binding:"-"`
}

@ -0,0 +1,20 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package cs
type (
// UserInfoList 用户信息列表
UserInfoList []*UserInfo
)
// UserInfo 用户基本信息
type UserInfo struct {
ID int64 `json:"id"`
Nickname string `json:"nickname"`
Username string `json:"username"`
Status int `json:"status"`
Avatar string `json:"avatar"`
IsAdmin bool `json:"is_admin"`
}

@ -21,6 +21,7 @@ const (
)
type (
// PostVisibleT 可访问类型0公开1私密2好友
PostVisibleT = dbr.PostVisibleT
SearchType string

@ -0,0 +1,14 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package core
import (
"github.com/rocboss/paopao-ce/internal/core/cs"
)
// TweetTimelineService 广场首页推文时间线服务
type TweetTimelineService interface {
TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error)
}

@ -4,42 +4,14 @@
package core
const (
TagCategoryHot TagCategory = "hot"
TagCategoryNew TagCategory = "new"
import (
"github.com/rocboss/paopao-ce/internal/core/cs"
)
type TagCategory string
type Tag struct {
ID int64 `json:"id" db:"id"`
UserID int64 `json:"user_id" db:"user_id"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num" db:"quote_num"`
}
type TagFormated struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
}
// TopicService 话题服务
type TopicService interface {
UpsertTags(userId int64, tags []string) ([]*Tag, error)
UpsertTags(userId int64, tags []string) (cs.TagInfoList, error)
DecrTagsById(ids []int64) error
GetTags(category TagCategory, offset int, limit int) ([]*Tag, error)
GetTagsByKeyword(keyword string) ([]*Tag, error)
}
func (t *Tag) Format() *TagFormated {
return &TagFormated{
ID: t.ID,
UserID: t.UserID,
User: &UserFormated{},
Tag: t.Tag,
QuoteNum: t.QuoteNum,
}
ListTags(typ cs.TagType, offset int, limit int) (cs.TagList, error)
TagsByKeyword(keyword string) (cs.TagInfoList, error)
}

@ -5,6 +5,7 @@
package core
import (
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
)
@ -39,8 +40,7 @@ type (
}
)
// TweetService 推文检索服务
type TweetService interface {
type oldTweetService interface {
GetPostByID(id int64) (*Post, error)
GetPosts(conditions *ConditionsT, offset, limit int) ([]*Post, error)
GetPostCount(conditions *ConditionsT) (int64, error)
@ -55,9 +55,21 @@ type TweetService interface {
GetPostContentByID(id int64) (*PostContent, error)
}
// TweetManageService 推文管理服务,包括创建/删除/更新推文
type TweetManageService interface {
CreateAttachment(attachment *Attachment) (*Attachment, error)
// TweetService 推文检索服务
type TweetService interface {
oldTweetService
TweetInfoById(id int64) (*cs.TweetInfo, error)
TweetItemById(id int64) (*cs.TweetItem, error)
UserTweets(visitorId, userId int64) (cs.TweetList, error)
ReactionByTweetId(userId int64, tweetId int64) (*cs.ReactionItem, error)
UserReactions(userId int64, offset int, limit int) (cs.ReactionList, error)
FavoriteByTweetId(userId int64, tweetId int64) (*cs.FavoriteItem, error)
UserFavorites(userId int64, offset int, limit int) (cs.FavoriteList, error)
AttachmentByTweetId(userId int64, tweetId int64) (*cs.AttachmentBill, error)
}
type oldTweetManageService interface {
CreatePost(post *Post) (*Post, error)
DeletePost(post *Post) ([]string, error)
LockPost(post *Post) error
@ -71,13 +83,41 @@ type TweetManageService interface {
CreatePostContent(content *PostContent) (*PostContent, error)
}
// TweetHelpService 推文辅助服务
type TweetHelpService interface {
// TweetManageService 推文管理服务,包括创建/删除/更新推文
type TweetManageService interface {
oldTweetManageService
CreateAttachment(obj *cs.Attachment) (int64, error)
CreateTweet(userId int64, req *cs.NewTweetReq) (*cs.TweetItem, error)
DeleteTweet(userId int64, tweetId int64) ([]string, error)
LockTweet(userId int64, tweetId int64) error
StickTweet(userId int64, tweetId int64) error
VisibleTweet(userId int64, visibility cs.TweetVisibleType) error
CreateReaction(userId int64, tweetId int64) error
DeleteReaction(userId int64, reactionId int64) error
CreateFavorite(userId int64, tweetId int64) error
DeleteFavorite(userId int64, favoriteId int64) error
}
type oldTweetHelpService interface {
RevampPosts(posts []*PostFormated) ([]*PostFormated, error)
MergePosts(posts []*Post) ([]*PostFormated, error)
}
// TweetHelpService 推文辅助服务
type TweetHelpService interface {
oldTweetHelpService
RevampTweets(tweets cs.TweetList) (cs.TweetList, error)
MergeTweets(tweets cs.TweetInfo) (cs.TweetList, error)
}
type oldIndexPostsService interface {
IndexPosts(user *User, offset int, limit int) (*IndexTweetList, error)
}
// IndexPostsService 广场首页推文列表服务
type IndexPostsService interface {
IndexPosts(user *User, offset int, limit int) (*IndexTweetList, error)
oldIndexPostsService
TweetTimelineService
}

@ -15,6 +15,8 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/allegro/bigcache/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/pkg/debug"
"github.com/rocboss/paopao-ce/pkg/types"
"github.com/sirupsen/logrus"
)
@ -56,6 +58,11 @@ func (s *bigCacheIndexServant) IndexPosts(user *core.User, offset int, limit int
return posts, nil
}
func (s *bigCacheIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *bigCacheIndexServant) getPosts(key string) (*core.IndexTweetList, error) {
data, err := s.cache.Get(key)
if err != nil {

@ -7,6 +7,8 @@ package cache
import (
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/pkg/debug"
)
var (
@ -22,6 +24,11 @@ func (s *noneCacheIndexServant) IndexPosts(user *core.User, offset int, limit in
return s.ips.IndexPosts(user, offset, limit)
}
func (s *noneCacheIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *noneCacheIndexServant) SendAction(_act core.IdxAct, _post *core.Post) {
// empty
}

@ -10,6 +10,8 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/pkg/debug"
"github.com/sirupsen/logrus"
)
@ -47,6 +49,11 @@ func (s *simpleCacheIndexServant) IndexPosts(user *core.User, offset int, limit
return s.ips.IndexPosts(user, offset, limit)
}
func (s *simpleCacheIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *simpleCacheIndexServant) SendAction(act core.IdxAct, _post *core.Post) {
select {
case s.indexActionCh <- act:

@ -4,7 +4,10 @@
package dbr
import "gorm.io/gorm"
import (
"github.com/rocboss/paopao-ce/internal/core/cs"
"gorm.io/gorm"
)
const (
UserStatusNormal int = iota + 1
@ -87,6 +90,11 @@ func (u *User) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]
return users, nil
}
func (u *User) ListUserInfoById(db *gorm.DB, ids []int64) (res cs.UserInfoList, err error) {
err = db.Model(u).Where("id IN ?", ids).Find(&res).Error
return
}
func (u *User) Create(db *gorm.DB) (*User, error) {
err := db.Create(&u).Error

@ -6,6 +6,7 @@ package jinzhu
import (
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
"github.com/rocboss/paopao-ce/pkg/debug"
"github.com/sirupsen/logrus"
@ -75,12 +76,22 @@ func (s *friendIndexServant) IndexPosts(user *core.User, offset int, limit int)
}, nil
}
func (s *friendIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
// IndexPosts 根据userId查询广场推文列表
func (s *followIndexServant) IndexPosts(user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *followIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
// IndexPosts 根据userId查询广场推文列表获取公开可见Tweet或者所属用户的私有Tweet
func (s *lightIndexServant) IndexPosts(user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
predicates := dbr.Predicates{
@ -114,6 +125,11 @@ func (s *lightIndexServant) IndexPosts(user *core.User, offset int, limit int) (
}, nil
}
func (s *lightIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
// simpleCacheIndexGetPosts simpleCacheIndex 专属获取广场推文列表函数
func (s *simpleIndexPostsServant) IndexPosts(_user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
predicates := dbr.Predicates{
@ -143,6 +159,11 @@ func (s *simpleIndexPostsServant) IndexPosts(_user *core.User, offset int, limit
}, nil
}
func (s *simpleIndexPostsServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func newFriendIndexService(db *gorm.DB, ams core.AuthorizationManageService, ths core.TweetHelpService) core.IndexPostsService {
return &friendIndexServant{
ams: ams,

@ -8,6 +8,7 @@ import (
"strings"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
"gorm.io/gorm"
)
@ -26,7 +27,7 @@ func newTopicService(db *gorm.DB) core.TopicService {
}
}
func (s *topicServant) UpsertTags(userId int64, tags []string) (_ []*core.Tag, err error) {
func (s *topicServant) UpsertTags(userId int64, tags []string) (_ cs.TagInfoList, err error) {
db := s.db.Begin()
defer func() {
if err == nil {
@ -50,36 +51,55 @@ func (s *topicServant) DecrTagsById(ids []int64) (err error) {
return decrTagsByIds(db, ids)
}
func (s *topicServant) GetTags(category core.TagCategory, offset, limit int) (res []*core.Tag, err error) {
func (s *topicServant) ListTags(typ cs.TagType, offset, limit int) (res cs.TagList, err error) {
conditions := &core.ConditionsT{}
switch category {
case core.TagCategoryHot:
switch typ {
case cs.TagTypeHot:
// 热门标签
conditions = &core.ConditionsT{
"ORDER": "quote_num DESC",
}
case core.TagCategoryNew:
case cs.TagTypeNew:
// 最新标签
conditions = &core.ConditionsT{
"ORDER": "id DESC",
}
}
// TODO: 优化查询方式,直接返回[]*core.Tag, 目前保持先转换一下
var tags []*dbr.Tag
var (
tags []*dbr.Tag
tagMap map[int64]*cs.TagItem
item *cs.TagItem
)
if tags, err = (&dbr.Tag{}).List(s.db, conditions, offset, limit); err == nil {
if len(tags) == 0 {
return
}
ids := make([]int64, 0, len(tags))
for _, tag := range tags {
res = append(res, &core.Tag{
item = &cs.TagItem{
ID: tag.ID,
UserID: tag.UserID,
Tag: tag.Tag,
QuoteNum: tag.QuoteNum,
})
}
tagMap[item.UserID] = item
res = append(res, item)
ids = append(ids, tag.UserID)
}
userInfos, err := (&dbr.User{}).ListUserInfoById(s.db, ids)
if err != nil {
return nil, err
}
for _, userInfo := range userInfos {
item = tagMap[userInfo.ID]
item.User = userInfo
}
}
return
}
func (s *topicServant) GetTagsByKeyword(keyword string) (res []*core.Tag, err error) {
func (s *topicServant) TagsByKeyword(keyword string) (res cs.TagInfoList, err error) {
keyword = "%" + strings.Trim(keyword, " ") + "%"
tag := &dbr.Tag{}
var tags []*dbr.Tag
@ -95,7 +115,7 @@ func (s *topicServant) GetTagsByKeyword(keyword string) (res []*core.Tag, err er
}
if err == nil {
for _, tag := range tags {
res = append(res, &core.Tag{
res = append(res, &cs.TagInfo{
ID: tag.ID,
UserID: tag.UserID,
Tag: tag.Tag,

@ -9,7 +9,9 @@ import (
"time"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
"github.com/rocboss/paopao-ce/pkg/debug"
"gorm.io/gorm"
)
@ -128,6 +130,16 @@ func (s *tweetHelpServant) RevampPosts(posts []*core.PostFormated) ([]*core.Post
return posts, nil
}
func (s *tweetHelpServant) RevampTweets(tweets cs.TweetList) (cs.TweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetHelpServant) MergeTweets(tweets cs.TweetInfo) (cs.TweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetHelpServant) getPostContentsByIDs(ids []int64) ([]*dbr.PostContent, error) {
return (&dbr.PostContent{}).List(s.db, &dbr.ConditionsT{
"post_id IN ?": ids,
@ -160,8 +172,9 @@ func (s *tweetManageServant) CreatePostContent(content *core.PostContent) (*core
return content.Create(s.db)
}
func (s *tweetManageServant) CreateAttachment(attachment *core.Attachment) (*core.Attachment, error) {
return attachment.Create(s.db)
func (s *tweetManageServant) CreateAttachment(obj *cs.Attachment) (int64, error) {
// TODO
return 0, debug.ErrNotImplemented
}
func (s *tweetManageServant) CreatePost(post *core.Post) (*core.Post, error) {
@ -323,6 +336,51 @@ func (s *tweetManageServant) DeletePostStar(p *core.PostStar) error {
return p.Delete(s.db)
}
func (s *tweetManageServant) CreateTweet(userId int64, req *cs.NewTweetReq) (*cs.TweetItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetManageServant) DeleteTweet(userId int64, tweetId int64) ([]string, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetManageServant) LockTweet(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) StickTweet(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) VisibleTweet(userId int64, visibility cs.TweetVisibleType) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) CreateReaction(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) DeleteReaction(userId int64, reactionId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) CreateFavorite(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) DeleteFavorite(userId int64, favoriteId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetServant) GetPostByID(id int64) (*core.Post, error) {
post := &dbr.Post{
Model: &dbr.Model{
@ -430,3 +488,43 @@ func (s *tweetServant) GetPostContentByID(id int64) (*core.PostContent, error) {
},
}).Get(s.db)
}
func (s *tweetServant) TweetInfoById(id int64) (*cs.TweetInfo, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) TweetItemById(id int64) (*cs.TweetItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) UserTweets(visitorId, userId int64) (cs.TweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) ReactionByTweetId(userId int64, tweetId int64) (*cs.ReactionItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) UserReactions(userId int64, offset int, limit int) (cs.ReactionList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) FavoriteByTweetId(userId int64, tweetId int64) (*cs.FavoriteItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) UserFavorites(userId int64, offset int, limit int) (cs.FavoriteList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) AttachmentByTweetId(userId int64, tweetId int64) (*cs.AttachmentBill, error) {
// TODO
return nil, debug.ErrNotImplemented
}

@ -70,33 +70,6 @@ func (s *userManageServant) GetUsersByKeyword(keyword string) ([]*core.User, err
}
}
func (s *userManageServant) GetTagsByKeyword(keyword string) (res []*core.Tag, err error) {
tag := &dbr.Tag{}
keyword = "%" + strings.Trim(keyword, " ") + "%"
var tags []*dbr.Tag
if keyword == "%%" {
tags, err = tag.List(s.db, &dbr.ConditionsT{
"ORDER": "quote_num DESC",
}, 0, 6)
} else {
tags, err = tag.List(s.db, &dbr.ConditionsT{
"tag LIKE ?": keyword,
"ORDER": "quote_num DESC",
}, 0, 6)
}
if err == nil {
for _, tag := range tags {
res = append(res, &core.Tag{
ID: tag.ID,
UserID: tag.UserID,
Tag: tag.Tag,
QuoteNum: tag.QuoteNum,
})
}
}
return
}
func (s *userManageServant) CreateUser(user *dbr.User) (*core.User, error) {
return user.Create(s.db)
}

@ -5,12 +5,12 @@
package jinzhu
import (
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
"gorm.io/gorm"
)
func createTags(db *gorm.DB, userId int64, tags []string) (res []*core.Tag, err error) {
func createTags(db *gorm.DB, userId int64, tags []string) (res cs.TagInfoList, err error) {
for _, name := range tags {
tag := &dbr.Tag{Tag: name}
if tag, err = tag.Get(db); err == nil {
@ -28,7 +28,7 @@ func createTags(db *gorm.DB, userId int64, tags []string) (res []*core.Tag, err
return
}
}
res = append(res, &core.Tag{
res = append(res, &cs.TagInfo{
ID: tag.ID,
UserID: tag.UserID,
Tag: tag.Tag,

@ -7,6 +7,7 @@ package sakila
import (
"github.com/jmoiron/sqlx"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/pkg/debug"
)
@ -48,24 +49,44 @@ func (s *friendIndexServant) IndexPosts(user *core.User, offset int, limit int)
return nil, debug.ErrNotImplemented
}
func (s *friendIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
// IndexPosts 根据userId查询广场推文列表简单做到不同用户的主页都是不同的
func (s *followIndexServant) IndexPosts(user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *followIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
// IndexPosts 根据userId查询广场推文列表简单做到不同用户的主页都是不同的
func (s *lightIndexServant) IndexPosts(user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *lightIndexServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
// simpleCacheIndexGetPosts simpleCacheIndex 专属获取广场推文列表函数
func (s *simpleIndexPostsServant) IndexPosts(_user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *simpleIndexPostsServant) TweetTimeline(userId int64, offset int, limit int) (*cs.TweetBox, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func newFriendIndexService(db *sqlx.DB, ams core.AuthorizationManageService, ths core.TweetHelpService) core.IndexPostsService {
return &friendIndexServant{
ams: ams,

@ -10,6 +10,7 @@ import (
"github.com/jmoiron/sqlx"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
)
var (
@ -30,12 +31,12 @@ type topicServant struct {
sqlIncrTagsById string
}
func (s *topicServant) UpsertTags(userId int64, tags []string) (res []*core.Tag, xerr error) {
func (s *topicServant) UpsertTags(userId int64, tags []string) (res cs.TagInfoList, xerr error) {
if len(tags) == 0 {
return nil, nil
}
xerr = s.with(func(tx *sqlx.Tx) error {
var upTags []*core.Tag
var upTags cs.TagInfoList
if err := s.inSelect(tx, &upTags, s.sqlTagsForIncr, tags); err != nil {
return err
}
@ -77,7 +78,7 @@ func (s *topicServant) UpsertTags(userId int64, tags []string) (res []*core.Tag,
}
ids = append(ids, id)
}
var newTags []*core.Tag
var newTags cs.TagInfoList
if err := s.inSelect(tx, &newTags, s.sqlTagsByIdB, ids); err != nil {
return err
}
@ -99,17 +100,17 @@ func (s *topicServant) DecrTagsById(ids []int64) error {
})
}
func (s *topicServant) GetTags(category core.TagCategory, offset int, limit int) (res []*core.Tag, err error) {
switch category {
case core.TagCategoryHot:
func (s *topicServant) ListTags(typ cs.TagType, offset int, limit int) (res cs.TagList, err error) {
switch typ {
case cs.TagTypeHot:
err = s.stmtHotTags.Select(&res, offset, limit)
case core.TagCategoryNew:
case cs.TagTypeNew:
err = s.stmtNewestTags.Select(&res, offset, limit)
}
return
}
func (s *topicServant) GetTagsByKeyword(keyword string) (res []*core.Tag, err error) {
func (s *topicServant) TagsByKeyword(keyword string) (res cs.TagInfoList, err error) {
keyword = "%" + strings.Trim(keyword, " ") + "%"
if keyword == "%%" {
err = s.stmtTagsByKeywordA.Select(&res)
@ -122,8 +123,8 @@ func (s *topicServant) GetTagsByKeyword(keyword string) (res []*core.Tag, err er
func newTopicService(db *sqlx.DB) core.TopicService {
return &topicServant{
sqlxServant: newSqlxServant(db),
stmtNewestTags: c(`SELECT id, user_id, tag, quote_num FROM @tag WHERE is_del = 0 AND quote_num > 0 ORDER BY id DESC OFFSET ? LIMIT ?`),
stmtHotTags: c(`SELECT id, user_id, tag, quote_num FROM @tag WHERE is_del = 0 AND quote_num > 0 ORDER BY quote_num DESC OFFSET ? LIMIT ?`),
stmtNewestTags: c(`SELECT t.id id, t.user_id user_id, t.tag tag, t.quote_num quote_num, u.id, u.nickname, u.username, u.status, u.avatar, u.is_admin FROM @tag t JOIN @user u ON t.user_id = u.id WHERE t.is_del = 0 AND t.quote_num > 0 ORDER BY t.id DESC OFFSET ? LIMIT ?`),
stmtHotTags: c(`SELECT t.id id, t.user_id user_id, t.tag tag, t.quote_num quote_num, u.id, u.nickname, u.username, u.status, u.avatar, u.is_admin FROM @tag t JOIN @user u ON t.user_id = u.id WHERE t.is_del = 0 AND t.quote_num > 0 ORDER BY t.quote_num DESC OFFSET ? LIMIT ?`),
stmtTagsByKeywordA: c(`SELECT id, user_id, tag, quote_num FROM @tag WHERE is_del = 0 ORDER BY quote_num DESC OFFSET 0 LIMIT 6`),
stmtTagsByKeywordB: c(`SELECT id, user_id, tag, quote_num FROM @tag WHERE is_del = 0 AND tag LIKE ? ORDER BY quote_num DESC OFFSET 0 LIMIT 6`),
stmtInsertTag: c(`INSERT INTO @tag (user_id, tag, created_on, modified_on, quote_num) VALUES (?, ?, ?, ?, 1)`),

@ -7,6 +7,7 @@ package sakila
import (
"github.com/jmoiron/sqlx"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
"github.com/rocboss/paopao-ce/pkg/debug"
"gorm.io/gorm"
@ -54,6 +55,16 @@ func (s *tweetHelpServant) RevampPosts(posts []*core.PostFormated) ([]*core.Post
return nil, nil
}
func (s *tweetHelpServant) RevampTweets(tweets cs.TweetList) (cs.TweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetHelpServant) MergeTweets(tweets cs.TweetInfo) (cs.TweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetHelpServant) getPostContentsByIDs(ids []int64) ([]*dbr.PostContent, error) {
// TODO
debug.NotImplemented()
@ -84,10 +95,9 @@ func (s *tweetManageServant) CreatePostContent(content *core.PostContent) (*core
return nil, nil
}
func (s *tweetManageServant) CreateAttachment(attachment *core.Attachment) (*core.Attachment, error) {
func (s *tweetManageServant) CreateAttachment(obj *cs.Attachment) (int64, error) {
// TODO
debug.NotImplemented()
return nil, nil
return 0, debug.ErrNotImplemented
}
func (s *tweetManageServant) CreatePost(post *core.Post) (*core.Post, error) {
@ -144,6 +154,51 @@ func (s *tweetManageServant) DeletePostStar(p *core.PostStar) error {
return nil
}
func (s *tweetManageServant) CreateTweet(userId int64, req *cs.NewTweetReq) (*cs.TweetItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetManageServant) DeleteTweet(userId int64, tweetId int64) ([]string, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetManageServant) LockTweet(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) StickTweet(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) VisibleTweet(userId int64, visibility cs.TweetVisibleType) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) CreateReaction(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) DeleteReaction(userId int64, reactionId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) CreateFavorite(userId int64, tweetId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetManageServant) DeleteFavorite(userId int64, favoriteId int64) error {
// TODO
return debug.ErrNotImplemented
}
func (s *tweetServant) GetPostByID(id int64) (*core.Post, error) {
// TODO
debug.NotImplemented()
@ -228,6 +283,46 @@ func (s *tweetServant) GetPostContentByID(id int64) (*core.PostContent, error) {
return nil, nil
}
func (s *tweetServant) TweetInfoById(id int64) (*cs.TweetInfo, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) TweetItemById(id int64) (*cs.TweetItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) UserTweets(visitorId, userId int64) (cs.TweetList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) ReactionByTweetId(userId int64, tweetId int64) (*cs.ReactionItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) UserReactions(userId int64, offset int, limit int) (cs.ReactionList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) FavoriteByTweetId(userId int64, tweetId int64) (*cs.FavoriteItem, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) UserFavorites(userId int64, offset int, limit int) (cs.FavoriteList, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func (s *tweetServant) AttachmentByTweetId(userId int64, tweetId int64) (*cs.AttachmentBill, error) {
// TODO
return nil, debug.ErrNotImplemented
}
func newTweetService(db *sqlx.DB) core.TweetService {
return &tweetServant{
sqlxServant: newSqlxServant(db),

@ -51,12 +51,6 @@ func (s *userManageServant) GetUsersByKeyword(keyword string) ([]*core.User, err
return nil, nil
}
func (s *userManageServant) GetTagsByKeyword(keyword string) ([]*core.Tag, error) {
// TODO
debug.NotImplemented()
return nil, nil
}
func (s *userManageServant) CreateUser(user *core.User) (*core.User, error) {
// TODO
debug.NotImplemented()

@ -10,6 +10,7 @@ import (
"strings"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
)
type PostContentItem struct {
@ -120,12 +121,12 @@ type UploadAttachmentReq struct {
}
type UploadAttachmentResp struct {
UserID int64 `json:"user_id"`
FileSize int64 `json:"file_size"`
ImgWidth int `json:"img_width"`
ImgHeight int `json:"img_height"`
Type core.AttachmentType `json:"type"`
Content string `json:"content"`
UserID int64 `json:"user_id"`
FileSize int64 `json:"file_size"`
ImgWidth int `json:"img_width"`
ImgHeight int `json:"img_height"`
Type cs.AttachmentType `json:"type"`
Content string `json:"content"`
}
type DownloadAttachmentPrecheckReq struct {

@ -6,11 +6,12 @@ package web
import (
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/servants/base"
"github.com/rocboss/paopao-ce/pkg/debug"
)
type TagType = core.TagCategory
type TagType = cs.TagType
type TweetDetailReq struct {
TweetId int64 `form:"id"`
@ -34,7 +35,7 @@ type TopicListReq struct {
// TopicListResp 主题返回值
// TODO: 优化内容定义
type TopicListResp struct {
Topics []*core.TagFormated `json:"topics"`
Topics cs.TagList `json:"topics"`
}
type GetCaptchaResp struct {

@ -5,9 +5,9 @@
package broker
import (
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
)
func CreateAttachment(attachment *core.Attachment) (*core.Attachment, error) {
return ds.CreateAttachment(attachment)
func CreateAttachment(obj *cs.Attachment) (int64, error) {
return ds.CreateAttachment(obj)
}

@ -14,12 +14,13 @@ import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/pkg/errcode"
"github.com/rocboss/paopao-ce/pkg/util"
"github.com/sirupsen/logrus"
)
type TagType = core.TagCategory
type TagType = cs.TagType
type PostListReq struct {
Conditions *core.ConditionsT
@ -529,36 +530,16 @@ func PushPostsToSearch(c *gin.Context) {
}
}
func GetPostTags(param *PostTagsReq) ([]*core.TagFormated, error) {
func GetPostTags(param *PostTagsReq) (cs.TagList, error) {
num := param.Num
if num > conf.AppSetting.MaxPageSize {
num = conf.AppSetting.MaxPageSize
}
tags, err := ds.GetTags(core.TagCategory(param.Type), 0, num)
tags, err := ds.ListTags(param.Type, 0, num)
if err != nil {
return nil, err
}
// 获取创建者User IDs
userIds := []int64{}
for _, tag := range tags {
userIds = append(userIds, tag.UserID)
}
users, _ := ds.GetUsersByIDs(userIds)
tagsFormated := []*core.TagFormated{}
for _, tag := range tags {
tagFormated := tag.Format()
for _, user := range users {
if user.ID == tagFormated.UserID {
tagFormated.User = user.Format()
}
}
tagsFormated = append(tagsFormated, tagFormated)
}
return tagsFormated, nil
return tags, nil
}
func CheckPostAttachmentIsPaid(postID, userID int64) bool {

@ -461,7 +461,7 @@ func GetSuggestUsers(keyword string) ([]string, error) {
// GetSuggestTags 根据关键词获取标签推荐
func GetSuggestTags(keyword string) ([]string, error) {
tags, err := ds.GetTagsByKeyword(keyword)
tags, err := ds.TagsByKeyword(keyword)
if err != nil {
return nil, err
}

@ -361,7 +361,7 @@ func (s *coreSrv) ChangePassword(req *web.ChangePasswordReq) mir.Error {
}
func (s *coreSrv) SuggestTags(req *web.SuggestTagsReq) (*web.SuggestTagsResp, mir.Error) {
tags, err := s.Ds.GetTagsByKeyword(req.Keyword)
tags, err := s.Ds.TagsByKeyword(req.Keyword)
if err != nil {
logrus.Errorf("Ds.GetTagsByKeyword err: %s", err)
return nil, xerror.ServerError

@ -16,6 +16,7 @@ import (
api "github.com/rocboss/paopao-ce/auto/api/v1"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/model/web"
"github.com/rocboss/paopao-ce/internal/servants/base"
"github.com/rocboss/paopao-ce/internal/servants/chain"
@ -30,11 +31,11 @@ var (
_ api.PrivBinding = (*privBinding)(nil)
_ api.PrivRender = (*privRender)(nil)
_uploadAttachmentTypeMap = map[string]core.AttachmentType{
"public/image": core.AttachmentTypeImage,
"public/avatar": core.AttachmentTypeImage,
"public/video": core.AttachmentTypeVideo,
"attachment": core.AttachmentTypeOther,
_uploadAttachmentTypeMap = map[string]cs.AttachmentType{
"public/image": cs.AttachmentTypeImage,
"public/avatar": cs.AttachmentTypeImage,
"public/video": cs.AttachmentTypeVideo,
"attachment": cs.AttachmentTypeOther,
}
)
@ -156,20 +157,20 @@ func (s *privSrv) UploadAttachment(req *web.UploadAttachmentReq) (*web.UploadAtt
}
// 构造附件Model
attachment := &core.Attachment{
attachment := &cs.Attachment{
UserID: req.Uid,
FileSize: req.FileSize,
Content: objectUrl,
Type: _uploadAttachmentTypeMap[req.UploadType],
}
if attachment.Type == core.AttachmentTypeImage {
if attachment.Type == cs.AttachmentTypeImage {
var src image.Image
src, err = imaging.Decode(req.File)
if err == nil {
attachment.ImgWidth, attachment.ImgHeight = getImageSize(src.Bounds())
}
}
attachment, err = s.Ds.CreateAttachment(attachment)
attachment.ID, err = s.Ds.CreateAttachment(attachment)
if err != nil {
logrus.Errorf("Ds.CreateAttachment err: %s", err)
return nil, _errFileUploadFailed

@ -75,28 +75,12 @@ func (s *pubSrv) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Error
if num > conf.AppSetting.MaxPageSize {
num = conf.AppSetting.MaxPageSize
}
tags, err := s.Ds.GetTags(req.Type, 0, num)
tags, err := s.Ds.ListTags(req.Type, 0, num)
if err != nil {
return nil, _errGetPostTagsFailed
}
// 获取创建者User IDs
userIds := []int64{}
for _, tag := range tags {
userIds = append(userIds, tag.UserID)
}
users, _ := s.Ds.GetUsersByIDs(userIds)
tagsFormated := []*core.TagFormated{}
for _, tag := range tags {
tagFormated := tag.Format()
for _, user := range users {
if user.ID == tagFormated.UserID {
tagFormated.User = user.Format()
}
}
tagsFormated = append(tagsFormated, tagFormated)
}
return &web.TopicListResp{
Topics: tagsFormated,
Topics: tags,
}, nil
}

@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/servants/web/broker"
"github.com/rocboss/paopao-ce/pkg/app"
"github.com/rocboss/paopao-ce/pkg/convert"
@ -18,11 +19,11 @@ import (
"github.com/sirupsen/logrus"
)
var uploadAttachmentTypeMap = map[string]core.AttachmentType{
"public/image": core.AttachmentTypeImage,
"public/avatar": core.AttachmentTypeImage,
"public/video": core.AttachmentTypeVideo,
"attachment": core.AttachmentTypeOther,
var uploadAttachmentTypeMap = map[string]cs.AttachmentType{
"public/image": cs.AttachmentTypeImage,
"public/avatar": cs.AttachmentTypeImage,
"public/video": cs.AttachmentTypeVideo,
"attachment": cs.AttachmentTypeOther,
}
func GeneratePath(s string) string {
@ -114,7 +115,7 @@ func UploadAttachment(c *gin.Context) {
}
// 构造附件Model
attachment := &core.Attachment{
attachment := &cs.Attachment{
FileSize: fileHeader.Size,
Content: objectUrl,
}
@ -124,7 +125,7 @@ func UploadAttachment(c *gin.Context) {
}
attachment.Type = uploadAttachmentTypeMap[uploadType]
if attachment.Type == core.AttachmentTypeImage {
if attachment.Type == cs.AttachmentTypeImage {
var src image.Image
src, err = imaging.Decode(file)
if err == nil {
@ -132,7 +133,7 @@ func UploadAttachment(c *gin.Context) {
}
}
attachment, err = broker.CreateAttachment(attachment)
attachment.ID, err = broker.CreateAttachment(attachment)
if err != nil {
logrus.Errorf("service.CreateAttachment err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)

Loading…
Cancel
Save