feat:docker新增统计的微服务

This commit is contained in:
zerosaturation 2026-06-11 16:35:56 +08:00
parent 3ec096ecd9
commit 280b4bbee0
19 changed files with 412 additions and 19 deletions

View File

@ -33,7 +33,8 @@ func (r *SeasonRepository) GetActiveSeason() (*models.Season, error) {
func (r *SeasonRepository) GetEndedSeasons() ([]*models.Season, error) { func (r *SeasonRepository) GetEndedSeasons() ([]*models.Season, error) {
var seasons []*models.Season 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 return seasons, err
} }

View File

@ -54,7 +54,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
echo "Built aichatservice" && \ echo "Built aichatservice" && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
-o /tmp/lasercompositor services/laserCompositor/main.go && \ -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 ---- # ---- Runtime Stage: Gateway ----
FROM --platform=linux/amd64 alpine:3.19 AS 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 CMD wget --no-verbose --tries=1 --spider http://localhost:7002/health || exit 1
ENTRYPOINT ["/app/lasercompositor"] 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"]

View File

@ -77,7 +77,8 @@ while [[ $# -gt 0 ]]; do
echo "" echo ""
echo "服务名 (可选):" echo "服务名 (可选):"
echo " gateway, userService, socialService, assetService," echo " gateway, userService, socialService, assetService,"
echo " galleryService, activityService, taskService, starbookService, aiChatService" echo " galleryService, activityService, taskService, starbookService, aiChatService,"
echo " laserCompositor, statisticService"
echo "" echo ""
echo "示例:" echo "示例:"
echo " $0 # 构建所有服务" echo " $0 # 构建所有服务"
@ -99,6 +100,7 @@ while [[ $# -gt 0 ]]; do
task|taskService) SERVICES+=("taskService") ;; task|taskService) SERVICES+=("taskService") ;;
starbook|starbookService) SERVICES+=("starbookService") ;; starbook|starbookService) SERVICES+=("starbookService") ;;
ai|aiChatService|aichatservice) SERVICES+=("aiChatService") ;; ai|aiChatService|aichatservice) SERVICES+=("aiChatService") ;;
statistic|statisticService|statisticservice) SERVICES+=("statisticservice") ;;
all) all)
# all 关键字,构建所有服务 # all 关键字,构建所有服务
SERVICES=() SERVICES=()
@ -117,7 +119,7 @@ done
# ==================== 服务列表 ==================== # ==================== 服务列表 ====================
# 所有可用服务及其配置(使用小写 target 名) # 所有可用服务及其配置(使用小写 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 if [ ${#SERVICES[@]} -eq 0 ]; then
@ -208,6 +210,7 @@ main() {
starbookservice) docker_target="starbookservice" ;; starbookservice) docker_target="starbookservice" ;;
aichatservice) docker_target="aichatservice" ;; aichatservice) docker_target="aichatservice" ;;
lasercompositor) docker_target="lasercompositor" ;; lasercompositor) docker_target="lasercompositor" ;;
statisticservice) docker_target="statisticservice" ;;
# 兼容旧的大写服务名 # 兼容旧的大写服务名
userService) docker_target="userservice" ;; userService) docker_target="userservice" ;;
socialService) docker_target="socialservice" ;; socialService) docker_target="socialservice" ;;
@ -216,6 +219,8 @@ main() {
activityService) docker_target="activityservice" ;; activityService) docker_target="activityservice" ;;
taskService) docker_target="taskservice" ;; taskService) docker_target="taskservice" ;;
starbookService) docker_target="starbookservice" ;; starbookService) docker_target="starbookservice" ;;
statisticService) docker_target="statisticservice" ;;
statisticservice) docker_target="statisticservice" ;;
*) docker_target="$service" ;; *) docker_target="$service" ;;
esac esac

View File

@ -1,13 +1,18 @@
#!/bin/bash #!/bin/bash
# =================================================================== # ===================================================================
# Docker 容器日志 & 系统日志清理脚本 # Docker 容器日志 & 镜像 & 系统日志 自动清理脚本
# 功能:当容器日志或 /var/log 超过阈值时自动清理 # 功能:
# 1. 容器日志超过阈值时自动 truncate
# 2. 清理悬空 Docker 镜像 (<none>:<none>)
# 3. 清理旧版本 topfans/* 镜像(保留 latest 和 v1.0.7
# 4. 清理 /var/log 系统日志
# 使用:./cleanup-logs.sh [容器日志阈值GB] [系统日志阈值GB] # 使用:./cleanup-logs.sh [容器日志阈值GB] [系统日志阈值GB]
# 示例:./cleanup-logs.sh 2 2 # 示例:./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 才清理 CONTAINER_THRESHOLD=${1:-2}
SYSLOG_THRESHOLD=${2:-2} # 默认系统日志超过 2GB 才清理 SYSLOG_THRESHOLD=${2:-2}
echo "==========================================" echo "=========================================="
echo "日志清理脚本" echo "日志清理脚本"
@ -23,8 +28,8 @@ total_container_log=0
cleared_containers=0 cleared_containers=0
for container in $(docker ps -q); do for container in $(docker ps -q); do
log_file=$(docker inspect --format='{{.LogPath}}' "$container" 2>/dev/null) log_file=$(docker inspect --format="{{.LogPath}}" "$container" 2>/dev/null)
container_name=$(docker inspect --format='{{.Name}}' "$container" 2>/dev/null | sed 's/^\///') container_name=$(docker inspect --format="{{.Name}}" "$container" 2>/dev/null | sed "s/^\///")
if [ -f "$log_file" ]; then if [ -f "$log_file" ]; then
size_bytes=$(stat -c%s "$log_file" 2>/dev/null || echo 0) 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 "容器日志总大小: ${total_container_gb}GB"
echo "已清理容器数: $cleared_containers" 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 ""
echo "=== 检查系统日志 ===" echo "=== 检查系统日志 ==="
total_syslog_size=0 total_syslog_size=0
cleared_logs=0 cleared_logs=0
# 找出 /var/log 下超过阈值的大文件并清理
for logfile in $(find /var/log -type f -name "*.log" 2>/dev/null); do for logfile in $(find /var/log -type f -name "*.log" 2>/dev/null); do
if [ -f "$logfile" ]; then if [ -f "$logfile" ]; then
size_bytes=$(stat -c%s "$logfile" 2>/dev/null || echo 0) 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 fi
done done
# 也清理旧的压缩日志
for gzfile in $(find /var/log -type f -name "*.gz" 2>/dev/null | head -20); do for gzfile in $(find /var/log -type f -name "*.gz" 2>/dev/null | head -20); do
if [ -f "$gzfile" ]; then if [ -f "$gzfile" ]; then
rm -f "$gzfile" 2>/dev/null && echo " ✅ 已删: $gzfile" rm -f "$gzfile" 2>/dev/null && echo " ✅ 已删: $gzfile"
fi fi
done done
# 清理旧的 log.1, log.2 等轮转文件
for oldlog in $(find /var/log -type f -name "*.log.[0-9]*" 2>/dev/null); do for oldlog in $(find /var/log -type f -name "*.log.[0-9]*" 2>/dev/null); do
rm -f "$oldlog" 2>/dev/null && echo " ✅ 已删: $oldlog" rm -f "$oldlog" 2>/dev/null && echo " ✅ 已删: $oldlog"
done 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)) total_syslog_gb=$((total_syslog_size / 1024 / 1024 / 1024))
echo "系统日志总大小: ${total_syslog_gb}GB" echo "系统日志总大小: ${total_syslog_gb}GB"
echo "已清理日志数: $cleared_logs" echo "已清理日志数: $cleared_logs"
# ========== 3. 磁盘状态 ========== # ========== 5. 磁盘状态 ==========
echo "" echo ""
echo "=== 当前磁盘状态 ===" echo "=== 当前磁盘状态 ==="
df -h / df -h /

View File

@ -80,6 +80,7 @@ SERVICES=(
"starbookservice" "starbookservice"
"aichatservice" "aichatservice"
"lasercompositor" "lasercompositor"
"statisticservice"
) )
# ==================== 服务器配置 ==================== # ==================== 服务器配置 ====================

View File

