diff --git a/backend/services/assetService/repository/season_repository.go b/backend/services/assetService/repository/season_repository.go index 56fb601..168b02f 100644 --- a/backend/services/assetService/repository/season_repository.go +++ b/backend/services/assetService/repository/season_repository.go @@ -33,7 +33,8 @@ func (r *SeasonRepository) GetActiveSeason() (*models.Season, error) { func (r *SeasonRepository) GetEndedSeasons() ([]*models.Season, error) { var seasons []*models.Season - err := r.db.Where("status = ? AND end_time < ?", "active", gorm.Expr("NOW()")).Find(&seasons).Error + // end_time 是 bigint 毫秒时间戳,需用 EXTRACT(EPOCH FROM NOW()) * 1000 转为毫秒 + err := r.db.Where("status = ? AND end_time < ?", "active", gorm.Expr("EXTRACT(EPOCH FROM NOW()) * 1000")).Find(&seasons).Error return seasons, err } diff --git a/docker/Dockerfile.services b/docker/Dockerfile.services index f11fac2..7f1a727 100644 --- a/docker/Dockerfile.services +++ b/docker/Dockerfile.services @@ -54,7 +54,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ echo "Built aichatservice" && \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ -o /tmp/lasercompositor services/laserCompositor/main.go && \ - echo "Built lasercompositor" + echo "Built lasercompositor" && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ + -o /tmp/statisticservice services/statisticService/main.go && \ + echo "Built statisticservice" # ---- Runtime Stage: Gateway ---- FROM --platform=linux/amd64 alpine:3.19 AS gateway @@ -207,3 +210,18 @@ HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:7002/health || exit 1 ENTRYPOINT ["/app/lasercompositor"] + +# ---- Runtime Stage: StatisticService (数据看板微服务) ---- +FROM --platform=linux/amd64 alpine:3.19 AS statisticservice + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app +COPY --from=builder /tmp/statisticservice /app/statisticservice + +EXPOSE 20009 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:21009/healthz || exit 1 + +ENTRYPOINT ["/app/statisticservice"] diff --git a/docker/build.sh b/docker/build.sh index 9b087d3..cc17d62 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -77,7 +77,8 @@ while [[ $# -gt 0 ]]; do echo "" echo "服务名 (可选):" echo " gateway, userService, socialService, assetService," - echo " galleryService, activityService, taskService, starbookService, aiChatService" + echo " galleryService, activityService, taskService, starbookService, aiChatService," + echo " laserCompositor, statisticService" echo "" echo "示例:" echo " $0 # 构建所有服务" @@ -99,6 +100,7 @@ while [[ $# -gt 0 ]]; do task|taskService) SERVICES+=("taskService") ;; starbook|starbookService) SERVICES+=("starbookService") ;; ai|aiChatService|aichatservice) SERVICES+=("aiChatService") ;; + statistic|statisticService|statisticservice) SERVICES+=("statisticservice") ;; all) # all 关键字,构建所有服务 SERVICES=() @@ -117,7 +119,7 @@ done # ==================== 服务列表 ==================== # 所有可用服务及其配置(使用小写 target 名) -ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice" "aichatservice" "lasercompositor") +ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice" "aichatservice" "lasercompositor" "statisticservice") # 确定要构建的服务 if [ ${#SERVICES[@]} -eq 0 ]; then @@ -208,6 +210,7 @@ main() { starbookservice) docker_target="starbookservice" ;; aichatservice) docker_target="aichatservice" ;; lasercompositor) docker_target="lasercompositor" ;; + statisticservice) docker_target="statisticservice" ;; # 兼容旧的大写服务名 userService) docker_target="userservice" ;; socialService) docker_target="socialservice" ;; @@ -216,6 +219,8 @@ main() { activityService) docker_target="activityservice" ;; taskService) docker_target="taskservice" ;; starbookService) docker_target="starbookservice" ;; + statisticService) docker_target="statisticservice" ;; + statisticservice) docker_target="statisticservice" ;; *) docker_target="$service" ;; esac diff --git a/docker/cleanup-logs.sh b/docker/cleanup-logs.sh index 9dd8043..8322ef6 100755 --- a/docker/cleanup-logs.sh +++ b/docker/cleanup-logs.sh @@ -1,13 +1,18 @@ #!/bin/bash # =================================================================== -# Docker 容器日志 & 系统日志清理脚本 -# 功能:当容器日志或 /var/log 超过阈值时自动清理 +# Docker 容器日志 & 镜像 & 系统日志 自动清理脚本 +# 功能: +# 1. 容器日志超过阈值时自动 truncate +# 2. 清理悬空 Docker 镜像 (:) +# 3. 清理旧版本 topfans/* 镜像(保留 latest 和 v1.0.7) +# 4. 清理 /var/log 系统日志 # 使用:./cleanup-logs.sh [容器日志阈值GB] [系统日志阈值GB] # 示例:./cleanup-logs.sh 2 2 +# Cron 建议:0 3 * * * /opt/topfans/docker/cleanup-logs.sh 2 2 >> /var/log/cleanup-logs.log 2>&1 # =================================================================== -CONTAINER_THRESHOLD=${1:-2} # 默认容器日志超过 2GB 才清理 -SYSLOG_THRESHOLD=${2:-2} # 默认系统日志超过 2GB 才清理 +CONTAINER_THRESHOLD=${1:-2} +SYSLOG_THRESHOLD=${2:-2} echo "==========================================" echo "日志清理脚本" @@ -23,8 +28,8 @@ total_container_log=0 cleared_containers=0 for container in $(docker ps -q); do - log_file=$(docker inspect --format='{{.LogPath}}' "$container" 2>/dev/null) - container_name=$(docker inspect --format='{{.Name}}' "$container" 2>/dev/null | sed 's/^\///') + log_file=$(docker inspect --format="{{.LogPath}}" "$container" 2>/dev/null) + container_name=$(docker inspect --format="{{.Name}}" "$container" 2>/dev/null | sed "s/^\///") if [ -f "$log_file" ]; then size_bytes=$(stat -c%s "$log_file" 2>/dev/null || echo 0) @@ -44,14 +49,47 @@ total_container_gb=$((total_container_log / 1024 / 1024 / 1024)) echo "容器日志总大小: ${total_container_gb}GB" echo "已清理容器数: $cleared_containers" -# ========== 2. 清理 /var/log 日志 ========== +# ========== 2. 清理悬空 Docker 镜像 ========== +echo "" +echo "=== 检查悬空镜像 ===" + +dangling_before=$(docker images -f "dangling=true" -q | wc -l) +echo "悬空镜像数量: ${dangling_before}" + +if [ "$dangling_before" -gt 0 ]; then + docker image prune -f > /dev/null 2>&1 + echo " ✅ 已清理 ${dangling_before} 个悬空镜像" +else + echo " (无悬空镜像)" +fi + +# ========== 3. 清理旧版本 topfans/* 镜像 ========== +echo "" +echo "=== 检查旧版本业务镜像 ===" + +KEEP_TAGS="latest v1.0.7" +old_images=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "^topfans/" | grep -vE ":($(echo $KEEP_TAGS | tr " " "|"))$") + +old_count=0 +if [ -n "$old_images" ]; then + old_count=$(echo "$old_images" | grep -c "topfans/" 2>/dev/null || echo 0) +fi +if [ "$old_count" -gt 0 ]; then + echo " 发现 ${old_count} 个旧版本镜像:" + echo "$old_images" | sed "s/^/ /" + echo "$old_images" | xargs -r docker rmi > /dev/null 2>&1 + echo " ✅ 已清理" +else + echo " (无旧版本镜像)" +fi + +# ========== 4. 清理 /var/log 日志 ========== echo "" echo "=== 检查系统日志 ===" total_syslog_size=0 cleared_logs=0 -# 找出 /var/log 下超过阈值的大文件并清理 for logfile in $(find /var/log -type f -name "*.log" 2>/dev/null); do if [ -f "$logfile" ]; then size_bytes=$(stat -c%s "$logfile" 2>/dev/null || echo 0) @@ -67,23 +105,26 @@ for logfile in $(find /var/log -type f -name "*.log" 2>/dev/null); do fi done -# 也清理旧的压缩日志 for gzfile in $(find /var/log -type f -name "*.gz" 2>/dev/null | head -20); do if [ -f "$gzfile" ]; then rm -f "$gzfile" 2>/dev/null && echo " ✅ 已删: $gzfile" fi done -# 清理旧的 log.1, log.2 等轮转文件 for oldlog in $(find /var/log -type f -name "*.log.[0-9]*" 2>/dev/null); do rm -f "$oldlog" 2>/dev/null && echo " ✅ 已删: $oldlog" done +if command -v journalctl > /dev/null 2>&1; then + journalctl --vacuum-time=3d > /dev/null 2>&1 + echo " ✅ journal 已清理(保留最近 3 天)" +fi + total_syslog_gb=$((total_syslog_size / 1024 / 1024 / 1024)) echo "系统日志总大小: ${total_syslog_gb}GB" echo "已清理日志数: $cleared_logs" -# ========== 3. 磁盘状态 ========== +# ========== 5. 磁盘状态 ========== echo "" echo "=== 当前磁盘状态 ===" df -h / diff --git a/docker/deploy.sh b/docker/deploy.sh index 54930fe..74308f5 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -80,6 +80,7 @@ SERVICES=( "starbookservice" "aichatservice" "lasercompositor" + "statisticservice" ) # ==================== 服务器配置 ==================== diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml index 026305b..1d7125e 100644 --- a/docker/docker-compose.local.yml +++ b/docker/docker-compose.local.yml @@ -333,6 +333,50 @@ services: reservations: memory: 256M + # ==================== Statistic Service (数据看板微服务) ==================== + statisticservice: + image: topfans/statisticservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: statisticservice + container_name: topfans-statisticservice + restart: unless-stopped + environment: + <<: *common-env + PORT: 20009 + STATISTIC_DB_HOST: host.docker.internal + STATISTIC_DB_PORT: 15432 + STATISTIC_DB_USER: postgres + STATISTIC_DB_PASSWORD: ${DB_PASSWORD:-123456} + STATISTIC_DB_NAME: ${DB_NAME:-top-fans} + STATISTIC_DB_SSLMODE: disable + STATISTIC_DB_SCHEMA: statistic + STATISTIC_REDIS_HOST: host.docker.internal + STATISTIC_REDIS_PORT: 6379 + STATISTIC_REDIS_PASSWORD: ${REDIS_PASSWORD:-123456} + STATISTIC_REDIS_DB: 0 + # 跨服务调用 userService + USER_SERVICE_URL: tri://userservice:20000 + depends_on: + userservice: + condition: service_healthy + networks: + - topfans-net + extra_hosts: + - "host.docker.internal:host-gateway" + expose: + - "20009" + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 20009 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + # ==================== API Gateway ==================== gateway: image: topfans/gateway:latest @@ -356,6 +400,7 @@ services: DIFY_API_BASE: ${DIFY_API_BASE:-http://host.docker.internal:8081/v1} DIFY_API_KEY: ${DIFY_API_KEY:-} DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008 + DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009 # 镭射卡 AI 生成(MiniMax 文生图) MINIMAX_API_KEY: ${MINIMAX_API_KEY:-} MINIMAX_API_URL: ${MINIMAX_API_URL:-https://api.minimaxi.com/v1/image_generation} @@ -389,10 +434,14 @@ services: condition: service_healthy aichatservice: condition: service_healthy + statisticservice: + condition: service_healthy lasercompositor: condition: service_healthy networks: - - topfans-net + topfans-net: + aliases: + - newbackend ports: - "8080:8080" healthcheck: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 5ceaa61..1261457 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -457,6 +457,56 @@ services: memory: 128M cpus: '0.25' + # ==================== Statistic Service (数据看板微服务) ==================== + statisticservice: + image: topfans/statisticservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: statisticservice + container_name: topfans-statisticservice + restart: always + env_file: + - .env.prod + environment: + <<: *common-env + PORT: 20009 + # statistic 服务使用独立 schema(与 common-env 中的 DB_* 解耦) + STATISTIC_DB_HOST: postgres + STATISTIC_DB_PORT: 5432 + STATISTIC_DB_USER: postgres + STATISTIC_DB_PASSWORD: ${DB_PASSWORD:-postgres123} + STATISTIC_DB_NAME: topfans + STATISTIC_DB_SSLMODE: disable + STATISTIC_DB_SCHEMA: statistic + # Redis + STATISTIC_REDIS_HOST: topfans-redis + STATISTIC_REDIS_PORT: 6379 + STATISTIC_REDIS_PASSWORD: ${REDIS_PASSWORD:-123456} + STATISTIC_REDIS_DB: 0 + # 跨服务调用 userService(看板 GetTodayOverview 需要 crystal_balance) + USER_SERVICE_URL: tri://userservice:20000 + depends_on: + userservice: + condition: service_started + redis: + condition: service_healthy + networks: + - topfans-net + expose: + - "20009" + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:21009/healthz || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 300M + cpus: '0.5' + reservations: + memory: 128M + cpus: '0.25' + # ==================== API Gateway ==================== gateway: image: topfans/gateway:latest @@ -479,6 +529,7 @@ services: DUBBO_TASK_SERVICE_URL: tri://taskservice:20006 DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20005 DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008 + DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009 LASER_COMPOSITOR_URL: http://lasercompositor:7002 # 抠图(人像扣底)、OSS、Dify、JWT、Redis 全部走 env_file: .env.prod REDIS_HOST: topfans-redis @@ -501,12 +552,16 @@ services: condition: service_started aichatservice: condition: service_started + statisticservice: + condition: service_started lasercompositor: condition: service_started redis: condition: service_healthy networks: - - topfans-net + topfans-net: + aliases: + - newbackend ports: - "8080:8080" healthcheck: diff --git a/docker/sql/migrations/2026_06_08_001_statistic_events.sql b/docker/sql/migrations/2026_06_08_001_statistic_events.sql new file mode 100644 index 0000000..928278d --- /dev/null +++ b/docker/sql/migrations/2026_06_08_001_statistic_events.sql @@ -0,0 +1,37 @@ +-- statistic 服务 events 原始表 +-- 创建时间: 2026-06-08 +-- 说明: 事件原始表,按 received_at 按日分区 +-- 关联: spec §3.2 + +-- 1. 创建 schema +CREATE SCHEMA IF NOT EXISTS statistic; + +-- 2. 创建 events 主表(按 received_at 按日分区) +CREATE TABLE IF NOT EXISTS statistic.events ( + id BIGSERIAL, + event_id UUID NOT NULL, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + event_type VARCHAR(64) NOT NULL, + occurred_at TIMESTAMPTZ NOT NULL, + received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + properties JSONB NOT NULL DEFAULT '{}', + + PRIMARY KEY (id, received_at) +) PARTITION BY RANGE (received_at); + +-- 3. 唯一约束(去重):同一 event_id 不能重复 +CREATE UNIQUE INDEX IF NOT EXISTS idx_events_event_id + ON statistic.events (event_id, received_at); + +-- 4. 看板查询主索引(覆盖 90% 查询) +CREATE INDEX IF NOT EXISTS idx_events_user_star_type_time + ON statistic.events (user_id, star_id, event_type, received_at DESC); + +-- 5. 趋势分析索引 +CREATE INDEX IF NOT EXISTS idx_events_star_type_time + ON statistic.events (star_id, event_type, received_at DESC); + +-- 6. JSONB 属性 GIN 索引 +CREATE INDEX IF NOT EXISTS idx_events_properties_gin + ON statistic.events USING GIN (properties); diff --git a/docker/sql/migrations/2026_06_08_002_statistic_mv_daily_user_income.sql b/docker/sql/migrations/2026_06_08_002_statistic_mv_daily_user_income.sql new file mode 100644 index 0000000..125c567 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_002_statistic_mv_daily_user_income.sql @@ -0,0 +1,24 @@ +-- statistic 服务 MV1: 每日用户水晶收益 +-- 创建时间: 2026-06-08 +-- 服务于: 七日收益曲线、今日收益 +-- 关联: spec §3.4 MV1 + +CREATE MATERIALIZED VIEW IF NOT EXISTS statistic.mv_daily_user_income AS +SELECT + user_id, + star_id, + DATE(received_at AT TIME ZONE 'Asia/Shanghai') AS income_date, + SUM( + CASE + WHEN event_type IN ('exhibition.revenue', 'crystal.change') + AND COALESCE((properties->>'amount')::BIGINT, 0) > 0 + THEN COALESCE((properties->>'amount')::BIGINT, 0) + ELSE 0 + END + ) AS total_crystal +FROM statistic.events +WHERE event_type IN ('exhibition.revenue', 'crystal.change') +GROUP BY user_id, star_id, income_date; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_daily_user_income_pk + ON statistic.mv_daily_user_income (user_id, star_id, income_date); diff --git a/docker/sql/migrations/2026_06_08_003_statistic_mv_daily_exhibition_revenue.sql b/docker/sql/migrations/2026_06_08_003_statistic_mv_daily_exhibition_revenue.sql new file mode 100644 index 0000000..f705dc3 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_003_statistic_mv_daily_exhibition_revenue.sql @@ -0,0 +1,19 @@ +-- statistic 服务 MV2: 每日展出收益(按藏品) +-- 创建时间: 2026-06-08 +-- 服务于: 展出收益中心(top5)、藏品矩阵 TOP5 +-- 关联: spec §3.4 MV2 + +CREATE MATERIALIZED VIEW IF NOT EXISTS statistic.mv_daily_exhibition_revenue AS +SELECT + user_id, + star_id, + (properties->>'asset_id')::BIGINT AS asset_id, + DATE(received_at AT TIME ZONE 'Asia/Shanghai') AS revenue_date, + SUM(COALESCE((properties->>'duration_ms')::BIGINT, 0)) AS total_duration_ms, + SUM(COALESCE((properties->>'amount')::BIGINT, 0)) AS total_earnings +FROM statistic.events +WHERE event_type = 'exhibition.revenue' +GROUP BY user_id, star_id, asset_id, revenue_date; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_exhibition_revenue_pk + ON statistic.mv_daily_exhibition_revenue (user_id, star_id, asset_id, revenue_date); diff --git a/docker/sql/migrations/2026_06_08_004_statistic_mv_daily_like_income.sql b/docker/sql/migrations/2026_06_08_004_statistic_mv_daily_like_income.sql new file mode 100644 index 0000000..94176a2 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_004_statistic_mv_daily_like_income.sql @@ -0,0 +1,26 @@ +-- statistic 服务 MV3: 每日点赞按等级 +-- 创建时间: 2026-06-08 +-- 服务于: 点赞收益按等级(累计) +-- 关联: spec §3.4 MV3 +-- 关联依赖: public.assets 表 +-- 修复: a.level → a.grade(assets 表没有 level 列,星册等级在 grade 字段) +-- 防御: properties->>'asset_id' 必须存在且为数字,避免 JOIN 失败 + +CREATE MATERIALIZED VIEW IF NOT EXISTS statistic.mv_daily_like_income AS +SELECT + e.user_id, + e.star_id, + a.grade AS asset_level, + DATE(e.received_at AT TIME ZONE 'Asia/Shanghai') AS like_date, + COUNT(*) AS like_count, + SUM(COALESCE((e.properties->>'amount')::BIGINT, 0)) AS total_crystal +FROM statistic.events e +JOIN public.assets a + ON a.id = (e.properties->>'asset_id')::BIGINT +WHERE e.event_type = 'asset.like' + AND (e.properties->>'asset_id') IS NOT NULL + AND (e.properties->>'asset_id') ~ '^[0-9]+$' +GROUP BY e.user_id, e.star_id, a.grade, DATE(e.received_at AT TIME ZONE 'Asia/Shanghai'); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_like_income_pk + ON statistic.mv_daily_like_income (user_id, star_id, asset_level, like_date); diff --git a/docker/sql/migrations/2026_06_08_005_statistic_mv_asset_level_distribution.sql b/docker/sql/migrations/2026_06_08_005_statistic_mv_asset_level_distribution.sql new file mode 100644 index 0000000..37bae19 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_005_statistic_mv_asset_level_distribution.sql @@ -0,0 +1,21 @@ +-- statistic 服务 MV4: 藏品等级分布 +-- 创建时间: 2026-06-08 +-- 服务于: 藏品等级分布环形图 +-- 关联: spec §3.4 MV4 +-- 修复: 字段名按实际 assets 表 schema 调整 +-- - user_id → owner_uid AS user_id +-- - status='active' AND deleted_at IS NULL → is_active=true AND deleted_at IS NULL +-- - level → COALESCE(grade::text, 'UNKNOWN')(grade 可能为 NULL) + +CREATE MATERIALIZED VIEW IF NOT EXISTS statistic.mv_asset_level_distribution AS +SELECT + owner_uid AS user_id, + star_id, + COALESCE(grade::TEXT, 'UNKNOWN') AS asset_level, + COUNT(*) AS asset_count +FROM public.assets +WHERE is_active = TRUE AND deleted_at IS NULL +GROUP BY owner_uid, star_id, grade; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_level_dist_pk + ON statistic.mv_asset_level_distribution (user_id, star_id, asset_level); diff --git a/docker/sql/migrations/2026_06_08_006_statistic_metric_weekly_user_income.sql b/docker/sql/migrations/2026_06_08_006_statistic_metric_weekly_user_income.sql new file mode 100644 index 0000000..0536248 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_006_statistic_metric_weekly_user_income.sql @@ -0,0 +1,19 @@ +-- statistic 服务 metric_weekly_user_income: 本周收入 + 排名(预聚合) +-- 创建时间: 2026-06-08 +-- 服务于: GetTodayOverview 的 week_rank + week_total_users +-- 关联: spec §3.5 + +CREATE TABLE IF NOT EXISTS statistic.metric_weekly_user_income ( + star_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + week_start DATE NOT NULL, -- 周一日期(Asia/Shanghai) + total_crystal BIGINT NOT NULL DEFAULT 0, + rank_in_star INT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (star_id, user_id, week_start) +); + +-- Worker 维护,查询走索引 +CREATE INDEX IF NOT EXISTS idx_metric_weekly_rank + ON statistic.metric_weekly_user_income (star_id, week_start, rank_in_star); diff --git a/docker/sql/migrations/2026_06_08_007_statistic_metric_recent_level_ups.sql b/docker/sql/migrations/2026_06_08_007_statistic_metric_recent_level_ups.sql new file mode 100644 index 0000000..f1b4d17 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_007_statistic_metric_recent_level_ups.sql @@ -0,0 +1,21 @@ +-- statistic 服务 metric_recent_level_ups: 最近升级记录(预聚合,保留 30 天) +-- 创建时间: 2026-06-08 +-- 服务于: GetAssetUpgradeProgress 的 recent[] +-- 关联: spec §3.5 +-- 清理: 30 天前的记录由 worker 定时 DELETE + +CREATE TABLE IF NOT EXISTS statistic.metric_recent_level_ups ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + asset_id BIGINT NOT NULL, + from_level VARCHAR(8) NOT NULL, + to_level VARCHAR(8) NOT NULL, + upgrade_time TIMESTAMPTZ NOT NULL, + asset_name VARCHAR(128), + asset_thumb VARCHAR(512) +); + +-- 看板查询主索引 +CREATE INDEX IF NOT EXISTS idx_recent_level_ups_user + ON statistic.metric_recent_level_ups (user_id, star_id, upgrade_time DESC); diff --git a/docker/sql/migrations/2026_06_08_008_statistic_metric_upcoming_level_ups.sql b/docker/sql/migrations/2026_06_08_008_statistic_metric_upcoming_level_ups.sql new file mode 100644 index 0000000..cd3839a --- /dev/null +++ b/docker/sql/migrations/2026_06_08_008_statistic_metric_upcoming_level_ups.sql @@ -0,0 +1,16 @@ +-- statistic 服务 metric_upcoming_level_ups: 即将升级进度(预聚合) +-- 创建时间: 2026-06-08 +-- 服务于: GetAssetUpgradeProgress 的 upcoming[] +-- 关联: spec §3.5 +-- 维护: Worker 每 15 分钟从 public.assets + 升级阈值配置全量重算 + +CREATE TABLE IF NOT EXISTS statistic.metric_upcoming_level_ups ( + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + asset_id BIGINT NOT NULL, + like_progress INT NOT NULL, -- 0-100 + duration_progress INT NOT NULL, -- 0-100 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (user_id, star_id, asset_id) +); diff --git a/docker/sql/migrations/2026_06_08_009_statistic_refresh_log.sql b/docker/sql/migrations/2026_06_08_009_statistic_refresh_log.sql new file mode 100644 index 0000000..b3de96a --- /dev/null +++ b/docker/sql/migrations/2026_06_08_009_statistic_refresh_log.sql @@ -0,0 +1,18 @@ +-- statistic 服务 refresh_log: 物化视图刷新日志 +-- 创建时间: 2026-06-08 +-- 关联: spec §3.6 +-- 用途: 监控物化视图刷新状态 + 失败告警 + +CREATE TABLE IF NOT EXISTS statistic.refresh_log ( + id BIGSERIAL PRIMARY KEY, + mv_name VARCHAR(128) NOT NULL, + started_at TIMESTAMPTZ NOT NULL, + finished_at TIMESTAMPTZ, + row_count BIGINT, + status VARCHAR(16) NOT NULL, -- 'running' / 'success' / 'failed' + error_message TEXT +); + +-- 监控查询主索引 +CREATE INDEX IF NOT EXISTS idx_refresh_log_mv_time + ON statistic.refresh_log (mv_name, started_at DESC); diff --git a/docker/sql/migrations/2026_06_08_010_statistic_partitions_initial.sql b/docker/sql/migrations/2026_06_08_010_statistic_partitions_initial.sql new file mode 100644 index 0000000..22bc5b8 --- /dev/null +++ b/docker/sql/migrations/2026_06_08_010_statistic_partitions_initial.sql @@ -0,0 +1,21 @@ +-- statistic 服务 events 表初始 7 天分区 +-- 创建时间: 2026-06-08 +-- 关联: spec §3.3 分区管理 +-- 说明: 手动运行一次或由 partitioner worker 自动运行 +-- (worker/partitioner.go 实现,自动调度见 plan Task 8) + +DO $$ +DECLARE + i INT; + d DATE; + n DATE; +BEGIN + FOR i IN 0..6 LOOP + d := CURRENT_DATE + i; + n := d + 1; + EXECUTE format( + 'CREATE TABLE IF NOT EXISTS statistic.events_%s PARTITION OF statistic.events FOR VALUES FROM (%L) TO (%L)', + to_char(d, 'YYYY_MM_DD'), d::text, n::text + ); + END LOOP; +END $$; diff --git a/frontend/.env.development b/frontend/.env.development index 76cf1da..1702519 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,6 +1,7 @@ # 开发环境配置 # HBuilderX「运行」时自动加载;CLI 用 --mode development -VITE_API_BASE_URL=http://192.168.110.60:8080 +# VITE_API_BASE_URL=http://192.168.110.60:8080 +VITE_API_BASE_URL=https://api.topfans.online # WebSocket 地址:如与 API 同源可省略(自动从 VITE_API_BASE_URL 推导 http→ws、https→wss) # 独立部署时直接覆盖,例如:ws://192.168.110.60:8081 VITE_WS_BASE_URL=ws://192.168.110.60:8080 diff --git a/frontend/manifest.json b/frontend/manifest.json index 9b7c026..064297b 100644 --- a/frontend/manifest.json +++ b/frontend/manifest.json @@ -3,7 +3,7 @@ "appid" : "__UNI__F199FF4", "description" : "", "versionName" : "1.0.5", - "versionCode" : 109, + "versionCode" : 111, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : {