From f6e1caad8bddafa5b4c8317d815d9550b7ad7b41 Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Fri, 24 Apr 2026 18:04:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9Edocker=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ranking_repository.go | 59 ++- docker/Dockerfile.services | 38 +- docker/build.sh | 10 +- docker/deploy.sh | 34 +- docker/docker-compose.local.yml | 70 +++ docker/docker-compose.prod.yml | 113 +++- docker/init-db.sql | 104 +++- docker/sql/migrations/V1__init_schema.sql | 491 ++++++++++++++++++ .../V2__add_deleted_at_to_exhibitions.sql | 4 + docker/sql/migrations/V3__seed_data.sql | 17 + docker/sql/migrations/V4__seed_data.sql | 53 ++ frontend/pages.json | 24 +- frontend/pages/components/BannerTop3.vue | 2 +- frontend/pages/components/StarbookContent.vue | 75 ++- .../pages/discover/generation-loading.vue | 134 +---- frontend/pages/discover/generation-result.vue | 230 ++------ frontend/utils/api.js | 4 +- 17 files changed, 1052 insertions(+), 410 deletions(-) create mode 100644 docker/sql/migrations/V1__init_schema.sql create mode 100644 docker/sql/migrations/V2__add_deleted_at_to_exhibitions.sql create mode 100644 docker/sql/migrations/V3__seed_data.sql create mode 100644 docker/sql/migrations/V4__seed_data.sql diff --git a/backend/services/assetService/repository/ranking_repository.go b/backend/services/assetService/repository/ranking_repository.go index 5079b0c..44662ce 100644 --- a/backend/services/assetService/repository/ranking_repository.go +++ b/backend/services/assetService/repository/ranking_repository.go @@ -98,10 +98,11 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str now := time.Now().UnixMilli() startOfMonth := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local).UnixMilli() - // 构建基础查询,JOIN 用户表和粉丝档案表获取昵称和头像(一次查询,避免 N+1 问题) + // 构建基础查询,JOIN 用户表和粉丝档案表获取昵称和头像 + // 使用 GROUP BY 去重,并用聚合函数获取 owner 信息 + // is_original 是布尔值,需要转为 int 再取 MAX db := r.db.Model(&models.Asset{}). - Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, assets.like_count, assets.is_original, fp.nickname as owner_nickname, fp.avatar_url as owner_avatar"). - + Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, MAX(assets.like_count) as like_count, MAX(assets.is_original::int) as is_original, MAX(fp.nickname) as owner_nickname, MAX(fp.avatar_url) as owner_avatar"). Joins("LEFT JOIN fan_profiles fp ON fp.user_id = assets.owner_uid AND fp.star_id = ?", starID). Where("assets.star_id = ? AND assets.is_active = ? AND assets.status = ?", starID, true, models.AssetStatusActive) @@ -112,20 +113,21 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str // 根据维度添加条件 switch dimension { case "displaying": - // 展示中:关联 Exhibition 表,筛选未过期的,且是当前star的展馆 - db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Joins("INNER JOIN fan_profiles ON fan_profiles.id = exhibitions.host_profile_id"). + // 展示中:关联 Exhibition 表,筛选未过期的、未删除的,且是当前star的展品 + // occupier_star_id 表示展品所属的明星 + db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL"). Where("exhibitions.expire_at > ?", now). - Where("fan_profiles.star_id = ?", starID) + Where("exhibitions.occupier_star_id = ?", starID) case "month": - // 本月:本月内展览过的(未下架或本月内下架的) + // 本月:本月内展览过的藏品(包括已下架的,只要 expire_at 在本月内即可) db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Where("exhibitions.expire_at >= ?", startOfMonth) + Where("exhibitions.expire_at >= ?", startOfMonth). + Where("exhibitions.occupier_star_id = ?", starID) case "total": // 全部:直接使用 assets 表的 like_count,无需额外条件 } - // 统计总数 + // 统计总数(先查询 ID 列表再 Count,避免 DISTINCT 干扰) var total int64 countDB := r.db.Model(&models.Asset{}). Select("assets.id"). @@ -136,22 +138,23 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str } switch dimension { case "displaying": - countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Joins("INNER JOIN fan_profiles AS host_fp ON host_fp.id = exhibitions.host_profile_id"). + countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL"). Where("exhibitions.expire_at > ?", now). - Where("host_fp.star_id = ?", starID) + Where("exhibitions.occupier_star_id = ?", starID) case "month": - // 本月:本月内展览过的(未下架或本月内下架的) + // 本月:本月内展览过的藏品(包括已下架的) countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Where("exhibitions.expire_at >= ?", startOfMonth) + Where("exhibitions.expire_at >= ?", startOfMonth). + Where("exhibitions.occupier_star_id = ?", starID) } if err := countDB.Count(&total).Error; err != nil { return nil, 0, err } - // 查询列表 + // 查询列表(使用 GROUP BY 去重,按点赞数排序) var results []*RankingItem - err := db.Order("assets.like_count DESC, assets.id ASC"). + err := db.Group("assets.id"). + Order("MAX(assets.like_count) DESC, assets.id ASC"). Limit(limit). Offset(offset). Scan(&results).Error @@ -187,15 +190,15 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str // 根据维度添加条件,与 getHotRankingByDimension 保持一致 switch dimension { case "displaying": - // 展示中:关联 Exhibition 表,筛选未过期的,且是当前star的展馆 - db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Joins("INNER JOIN fan_profiles AS host_fp ON host_fp.id = exhibitions.host_profile_id"). + // 展示中:关联 Exhibition 表,筛选未过期的、未删除的,且是当前star的展品 + db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL"). Where("exhibitions.expire_at > ?", now). - Where("host_fp.star_id = ?", starID) + Where("exhibitions.occupier_star_id = ?", starID) case "month": - // 本月:本月内展览过的(未下架或本月内下架的) + // 本月:本月内展览过的藏品(包括已下架的) db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Where("exhibitions.expire_at >= ?", startOfMonth) + Where("exhibitions.expire_at >= ?", startOfMonth). + Where("exhibitions.occupier_star_id = ?", starID) } // 获取用户在该star下点赞数最高的藏品 @@ -231,14 +234,14 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str switch dimension { case "displaying": - rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Joins("INNER JOIN fan_profiles AS host_fp ON host_fp.id = exhibitions.host_profile_id"). + rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL"). Where("exhibitions.expire_at > ?", now). - Where("host_fp.star_id = ?", starID) + Where("exhibitions.occupier_star_id = ?", starID) case "month": - // 本月:本月内展览过的(未下架或本月内下架的) + // 本月:本月内展览过的藏品(包括已下架的) rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). - Where("exhibitions.expire_at >= ?", startOfMonth) + Where("exhibitions.expire_at >= ?", startOfMonth). + Where("exhibitions.occupier_star_id = ?", starID) } if err := rankingDB.Count(&rank).Error; err != nil { diff --git a/docker/Dockerfile.services b/docker/Dockerfile.services index 081e85f..b6486d4 100644 --- a/docker/Dockerfile.services +++ b/docker/Dockerfile.services @@ -42,7 +42,13 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ 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" + echo "Built activityservice" && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ + -o /tmp/taskservice services/taskService/main.go && \ + echo "Built taskservice" && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \ + -o /tmp/starbookservice services/starbookService/main.go && \ + echo "Built starbookservice" # ---- Runtime Stage: Gateway ---- FROM --platform=linux/amd64 alpine:3.19 AS gateway @@ -135,3 +141,33 @@ 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"] + +# ---- Runtime Stage: TaskService ---- +FROM --platform=linux/amd64 alpine:3.19 AS taskservice + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app +COPY --from=builder /tmp/taskservice /app/taskservice + +EXPOSE 20006 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:21006 || exit 1 + +ENTRYPOINT ["/app/taskservice"] + +# ---- Runtime Stage: StarbookService ---- +FROM --platform=linux/amd64 alpine:3.19 AS starbookservice + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app +COPY --from=builder /tmp/starbookservice /app/starbookservice + +EXPOSE 20007 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:21007 || exit 1 + +ENTRYPOINT ["/app/starbookservice"] diff --git a/docker/build.sh b/docker/build.sh index fc24e56..77210bc 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -77,7 +77,7 @@ while [[ $# -gt 0 ]]; do echo "" echo "服务名 (可选):" echo " gateway, userService, socialService, assetService," - echo " galleryService, activityService" + echo " galleryService, activityService, taskService, starbookService" echo "" echo "示例:" echo " $0 # 构建所有服务" @@ -96,6 +96,8 @@ while [[ $# -gt 0 ]]; do asset|assetService) SERVICES+=("assetService") ;; gallery|galleryService) SERVICES+=("galleryService") ;; activity|activityService) SERVICES+=("activityService") ;; + task|taskService) SERVICES+=("taskService") ;; + starbook|starbookService) SERVICES+=("starbookService") ;; all) # all 关键字,构建所有服务 SERVICES=() @@ -114,7 +116,7 @@ done # ==================== 服务列表 ==================== # 所有可用服务及其配置(使用小写 target 名) -ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice") +ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice") # 确定要构建的服务 if [ ${#SERVICES[@]} -eq 0 ]; then @@ -201,12 +203,16 @@ main() { assetservice) docker_target="assetservice" ;; galleryservice) docker_target="galleryservice" ;; activityservice) docker_target="activityservice" ;; + taskservice) docker_target="taskservice" ;; + starbookservice) docker_target="starbookservice" ;; # 兼容旧的大写服务名 userService) docker_target="userservice" ;; socialService) docker_target="socialservice" ;; assetService) docker_target="assetservice" ;; galleryService) docker_target="galleryservice" ;; activityService) docker_target="activityservice" ;; + taskService) docker_target="taskservice" ;; + starbookService) docker_target="starbookservice" ;; *) docker_target="$service" ;; esac diff --git a/docker/deploy.sh b/docker/deploy.sh index fe4574d..fc4347b 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -76,6 +76,8 @@ SERVICES=( "assetservice" "galleryservice" "activityservice" + "taskservice" + "starbookservice" ) # ==================== 服务器配置 ==================== @@ -83,19 +85,32 @@ SERVICES=( SERVER_HOST="101.132.250.62" # 服务器 IP 或域名 SERVER_PORT="22" # SSH 端口 SERVER_USER="root" # SSH 用户名 -SERVER_PASSWORD=">n73qBnCja-,#VF+Wq" # 服务器密码 +SERVER_PASSWORD="" # 服务器密码(仅在未配置 SSH 密钥时使用) SERVER_PATH="/opt/topfans/docker" # 服务器上 docker 目录路径 +SSH_KEY_PATH="$HOME/.ssh/id_rsa" # SSH 密钥路径,默认使用 ~/.ssh/id_rsa # ==================== SSH 别名 ==================== -# 使用 sshpass 执行 SSH 命令 +# 优先使用 SSH 密钥,如果失败则使用密码 ssh_cmd() { - sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" + if [ -n "$SERVER_PASSWORD" ]; then + sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" + else + ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" + fi } ssh_cmd_batch() { - sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" + if [ -n "$SERVER_PASSWORD" ]; then + sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" + else + ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" + fi } scp_cmd() { - sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" "$@" + if [ -n "$SERVER_PASSWORD" ]; then + sshpass -p "$SERVER_PASSWORD" scp -o StrictHostKeyChecking=no -P "$SERVER_PORT" "$@" + else + scp -o StrictHostKeyChecking=no -P "$SERVER_PORT" "$@" + fi } # ==================== 打印函数 ==================== @@ -148,7 +163,11 @@ ${YELLOW}示例:${NC} ${YELLOW}前提准备:${NC} 1. 修改 SERVER_HOST 为你的服务器 IP - 2. 配置服务器 SSH 免密登录(建议) + 2. 配置 SSH 密钥登录(推荐): + - 生成密钥:ssh-keygen -t rsa + - 上传公钥:ssh-copy-id -i ~/.ssh/id_rsa.pub root@你的服务器IP + - 设置 SSH_KEY_PATH="~/.ssh/id_rsa" + 3. 或使用密码登录:设置 SERVER_PASSWORD EOF } @@ -341,10 +360,11 @@ ENDSSH # 上传配置文件 print_step "📤 上传配置文件" - print_msg "$YELLOW" "上传 docker-compose.prod.yml 和 .env.prod..." + print_msg "$YELLOW" "上传 docker-compose.prod.yml, .env.prod, init-db.sql..." scp_cmd "${SCRIPT_DIR}/docker-compose.prod.yml" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" scp_cmd "${SCRIPT_DIR}/.env.prod" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" scp_cmd "${SCRIPT_DIR}/init-db.sql" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" + scp_cmd -r "${SCRIPT_DIR}/sql" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" print_msg "$GREEN" "✅ 配置文件上传完成" # 从 tar 文件加载镜像 diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml index 2909834..3eb97ab 100644 --- a/docker/docker-compose.local.yml +++ b/docker/docker-compose.local.yml @@ -192,6 +192,70 @@ services: reservations: memory: 256M + taskservice: + image: topfans/taskservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: taskservice + container_name: topfans-taskservice + restart: unless-stopped + environment: + <<: *common-env + PORT: 20006 + USER_SERVICE_URL: tri://userservice:20000 + depends_on: + userservice: + condition: service_healthy + networks: + - topfans-net + extra_hosts: + - "host.docker.internal:host-gateway" + expose: + - "20006" + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 20006 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + + starbookservice: + image: topfans/starbookservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: starbookservice + container_name: topfans-starbookservice + restart: unless-stopped + environment: + <<: *common-env + PORT: 20007 + 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: + - "20007" + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 20007 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + # ==================== API Gateway ==================== gateway: image: topfans/gateway:latest @@ -210,6 +274,8 @@ services: DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003 DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20004 DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20005 + DUBBO_TASK_SERVICE_URL: tri://taskservice:20006 + DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20007 depends_on: userservice: condition: service_healthy @@ -221,6 +287,10 @@ services: condition: service_healthy activityservice: condition: service_healthy + taskservice: + condition: service_healthy + starbookservice: + condition: service_healthy networks: - topfans-net ports: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index bbe2cdb..0caec09 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -44,7 +44,6 @@ services: <<: *postgres-env volumes: - postgres_data:/var/lib/postgresql - - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro ports: - "5432:5432" networks: @@ -59,6 +58,27 @@ services: reservations: memory: 128M + # ==================== Flyway Migration ==================== + flyway: + image: flyway/flyway:10 + container_name: topfans-flyway + restart: "no" + depends_on: + postgres: + condition: service_healthy + environment: + - FLYWAY_URL=jdbc:postgresql://postgres:5432/topfans + - FLYWAY_USER=postgres + - FLYWAY_PASSWORD=${DB_PASSWORD:-postgres123} + - FLYWAY_SCHEMAS=public + - FLYWAY_PLACEHOLDER_REPLACEMENT=true + volumes: + - ./sql/migrations:/flyway/sql + - flyway_data:/flyway/data + command: migrate -baselineOnMigrate=true + networks: + - topfans-net + # ==================== Dubbo Services ==================== userservice: image: topfans/userservice:latest @@ -77,8 +97,8 @@ services: DB_PASSWORD: ${DB_PASSWORD:-postgres123} DB_NAME: topfans depends_on: - postgres: - condition: service_started + flyway: + condition: service_completed_successfully networks: - topfans-net expose: @@ -253,6 +273,80 @@ services: memory: 32M cpus: '0.25' + taskservice: + image: topfans/taskservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: taskservice + container_name: topfans-taskservice + restart: always + environment: + <<: *common-env + PORT: 20006 + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: postgres + DB_PASSWORD: ${DB_PASSWORD:-postgres123} + DB_NAME: topfans + USER_SERVICE_URL: tri://userservice:20000 + depends_on: + userservice: + condition: service_started + networks: + - topfans-net + expose: + - "20006" + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:21006 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 150M + cpus: '0.5' + reservations: + memory: 64M + cpus: '0.25' + + starbookservice: + image: topfans/starbookservice:latest + build: + context: .. + dockerfile: docker/Dockerfile.services + target: starbookservice + container_name: topfans-starbookservice + restart: always + environment: + <<: *common-env + PORT: 20007 + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: postgres + DB_PASSWORD: ${DB_PASSWORD:-postgres123} + DB_NAME: topfans + ASSET_SERVICE_URL: tri://assetservice:20003 + depends_on: + userservice: + condition: service_started + assetservice: + condition: service_started + networks: + - topfans-net + expose: + - "20007" + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:21007 || exit 1"] + <<: *healthcheck + deploy: + resources: + limits: + memory: 150M + cpus: '0.5' + reservations: + memory: 64M + cpus: '0.25' + # ==================== API Gateway ==================== gateway: image: topfans/gateway:latest @@ -273,6 +367,8 @@ services: DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003 DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20001 DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20004 + DUBBO_TASK_SERVICE_URL: tri://taskservice:20006 + DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20007 depends_on: userservice: condition: service_started @@ -284,6 +380,10 @@ services: condition: service_started activityservice: condition: service_started + taskservice: + condition: service_started + starbookservice: + condition: service_started networks: - topfans-net ports: @@ -300,10 +400,11 @@ services: memory: 64M cpus: '0.25' +volumes: + postgres_data: + flyway_data: + networks: topfans-net: driver: bridge external: true - -volumes: - postgres_data: diff --git a/docker/init-db.sql b/docker/init-db.sql index 5412619..61dee6a 100644 --- a/docker/init-db.sql +++ b/docker/init-db.sql @@ -253,6 +253,54 @@ CREATE SEQUENCE public.asset_likes_id_seq ALTER SEQUENCE public.asset_likes_id_seq OWNED BY public.asset_likes.id; +-- +-- Name: asset_registry; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.asset_registry ( + id bigint NOT NULL, + asset_id bigint NOT NULL, + asset_type character varying(20) NOT NULL, + owner_uid bigint NOT NULL, + star_id bigint NOT NULL, + grade integer, + collection_category character varying(50), + activity_id bigint, + activity_type character varying(50), + status integer DEFAULT 0 NOT NULL, + like_count integer DEFAULT 0 NOT NULL, + display_status integer DEFAULT 0 NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + + +-- +-- Name: TABLE asset_registry; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.asset_registry IS '资产注册表(星册体系)'; + + +-- +-- Name: asset_registry_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.asset_registry_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: asset_registry_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.asset_registry_id_seq OWNED BY public.asset_registry.id; + + -- -- Name: assets; Type: TABLE; Schema: public; Owner: - -- @@ -408,7 +456,8 @@ CREATE TABLE public.exhibitions ( start_time bigint NOT NULL, expire_at bigint NOT NULL, created_at bigint NOT NULL, - updated_at bigint NOT NULL + updated_at bigint NOT NULL, + deleted_at bigint ); @@ -845,6 +894,13 @@ ALTER TABLE ONLY public.activity_user_stats ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.asset_likes ALTER COLUMN id SET DEFAULT nextval('public.asset_likes_id_seq'::regclass); +-- +-- Name: asset_registry id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_registry ALTER COLUMN id SET DEFAULT nextval('public.asset_registry_id_seq'::regclass); + + -- -- Name: assets id; Type: DEFAULT; Schema: public; Owner: - -- @@ -852,6 +908,13 @@ ALTER TABLE ONLY public.asset_likes ALTER COLUMN id SET DEFAULT nextval('public. ALTER TABLE ONLY public.assets ALTER COLUMN id SET DEFAULT nextval('public.assets_id_seq'::regclass); +-- +-- Name: asset_registry id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_registry ALTER COLUMN id SET DEFAULT nextval('public.asset_registry_id_seq'::regclass); + + -- -- Name: booth_slots slot_id; Type: DEFAULT; Schema: public; Owner: - -- @@ -969,6 +1032,14 @@ ALTER TABLE ONLY public.asset_likes ADD CONSTRAINT asset_likes_pkey PRIMARY KEY (id); +-- +-- Name: asset_registry asset_registry_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_registry + ADD CONSTRAINT asset_registry_pkey PRIMARY KEY (id); + + -- -- Name: assets assets_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1180,6 +1251,13 @@ CREATE INDEX idx_asset_likes_asset ON public.asset_likes USING btree (asset_id); CREATE INDEX idx_asset_likes_user_star ON public.asset_likes USING btree (user_id, star_id); +-- +-- Name: idx_registry_owner_star; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_registry_owner_star ON public.asset_registry USING btree (owner_uid, star_id); + + -- -- Name: idx_assets_created_at; Type: INDEX; Schema: public; Owner: - -- @@ -1549,6 +1627,30 @@ ALTER TABLE ONLY public.asset_likes ADD CONSTRAINT fk_asset_likes_user FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; +-- +-- Name: asset_registry fk_registry_asset; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_registry + ADD CONSTRAINT fk_registry_asset FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; + + +-- +-- Name: asset_registry fk_registry_owner; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_registry + ADD CONSTRAINT fk_registry_owner FOREIGN KEY (owner_uid) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: asset_registry fk_registry_star; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_registry + ADD CONSTRAINT fk_registry_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; + + -- -- Name: assets fk_assets_owner; Type: FK CONSTRAINT; Schema: public; Owner: - -- diff --git a/docker/sql/migrations/V1__init_schema.sql b/docker/sql/migrations/V1__init_schema.sql new file mode 100644 index 0000000..ef43839 --- /dev/null +++ b/docker/sql/migrations/V1__init_schema.sql @@ -0,0 +1,491 @@ +-- V1__init_schema.sql +-- 初始表结构 + +-- postgres 扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ===================================================== +-- 用户相关表 +-- ===================================================== + +-- 用户表 +CREATE TABLE public.users ( + id bigint NOT NULL, + mobile character varying(11) NOT NULL, + password_hash character varying(255) NOT NULL, + access_token text, + token_expires_at bigint, + avatar_url character varying(500), + global_wallet_address character varying(100), + is_active boolean DEFAULT true NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL, + deleted_at bigint +); + +CREATE SEQUENCE public.users_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); +ALTER TABLE ONLY public.users ADD CONSTRAINT users_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_users_mobile ON public.users USING btree (mobile); +CREATE INDEX idx_users_deleted_at ON public.users USING btree (deleted_at); +ALTER TABLE ONLY public.users ADD CONSTRAINT fk_users FOREIGN KEY (owner_uid) REFERENCES public.users(id) ON DELETE CASCADE; + +-- 明星信息表 +CREATE TABLE public.stars ( + star_id bigint NOT NULL, + name character varying(100) NOT NULL, + tag character varying(100), + name_en character varying(100), + pic_url character varying(500), + description text, + identity_id character varying(50) NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.stars_star_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.stars ALTER COLUMN star_id SET DEFAULT nextval('public.stars_star_id_seq'::regclass); +ALTER TABLE ONLY public.stars ADD CONSTRAINT stars_pkey PRIMARY KEY (star_id); +CREATE UNIQUE INDEX uk_stars_identity_id ON public.stars USING btree (identity_id); + +-- 粉丝档案表 +CREATE TABLE public.fan_profiles ( + id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + nickname character varying(50) NOT NULL, + level integer DEFAULT 1 NOT NULL, + times integer DEFAULT 1 NOT NULL, + social integer DEFAULT 0 NOT NULL, + experience bigint DEFAULT 0 NOT NULL, + coin_balance bigint DEFAULT 0 NOT NULL, + crystal_balance bigint DEFAULT 0 NOT NULL, + tags jsonb, + starbook_limit integer DEFAULT 3 NOT NULL, + slot_limit integer DEFAULT 3 NOT NULL, + assets_count integer DEFAULT 0 NOT NULL, + chain_address character varying(100), + is_active boolean DEFAULT true NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL, + avatar_url character varying(500) +); + +CREATE SEQUENCE public.fan_profiles_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.fan_profiles ALTER COLUMN id SET DEFAULT nextval('public.fan_profiles_id_seq'::regclass); +ALTER TABLE ONLY public.fan_profiles ADD CONSTRAINT fan_profiles_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_fan_profiles_user_star ON public.fan_profiles USING btree (user_id, star_id); +CREATE UNIQUE INDEX uk_fan_profiles_star_nickname ON public.fan_profiles USING btree (star_id, nickname); +CREATE INDEX idx_fan_profiles_user_id ON public.fan_profiles USING btree (user_id); +CREATE INDEX idx_fan_profiles_star_id ON public.fan_profiles USING btree (star_id); +ALTER TABLE ONLY public.fan_profiles ADD CONSTRAINT fk_fan_profiles_user FOREIGN KEY (user_id) REFERENCES public.users(id); +ALTER TABLE ONLY public.fan_profiles ADD CONSTRAINT fk_fan_profiles_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id); + +-- ===================================================== +-- 藏品相关表 +-- ===================================================== + +-- 资产表(藏品) +CREATE TABLE public.assets ( + id bigint NOT NULL, + owner_uid bigint NOT NULL, + star_id bigint NOT NULL, + name character varying(100) NOT NULL, + cover_url character varying(500) NOT NULL, + material_url character varying(500), + description text, + rarity integer, + tags jsonb, + visibility character varying(20) DEFAULT 'private'::character varying, + status integer DEFAULT 0 NOT NULL, + tx_hash character varying(100), + block_number bigint, + like_count integer DEFAULT 0 NOT NULL, + is_original boolean DEFAULT false NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL, + minted_at bigint, + deleted_at bigint, + is_active boolean DEFAULT true NOT NULL, + info text +); + +CREATE SEQUENCE public.assets_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.assets ALTER COLUMN id SET DEFAULT nextval('public.assets_id_seq'::regclass); +ALTER TABLE ONLY public.assets ADD CONSTRAINT assets_pkey PRIMARY KEY (id); +CREATE INDEX idx_assets_created_at ON public.assets USING btree (created_at DESC); +CREATE INDEX idx_assets_deleted_at ON public.assets USING btree (deleted_at); +CREATE INDEX idx_assets_owner_star ON public.assets USING btree (owner_uid, star_id); +CREATE INDEX idx_assets_star_active ON public.assets USING btree (star_id, is_active); +CREATE INDEX idx_assets_status ON public.assets USING btree (status); +CREATE INDEX idx_assets_tx_hash ON public.assets USING btree (tx_hash); +ALTER TABLE ONLY public.assets ADD CONSTRAINT fk_assets_owner FOREIGN KEY (owner_uid) REFERENCES public.users(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.assets ADD CONSTRAINT fk_assets_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; + +-- 资产注册表(星册体系) +CREATE TABLE public.asset_registry ( + id bigint NOT NULL, + asset_id bigint NOT NULL, + asset_type character varying(20) NOT NULL, + owner_uid bigint NOT NULL, + star_id bigint NOT NULL, + grade integer, + collection_category character varying(50), + activity_id bigint, + activity_type character varying(50), + status integer DEFAULT 0 NOT NULL, + like_count integer DEFAULT 0 NOT NULL, + display_status integer DEFAULT 0 NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.asset_registry_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.asset_registry ALTER COLUMN id SET DEFAULT nextval('public.asset_registry_id_seq'::regclass); +ALTER TABLE ONLY public.asset_registry ADD CONSTRAINT asset_registry_pkey PRIMARY KEY (id); +CREATE INDEX idx_registry_owner_star ON public.asset_registry USING btree (owner_uid, star_id); +ALTER TABLE ONLY public.asset_registry ADD CONSTRAINT fk_registry_asset FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.asset_registry ADD CONSTRAINT fk_registry_owner FOREIGN KEY (owner_uid) REFERENCES public.users(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.asset_registry ADD CONSTRAINT fk_registry_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; + +-- 点赞记录表 +CREATE TABLE public.asset_likes ( + id bigint NOT NULL, + asset_id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + created_at bigint NOT NULL +); + +CREATE SEQUENCE public.asset_likes_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.asset_likes ALTER COLUMN id SET DEFAULT nextval('public.asset_likes_id_seq'::regclass); +ALTER TABLE ONLY public.asset_likes ADD CONSTRAINT asset_likes_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_asset_likes_user_asset ON public.asset_likes USING btree (user_id, asset_id); +CREATE INDEX idx_asset_likes_asset ON public.asset_likes USING btree (asset_id); +CREATE INDEX idx_asset_likes_user_star ON public.asset_likes USING btree (user_id, star_id); +ALTER TABLE ONLY public.asset_likes ADD CONSTRAINT fk_asset_likes_asset FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.asset_likes ADD CONSTRAINT fk_asset_likes_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; +ALTER TABLE ONLY public.asset_likes ADD CONSTRAINT fk_asset_likes_user FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + +-- 铸造订单表 +CREATE TABLE public.mint_orders ( + order_id character varying(100) NOT NULL, + user_id bigint NOT NULL, + asset_id bigint, + star_id bigint NOT NULL, + status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + cost_crystal bigint DEFAULT 0, + error_message text, + retry_count integer DEFAULT 0, + material_url character varying(500), + name character varying(100), + description text, + material_type character varying(50), + event character varying(100), + created_at bigint NOT NULL, + updated_at bigint NOT NULL, + minted_at bigint, + info text +); + +ALTER TABLE ONLY public.mint_orders ADD CONSTRAINT mint_orders_pkey PRIMARY KEY (order_id); +CREATE INDEX idx_mint_orders_asset ON public.mint_orders USING btree (asset_id); +CREATE INDEX idx_mint_orders_created_at ON public.mint_orders USING btree (created_at DESC); +CREATE INDEX idx_mint_orders_status ON public.mint_orders USING btree (status); +CREATE INDEX idx_mint_orders_user_star ON public.mint_orders USING btree (user_id, star_id); +ALTER TABLE ONLY public.mint_orders ADD CONSTRAINT fk_mint_orders_asset FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE SET NULL; +ALTER TABLE ONLY public.mint_orders ADD CONSTRAINT fk_mint_orders_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; +ALTER TABLE ONLY public.mint_orders ADD CONSTRAINT fk_mint_orders_user FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + +-- ===================================================== +-- 展馆相关表 +-- ===================================================== + +-- 展位表 +CREATE TABLE public.booth_slots ( + slot_id bigint NOT NULL, + host_profile_id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + slot_index bigint NOT NULL, + visibility character varying(20) DEFAULT 'public'::character varying, + is_enabled boolean DEFAULT false, + unlock_type character varying(20), + unlock_value bigint, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.booth_slots_slot_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.booth_slots ALTER COLUMN slot_id SET DEFAULT nextval('public.booth_slots_slot_id_seq'::regclass); +ALTER TABLE ONLY public.booth_slots ADD CONSTRAINT booth_slots_pkey PRIMARY KEY (slot_id); +CREATE UNIQUE INDEX idx_host_slot ON public.booth_slots USING btree (host_profile_id, slot_index); +CREATE INDEX idx_star_enabled ON public.booth_slots USING btree (star_id); +CREATE INDEX idx_user_star ON public.booth_slots USING btree (user_id, star_id); +ALTER TABLE ONLY public.booth_slots ADD CONSTRAINT fk_booth_slots_profile FOREIGN KEY (host_profile_id) REFERENCES public.fan_profiles(id) ON DELETE CASCADE; + +-- 展品展示表 +CREATE TABLE public.exhibitions ( + id bigint NOT NULL, + 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, + deleted_at bigint +); + +CREATE SEQUENCE public.exhibitions_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.exhibitions ALTER COLUMN id SET DEFAULT nextval('public.exhibitions_id_seq'::regclass); +ALTER TABLE ONLY public.exhibitions ADD CONSTRAINT exhibitions_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_asset ON public.exhibitions USING btree (asset_id); +CREATE INDEX idx_expire ON public.exhibitions USING btree (expire_at); +CREATE INDEX idx_host ON public.exhibitions USING btree (host_profile_id); +CREATE INDEX idx_occupier ON public.exhibitions USING btree (occupier_uid, occupier_star_id); +CREATE INDEX idx_slot ON public.exhibitions USING btree (slot_id); +ALTER TABLE ONLY public.exhibitions ADD CONSTRAINT fk_exhibitions_asset FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.exhibitions ADD CONSTRAINT fk_exhibitions_slot FOREIGN KEY (slot_id) REFERENCES public.booth_slots(slot_id) ON DELETE CASCADE; + +-- 展览收入记录表 +CREATE TABLE public.exhibition_revenue_records ( + id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + exhibition_id bigint NOT NULL, + asset_id bigint NOT NULL, + slot_id bigint NOT NULL, + slot_owner_uid bigint NOT NULL, + slot_type character varying(20) NOT NULL, + crystal_amount bigint NOT NULL, + cycle_start_time bigint NOT NULL, + cycle_end_time bigint NOT NULL, + status character varying(20) DEFAULT 'claimable'::character varying NOT NULL, + claimed_at bigint, + created_at bigint NOT NULL +); + +CREATE SEQUENCE public.exhibition_revenue_records_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.exhibition_revenue_records ALTER COLUMN id SET DEFAULT nextval('public.exhibition_revenue_records_id_seq'::regclass); +ALTER TABLE ONLY public.exhibition_revenue_records ADD CONSTRAINT exhibition_revenue_records_pkey PRIMARY KEY (id); +CREATE INDEX idx_user_revenue ON public.exhibition_revenue_records USING btree (user_id, star_id); + +-- ===================================================== +-- 社交相关表 +-- ===================================================== + +-- 好友请求表 +CREATE TABLE public.friend_requests ( + id bigint NOT NULL, + from_user_id bigint NOT NULL, + to_user_id bigint NOT NULL, + star_id bigint NOT NULL, + message character varying(200), + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL, + expires_at bigint, + processed_at bigint +); + +CREATE SEQUENCE public.friend_requests_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.friend_requests ALTER COLUMN id SET DEFAULT nextval('public.friend_requests_id_seq'::regclass); +ALTER TABLE ONLY public.friend_requests ADD CONSTRAINT friend_requests_pkey PRIMARY KEY (id); +CREATE INDEX idx_friend_requests_expires ON public.friend_requests USING btree (expires_at); +CREATE INDEX idx_friend_requests_from_status ON public.friend_requests USING btree (from_user_id, status, created_at DESC); +CREATE INDEX idx_friend_requests_star ON public.friend_requests USING btree (star_id); +CREATE INDEX idx_friend_requests_to_status ON public.friend_requests USING btree (to_user_id, status, created_at DESC); +CREATE INDEX idx_friend_requests_users_star ON public.friend_requests USING btree (from_user_id, to_user_id, star_id, created_at DESC); +ALTER TABLE ONLY public.friend_requests ADD CONSTRAINT fk_friend_requests_from_user FOREIGN KEY (from_user_id) REFERENCES public.users(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.friend_requests ADD CONSTRAINT fk_friend_requests_to_user FOREIGN KEY (to_user_id) REFERENCES public.users(id) ON DELETE CASCADE; + +-- 好友关系表 +CREATE TABLE public.friendships ( + id bigint NOT NULL, + user_id bigint NOT NULL, + friend_id bigint NOT NULL, + star_id bigint NOT NULL, + status character varying(20) DEFAULT 'accepted'::character varying NOT NULL, + remark character varying(50), + intimacy integer DEFAULT 0 NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.friendships_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.friendships ALTER COLUMN id SET DEFAULT nextval('public.friendships_id_seq'::regclass); +ALTER TABLE ONLY public.friendships ADD CONSTRAINT friendships_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_friendships_user_friend_star ON public.friendships USING btree (user_id, friend_id, star_id); +CREATE INDEX idx_friendships_friend_star ON public.friendships USING btree (friend_id); +CREATE INDEX idx_friendships_list_query ON public.friendships USING btree (user_id, friend_id, star_id, status, remark, created_at); +CREATE INDEX idx_friendships_user_star_created ON public.friendships USING btree (user_id, star_id, created_at DESC); +CREATE INDEX idx_friendships_user_star_status ON public.friendships USING btree (user_id, star_id, status); +ALTER TABLE ONLY public.friendships ADD CONSTRAINT fk_friendships_friend FOREIGN KEY (friend_id) REFERENCES public.users(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.friendships ADD CONSTRAINT fk_friendships_user FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + +-- ===================================================== +-- 任务相关表 +-- ===================================================== + +-- 任务定义表 +CREATE TABLE public.task_definitions ( + id bigint NOT NULL, + task_key character varying(50) NOT NULL, + task_type character varying(20) NOT NULL, + name character varying(100) NOT NULL, + description text, + crystal_reward bigint DEFAULT 0 NOT NULL, + exp_reward bigint DEFAULT 0 NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.task_definitions_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.task_definitions ALTER COLUMN id SET DEFAULT nextval('public.task_definitions_id_seq'::regclass); +ALTER TABLE ONLY public.task_definitions ADD CONSTRAINT task_definitions_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX idx_task_definitions_task_key ON public.task_definitions USING btree (task_key); + +-- 用户任务进度表 +CREATE TABLE public.user_task_progress ( + id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + task_key character varying(50) NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + completed_at bigint, + claimed_at bigint, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.user_task_progress_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.user_task_progress ALTER COLUMN id SET DEFAULT nextval('public.user_task_progress_id_seq'::regclass); +ALTER TABLE ONLY public.user_task_progress ADD CONSTRAINT user_task_progress_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_user_task ON public.user_task_progress USING btree (user_id, star_id, task_key); + +-- 用户引导状态表 +CREATE TABLE public.user_onboarding_status ( + id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + is_onboarding_completed boolean DEFAULT false NOT NULL, + is_onboarding_claimed boolean DEFAULT false NOT NULL, + has_friend_display_bonus boolean DEFAULT false NOT NULL, + onboarding_completed_at bigint, + onboarding_claimed_at bigint, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.user_onboarding_status_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.user_onboarding_status ALTER COLUMN id SET DEFAULT nextval('public.user_onboarding_status_id_seq'::regclass); +ALTER TABLE ONLY public.user_onboarding_status ADD CONSTRAINT user_onboarding_status_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_user_star_onboarding ON public.user_onboarding_status USING btree (user_id, star_id); + +-- ===================================================== +-- 活动相关表 +-- ===================================================== + +-- 活动表 +CREATE TABLE public.activities ( + id bigint NOT NULL, + activity_type character varying(50) NOT NULL, + title character varying(100) NOT NULL, + description text, + star_id bigint NOT NULL, + start_time bigint NOT NULL, + end_time bigint NOT NULL, + target_progress bigint DEFAULT 1000 NOT NULL, + current_progress bigint DEFAULT 0 NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying, + stage_configs jsonb, + created_at bigint NOT NULL, + updated_at bigint NOT NULL, + theme character varying(100), + overall_end_time bigint DEFAULT 0 +); + +CREATE SEQUENCE public.activities_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.activities ALTER COLUMN id SET DEFAULT nextval('public.activities_id_seq'::regclass); +ALTER TABLE ONLY public.activities ADD CONSTRAINT activities_pkey PRIMARY KEY (id); +CREATE INDEX idx_activities_star_id ON public.activities USING btree (star_id); +CREATE INDEX idx_activities_start_end ON public.activities USING btree (start_time, end_time); +CREATE INDEX idx_activities_status ON public.activities USING btree (status); + +-- 活动道具表 +CREATE TABLE public.activity_items ( + id bigint NOT NULL, + activity_id bigint NOT NULL, + item_type character varying(50) NOT NULL, + item_name character varying(50) NOT NULL, + icon_url character varying(500), + crystal_cost bigint NOT NULL, + contribution_points bigint NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.activity_items_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.activity_items ALTER COLUMN id SET DEFAULT nextval('public.activity_items_id_seq'::regclass); +ALTER TABLE ONLY public.activity_items ADD CONSTRAINT activity_items_pkey PRIMARY KEY (id); +CREATE INDEX idx_activity_items_activity ON public.activity_items USING btree (activity_id); +CREATE INDEX idx_activity_items_type ON public.activity_items USING btree (item_type); +ALTER TABLE ONLY public.activity_items ADD CONSTRAINT fk_activity_items_activity FOREIGN KEY (activity_id) REFERENCES public.activities(id) ON DELETE CASCADE; + +-- 活动贡献记录表 +CREATE TABLE public.activity_contributions ( + id bigint NOT NULL, + activity_id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + item_id bigint NOT NULL, + item_type character varying(50) NOT NULL, + quantity integer DEFAULT 1 NOT NULL, + crystal_spent bigint NOT NULL, + contribution_points bigint NOT NULL, + created_at bigint NOT NULL +); + +CREATE SEQUENCE public.activity_contributions_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.activity_contributions ALTER COLUMN id SET DEFAULT nextval('public.activity_contributions_id_seq'::regclass); +ALTER TABLE ONLY public.activity_contributions ADD CONSTRAINT activity_contributions_pkey PRIMARY KEY (id); +CREATE INDEX idx_activity_contributions_activity ON public.activity_contributions USING btree (activity_id); +CREATE INDEX idx_activity_contributions_created ON public.activity_contributions USING btree (created_at DESC); +CREATE INDEX idx_activity_contributions_user_star ON public.activity_contributions USING btree (user_id, star_id); +ALTER TABLE ONLY public.activity_contributions ADD CONSTRAINT fk_activity_contributions_activity FOREIGN KEY (activity_id) REFERENCES public.activities(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.activity_contributions ADD CONSTRAINT fk_activity_contributions_item FOREIGN KEY (item_id) REFERENCES public.activity_items(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.activity_contributions ADD CONSTRAINT fk_activity_contributions_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; +ALTER TABLE ONLY public.activity_contributions ADD CONSTRAINT fk_activity_contributions_user FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + +-- 用户活动统计表 +CREATE TABLE public.activity_user_stats ( + id bigint NOT NULL, + activity_id bigint NOT NULL, + user_id bigint NOT NULL, + star_id bigint NOT NULL, + total_contribution bigint DEFAULT 0 NOT NULL, + total_crystal_spent bigint DEFAULT 0 NOT NULL, + total_items integer DEFAULT 0 NOT NULL, + last_contribute_at bigint NOT NULL, + created_at bigint NOT NULL, + updated_at bigint NOT NULL +); + +CREATE SEQUENCE public.activity_user_stats_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; +ALTER TABLE ONLY public.activity_user_stats ALTER COLUMN id SET DEFAULT nextval('public.activity_user_stats_id_seq'::regclass); +ALTER TABLE ONLY public.activity_user_stats ADD CONSTRAINT activity_user_stats_pkey PRIMARY KEY (id); +CREATE UNIQUE INDEX uk_activity_user_star ON public.activity_user_stats UNIQUE (activity_id, user_id, star_id); +CREATE INDEX idx_activity_user_stats_activity ON public.activity_user_stats USING btree (activity_id); +CREATE INDEX idx_activity_user_stats_contribution ON public.activity_user_stats USING btree (activity_id, total_contribution DESC); +CREATE INDEX idx_activity_user_stats_user_star ON public.activity_user_stats USING btree (user_id, star_id); +ALTER TABLE ONLY public.activity_user_stats ADD CONSTRAINT fk_activity_user_stats_activity FOREIGN KEY (activity_id) REFERENCES public.activities(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.activity_user_stats ADD CONSTRAINT fk_activity_user_stats_star FOREIGN KEY (star_id) REFERENCES public.stars(star_id) ON DELETE CASCADE; +ALTER TABLE ONLY public.activity_user_stats ADD CONSTRAINT fk_activity_user_stats_user FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; \ No newline at end of file diff --git a/docker/sql/migrations/V2__add_deleted_at_to_exhibitions.sql b/docker/sql/migrations/V2__add_deleted_at_to_exhibitions.sql new file mode 100644 index 0000000..9ba46ff --- /dev/null +++ b/docker/sql/migrations/V2__add_deleted_at_to_exhibitions.sql @@ -0,0 +1,4 @@ +-- V2__add_deleted_at_to_exhibitions.sql +-- 为 exhibitions 表添加 deleted_at 字段(软删除) + +ALTER TABLE public.exhibitions ADD COLUMN IF NOT EXISTS deleted_at bigint; \ No newline at end of file diff --git a/docker/sql/migrations/V3__seed_data.sql b/docker/sql/migrations/V3__seed_data.sql new file mode 100644 index 0000000..e348238 --- /dev/null +++ b/docker/sql/migrations/V3__seed_data.sql @@ -0,0 +1,17 @@ +-- V3__seed_data.sql +-- 种子数据(测试用) + +-- 插入测试明星 +INSERT INTO public.stars (star_id, name, tag, identity_id, is_active, created_at, updated_at) VALUES +(87, '测试明星', '测试', 'test_star_001', true, 1704067200000, 1704067200000) +ON CONFLICT DO NOTHING; + +-- 插入测试用户 +INSERT INTO public.users (id, mobile, password_hash, is_active, created_at, updated_at) VALUES +(1, '13800138000', '$2a$10$N9qo8uLOickgx2ZMRZoMye6e7q5R5pR6Vx9qPCr5dLq5R5pR6Vx9q', true, 1704067200000, 1704067200000) +ON CONFLICT DO NOTHING; + +-- 插入测试粉丝档案 +INSERT INTO public.fan_profiles (id, user_id, star_id, nickname, level, times, social, experience, coin_balance, crystal_balance, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at, avatar_url) VALUES +(1, 1, 87, '测试用户', 5, 3, 10, 1000, 500, 100, 3, 3, 5, true, 1704067200000, 1704067200000, 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/avatar/1/87/avatar.png') +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/docker/sql/migrations/V4__seed_data.sql b/docker/sql/migrations/V4__seed_data.sql new file mode 100644 index 0000000..04ca1b3 --- /dev/null +++ b/docker/sql/migrations/V4__seed_data.sql @@ -0,0 +1,53 @@ +-- V4__seed_data.sql +-- 种子数据 + +-- 插入明星数据 +INSERT INTO public.stars (star_id, name, tag, name_en, pic_url, description, identity_id, is_active, created_at, updated_at) VALUES +(87, '肖战', '小飞侠', 'Xiao Zhan', NULL, NULL, 'xz', true, 1773322573872, 1773322573872), +(88, '王一博', '小摩托', 'Wang Yibo', NULL, NULL, 'wyb', true, 1773322573872, 1773322573872), +(89, '杨洋', NULL, 'Yang Yang', NULL, NULL, 'yy', true, 1773322573872, 1773322573872), +(93, 'Lisa', 'BLACKPINK', 'Lalisa Manoban', 'https://example.com/lisa.jpg', 'BLACKPINK成员', 'lisa_test_001', true, 1773407317000, 1773407317000), +(94, 'Jennie', 'BLACKPINK', 'Jennie Kim', 'https://example.com/jennie.jpg', 'BLACKPINK成员', 'jennie_test_001', true, 1773407317000, 1773407317000), +(95, 'Rosé', 'BLACKPINK', 'Park Chae-young', 'https://example.com/rose.jpg', 'BLACKPINK成员', 'rose_test_001', true, 1773407317000, 1773407317000) +ON CONFLICT (star_id) DO UPDATE SET name = EXCLUDED.name, tag = EXCLUDED.tag; + +-- 插入用户数据 +INSERT INTO public.users (id, mobile, password_hash, access_token, token_expires_at, avatar_url, global_wallet_address, is_active, created_at, updated_at, deleted_at) VALUES +(1, '13900000001', '$2a$10$bNqpiVCSSrhxAsUelgkEp.j1untPPWMDT5208fmcUPKpsFHZlhjrW', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJzdGFyX2lkIjo4NywidXBkYXRlZF9hdCI6MTc3MzU4MTI5MiwiZXhwIjoxNzc3NTI5OTg5LCJpYXQiOjE3NzY5MjUxODl9.LooVCY-ST2y02qKud8A6GRVL1RxOy4pXVD6KC7NLs6w', 1777529989817, 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/avatar/1/87/avatar.png', NULL, true, 1773322609887, 1773581292, NULL), +(2, '17319409126', '$2a$10$bNqpiVCSSrhxAsUelgkEp.j1untPPWMDT5208fmcUPKpsFHZlhjrW', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJzdGFyX2lkIjo4NywidXBkYXRlZF9hdCI6MTc3MzMyNDEwMTU5OCwiZXhwIjoxNzc0NDIxOTIxLCJpYXQiOjE3NzM4MTcxMjF9.GnAL9exh-VcXDHqwTn1bNKVAEAg6xfMMpd6JTqkwugk', 1774421921871, NULL, NULL, true, 1773324101598, 1773324101598, NULL), +(8, '13800000001', '$2a$10$LC8XORj6Nx9nT6j5QHfN5u2nB4z8e0v2dWm7X2uF5m6W6br8NnDPS', NULL, NULL, NULL, NULL, true, 1773407317000, 1776649807972, NULL), +(9, '13800000002', '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', NULL, NULL, NULL, NULL, true, 1773407317000, 1773407317000, NULL), +(10, '13800000003', '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', NULL, NULL, NULL, NULL, true, 1773407317000, 1773407317000, NULL), +(11, '13800000004', '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', NULL, NULL, NULL, NULL, true, 1773407317000, 1773407317000, NULL), +(12, '13800000005', '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', NULL, NULL, NULL, NULL, true, 1773407317000, 1773407317000, NULL) +ON CONFLICT (id) DO UPDATE SET access_token = EXCLUDED.access_token, updated_at = EXCLUDED.updated_at; + +-- 插入粉丝档案数据 +INSERT INTO public.fan_profiles (id, user_id, star_id, nickname, level, times, social, experience, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, chain_address, is_active, created_at, updated_at, avatar_url) VALUES +(1, 1, 87, '测试用户', 6, 1, 0, 1750, 0, 10919, '[]', 3, 3, 42, NULL, true, 1773322609890, 1776926969, 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/avatar/1/87/avatar.png'), +(2, 2, 87, 'topfans1', 1, 1, 0, 0, 0, 0, '[]', 3, 3, 0, NULL, true, 1773324101600, 1773324101600, NULL) +ON CONFLICT (id) DO UPDATE SET level = EXCLUDED.level, experience = EXCLUDED.experience, updated_at = EXCLUDED.updated_at; + +-- 插入 Booth Slots 数据 +INSERT INTO public.booth_slots (slot_id, host_profile_id, user_id, star_id, slot_index, visibility, is_enabled, unlock_type, unlock_value, created_at, updated_at) VALUES +(37, 1, 1, 87, 1, 'public', true, 'free', 0, 1773748940460, 1773748940460), +(38, 1, 1, 87, 2, 'public', true, 'free', 0, 1773748940460, 1773748940460), +(39, 1, 1, 87, 3, 'public', true, 'free', 0, 1773748940460, 1773748940460), +(40, 1, 1, 87, 4, 'private', true, 'free', 0, 1773748940460, 1773748940460), +(41, 1, 1, 87, 5, 'private', true, 'free', 0, 1773748940460, 1773748940460), +(42, 1, 1, 87, 6, 'private', true, 'free', 0, 1773748940460, 1773748940460) +ON CONFLICT (slot_id) DO NOTHING; + +-- 插入 Assets 数据 +INSERT INTO public.assets (id, owner_uid, star_id, name, cover_url, material_url, description, grade, tags, visibility, status, tx_hash, block_number, like_count, is_original, created_at, updated_at, minted_at, deleted_at, is_active, info) VALUES +(17, 1, 87, '1', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/covers/17_1775472017.png', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/ai_generated_1775472013603_g87iwk.png', NULL, NULL, NULL, 'private', 1, '0x1922ffb92fcd1a7e4a8c6b3347e8e0791f6107fa44cbde1734ecb1bcd861d63c', 1493537, 4, false, 1775472014756, 1775472018619, 1775472018619, NULL, true, NULL), +(18, 1, 87, '1', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/covers/18_1775539093.png', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/ai_generated_1775539090338_fwzxhs.png', NULL, NULL, NULL, 'private', 1, '0x40ca6efcf29f47731453f01b5c364cde52acbce597c1fbf8261301cc44ec4b7a', 1406147, 2, false, 1775539090589, 1775539093945, 1775539093945, NULL, true, NULL), +(19, 1, 87, '未命名藏品', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/covers/19_1775812743.png', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/ai_generated_1775812740051_gf8ew2.png', NULL, NULL, NULL, 'private', 1, '0x0e3e47c58814e52465ac1e928f3392ff1c692ea448103fcb03c4a10077141308', 1219083, 3, false, 1775812740261, 1775812743690, 1775812743690, NULL, true, '111'), +(20, 1, 87, '星卡', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/covers/20_1775812994.png', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/1775812991404.jpg', NULL, NULL, NULL, 'private', 1, '0x49d04d7463cf03374f6e622d739f69e83cd4ba84b145627f039a22340daead93', 1938046, 2, false, 1775812991507, 1775812994841, 1775812994841, NULL, true, '11'), +(21, 1, 87, '星卡', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/covers/21_1775813155.png', 'https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/1/87/1775813152104.jpg', NULL, NULL, NULL, 'private', 1, '0x6d7c0c276d7485d17e6dea4c9d1e394315613d69114699d341c221a22e8156f8', 1118388, 2, false, 1775813152198, 1775813155515, 1775813155515, NULL, true, '2') +ON CONFLICT (id) DO UPDATE SET like_count = EXCLUDED.like_count; + +-- 插入 Exhibitions 数据 +INSERT INTO public.exhibitions (id, asset_id, slot_id, host_profile_id, occupier_uid, occupier_star_id, start_time, expire_at, created_at, updated_at, deleted_at) VALUES +(1, 17, 37, 1, 1, 87, 1776926400000, 1777555200000, 1776926400000, 1776926400000, NULL) +ON CONFLICT (id) DO NOTHING; \ No newline at end of file diff --git a/frontend/pages.json b/frontend/pages.json index 359f876..4a2f41f 100644 --- a/frontend/pages.json +++ b/frontend/pages.json @@ -161,18 +161,18 @@ "navigationStyle": "custom" } }, - { - "path": "pages/tasks/daily-tasks", - "style": { - "navigationStyle": "custom" - } - }, - { - "path": "pages/tasks/guide", - "style": { - "navigationStyle": "custom" - } - }, + // { + // "path": "pages/tasks/daily-tasks", + // "style": { + // "navigationStyle": "custom" + // } + // }, + // { + // "path": "pages/tasks/guide", + // "style": { + // "navigationStyle": "custom" + // } + // }, { "path": "pages/tasks/revenue", "style": { diff --git a/frontend/pages/components/BannerTop3.vue b/frontend/pages/components/BannerTop3.vue index 6fbbf3b..82ce969 100644 --- a/frontend/pages/components/BannerTop3.vue +++ b/frontend/pages/components/BannerTop3.vue @@ -29,7 +29,7 @@ 用户 : diff --git a/frontend/pages/components/StarbookContent.vue b/frontend/pages/components/StarbookContent.vue index d5f6a1e..b0e28ae 100644 --- a/frontend/pages/components/StarbookContent.vue +++ b/frontend/pages/components/StarbookContent.vue @@ -66,11 +66,12 @@ > - - {{ item.display_status === 1 ? '已展示' : '待展示' }} + + 已展示 {{ item.name }} @@ -89,7 +90,7 @@ - + @@ -108,11 +109,12 @@ > - - {{ item.display_status === 1 ? '已展示' : '待展示' }} + + 已展示 {{ item.name }} @@ -131,7 +133,7 @@ - + @@ -150,11 +152,12 @@ > - - {{ item.display_status === 1 ? '已展示' : '待展示' }} + + 已展示 {{ item.name }} @@ -540,10 +543,15 @@ watch(() => props.isActive, (newVal) => { width: 192rpx; height: 224rpx; border-radius: 16rpx; - background: rgba(255, 255, 255, 0.05); + /* background: rgba(255, 255, 255, 0.05); */ display: block; } +/* 已展示的图片 - 灰色滤镜 */ +.nft-image-displayed { + filter: grayscale(23%); +} + /* 更多占位符 */ .more-placeholder { background: rgba(255, 255, 255, 0.1); @@ -559,26 +567,45 @@ watch(() => props.isActive, (newVal) => { text-align: center; } -/* 展示状态标签 */ -.status-badge { +/* 展示状态覆盖层 - 居中显示 */ +.status-overlay { position: absolute; - top: 8rpx; - right: 8rpx; - border-radius: 8rpx; - padding: 4rpx 8rpx; + top: 0; + left: 0; + width: 192rpx; + height: 224rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16rpx; z-index: 1; + } -.status-badge-active { - background: linear-gradient(135deg, #FFD700, #FFA500); - box-shadow: 0 0 12rpx rgba(255, 215, 0, 0.6); -} -.status-badge-pending { - background: rgba(0, 0, 0, 0.6); -} -.status-text { - font-size: 18rpx; + +.status-text-center { + font-size: 24rpx; color: #fff; font-weight: bold; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8); + /* 渐变:左浅橙粉 → 右柔粉红 */ + background: linear-gradient(to bottom right, + #F0E4B1 0%, /* 左:浅橙粉 */ + #F08399 50%, + #B94E73 100% /* 右:柔粉红 */ + ); + + border-radius: 24rpx; /* 胶囊形 */ + + /* 立体感核心:多层阴影 + 内阴影模拟凸起 */ + box-shadow: + /* 外层投影 - 让按钮浮起 */ + 0 4rpx 12rpx rgba(255, 143, 158, 0.2), + 0 2rpx 6rpx rgba(255, 143, 158, 0.15), + + /* 内阴影 - 模拟顶部受光 + 底部凹陷 */ + inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */ + inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */ + padding: 16rpx; } .nft-name { diff --git a/frontend/pages/discover/generation-loading.vue b/frontend/pages/discover/generation-loading.vue index dc39c74..1e296f6 100644 --- a/frontend/pages/discover/generation-loading.vue +++ b/frontend/pages/discover/generation-loading.vue @@ -5,15 +5,7 @@ - - - - TOPFANS - - ? - - - + @@ -206,11 +198,11 @@ onMounted(() => { .gift-box { position: absolute; - top: 20%; + top: 25%; left: 50%; transform: translateX(-50%); - width: 450rpx; - height: 450rpx; + width: 480rpx; + height: 480rpx; z-index: 2; animation: float 3s ease-in-out infinite; } @@ -224,125 +216,9 @@ onMounted(() => { } } -.gift-container { - position: relative; +.gift-image { width: 100%; height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.gift-bow { - position: absolute; - top: -30rpx; - width: 200rpx; - height: 80rpx; - background: linear-gradient(135deg, #FFB6D9 0%, #FFA8C5 50%, #FFB6D9 100%); - border-radius: 50rpx 50rpx 20rpx 20rpx; - box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.4); - z-index: 3; -} - -.gift-bow::before, -.gift-bow::after { - content: ''; - position: absolute; - top: 10rpx; - width: 80rpx; - height: 60rpx; - background: linear-gradient(135deg, #FFB6D9 0%, #FFA8C5 100%); - border-radius: 50%; -} - -.gift-bow::before { - left: -40rpx; - transform: rotate(-20deg); -} - -.gift-bow::after { - right: -40rpx; - transform: rotate(20deg); -} - -.gift-body { - position: relative; - width: 350rpx; - height: 350rpx; - background: linear-gradient(135deg, #FFE5F0 0%, #FFD4E8 50%, #FFC9E3 100%); - border-radius: 30rpx; - box-shadow: 0 16rpx 48rpx rgba(255, 107, 157, 0.3); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - overflow: hidden; -} - -.gift-body::before { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 60rpx; - height: 100%; - background: linear-gradient(180deg, rgba(255, 182, 217, 0.6) 0%, rgba(255, 168, 197, 0.6) 100%); -} - -.gift-body::after { - content: ''; - position: absolute; - top: 50%; - left: 0; - transform: translateY(-50%); - width: 100%; - height: 60rpx; - background: linear-gradient(90deg, rgba(255, 182, 217, 0.6) 0%, rgba(255, 168, 197, 0.6) 100%); -} - -.gift-text { - position: absolute; - top: 40rpx; - font-size: 36rpx; - font-weight: bold; - color: #FF6B9D; - letter-spacing: 4rpx; - text-shadow: 0 2rpx 8rpx rgba(255, 107, 157, 0.3); - z-index: 2; -} - -.gift-window { - position: relative; - width: 180rpx; - height: 180rpx; - background: linear-gradient(135deg, #FFF5FA 0%, #FFEBF3 100%); - border-radius: 20rpx; - display: flex; - align-items: center; - justify-content: center; - box-shadow: inset 0 4rpx 12rpx rgba(255, 107, 157, 0.2); - z-index: 2; -} - -.question-mark { - font-size: 120rpx; - font-weight: bold; - color: #FFB6D9; - text-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.3); - animation: questionPulse 2s ease-in-out infinite; -} - -@keyframes questionPulse { - 0%, 100% { - opacity: 0.8; - transform: scale(1); - } - 50% { - opacity: 1; - transform: scale(1.05); - } } .progress-container { diff --git a/frontend/pages/discover/generation-result.vue b/frontend/pages/discover/generation-result.vue index 01af5eb..33d3626 100644 --- a/frontend/pages/discover/generation-result.vue +++ b/frontend/pages/discover/generation-result.vue @@ -49,19 +49,18 @@ - - - - - TOPFANS - - - - - ? - - - + + @@ -1086,16 +1085,16 @@ onMounted(() => { bottom: 20%; left: 50%; transform: translateX(-50%); - width: 420rpx; - height: 450rpx; + width: 480rpx; + height: 480rpx; z-index: 3; animation: giftFloat 3s ease-in-out infinite; transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); - perspective: 1000rpx; } .gift-box.gift-opened { - bottom: 5%; + width: 17rem; + bottom: 15%; animation: none; } @@ -1108,192 +1107,29 @@ onMounted(() => { } } -.gift-container { - position: relative; +.gift-image { width: 100%; height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-end; + transition: opacity 0.5s ease-in-out; } -.gift-lid { - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 340rpx; - height: 140rpx; - z-index: 3; - transition: all 1s cubic-bezier(0.4, 0, 0.2, 1); - transform-style: preserve-3d; - transform-origin: bottom left; +.gift-image-opened { + width: 17rem; + animation: giftOpenPop 0.5s ease-out; } -.gift-opened .gift-lid { - transform: translateX(-70%) translateY(140rpx) rotateZ(-45deg); - opacity: 0.9; -} - -.gift-bow { - position: absolute; - top: -30rpx; - left: 50%; - transform: translateX(-50%); - width: 200rpx; - height: 80rpx; - z-index: 4; -} - -.gift-bow::before { - content: ''; - position: absolute; - top: 20rpx; - left: 50%; - transform: translateX(-50%); - width: 40rpx; - height: 60rpx; - background: linear-gradient(135deg, #FFB6D9 0%, #FF9DC5 100%); - border-radius: 8rpx; - box-shadow: 0 4rpx 15rpx rgba(255, 107, 157, 0.4); -} - -.gift-bow::after { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 180rpx; - height: 50rpx; - background: linear-gradient(135deg, #FFB6D9 0%, #FF9DC5 50%, #FFB6D9 100%); - border-radius: 30rpx; - box-shadow: 0 6rpx 20rpx rgba(255, 107, 157, 0.5); -} - -.gift-lid-top { - position: relative; - width: 340rpx; - height: 100rpx; - background: linear-gradient(180deg, - #FFF0F5 0%, - #FFE5F0 30%, - #FFD4E8 100%); - border-radius: 20rpx 20rpx 8rpx 8rpx; - box-shadow: - 0 10rpx 40rpx rgba(255, 107, 157, 0.4), - inset 0 -5rpx 15rpx rgba(255, 182, 217, 0.3), - inset 0 5rpx 15rpx rgba(255, 255, 255, 0.5); - display: flex; - align-items: center; - justify-content: center; - overflow: visible; - border: 3rpx solid rgba(255, 255, 255, 0.6); -} - -.gift-lid-top::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 280rpx; - height: 60rpx; - background: linear-gradient(90deg, - transparent 0%, - rgba(255, 182, 217, 0.4) 50%, - transparent 100%); - border-radius: 30rpx; -} - -.gift-text { - position: relative; - font-size: 36rpx; - font-weight: bold; - color: #FF6B9D; - letter-spacing: 4rpx; - text-shadow: - 0 2rpx 8rpx rgba(255, 107, 157, 0.5), - 0 0 20rpx rgba(255, 182, 217, 0.3); - z-index: 2; -} - -.gift-body { - position: relative; - width: 340rpx; - height: 280rpx; - background: linear-gradient(180deg, - #FFE5F0 0%, - #FFD4E8 50%, - #FFC9E3 100%); - border-radius: 8rpx 8rpx 20rpx 20rpx; - box-shadow: - 0 15rpx 50rpx rgba(255, 107, 157, 0.4), - inset 0 5rpx 20rpx rgba(255, 255, 255, 0.3), - inset 0 -5rpx 20rpx rgba(255, 182, 217, 0.3); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - overflow: hidden; - border: 3rpx solid rgba(255, 255, 255, 0.5); - border-top: none; -} - -.gift-body::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 8rpx; - background: linear-gradient(90deg, - rgba(255, 107, 157, 0.3) 0%, - rgba(255, 182, 217, 0.5) 50%, - rgba(255, 107, 157, 0.3) 100%); -} - -.gift-body::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 90%; - height: 90%; - background: radial-gradient(circle at center, - rgba(255, 255, 255, 0.2) 0%, - transparent 70%); - pointer-events: none; -} - -.gift-window { - position: relative; - width: 180rpx; - height: 180rpx; - background: linear-gradient(135deg, - #FFF5FA 0%, - #FFEBF3 50%, - #FFE0ED 100%); - border-radius: 20rpx; - display: flex; - align-items: center; - justify-content: center; - box-shadow: - inset 0 4rpx 15rpx rgba(255, 107, 157, 0.2), - 0 4rpx 20rpx rgba(255, 182, 217, 0.3); - z-index: 2; - border: 2rpx solid rgba(255, 182, 217, 0.4); -} - -.question-mark { - font-size: 120rpx; - font-weight: bold; - color: #FFB6D9; - text-shadow: - 0 4rpx 15rpx rgba(255, 107, 157, 0.4), - 0 0 30rpx rgba(255, 182, 217, 0.3); +@keyframes giftOpenPop { + 0% { + transform: scale(0.8); + opacity: 0; + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + opacity: 1; + } } .bottom-action { diff --git a/frontend/utils/api.js b/frontend/utils/api.js index 2e495a5..6da7c1a 100644 --- a/frontend/utils/api.js +++ b/frontend/utils/api.js @@ -1,7 +1,7 @@ // API 基础配置 -// const baseURL = 'http://101.132.250.62:8080' +const baseURL = 'http://101.132.250.62:8080' // const baseURL = 'http://192.168.110.60:8080' -const baseURL = 'http://localhost:8080' +// const baseURL = 'http://localhost:8080' // 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false) const USE_MOCK_API = false