feat:docker新增统计的微服务
This commit is contained in:
parent
3ec096ecd9
commit
280b4bbee0
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
#!/bin/bash
|
||||
# ===================================================================
|
||||
# Docker 容器日志 & 系统日志清理脚本
|
||||
# 功能:当容器日志或 /var/log 超过阈值时自动清理
|
||||
# Docker 容器日志 & 镜像 & 系统日志 自动清理脚本
|
||||
# 功能:
|
||||
# 1. 容器日志超过阈值时自动 truncate
|
||||
# 2. 清理悬空 Docker 镜像 (<none>:<none>)
|
||||
# 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 /
|
||||
|
||||
@ -80,6 +80,7 @@ SERVICES=(
|
||||
"starbookservice"
|
||||
"aichatservice"
|
||||
"lasercompositor"
|
||||
"statisticservice"
|
||||
)
|
||||
|
||||
# ==================== 服务器配置 ====================
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
37
docker/sql/migrations/2026_06_08_001_statistic_events.sql
Normal file
37
docker/sql/migrations/2026_06_08_001_statistic_events.sql
Normal 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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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)
|
||||
);
|
||||
@ -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);
|
||||
@ -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 $$;
|
||||
@ -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
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"appid" : "__UNI__F199FF4",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.5",
|
||||
"versionCode" : 109,
|
||||
"versionCode" : 111,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user