add list posts
This commit is contained in:
parent
518e1673fe
commit
98605573f3
|
@ -0,0 +1,5 @@
|
|||
use digimon_tweeting;
|
||||
db.post.createIndex({ "uid": 1, "create_time": 1 });
|
||||
db.post.createIndex({ "is_ad": 1, "create_time": 1 });
|
||||
db.post.createIndex({ "create_time": 1 });
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
db.post_likes.createIndex(
|
||||
{ "target_id": 1, "uid": 1, "type": 1 },
|
||||
{ unique: true }
|
||||
);
|
||||
db.post_likes.createIndex({ "create_time": 1 });
|
|
@ -81,14 +81,30 @@ message ListPostsResp {
|
|||
// 讚/不讚請求
|
||||
message LikeReq {
|
||||
string target_id = 1; // 目標ID(可以是貼文ID或評論ID)
|
||||
int64 user_id = 2; // 點讚的用戶ID
|
||||
string uid = 2; // 點讚的用戶ID
|
||||
int64 like_type = 3; // 讚或爛的類型
|
||||
}
|
||||
|
||||
message GetLikeStatusReq{
|
||||
string uid = 1; // 點讚的用戶ID
|
||||
repeated string target_id = 2; // 目標ID(可以是貼文ID或評論ID)
|
||||
int64 like_type = 3; // 讚或爛的類型
|
||||
}
|
||||
|
||||
message GetLikeStatusItem{
|
||||
string target_id = 1; // 目標ID(可以是貼文ID或評論ID)
|
||||
bool status = 2; // 是否有按過
|
||||
}
|
||||
|
||||
message GetLikeStatusResp{
|
||||
repeated GetLikeStatusItem data = 1; // 目標ID(可以是貼文ID或評論ID)
|
||||
}
|
||||
|
||||
|
||||
// 讚/不讚項目
|
||||
message LikeItem {
|
||||
string target_id = 1; // 目標ID(可以是貼文ID或評論ID)
|
||||
int64 user_id = 2; // 點讚的用戶ID
|
||||
string uid = 2; // 點讚的用戶ID
|
||||
int64 like_type = 3; // 讚或爛的類型
|
||||
}
|
||||
|
||||
|
@ -96,8 +112,8 @@ message LikeItem {
|
|||
message LikeListReq {
|
||||
string target_id = 1; // 目標ID(可以是貼文ID或評論ID)
|
||||
int64 like_type = 2; // 讚或爛的類型
|
||||
int32 page_index = 3; // 當前頁碼
|
||||
int32 page_size = 4; // 每頁顯示數量
|
||||
int64 page_index = 3; // 當前頁碼
|
||||
int64 page_size = 4; // 每頁顯示數量
|
||||
}
|
||||
|
||||
// 讚/不讚列表回應
|
||||
|
@ -172,7 +188,7 @@ service PostService {
|
|||
// Like 點讚/取消讚 貼文
|
||||
rpc Like(LikeReq) returns (OKResp);
|
||||
// GetLikeStatus 取得讚/不讚狀態
|
||||
rpc GetLikeStatus(LikeReq) returns (OKResp);
|
||||
rpc GetLikeStatus(GetLikeStatusReq) returns (GetLikeStatusResp);
|
||||
// LikeList 取得讚/不讚列表
|
||||
rpc LikeList(LikeListReq) returns (LikeListResp);
|
||||
// CountLike 取得讚/不讚數量
|
||||
|
|
|
@ -11,3 +11,14 @@ const (
|
|||
AdTypeOnlyAd
|
||||
AdTypeOnlyNotAd
|
||||
)
|
||||
|
||||
type LikeType int8
|
||||
|
||||
func (l LikeType) ToInt32() int8 {
|
||||
return int8(l)
|
||||
}
|
||||
|
||||
const (
|
||||
LikeTypeLike LikeType = iota + 1 // 按揍
|
||||
LikeTypeDisLike
|
||||
)
|
||||
|
|
|
@ -23,6 +23,8 @@ func NewGetLikeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
|
|||
}
|
||||
}
|
||||
|
||||
// 這個人按讚的文章列表(輸入UID 以及文章id,返回這個人有沒有對這些文章按讚)
|
||||
|
||||
// GetLikeStatus 取得讚/不讚狀態
|
||||
func (l *GetLikeStatusLogic) GetLikeStatus(in *tweeting.LikeReq) (*tweeting.OKResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package postservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-tweeting-service/internal/domain"
|
||||
model "app-cloudep-tweeting-service/internal/model/mongo"
|
||||
ers "code.30cm.net/digimon/library-go/errs"
|
||||
"context"
|
||||
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
|
@ -23,9 +26,60 @@ func NewLikeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeList
|
|||
}
|
||||
}
|
||||
|
||||
// 換句話說就是對這個文章按讚的人的列表
|
||||
|
||||
type likeListReq struct {
|
||||
Target string `json:"target" validate:"required"`
|
||||
LikeType domain.LikeType `json:"like_type" validate:"required,oneof=1 2"`
|
||||
PageSize int64 `json:"page_size" validate:"required"`
|
||||
PageIndex int64 `json:"page_index" validate:"required"`
|
||||
}
|
||||
|
||||
// LikeList 取得讚/不讚列表
|
||||
func (l *LikeListLogic) LikeList(in *tweeting.LikeListReq) (*tweeting.LikeListResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &tweeting.LikeListResp{}, nil
|
||||
// 驗證資料
|
||||
if err := l.svcCtx.Validate.ValidateAll(&likeListReq{
|
||||
Target: in.TargetId,
|
||||
LikeType: domain.LikeType(in.GetLikeType()),
|
||||
PageSize: in.GetPageSize(),
|
||||
PageIndex: in.GetPageIndex(),
|
||||
}); err != nil {
|
||||
return nil, ers.InvalidFormat(err.Error())
|
||||
}
|
||||
|
||||
result, total, err := l.svcCtx.PostLikeModel.FindLikeUsers(l.ctx, &model.QueryPostLikeReq{
|
||||
Target: in.GetTargetId(),
|
||||
LikeType: domain.LikeType(in.GetLikeType()),
|
||||
PageSize: in.GetPageSize(),
|
||||
PageIndex: in.GetPageIndex(),
|
||||
})
|
||||
if err != nil {
|
||||
e := domain.PostMongoErrorL(
|
||||
logx.WithContext(l.ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: in},
|
||||
{Key: "func", Value: "PostModel.LikeDislike"},
|
||||
{Key: "err", Value: err},
|
||||
},
|
||||
"failed to like or dislike").Wrap(err)
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var list = make([]*tweeting.LikeItem, 0, len(result))
|
||||
for _, item := range result {
|
||||
list = append(list, &tweeting.LikeItem{
|
||||
LikeType: int64(item.Type),
|
||||
TargetId: item.TargetID,
|
||||
Uid: item.UID,
|
||||
})
|
||||
}
|
||||
|
||||
return &tweeting.LikeListResp{
|
||||
List: list,
|
||||
Page: &tweeting.Pager{
|
||||
Size: in.GetPageSize(),
|
||||
Index: in.GetPageIndex(),
|
||||
Total: total,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package postservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-tweeting-service/internal/domain"
|
||||
model "app-cloudep-tweeting-service/internal/model/mongo"
|
||||
ers "code.30cm.net/digimon/library-go/errs"
|
||||
"context"
|
||||
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
|
@ -23,9 +26,39 @@ func NewLikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeLogic {
|
|||
}
|
||||
}
|
||||
|
||||
type likeReq struct {
|
||||
Target string `json:"target" validate:"required"`
|
||||
UID string `json:"uid" validate:"required"`
|
||||
LikeType domain.LikeType `json:"like_type" validate:"required,oneof=1 2"`
|
||||
}
|
||||
|
||||
// Like 點讚/取消讚 貼文
|
||||
func (l *LikeLogic) Like(in *tweeting.LikeReq) (*tweeting.OKResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 驗證資料
|
||||
if err := l.svcCtx.Validate.ValidateAll(&likeReq{
|
||||
Target: in.TargetId,
|
||||
UID: in.GetUid(),
|
||||
LikeType: domain.LikeType(in.GetLikeType()),
|
||||
}); err != nil {
|
||||
return nil, ers.InvalidFormat(err.Error())
|
||||
}
|
||||
|
||||
err := l.svcCtx.PostLikeModel.LikeDislike(l.ctx, &model.PostLikes{
|
||||
TargetID: in.GetTargetId(),
|
||||
UID: in.GetUid(),
|
||||
Type: int8(in.GetLikeType()),
|
||||
})
|
||||
if err != nil {
|
||||
e := domain.PostMongoErrorL(
|
||||
logx.WithContext(l.ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: in},
|
||||
{Key: "func", Value: "PostModel.LikeDislike"},
|
||||
{Key: "err", Value: err},
|
||||
},
|
||||
"failed to like or dislike").Wrap(err)
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return &tweeting.OKResp{}, nil
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
)
|
||||
|
||||
type comment_likesModel interface {
|
||||
Insert(ctx context.Context, data *Comment_likes) error
|
||||
FindOne(ctx context.Context, id string) (*Comment_likes, error)
|
||||
Update(ctx context.Context, data *Comment_likes) (*mongo.UpdateResult, error)
|
||||
Insert(ctx context.Context, data *CommentLikes) error
|
||||
FindOne(ctx context.Context, id string) (*CommentLikes, error)
|
||||
Update(ctx context.Context, data *CommentLikes) (*mongo.UpdateResult, error)
|
||||
Delete(ctx context.Context, id string) (int64, error)
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ func newDefaultComment_likesModel(conn *mon.Model) *defaultComment_likesModel {
|
|||
return &defaultComment_likesModel{conn: conn}
|
||||
}
|
||||
|
||||
func (m *defaultComment_likesModel) Insert(ctx context.Context, data *Comment_likes) error {
|
||||
func (m *defaultComment_likesModel) Insert(ctx context.Context, data *CommentLikes) error {
|
||||
if data.ID.IsZero() {
|
||||
data.ID = primitive.NewObjectID()
|
||||
data.CreateAt = time.Now()
|
||||
|
@ -37,13 +37,13 @@ func (m *defaultComment_likesModel) Insert(ctx context.Context, data *Comment_li
|
|||
return err
|
||||
}
|
||||
|
||||
func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*Comment_likes, error) {
|
||||
func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*CommentLikes, error) {
|
||||
oid, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectId
|
||||
}
|
||||
|
||||
var data Comment_likes
|
||||
var data CommentLikes
|
||||
|
||||
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
|
||||
switch err {
|
||||
|
@ -56,7 +56,7 @@ func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*Co
|
|||
}
|
||||
}
|
||||
|
||||
func (m *defaultComment_likesModel) Update(ctx context.Context, data *Comment_likes) (*mongo.UpdateResult, error) {
|
||||
func (m *defaultComment_likesModel) Update(ctx context.Context, data *CommentLikes) (*mongo.UpdateResult, error) {
|
||||
data.UpdateAt = time.Now()
|
||||
|
||||
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Comment_likes struct {
|
||||
type CommentLikes struct {
|
||||
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
// TODO: Fill your own fields
|
||||
UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
package model
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/stores/mon"
|
||||
import (
|
||||
"app-cloudep-tweeting-service/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ Post_likesModel = (*customPost_likesModel)(nil)
|
||||
|
||||
|
@ -9,6 +19,15 @@ type (
|
|||
// and implement the added methods in customPost_likesModel.
|
||||
Post_likesModel interface {
|
||||
post_likesModel
|
||||
LikeDislike(ctx context.Context, postLike *PostLikes) error
|
||||
FindLikeUsers(ctx context.Context, param *QueryPostLikeReq) ([]*PostLikes, int64, error)
|
||||
}
|
||||
|
||||
QueryPostLikeReq struct {
|
||||
Target string `bson:"target"`
|
||||
LikeType domain.LikeType `bson:"like_type"`
|
||||
PageIndex int64 `bson:"page_index"`
|
||||
PageSize int64 `bson:"page_size"`
|
||||
}
|
||||
|
||||
customPost_likesModel struct {
|
||||
|
@ -23,3 +42,78 @@ func NewPost_likesModel(url, db, collection string) Post_likesModel {
|
|||
defaultPost_likesModel: newDefaultPost_likesModel(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultPost_likesModel) LikeDislike(ctx context.Context, postLike *PostLikes) error {
|
||||
// 使用 target_id、uid、type 來查詢資料是否存在
|
||||
filter := bson.M{
|
||||
"target_id": postLike.TargetID,
|
||||
"uid": postLike.UID,
|
||||
"type": postLike.Type,
|
||||
}
|
||||
|
||||
// 查詢資料是否存在
|
||||
var existingPostLike PostLikes
|
||||
err := m.conn.FindOne(ctx, &existingPostLike, filter)
|
||||
|
||||
if err == nil {
|
||||
// 資料存在,進行刪除操作
|
||||
_, err = m.conn.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
return err // 刪除失敗
|
||||
}
|
||||
return nil // 刪除成功
|
||||
} else if errors.Is(mongo.ErrNoDocuments, err) {
|
||||
// 資料不存在,進行插入操作
|
||||
postLike.ID = primitive.NewObjectID() // 設置新的 ObjectID
|
||||
postLike.CreateAt = time.Now().UTC().UnixNano()
|
||||
_, err = m.conn.InsertOne(ctx, postLike)
|
||||
if err != nil {
|
||||
return err // 插入失敗
|
||||
}
|
||||
return nil // 插入成功
|
||||
} else {
|
||||
// 其他錯誤
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultPost_likesModel) FindLikeUsers(ctx context.Context, param *QueryPostLikeReq) ([]*PostLikes, int64, error) {
|
||||
// 建立篩選條件
|
||||
filter := bson.M{}
|
||||
|
||||
// 如果指定了 Target 條件,將其添加到篩選條件中
|
||||
if param.Target != "" {
|
||||
filter["target_id"] = param.Target
|
||||
}
|
||||
|
||||
// 如果指定了 LikeType 條件,將其添加到篩選條件中
|
||||
if param.LikeType != 0 {
|
||||
filter["type"] = param.LikeType
|
||||
}
|
||||
|
||||
// 計算符合條件的文檔總數
|
||||
totalCount, err := m.conn.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 設置分頁和排序選項
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{{"createAt", -1}}) // 按照創建時間倒序排序
|
||||
if param.PageSize > 0 {
|
||||
opts.SetLimit(param.PageSize)
|
||||
}
|
||||
if param.PageIndex > 0 && param.PageSize > 0 {
|
||||
opts.SetSkip((param.PageIndex - 1) * param.PageSize)
|
||||
}
|
||||
|
||||
// 查詢符合條件的文檔
|
||||
var results []*PostLikes
|
||||
err = m.conn.Find(ctx, &results, filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 返回結果集、總數和錯誤信息
|
||||
return results, totalCount, nil
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
)
|
||||
|
||||
type post_likesModel interface {
|
||||
Insert(ctx context.Context, data *Post_likes) error
|
||||
FindOne(ctx context.Context, id string) (*Post_likes, error)
|
||||
Update(ctx context.Context, data *Post_likes) (*mongo.UpdateResult, error)
|
||||
Insert(ctx context.Context, data *PostLikes) error
|
||||
FindOne(ctx context.Context, id string) (*PostLikes, error)
|
||||
Update(ctx context.Context, data *PostLikes) (*mongo.UpdateResult, error)
|
||||
Delete(ctx context.Context, id string) (int64, error)
|
||||
}
|
||||
|
||||
|
@ -26,24 +26,23 @@ func newDefaultPost_likesModel(conn *mon.Model) *defaultPost_likesModel {
|
|||
return &defaultPost_likesModel{conn: conn}
|
||||
}
|
||||
|
||||
func (m *defaultPost_likesModel) Insert(ctx context.Context, data *Post_likes) error {
|
||||
func (m *defaultPost_likesModel) Insert(ctx context.Context, data *PostLikes) error {
|
||||
if data.ID.IsZero() {
|
||||
data.ID = primitive.NewObjectID()
|
||||
data.CreateAt = time.Now()
|
||||
data.UpdateAt = time.Now()
|
||||
data.CreateAt = time.Now().UTC().UnixNano()
|
||||
}
|
||||
|
||||
_, err := m.conn.InsertOne(ctx, data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*Post_likes, error) {
|
||||
func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*PostLikes, error) {
|
||||
oid, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectId
|
||||
}
|
||||
|
||||
var data Post_likes
|
||||
var data PostLikes
|
||||
|
||||
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
|
||||
switch err {
|
||||
|
@ -56,9 +55,7 @@ func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*Post_
|
|||
}
|
||||
}
|
||||
|
||||
func (m *defaultPost_likesModel) Update(ctx context.Context, data *Post_likes) (*mongo.UpdateResult, error) {
|
||||
data.UpdateAt = time.Now()
|
||||
|
||||
func (m *defaultPost_likesModel) Update(ctx context.Context, data *PostLikes) (*mongo.UpdateResult, error) {
|
||||
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
|
||||
return res, err
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Post_likes struct {
|
||||
type PostLikes struct {
|
||||
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
// TODO: Fill your own fields
|
||||
UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
|
||||
CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
|
||||
TargetID string `bson:"target_id" json:"target_id"`
|
||||
UID string `bson:"uid" json:"uid"`
|
||||
Type int8 `bson:"type" json:"type"`
|
||||
CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PostLikes) CollectionName() string {
|
||||
return "post_like"
|
||||
}
|
||||
|
|
|
@ -161,12 +161,13 @@ func (m *defaultPostModel) Find(ctx context.Context, param *QueryPostModelReq) (
|
|||
}
|
||||
|
||||
// 分頁處理
|
||||
options := options.Find()
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{{"create_time", -1}})
|
||||
if param.PageSize > 0 {
|
||||
options.SetLimit(param.PageSize)
|
||||
opts.SetLimit(param.PageSize)
|
||||
}
|
||||
if param.PageIndex > 0 {
|
||||
options.SetSkip((param.PageIndex - 1) * param.PageSize)
|
||||
opts.SetSkip((param.PageIndex - 1) * param.PageSize)
|
||||
}
|
||||
|
||||
// 計算總數(不考慮分頁)
|
||||
|
@ -177,7 +178,7 @@ func (m *defaultPostModel) Find(ctx context.Context, param *QueryPostModelReq) (
|
|||
|
||||
result := make([]*Post, 0, param.PageSize)
|
||||
// 執行查詢
|
||||
err = m.conn.Find(ctx, &result, filter, options)
|
||||
err = m.conn.Find(ctx, &result, filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
|
@ -27,3 +27,7 @@ type Media struct {
|
|||
Type string // media type jpeg, m3u8 之類的
|
||||
Links string // 連結的網址
|
||||
}
|
||||
|
||||
func (p *Post) CollectionName() string {
|
||||
return "post"
|
||||
}
|
||||
|
|
|
@ -12,15 +12,19 @@ type ServiceContext struct {
|
|||
Validate vi.Validate
|
||||
|
||||
PostModel model.PostModel
|
||||
PostLikeModel model.Post_likesModel
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
baseMongo := MustMongoConnectUrl(c)
|
||||
postCollection := model.Post{}
|
||||
postLikeCollection := model.PostLikes{}
|
||||
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Validate: vi.MustValidator(),
|
||||
PostModel: model.NewPostModel(baseMongo, c.Mongo.Database, "post", c.Cache),
|
||||
PostModel: model.NewPostModel(baseMongo, c.Mongo.Database, postCollection.CollectionName(), c.Cache),
|
||||
PostLikeModel: model.NewPost_likesModel(baseMongo, c.Mongo.Database, postLikeCollection.CollectionName()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue