feat: 点赞列表修改,增加光栅卡字段

This commit is contained in:
zerosaturation 2026-05-19 14:44:19 +08:00
parent 9ac75d034b
commit 94b9271184
11 changed files with 50 additions and 18 deletions

View File

@ -894,6 +894,7 @@ func (ctrl *SocialController) UnlikeAsset(c *gin.Context) {
// @Security BearerAuth // @Security BearerAuth
// @Param page query int false "页码默认1" // @Param page query int false "页码默认1"
// @Param page_size query int false "每页数量默认20最大100" // @Param page_size query int false "每页数量默认20最大100"
// @Param order_by query string false "排序字段liked_at(默认), like_count(按点赞数)"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Router /api/v1/me/liked-assets [get] // @Router /api/v1/me/liked-assets [get]
func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) { func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) {
@ -938,6 +939,7 @@ func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) {
"liked_at": item.LikedAt, "liked_at": item.LikedAt,
"earnings": item.Earnings, "earnings": item.Earnings,
"hourly_earnings": item.HourlyEarnings, "hourly_earnings": item.HourlyEarnings,
"is_lenticular": item.IsLenticular,
}) })
} }

View File

@ -33,6 +33,7 @@ type Exhibition struct {
CreatedAt int64 `gorm:"column:created_at;not null"` CreatedAt int64 `gorm:"column:created_at;not null"`
UpdatedAt int64 `gorm:"column:updated_at;not null"` UpdatedAt int64 `gorm:"column:updated_at;not null"`
DeletedAt *int64 `gorm:"column:deleted_at"` DeletedAt *int64 `gorm:"column:deleted_at"`
IsProcessed bool `gorm:"column:is_processed;default:false"`
} }
// TableName 指定表名 // TableName 指定表名

View File

