From 94457ffa8a034d717d8b70a5f63befa3b8a1994c Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Tue, 23 Jun 2026 19:37:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E9=81=97=E7=95=99bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/Dockerfile.services | 20 +++++- docker/build.sh | 6 +- docker/deploy.sh | 1 + docker/docker-compose.local.yml | 45 ++++++++++++ docker/docker-compose.prod.yml | 48 +++++++++++++ .../square/components/HotCategoryBlock.vue | 69 ++++++++++++++++--- frontend/pages/square/square.vue | 18 ++++- 7 files changed, 195 insertions(+), 12 deletions(-) diff --git a/docker/Dockerfile.services b/docker/Dockerfile.services index be59782..3ded775 100644 --- a/docker/Dockerfile.services +++ b/docker/Dockerfile.services @@ -60,7 +60,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ echo "Built statisticservice" && \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ -o /tmp/notificationservice services/notificationService/main.go && \ - echo "Built notificationservice" + echo "Built notificationservice" && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ + -o /tmp/moderationservice services/moderationService/main.go && \ + echo "Built moderationservice" # ---- Runtime Stage: Gateway ---- FROM --platform=linux/amd64 alpine:3.19 AS gateway @@ -243,3 +246,18 @@ HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:21010/healthz || exit 1 ENTRYPOINT ["/app/notificationservice"] + +# ---- Runtime Stage: ModerationService (举报 / 反馈 / 自动隐藏) ---- +FROM --platform=linux/amd64 alpine:3.19 AS moderationservice + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app +COPY --from=builder /tmp/moderationservice /app/moderationservice + +EXPOSE 20011 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:20011 || exit 1 + +ENTRYPOINT ["/app/moderationservice"] diff --git a/docker/build.sh b/docker/build.sh index 5384e0b..52a0a20 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -79,6 +79,7 @@ while [[ $# -gt 0 ]]; do echo " gateway, userService, socialService, assetService," echo " galleryService, activityService, taskService, starbookService, aiChatService," echo " laserCompositor, statisticService" + echo " notificationService, moderationService" echo "" echo "示例:" echo " $0 # 构建所有服务" @@ -102,6 +103,7 @@ while [[ $# -gt 0 ]]; do ai|aiChatService|aichatservice) SERVICES+=("aiChatService") ;; statistic|statisticService|statisticservice) SERVICES+=("statisticservice") ;; notification|notificationService|notificationservice) SERVICES+=("notificationservice") ;; + moderation|moderationService|moderationservice) SERVICES+=("moderationservice") ;; all) # all 关键字,构建所有服务 SERVICES=() @@ -120,7 +122,7 @@ done # ==================== 服务列表 ==================== # 所有可用服务及其配置(使用小写 target 名) -ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice" "aichatservice" "lasercompositor" "statisticservice" "notificationservice") +ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice" "aichatservice" "lasercompositor" "statisticservice" "notificationservice" "moderationservice") # 确定要构建的服务 if [ ${#SERVICES[@]} -eq 0 ]; then @@ -213,6 +215,7 @@ main() { lasercompositor) docker_target="lasercompositor" ;; statisticservice) docker_target="statisticservice" ;; notificationservice) docker_target="notificationservice" ;; + moderationservice) docker_target="moderationservice" ;; # 兼容旧的大写服务名 userService) docker_target="userservice" ;; socialService) docker_target="socialservice" ;; @@ -224,6 +227,7 @@ main() { statisticService) docker_target="statisticservice" ;; statisticservice) docker_target="statisticservice" ;; notificationService) docker_target="notificationservice" ;; + moderationService) docker_target="moderationservice" ;; *) docker_target="$service" ;; esac diff --git a/docker/deploy.sh b/docker/deploy.sh index c5111ef..8cc95c5 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -82,6 +82,7 @@ SERVICES=( "lasercompositor" "statisticservice" "notificationservice" + "moderationservice" ) # ==================== 服务器配置 ==================== diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml index 87baf36..307c539 100644 --- a/docker/docker-compose.local.yml +++ b/docker/docker-compose.local.yml @@ -410,6 +410,48 @@ services: reservations: memory: 256M + # 内容审核服务(举报 / 反馈 / 自动隐藏) + # 依赖 user / asset / notification Dubbo 服务,以及 Postgres + Redis + moderationservice: + image: topfans/moderationservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: moderationservice + container_name: topfans-moderationservice + restart: unless-stopped + environment: + <<: *common-env + PORT: 20011 + REDIS_HOST: host.docker.internal + REDIS_PORT: 6379 + REDIS_DB: 0 + DUBBO_USER_SERVICE_URL: tri://userservice:20000 + DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003 + DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010 + depends_on: + userservice: + condition: service_healthy + assetservice: + condition: service_healthy + notificationservice: + condition: service_healthy + networks: + - topfans-net + extra_hosts: + - "host.docker.internal:host-gateway" + expose: + - "20011" + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 20011 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + # ==================== API Gateway ==================== gateway: image: topfans/gateway:latest @@ -435,6 +477,7 @@ services: DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008 DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009 DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010 + DUBBO_MODERATION_SERVICE_URL: tri://moderationservice:20011 # 镭射卡 AI 生成(MiniMax 文生图) MINIMAX_API_KEY: ${MINIMAX_API_KEY:-} MINIMAX_API_URL: ${MINIMAX_API_URL:-https://api.minimaxi.com/v1/image_generation} @@ -472,6 +515,8 @@ services: condition: service_healthy notificationservice: condition: service_healthy + moderationservice: + condition: service_healthy lasercompositor: condition: service_healthy networks: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 73de600..d4627e2 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -552,6 +552,51 @@ services: memory: 128M cpus: '0.25' + # 内容审核服务(举报 / 反馈 / 自动隐藏) + # 依赖 user / asset / notification Dubbo 服务,以及 Postgres + Redis + moderationservice: + image: topfans/moderationservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: moderationservice + container_name: topfans-moderationservice + restart: always + env_file: + - .env.prod + environment: + <<: *common-env + PORT: 20011 + DUBBO_USER_SERVICE_URL: tri://userservice:20000 + DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003 + DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + userservice: + condition: service_started + assetservice: + condition: service_started + notificationservice: + condition: service_started + networks: + - topfans-net + expose: + - "20011" + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:20011 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 300M + cpus: '0.5' + reservations: + memory: 128M + cpus: '0.25' + # ==================== API Gateway ==================== gateway: image: topfans/gateway:latest @@ -576,6 +621,7 @@ services: DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008 DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009 DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010 + DUBBO_MODERATION_SERVICE_URL: tri://moderationservice:20011 LASER_COMPOSITOR_URL: http://lasercompositor:7002 # 抠图(人像扣底)、OSS、Dify、JWT、Redis 全部走 env_file: .env.prod REDIS_HOST: topfans-redis @@ -602,6 +648,8 @@ services: condition: service_started notificationservice: condition: service_started + moderationservice: + condition: service_started lasercompositor: condition: service_started redis: diff --git a/frontend/pages/square/components/HotCategoryBlock.vue b/frontend/pages/square/components/HotCategoryBlock.vue index 0c2692d..92aa880 100644 --- a/frontend/pages/square/components/HotCategoryBlock.vue +++ b/frontend/pages/square/components/HotCategoryBlock.vue @@ -33,6 +33,11 @@ :lower-threshold="80" :scroll-into-view="scrollIntoView" :scroll-with-animation="false" + :refresher-enabled="true" + :refresher-triggered="refreshing" + refresher-default-style="white" + refresher-background="transparent" + @refresherrefresh="handleRefresh" @scrolltolower="handleScrollToLower" > @@ -174,6 +179,8 @@ const items = ref([]); const loading = ref(false); const likingMap = ref({}); const activeTabKey = ref(""); +// 下拉刷新状态:与 scroll-view 的 :refresher-triggered 双向绑定 +const refreshing = ref(false); // ===== 分页状态 ===== // currentPage : 已加载到的页码(从 1 开始) @@ -330,17 +337,18 @@ const resetAndLoad = () => { // 2) 立刻 set items.value → 列表瞬时出现,骨架屏立即下线 // 3) 后台并行跑 getAssetCoverRealUrl 拿到精确的预签名 URL,逐个 patch 回 items.value[i].cover_url // (Vue 3 ref 数组里的对象是 reactive 代理,单个属性变更会触发该卡片重渲染) -const loadData = async ({ append = false } = {}) => { +const loadData = async ({ append = false, silent = false } = {}) => { const tab = activeTab.value; if (!tab || typeof tab.fetch !== "function") { console.warn("[HotCategoryBlock] 当前 tab 未配置 fetch:", tab); - if (!append) items.value = []; - return; + if (!append && !silent) items.value = []; + return false; } // 首页用 loading 整屏骨架;分页用 loadingMore 底部小指示器 + // silent=true 时跳过骨架屏、保留旧 items(用于下拉刷新,避免列表闪烁/抖动) if (append) { loadingMore.value = true; - } else { + } else if (!silent) { loading.value = true; } try { @@ -401,13 +409,17 @@ const loadData = async ({ append = false } = {}) => { } else { hasMore.value = rawItems.length >= PAGE_SIZE; } + return true; } else if (!append) { - items.value = []; + if (!silent) items.value = []; hasMore.value = false; + return false; } + return false; } catch (e) { console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e); - if (!append) items.value = []; + if (!append && !silent) items.value = []; + return false; } finally { loading.value = false; loadingMore.value = false; @@ -424,6 +436,47 @@ const handleScrollToLower = () => { loadData({ append: true }); }; +// 处理下拉刷新 +// 设计原则(避免抖动): +// - 不清空 items.value:保留旧列表让用户视觉连续,避免内容高度突变 +// - 不重置 scrollIntoView:用户下拉时已经在顶部,强制滚动会和手势冲突 +// - silent=true 跳过骨架屏:loadData 内部不会触发 loading 状态切换 +// - 新数据回来后整体替换 items.value,scroll-view 平滑过渡到新列表 +const handleRefresh = async () => { + if (refreshing.value || loading.value) return; + refreshing.value = true; + try { + currentPage.value = 1; + hasMore.value = true; + loadingMore.value = false; + const success = await loadData({ append: false, silent: true }); + if (success) { + uni.showToast({ + title: "刷新成功", + icon: "success", + duration: 1200, + }); + } else { + uni.showToast({ + title: "刷新失败", + icon: "none", + duration: 1500, + }); + } + } catch (e) { + console.error("[HotCategoryBlock] 刷新失败", e?.message ?? e); + uni.showToast({ + title: "刷新失败", + icon: "none", + duration: 1500, + }); + } finally { + setTimeout(() => { + refreshing.value = false; + }, 500); + } +}; + onMounted(() => { uni.$on("assetLiked", onAssetLiked); loadData(); @@ -458,7 +511,7 @@ onUnmounted(() => { overflow: hidden; position: relative; z-index: 2; - margin-top: 32rpx; + margin-top: 16rpx; } .ranking-tabs { @@ -627,7 +680,7 @@ onUnmounted(() => { border-bottom-right-radius: 12px; border-bottom-left-radius: 12px; // opacity: 0.8; - padding: 40rpx 20rpx 32rpx; + padding: 24rpx 20rpx 32rpx; overflow: hidden; } diff --git a/frontend/pages/square/square.vue b/frontend/pages/square/square.vue index c178cf9..4dbdcdd 100644 --- a/frontend/pages/square/square.vue +++ b/frontend/pages/square/square.vue @@ -94,7 +94,7 @@