diff --git a/backend/services/socialService/provider/social_provider.go b/backend/services/socialService/provider/social_provider.go index 65e9e68..e572da7 100644 --- a/backend/services/socialService/provider/social_provider.go +++ b/backend/services/socialService/provider/social_provider.go @@ -253,7 +253,8 @@ func (p *SocialProvider) LikeAsset(ctx context.Context, req *pb.LikeAssetRequest ) // 调用业务逻辑层 - if err := p.assetLikeService.LikeAsset(ctx, req.AssetId, userID, starID); err != nil { + likeCount, err := p.assetLikeService.LikeAsset(ctx, req.AssetId, userID, starID) + if err != nil { logger.Logger.Error("Failed to like asset", zap.Error(err), zap.Int64("asset_id", req.AssetId), @@ -274,6 +275,9 @@ func (p *SocialProvider) LikeAsset(ctx context.Context, req *pb.LikeAssetRequest Message: "Successfully liked asset", Timestamp: time.Now().UnixMilli(), }, + AssetId: req.AssetId, + NewLikeCount: likeCount, + IsLiked: true, }, nil } diff --git a/backend/services/socialService/service/asset_like_service.go b/backend/services/socialService/service/asset_like_service.go index 457ba86..933e13b 100644 --- a/backend/services/socialService/service/asset_like_service.go +++ b/backend/services/socialService/service/asset_like_service.go @@ -30,7 +30,7 @@ func NewAssetLikeService(assetClient *client.AssetClient, socialRepo repository. } // LikeAsset 点赞资产 -func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starID int64) error { +func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starID int64) (int32, error) { logger.Logger.Debug("AssetLikeService.LikeAsset called", zap.Int64("asset_id", assetID), zap.Int64("user_id", userID), @@ -47,7 +47,7 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Error(err), zap.Int64("asset_id", assetID), ) - return fmt.Errorf("获取藏品信息失败,请稍后重试") + return 0, fmt.Errorf("获取藏品信息失败,请稍后重试") } if getAssetResp.Base.Code != pbCommon.StatusCode_STATUS_OK { @@ -56,7 +56,7 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Int32("code", int32(getAssetResp.Base.Code)), zap.String("message", getAssetResp.Base.Message), ) - return fmt.Errorf("藏品不存在") + return 0, fmt.Errorf("藏品不存在") } // 2. 检查是否已点赞 @@ -71,7 +71,7 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Error(err), zap.Int64("asset_id", assetID), ) - return fmt.Errorf("点赞失败,请稍后重试") + return 0, fmt.Errorf("点赞失败,请稍后重试") } if checkLikeResp.IsLiked { @@ -79,7 +79,7 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Int64("asset_id", assetID), zap.Int64("user_id", userID), ) - return fmt.Errorf("当前展示已点赞") + return 0, fmt.Errorf("当前展示已点赞") } // 3. 调用 Asset Service 点赞接口 @@ -92,7 +92,7 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Error(err), zap.Int64("asset_id", assetID), ) - return fmt.Errorf("点赞失败,请稍后重试") + return 0, fmt.Errorf("点赞失败,请稍后重试") } if likeResp.Base.Code != pbCommon.StatusCode_STATUS_OK { @@ -101,19 +101,20 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Int32("code", int32(likeResp.Base.Code)), zap.String("message", likeResp.Base.Message), ) - return fmt.Errorf("点赞失败: %s", likeResp.Base.Message) + return 0, fmt.Errorf("点赞失败: %s", likeResp.Base.Message) } logger.Logger.Info("Successfully liked asset", zap.Int64("asset_id", assetID), zap.Int64("user_id", userID), zap.Int64("star_id", starID), + zap.Int32("new_like_count", likeResp.LikeCount), ) // 缓存失效 _ = database.InvalidateAssetLikersCache(ctx, assetID) - return nil + return likeResp.LikeCount, nil } // UnlikeAsset 取消点赞资产 diff --git a/docker/.env.prod b/docker/.env.prod index 3352095..09f1fc2 100644 --- a/docker/.env.prod +++ b/docker/.env.prod @@ -30,3 +30,10 @@ REDIS_HOST=topfans-redis REDIS_PORT=6379 REDIS_PASSWORD=123456 REDIS_DB=0 + +# ==================== SMS Configuration ==================== +SMS_ACCESS_KEY_ID=LTAI5t6QcdJHpYbCPxM8SXYE +SMS_ACCESS_KEY_SECRET=ybvjSEb7wilMt3qT5nOppYPoNVayCD +SMS_SIGN_NAME=TopFans +SMS_TEMPLATE_CODE=SMS_314621237 +SMS_REGION=cn-hangzhou diff --git a/docker/Dockerfile.services b/docker/Dockerfile.services index b6486d4..703761d 100644 --- a/docker/Dockerfile.services +++ b/docker/Dockerfile.services @@ -120,10 +120,10 @@ RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /tmp/galleryservice /app/galleryservice -EXPOSE 20004 +EXPOSE 20001 HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:20004 || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:21001 || exit 1 ENTRYPOINT ["/app/galleryservice"] @@ -135,10 +135,10 @@ RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /tmp/activityservice /app/activityservice -EXPOSE 20005 +EXPOSE 20004 HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:20005 || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:21004 || exit 1 ENTRYPOINT ["/app/activityservice"] @@ -165,9 +165,9 @@ RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /tmp/starbookservice /app/starbookservice -EXPOSE 20007 +EXPOSE 20005 HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:21007 || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:21005 || exit 1 ENTRYPOINT ["/app/starbookservice"] diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index d6d3703..ce68a68 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -63,12 +63,14 @@ services: image: redis:latest container_name: topfans-redis restart: always + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:-123456} ports: - "6379:6379" networks: - topfans-net healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-123456}", "ping"] interval: 30s timeout: 10s retries: 5 @@ -117,9 +119,20 @@ services: DB_USER: postgres DB_PASSWORD: ${DB_PASSWORD:-postgres123} DB_NAME: topfans + REDIS_HOST: topfans-redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:-123456} + REDIS_DB: 0 + SMS_ACCESS_KEY_ID: ${SMS_ACCESS_KEY_ID:-} + SMS_ACCESS_KEY_SECRET: ${SMS_ACCESS_KEY_SECRET:-} + SMS_SIGN_NAME: ${SMS_SIGN_NAME:-} + SMS_TEMPLATE_CODE: ${SMS_TEMPLATE_CODE:-} + SMS_REGION: ${SMS_REGION:-cn-hangzhou} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy networks: - topfans-net expose: @@ -276,9 +289,15 @@ services: DB_PASSWORD: ${DB_PASSWORD:-postgres123} DB_NAME: topfans USER_SERVICE_URL: tri://userservice:20000 + REDIS_HOST: topfans-redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:-123456} + REDIS_DB: 0 depends_on: userservice: condition: service_started + redis: + condition: service_healthy networks: - topfans-net expose: @@ -342,7 +361,7 @@ services: restart: always environment: <<: *common-env - PORT: 20007 + PORT: 20005 DB_HOST: postgres DB_PORT: 5432 DB_USER: postgres @@ -357,9 +376,9 @@ services: networks: - topfans-net expose: - - "20007" + - "20005" healthcheck: - test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:21007 || exit 1"] + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:21005 || exit 1"] <<: *healthcheck deploy: resources: @@ -391,8 +410,11 @@ services: DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20001 DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20004 DUBBO_TASK_SERVICE_URL: tri://taskservice:20006 - DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20007 + DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20005 REDIS_HOST: topfans-redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:-123456} + REDIS_DB: 0 depends_on: userservice: condition: service_started @@ -408,6 +430,8 @@ services: condition: service_started starbookservice: condition: service_started + redis: + condition: service_healthy networks: - topfans-net ports: diff --git a/frontend/pages/square/components/CreationGrid.vue b/frontend/pages/square/components/CreationGrid.vue index 49e84a8..71eb495 100644 --- a/frontend/pages/square/components/CreationGrid.vue +++ b/frontend/pages/square/components/CreationGrid.vue @@ -2,6 +2,10 @@ + + + + #{{ item.certificate_id }} @@ -47,6 +51,7 @@ const creationList = ref([]) const cursor = ref('') const isLoading = ref(false) const noMore = ref(false) +const likingMap = ref({}) let isComponentMounted = false const formatCount = (count) => { @@ -141,11 +146,28 @@ watch(() => props.isActive, (active) => { onMounted(() => { isComponentMounted = true + // 监听点赞事件,实时更新点赞数 + uni.$on('assetLiked', ({ asset_id, data }) => { + console.log('[CreationGrid] 收到点赞事件', asset_id, data) + // 触发动画 + likingMap.value = { ...likingMap.value, [asset_id]: true } + setTimeout(() => { + likingMap.value = { ...likingMap.value, [asset_id]: false } + }, 600) + const idx = creationList.value.findIndex(c => c.id === asset_id) + console.log('[CreationGrid] 卡片索引:', idx, '总卡片数:', creationList.value.length) + if (idx !== -1 && data && typeof data.new_like_count === 'number') { + console.log('[CreationGrid] 更新点赞数:', creationList.value[idx].like_count, '->', data.new_like_count) + creationList.value[idx].like_count = data.new_like_count + creationList.value = [...creationList.value] + } + }) loadUsers() }) onUnmounted(() => { isComponentMounted = false + uni.off('assetLiked') }) defineExpose({ loadMore }) @@ -168,6 +190,7 @@ defineExpose({ loadMore }) overflow: hidden; backdrop-filter: blur(10rpx); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); + position: relative; } .creation-image { @@ -175,7 +198,44 @@ defineExpose({ loadMore }) height: 400rpx; } +/* 光波动画 */ +.wf-like-wave { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 400rpx; + pointer-events: none; + opacity: 0; + z-index: 1; +} + +.wf-like-wave-outer { + background: radial-gradient(circle, rgba(255, 107, 107, 0.8) 0%, transparent 70%); +} + +.wf-like-wave-inner { + background: radial-gradient(circle, rgba(255, 184, 0, 0.6) 0%, transparent 70%); +} + +.wf-like-wave-active { + animation: likeWave 0.6s ease-out forwards; +} + +@keyframes likeWave { + 0% { + opacity: 0.9; + transform: scale(0.8); + } + 100% { + opacity: 0; + transform: scale(1.5); + } +} + .creation-info { + position: relative; + z-index: 2; padding: 16rpx; } diff --git a/frontend/pages/square/components/WaterfallGrid.vue b/frontend/pages/square/components/WaterfallGrid.vue index 0a3aa98..5662489 100644 --- a/frontend/pages/square/components/WaterfallGrid.vue +++ b/frontend/pages/square/components/WaterfallGrid.vue @@ -918,6 +918,15 @@ onMounted(() => { isIOS = sysInfo.platform === 'ios' const containerH = props.screenHeight - props.bannerBottom layout = new WaterfallLayout(props.screenWidth, containerH, GAP) + // 监听点赞事件,实时更新点赞数 + uni.$on('assetLiked', ({ asset_id, data }) => { + const idx = cards.value.findIndex(c => c.id === asset_id) + if (idx !== -1 && data && typeof data.likes === 'number') { + cards.value[idx].likes = data.likes + // 强制触发响应式更新 + cards.value = [...cards.value] + } + }) loadUsers().then(() => { isInitialLoading = false nextTick(() => { @@ -1034,6 +1043,7 @@ onUnmounted(() => { try { uni.offAppShow(onAppShowHandler) uni.offAppHide(onAppHideHandler) + uni.off('assetLiked') } catch (_) { } } }) diff --git a/frontend/pages/square/square.vue b/frontend/pages/square/square.vue index eb1c803..01131c9 100644 --- a/frontend/pages/square/square.vue +++ b/frontend/pages/square/square.vue @@ -97,6 +97,7 @@ import BannerCarousel from './components/BannerCarousel.vue' import CreationGrid from './components/CreationGrid.vue' import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js' import { useBanner } from './composables/useBanner.js' +import { doubleTapLike } from '@/utils/likeHelper.js' import { USE_MOCK_DATA } from './config/mockData.js' // ========== Store & User Info ========== @@ -111,6 +112,8 @@ const showRankingModal = ref(false) const isActive = ref(true) const isFixed = ref(false) const creationGridRef = ref(null) +const cardTapTimers = {} +const likingMap = ref({}) // ========== 分类配置 ========== const categories = ref([ @@ -145,7 +148,32 @@ const bannerBottomPx = computed(() => Math.round(screenWidth.value / 750 * 715)) // ========== Handlers ========== const handleCardClick = (card) => { - // CreationGrid 组件内部已处理单击跳转和双击点赞 + if (cardTapTimers[card.id]) { + // 第二次点击,双击点赞 + clearTimeout(cardTapTimers[card.id]); + delete cardTapTimers[card.id]; + + + // 触发动画 + likingMap.value = { ...likingMap.value, [card.id]: true }; + setTimeout(() => { + likingMap.value = { ...likingMap.value, [card.id]: false }; + }, 600); + + doubleTapLike(card.id, card.exhibition_id || 0, (success) => { + if (success) { + uni.showToast({ title: '点赞成功', icon: 'success' }) + } + }); + } else { + // 第一次点击,单击跳转 + if (card.id) { + cardTapTimers[card.id] = setTimeout(() => { + delete cardTapTimers[card.id]; + uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${card.id}` }); + }, 300); + } + } } const handleScrollToLower = () => { diff --git a/frontend/utils/likeHelper.js b/frontend/utils/likeHelper.js index 1a0a098..02c580d 100644 --- a/frontend/utils/likeHelper.js +++ b/frontend/utils/likeHelper.js @@ -54,7 +54,7 @@ export function doubleTapLike(assetId, exhibitionId, callback) { // } likeAssetApi(assetId).then(res => { - console.log('点赞成功', res); + console.log('[likeHelper] 点赞成功', res); // 记录点赞 try { const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {}; @@ -64,6 +64,7 @@ export function doubleTapLike(assetId, exhibitionId, callback) { console.error('存储点赞记录失败:', e); } // 触发全局点赞成功事件 + console.log('[likeHelper] 触发 assetLiked 事件', { asset_id: assetId, exhibition_id: actualExhibitionId, data: res.data }); uni.$emit('assetLiked', { asset_id: assetId, exhibition_id: actualExhibitionId,