@ -2349,6 +2349,7 @@ type LikedAssetItem struct {
LikedAt int64 `protobuf:"varint,5,opt,name=liked_at,json=likedAt,proto3" json:"liked_at,omitempty"` // 点赞时间(毫秒时间戳) LikedAt int64 `protobuf:"varint,5,opt,name=liked_at,json=likedAt,proto3" json:"liked_at,omitempty"` // 点赞时间(毫秒时间戳)
Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益 Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益
HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益 HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益
IsLenticular bool `protobuf:"varint,8,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2432,6 +2433,13 @@ func (x *LikedAssetItem) GetHourlyEarnings() float64 {
return 0 return 0
} }
func (x *LikedAssetItem) GetIsLenticular() bool {
if x != nil {
return x.IsLenticular
}
return false
}
// 获取我今日点赞的作品列表请求(暂不实现) // 获取我今日点赞的作品列表请求(暂不实现)
type GetMyTodayLikedAssetsRequest struct { type GetMyTodayLikedAssetsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -2936,7 +2944,7 @@ const file_social_proto_rawDesc = "" +
"\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" + "\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" +
"\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" + "\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" +
"\bhas_more\x18\x05 \x01(\bR\ahasMore\"\xdb\x01\n" + "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\x80\x02\n" +
"\x0eLikedAssetItem\x12\x19\n" + "\x0eLikedAssetItem\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
@ -2945,7 +2953,8 @@ const file_social_proto_rawDesc = "" +
"like_count\x18\x04 \x01(\x05R\tlikeCount\x12\x19\n" + "like_count\x18\x04 \x01(\x05R\tlikeCount\x12\x19\n" +
"\bliked_at\x18\x05 \x01(\x03R\alikedAt\x12\x1a\n" + "\bliked_at\x18\x05 \x01(\x03R\alikedAt\x12\x1a\n" +
"\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" + "\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" +
"\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\"O\n" + "\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\x12#\n" +
"\ris_lenticular\x18\b \x01(\bR\fisLenticular\"O\n" +
"\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" + "\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" +
"\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" + "\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" +

View File

@ -293,6 +293,7 @@ message LikedAssetItem {
int64 liked_at = 5; // int64 liked_at = 5; //
int64 earnings = 6; // int64 earnings = 6; //
double hourly_earnings = 7; // double hourly_earnings = 7; //
bool is_lenticular = 8; //
} }
// //

View File

@ -0,0 +1,8 @@
-- Migration: Add is_processed field to exhibitions table
-- Description: Mark exhibitions as processed to prevent duplicate revenue record generation
-- Date: 2026-05-19
ALTER TABLE exhibitions ADD COLUMN IF NOT EXISTS is_processed BOOLEAN DEFAULT false;
-- Create index for faster queries on unprocessed expired exhibitions
CREATE INDEX IF NOT EXISTS idx_exhibitions_unprocessed ON exhibitions(expire_at) WHERE is_processed = false AND deleted_at IS NULL;

View File

@ -40,6 +40,8 @@ type GalleryRepository interface {
RemoveExhibitionTx(exhibitionID int64, assetID int64) error RemoveExhibitionTx(exhibitionID int64, assetID int64) error
// 更新展览过期时间(用于清理后防止重复处理) // 更新展览过期时间(用于清理后防止重复处理)
UpdateExhibitionExpireAt(exhibitionID int64, expireAt int64) error UpdateExhibitionExpireAt(exhibitionID int64, expireAt int64) error
// 设置展品已处理(用于清理后标记已处理,防止重复生成收益记录)
SetExhibitionProcessed(exhibitionID int64, processed bool) error
// ========== 我的作品相关 ========== // ========== 我的作品相关 ==========
@ -299,10 +301,10 @@ func (r *galleryRepository) DeleteExhibitionByAsset(assetID int64) error {
}).Error }).Error
} }
// GetExpiredExhibitions 获取过期的展品展示记录(不含已删除 // GetExpiredExhibitions 获取过期的展品展示记录(不含已删除且未处理
func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error) { func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error) {
var exhibitions []*models.Exhibition var exhibitions []*models.Exhibition
err := r.db.Where("expire_at <= ? AND deleted_at IS NULL", beforeTime).Find(&exhibitions).Error err := r.db.Where("expire_at <= ? AND deleted_at IS NULL AND is_processed = false", beforeTime).Find(&exhibitions).Error
return exhibitions, err return exhibitions, err
} }
@ -399,6 +401,13 @@ func (r *galleryRepository) UpdateExhibitionExpireAt(exhibitionID int64, expireA
Update("expire_at", expireAt).Error Update("expire_at", expireAt).Error
} }
// SetExhibitionProcessed 设置展品已处理标记(用于清理后标记已处理,防止重复生成收益记录)
func (r *galleryRepository) SetExhibitionProcessed(exhibitionID int64, processed bool) error {
return r.db.Model(&models.Exhibition{}).
Where("id = ?", exhibitionID).
Update("is_processed", processed).Error
}
// ========== 我的作品相关实现 ========== // ========== 我的作品相关实现 ==========
// GetMyExhibitedAssets 获取我展出的作品列表(返回展出中且未下架的,含收益) // GetMyExhibitedAssets 获取我展出的作品列表(返回展出中且未下架的,含收益)

View File

@ -146,9 +146,9 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
successCount++ successCount++
// 4.5 更新展览过期时间为历史值,防止重复处理(领取收益时才会真正下架) // 4.5 标记展品已处理,防止重复生成收益记录
if err := w.repo.UpdateExhibitionExpireAt(e.ID, now-3600000); err != nil { if err := w.repo.SetExhibitionProcessed(e.ID, true); err != nil {
logger.Logger.Error("更新展览过期时间失败", logger.Logger.Error("标记展品已处理失败",
zap.Int64("exhibition_id", e.ID), zap.Int64("exhibition_id", e.ID),
zap.Error(err)) zap.Error(err))
} }

View File

@ -147,6 +147,7 @@ type LikedAssetInfo struct {
LikedAt int64 LikedAt int64
Earnings int64 Earnings int64
HourlyEarnings float64 HourlyEarnings float64
IsLenticular bool // 是否为光栅卡
} }
// RandomUserInfo 随机用户信息 // RandomUserInfo 随机用户信息
@ -590,29 +591,29 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page
} }
// 计数查询(使用 DISTINCT 因为一个资产可能在多个展位展出) // 计数查询(使用 DISTINCT 因为一个资产可能在多个展位展出)
// 只要资产未删除且未下架就显示,包含已过期的(用户可继续查看点赞记录) // 显示展品存在且未领取is_processed=false无论是否过期
// 不显示已领取的is_processed=true或从未展出过的
countQuery := r.db.Model(&models.AssetLike{}). countQuery := r.db.Model(&models.AssetLike{}).
Joins("JOIN assets a ON a.id = asset_likes.asset_id"). Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id"). Joins("JOIN exhibitions e ON e.asset_id = a.id AND e.deleted_at IS NULL AND e.is_processed = false").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("a.deleted_at IS NULL AND a.is_active = ?", true). Where("a.deleted_at IS NULL AND a.is_active = ?", true)
Where("e.deleted_at IS NULL")
if err := countQuery.Distinct("asset_likes.asset_id").Count(&total).Error; err != nil { if err := countQuery.Distinct("asset_likes.asset_id").Count(&total).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
// 数据查询:只过滤已下架的,不过滤过期 // 数据查询:只过滤已下架的,不过滤过期(已过期但未领取的仍显示)
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
err := r.db.Model(&models.AssetLike{}). err := r.db.Model(&models.AssetLike{}).
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count, Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
asset_likes.created_at as liked_at`). asset_likes.created_at as liked_at,
(a.tags @> '["craft:lenticular"]') as is_lenticular`).
Joins("JOIN assets a ON a.id = asset_likes.asset_id"). Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id"). Joins("JOIN exhibitions e ON e.asset_id = a.id AND e.deleted_at IS NULL AND e.is_processed = false").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("a.deleted_at IS NULL AND a.is_active = ?", true). Where("a.deleted_at IS NULL AND a.is_active = ?", true).
Where("e.deleted_at IS NULL"). Group("asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at, is_lenticular").
Group("asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at").
Order(orderClause). Order(orderClause).
Limit(pageSize). Limit(pageSize).
Offset(offset). Offset(offset).

View File

@ -278,6 +278,7 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi
LikedAt: item.LikedAt, LikedAt: item.LikedAt,
Earnings: item.Earnings, Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings, HourlyEarnings: item.HourlyEarnings,
IsLenticular: item.IsLenticular,
}) })
} }

View File

@ -217,7 +217,7 @@
.level-badge { .level-badge {
position: absolute; position: absolute;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%); background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
width: 88rpx; width: 112rpx;
height: 24rpx; height: 24rpx;
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -393,7 +393,7 @@ const handleExhibitionCardTap = (item, index) => {
// exhibitionWorks.value[index].earnings = data.earnings; // exhibitionWorks.value[index].earnings = data.earnings;
// } else { // } else {
// //
await loadExhibitedAssets(); await loadLikedAssets();
// } // }
uni.showToast({ title: '点赞成功', icon: 'success' }); uni.showToast({ title: '点赞成功', icon: 'success' });