初始化项目
This commit is contained in:
commit
0e6d0c3584
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/frontend/node_modules
|
||||||
|
/frontend/unpackage/
|
||||||
|
.DS_Store
|
||||||
|
frontend/static/.DS_Store
|
||||||
|
frontend/pages/.DS_Store
|
||||||
|
frontend/.hbuilderx/launch.json
|
||||||
|
.idea
|
||||||
22
docker/.env.local
Normal file
22
docker/.env.local
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# TopFans Docker - Local Development Environment Variables
|
||||||
|
# ===================================================================
|
||||||
|
# Copy this file to .env and modify values as needed
|
||||||
|
# Usage: docker-compose -f docker-compose.local.yml --profile local up -d
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# Database (must match POSTGRES_PASSWORD in compose file)
|
||||||
|
DB_PASSWORD=postgres123
|
||||||
|
|
||||||
|
# JWT Secret - CHANGE IN PRODUCTION
|
||||||
|
JWT_SECRET=topfans-secret-key-local-dev-only
|
||||||
|
|
||||||
|
# OSS Configuration (from backend code defaults)
|
||||||
|
OSS_REGION=cn-shanghai
|
||||||
|
OSS_BUCKET_NAME=top-fans-test
|
||||||
|
OSS_STS_ROLE_ARN=acs:ram::1387642798143585:role/top-fans-oss-user
|
||||||
|
OSS_ACCESS_KEY_ID=LTAI5tNaAjTNiHnefMCG3q4J
|
||||||
|
OSS_ACCESS_KEY_SECRET=48wwZvNkUn1PO1xWjV4HuE5JjB6G7c
|
||||||
|
OSS_AVATAR_DIR=avatar/
|
||||||
|
OSS_ASSET_DIR=asset/
|
||||||
|
OSS_TOKEN_EXPIRE_TIME=3600
|
||||||
22
docker/.env.prod
Normal file
22
docker/.env.prod
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# TopFans Docker - Production Environment Variables
|
||||||
|
# ===================================================================
|
||||||
|
# IMPORTANT: Change all secrets before deploying to production
|
||||||
|
# Usage: docker-compose -f docker-compose.prod.yml --profile prod up -d
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# Database - MUST CHANGE
|
||||||
|
DB_PASSWORD=CHANGE_ME_TO_A_SECURE_PASSWORD
|
||||||
|
|
||||||
|
# JWT Secret - MUST CHANGE
|
||||||
|
JWT_SECRET=CHANGE_ME_TO_A_VERY_LONG_RANDOM_STRING
|
||||||
|
|
||||||
|
# OSS Configuration
|
||||||
|
OSS_REGION=cn-shanghai
|
||||||
|
OSS_BUCKET_NAME=top-fans-prod
|
||||||
|
OSS_STS_ROLE_ARN=acs:ram::1387642798143585:role/top-fans-oss-user
|
||||||
|
OSS_ACCESS_KEY_ID=YOUR_OSS_ACCESS_KEY_ID
|
||||||
|
OSS_ACCESS_KEY_SECRET=YOUR_OSS_ACCESS_KEY_SECRET
|
||||||
|
OSS_AVATAR_DIR=avatar/
|
||||||
|
OSS_ASSET_DIR=asset/
|
||||||
|
OSS_TOKEN_EXPIRE_TIME=3600
|
||||||
137
docker/Dockerfile.services
Normal file
137
docker/Dockerfile.services
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# TopFans - Multi-stage Build for Go Services
|
||||||
|
# Stage 1: Build all services in parallel
|
||||||
|
# Stage 2: Create minimal runtime image for each service
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# ---- Build Stage ----
|
||||||
|
FROM --platform=linux/amd64 golang:1.25-alpine AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache git make
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy go mod files first for better caching
|
||||||
|
COPY backend/go.mod backend/go.sum ./
|
||||||
|
|
||||||
|
# 配置 Go 模块代理,解决网络问题
|
||||||
|
RUN go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/go-proxy/,direct && \
|
||||||
|
go env -w GOSUMDB=off && \
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY backend/ ./
|
||||||
|
|
||||||
|
# Build all services with optimization flags
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /tmp/gateway gateway/main.go && \
|
||||||
|
echo "Built gateway" && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /tmp/userservice services/userService/main.go && \
|
||||||
|
echo "Built userservice" && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /tmp/socialservice services/socialService/main.go && \
|
||||||
|
echo "Built socialservice" && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /tmp/assetservice services/assetService/main.go && \
|
||||||
|
echo "Built assetservice" && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /tmp/galleryservice services/galleryService/main.go && \
|
||||||
|
echo "Built galleryservice" && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /tmp/activityservice services/activityService/main.go && \
|
||||||
|
echo "Built activityservice"
|
||||||
|
|
||||||
|
# ---- Runtime Stage: Gateway ----
|
||||||
|
FROM --platform=linux/amd64 alpine:3.19 AS gateway
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /tmp/gateway /app/gateway
|
||||||
|
|
||||||
|
ENV GIN_MODE=release
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/gateway"]
|
||||||
|
CMD ["-port=8080"]
|
||||||
|
|
||||||
|
# ---- Runtime Stage: UserService ----
|
||||||
|
FROM --platform=linux/amd64 alpine:3.19 AS userservice
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /tmp/userservice /app/userservice
|
||||||
|
|
||||||
|
EXPOSE 20000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:20000 || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/userservice"]
|
||||||
|
|
||||||
|
# ---- Runtime Stage: SocialService ----
|
||||||
|
FROM --platform=linux/amd64 alpine:3.19 AS socialservice
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /tmp/socialservice /app/socialservice
|
||||||
|
|
||||||
|
EXPOSE 20002
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:20002 || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/socialservice"]
|
||||||
|
|
||||||
|
# ---- Runtime Stage: AssetService ----
|
||||||
|
FROM --platform=linux/amd64 alpine:3.19 AS assetservice
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /tmp/assetservice /app/assetservice
|
||||||
|
|
||||||
|
EXPOSE 20003
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:20003 || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/assetservice"]
|
||||||
|
|
||||||
|
# ---- Runtime Stage: GalleryService ----
|
||||||
|
FROM --platform=linux/amd64 alpine:3.19 AS galleryservice
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /tmp/galleryservice /app/galleryservice
|
||||||
|
|
||||||
|
EXPOSE 20004
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:20004 || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/galleryservice"]
|
||||||
|
|
||||||
|
# ---- Runtime Stage: ActivityService ----
|
||||||
|
FROM --platform=linux/amd64 alpine:3.19 AS activityservice
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /tmp/activityservice /app/activityservice
|
||||||
|
|
||||||
|
EXPOSE 20005
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:20005 || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/activityservice"]
|
||||||
258
docker/build.sh
Executable file
258
docker/build.sh
Executable file
@ -0,0 +1,258 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===================================================================
|
||||||
|
# TopFans Docker 一键构建脚本
|
||||||
|
# 功能:编译并打包所有后端服务为 Docker 镜像
|
||||||
|
# ===================================================================
|
||||||
|
#
|
||||||
|
# 使用方式:
|
||||||
|
# ./build.sh # 构建所有服务(默认)
|
||||||
|
# ./build.sh --no-cache # 无缓存构建
|
||||||
|
# ./build.sh gateway # 仅构建 Gateway
|
||||||
|
# ./build.sh userService # 仅构建 UserService
|
||||||
|
# ./build.sh prod # 使用生产配置构建
|
||||||
|
#
|
||||||
|
# 示例:
|
||||||
|
# ./build.sh # 构建所有服务
|
||||||
|
# ./build.sh --no-cache # 清除缓存重新构建
|
||||||
|
# ./build.sh gateway userService # 仅构建指定服务
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
set -e # 遇到错误立即退出
|
||||||
|
|
||||||
|
# ==================== 颜色定义 ====================
|
||||||
|
# 用于终端输出彩色文字,提升可读性
|
||||||
|
RED='\033[0;31m' # 红色 - 错误信息
|
||||||
|
GREEN='\033[0;32m' # 绿色 - 成功信息
|
||||||
|
YELLOW='\033[1;33m' # 黄色 - 警告/进度信息
|
||||||
|
BLUE='\033[0;34m' # 蓝色 - 标题信息
|
||||||
|
NC='\033[0m' # 重置颜色
|
||||||
|
|
||||||
|
# ==================== 路径配置 ====================
|
||||||
|
# 获取脚本所在目录(支持从任意目录调用)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# ==================== 构建配置 ====================
|
||||||
|
# 镜像前缀,用于标记和组织镜像(必须小写)
|
||||||
|
IMAGE_PREFIX="topfans"
|
||||||
|
# Dockerfile 路径
|
||||||
|
DOCKERFILE="Dockerfile.services"
|
||||||
|
# 目标架构(amd64 适用于大多数服务器)
|
||||||
|
TARGET_ARCH="linux/amd64"
|
||||||
|
|
||||||
|
# ==================== 默认设置 ====================
|
||||||
|
# 是否使用缓存(默认使用,构建更快)
|
||||||
|
NO_CACHE=""
|
||||||
|
# 构建配置文件(local 或 prod)
|
||||||
|
PROFILE="local"
|
||||||
|
# 要构建的服务列表(留空则构建全部)
|
||||||
|
SERVICES=()
|
||||||
|
|
||||||
|
# ==================== 解析命令行参数 ====================
|
||||||
|
# 支持多种使用方式
|
||||||
|
# --no-cache: 清除 Docker 缓存,强制重新构建
|
||||||
|
# --profile prod: 使用生产配置
|
||||||
|
# 服务名: 仅构建指定服务(如 gateway, userService 等)
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--no-cache)
|
||||||
|
# 启用无缓存构建,适用于代码更新后确保完全重新编译
|
||||||
|
NO_CACHE="--no-cache"
|
||||||
|
echo -e "${YELLOW}警告: 无缓存模式,构建时间较长${NC}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--profile)
|
||||||
|
# 指定构建配置(local 或 prod)
|
||||||
|
PROFILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
# 显示帮助信息
|
||||||
|
echo "用法: $0 [选项] [服务名...]"
|
||||||
|
echo ""
|
||||||
|
echo "选项:"
|
||||||
|
echo " --no-cache 不使用 Docker 缓存,强制重新构建"
|
||||||
|
echo " --profile <env> 使用配置文件 (local/prod),默认 local"
|
||||||
|
echo " --help, -h 显示此帮助信息"
|
||||||
|
echo ""
|
||||||
|
echo "服务名 (可选):"
|
||||||
|
echo " gateway, userService, socialService, assetService,"
|
||||||
|
echo " galleryService, activityService"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 # 构建所有服务"
|
||||||
|
echo " $0 --no-cache # 无缓存构建所有服务"
|
||||||
|
echo " $0 gateway # 仅构建 Gateway"
|
||||||
|
echo " $0 --profile prod # 使用生产配置构建"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# 未知参数,假设是服务名
|
||||||
|
# 支持的服务名映射(别名 -> 实际 target 名)
|
||||||
|
case $1 in
|
||||||
|
gateway) SERVICES+=("gateway") ;;
|
||||||
|
user|userService) SERVICES+=("userService") ;;
|
||||||
|
social|socialService) SERVICES+=("socialService") ;;
|
||||||
|
asset|assetService) SERVICES+=("assetService") ;;
|
||||||
|
gallery|galleryService) SERVICES+=("galleryService") ;;
|
||||||
|
activity|activityService) SERVICES+=("activityService") ;;
|
||||||
|
all)
|
||||||
|
# all 关键字,构建所有服务
|
||||||
|
SERVICES=()
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}错误: 未知服务 '$1'${NC}"
|
||||||
|
echo "使用 --help 查看可用服务列表"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ==================== 服务列表 ====================
|
||||||
|
# 所有可用服务及其配置(使用小写 target 名)
|
||||||
|
ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice")
|
||||||
|
|
||||||
|
# 确定要构建的服务
|
||||||
|
if [ ${#SERVICES[@]} -eq 0 ]; then
|
||||||
|
# 未指定服务,构建全部
|
||||||
|
SERVICE_NAMES=("${ALL_SERVICES_NAME[@]}")
|
||||||
|
else
|
||||||
|
SERVICE_NAMES=("${SERVICES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 构建函数 ====================
|
||||||
|
|
||||||
|
# 打印分隔线
|
||||||
|
print_line() {
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 打印带颜色的消息
|
||||||
|
print_msg() {
|
||||||
|
local color=$1
|
||||||
|
local msg=$2
|
||||||
|
echo -e "${color}${msg}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建单个服务的镜像
|
||||||
|
# 参数: service_name docker_target
|
||||||
|
build_service() {
|
||||||
|
local service_name=$1
|
||||||
|
local docker_target=$2
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}正在构建 ${service_name}...${NC}"
|
||||||
|
|
||||||
|
# 检查 Dockerfile 是否存在
|
||||||
|
if [ ! -f "$DOCKERFILE" ]; then
|
||||||
|
echo -e "${RED}错误: Dockerfile ($DOCKERFILE) 不存在${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 构建镜像
|
||||||
|
# 镜像命名: topfans/gateway:latest, topfans/userService:latest 等
|
||||||
|
# -f: 指定 Dockerfile
|
||||||
|
# --target: 指定构建阶段(对应 Dockerfile 中的 AS xxx)
|
||||||
|
# --platform: 指定目标平台
|
||||||
|
# $NO_CACHE: 是否使用缓存
|
||||||
|
docker build \
|
||||||
|
$NO_CACHE \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
--target "$docker_target" \
|
||||||
|
--platform "$TARGET_ARCH" \
|
||||||
|
-t "${IMAGE_PREFIX}/${service_name}:latest" \
|
||||||
|
../
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_msg "$GREEN" "✅ ${service_name} 构建成功"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_msg "$RED" "❌ ${service_name} 构建失败"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 主构建流程 ====================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
print_line
|
||||||
|
print_msg "$BLUE" " TopFans Docker 构建脚本"
|
||||||
|
print_msg "$BLUE" " 配置: ${PROFILE}"
|
||||||
|
print_msg "$BLUE" " 架构: ${TARGET_ARCH}"
|
||||||
|
print_line
|
||||||
|
|
||||||
|
# 记录开始时间
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
# 统计构建结果
|
||||||
|
SUCCESS_COUNT=0
|
||||||
|
FAIL_COUNT=0
|
||||||
|
|
||||||
|
# 构建每个服务
|
||||||
|
for service in "${SERVICE_NAMES[@]}"; do
|
||||||
|
# 服务名到 target 的映射(统一使用小写 target)
|
||||||
|
case $service in
|
||||||
|
gateway) docker_target="gateway" ;;
|
||||||
|
userservice) docker_target="userservice" ;;
|
||||||
|
socialservice) docker_target="socialservice" ;;
|
||||||
|
assetservice) docker_target="assetservice" ;;
|
||||||
|
galleryservice) docker_target="galleryservice" ;;
|
||||||
|
activityservice) docker_target="activityservice" ;;
|
||||||
|
# 兼容旧的大写服务名
|
||||||
|
userService) docker_target="userservice" ;;
|
||||||
|
socialService) docker_target="socialservice" ;;
|
||||||
|
assetService) docker_target="assetservice" ;;
|
||||||
|
galleryService) docker_target="galleryservice" ;;
|
||||||
|
activityService) docker_target="activityservice" ;;
|
||||||
|
*) docker_target="$service" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if build_service "$service" "$docker_target"; then
|
||||||
|
((SUCCESS_COUNT++))
|
||||||
|
else
|
||||||
|
((FAIL_COUNT++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ==================== 构建结果 ====================
|
||||||
|
print_line
|
||||||
|
print_msg "$BLUE" " 构建完成"
|
||||||
|
|
||||||
|
# 计算耗时
|
||||||
|
END_TIME=$(date +%s)
|
||||||
|
ELAPSED=$((END_TIME - START_TIME))
|
||||||
|
MINUTES=$((ELAPSED / 60))
|
||||||
|
SECONDS=$((ELAPSED % 60))
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_msg "$GREEN" "✅ 成功: ${SUCCESS_COUNT} 个服务"
|
||||||
|
if [ $FAIL_COUNT -gt 0 ]; then
|
||||||
|
print_msg "$RED" "❌ 失败: ${FAIL_COUNT} 个服务"
|
||||||
|
fi
|
||||||
|
echo -e "${YELLOW}⏱ 耗时: ${MINUTES}分${SECONDS}秒${NC}"
|
||||||
|
print_line
|
||||||
|
|
||||||
|
# 显示构建的镜像
|
||||||
|
echo ""
|
||||||
|
print_msg "$BLUE" "构建的镜像列表:"
|
||||||
|
echo ""
|
||||||
|
for service in "${SERVICE_NAMES[@]}"; do
|
||||||
|
# 获取镜像大小
|
||||||
|
SIZE=$(docker images "${IMAGE_PREFIX}/${service}:latest" --format "{{.Size}}" 2>/dev/null || echo "未知")
|
||||||
|
echo -e " ${GREEN}${IMAGE_PREFIX}/${service}:latest${NC} (${SIZE})"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_msg "$GREEN" "🎉 构建完成!"
|
||||||
|
echo ""
|
||||||
|
echo "下一步:"
|
||||||
|
echo " 启动服务 (本地): docker-compose -f docker-compose.local.yml --profile local up -d"
|
||||||
|
echo " 启动服务 (生产): docker-compose -f docker-compose.prod.yml --profile prod up -d"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行主函数
|
||||||
|
main
|
||||||
597
docker/deploy.sh
Executable file
597
docker/deploy.sh
Executable file
@ -0,0 +1,597 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===================================================================
|
||||||
|
# TopFans 打包上传部署脚本
|
||||||
|
# 功能:
|
||||||
|
# - 本地:构建镜像 → 推送到镜像仓库
|
||||||
|
# - 远程:拉取镜像 → 启动服务
|
||||||
|
# - 回滚:回滚到指定版本
|
||||||
|
# ===================================================================
|
||||||
|
#
|
||||||
|
# 使用前提:
|
||||||
|
# 1. 已安装 Docker
|
||||||
|
# 2. 已创建阿里云容器镜像仓库
|
||||||
|
# 3. 服务器已配置 SSH 免密登录(建议)
|
||||||
|
#
|
||||||
|
# 使用方式:
|
||||||
|
# # 本地构建 + 推送
|
||||||
|
# ./deploy.sh build v1.0.0
|
||||||
|
#
|
||||||
|
# # 远程部署(从仓库拉取 + 启动)
|
||||||
|
# ./deploy.sh deploy v1.0.0
|
||||||
|
#
|
||||||
|
# # 回滚到指定版本
|
||||||
|
# ./deploy.sh rollback v1.0.0
|
||||||
|
#
|
||||||
|
# # 查看部署历史
|
||||||
|
# ./deploy.sh history
|
||||||
|
#
|
||||||
|
# # 一键构建 + 推送 + 部署(本地构建完成后远程部署)
|
||||||
|
# ./deploy.sh all v1.0.0
|
||||||
|
#
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ==================== 颜色定义 ====================
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# ==================== 路径配置 ====================
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# ==================== 镜像仓库配置 ====================
|
||||||
|
# ⚠️ 需要修改为你的阿里云仓库地址
|
||||||
|
REGISTRY_HOST="registry.cn-hangzhou.aliyuncs.com"
|
||||||
|
NAMESPACE="你的命名空间" # ⚠️ 修改这里
|
||||||
|
|
||||||
|
# 服务列表(必须与 docker-compose 中的服务名一致)
|
||||||
|
SERVICES=(
|
||||||
|
"gateway"
|
||||||
|
"userservice"
|
||||||
|
"socialservice"
|
||||||
|
"assetservice"
|
||||||
|
"galleryservice"
|
||||||
|
"activityservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== 服务器配置 ====================
|
||||||
|
# ⚠️ 修改为你的服务器信息
|
||||||
|
SERVER_HOST="" # 服务器 IP 或域名
|
||||||
|
SERVER_PORT="22" # SSH 端口
|
||||||
|
SERVER_USER="root" # SSH 用户名
|
||||||
|
SERVER_PATH="/opt/topfans/docker" # 服务器上 docker 目录路径
|
||||||
|
|
||||||
|
# ==================== 打印函数 ====================
|
||||||
|
print_step() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${BLUE} $1${NC}"
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_msg() {
|
||||||
|
local color=$1
|
||||||
|
local msg=$2
|
||||||
|
echo -e "${color}${msg}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 帮助信息 ====================
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
${MAGENTA}TopFans 部署脚本${NC}
|
||||||
|
|
||||||
|
${YELLOW}用法:${NC}
|
||||||
|
$0 <命令> [版本号] [选项]
|
||||||
|
|
||||||
|
${YELLOW}命令:${NC}
|
||||||
|
${GREEN}build${NC} <版本号> 本地构建镜像并推送到仓库
|
||||||
|
${GREEN}deploy${NC} <版本号> 远程部署(从仓库拉取 + 启动服务)
|
||||||
|
${GREEN}rollback${NC} <版本号> 回滚到指定版本
|
||||||
|
${GREEN}history${NC} 查看部署历史
|
||||||
|
${GREEN}all${NC} <版本号> 一键构建 + 推送 + 部署
|
||||||
|
${GREEN}clean${NC} 清理本地镜像(谨慎使用)
|
||||||
|
|
||||||
|
${YELLOW}选项:${NC}
|
||||||
|
--server <IP> 指定服务器(覆盖配置文件)
|
||||||
|
--skip-build 跳过构建(用于已构建过的情况)
|
||||||
|
--skip-push 跳过推送(用于已推送过的情况)
|
||||||
|
--force 强制执行(不确认)
|
||||||
|
--help, -h 显示此帮助
|
||||||
|
|
||||||
|
${YELLOW}示例:${NC}
|
||||||
|
$0 build v1.0.0 # 构建并推送
|
||||||
|
$0 deploy v1.0.0 --server 192.168.1.100 # 部署到服务器
|
||||||
|
$0 rollback v0.9.0 # 回滚到 v0.9.0
|
||||||
|
$0 history # 查看部署历史
|
||||||
|
$0 all v1.0.0 --server 192.168.1.100 # 一键完成所有操作
|
||||||
|
|
||||||
|
${YELLOW}前提准备:${NC}
|
||||||
|
1. 阿里云容器镜像: https://cr.console.aliyun.com/
|
||||||
|
2. 修改脚本中的 REGISTRY_HOST 和 NAMESPACE
|
||||||
|
3. 修改 SERVER_HOST 为你的服务器 IP
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 配置检查 ====================
|
||||||
|
check_config() {
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
if [ "$NAMESPACE" = "你的命名空间" ]; then
|
||||||
|
print_msg "$RED" "错误: 请修改 deploy.sh 中的 NAMESPACE 为你的阿里云仓库命名空间"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$SERVER_HOST" ]; then
|
||||||
|
print_msg "$YELLOW" "警告: SERVER_HOST 未设置,远程部署功能将不可用"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 1. 构建镜像 ====================
|
||||||
|
do_build() {
|
||||||
|
print_step "🔨 构建 Docker 镜像"
|
||||||
|
|
||||||
|
# 调用构建脚本
|
||||||
|
./build.sh --no-cache
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
print_msg "$RED" "❌ 构建失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_msg "$GREEN" "✅ 镜像构建完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 2. 推送镜像 ====================
|
||||||
|
do_push() {
|
||||||
|
local version=$1
|
||||||
|
|
||||||
|
print_step "🔑 登录镜像仓库"
|
||||||
|
|
||||||
|
# 登录(可能需要输入密码)
|
||||||
|
docker login --username="${NAMESPACE}" "${REGISTRY_HOST}" || {
|
||||||
|
print_msg "$RED" "❌ 登录失败"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_msg "$GREEN" "✅ 登录成功"
|
||||||
|
|
||||||
|
print_step "📦 推送镜像到仓库"
|
||||||
|
|
||||||
|
local failed=()
|
||||||
|
local pushed=()
|
||||||
|
|
||||||
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
|
local local_image="topfans/${SERVICE}:latest"
|
||||||
|
local remote_image="${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:v${version}"
|
||||||
|
local latest_image="${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:latest"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_msg "$YELLOW" "处理 ${SERVICE}..."
|
||||||
|
|
||||||
|
# 打标签
|
||||||
|
docker tag "${local_image}" "${remote_image}"
|
||||||
|
docker tag "${local_image}" "${latest_image}"
|
||||||
|
|
||||||
|
# 推送版本标签
|
||||||
|
echo -e " ${CYAN}→ ${remote_image}${NC}"
|
||||||
|
if docker push "${remote_image}"; then
|
||||||
|
echo -e " ${GREEN}✅ 已推送${NC}"
|
||||||
|
pushed+=("${SERVICE}")
|
||||||
|
else
|
||||||
|
echo -e " ${RED}❌ 推送失败${NC}"
|
||||||
|
failed+=("${SERVICE}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 推送 latest
|
||||||
|
echo ""
|
||||||
|
print_msg "$YELLOW" "推送 latest 标签..."
|
||||||
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
|
local latest_image="${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:latest"
|
||||||
|
docker push "${latest_image}" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
print_step "📊 推送结果"
|
||||||
|
|
||||||
|
if [ ${#failed[@]} -eq 0 ]; then
|
||||||
|
print_msg "$GREEN" "✅ 全部推送成功"
|
||||||
|
else
|
||||||
|
print_msg "$RED" "❌ 失败: ${failed[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_msg "$CYAN" "已推送的镜像: v${version}"
|
||||||
|
for SERVICE in "${pushed[@]}"; do
|
||||||
|
echo -e " ${GREEN}${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:v${version}${NC}"
|
||||||
|
done
|
||||||
|
|
||||||
|
return $([ ${#failed[@]} -eq 0 ] && echo 0 || echo 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 3. 远程部署 ====================
|
||||||
|
do_deploy() {
|
||||||
|
local version=$1
|
||||||
|
|
||||||
|
if [ -z "$SERVER_HOST" ]; then
|
||||||
|
print_msg "$RED" "错误: 请设置 SERVER_HOST(服务器 IP)"
|
||||||
|
print_msg "$YELLOW" "使用方法: $0 deploy ${version} --server <IP>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_step "🚀 远程部署到 ${SERVER_HOST}"
|
||||||
|
|
||||||
|
# 构建远程部署脚本
|
||||||
|
local remote_script="
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo '=== 1. 创建部署目录 ==='
|
||||||
|
mkdir -p ${SERVER_PATH}
|
||||||
|
cd ${SERVER_PATH}
|
||||||
|
|
||||||
|
echo '=== 2. 登录镜像仓库 ==='
|
||||||
|
# 注意:需要提前在服务器上配置 docker login,或者使用阿里云 AccessKey 登录
|
||||||
|
# 这里假设已配置免密登录或使用 docker-credential-ecr-login
|
||||||
|
echo '使用镜像: ${REGISTRY_HOST}/${NAMESPACE}'
|
||||||
|
|
||||||
|
echo '=== 3. 拉取镜像 ==='
|
||||||
|
for service in ${SERVICES[*]}; do
|
||||||
|
echo \"拉取 topfans-\$service:v${version}...\"
|
||||||
|
docker pull ${REGISTRY_HOST}/${NAMESPACE}/topfans-\$service:v${version}
|
||||||
|
done
|
||||||
|
|
||||||
|
echo '=== 4. 打 latest 标签 ==='
|
||||||
|
for service in ${SERVICES[*]}; do
|
||||||
|
docker tag ${REGISTRY_HOST}/${NAMESPACE}/topfans-\$service:v${version} \
|
||||||
|
${REGISTRY_HOST}/${NAMESPACE}/topfans-\$service:latest
|
||||||
|
done
|
||||||
|
|
||||||
|
echo '=== 5. 停止现有服务 ==='
|
||||||
|
docker-compose -f docker-compose.prod.yml down 2>/dev/null || true
|
||||||
|
|
||||||
|
echo '=== 6. 启动服务 ==='
|
||||||
|
docker-compose -f docker-compose.prod.yml --profile prod up -d
|
||||||
|
|
||||||
|
echo '=== 7. 等待服务就绪 ==='
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
echo '=== 8. 健康检查 ==='
|
||||||
|
curl -s http://localhost:8080/health > /dev/null && echo '✅ Gateway 健康' || echo '⚠️ Gateway 可能未就绪'
|
||||||
|
|
||||||
|
echo '=== 9. 记录部署历史 ==='
|
||||||
|
echo '{\"version\":\"${version}\",\"deployed_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"services\":${#SERVICES[@]}}' \
|
||||||
|
>> ${SERVER_PATH}/deploy_history.json
|
||||||
|
|
||||||
|
echo ''
|
||||||
|
echo '✅ 部署完成!'
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
"
|
||||||
|
|
||||||
|
# 执行远程脚本
|
||||||
|
print_msg "$YELLOW" "正在连接 ${SERVER_USER}@${SERVER_HOST}..."
|
||||||
|
|
||||||
|
ssh -p "${SERVER_PORT}" -T "${SERVER_USER}@${SERVER_HOST}" << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo '=== 1. 检查 Docker 环境 ==='
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo '❌ Docker 未安装'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||||
|
echo '❌ docker-compose 未安装'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '✅ Docker 环境就绪'
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
# 由于 heredoc 在复杂脚本中有问题,这里简化为直接执行关键命令
|
||||||
|
print_msg "$YELLOW" "执行部署命令..."
|
||||||
|
|
||||||
|
# 分步执行远程命令
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
mkdir -p ${SERVER_PATH} && \
|
||||||
|
echo '目录就绪'
|
||||||
|
"
|
||||||
|
|
||||||
|
print_msg "$GREEN" "✅ 服务器目录就绪"
|
||||||
|
|
||||||
|
# 拉取镜像(逐个拉取以便查看进度)
|
||||||
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
|
print_msg "$YELLOW" "拉取 ${SERVICE}..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
docker pull ${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:v${version}
|
||||||
|
"
|
||||||
|
print_msg "$GREEN" "✅ ${SERVICE} 拉取完成"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 打标签
|
||||||
|
print_msg "$YELLOW" "打 latest 标签..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
for service in ${SERVICES[*]}; do
|
||||||
|
docker tag ${REGISTRY_HOST}/${NAMESPACE}/topfans-\$service:v${version} \
|
||||||
|
${REGISTRY_HOST}/${NAMESPACE}/topfans-\$service:latest
|
||||||
|
done
|
||||||
|
echo '标签完成'
|
||||||
|
"
|
||||||
|
|
||||||
|
# 停止旧服务
|
||||||
|
print_msg "$YELLOW" "停止旧服务..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
cd ${SERVER_PATH} && \
|
||||||
|
docker-compose -f docker-compose.prod.yml down 2>/dev/null || true
|
||||||
|
"
|
||||||
|
|
||||||
|
# 启动新服务
|
||||||
|
print_msg "$YELLOW" "启动服务..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
cd ${SERVER_PATH} && \
|
||||||
|
docker-compose -f docker-compose.prod.yml --profile prod up -d
|
||||||
|
"
|
||||||
|
|
||||||
|
# 等待并检查
|
||||||
|
print_msg "$YELLOW" "等待服务启动 (15s)..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
print_step "📊 部署结果"
|
||||||
|
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
echo ''
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
echo ''
|
||||||
|
echo -n 'Gateway 健康检查: '
|
||||||
|
curl -s http://localhost:8080/health > /dev/null && echo '✅ OK' || echo '⚠️ 检查失败'
|
||||||
|
"
|
||||||
|
|
||||||
|
print_msg "$GREEN" "✅ 远程部署完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 4. 回滚 ====================
|
||||||
|
do_rollback() {
|
||||||
|
local version=$1
|
||||||
|
|
||||||
|
if [ -z "$SERVER_HOST" ]; then
|
||||||
|
print_msg "$RED" "错误: 请设置 SERVER_HOST"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_step "🔄 回滚到版本 v${version}"
|
||||||
|
|
||||||
|
print_msg "$YELLOW" "正在回滚 ${SERVER_HOST} 上的服务..."
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
print_msg "$YELLOW" "停止服务..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
cd ${SERVER_PATH} && \
|
||||||
|
docker-compose -f docker-compose.prod.yml down
|
||||||
|
"
|
||||||
|
|
||||||
|
# 拉取指定版本镜像并打标签
|
||||||
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
|
print_msg "$YELLOW" "拉取 ${SERVICE}:v${version}..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
docker pull ${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:v${version}
|
||||||
|
docker tag ${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:v${version} \
|
||||||
|
${REGISTRY_HOST}/${NAMESPACE}/topfans-${SERVICE}:latest
|
||||||
|
"
|
||||||
|
print_msg "$GREEN" "✅ ${SERVICE} 回滚完成"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
print_msg "$YELLOW" "启动服务..."
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
cd ${SERVER_PATH} && \
|
||||||
|
docker-compose -f docker-compose.prod.yml --profile prod up -d
|
||||||
|
"
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
print_step "📊 回滚结果"
|
||||||
|
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
echo ''
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
echo ''
|
||||||
|
echo -n 'Gateway 健康检查: '
|
||||||
|
curl -s http://localhost:8080/health > /dev/null && echo '✅ OK' || echo '⚠️ 检查失败'
|
||||||
|
"
|
||||||
|
|
||||||
|
print_msg "$GREEN" "✅ 回滚完成!当前版本: v${version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 5. 查看历史 ====================
|
||||||
|
do_history() {
|
||||||
|
if [ -z "$SERVER_HOST" ]; then
|
||||||
|
print_msg "$RED" "错误: 请设置 SERVER_HOST"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_step "📜 部署历史"
|
||||||
|
|
||||||
|
ssh -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "
|
||||||
|
if [ -f ${SERVER_PATH}/deploy_history.json ]; then
|
||||||
|
cat ${SERVER_PATH}/deploy_history.json
|
||||||
|
else
|
||||||
|
echo '暂无部署历史'
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_msg "$YELLOW" "查看实时日志: ssh ${SERVER_USER}@${SERVER_HOST} 'docker-compose -f ${SERVER_PATH}/docker-compose.prod.yml logs -f'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 6. 清理本地镜像 ====================
|
||||||
|
do_clean() {
|
||||||
|
print_step "🧹 清理本地镜像"
|
||||||
|
|
||||||
|
print_msg "$RED" "警告: 将删除所有 topfans 镜像"
|
||||||
|
print_msg "$YELLOW" "列出当前镜像:"
|
||||||
|
|
||||||
|
docker images | grep topfans || echo "无 topfans 镜像"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
read -p "确认删除? (y/N): " confirm
|
||||||
|
if [ "$confirm" != "y" ]; then
|
||||||
|
print_msg "$YELLOW" "已取消"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker images | grep topfans | awk '{print $3}' | xargs -r docker rmi -f
|
||||||
|
print_msg "$GREEN" "✅ 清理完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 主函数 ====================
|
||||||
|
main() {
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local command=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
local version=""
|
||||||
|
local skip_build=false
|
||||||
|
local skip_push=false
|
||||||
|
local force=false
|
||||||
|
|
||||||
|
# 解析剩余参数
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--server)
|
||||||
|
SERVER_HOST="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-build)
|
||||||
|
skip_build=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-push)
|
||||||
|
skip_push=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
force=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
v*)
|
||||||
|
version="${1#v}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}错误: 未知参数 '$1'${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# 检查配置
|
||||||
|
check_config || true
|
||||||
|
|
||||||
|
case $command in
|
||||||
|
build)
|
||||||
|
# 构建 + 推送
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echo -e "${RED}错误: 请指定版本号${NC}"
|
||||||
|
echo "用法: $0 build v1.0.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}版本: v${version}${NC}"
|
||||||
|
echo -e "${CYAN}仓库: ${REGISTRY_HOST}/${NAMESPACE}${NC}"
|
||||||
|
|
||||||
|
if [ "$skip_build" = false ]; then
|
||||||
|
do_build
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$skip_push" = false ]; then
|
||||||
|
do_push "$version"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
deploy)
|
||||||
|
# 远程部署
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echo -e "${RED}错误: 请指定版本号${NC}"
|
||||||
|
echo "用法: $0 deploy v1.0.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}版本: v${version}${NC}"
|
||||||
|
echo -e "${CYAN}目标: ${SERVER_USER}@${SERVER_HOST}${NC}"
|
||||||
|
|
||||||
|
do_deploy "$version"
|
||||||
|
;;
|
||||||
|
|
||||||
|
rollback)
|
||||||
|
# 回滚
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echo -e "${RED}错误: 请指定要回滚的版本${NC}"
|
||||||
|
echo "用法: $0 rollback v1.0.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${RED}⚠️ 确认回滚到 v${version}?${NC}"
|
||||||
|
[ "$force" = false ] && read -p "确认? (y/N): " confirm && [ "$confirm" != "y" ] && exit 0
|
||||||
|
|
||||||
|
do_rollback "$version"
|
||||||
|
;;
|
||||||
|
|
||||||
|
history)
|
||||||
|
do_history
|
||||||
|
;;
|
||||||
|
|
||||||
|
clean)
|
||||||
|
do_clean
|
||||||
|
;;
|
||||||
|
|
||||||
|
all)
|
||||||
|
# 构建 + 推送 + 部署
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echo -e "${RED}错误: 请指定版本号${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$SERVER_HOST" ]; then
|
||||||
|
echo -e "${RED}错误: 请设置 SERVER_HOST 或使用 --server 参数${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${MAGENTA}一键部署流程:${NC}"
|
||||||
|
echo " 1. 构建镜像"
|
||||||
|
echo " 2. 推送到仓库"
|
||||||
|
echo " 3. 部署到 ${SERVER_HOST}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[ "$force" = false ] && read -p "继续? (y/N): " confirm && [ "$confirm" != "y" ] && exit 0
|
||||||
|
|
||||||
|
do_build
|
||||||
|
do_push "$version"
|
||||||
|
do_deploy "$version"
|
||||||
|
|
||||||
|
print_step "🎉 全部完成!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo -e "${RED}错误: 未知命令 '$command'${NC}"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
240
docker/docker-compose.local.yml
Normal file
240
docker/docker-compose.local.yml
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# TopFans Docker Compose - Local Development (8G+ RAM)
|
||||||
|
# ===================================================================
|
||||||
|
# Usage:
|
||||||
|
# docker-compose -f docker-compose.local.yml --profile local up -d
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
x-common-env: &common-env
|
||||||
|
GIN_MODE: debug
|
||||||
|
ENV: development
|
||||||
|
LOG_LEVEL: info
|
||||||
|
DB_HOST: host.docker.internal
|
||||||
|
DB_PORT: 15432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-123456}
|
||||||
|
DB_NAME: ${DB_NAME:-top-fans}
|
||||||
|
DB_SSLMODE: disable
|
||||||
|
|
||||||
|
x-healthcheck: &healthcheck
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 20000 || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ==================== Dubbo Services ====================
|
||||||
|
# Start with UserService (root service - all others depend on it)
|
||||||
|
userservice:
|
||||||
|
image: topfans/userservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: userservice
|
||||||
|
container_name: topfans-userservice
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20000
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
expose:
|
||||||
|
- "20000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 20000 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Dubbo services (can start in parallel after UserService is ready)
|
||||||
|
assetservice:
|
||||||
|
image: topfans/assetservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: assetservice
|
||||||
|
container_name: topfans-assetservice
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20003
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
OSS_REGION: ${OSS_REGION:-cn-shanghai}
|
||||||
|
OSS_BUCKET_NAME: ${OSS_BUCKET_NAME:-top-fans-test}
|
||||||
|
OSS_STS_ROLE_ARN: ${OSS_STS_ROLE_ARN:-acs:ram::1387642798143585:role/top-fans-oss-user}
|
||||||
|
OSS_ACCESS_KEY_ID: ${OSS_ACCESS_KEY_ID:-}
|
||||||
|
OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:-}
|
||||||
|
OSS_AVATAR_DIR: ${OSS_AVATAR_DIR:-avatar/}
|
||||||
|
OSS_ASSET_DIR: ${OSS_ASSET_DIR:-asset/}
|
||||||
|
OSS_TOKEN_EXPIRE_TIME: ${OSS_TOKEN_EXPIRE_TIME:-3600}
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
expose:
|
||||||
|
- "20003"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 20003 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
socialservice:
|
||||||
|
image: topfans/socialservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: socialservice
|
||||||
|
container_name: topfans-socialservice
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20002
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
assetservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
expose:
|
||||||
|
- "20002"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 20002 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
galleryservice:
|
||||||
|
image: topfans/galleryservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: galleryservice
|
||||||
|
container_name: topfans-galleryservice
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20004
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
assetservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
expose:
|
||||||
|
- "20004"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 20004 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
activityservice:
|
||||||
|
image: topfans/activityservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: activityservice
|
||||||
|
container_name: topfans-activityservice
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20005
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
expose:
|
||||||
|
- "20005"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 20005 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# ==================== API Gateway ====================
|
||||||
|
gateway:
|
||||||
|
image: topfans/gateway:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: gateway
|
||||||
|
container_name: topfans-gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
SERVER_PORT: 8080
|
||||||
|
JWT_SECRET: ${JWT_SECRET:-topfans-secret-key-please-change-in-production}
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
DUBBO_SOCIAL_SERVICE_URL: tri://socialservice:20002
|
||||||
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||||
|
DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20004
|
||||||
|
DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20005
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
assetservice:
|
||||||
|
condition: service_healthy
|
||||||
|
socialservice:
|
||||||
|
condition: service_healthy
|
||||||
|
galleryservice:
|
||||||
|
condition: service_healthy
|
||||||
|
activityservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
networks:
|
||||||
|
topfans-net:
|
||||||
|
driver: bridge
|
||||||
306
docker/docker-compose.prod.yml
Normal file
306
docker/docker-compose.prod.yml
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# TopFans Docker Compose - Production (4G RAM, 2 CPU)
|
||||||
|
# ===================================================================
|
||||||
|
# Usage:
|
||||||
|
# docker-compose -f docker-compose.prod.yml --profile prod up -d
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
x-common-env: &common-env
|
||||||
|
GIN_MODE: release
|
||||||
|
ENV: production
|
||||||
|
LOG_LEVEL: info
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
DB_NAME: topfans
|
||||||
|
DB_SSLMODE: disable
|
||||||
|
|
||||||
|
x-postgres-env: &postgres-env
|
||||||
|
POSTGRES_DB: topfans
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
POSTGRES_MAX_CONNECTIONS: 100
|
||||||
|
POSTGRES_SHARED_BUFFERS: 128MB
|
||||||
|
POSTGRES_WORK_MEM: 8MB
|
||||||
|
POSTGRES_MAINTENANCE_WORK_MEM: 64MB
|
||||||
|
POSTGRES_EFFECTIVE_CACHE_SIZE: 512MB
|
||||||
|
POSTGRES_CHECKPOINT_COMPLETION_TARGET: 0.9
|
||||||
|
POSTGRES_WAL_BUFFERS: 8MB
|
||||||
|
|
||||||
|
x-healthcheck: &healthcheck
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ==================== Database ====================
|
||||||
|
postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
container_name: topfans-postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *postgres-env
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql
|
||||||
|
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d topfans"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 400M
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
# ==================== Dubbo Services ====================
|
||||||
|
userservice:
|
||||||
|
image: topfans/userservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: userservice
|
||||||
|
container_name: topfans-userservice
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20000
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
DB_NAME: topfans
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
expose:
|
||||||
|
- "20000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:20000 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 150M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.25'
|
||||||
|
|
||||||
|
assetservice:
|
||||||
|
image: topfans/assetservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: assetservice
|
||||||
|
container_name: topfans-assetservice
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20003
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
DB_NAME: topfans
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
OSS_REGION: ${OSS_REGION:-cn-shanghai}
|
||||||
|
OSS_BUCKET_NAME: ${OSS_BUCKET_NAME:-top-fans-test}
|
||||||
|
OSS_STS_ROLE_ARN: ${OSS_STS_ROLE_ARN:-acs:ram::1387642798143585:role/top-fans-oss-user}
|
||||||
|
OSS_ACCESS_KEY_ID: ${OSS_ACCESS_KEY_ID:-}
|
||||||
|
OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:-}
|
||||||
|
OSS_AVATAR_DIR: ${OSS_AVATAR_DIR:-avatar/}
|
||||||
|
OSS_ASSET_DIR: ${OSS_ASSET_DIR:-asset/}
|
||||||
|
OSS_TOKEN_EXPIRE_TIME: ${OSS_TOKEN_EXPIRE_TIME:-3600}
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
expose:
|
||||||
|
- "20003"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:20003 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 200M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.25'
|
||||||
|
|
||||||
|
socialservice:
|
||||||
|
image: topfans/socialservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: socialservice
|
||||||
|
container_name: topfans-socialservice
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20002
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
DB_NAME: topfans
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
assetservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
expose:
|
||||||
|
- "20002"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:20002 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 150M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.25'
|
||||||
|
|
||||||
|
galleryservice:
|
||||||
|
image: topfans/galleryservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: galleryservice
|
||||||
|
container_name: topfans-galleryservice
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20004
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
DB_NAME: topfans
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
assetservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
expose:
|
||||||
|
- "20004"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:20004 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 150M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.25'
|
||||||
|
|
||||||
|
activityservice:
|
||||||
|
image: topfans/activityservice:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: activityservice
|
||||||
|
container_name: topfans-activityservice
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
PORT: 20005
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres123}
|
||||||
|
DB_NAME: topfans
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
expose:
|
||||||
|
- "20005"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:20005 || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 100M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 32M
|
||||||
|
cpus: '0.25'
|
||||||
|
|
||||||
|
# ==================== API Gateway ====================
|
||||||
|
gateway:
|
||||||
|
image: topfans/gateway:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.services
|
||||||
|
target: gateway
|
||||||
|
container_name: topfans-gateway
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
SERVER_PORT: 8080
|
||||||
|
JWT_SECRET: ${JWT_SECRET:-topfans-secret-key-please-change-in-production}
|
||||||
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||||
|
DUBBO_SOCIAL_SERVICE_URL: tri://socialservice:20002
|
||||||
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||||
|
DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20004
|
||||||
|
DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20005
|
||||||
|
depends_on:
|
||||||
|
userservice:
|
||||||
|
condition: service_healthy
|
||||||
|
assetservice:
|
||||||
|
condition: service_healthy
|
||||||
|
socialservice:
|
||||||
|
condition: service_healthy
|
||||||
|
galleryservice:
|
||||||
|
condition: service_healthy
|
||||||
|
activityservice:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- topfans-net
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]
|
||||||
|
<<: *healthcheck
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 200M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.25'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
topfans-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
401
docker/init-db.sql
Normal file
401
docker/init-db.sql
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
-- ===================================================================
|
||||||
|
-- TopFans Database Initialization Script
|
||||||
|
-- ===================================================================
|
||||||
|
-- This script runs automatically when PostgreSQL container starts
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
-- TopFans 数据库初始化脚本
|
||||||
|
-- 创建所有表结构和索引
|
||||||
|
|
||||||
|
-- 开启扩展
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. stars 表 - 明星信息表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS stars (
|
||||||
|
star_id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
tag VARCHAR(100),
|
||||||
|
name_en VARCHAR(100),
|
||||||
|
pic_url VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
identity_id VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. users 表 - 用户表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
mobile VARCHAR(11) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
access_token TEXT,
|
||||||
|
token_expires_at BIGINT,
|
||||||
|
avatar_url VARCHAR(500),
|
||||||
|
global_wallet_address VARCHAR(100),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
deleted_at BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. fan_profiles 表 - 粉丝档案表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS fan_profiles (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
nickname VARCHAR(50) NOT NULL,
|
||||||
|
level INTEGER NOT NULL DEFAULT 1,
|
||||||
|
times INTEGER NOT NULL DEFAULT 1,
|
||||||
|
social INTEGER NOT NULL DEFAULT 0,
|
||||||
|
experience BIGINT NOT NULL DEFAULT 0,
|
||||||
|
coin_balance BIGINT NOT NULL DEFAULT 0,
|
||||||
|
crystal_balance BIGINT NOT NULL DEFAULT 0,
|
||||||
|
tags JSONB,
|
||||||
|
avatar_url VARCHAR(500),
|
||||||
|
starbook_limit INTEGER NOT NULL DEFAULT 3,
|
||||||
|
slot_limit INTEGER NOT NULL DEFAULT 3,
|
||||||
|
assets_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
chain_address VARCHAR(100),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_fan_profiles_user_star UNIQUE (user_id, star_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_fan_profiles_star_nickname ON fan_profiles(star_id, nickname);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fan_profiles_user_id ON fan_profiles(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fan_profiles_star_id ON fan_profiles(star_id);
|
||||||
|
|
||||||
|
ALTER TABLE fan_profiles
|
||||||
|
ADD CONSTRAINT fk_fan_profiles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_fan_profiles_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. assets 表 - 资产表(藏品)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS assets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
owner_uid BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
cover_url VARCHAR(500) NOT NULL,
|
||||||
|
material_url VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
rarity INTEGER,
|
||||||
|
tags JSONB,
|
||||||
|
visibility VARCHAR(20) NOT NULL DEFAULT 'private',
|
||||||
|
status INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tx_hash VARCHAR(100),
|
||||||
|
block_number BIGINT,
|
||||||
|
like_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_original BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
minted_at BIGINT,
|
||||||
|
deleted_at BIGINT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_owner_star ON assets(owner_uid, star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_star_active ON assets(star_id, is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_status ON assets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_tx_hash ON assets(tx_hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_created_at ON assets(created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_deleted_at ON assets(deleted_at);
|
||||||
|
|
||||||
|
ALTER TABLE assets
|
||||||
|
ADD CONSTRAINT fk_assets_owner FOREIGN KEY (owner_uid) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_assets_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 5. mint_orders 表 - 铸造订单表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS mint_orders (
|
||||||
|
order_id VARCHAR(100) PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
asset_id BIGINT,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
cost_crystal BIGINT DEFAULT 0,
|
||||||
|
error_message TEXT,
|
||||||
|
retry_count INTEGER DEFAULT 0,
|
||||||
|
material_url VARCHAR(500),
|
||||||
|
name VARCHAR(100),
|
||||||
|
description TEXT,
|
||||||
|
material_type VARCHAR(50),
|
||||||
|
event VARCHAR(100),
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
minted_at BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_orders_user_star ON mint_orders(user_id, star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_orders_asset ON mint_orders(asset_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_orders_status ON mint_orders(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_orders_created_at ON mint_orders(created_at DESC);
|
||||||
|
|
||||||
|
ALTER TABLE mint_orders
|
||||||
|
ADD CONSTRAINT fk_mint_orders_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_mint_orders_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE SET NULL,
|
||||||
|
ADD CONSTRAINT fk_mint_orders_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 6. asset_likes 表 - 点赞记录表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS asset_likes (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
asset_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_asset_likes_user_asset UNIQUE (user_id, asset_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_asset_likes_asset ON asset_likes(asset_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_asset_likes_user_star ON asset_likes(user_id, star_id);
|
||||||
|
|
||||||
|
ALTER TABLE asset_likes
|
||||||
|
ADD CONSTRAINT fk_asset_likes_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_asset_likes_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_asset_likes_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 7. friendships 表 - 好友关系表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS friendships (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
friend_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'accepted',
|
||||||
|
remark VARCHAR(50),
|
||||||
|
intimacy INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_friendships_user_friend_star UNIQUE (user_id, friend_id, star_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_user_star_status ON friendships(user_id, star_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_user_star_created ON friendships(user_id, star_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_friend_star ON friendships(friend_id, star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_list_query ON friendships(user_id, star_id, status, created_at DESC);
|
||||||
|
|
||||||
|
ALTER TABLE friendships
|
||||||
|
ADD CONSTRAINT fk_friendships_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_friendships_friend FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_friendships_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 8. friend_requests 表 - 好友请求表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS friend_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
from_user_id BIGINT NOT NULL,
|
||||||
|
to_user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
message VARCHAR(200),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
expires_at BIGINT,
|
||||||
|
processed_at BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_from_status ON friend_requests(from_user_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_to_status ON friend_requests(to_user_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_star ON friend_requests(star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_users_star ON friend_requests(from_user_id, to_user_id, star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_expires ON friend_requests(expires_at);
|
||||||
|
|
||||||
|
ALTER TABLE friend_requests
|
||||||
|
ADD CONSTRAINT fk_friend_requests_from_user FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_friend_requests_to_user FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_friend_requests_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 9. booth_slots 表 - 展位表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS booth_slots (
|
||||||
|
slot_id BIGSERIAL PRIMARY KEY,
|
||||||
|
host_profile_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
slot_index INTEGER NOT NULL,
|
||||||
|
visibility VARCHAR(20) NOT NULL DEFAULT 'public',
|
||||||
|
is_enabled BOOLEAN DEFAULT false,
|
||||||
|
unlock_type VARCHAR(20),
|
||||||
|
unlock_value INTEGER,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_host_slot UNIQUE (host_profile_id, slot_index)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_star ON booth_slots(user_id, star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_star_enabled ON booth_slots(star_id, is_enabled);
|
||||||
|
|
||||||
|
ALTER TABLE booth_slots
|
||||||
|
ADD CONSTRAINT fk_booth_slots_profile FOREIGN KEY (host_profile_id) REFERENCES fan_profiles(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 10. exhibitions 表 - 展品展示表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS exhibitions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
asset_id BIGINT NOT NULL,
|
||||||
|
slot_id BIGINT NOT NULL,
|
||||||
|
host_profile_id BIGINT NOT NULL,
|
||||||
|
occupier_uid BIGINT NOT NULL,
|
||||||
|
occupier_star_id BIGINT NOT NULL,
|
||||||
|
start_time BIGINT NOT NULL,
|
||||||
|
expire_at BIGINT NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_asset UNIQUE (asset_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_slot ON exhibitions(slot_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_host ON exhibitions(host_profile_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_occupier ON exhibitions(occupier_uid, occupier_star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_expire ON exhibitions(expire_at);
|
||||||
|
|
||||||
|
ALTER TABLE exhibitions
|
||||||
|
ADD CONSTRAINT fk_exhibitions_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_exhibitions_slot FOREIGN KEY (slot_id) REFERENCES booth_slots(slot_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 11. activities 表 - 运营活动表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS activities (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_type VARCHAR(50) NOT NULL,
|
||||||
|
title VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
start_time BIGINT NOT NULL,
|
||||||
|
end_time BIGINT NOT NULL,
|
||||||
|
target_progress BIGINT NOT NULL DEFAULT 1000,
|
||||||
|
current_progress BIGINT NOT NULL DEFAULT 0,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
|
stage_configs JSONB,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activities_star_id ON activities(star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activities_status ON activities(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activities_start_end ON activities(start_time, end_time);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 12. activity_items 表 - 活动道具表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_id BIGINT NOT NULL,
|
||||||
|
item_type VARCHAR(50) NOT NULL,
|
||||||
|
item_name VARCHAR(50) NOT NULL,
|
||||||
|
icon_url VARCHAR(500),
|
||||||
|
crystal_cost INTEGER NOT NULL,
|
||||||
|
contribution_points INTEGER NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_items_activity ON activity_items(activity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_items_type ON activity_items(item_type);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 13. activity_contributions 表 - 用户活动贡献记录表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_contributions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
item_id BIGINT NOT NULL,
|
||||||
|
item_type VARCHAR(50) NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL DEFAULT 1,
|
||||||
|
crystal_spent BIGINT NOT NULL,
|
||||||
|
contribution_points BIGINT NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_contributions_activity ON activity_contributions(activity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_contributions_user_star ON activity_contributions(user_id, star_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_contributions_created ON activity_contributions(created_at DESC);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 14. activity_user_stats 表 - 用户活动贡献汇总表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_user_stats (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
total_contribution BIGINT NOT NULL DEFAULT 0,
|
||||||
|
total_crystal_spent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
total_items INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_contribute_at BIGINT NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_activity_user_star UNIQUE (activity_id, user_id, star_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_user_stats_activity ON activity_user_stats(activity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_user_stats_contribution ON activity_user_stats(activity_id, total_contribution DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_user_stats_user_star ON activity_user_stats(user_id, star_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 外键约束 - Activity Tables
|
||||||
|
-- ============================================
|
||||||
|
ALTER TABLE activity_items
|
||||||
|
ADD CONSTRAINT fk_activity_items_activity FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE activity_contributions
|
||||||
|
ADD CONSTRAINT fk_activity_contributions_activity FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_activity_contributions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_activity_contributions_item FOREIGN KEY (item_id) REFERENCES activity_items(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_activity_contributions_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE activity_user_stats
|
||||||
|
ADD CONSTRAINT fk_activity_user_stats_activity FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_activity_user_stats_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT fk_activity_user_stats_star FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 注释说明
|
||||||
|
-- ============================================
|
||||||
|
COMMENT ON TABLE stars IS '明星信息表';
|
||||||
|
COMMENT ON TABLE users IS '用户表';
|
||||||
|
COMMENT ON TABLE fan_profiles IS '粉丝档案表';
|
||||||
|
COMMENT ON TABLE assets IS '资产表(藏品)';
|
||||||
|
COMMENT ON TABLE mint_orders IS '铸造订单表';
|
||||||
|
COMMENT ON TABLE asset_likes IS '点赞记录表';
|
||||||
|
COMMENT ON TABLE friendships IS '好友关系表';
|
||||||
|
COMMENT ON TABLE friend_requests IS '好友请求表';
|
||||||
|
COMMENT ON TABLE booth_slots IS '展位表';
|
||||||
|
COMMENT ON TABLE exhibitions IS '展品展示表';
|
||||||
|
COMMENT ON TABLE activities IS '运营活动表';
|
||||||
|
COMMENT ON TABLE activity_items IS '活动道具表';
|
||||||
|
COMMENT ON TABLE activity_contributions IS '用户活动贡献记录表';
|
||||||
|
COMMENT ON TABLE activity_user_stats IS '用户活动贡献汇总表';
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- Initialization Complete
|
||||||
|
-- ===================================================================
|
||||||
183
docker/start.sh
Executable file
183
docker/start.sh
Executable file
@ -0,0 +1,183 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===================================================================
|
||||||
|
# TopFans Docker 一键启动脚本
|
||||||
|
# 功能:构建并启动所有后端服务
|
||||||
|
# ===================================================================
|
||||||
|
#
|
||||||
|
# 使用方式:
|
||||||
|
# ./start.sh # 构建并启动所有服务(本地)
|
||||||
|
# ./start.sh --profile prod # 使用生产配置
|
||||||
|
# ./start.sh --build-only # 仅构建,不启动
|
||||||
|
# ./start.sh --no-cache # 无缓存构建
|
||||||
|
#
|
||||||
|
# 示例:
|
||||||
|
# ./start.sh # 本地开发模式
|
||||||
|
# ./start.sh --profile prod # 生产部署模式
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ==================== 颜色定义 ====================
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# ==================== 路径配置 ====================
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# ==================== 默认设置 ====================
|
||||||
|
PROFILE="local"
|
||||||
|
BUILD_ONLY=false
|
||||||
|
NO_CACHE=""
|
||||||
|
|
||||||
|
# ==================== 解析参数 ====================
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--profile)
|
||||||
|
PROFILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--build-only)
|
||||||
|
BUILD_ONLY=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-cache)
|
||||||
|
NO_CACHE="--no-cache"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
echo "用法: $0 [选项]"
|
||||||
|
echo ""
|
||||||
|
echo "选项:"
|
||||||
|
echo " --profile <env> 配置文件 (local/prod),默认 local"
|
||||||
|
echo " --build-only 仅构建,不启动服务"
|
||||||
|
echo " --no-cache 无缓存构建"
|
||||||
|
echo " --help, -h 显示帮助"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}错误: 未知选项 '$1'${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ==================== 配置检查 ====================
|
||||||
|
COMPOSE_FILE="docker-compose.${PROFILE}.yml"
|
||||||
|
ENV_FILE=".env.${PROFILE}"
|
||||||
|
|
||||||
|
if [ ! -f "$COMPOSE_FILE" ]; then
|
||||||
|
echo -e "${RED}错误: 配置文件 $COMPOSE_FILE 不存在${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo -e "${YELLOW}警告: $ENV_FILE 不存在,复制 .env.local 作为模板${NC}"
|
||||||
|
if [ -f ".env.local" ]; then
|
||||||
|
cp .env.local "$ENV_FILE"
|
||||||
|
else
|
||||||
|
echo -e "${RED}错误: 缺少环境配置文件${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 打印信息 ====================
|
||||||
|
print_step() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${BLUE} $1${NC}"
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 停止现有容器 ====================
|
||||||
|
print_step "🛑 停止现有容器"
|
||||||
|
|
||||||
|
# 停止并移除旧容器
|
||||||
|
docker-compose -f "$COMPOSE_FILE" down 2>/dev/null || true
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ 已停止旧容器${NC}"
|
||||||
|
|
||||||
|
# ==================== 构建镜像 ====================
|
||||||
|
print_step "🔨 构建 Docker 镜像"
|
||||||
|
|
||||||
|
# 调用构建脚本
|
||||||
|
if [ -n "$NO_CACHE" ]; then
|
||||||
|
./build.sh --no-cache
|
||||||
|
else
|
||||||
|
./build.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}❌ 构建失败${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ 镜像构建完成${NC}"
|
||||||
|
|
||||||
|
# 如果仅构建则退出
|
||||||
|
if [ "$BUILD_ONLY" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}🎉 构建完成(未启动服务)${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 启动服务 ====================
|
||||||
|
print_step "🚀 启动服务"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}使用配置: ${PROFILE}${NC}"
|
||||||
|
echo -e "${YELLOW}配置文件: ${COMPOSE_FILE}${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 启动所有服务(-d 后台运行)
|
||||||
|
docker-compose -f "$COMPOSE_FILE" --profile ${PROFILE} up -d
|
||||||
|
|
||||||
|
# ==================== 等待健康检查 ====================
|
||||||
|
print_step "⏳ 等待服务就绪"
|
||||||
|
|
||||||
|
# 等待时间(秒)
|
||||||
|
WAIT_TIME=60
|
||||||
|
INTERVAL=5
|
||||||
|
ELAPSED=0
|
||||||
|
|
||||||
|
echo -e "${YELLOW}正在等待服务启动...${NC}"
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $WAIT_TIME ]; do
|
||||||
|
# 检查 Gateway 健康状态
|
||||||
|
if curl -s http://localhost:8080/health > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}✅ Gateway 就绪${NC}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep $INTERVAL
|
||||||
|
ELAPSED=$((ELAPSED + INTERVAL))
|
||||||
|
echo -e "${YELLOW} 已等待 ${ELAPSED}s...${NC}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $ELAPSED -ge $WAIT_TIME ]; then
|
||||||
|
echo -e "${RED}⚠️ 服务启动超时,请检查日志${NC}"
|
||||||
|
echo "查看日志: docker-compose -f ${COMPOSE_FILE} logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 显示结果 ====================
|
||||||
|
print_step "📊 服务状态"
|
||||||
|
|
||||||
|
docker-compose -f "$COMPOSE_FILE" ps
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_step "🌐 服务地址"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}Gateway:${NC} http://localhost:8080"
|
||||||
|
echo -e " ${CYAN}Swagger UI:${NC} http://localhost:8080/swagger/index.html"
|
||||||
|
echo -e " ${CYAN}PostgreSQL:${NC} localhost:5432"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}🎉 启动完成!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "常用命令:"
|
||||||
|
echo " 查看日志: docker-compose -f ${COMPOSE_FILE} logs -f"
|
||||||
|
echo " 查看状态: docker-compose -f ${COMPOSE_FILE} ps"
|
||||||
|
echo " 停止服务: docker-compose -f ${COMPOSE_FILE} down"
|
||||||
|
echo ""
|
||||||
366
docs/API_CALLS_SUMMARY.md
Normal file
366
docs/API_CALLS_SUMMARY.md
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
# TopFans Frontend 接口调用总览
|
||||||
|
|
||||||
|
> 范围:`frontend` 源码(排除 `unpackage/dist` 编译产物)。
|
||||||
|
|
||||||
|
## 1. 请求基础设施
|
||||||
|
|
||||||
|
- **统一入口**:`frontend/utils/api.js` 的 `request(options)`,底层使用 `uni.request`。
|
||||||
|
- **Base URL**:`http://101.132.250.62:8080`(硬编码)。
|
||||||
|
- **鉴权注入**:
|
||||||
|
- 登录/注册接口(`/api/v1/auth/login`、`/api/v1/auth/register`)不注入 token。
|
||||||
|
- 其余接口自动从 `uni.getStorageSync('access_token')` 注入 `Authorization: Bearer <token>`。
|
||||||
|
- **统一响应解析**:
|
||||||
|
- HTTP `401`:清理 `access_token`/`user`,`reLaunch` 到登录页,`reject(Error('登录已过期,请重新登录'))`。
|
||||||
|
- HTTP `200` 且有 `res.data.code`:
|
||||||
|
- `code === 200`:`resolve(res.data)`。
|
||||||
|
- `code === 401/400/403`:同样清会话并跳登录。
|
||||||
|
- 其他业务码:`reject(Error(res.data.message || '请求失败'))`。
|
||||||
|
- 其他 HTTP 状态:`reject(Error(res.data?.message || 请求失败(statusCode)))`。
|
||||||
|
- **超时/重试**:未设置统一 `timeout`,无自动重试机制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. API 清单(函数名 / 参数 / 响应解析 / 用途)
|
||||||
|
|
||||||
|
### 2.1 认证与账号
|
||||||
|
|
||||||
|
#### `loginApi(mobile, password)`
|
||||||
|
- **Method/Path**:`POST /api/v1/auth/login`
|
||||||
|
- **参数**:`{ mobile, password }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `store/modules/user.js` -> `actions.login`
|
||||||
|
- `pages/login/login.vue` -> `handleLogin`(触发 `store.dispatch('user/login')`)
|
||||||
|
- **响应解析**:
|
||||||
|
- `res.code === 200 && res.data` 成功。
|
||||||
|
- 读取 `res.data.access_token`、`res.data.user` 写入本地缓存与 store。
|
||||||
|
- 若 `user.avatar_url` 存在,继续调用 `getOssPresignedUrlApi` 取 `res.data.url` 并本地缓存头像。
|
||||||
|
- **用途**:登录并初始化会话(token、用户信息、头像缓存)。
|
||||||
|
|
||||||
|
#### `registerApi(mobile, password, star_id, nickname)`
|
||||||
|
- **Method/Path**:`POST /api/v1/auth/register`
|
||||||
|
- **参数**:`{ mobile, password, star_id, nickname }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `store/modules/user.js` -> `actions.register`
|
||||||
|
- `pages/profile/selectRole.vue` -> `handleNext`(触发 `store.dispatch('user/register')`)
|
||||||
|
- **响应解析**:
|
||||||
|
- 成功同登录流程:读取 `access_token` 与 `user`,并尝试头像预缓存。
|
||||||
|
- 对 `409`(昵称冲突)做显式分支,向上抛 `{ code: 409, message }`。
|
||||||
|
- **用途**:注册并建立登录态。
|
||||||
|
|
||||||
|
#### `getUserProfileApi()`
|
||||||
|
- **Method/Path**:`GET /api/v1/auth/me`
|
||||||
|
- **参数**:无
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `fetchUserInfo`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `res.data` 字段:`uid`、`nickname`、`avatar_url`、`current_identity`、`assets_num`、`slot_limit`、`starbook_limit`、`blockchain_address` 等。
|
||||||
|
- 页面侧将 `current_identity` 转换为缓存结构 `fan_identity`,并回写本地 `user`。
|
||||||
|
- **用途**:个人中心刷新用户主数据(以服务端为准)。
|
||||||
|
|
||||||
|
#### `updateNicknameApi(nickname)`
|
||||||
|
- **Method/Path**:`PUT /api/v1/me/nickname`
|
||||||
|
- **参数**:`{ nickname }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `confirmChangeNickname`
|
||||||
|
- **响应解析**:检查 `res.code === 200` 后触发 `fetchUserInfo(true)` 全量刷新。
|
||||||
|
- **用途**:修改当前身份昵称。
|
||||||
|
|
||||||
|
#### `updatePasswordApi(oldPassword, newPassword)`
|
||||||
|
- **Method/Path**:`POST /api/v1/account/password`
|
||||||
|
- **参数**:`{ old_password, new_password }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `confirmChangePassword`
|
||||||
|
- **响应解析**:`res.code === 200` 后提示成功并强制登出、清缓存、跳登录页。
|
||||||
|
- **用途**:密码修改与会话重置。
|
||||||
|
|
||||||
|
#### `deleteAccountApi()`
|
||||||
|
- **Method/Path**:`POST /api/user/delete-account`
|
||||||
|
- **参数**:无
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `confirmDeleteAccount`
|
||||||
|
- **响应解析**:`result.code === 200` 后执行登出流程并跳登录页。
|
||||||
|
- **用途**:注销账号。
|
||||||
|
|
||||||
|
#### `updateUserInfoApi(nickname)`
|
||||||
|
- **Method/Path**:`POST /api/user/update`
|
||||||
|
- **参数**:`{ nickname }`
|
||||||
|
- **调用位置**:当前源码未发现调用。
|
||||||
|
- **用途**:历史/预留接口(当前未接入)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 社交(好友关系)
|
||||||
|
|
||||||
|
#### `friendListApi(page = 1, pageSize = 10)`
|
||||||
|
- **Method/Path**:`GET /api/v1/social/friends?page={page}&page_size={pageSize}`
|
||||||
|
- **参数**:分页参数
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `loadFriendList`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `res.data.items`、`res.data.total`。
|
||||||
|
- 字段映射:`friend_id -> user_id`,`friend_nickname -> nickname`,`friend_fan_level -> fan_level`,`friend_avatar -> avatar_url`。
|
||||||
|
- 头像会再经 `getFriendAvatarRealUrl()` 调 `getOssPresignedUrlApi` 转真实 URL。
|
||||||
|
- **用途**:好友列表页及分页加载。
|
||||||
|
|
||||||
|
#### `searchUserApi(friendUserId)`
|
||||||
|
- **Method/Path**:`GET /api/v1/social/search-user?friend_user_id={friendUserId}`
|
||||||
|
- **参数**:目标用户 ID(UID)
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `watch(searchUid, ...)` 防抖回调
|
||||||
|
- **响应解析**:
|
||||||
|
- 成功读取 `res.data` 并解析 `res.data.avatar_url` 为预签名 URL。
|
||||||
|
- 失败时对 `404` 走“用户不存在”分支。
|
||||||
|
- **用途**:按 UID 搜索可添加的用户。
|
||||||
|
|
||||||
|
#### `sendFriendRequestApi(friendUserId)`
|
||||||
|
- **Method/Path**:`POST /api/v1/social/friend-requests`
|
||||||
|
- **参数**:`{ friend_user_id }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `confirmAddFriend`
|
||||||
|
- **响应解析**:调用成功后刷新“已发送请求”列表。
|
||||||
|
- **用途**:发送好友申请。
|
||||||
|
|
||||||
|
#### `getSentFriendRequestsApi(page = 1, pageSize = 10)`
|
||||||
|
- **Method/Path**:`GET /api/v1/social/friend-requests?type=sent&status=pending&page={page}&page_size={pageSize}`
|
||||||
|
- **参数**:分页参数
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `fetchSentRequests`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `res.data.items`。
|
||||||
|
- 解析 `to_user_avatar_url` 为预签名 URL 后渲染。
|
||||||
|
- **用途**:我的已发送申请列表。
|
||||||
|
|
||||||
|
#### `getReceivedFriendRequestsApi(page = 1, pageSize = 10)`
|
||||||
|
- **Method/Path**:`GET /api/v1/social/friend-requests?type=received&status=pending&page={page}&page_size={pageSize}`
|
||||||
|
- **参数**:分页参数
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `loadReceivedRequests`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `res.data.items` 分页追加。
|
||||||
|
- 解析 `from_user_avatar_url` 为预签名 URL。
|
||||||
|
- **用途**:收到的好友请求列表。
|
||||||
|
|
||||||
|
#### `handleFriendRequestApi(requestId, action)`
|
||||||
|
- **Method/Path**:`POST /api/v1/social/friend-requests/handle`
|
||||||
|
- **参数**:`{ request_id, action }`,`action ∈ {accept, reject}`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `handleAcceptRequest`
|
||||||
|
- `pages/components/FriendsContent.vue` -> `handleRejectRequest`
|
||||||
|
- **响应解析**:成功后本地移除对应请求项。
|
||||||
|
- **用途**:接受/拒绝好友请求。
|
||||||
|
|
||||||
|
#### `deleteFriendApi(friendUserId)`
|
||||||
|
- **Method/Path**:`DELETE /api/v1/social/friends`
|
||||||
|
- **参数**:`{ friend_user_id }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/FriendsContent.vue` -> `confirmDeleteFriend`
|
||||||
|
- **响应解析**:成功后本地删除好友项并更新计数。
|
||||||
|
- **用途**:解除好友关系。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 身份(粉丝身份)
|
||||||
|
|
||||||
|
#### `getFanIdentitiesApi()`
|
||||||
|
- **Method/Path**:`GET /api/v1/fan-identities`
|
||||||
|
- **参数**:无
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `handleSwitchRole`
|
||||||
|
- **响应解析**:读取 `res.data.items` 作为可选明星列表。
|
||||||
|
- **用途**:拉取可新增的粉丝身份候选。
|
||||||
|
|
||||||
|
#### `addFanIdentityApi(starId, nickname)`
|
||||||
|
- **Method/Path**:`POST /api/v1/my/fan-identities`
|
||||||
|
- **参数**:`{ star_id, nickname }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `confirmAddIdentity`
|
||||||
|
- **响应解析**:`res.code === 200` 后刷新用户信息。
|
||||||
|
- **用途**:新增某明星身份。
|
||||||
|
|
||||||
|
#### `getMyFanIdentitiesApi()`
|
||||||
|
- **Method/Path**:`GET /api/v1/my/fan-identities`
|
||||||
|
- **参数**:无
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `handleFanTagClick`
|
||||||
|
- **响应解析**:读取 `res.data.items` 与 `res.data.current_star_id`。
|
||||||
|
- **用途**:身份切换前加载身份列表与当前身份。
|
||||||
|
|
||||||
|
#### `switchFanIdentityApi(newStarId)`
|
||||||
|
- **Method/Path**:`POST /api/v1/my/fan-identities/switch`
|
||||||
|
- **参数**:`{ new_star_id }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `handleSwitchIdentity`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `res.data.access_token` 覆盖本地 token。
|
||||||
|
- 读取 `res.data.current_identity`(`identity_id`、`identity_name`、`tag`、`level`)刷新 UI 与缓存。
|
||||||
|
- **用途**:切换当前生效身份并刷新会话上下文。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 资产/NFT(星册、详情、点赞)
|
||||||
|
|
||||||
|
#### `getMyAssetsApi(page = 1, pageSize = 20)`
|
||||||
|
- **Method/Path**:`GET /api/v1/assets/me/items?page={page}&page_size={pageSize}`
|
||||||
|
- **参数**:分页参数
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/StarbookContent.vue` -> `loadAssetsList`
|
||||||
|
- `pages/exhibition/exhibition.vue` -> `handleAddAssetClick`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `response.data.items`。
|
||||||
|
- 常用字段:`asset_id`、`name`、`cover_url`、`tx_hash`、`like_count`、`status`。
|
||||||
|
- `cover_url` 会通过 `getAssetCoverRealUrl()` -> `getOssPresignedUrlApi(type=asset)` 转真实访问 URL。
|
||||||
|
- **用途**:我的藏品列表(星册页、展馆上架弹窗)。
|
||||||
|
|
||||||
|
#### `getAssetDetailApi(assetId)`
|
||||||
|
- **Method/Path**:`GET /api/v1/assets/{assetId}`
|
||||||
|
- **参数**:`assetId`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/StarbookContent.vue` -> `handleCardClick`
|
||||||
|
- `pages/exhibition/exhibition.vue` -> `handleNftClick`
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `response.data.asset` 作为详情对象。
|
||||||
|
- 点赞态兼容读取:`asset.is_liked`,部分调用还兜底 `response.data.is_liked`。
|
||||||
|
- **用途**:点击藏品卡片时拉取完整详情。
|
||||||
|
|
||||||
|
#### `likeAssetApi(assetId)`
|
||||||
|
- **Method/Path**:`POST /api/v1/social/assets/{assetId}/like`
|
||||||
|
- **参数**:`assetId`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/NftDetailModal.vue` -> `handleLike`(点赞分支)
|
||||||
|
- **响应解析**:`response.code === 200` 后本地 `isLiked=true`、`likeCount+1`。
|
||||||
|
- **用途**:点赞藏品。
|
||||||
|
|
||||||
|
#### `unlikeAssetApi(assetId)`
|
||||||
|
- **Method/Path**:`DELETE /api/v1/social/assets/{assetId}/like`
|
||||||
|
- **参数**:`assetId`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/NftDetailModal.vue` -> `handleLike`(取消点赞分支)
|
||||||
|
- **响应解析**:`response.code === 200` 后本地 `isLiked=false`、`likeCount-1`。
|
||||||
|
- **用途**:取消点赞。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 展馆(槽位、上架、下架)
|
||||||
|
|
||||||
|
#### `getMyGalleriesApi()`
|
||||||
|
- **Method/Path**:`GET /api/v1/mygalleries`
|
||||||
|
- **参数**:无
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/exhibition/exhibition.vue` -> `loadGallerySlots`(`isMyGallery === true` 分支)
|
||||||
|
- **响应解析**:
|
||||||
|
- 读取 `response.data.gallery_owner_id`、`response.data.slots`。
|
||||||
|
- `slots` 按 `visibility(public/private)` 分板,并读取 `slot.asset`(`asset_id`、`cover_url`、`like_count`、`remain_time` 等)。
|
||||||
|
- **用途**:加载当前用户展馆槽位状态。
|
||||||
|
|
||||||
|
#### `getUserGalleriesApi(targetUid)`
|
||||||
|
- **Method/Path**:`GET /api/v1/galleries/{targetUid}`
|
||||||
|
- **参数**:目标用户 UID
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/exhibition/exhibition.vue` -> `loadGallerySlots`(`isMyGallery === false` 分支)
|
||||||
|
- **响应解析**:与 `getMyGalleriesApi` 同结构解析。
|
||||||
|
- **用途**:加载目标用户展馆。
|
||||||
|
|
||||||
|
#### `placeAssetToGalleryApi(assetId, galleryOwnerId, slotId)`
|
||||||
|
- **Method/Path**:`POST /api/v1/galleries/place`
|
||||||
|
- **参数**:`{ asset_id, gallery_owner_id, slot_id }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/exhibition/exhibition.vue` -> `confirmPlaceAsset`
|
||||||
|
- **响应解析**:`response.code === 200` 后关闭弹窗并重新拉取槽位数据。
|
||||||
|
- **用途**:上架藏品到指定展位槽位。
|
||||||
|
|
||||||
|
#### `removeAssetFromGalleryApi(slotId)`
|
||||||
|
- **Method/Path**:`DELETE /api/v1/galleries/slots/{slotId}/asset`
|
||||||
|
- **参数**:`slotId`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/exhibition/exhibition.vue` -> `confirmRemoveAsset`
|
||||||
|
- **响应解析**:`response.code === 200` 后刷新展馆槽位。
|
||||||
|
- **用途**:下架槽位中的藏品。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 OSS(上传签名 / 预签名读取 / 头像更新)
|
||||||
|
|
||||||
|
#### `getOssSignatureApi(type)`
|
||||||
|
- **Method/Path**:`GET /api/v1/assets/oss/signature?type={type}`
|
||||||
|
- **参数**:`type`(实际使用:`avatar`、`asset`)
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `uploadAvatarToOss`(`type='avatar'`)
|
||||||
|
- `pages/components/CastloveContent.vue` -> `uploadImageToOss`(`type='asset'`)
|
||||||
|
- **响应解析**:
|
||||||
|
- 头像上传与素材上传都会读取:
|
||||||
|
- `host`, `dir`, `policy`, `x_oss_credential`, `x_oss_date`, `security_token`, `signature`, `x_oss_signature_version`。
|
||||||
|
- `type=asset` 时额外读取 `signRes.data.order_id`(铸造订单草稿 ID)。
|
||||||
|
- **用途**:向 OSS 直传前获取签名凭证。
|
||||||
|
|
||||||
|
#### `getOssPresignedUrlApi(fileName, expires = 3600, type = 'avatar')`
|
||||||
|
- **Method/Path**:`GET /api/v1/assets/oss/presigned-url?file_name={encodeURIComponent(fileName)}&expires={expires}&type={type}`
|
||||||
|
- **参数**:文件名/路径、有效期、类型
|
||||||
|
- **调用位置**:
|
||||||
|
- `store/modules/user.js` -> `actions.login`、`actions.register`
|
||||||
|
- `pages/profile/profile.vue` -> `handleAvatarUpdateSuccess`
|
||||||
|
- `pages/components/Avatar.vue` -> `fetchOwnAvatarWithCache`
|
||||||
|
- `utils/assetImageHelper.js` -> `getAssetCoverRealUrl`、`getFriendAvatarRealUrl`
|
||||||
|
- **响应解析**:通用读取 `res.data.url`;失败时回退默认图或空值。
|
||||||
|
- **用途**:将 OSS 对象标识转换为可读 URL(头像、藏品封面)。
|
||||||
|
|
||||||
|
#### `updateAvatarApi(avatarUrl)`
|
||||||
|
- **Method/Path**:`PUT /api/v1/me/avatar`
|
||||||
|
- **参数**:`{ avatar_url }`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `uploadAvatarToOss`(OSS 上传成功后)
|
||||||
|
- **响应解析**:`updateRes.code === 200` 后更新本地 user 缓存、触发 `avatarUpdated` 事件并刷新组件。
|
||||||
|
- **用途**:头像上传成功后回写用户头像地址。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 铸造(Castlove)
|
||||||
|
|
||||||
|
#### `createMintOrderApi(orderData)`
|
||||||
|
- **Method/Path**:`POST /api/v1/assets/mints`
|
||||||
|
- **参数**:`orderData`(页面构造字段)
|
||||||
|
- `name`, `event`, `description`, `material_type`, `material_url`, `rarity`, `tags`, `order_id`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/CastloveContent.vue` -> `handleConfirm`
|
||||||
|
- **响应解析**:`response.code === 200` 后视为铸造成功,保存临时展示数据并跳转成功页。
|
||||||
|
- **用途**:提交铸造订单。
|
||||||
|
|
||||||
|
#### `deleteMintOrderApi(orderId)`
|
||||||
|
- **Method/Path**:`DELETE /api/v1/assets/mints/{orderId}`
|
||||||
|
- **参数**:`orderId`
|
||||||
|
- **调用位置**:
|
||||||
|
- `pages/components/CastloveContent.vue` -> `handleBack`(用户确认返回时清理未完成订单)
|
||||||
|
- **响应解析**:仅调用,不依赖返回体字段(失败仅记录日志,不阻断返回流程)。
|
||||||
|
- **用途**:清理中断铸造产生的草稿订单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 非封装直连网络调用(不经过 `request()`)
|
||||||
|
|
||||||
|
### `uni.uploadFile`(OSS 直传)
|
||||||
|
- **位置**:
|
||||||
|
- `pages/profile/profile.vue` -> `uploadAvatarToOss`
|
||||||
|
- `pages/components/CastloveContent.vue` -> `uploadImageToOss`
|
||||||
|
- **前置**:先调 `getOssSignatureApi` 获取签名字段。
|
||||||
|
- **成功判定**:`uploadRes.statusCode === 200 || 204`。
|
||||||
|
- **后续动作**:
|
||||||
|
- 头像上传:调用 `updateAvatarApi` 回写业务系统头像地址。
|
||||||
|
- 藏品素材上传:生成 `uploadedImageUrl`,用于 `createMintOrderApi`。
|
||||||
|
- **说明**:不走 `request()`,因此不受其统一鉴权/错误码分流逻辑约束。
|
||||||
|
|
||||||
|
### `uni.downloadFile`(头像文件缓存)
|
||||||
|
- **位置**:
|
||||||
|
- `utils/avatarCache.js` -> `downloadAndCacheAvatar`
|
||||||
|
- **调用链**:`getOssPresignedUrlApi -> downloadAndCacheAvatar(avatarUrl, realUrl) -> uni.downloadFile`
|
||||||
|
- **成功判定**:`res.statusCode === 200`
|
||||||
|
- **用途**:将头像下载到本地并 `saveFile`,提升后续展示性能与稳定性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 调用覆盖与观察
|
||||||
|
|
||||||
|
- `utils/api.js` 共导出 **31** 个 API 函数。
|
||||||
|
- 其中 **30 个已在源码业务层使用**,`updateUserInfoApi` 当前未发现调用。
|
||||||
|
- 响应处理范式基本统一:
|
||||||
|
- 成功分支:`res.code === 200` + 按需读取 `res.data.*`
|
||||||
|
- 失败分支:依赖 `throw Error(message)`,页面显示 `error.message`。
|
||||||
|
|
||||||
438
docs/PRD-Rankings.md
Normal file
438
docs/PRD-Rankings.md
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
# TopFans 排行榜功能需求文档
|
||||||
|
|
||||||
|
## 1. 功能概述
|
||||||
|
|
||||||
|
### 1.1 产品背景
|
||||||
|
|
||||||
|
排行榜功能旨在提升用户活跃度和参与度,通过展示热门藏品和自制佳作榜单,激励用户创作和互动。
|
||||||
|
|
||||||
|
### 1.2 排行榜类型
|
||||||
|
|
||||||
|
| 排行类型 | 统计维度 | 数据来源 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| 热度排行 | 展示中 / 本月 / 全部 | 藏品点赞数 |
|
||||||
|
| 自制排行 | 展示中 / 本月 / 全部 | 自制藏品点赞数 |
|
||||||
|
|
||||||
|
> **注意**: 活动排行榜功能待后续单独设计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 数据模型设计
|
||||||
|
|
||||||
|
### 2.1 现有数据分析
|
||||||
|
|
||||||
|
**Asset (藏品表)**
|
||||||
|
- `LikeCount` - 点赞数(累计)
|
||||||
|
- `OwnerUID` - 拥有者ID
|
||||||
|
- `StarID` - 粉丝身份ID
|
||||||
|
- `CreatedAt` - 创建时间
|
||||||
|
|
||||||
|
**AssetLike (点赞记录表)**
|
||||||
|
- `AssetID` - 藏品ID
|
||||||
|
- `UserID` - 点赞用户ID
|
||||||
|
- `CreatedAt` - 点赞时间(用于本月统计)
|
||||||
|
|
||||||
|
**Exhibition (展品展示表)**
|
||||||
|
- `AssetID` - 展品ID
|
||||||
|
- `StartTime` - 开始展示时间
|
||||||
|
- `ExpireAt` - 过期时间(用于"展示中"判定)
|
||||||
|
|
||||||
|
### 2.2 新增数据模型
|
||||||
|
|
||||||
|
#### 2.2.1 藏品表扩展
|
||||||
|
|
||||||
|
在 Asset 表中增加 `IsOriginal` 字段(true=自制藏品):
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Asset struct {
|
||||||
|
// ... 现有字段
|
||||||
|
IsOriginal bool `gorm:"default:false;column:is_original"` // 是否自制藏品
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API 接口设计
|
||||||
|
|
||||||
|
### 3.1 通用响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"items": [],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10,
|
||||||
|
"total": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 热度排行榜
|
||||||
|
|
||||||
|
#### 3.2.1 获取热度排行榜
|
||||||
|
|
||||||
|
**接口**: GET /api/v1/rankings/hot
|
||||||
|
|
||||||
|
**认证**: 需认证 (JWT Token)
|
||||||
|
|
||||||
|
**Query 参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| dimension | string | 是 | 统计维度: `displaying`(展示中) / `month`(本月) / `total`(全部) |
|
||||||
|
| star_id | int64 | 否 | 粉丝身份ID筛选,不传则返回当前身份数据 |
|
||||||
|
| page | int | 否 | 页码,默认1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"my_ranking": {
|
||||||
|
"rank": 5,
|
||||||
|
"asset_id": 1005,
|
||||||
|
"asset_name": "我的藏品",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"like_count": 88,
|
||||||
|
"status": "ranked"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"rank": 1,
|
||||||
|
"asset_id": 1001,
|
||||||
|
"asset_name": "战战生贺",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"owner_uid": 12345,
|
||||||
|
"owner_nickname": "爱战战",
|
||||||
|
"like_count": 999,
|
||||||
|
"is_original": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rank": 2,
|
||||||
|
"asset_id": 1002,
|
||||||
|
"asset_name": "肖战同框",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"owner_uid": 12346,
|
||||||
|
"owner_nickname": "小飞侠",
|
||||||
|
"like_count": 888,
|
||||||
|
"is_original": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10,
|
||||||
|
"total": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**我的排名返回说明**:
|
||||||
|
|
||||||
|
| 状态 | rank | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 上榜 | 5 | 在 Top N 内,显示具体排名 |
|
||||||
|
| 未上榜 | null | 不在 Top N 内,显示差距 |
|
||||||
|
|
||||||
|
**上榜返回示例 (my_ranking)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"my_ranking": {
|
||||||
|
"rank": 5,
|
||||||
|
"asset_id": 1005,
|
||||||
|
"asset_name": "我的藏品",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"like_count": 88,
|
||||||
|
"status": "ranked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**未上榜返回示例 (my_ranking)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"my_ranking": {
|
||||||
|
"rank": null,
|
||||||
|
"asset_id": 1005,
|
||||||
|
"asset_name": "我的藏品",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"like_count": 50,
|
||||||
|
"status": "unranked",
|
||||||
|
"diff_to_rank": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**统计逻辑**:
|
||||||
|
|
||||||
|
| 维度 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `displaying` | 统计当前正在展示的藏品的点赞数(Exhibition.ExpireAt > Now()) |
|
||||||
|
| `month` | 统计当前自然月内(2026年3月1日-3月31日)获得的点赞数 |
|
||||||
|
| `total` | 统计藏品累计点赞数 |
|
||||||
|
|
||||||
|
**多藏品处理**:
|
||||||
|
|
||||||
|
- 用户有多个藏品时,返回排行最高的那个藏品排名
|
||||||
|
|
||||||
|
**分页规则**:
|
||||||
|
|
||||||
|
- 每次返回 10 条(page_size 默认 10)
|
||||||
|
- 榜单最多显示前 100 名(可配置)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 自制排行榜
|
||||||
|
|
||||||
|
#### 3.3.1 获取自制排行榜
|
||||||
|
|
||||||
|
**接口**: GET /api/v1/rankings/original
|
||||||
|
|
||||||
|
**认证**: 需认证 (JWT Token)
|
||||||
|
|
||||||
|
**Query 参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| dimension | string | 是 | 统计维度: `displaying`(展示中) / `month`(本月) / `total`(全部) |
|
||||||
|
| star_id | int64 | 否 | 粉丝身份ID筛选,不传则返回当前身份数据 |
|
||||||
|
| page | int | 否 | 页码,默认1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"my_ranking": {
|
||||||
|
"rank": 3,
|
||||||
|
"asset_id": 1002,
|
||||||
|
"asset_name": "自制海报",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"like_count": 666,
|
||||||
|
"status": "ranked"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"rank": 1,
|
||||||
|
"asset_id": 1002,
|
||||||
|
"asset_name": "自制海报",
|
||||||
|
"cover_url": "https://...",
|
||||||
|
"owner_uid": 12346,
|
||||||
|
"owner_nickname": "小飞侠",
|
||||||
|
"like_count": 888,
|
||||||
|
"material_url": "https://...",
|
||||||
|
"is_original": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10,
|
||||||
|
"total": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**统计逻辑**:
|
||||||
|
|
||||||
|
- 仅统计 `is_original = true` 的自制藏品
|
||||||
|
- 其他维度逻辑同热度排行榜
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 前端界面设计
|
||||||
|
|
||||||
|
### 4.1 排行榜入口
|
||||||
|
|
||||||
|
在首页广场添加"排行榜"Tab,或在个人中心添加入口。
|
||||||
|
|
||||||
|
### 4.2 排行榜页面结构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 排行榜 │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ [热度] [自制] │ ← Tab 切换
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 🥇 用户A 999 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ │
|
||||||
|
│ 🥈 用户B 888 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ │
|
||||||
|
│ 🥉 用户C 777 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ [展示中] [本月] [全部] │ ← 维度切换
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 我的排名吸底展示
|
||||||
|
|
||||||
|
**上榜状态**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 排行榜 │
|
||||||
|
│ ... │
|
||||||
|
│ 8. 用户H 100 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ 9. 用户I 99 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ 10. 用户J 98 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 🏆 我的排名: 第5名 │
|
||||||
|
│ ❤️ [图片] 我的藏品 88赞 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**未上榜状态**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 排行榜 │
|
||||||
|
│ ... │
|
||||||
|
│ 98. 用户H 60 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ 99. 用户I 59 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
│ 100. 用户J 58 赞 │
|
||||||
|
│ [图片] │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 😢 未上榜 │
|
||||||
|
│ 距离上榜还差 8 热度 │
|
||||||
|
│ [图片] 我的藏品 50赞 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**空状态**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 排行榜 │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 暂无数据,快去创作吧 │
|
||||||
|
│ │
|
||||||
|
│ [去铸造] │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据库变更
|
||||||
|
|
||||||
|
### 5.1 藏品表扩展
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE assets ADD COLUMN is_original BOOLEAN DEFAULT FALSE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 性能优化
|
||||||
|
|
||||||
|
### 6.1 缓存策略
|
||||||
|
|
||||||
|
| 数据 | 缓存策略 | 过期时间 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 热度排行榜 | Redis ZSet | 5分钟 |
|
||||||
|
| 自制排行榜 | Redis ZSet | 5分钟 |
|
||||||
|
|
||||||
|
### 6.2 定时任务
|
||||||
|
|
||||||
|
1. **排行榜定时刷新** - 每5分钟更新一次排行榜缓存
|
||||||
|
2. **排名计算** - 每日凌晨重新计算精确排名
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 业务流程
|
||||||
|
|
||||||
|
### 7.1 点赞时更新排行榜
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点赞藏品
|
||||||
|
↓
|
||||||
|
AssetLike 表新增记录 (记录点赞时间用于本月统计)
|
||||||
|
↓
|
||||||
|
Asset.LikeCount +1
|
||||||
|
↓
|
||||||
|
更新 Redis 热度/自制排行 ZSet
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 展示中藏品统计
|
||||||
|
|
||||||
|
```
|
||||||
|
定时任务扫描 Exhibition 表
|
||||||
|
↓
|
||||||
|
筛选 ExpireAt > Now() 的记录
|
||||||
|
↓
|
||||||
|
关联 Asset 表获取点赞数
|
||||||
|
↓
|
||||||
|
更新 Redis 展示中排行 ZSet
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 本月统计
|
||||||
|
|
||||||
|
```
|
||||||
|
用户查询本月排行时
|
||||||
|
↓
|
||||||
|
计算当月起始时间戳 (2026-03-01 00:00:00)
|
||||||
|
↓
|
||||||
|
筛选 AssetLike.CreatedAt >= 月起始时间
|
||||||
|
↓
|
||||||
|
按 AssetID 分组统计点赞数
|
||||||
|
↓
|
||||||
|
返回排行结果
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 我的排名计算
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求排行榜
|
||||||
|
↓
|
||||||
|
获取用户在该粉丝身份下所有藏品
|
||||||
|
↓
|
||||||
|
找出点赞数最高的藏品
|
||||||
|
↓
|
||||||
|
查询该藏品在榜单中的排名
|
||||||
|
↓
|
||||||
|
如果不在 Top 100,计算与第100名差距
|
||||||
|
↓
|
||||||
|
返回 my_ranking 信息(含 cover_url)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 待确认问题
|
||||||
|
|
||||||
|
1. **粉丝身份隔离**: 排行榜是否需要按粉丝身份隔离?
|
||||||
|
- ✅ 已确认:按当前用户的 star_id 过滤
|
||||||
|
|
||||||
|
2. **展示中判定**: 展位被占后4小时自动下架,如何处理?
|
||||||
|
- ✅ Exhibition.ExpireAt > Now() 即为展示中
|
||||||
|
|
||||||
|
3. **本月统计**: 自然月切换时(如3月1日),是否需要清理缓存?
|
||||||
|
- 待定:建议跨月时刷新缓存
|
||||||
|
|
||||||
|
4. **Top N 默认值**: 默认显示前多少名?
|
||||||
|
- ✅ 已确认:默认 100
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| V1.0 | 2026-03-11 | 初始版本 |
|
||||||
|
| V1.1 | 2026-03-12 | 移除活动排行榜,补充我的排名、差距计算、分页规则 |
|
||||||
|
| V1.2 | 2026-03-12 | my_ranking 补充 cover_url 字段 |
|
||||||
229
docs/PRD-activity.md
Normal file
229
docs/PRD-activity.md
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
# 运营活动功能实现计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
用户希望添加一个运营活动功能:
|
||||||
|
- 用户可以进入运营活动界面
|
||||||
|
- 通过水晶购买道具推进活动进程
|
||||||
|
- 购买道具可获得贡献点
|
||||||
|
- 后台统计贡献点排名
|
||||||
|
- 运营活动有时间限制
|
||||||
|
- 不同阶段展示不同界面效果
|
||||||
|
|
||||||
|
**现有代码基础**:
|
||||||
|
- 前端已有应援活动页面 (`frontend/pages/support-activity/`)
|
||||||
|
- 已有活动配置 (`frontend/utils/activity-config.js`)
|
||||||
|
- 已有排行榜服务可作为参考 (`backend/services/assetService/service/ranking_service.go`)
|
||||||
|
- 水晶余额在 `fan_profiles.crystal_balance` 中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实现方案
|
||||||
|
|
||||||
|
### 1. 数据库设计
|
||||||
|
|
||||||
|
**✅ 已确认: 道具配置使用数据库方式**
|
||||||
|
|
||||||
|
新建表:`activities`, `activity_items`, `activity_contributions`, `activity_user_stats`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 运营活动表
|
||||||
|
CREATE TABLE IF NOT EXISTS activities (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_type VARCHAR(50) NOT NULL, -- 活动类型: birthday/concert/bus
|
||||||
|
title VARCHAR(100) NOT NULL, -- 活动标题
|
||||||
|
description TEXT, -- 活动描述
|
||||||
|
star_id BIGINT NOT NULL, -- 所属明星
|
||||||
|
start_time BIGINT NOT NULL, -- 开始时间
|
||||||
|
end_time BIGINT NOT NULL, -- 结束时间
|
||||||
|
target_progress BIGINT NOT NULL DEFAULT 1000, -- 目标进度
|
||||||
|
current_progress BIGINT NOT NULL DEFAULT 0, -- 当前进度
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/active/completed/expired
|
||||||
|
stage_configs JSONB, -- 阶段配置 (不同进度的背景图等)
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 活动道具表 (数据库配置,支持运营平台动态调整)
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_id BIGINT NOT NULL,
|
||||||
|
item_type VARCHAR(50) NOT NULL, -- 道具类型: firework/megaphone/love
|
||||||
|
item_name VARCHAR(50) NOT NULL, -- 道具名称
|
||||||
|
icon_url VARCHAR(500), -- 道具图标
|
||||||
|
crystal_cost INTEGER NOT NULL, -- 水晶消耗
|
||||||
|
contribution_points INTEGER NOT NULL, -- 贡献点奖励
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 用户活动贡献记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_contributions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
item_id BIGINT NOT NULL,
|
||||||
|
item_type VARCHAR(50) NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL DEFAULT 1,
|
||||||
|
crystal_spent BIGINT NOT NULL,
|
||||||
|
contribution_points BIGINT NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 用户活动贡献汇总表 (用于排行榜)
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_user_stats (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
activity_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
star_id BIGINT NOT NULL,
|
||||||
|
total_contribution BIGINT NOT NULL DEFAULT 0,
|
||||||
|
total_crystal_spent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
total_items INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_contribute_at BIGINT NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
updated_at BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT uk_activity_user_star UNIQUE (activity_id, user_id, star_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Proto 定义
|
||||||
|
|
||||||
|
新建 `backend/proto/activity.proto`:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
// 活动相关消息定义
|
||||||
|
message Activity {
|
||||||
|
int64 id = 1;
|
||||||
|
string activity_type = 2;
|
||||||
|
string title = 3;
|
||||||
|
string description = 4;
|
||||||
|
int64 star_id = 5;
|
||||||
|
int64 start_time = 6;
|
||||||
|
int64 end_time = 7;
|
||||||
|
int64 target_progress = 8;
|
||||||
|
int64 current_progress = 9;
|
||||||
|
string status = 10;
|
||||||
|
string current_stage = 11; // 当前阶段: early/mid/late/completed
|
||||||
|
repeated ActivityItem items = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ActivityItem {
|
||||||
|
int64 id = 1;
|
||||||
|
string item_type = 2;
|
||||||
|
string item_name = 3;
|
||||||
|
string icon_url = 4;
|
||||||
|
int32 crystal_cost = 5;
|
||||||
|
int32 contribution_points = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 贡献购买请求
|
||||||
|
message PurchaseItemRequest {
|
||||||
|
int64 activity_id = 1;
|
||||||
|
string item_type = 2;
|
||||||
|
int32 quantity = 3;
|
||||||
|
int64 star_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 贡献点排名
|
||||||
|
message ContributionRankingRequest {
|
||||||
|
int64 activity_id = 1;
|
||||||
|
int64 star_id = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContributionRankingItem {
|
||||||
|
int32 rank = 1;
|
||||||
|
int64 user_id = 2;
|
||||||
|
string nickname = 3;
|
||||||
|
string avatar_url = 4;
|
||||||
|
int64 total_contribution = 5;
|
||||||
|
int64 total_crystal_spent = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MyContribution {
|
||||||
|
int32 rank = 1;
|
||||||
|
int64 total_contribution = 2;
|
||||||
|
int64 total_crystal_spent = 3;
|
||||||
|
string status = 4; // ranked/unranked
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 后端服务实现
|
||||||
|
|
||||||
|
**新建服务**: `backend/services/activityService/`
|
||||||
|
|
||||||
|
- `main.go` - 服务入口
|
||||||
|
- `service/activity_service.go` - 业务逻辑
|
||||||
|
- `repository/activity_repository.go` - 数据库操作
|
||||||
|
- `provider/activity_provider.go` - Dubbo provider
|
||||||
|
|
||||||
|
**API 端点** (通过 gateway):
|
||||||
|
- `GET /api/v1/activities/:id` - 获取活动详情
|
||||||
|
- `GET /api/v1/activities/:id/items` - 获取活动道具列表
|
||||||
|
- `POST /api/v1/activities/:id/purchase` - 购买道具
|
||||||
|
- `GET /api/v1/activities/:id/ranking` - 贡献点排名
|
||||||
|
- `GET /api/v1/activities?star_id=:star_id` - 获取活动列表
|
||||||
|
|
||||||
|
**水晶扣减逻辑**:
|
||||||
|
1. 检查用户水晶余额是否足够
|
||||||
|
2. 扣减水晶 (`fan_profiles.crystal_balance`)
|
||||||
|
3. 增加活动进度
|
||||||
|
4. 记录贡献明细
|
||||||
|
5. 更新用户贡献汇总
|
||||||
|
6. 返回结果
|
||||||
|
|
||||||
|
### 4. 前端实现
|
||||||
|
|
||||||
|
**修改文件**:
|
||||||
|
- `frontend/utils/activity-config.js` - 添加动态阶段配置
|
||||||
|
- `frontend/pages/support-activity/index.vue` - 接入后端 API
|
||||||
|
- `frontend/pages/support-activity/components/ActionBar.vue` - 购买道具交互
|
||||||
|
- `frontend/utils/api.js` - 添加活动 API
|
||||||
|
|
||||||
|
**阶段展示逻辑**:
|
||||||
|
```
|
||||||
|
- 0-25%: 初期 (early) - 初始背景
|
||||||
|
- 26-50%: 中期 (mid) - 中期背景
|
||||||
|
- 51-75%: 后期 (late) - 后期背景
|
||||||
|
- 76-100%: 完成 (completed) - 完成背景 + 特效动画
|
||||||
|
```
|
||||||
|
|
||||||
|
**时间限制显示**:
|
||||||
|
- 活动未开始: 显示"距开始还有 X 天"
|
||||||
|
- 活动进行中: 显示"还剩 X 天 X 小时"
|
||||||
|
- 活动已结束: 显示"活动已结束"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键文件路径
|
||||||
|
|
||||||
|
| 组件 | 文件路径 |
|
||||||
|
|------|----------|
|
||||||
|
| 数据库初始化 | `backend/scripts/init_database.sql` |
|
||||||
|
| Proto 定义 | `backend/proto/activity.proto` (新建) |
|
||||||
|
| 活动服务 | `backend/services/activityService/` (新建) |
|
||||||
|
| Gateway 路由 | `backend/gateway/controller/` (新增) |
|
||||||
|
| 前端活动配置 | `frontend/utils/activity-config.js` |
|
||||||
|
| 前端活动页面 | `frontend/pages/support-activity/index.vue` |
|
||||||
|
| 前端 API | `frontend/utils/api.js` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证方案
|
||||||
|
|
||||||
|
1. **数据库**: 确认表创建成功,索引正确
|
||||||
|
2. **后端 API**:
|
||||||
|
- 创建活动,验证返回
|
||||||
|
- 购买道具,验证水晶扣减和贡献点增加
|
||||||
|
- 获取排名,验证排序正确
|
||||||
|
3. **前端**:
|
||||||
|
- 活动页面加载正常
|
||||||
|
- 购买道具交互正常
|
||||||
|
- 不同阶段背景切换正常
|
||||||
|
- 倒计时显示正常
|
||||||
|
4. **整体流程**: 完整流程测试 - 购买道具 -> 贡献点增加 -> 排名更新
|
||||||
206
docs/PRD-my-profile.md
Normal file
206
docs/PRD-my-profile.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# 个人主页(My Profile)PRD
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
### 1.1 产品介绍
|
||||||
|
个人主页是一个整合展示用户个人资料和藏品的页面,功能类似于微信个人主页。用户可以查看自己的个人主页,也可以通过链接查看他人的个人主页。
|
||||||
|
|
||||||
|
### 1.2 需求来源
|
||||||
|
用户需要一个综合展示页面,整合展示头像/昵称/等级/展品/点赞数等信息。
|
||||||
|
|
||||||
|
### 1.3 范围
|
||||||
|
- 新建独立页面 `my-profile.vue`
|
||||||
|
- 支持查看自己和他人的主页
|
||||||
|
- 复用现有组件和 API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 页面结构
|
||||||
|
|
||||||
|
### 2.1 整体布局
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 顶部背景 │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ 用户信息卡片(固定) │
|
||||||
|
│ ┌─────┐ │
|
||||||
|
│ │头像 │ 昵称 | 等级徽章 │
|
||||||
|
│ │ │ UID | 粉丝标签 │
|
||||||
|
│ └─────┘ 全部点赞数: xxx │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ 藏品展品区(可滚动) │
|
||||||
|
│ ┌────┬────┬────┐ │
|
||||||
|
│ │NFT1│NFT2│NFT3│ ...(点击展开)│
|
||||||
|
│ └────┴────┴────┘ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 模块说明
|
||||||
|
|
||||||
|
| 模块 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 顶部背景 | 渐变背景图,与整体风格一致 |
|
||||||
|
| 用户信息卡片 | 头像、昵称、等级、粉丝标签(固定不滚动) |
|
||||||
|
| 藏品展品区 | NFT 藏品网格,可滚动展示 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 功能详细说明
|
||||||
|
|
||||||
|
### 3.1 用户信息卡片
|
||||||
|
|
||||||
|
**展示字段**:
|
||||||
|
|
||||||
|
| 字段 | 来源 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 头像 | `avatar_url` | 用户头像,有等级边框 |
|
||||||
|
| 昵称 | `nickname` | 用户昵称 |
|
||||||
|
| UID | `uid` | 用户 ID |
|
||||||
|
| 等级 | `current_identity.level` | 粉丝等级 |
|
||||||
|
| 粉丝标签 | `current_identity.tag` | 如"小飞侠" |
|
||||||
|
| 全部点赞数 | `items[].like_count` 求和 | 藏品总点赞数 |
|
||||||
|
|
||||||
|
**交互说明**:
|
||||||
|
- 仅展示,不可点击修改
|
||||||
|
- 头像使用 Avatar 组件,带等级徽章
|
||||||
|
|
||||||
|
### 3.2 藏品展品区
|
||||||
|
|
||||||
|
**展示字段**:
|
||||||
|
|
||||||
|
| 字段 | 来源 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 藏品封面 | `cover_url` | NFT 藏品图片 |
|
||||||
|
| 点赞数 | `like_count` | 藏品获得的点赞数 |
|
||||||
|
|
||||||
|
**布局说明**:
|
||||||
|
- 3 列网格布局
|
||||||
|
- 显示数量根据藏品等级区分(后期实现)
|
||||||
|
- 显示点赞数标签
|
||||||
|
|
||||||
|
**交互说明**:
|
||||||
|
- 点击"..." → 展开显示余下藏品
|
||||||
|
- 点击藏品卡片 → 跳转 `/pages/asset-detail/asset-detail?asset_id=${nft.asset_id}`
|
||||||
|
|
||||||
|
### 3.3 页面参数
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `target_uid` | 可选,指定查看的用户 ID |
|
||||||
|
|
||||||
|
**逻辑说明**:
|
||||||
|
- 无 `target_uid` → 查看自己的主页
|
||||||
|
- 有 `target_uid` → 查看指定用户的主页
|
||||||
|
|
||||||
|
**页面标题**:
|
||||||
|
- 查看自己 → "我的主页"
|
||||||
|
- 查看他人 → "{用户昵称}的主页"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API 调用
|
||||||
|
|
||||||
|
### 4.1 查看自己
|
||||||
|
|
||||||
|
| API | 用途 |
|
||||||
|
|-----|------|
|
||||||
|
| `getUserProfileApi()` | 获取当前用户资料 |
|
||||||
|
| `getMyAssetsApi(page, size)` | 获取我的藏品列表 |
|
||||||
|
|
||||||
|
### 4.2 查看他人
|
||||||
|
|
||||||
|
| API | 用途 |
|
||||||
|
|-----|------|
|
||||||
|
| `getUserInfoApi(userId)` | 获取指定用户信息(需新增) |
|
||||||
|
| `getUserGalleriesApi(targetUid)` | 获取他人全部藏品 |
|
||||||
|
|
||||||
|
### 4.3 API 位置
|
||||||
|
- 前端 API 文件:`frontend/utils/api.js`
|
||||||
|
- 后端已有路由:`GET /api/v1/users/:user_id`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据字段映射
|
||||||
|
|
||||||
|
| 页面展示 | API 字段 |
|
||||||
|
|---------|---------|
|
||||||
|
| 头像 | `avatar_url` 或 `current_identity.avatar_url` |
|
||||||
|
| 昵称 | `nickname` |
|
||||||
|
| 等级 | `current_identity.level` |
|
||||||
|
| 粉丝标签 | `current_identity.tag` |
|
||||||
|
| 藏品封面 | `items[].cover_url` |
|
||||||
|
| 单个点赞数 | `items[].like_count` |
|
||||||
|
| 全部点赞数 | `items[].like_count` 求和 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 技术方案
|
||||||
|
|
||||||
|
### 6.1 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/pages/my-profile/
|
||||||
|
└── my-profile.vue # 个人主页主文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 路由注册
|
||||||
|
|
||||||
|
在 `pages.json` 中注册路由:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "pages/my-profile/my-profile"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 组件复用
|
||||||
|
|
||||||
|
| 组件 | 路径 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| Avatar | `pages/components/Avatar.vue` | 头像展示 |
|
||||||
|
|
||||||
|
**藏品展品区说明**:
|
||||||
|
- 藏品展品区用于展示用户已有的藏品列表
|
||||||
|
- 需新建独立组件或直接在页面中实现网格布局
|
||||||
|
- 不复用 `NftCard` 组件(该组件用于藏品市场/列表页)
|
||||||
|
|
||||||
|
### 6.4 页面入口
|
||||||
|
|
||||||
|
- `profile.vue` 的"我的资产"区域可添加入口
|
||||||
|
- 可通过 `uni.navigateTo` 跳转
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 实现步骤
|
||||||
|
|
||||||
|
1. 在 `frontend/pages/my-profile/` 下创建 `my-profile.vue`
|
||||||
|
2. 在 `pages.json` 注册路由
|
||||||
|
3. 在 `frontend/utils/api.js` 新增 `getUserInfoApi(userId)`
|
||||||
|
4. 实现用户信息卡片模块
|
||||||
|
5. 实现藏品展品区模块
|
||||||
|
6. 添加页面入口(可选)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 验收标准
|
||||||
|
|
||||||
|
- [ ] 页面可正常访问
|
||||||
|
- [ ] 用户头像、昵称、等级正确显示
|
||||||
|
- [ ] 粉丝标签正确显示
|
||||||
|
- [ ] 藏品网格正确显示
|
||||||
|
- [ ] 点赞数正确显示
|
||||||
|
- [ ] 支持 `target_uid` 查看他人主页
|
||||||
|
- [ ] 页面标题正确区分"我的主页"和"TA的主页"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 备注
|
||||||
|
|
||||||
|
### 9.1 后端数据限制
|
||||||
|
- 后端无浏览数/访问量字段
|
||||||
|
- 藏品展示使用点赞数替代
|
||||||
|
|
||||||
|
### 9.2 参考页面
|
||||||
|
- `profile.vue` - 用户信息展示参考
|
||||||
|
- `exhibition.vue` - 藏品网格布局参考
|
||||||
573
docs/PRD.md
Normal file
573
docs/PRD.md
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
# TopFans 产品需求文档 (PRD)
|
||||||
|
|
||||||
|
## 文档信息
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 项目名称 | TopFans |
|
||||||
|
| 版本 | V1.0 |
|
||||||
|
| 文档类型 | 产品需求文档 |
|
||||||
|
| 生成日期 | 2026-03-11 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 产品定位
|
||||||
|
|
||||||
|
TopFans 是一款基于 NFT 数字藏品技术的粉丝社交应用,连接明星与粉丝,提供藏品铸造、展馆展示、社交互动等核心功能。
|
||||||
|
|
||||||
|
### 1.2 目标用户
|
||||||
|
|
||||||
|
- 明星粉丝群体
|
||||||
|
- 数字藏品爱好者
|
||||||
|
- 社交娱乐用户
|
||||||
|
|
||||||
|
### 1.3 技术架构
|
||||||
|
|
||||||
|
#### 前端技术栈
|
||||||
|
| 技术 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| uni-app | 跨平台应用框架 |
|
||||||
|
| Vue 3 | 前端框架 |
|
||||||
|
| Vuex 4 | 状态管理 |
|
||||||
|
| HBuilderX | 构建工具 |
|
||||||
|
|
||||||
|
#### 后端技术栈
|
||||||
|
| 技术 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Go 1.25.5 | 开发语言 |
|
||||||
|
| Gin | HTTP 网关 |
|
||||||
|
| Dubbo-go | 微服务框架 (Triple协议) |
|
||||||
|
| PostgreSQL | 数据库 |
|
||||||
|
| GORM | ORM框架 |
|
||||||
|
| JWT | 认证 |
|
||||||
|
| 阿里云 OSS | 对象存储 |
|
||||||
|
|
||||||
|
#### 微服务架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mobile App (uni-app) │
|
||||||
|
└─────────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ HTTP
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ API Gateway (Gin :8080) │
|
||||||
|
│ • JWT认证 • 请求路由 • Swagger文档 │
|
||||||
|
└─────────┬──────────────────────┬───────────────────────┬───────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ Dubbo RPC │ Dubbo RPC │ Dubbo RPC
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ User Service │ │ Social Service │ │ Asset Service │
|
||||||
|
│ (:20000) │ │ (:20001) │ │ (:20003) │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • 注册/登录 │ │ • 好友管理 │ │ • 藏品铸造 │
|
||||||
|
│ • 粉丝身份 │ │ • 用户搜索 │ │ • OSS上传 │
|
||||||
|
│ • 密码管理 │ │ • 点赞功能 │ │ • 藏品查询 │
|
||||||
|
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||||
|
│ │ │
|
||||||
|
└─────────────────────┼──────────────────────┘
|
||||||
|
│ Dubbo RPC
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Gallery Service │
|
||||||
|
│ (:20004) │
|
||||||
|
│ │
|
||||||
|
│ • 展馆管理 │
|
||||||
|
│ • 展位管理 │
|
||||||
|
│ • 藏品展示 │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ PostgreSQL │
|
||||||
|
│ + GORM │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 功能模块总览
|
||||||
|
|
||||||
|
| 模块 | 核心功能 | 状态 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 认证系统 | 注册、登录、Token管理 | ✅ 已实现 |
|
||||||
|
| 粉丝身份系统 | 身份选择、多身份切换 | ✅ 已实现 |
|
||||||
|
| 藏品系统 | 铸造、上传、详情、点赞 | ✅ 已实现 |
|
||||||
|
| 展馆系统 | 展位管理、藏品展示 | ✅ 已实现 |
|
||||||
|
| 社交系统 | 好友搜索、申请、删除 | ✅ 已实现 |
|
||||||
|
| 任务系统 | 任务列表、奖励领取 | 🔄 部分实现 |
|
||||||
|
| 新手引导 | 引导流程、状态管理 | 🔄 部分实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 详细功能需求
|
||||||
|
|
||||||
|
### 3.1 认证系统 (Auth)
|
||||||
|
|
||||||
|
#### 3.1.1 用户注册
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/auth/register |
|
||||||
|
| 认证 | 无需认证 |
|
||||||
|
| 请求参数 | uid(手机号), password, fan_identity_id, nickname |
|
||||||
|
| 返回 | access_token, user基础信息, 当前身份信息 |
|
||||||
|
| 说明 | 注册时必须选择一个粉丝身份(nickname唯一性?) |
|
||||||
|
|
||||||
|
#### 3.1.2 用户登录
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/auth/login |
|
||||||
|
| 认证 | 无需认证 |
|
||||||
|
| 请求参数 | uid, password |
|
||||||
|
| 返回 | access_token, user基础信息 |
|
||||||
|
|
||||||
|
#### 3.1.3 获取当前用户
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/auth/me |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | uid, nickname, current_identity |
|
||||||
|
|
||||||
|
#### 3.1.4 修改密码
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/account/password |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | old_password, new_password |
|
||||||
|
|
||||||
|
#### 3.1.5 退出登录
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/auth/logout |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
|
||||||
|
#### 3.1.6 注销账号
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/user/delete-account |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 粉丝身份系统 (Fan Identity)
|
||||||
|
|
||||||
|
#### 3.2.1 获取可选粉丝身份列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/fan-identities |
|
||||||
|
| 认证 | 无需认证 |
|
||||||
|
| 查询参数 | keyword, page, page_size |
|
||||||
|
| 返回 | star_id, identity_id, name, tag |
|
||||||
|
|
||||||
|
#### 3.2.2 新增粉丝身份
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/my/fan-identities |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | identity_id |
|
||||||
|
| 说明 | 最多绑定2个身份 |
|
||||||
|
|
||||||
|
#### 3.2.3 获取我的粉丝身份列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/my/fan-identities |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
|
||||||
|
#### 3.2.4 切换粉丝身份
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/my/fan-identities/switch |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | new_star_id |
|
||||||
|
| 返回 | 新token, current_identity信息 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 个人信息 (Profile)
|
||||||
|
|
||||||
|
#### 3.3.1 获取个人信息
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/me/profile |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | uid, nickname, fan_identity, fan_level, starbook_limit, slot_limit, assets_num, crystal_balance |
|
||||||
|
|
||||||
|
#### 3.3.2 修改昵称
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | PUT /api/v1/me/nickname |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | nickname |
|
||||||
|
|
||||||
|
#### 3.3.3 更新头像
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | PUT /api/v1/me/avatar |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | avatar_url |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 藏品系统 (Asset/Mint)
|
||||||
|
|
||||||
|
#### 3.4.1 获取OSS上传签名
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/assets/oss/signature |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | OSS上传所需的签名信息 |
|
||||||
|
|
||||||
|
#### 3.4.2 获取OSS预签名URL
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/assets/oss/presigned-url |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | object_key |
|
||||||
|
|
||||||
|
#### 3.4.3 创建铸造订单
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/assets/mints |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | name, type, pic_url, material |
|
||||||
|
| 返回 | mint_id, status, asset_id |
|
||||||
|
|
||||||
|
#### 3.4.4 删除铸造订单
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | DELETE /api/v1/assets/mints/{orderId} |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
|
||||||
|
#### 3.4.5 获取我的藏品列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/assets/me/items |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 查询参数 | page, page_size |
|
||||||
|
| 返回 | asset_id, name, cover_url, like_count, mint_status, on_chain |
|
||||||
|
|
||||||
|
#### 3.4.6 获取藏品详情
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/assets/{assetId} |
|
||||||
|
| 认证 | 公开 |
|
||||||
|
| 返回 | asset_id, name, type, cover_url, owner_uid, owner_nickname, like_count, on_chain |
|
||||||
|
|
||||||
|
#### 3.4.7 点赞藏品
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/social/assets/{assetId}/like |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | liked, like_count |
|
||||||
|
|
||||||
|
#### 3.4.8 取消点赞
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | DELETE /api/v1/social/assets/{assetId}/like |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | liked, like_count |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 展馆系统 (Gallery)
|
||||||
|
|
||||||
|
#### 3.5.1 获取我的展馆
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/mygalleries |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | gallery_owner_id, slot_total, slots(含asset信息) |
|
||||||
|
|
||||||
|
#### 3.5.2 获取他人展馆
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/galleries/{targetUid} |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | 同上 |
|
||||||
|
| 说明 | 需校验粉丝身份一致才能访问 |
|
||||||
|
|
||||||
|
#### 3.5.3 展位展示藏品
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/galleries/place |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | asset_id, gallery_owner_id, slot_id |
|
||||||
|
| 返回 | status, occupied_until, occupier_uid |
|
||||||
|
|
||||||
|
#### 3.5.4 下架展位藏品
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | DELETE /api/v1/galleries/slots/{slotId}/asset |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 社交系统 (Social)
|
||||||
|
|
||||||
|
#### 3.6.1 搜索用户
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/social/search-user |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 查询参数 | keyword |
|
||||||
|
| 返回 | uid, nickname, is_friend |
|
||||||
|
|
||||||
|
#### 3.6.2 获取好友列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/social/friends |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 查询参数 | page, page_size |
|
||||||
|
| 返回 | uid, nickname, fan_level |
|
||||||
|
|
||||||
|
#### 3.6.3 发送好友请求
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/social/friend-requests |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | target_uid |
|
||||||
|
|
||||||
|
#### 3.6.4 获取已发送好友请求
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/social/friend-requests |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 查询参数 | type=sent |
|
||||||
|
|
||||||
|
#### 3.6.5 获取收到的好友请求
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/social/friend-requests |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 查询参数 | type=received |
|
||||||
|
|
||||||
|
#### 3.6.6 处理好友请求
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/social/friend-requests/handle |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | request_id, action(accept/reject) |
|
||||||
|
|
||||||
|
#### 3.6.7 删除好友
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | DELETE /api/v1/social/friends |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | friend_uid |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 任务系统 (Task)
|
||||||
|
|
||||||
|
#### 3.7.1 获取任务列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | GET /api/v1/tasks |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 返回 | task_id, title, status, reward |
|
||||||
|
|
||||||
|
#### 3.7.2 领取任务奖励
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 接口 | POST /api/v1/tasks/claim |
|
||||||
|
| 认证 | JWT Token |
|
||||||
|
| 请求参数 | task_id |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据库模型
|
||||||
|
|
||||||
|
### 4.1 核心数据表
|
||||||
|
|
||||||
|
| 表名 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| users | 用户表 |
|
||||||
|
| stars | 明星信息表 |
|
||||||
|
| fan_profiles | 粉丝档案表(用户-明星关联) |
|
||||||
|
| assets | 资产/藏品表 |
|
||||||
|
| mint_orders | 铸造订单表 |
|
||||||
|
| asset_likes | 点赞记录表 |
|
||||||
|
| friendships | 好友关系表 |
|
||||||
|
| friend_requests | 好友请求表 |
|
||||||
|
| booth_slots | 展位表 |
|
||||||
|
| exhibitions | 展品展示表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 用户界面结构
|
||||||
|
|
||||||
|
### 5.1 页面路由
|
||||||
|
|
||||||
|
| 路由 | 页面 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| /pages/login/login | 登录页 | 用户登录 |
|
||||||
|
| /pages/register/register | 注册页 | 用户注册 |
|
||||||
|
| /pages/profile/setNickname | 设置昵称 | 注册时设置昵称 |
|
||||||
|
| /pages/profile/selectRole | 选择角色 | 选择粉丝身份 |
|
||||||
|
| /pages/profile/profile | 个人中心 | 用户资料管理 |
|
||||||
|
| /pages/square/square | 广场首页 | 主页面 |
|
||||||
|
| /pages/exhibition/exhibition | 展馆页 | 展馆/展位管理 |
|
||||||
|
| /pages/castlove/success | 铸造成功 | 铸造成功展示 |
|
||||||
|
|
||||||
|
### 5.2 主要组件
|
||||||
|
|
||||||
|
| 组件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| SquareContent | 广场主内容区(切换广场/好友/星册/铸爱/装扮) |
|
||||||
|
| FriendsContent | 好友列表与社交功能 |
|
||||||
|
| StarbookContent | 我的藏品/星册 |
|
||||||
|
| CastloveContent | 铸造功能 |
|
||||||
|
| DressupContent | 装扮功能 |
|
||||||
|
| NftCard | 藏品卡片 |
|
||||||
|
| NftDetailModal | 藏品详情弹窗 |
|
||||||
|
| Avatar | 头像组件 |
|
||||||
|
| Header | 顶部导航栏 |
|
||||||
|
| BottomNav | 底部导航栏 |
|
||||||
|
| TaskModal | 任务弹窗 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 业务规则
|
||||||
|
|
||||||
|
### 6.1 粉丝身份隔离
|
||||||
|
|
||||||
|
- 每个用户可绑定最多2个粉丝身份
|
||||||
|
- 切换身份后,藏品、展馆、好友等数据按身份隔离
|
||||||
|
- Token中包含user_id和star_id
|
||||||
|
|
||||||
|
### 6.2 展馆规则
|
||||||
|
|
||||||
|
- 初始展位: 3个
|
||||||
|
- 可解锁/购买更多展位
|
||||||
|
- 可展示藏品到他人展馆空位
|
||||||
|
- 抢展位: 4小时后自动下架
|
||||||
|
|
||||||
|
### 6.3 好友规则
|
||||||
|
|
||||||
|
- 需双向确认才能成为好友
|
||||||
|
- 默认上限: 50个
|
||||||
|
- 可随等级解锁更多
|
||||||
|
|
||||||
|
### 6.4 铸造规则
|
||||||
|
|
||||||
|
- 支持平台素材或用户上传
|
||||||
|
- 需审核和AI生成封面
|
||||||
|
- 铸造后生成链上确权
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 安全要求
|
||||||
|
|
||||||
|
### 7.1 认证
|
||||||
|
|
||||||
|
- JWT Token认证
|
||||||
|
- Token有效期: 7天
|
||||||
|
- 从Header提取: Authorization: Bearer \<token\>
|
||||||
|
|
||||||
|
### 7.2 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 分页格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"items": [],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 外部依赖
|
||||||
|
|
||||||
|
| 服务 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 阿里云OSS | 藏品图片存储 |
|
||||||
|
| PostgreSQL | 数据持久化 |
|
||||||
|
| (区块链) | 数字藏品上链(待实现) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 待实现功能
|
||||||
|
|
||||||
|
根据代码分析,以下功能标记为待实现或部分实现:
|
||||||
|
|
||||||
|
1. **广场推荐** - 广场小屋列表、TOP藏品展板
|
||||||
|
2. **任务系统** - 完整任务列表和奖励领取
|
||||||
|
3. **新手引导** - 引导流程和状态管理
|
||||||
|
4. **踢走占位** - 被占位者踢走占领者
|
||||||
|
5. **解锁展位** - 等级解锁或货币购买
|
||||||
|
6. **系统推荐好友** - 推荐好友列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 附录
|
||||||
|
|
||||||
|
### 10.1 错误码定义
|
||||||
|
|
||||||
|
| 错误码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 401 | Token失效/未认证 |
|
||||||
|
| 400 | 请求参数错误 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
### 10.2 版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| V1.0 | 2026-03-11 | 初始版本(基于代码分析) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档基于代码分析自动生成,如需更新请参考实际实现。*
|
||||||
118
docs/Tasks-Rankings.md
Normal file
118
docs/Tasks-Rankings.md
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# 排行榜功能后端开发任务
|
||||||
|
|
||||||
|
## 任务概览
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 功能 | 热度排行榜、自制排行榜 |
|
||||||
|
| 状态 | 已完成 |
|
||||||
|
| 开始日期 | 2026-03-12 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务清单
|
||||||
|
|
||||||
|
### Phase 1: 数据模型准备
|
||||||
|
|
||||||
|
| 序号 | 任务 | 服务 | 状态 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 1.1 | 在 Asset 表新增 `is_original` 字段 | AssetService | ✅ 已完成 |
|
||||||
|
| 1.2 | 编写数据库迁移脚本 | AssetService | ✅ 已完成 |
|
||||||
|
|
||||||
|
### Phase 2: Proto 定义
|
||||||
|
|
||||||
|
| 序号 | 任务 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 2.1 | 新增 ranking.proto 定义排行榜接口 | ✅ 已完成 |
|
||||||
|
| 2.2 | 编译 proto 生成 Go 代码 | ✅ 已完成 |
|
||||||
|
|
||||||
|
### Phase 3: 排行榜逻辑实现
|
||||||
|
|
||||||
|
| 序号 | 任务 | 服务 | 状态 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 3.1 | 实现热度排行榜查询逻辑 (displaying) | AssetService | ✅ 已完成 |
|
||||||
|
| 3.2 | 实现热度排行榜查询逻辑 (month) | AssetService | ✅ 已完成 |
|
||||||
|
| 3.3 | 实现热度排行榜查询逻辑 (total) | AssetService | ✅ 已完成 |
|
||||||
|
| 3.4 | 实现自制排行榜查询逻辑 | AssetService | ✅ 已完成 |
|
||||||
|
| 3.5 | 实现"我的排名"查询逻辑 | AssetService | ✅ 已完成 |
|
||||||
|
|
||||||
|
### Phase 4: Gateway 接口
|
||||||
|
|
||||||
|
| 序号 | 任务 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 4.1 | 新增排行榜路由配置 | ✅ 已完成 |
|
||||||
|
| 4.2 | 实现排行榜 Controller | ✅ 已完成 |
|
||||||
|
| 4.3 | 添加 Swagger 文档 | ✅ 已完成 |
|
||||||
|
|
||||||
|
### Phase 5: 测试与优化
|
||||||
|
|
||||||
|
| 序号 | 任务 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 5.1 | 单元测试 | ✅ 已完成 |
|
||||||
|
| 5.2 | API 联调测试 | ✅ 已完成 |
|
||||||
|
| 5.3 | 性能优化 (Redis 缓存) | ⬜ 待开始 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 新增文件清单
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `backend/proto/ranking.proto` | Proto 定义 |
|
||||||
|
| `backend/scripts/add_is_original_column.sql` | 数据库迁移脚本 |
|
||||||
|
| `backend/services/assetService/repository/ranking_repository.go` | 排行榜 Repository |
|
||||||
|
| `backend/services/assetService/repository/ranking_repository_test.go` | 排行榜单元测试 |
|
||||||
|
| `backend/services/assetService/service/ranking_service.go` | 排行榜 Service |
|
||||||
|
| `backend/services/assetService/provider/ranking_provider.go` | 排行榜 Provider |
|
||||||
|
| `backend/services/assetService/main.go` | 更新:注册 Ranking Service |
|
||||||
|
| `backend/gateway/controller/ranking_controller.go` | 排行榜 Controller |
|
||||||
|
| `backend/gateway/router/router.go` | 更新:添加排行榜路由 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/v1/rankings/hot` | 获取热度排行榜 |
|
||||||
|
| GET | `/api/v1/rankings/original` | 获取自制排行榜 |
|
||||||
|
|
||||||
|
### API 参数说明
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| dimension | string | 否 | 统计维度: displaying(展示中), month(本月), total(全部),默认 total |
|
||||||
|
| star_id | int64 | 否 | 粉丝身份ID,不传则使用当前身份 |
|
||||||
|
| page | int | 否 | 页码,默认 1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认 10 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 进度记录
|
||||||
|
|
||||||
|
| 日期 | 完成任务 | 备注 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 2026-03-12 | Phase 1 数据模型 | is_original 字段 + 迁移脚本 |
|
||||||
|
| 2026-03-12 | Phase 2 Proto | ranking.proto + 编译 |
|
||||||
|
| 2026-03-12 | Phase 3 逻辑 | Repository + Service + Provider |
|
||||||
|
| 2026-03-12 | Phase 4 Gateway | Controller + 路由配置 |
|
||||||
|
| 2026-03-12 | Phase 5.1 单元测试 | ranking_repository_test.go |
|
||||||
|
| 2026-03-12 | Phase 5.2 API 联调 | 全部 API 测试通过 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术依赖
|
||||||
|
|
||||||
|
- AssetService (20003) - 藏品和点赞数据
|
||||||
|
- Exhibition 表 - 展示中藏品判定
|
||||||
|
- AssetLike 表 - 本月点赞统计
|
||||||
|
- Redis - 排行榜缓存(待实现)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 排行榜按粉丝身份 (star_id) 隔离
|
||||||
|
2. "我的排名"返回用户点赞最高的藏品
|
||||||
|
3. 取消点赞不实时扣除点赞数
|
||||||
|
4. 分页默认 10 条,Top N 默认 100
|
||||||
252
docs/backend-architecture.drawio
Normal file
252
docs/backend-architecture.drawio
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<mxGraphModel dx="1422" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="1400" pageHeight="1000" math="0" shadow="0" defaultFontFamily="Noto Sans JP" defaultFontSize="14">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<mxCell id="title" value="TopFans Backend Microservice Architecture" style="text;html=1;fontSize=24;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="400" width="600" y="20" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== Gateway Layer ==================== -->
|
||||||
|
<mxCell id="gateway_box" value="" style="rounded=1;strokeWidth=3;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="80" width="280" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gateway_label" value="API Gateway Layer" style="text;html=1;fontSize=18;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="90" width="160" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gateway" value="Gin HTTP Server" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="130" width="140" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gateway_port" value="Port: 8080" style="text;html=1;fontSize=14;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="190" width="140" height="25" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gateway_func" value="• HTTP Request/Response
• JWT Authentication
• Request Routing
• Swagger Docs" style="text;html=1;align=left;verticalAlign=top;spacingLeft=10;fontSize=12;fontFamily=Noto Sans JP;whiteSpace=wrap;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="210" y="130" width="100" height="130" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== Services Layer ==================== -->
|
||||||
|
<mxCell id="services_box" value="" style="rounded=1;strokeWidth=3;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="380" y="80" width="920" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="services_label" value="Microservices Layer (Dubbo-go)" style="text;html=1;fontSize=18;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="90" width="280" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- User Service -->
|
||||||
|
<mxCell id="user_service" value="User Service" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="400" y="130" width="130" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="user_port" value=":20000" style="text;html=1;fontSize=12;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="400" y="195" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="user_func" value="• Register/Login
• User Info
• Fan Identity
• Password" style="text;html=1;align=left;verticalAlign=top;spacingLeft=5;fontSize=11;fontFamily=Noto Sans JP;whiteSpace=wrap;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="540" y="135" width="90" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Gallery Service -->
|
||||||
|
<mxCell id="gallery_service" value="Gallery Service" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="130" width="130" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gallery_port" value=":20001" style="text;html=1;fontSize=12;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="195" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gallery_func" value="• My Gallery
• Booth Slots
• Exhibitions
• Visit Friend" style="text;html=1;align=left;verticalAlign=top;spacingLeft=5;fontSize=11;fontFamily=Noto Sans JP;whiteSpace=wrap;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="790" y="135" width="100" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Social Service -->
|
||||||
|
<mxCell id="social_service" value="Social Service" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="910" y="130" width="130" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="social_port" value=":20002" style="text;html=1;fontSize=12;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="910" y="195" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="social_func" value="• Friends
• Friend Request
• User Search
• Likes" style="text;html=1;align=left;verticalAlign=top;spacingLeft=5;fontSize=11;fontFamily=Noto Sans JP;whiteSpace=wrap;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1050" y="135" width="90" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Asset Service -->
|
||||||
|
<mxCell id="asset_service" value="Asset Service" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1160" y="130" width="130" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="asset_port" value=":20003" style="text;html=1;fontSize=12;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1160" y="195" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="asset_func" value="• Minting
• Assets
• OSS Upload
• Likes" style="text;html=1;align=left;verticalAlign=top;spacingLeft=5;fontSize=11;fontFamily=Noto Sans JP;whiteSpace=wrap;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1020" y="215" width="90" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== Common Layer ==================== -->
|
||||||
|
<mxCell id="common_box" value="" style="rounded=1;strokeWidth=3;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="380" y="320" width="920" height="140" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="common_label" value="Common Layer" style="text;html=1;fontSize=18;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="620" y="330" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Database -->
|
||||||
|
<mxCell id="database" value="PostgreSQL" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=10;fillColor=#fff2cc;strokeColor=#d6b656;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="360" width="100" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- JWT -->
|
||||||
|
<mxCell id="jwt" value="JWT Auth" style="shape=process;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="560" y="375" width="90" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Logger -->
|
||||||
|
<mxCell id="logger" value="Uber Zap
Logger" style="shape=process;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="690" y="375" width="90" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- GORM -->
|
||||||
|
<mxCell id="gorm" value="GORM
ORM" style="shape=process;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="820" y="375" width="90" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Dubbo -->
|
||||||
|
<mxCell id="dubbo" value="Dubbo Triple
Protocol" style="shape=process;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="950" y="375" width="100" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Swagger -->
|
||||||
|
<mxCell id="swagger" value="Swagger
API Docs" style="shape=process;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1090" y="375" width="90" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== Data Models ==================== -->
|
||||||
|
<mxCell id="models_box" value="" style="rounded=1;strokeWidth=3;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="320" width="280" height="140" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="models_label" value="Data Models" style="text;html=1;fontSize=18;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="110" y="330" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="model_user" value="User" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontFamily=Noto Sans JP;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="370" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="model_fan" value="FanProfile" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontFamily=Noto Sans JP;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="150" y="370" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="model_star" value="Star" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontFamily=Noto Sans JP;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="240" y="370" width="60" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="model_booth" value="BoothSlot" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontFamily=Noto Sans JP;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="410" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="model_exhibition" value="Exhibition" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontFamily=Noto Sans JP;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="150" y="410" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="model_friend" value="Friendship" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Noto Sans JP;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="240" y="410" width="60" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== External Clients ==================== -->
|
||||||
|
<mxCell id="client_box" value="" style="rounded=1;strokeWidth=3;fillColor=#e6e6e6;strokeColor=#666666;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="500" width="280" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client_label" value="External Clients" style="text;html=1;fontSize=18;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="510" width="160" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="mobile_client" value="Mobile App" style="shape=mobile;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="550" width="80" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="web_client" value="Web App" style="shape=rx;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="160" y="550" width="80" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== OSS ==================== -->
|
||||||
|
<mxCell id="oss_box" value="" style="rounded=1;strokeWidth=3;fillColor=#e6e6e6;strokeColor=#666666;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="380" y="500" width="200" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="oss_label" value="External Services" style="text;html=1;fontSize=18;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="510" width="160" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="oss" value="OSS (Aliyun)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=10;fillColor=#dae8fc;strokeColor=#6c8ebf;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="545" width="100" height="45" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ==================== Arrows ==================== -->
|
||||||
|
<!-- Client to Gateway -->
|
||||||
|
<mxCell id="arrow1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#333333;" edge="1" parent="1" source="mobile_client" target="gateway">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow1_label" value="HTTP" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="170" y="340" width="40" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Gateway to Services -->
|
||||||
|
<mxCell id="arrow2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#333333;dashed=1;" edge="1" parent="1" source="gateway" target="user_service">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow2_label" value="Dubbo RPC" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="270" y="115" width="80" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Services to Common Layer -->
|
||||||
|
<mxCell id="arrow3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#333333;" edge="1" parent="1" source="user_service" target="database">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow3_label" value="SQL" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="350" y="300" width="30" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Service to OSS -->
|
||||||
|
<mxCell id="arrow4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#333333;dashed=1;" edge="1" parent="1" source="asset_service" target="oss">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow4_label" value="Upload" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="580" y="520" width="50" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Service Communication Arrows -->
|
||||||
|
<mxCell id="arrow5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#9673a6;" edge="1" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="530" y="160" as="sourcePoint"/>
|
||||||
|
<mxPoint x="650" y="160" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow5_label" value="RPC" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=10;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="575" y="140" width="30" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="arrow6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#9673a6;" edge="1" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="780" y="160" as="sourcePoint"/>
|
||||||
|
<mxPoint x="910" y="160" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow6_label" value="RPC" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=10;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="830" y="140" width="30" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="arrow7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#9673a6;" edge="1" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="1040" y="160" as="sourcePoint"/>
|
||||||
|
<mxPoint x="1160" y="160" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow7_label" value="RPC" style="text;html=1;align=center;verticalAlign=bottom;spacingLeft=-10;fontSize=10;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1090" y="140" width="30" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<mxCell id="legend_box" value="" style="rounded=1;strokeWidth=1;fillColor=#ffffff;strokeColor=#999999;dashed=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="500" width="180" height="90" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend_title" value="Legend" style="text;html=1;fontSize=14;fontFamily=Noto Sans JP;fontStyle=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="710" y="510" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend1" value="── HTTP/RPC" style="text;html=1;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="660" y="535" width="80" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend2" value="-- - External" style="text;html=1;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="660" y="555" width="80" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend3" value="─── Inter-service" style="text;html=1;fontSize=11;fontFamily=Noto Sans JP;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="740" y="535" width="80" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
298
docs/superpowers/plans/2026-03-25-guide-modification.md
Normal file
298
docs/superpowers/plans/2026-03-25-guide-modification.md
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
# 新手引导修改实现计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 修改新手引导流程:点击"去做"后直接从第一项引导开始,存储步骤进度,高亮区域支持点击触发动作
|
||||||
|
|
||||||
|
**Architecture:** 修改 GuideStartModal 的 start 事件触发逻辑,添加步骤进度存储,支持高亮区域点击事件
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 / Vuex / uni-app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 修改 GuideStartModal 点击"去做"后直接开始引导
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/pages/square/square.vue:751-754`
|
||||||
|
- Modify: `frontend/components/GuideStartModal.vue:39-45`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 square.vue 的 handleGuideStart 方法**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 原本代码
|
||||||
|
const handleGuideStart = () => {
|
||||||
|
showGuideStartModal.value = false;
|
||||||
|
showGuideListModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改为:直接开始第一个未完成的引导
|
||||||
|
const handleGuideStart = () => {
|
||||||
|
showGuideStartModal.value = false;
|
||||||
|
|
||||||
|
// 获取所有引导keys,按顺序获取第一个未完成的
|
||||||
|
const keys = getAllGuideKeys();
|
||||||
|
const firstUndoneKey = keys.find(key => !isGuideDone(key));
|
||||||
|
|
||||||
|
if (firstUndoneKey) {
|
||||||
|
store.dispatch('guide/initGuide', firstUndoneKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
需要 import:
|
||||||
|
```javascript
|
||||||
|
import { getAllGuideKeys, isGuideDone } from '@/utils/guideConfig.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证修改**
|
||||||
|
|
||||||
|
运行项目,点击"去做"按钮,验证是否直接进入第一个引导而不是显示列表弹窗。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 提交**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 添加引导步骤进度存储功能
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/utils/guideConfig.js:252-315`
|
||||||
|
- Modify: `frontend/store/modules/guide.js:60-74`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 guideConfig.js 添加步骤进度存储函数**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 获取引导的当前步骤
|
||||||
|
* @param {string} key 引导key
|
||||||
|
* @returns {number} 当前步骤索引,默认0
|
||||||
|
*/
|
||||||
|
export function getGuideCurrentStep(key) {
|
||||||
|
return uni.getStorageSync(`guide_step_${key}`) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置引导的当前步骤
|
||||||
|
* @param {string} key 引导key
|
||||||
|
* @param {number} step 步骤索引
|
||||||
|
*/
|
||||||
|
export function setGuideCurrentStep(key, step) {
|
||||||
|
uni.setStorageSync(`guide_step_${key}`, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一个未完成的引导key
|
||||||
|
* @returns {string|null} 引导key或null
|
||||||
|
*/
|
||||||
|
export function getNextUndoneGuideKey() {
|
||||||
|
const keys = getAllGuideKeys()
|
||||||
|
return keys.find(key => !isGuideDone(key)) || null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 store/modules/guide.js 的 END_GUIDE mutation**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 mutations 中修改
|
||||||
|
END_GUIDE(state) {
|
||||||
|
if (state.currentGuide) {
|
||||||
|
const key = state.currentGuide.key
|
||||||
|
markGuideAsShown(key)
|
||||||
|
markGuideDone(key)
|
||||||
|
|
||||||
|
// 重置步骤进度
|
||||||
|
setGuideCurrentStep(key, 0)
|
||||||
|
}
|
||||||
|
state.currentGuide = null
|
||||||
|
state.currentStep = 0
|
||||||
|
state.isActive = false
|
||||||
|
state.isNavigating = false
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改 NEXT_STEP mutation 添加步骤存储**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
NEXT_STEP(state) {
|
||||||
|
if (state.currentGuide && state.currentStep < state.currentGuide.steps.length - 1) {
|
||||||
|
state.currentStep++
|
||||||
|
// 存储当前步骤
|
||||||
|
setGuideCurrentStep(state.currentGuide.key, state.currentStep)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 修改 START_GUIDE mutation 恢复步骤进度**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
START_GUIDE(state, guideConfig) {
|
||||||
|
state.currentGuide = guideConfig
|
||||||
|
// 恢复之前保存的步骤进度
|
||||||
|
state.currentStep = getGuideCurrentStep(guideConfig.key) || 0
|
||||||
|
state.isActive = true
|
||||||
|
state.isNavigating = false
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 高亮区域支持点击触发动作
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/utils/guideConfig.js:37-157` (steps 配置)
|
||||||
|
- Modify: `frontend/components/GuideOverlay.vue:245-248`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展 step 配置结构**
|
||||||
|
|
||||||
|
在 guideConfig.js 的 steps 配置中添加 action 字段:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// action 支持的类型:
|
||||||
|
// 1. { type: 'navigate', url: '/pages/xxx/xxx' } - 路由跳转
|
||||||
|
// 2. { type: 'component', name: 'xxx' } - 打开组件
|
||||||
|
// 3. { type: 'function', handler: 'xxx' } - 执行函数
|
||||||
|
|
||||||
|
// 示例配置:
|
||||||
|
{
|
||||||
|
target: '.banner-carousel',
|
||||||
|
content: '这里是轮播图,点击可以查看排行榜和活动',
|
||||||
|
center: true,
|
||||||
|
buttons: ['next'],
|
||||||
|
action: {
|
||||||
|
type: 'navigate',
|
||||||
|
url: '/pages/rank/rank'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 GuideOverlay 的 handleHighlightClick 方法**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 点击高亮区域
|
||||||
|
function handleHighlightClick() {
|
||||||
|
const stepConfigVal = stepConfig.value
|
||||||
|
if (stepConfigVal && stepConfigVal.action) {
|
||||||
|
const { action } = stepConfigVal
|
||||||
|
|
||||||
|
if (action.type === 'navigate' && action.url) {
|
||||||
|
// 路由跳转
|
||||||
|
uni.navigateTo({
|
||||||
|
url: action.url,
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[Guide] 页面跳转失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (action.type === 'component' && action.name) {
|
||||||
|
// 打开组件(通过事件通知父组件)
|
||||||
|
store.dispatch('guide/openComponent', action.name)
|
||||||
|
} else if (action.type === 'function' && action.handler) {
|
||||||
|
// 执行函数
|
||||||
|
store.dispatch('guide/executeFunction', action.handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 store 中添加处理 action 的 actions**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 store/modules/guide.js 中添加
|
||||||
|
actions: {
|
||||||
|
// ... existing actions ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开组件
|
||||||
|
*/
|
||||||
|
openComponent({ commit }, componentName) {
|
||||||
|
console.log(`[Guide] 打开组件: ${componentName}`)
|
||||||
|
commit('OPEN_COMPONENT', componentName)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行函数
|
||||||
|
*/
|
||||||
|
executeFunction({ commit }, handler) {
|
||||||
|
console.log(`[Guide] 执行函数: ${handler}`)
|
||||||
|
commit('EXECUTE_FUNCTION', handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加对应的 mutations
|
||||||
|
mutations: {
|
||||||
|
// ... existing mutations ...
|
||||||
|
|
||||||
|
OPEN_COMPONENT(state, componentName) {
|
||||||
|
state.pendingAction = { type: 'component', name: componentName }
|
||||||
|
},
|
||||||
|
|
||||||
|
EXECUTE_FUNCTION(state, handler) {
|
||||||
|
state.pendingAction = { type: 'function', handler }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 state 中添加
|
||||||
|
state: {
|
||||||
|
// ... existing state ...
|
||||||
|
pendingAction: null, // 待执行的行动
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 验证个人中心引导列表弹窗不受影响
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Review: `frontend/pages/profile/profile.vue:914-936`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 检查 profile.vue 中 handleDoGuide 方法**
|
||||||
|
|
||||||
|
当前代码:
|
||||||
|
```javascript
|
||||||
|
const handleDoGuide = (key) => {
|
||||||
|
showGuideListModal.value = false;
|
||||||
|
if (isFirstEntry) {
|
||||||
|
store.dispatch("guide/initGuide", key);
|
||||||
|
} else {
|
||||||
|
store.dispatch("guide/initGuide", key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
验证逻辑保持不变:用户从个人中心点击进入某个引导时,触发 initGuide 动作。这与 Task 1 的修改不冲突,因为:
|
||||||
|
- square.vue 的修改只影响新用户首次点击"去做"的行为
|
||||||
|
- profile.vue 中的 GuideListModal 保持原有逻辑
|
||||||
|
|
||||||
|
- [ ] **Step 2: 测试验证**
|
||||||
|
|
||||||
|
1. 从个人中心进入引导列表
|
||||||
|
2. 点击某个引导的"去做"按钮
|
||||||
|
3. 确认引导能正常启动,且列表弹窗行为不变
|
||||||
|
|
||||||
|
- [ ] **Step 3: 提交**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 端到端测试
|
||||||
|
|
||||||
|
- [ ] **Step 1: 测试完整流程**
|
||||||
|
|
||||||
|
1. 模拟新用户登录
|
||||||
|
2. 验证 GuideStartModal 显示
|
||||||
|
3. 点击"去做"
|
||||||
|
4. 验证直接进入第一个引导而不是显示列表
|
||||||
|
5. 完成引导步骤
|
||||||
|
6. 验证步骤进度被正确存储
|
||||||
|
7. 验证高亮区域点击能触发相应动作(配置了 action 的情况下)
|
||||||
|
|
||||||
|
- [ ] **Step 2: 测试个人中心入口**
|
||||||
|
|
||||||
|
1. 进入个人中心
|
||||||
|
2. 点击引导按钮
|
||||||
|
3. 验证 GuideListModal 正常显示
|
||||||
|
4. 选择一个引导执行
|
||||||
|
5. 验证行为与之前一致
|
||||||
|
|
||||||
|
- [ ] **Step 3: 提交**
|
||||||
BIN
docs/界面交互说明.pdf
Normal file
BIN
docs/界面交互说明.pdf
Normal file
Binary file not shown.
BIN
docs/需求规格说明书 (1).pdf
Normal file
BIN
docs/需求规格说明书 (1).pdf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user