feat: Implement notification service and integrate with user interactions
- Added notification service to handle sending and listing notifications. - Integrated notification sending on user follow and order payment events. - Updated user service to include fetching followed tenants. - Enhanced content service to manage access control for content assets. - Implemented logic for listing content topics based on genre. - Updated creator service to manage content updates and pricing. - Improved order service to include detailed order information and notifications. - Added tests for notification CRUD operations and order details.
This commit is contained in:
@@ -102,11 +102,14 @@ func (s *content) Get(ctx context.Context, id string) (*content_dto.ContentDetai
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
// Interaction status
|
||||
// Interaction & Access status
|
||||
isLiked := false
|
||||
isFavorited := false
|
||||
hasAccess := false
|
||||
|
||||
if userID := ctx.Value(consts.CtxKeyUser); userID != nil {
|
||||
uid := cast.ToInt64(userID)
|
||||
// Interaction
|
||||
isLiked, _ = models.UserContentActionQuery.WithContext(ctx).
|
||||
Where(models.UserContentActionQuery.UserID.Eq(uid),
|
||||
models.UserContentActionQuery.ContentID.Eq(cid),
|
||||
@@ -117,16 +120,46 @@ func (s *content) Get(ctx context.Context, id string) (*content_dto.ContentDetai
|
||||
models.UserContentActionQuery.ContentID.Eq(cid),
|
||||
models.UserContentActionQuery.Type.Eq("favorite")).
|
||||
Exists()
|
||||
|
||||
// Access Check
|
||||
if item.UserID == uid {
|
||||
hasAccess = true // Owner
|
||||
} else {
|
||||
// Check Purchase
|
||||
exists, _ := models.ContentAccessQuery.WithContext(ctx).
|
||||
Where(models.ContentAccessQuery.UserID.Eq(uid),
|
||||
models.ContentAccessQuery.ContentID.Eq(cid),
|
||||
models.ContentAccessQuery.Status.Eq(consts.ContentAccessStatusActive)).
|
||||
Exists()
|
||||
if exists {
|
||||
hasAccess = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter Assets based on Access
|
||||
var accessibleAssets []*models.ContentAsset
|
||||
for _, ca := range item.ContentAssets {
|
||||
if hasAccess {
|
||||
accessibleAssets = append(accessibleAssets, ca)
|
||||
} else {
|
||||
// If no access, only allow Preview and Cover
|
||||
if ca.Role == consts.ContentAssetRolePreview || ca.Role == consts.ContentAssetRoleCover {
|
||||
accessibleAssets = append(accessibleAssets, ca)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detail := &content_dto.ContentDetail{
|
||||
ContentItem: s.toContentItemDTO(&item),
|
||||
Description: item.Description,
|
||||
Body: item.Body,
|
||||
MediaUrls: s.toMediaURLs(item.ContentAssets),
|
||||
MediaUrls: s.toMediaURLs(accessibleAssets),
|
||||
IsLiked: isLiked,
|
||||
IsFavorited: isFavorited,
|
||||
}
|
||||
// Pass IsPurchased/HasAccess info to frontend?
|
||||
detail.ContentItem.IsPurchased = hasAccess // Update DTO field logic if needed. IsPurchased usually means "Bought". Owner implies access but not necessarily purchased. But for UI "Play" button, IsPurchased=true is fine.
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
@@ -224,7 +257,13 @@ func (s *content) LikeComment(ctx context.Context, id string) error {
|
||||
uid := cast.ToInt64(userID)
|
||||
cmid := cast.ToInt64(id)
|
||||
|
||||
return models.Q.Transaction(func(tx *models.Query) error {
|
||||
// Fetch comment for author
|
||||
cm, err := models.CommentQuery.WithContext(ctx).Where(models.CommentQuery.ID.Eq(cmid)).First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
|
||||
err = models.Q.Transaction(func(tx *models.Query) error {
|
||||
exists, _ := tx.UserCommentAction.WithContext(ctx).
|
||||
Where(tx.UserCommentAction.UserID.Eq(uid), tx.UserCommentAction.CommentID.Eq(cmid), tx.UserCommentAction.Type.Eq("like")).
|
||||
Exists()
|
||||
@@ -240,6 +279,14 @@ func (s *content) LikeComment(ctx context.Context, id string) error {
|
||||
_, err := tx.Comment.WithContext(ctx).Where(tx.Comment.ID.Eq(cmid)).UpdateSimple(tx.Comment.Likes.Add(1))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if Notification != nil {
|
||||
_ = Notification.Send(ctx, cm.UserID, "interaction", "评论点赞", "有人点赞了您的评论")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *content) GetLibrary(ctx context.Context) ([]user_dto.ContentItem, error) {
|
||||
@@ -309,8 +356,56 @@ func (s *content) RemoveLike(ctx context.Context, contentId string) error {
|
||||
}
|
||||
|
||||
func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) {
|
||||
// topics usually hardcoded or from a dedicated table
|
||||
return []content_dto.Topic{}, nil
|
||||
var results []struct {
|
||||
Genre string
|
||||
Count int
|
||||
}
|
||||
err := models.ContentQuery.WithContext(ctx).UnderlyingDB().
|
||||
Model(&models.Content{}).
|
||||
Where("status = ?", consts.ContentStatusPublished).
|
||||
Select("genre, count(*) as count").
|
||||
Group("genre").
|
||||
Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
var topics []content_dto.Topic
|
||||
for i, r := range results {
|
||||
if r.Genre == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch latest content in this genre to get a cover
|
||||
var c models.Content
|
||||
models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.Genre.Eq(r.Genre), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).
|
||||
Order(models.ContentQuery.PublishedAt.Desc()).
|
||||
UnderlyingDB().
|
||||
Preload("ContentAssets").
|
||||
Preload("ContentAssets.Asset").
|
||||
First(&c)
|
||||
|
||||
cover := ""
|
||||
for _, ca := range c.ContentAssets {
|
||||
if (ca.Role == consts.ContentAssetRoleCover || ca.Role == consts.ContentAssetRoleMain) && ca.Asset != nil {
|
||||
cover = Common.GetAssetURL(ca.Asset.ObjectKey)
|
||||
if ca.Role == consts.ContentAssetRoleCover {
|
||||
break // Prefer cover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topics = append(topics, content_dto.Topic{
|
||||
ID: cast.ToString(i + 1), // Use index as ID for aggregation results
|
||||
Title: r.Genre,
|
||||
Tag: r.Genre,
|
||||
Count: r.Count,
|
||||
Cover: cover,
|
||||
})
|
||||
}
|
||||
return topics, nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
@@ -337,7 +432,7 @@ func (s *content) toContentItemDTO(item *models.Content) content_dto.ContentItem
|
||||
}
|
||||
// Cover
|
||||
if asset.Role == consts.ContentAssetRoleCover {
|
||||
dto.Cover = "http://mock/" + asset.Asset.ObjectKey
|
||||
dto.Cover = Common.GetAssetURL(asset.Asset.ObjectKey)
|
||||
}
|
||||
// Type detection
|
||||
switch asset.Asset.Type {
|
||||
@@ -352,7 +447,7 @@ func (s *content) toContentItemDTO(item *models.Content) content_dto.ContentItem
|
||||
if dto.Cover == "" && len(item.ContentAssets) > 0 {
|
||||
for _, asset := range item.ContentAssets {
|
||||
if asset.Asset != nil && asset.Asset.Type == consts.MediaAssetTypeImage {
|
||||
dto.Cover = "http://mock/" + asset.Asset.ObjectKey
|
||||
dto.Cover = Common.GetAssetURL(asset.Asset.ObjectKey)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -373,7 +468,7 @@ func (s *content) toMediaURLs(assets []*models.ContentAsset) []content_dto.Media
|
||||
var urls []content_dto.MediaURL
|
||||
for _, ca := range assets {
|
||||
if ca.Asset != nil {
|
||||
url := "http://mock/" + ca.Asset.ObjectKey
|
||||
url := Common.GetAssetURL(ca.Asset.ObjectKey)
|
||||
urls = append(urls, content_dto.MediaURL{
|
||||
Type: string(ca.Asset.Type),
|
||||
URL: url,
|
||||
@@ -391,7 +486,13 @@ func (s *content) addInteract(ctx context.Context, contentId, typ string) error
|
||||
uid := cast.ToInt64(userID)
|
||||
cid := cast.ToInt64(contentId)
|
||||
|
||||
return models.Q.Transaction(func(tx *models.Query) error {
|
||||
// Fetch content for author
|
||||
c, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).Select(models.ContentQuery.UserID, models.ContentQuery.Title).First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
|
||||
err = models.Q.Transaction(func(tx *models.Query) error {
|
||||
exists, _ := tx.UserContentAction.WithContext(ctx).
|
||||
Where(tx.UserContentAction.UserID.Eq(uid), tx.UserContentAction.ContentID.Eq(cid), tx.UserContentAction.Type.Eq(typ)).
|
||||
Exists()
|
||||
@@ -410,6 +511,20 @@ func (s *content) addInteract(ctx context.Context, contentId, typ string) error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if Notification != nil {
|
||||
actionName := "互动"
|
||||
if typ == "like" {
|
||||
actionName = "点赞"
|
||||
} else if typ == "favorite" {
|
||||
actionName = "收藏"
|
||||
}
|
||||
_ = Notification.Send(ctx, c.UserID, "interaction", "新的"+actionName, "有人"+actionName+"了您的作品: "+c.Title)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *content) removeInteract(ctx context.Context, contentId, typ string) error {
|
||||
|
||||
Reference in New Issue
Block a user