feat:去掉重复的点赞约束

This commit is contained in:
zerosaturation 2026-05-25 14:44:26 +08:00
parent 057f9bce1c
commit d7d17d8a38
12 changed files with 66 additions and 23 deletions

View File

@ -2247,6 +2247,8 @@ func (x *UnlikeAssetResponse) GetLikeCount() int32 {
type CheckAssetLikeRequest struct { type CheckAssetLikeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID
StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2288,6 +2290,20 @@ func (x *CheckAssetLikeRequest) GetAssetId() int64 {
return 0 return 0
} }
func (x *CheckAssetLikeRequest) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *CheckAssetLikeRequest) GetStarId() int64 {
if x != nil {
return x.StarId
}
return 0
}
// 检查是否已点赞响应 // 检查是否已点赞响应
type CheckAssetLikeResponse struct { type CheckAssetLikeResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -3828,9 +3844,11 @@ const file_asset_proto_rawDesc = "" +
"\x13UnlikeAssetResponse\x120\n" + "\x13UnlikeAssetResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" +
"\n" + "\n" +
"like_count\x18\x02 \x01(\x05R\tlikeCount\"2\n" + "like_count\x18\x02 \x01(\x05R\tlikeCount\"d\n" +
"\x15CheckAssetLikeRequest\x12\x19\n" + "\x15CheckAssetLikeRequest\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\"e\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x17\n" +
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x17\n" +
"\astar_id\x18\x03 \x01(\x03R\x06starId\"e\n" +
"\x16CheckAssetLikeResponse\x120\n" + "\x16CheckAssetLikeResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x19\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x19\n" +
"\bis_liked\x18\x02 \x01(\bR\aisLiked\"\x87\x01\n" + "\bis_liked\x18\x02 \x01(\bR\aisLiked\"\x87\x01\n" +

View File

@ -279,6 +279,8 @@ message UnlikeAssetResponse {
// RPCSocial Service调用 // RPCSocial Service调用
message CheckAssetLikeRequest { message CheckAssetLikeRequest {
int64 asset_id = 1; // ID int64 asset_id = 1; // ID
int64 user_id = 2; // ID
int64 star_id = 3; // ID
} }
// //

View File

@ -0,0 +1,6 @@
-- Drop the overly strict unique constraint that prevents users from liking
-- the same asset across different exhibitions. The correct constraint is
-- (user_id, asset_id, exhibition_id), not (user_id, asset_id, star_id).
-- This allows a user to like the same asset once per exhibition.
ALTER TABLE asset_likes DROP CONSTRAINT IF EXISTS uk_asset_likes_user_asset_star;

View File

@ -392,13 +392,14 @@ func (p *AssetProvider) LikeAsset(ctx context.Context, req *pb.LikeAssetRequest)
zap.Int64("asset_id", req.AssetId), zap.Int64("asset_id", req.AssetId),
zap.Error(err), zap.Error(err),
) )
// 返回响应但不返回 error让客户端检查 Base.Code
return &pb.LikeAssetResponse{ return &pb.LikeAssetResponse{
Base: &pbCommon.BaseResponse{ Base: &pbCommon.BaseResponse{
Code: appErrors.ToStatusCode(err), Code: appErrors.ToStatusCode(err),
Message: err.Error(), Message: err.Error(),
Timestamp: 0, Timestamp: 0,
}, },
}, err }, nil
} }
logger.Logger.Info("LikeAsset successful", logger.Logger.Info("LikeAsset successful",
@ -475,13 +476,18 @@ func (p *AssetProvider) UnlikeAsset(ctx context.Context, req *pb.UnlikeAssetRequ
func (p *AssetProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLikeRequest) (*pb.CheckAssetLikeResponse, error) { func (p *AssetProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLikeRequest) (*pb.CheckAssetLikeResponse, error) {
logger.Logger.Info("Received CheckAssetLike request", logger.Logger.Info("Received CheckAssetLike request",
zap.Int64("asset_id", req.AssetId), zap.Int64("asset_id", req.AssetId),
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
) )
// 从 Dubbo attachments 获取用户信息 // 直接使用请求中的用户信息
userID, starID, err := extractUserInfoFromDubboAttachments(ctx) userID := req.UserId
if err != nil { starID := req.StarId
logger.Logger.Error("Failed to extract user info from attachments",
zap.Error(err), if userID == 0 || starID == 0 {
logger.Logger.Error("Invalid user info in request",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
) )
return &pb.CheckAssetLikeResponse{ return &pb.CheckAssetLikeResponse{
Base: &pbCommon.BaseResponse{ Base: &pbCommon.BaseResponse{
@ -489,7 +495,7 @@ func (p *AssetProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLi
Message: "user authentication required", Message: "user authentication required",
Timestamp: 0, Timestamp: 0,
}, },
}, err }, fmt.Errorf("invalid user_id or star_id")
} }
// 调用Service层 // 调用Service层

View File

@ -47,6 +47,10 @@ func (s *AssetLikeService) isAssetExhibiting(assetID int64) (int64, error) {
Where("asset_id = ? AND expire_at > ?", assetID, nowMs). Where("asset_id = ? AND expire_at > ?", assetID, nowMs).
First(&exhibition).Error First(&exhibition).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound {
// 资产不在任何展出中,返回 0 表示未展出
return 0, nil
}
return 0, fmt.Errorf("failed to check exhibition status: %w", err) return 0, fmt.Errorf("failed to check exhibition status: %w", err)
} }
return exhibition.ID, nil return exhibition.ID, nil

View File

@ -62,6 +62,8 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI
// 2. 检查是否已点赞 // 2. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{ checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID, AssetId: assetID,
UserId: userID,
StarId: starID,
} }
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq) checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil { if err != nil {
@ -125,6 +127,8 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta
// 1. 检查是否已点赞 // 1. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{ checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID, AssetId: assetID,
UserId: userID,
StarId: starID,
} }
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq) checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil { if err != nil {
@ -187,6 +191,8 @@ func (s *AssetLikeService) CheckAssetLike(ctx context.Context, assetID, userID,
checkLikeReq := &assetPb.CheckAssetLikeRequest{ checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID, AssetId: assetID,
UserId: userID,
StarId: starID,
} }
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq) checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil { if err != nil {

BIN
backend/socialService Executable file

Binary file not shown.

View File

@ -383,13 +383,13 @@ const handleAvatarClick = () => {
if (pages.length > 0) { if (pages.length > 0) {
const currentPage = pages[pages.length - 1]; const currentPage = pages[pages.length - 1];
// //
if (currentPage.route === 'pages/profile/myWorks') { if (currentPage.route === 'pages/profile/profile') {
// //
return; return;
} }
} }
uni.navigateTo({ uni.navigateTo({
url: '/pages/profile/myWorks' url: '/pages/profile/profile'
}); });
}; };

View File

@ -10,9 +10,9 @@
</view> </view>
<!-- <text class="nav-title">我的作品</text> --> <!-- <text class="nav-title">我的作品</text> -->
<view class="nav-placeholder"></view> <view class="nav-placeholder"></view>
<view class="nav-settings" @tap="goToSettings"> <!-- <view class="nav-settings" @tap="goToSettings">
<image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image> <image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
</view> </view> -->
</view> </view>
<view class="scroll-content"> <view class="scroll-content">

View File

@ -1296,8 +1296,8 @@ onShow(() => {
} }
.close-icon-img { .close-icon-img {
width: 80rpx; width: 48rpx;
height: 80rpx; height: 48rpx;
position: relative; position: relative;
top: 96rpx; top: 96rpx;
left: 32rpx; left: 32rpx;
@ -1372,15 +1372,11 @@ onShow(() => {
} }
.uid-value {}
.toggle-icon { .toggle-icon {
width: 32rpx; width: 32rpx;
height: 32rpx; height: 32rpx;
} }
.address-value {}
.info-btn { .info-btn {
font-size: 20rpx; font-size: 20rpx;
color: #FFD700; color: #FFD700;

View File

@ -33,7 +33,7 @@
<script setup> <script setup>
defineProps({ defineProps({
modelValue: { type: String, default: 'random' } modelValue: { type: String, default: 'hot' }
}) })
defineEmits(['update:modelValue']) defineEmits(['update:modelValue'])
@ -42,7 +42,7 @@ const tabs = [
{ key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/3.png',iconWidth: 80, iconHeight: 80 }, { key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/3.png',iconWidth: 80, iconHeight: 80 },
{ key: 'new', label: '新鲜上架', emoji: null, icon: '/static/square/2.png',iconWidth: 80, iconHeight: 80 }, { key: 'new', label: '新鲜上架', emoji: null, icon: '/static/square/2.png',iconWidth: 80, iconHeight: 80 },
{ key: 'potential', label: '潜力之星', emoji: null, icon: '/static/square/1.png',iconWidth: 96, iconHeight: 96 }, { key: 'potential', label: '潜力之星', emoji: null, icon: '/static/square/1.png',iconWidth: 96, iconHeight: 96 },
{ key: 'random', label: '随机寻宝', emoji: null, icon: '/static/square/4.png',iconWidth: 80, iconHeight: 80 }, { key: 'myworks', label: '我的', emoji: null, icon: '/static/square/4.png',iconWidth: 80, iconHeight: 80 },
] ]
</script> </script>

View File

@ -89,14 +89,18 @@ const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname
const currentStarId = ref(uni.getStorageSync('star_id') || null) const currentStarId = ref(uni.getStorageSync('star_id') || null)
// ========== UI State ========== // ========== UI State ==========
const activeContentTab = ref('random') const activeContentTab = ref('hot')
const waterfallKey = ref(0) // WaterfallGrid const waterfallKey = ref(0) // WaterfallGrid
const navExpanded = ref(false) const navExpanded = ref(false)
const showRankingModal = ref(false) const showRankingModal = ref(false)
const isActive = ref(true) const isActive = ref(true)
// ========== Watch activeContentTab ========== // ========== Watch activeContentTab ==========
watch(activeContentTab, () => { watch(activeContentTab, (newTab) => {
if (newTab === 'myworks') {
uni.navigateTo({ url: '/pages/profile/myWorks' })
return
}
// WaterfallGridiOS CSS // WaterfallGridiOS CSS
waterfallKey.value++ waterfallKey.value++
}) })
@ -191,6 +195,7 @@ onMounted(() => {
onShow(() => { onShow(() => {
isActive.value = true isActive.value = true
activeContentTab.value = 'hot'
// //
// if (shouldShowGuideStartModal()) { // if (shouldShowGuideStartModal()) {
// uni.navigateTo({ // uni.navigateTo({