@ -333,6 +333,50 @@ services:
reservations: reservations:
memory: 256M 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 ==================== # ==================== API Gateway ====================
gateway: gateway:
image: topfans/gateway:latest image: topfans/gateway:latest
@ -356,6 +400,7 @@ services:
DIFY_API_BASE: ${DIFY_API_BASE:-http://host.docker.internal:8081/v1} DIFY_API_BASE: ${DIFY_API_BASE:-http://host.docker.internal:8081/v1}
DIFY_API_KEY: ${DIFY_API_KEY:-} DIFY_API_KEY: ${DIFY_API_KEY:-}
DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008 DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008
DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009
# 镭射卡 AI 生成MiniMax 文生图) # 镭射卡 AI 生成MiniMax 文生图)
MINIMAX_API_KEY: ${MINIMAX_API_KEY:-} MINIMAX_API_KEY: ${MINIMAX_API_KEY:-}
MINIMAX_API_URL: ${MINIMAX_API_URL:-https://api.minimaxi.com/v1/image_generation} MINIMAX_API_URL: ${MINIMAX_API_URL:-https://api.minimaxi.com/v1/image_generation}
@ -389,10 +434,14 @@ services:
condition: service_healthy condition: service_healthy
aichatservice: aichatservice:
condition: service_healthy condition: service_healthy
statisticservice:
condition: service_healthy
lasercompositor: lasercompositor:
condition: service_healthy condition: service_healthy
networks: networks:
- topfans-net topfans-net:
aliases:
- newbackend
ports: ports:
- "8080:8080" - "8080:8080"
healthcheck: healthcheck:

View File

@ -457,6 +457,56 @@ services:
memory: 128M memory: 128M
cpus: '0.25' 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 ==================== # ==================== API Gateway ====================
gateway: gateway:
image: topfans/gateway:latest image: topfans/gateway:latest
@ -479,6 +529,7 @@ services:
DUBBO_TASK_SERVICE_URL: tri://taskservice:20006 DUBBO_TASK_SERVICE_URL: tri://taskservice:20006
DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20005 DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20005
DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008 DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008
DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009
LASER_COMPOSITOR_URL: http://lasercompositor:7002 LASER_COMPOSITOR_URL: http://lasercompositor:7002
# 抠图人像扣底、OSS、Dify、JWT、Redis 全部走 env_file: .env.prod # 抠图人像扣底、OSS、Dify、JWT、Redis 全部走 env_file: .env.prod
REDIS_HOST: topfans-redis REDIS_HOST: topfans-redis
@ -501,12 +552,16 @@ services:
condition: service_started condition: service_started
aichatservice: aichatservice:
condition: service_started condition: service_started
statisticservice:
condition: service_started
lasercompositor: lasercompositor:
condition: service_started condition: service_started
redis: redis:
condition: service_healthy condition: service_healthy
networks: networks:
- topfans-net topfans-net:
aliases:
- newbackend
ports: ports:
- "8080:8080" - "8080:8080"
healthcheck: healthcheck:

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,26 @@
-- statistic 服务 MV3: 每日点赞按等级
-- 创建时间: 2026-06-08
-- 服务于: 点赞收益按等级(累计)
-- 关联: spec §3.4 MV3
-- 关联依赖: public.assets 表
-- 修复: a.level → a.gradeassets 表没有 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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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)
);

View File

@ -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);

View File

@ -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 $$;

View File

@ -1,6 +1,7 @@
# 开发环境配置 # 开发环境配置
# HBuilderX「运行」时自动加载;CLI 用 --mode development # 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 # WebSocket 地址:如与 API 同源可省略(自动从 VITE_API_BASE_URL 推导 http→ws、https→wss
# 独立部署时直接覆盖例如ws://192.168.110.60:8081 # 独立部署时直接覆盖例如ws://192.168.110.60:8081
VITE_WS_BASE_URL=ws://192.168.110.60:8080 VITE_WS_BASE_URL=ws://192.168.110.60:8080

View File

@ -3,7 +3,7 @@
"appid" : "__UNI__F199FF4", "appid" : "__UNI__F199FF4",
"description" : "", "description" : "",
"versionName" : "1.0.5", "versionName" : "1.0.5",
"versionCode" : 109, "versionCode" : 111,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {