diff --git a/prisma/migrations/20251026015433_add_material_type/migration.sql b/prisma/migrations/20251026015433_add_material_type/migration.sql new file mode 100644 index 0000000..00c4b36 --- /dev/null +++ b/prisma/migrations/20251026015433_add_material_type/migration.sql @@ -0,0 +1,143 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "users" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'USER', + "isActive" BOOLEAN NOT NULL DEFAULT false, + "canDownload" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "materials" ( + "id" TEXT NOT NULL, + "previewUrl" TEXT NOT NULL, + "productModel" TEXT NOT NULL, + "categoryName" TEXT NOT NULL, + "sceneName" TEXT NOT NULL, + "materialType" TEXT NOT NULL DEFAULT '3DMAX', + "renderer" TEXT NOT NULL DEFAULT 'CR', + "authorName" TEXT NOT NULL, + "zipFileName" TEXT NOT NULL, + "zipFilePath" TEXT NOT NULL, + "zipFileSize" BIGINT NOT NULL DEFAULT 0, + "downloadCount" INTEGER NOT NULL DEFAULT 0, + "likeCount" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "materials_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "material_likes" ( + "id" TEXT NOT NULL, + "materialId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "material_likes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "categories" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "categories_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "scenes" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "scenes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "system_settings" ( + "id" TEXT NOT NULL, + "headerBgColor" TEXT NOT NULL DEFAULT '#1f2937', + "logoUrl" TEXT NOT NULL DEFAULT 'http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png', + "siteName" TEXT NOT NULL DEFAULT 'FLNXI素材库', + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "system_settings_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "users_username_key" ON "users"("username"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_userId_idx" ON "materials"("userId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_createdAt_idx" ON "materials"("createdAt"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_categoryName_idx" ON "materials"("categoryName"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_sceneName_idx" ON "materials"("sceneName"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_materialType_idx" ON "materials"("materialType"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_downloadCount_idx" ON "materials"("downloadCount"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_likeCount_idx" ON "materials"("likeCount"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "material_likes_materialId_idx" ON "material_likes"("materialId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "material_likes_userId_idx" ON "material_likes"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "material_likes_materialId_userId_key" ON "material_likes"("materialId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "categories_name_key" ON "categories"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "scenes_name_key" ON "scenes"("name"); + +-- AddForeignKey(Postgres 不支持 IF NOT EXISTS,因此使用 DO 块捕获重复异常) +DO $$ +BEGIN + ALTER TABLE "materials" ADD CONSTRAINT "materials_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +DO $$ +BEGIN + ALTER TABLE "material_likes" ADD CONSTRAINT "material_likes_materialId_fkey" FOREIGN KEY ("materialId") REFERENCES "materials"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +DO $$ +BEGIN + ALTER TABLE "material_likes" ADD CONSTRAINT "material_likes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; diff --git a/prisma/migrations/20251122000000_create_upload_templates_baseline/migration.sql b/prisma/migrations/20251122000000_create_upload_templates_baseline/migration.sql new file mode 100644 index 0000000..2f3159e --- /dev/null +++ b/prisma/migrations/20251122000000_create_upload_templates_baseline/migration.sql @@ -0,0 +1,50 @@ +-- 基线迁移:为上传模板体系创建基础表,便于后续增量迁移在 shadow 数据库中正常回放 +-- 使用 IF NOT EXISTS 保证在已有线上库上执行时是幂等、安全的 + +-- UploadTemplate:上传模板主表 +CREATE TABLE IF NOT EXISTS "UploadTemplate" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "materialType" TEXT NOT NULL DEFAULT '其他', + "materialKind" TEXT NOT NULL DEFAULT 'IMAGE', + "version" INTEGER NOT NULL DEFAULT 1, + "fieldGroups" TEXT, + "displayConfig" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UploadTemplate_pkey" PRIMARY KEY ("id") +); + +-- UploadField:模板字段表 +CREATE TABLE IF NOT EXISTS "UploadField" ( + "id" TEXT NOT NULL, + "templateId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "label" TEXT NOT NULL, + "type" TEXT NOT NULL, + "required" BOOLEAN NOT NULL DEFAULT false, + "orderIndex" INTEGER NOT NULL DEFAULT 0, + "layoutWidth" TEXT, + "validations" TEXT, + "optionsSource" TEXT, + "optionsStatic" TEXT, + "allowedExtensions" TEXT, + "visibleOnHomeFilter" BOOLEAN NOT NULL DEFAULT false, + "isSearchable" BOOLEAN NOT NULL DEFAULT false, + "isReadOnly" BOOLEAN NOT NULL DEFAULT false, + "addToCategory" BOOLEAN NOT NULL DEFAULT false, + "conditions" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UploadField_pkey" PRIMARY KEY ("id") +); + +-- 关联关系:字段隶属于模板 +ALTER TABLE "UploadField" +ADD CONSTRAINT "UploadField_templateId_fkey" +FOREIGN KEY ("templateId") REFERENCES "UploadTemplate"("id") +ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251122225852_add_add_to_category_field/migration.sql b/prisma/migrations/20251122225852_add_add_to_category_field/migration.sql new file mode 100644 index 0000000..4295f83 --- /dev/null +++ b/prisma/migrations/20251122225852_add_add_to_category_field/migration.sql @@ -0,0 +1,53 @@ +-- 确保上传模板/字段基础表存在(若之前迁移缺失时可兜底创建) +CREATE TABLE IF NOT EXISTS "UploadTemplate" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "materialType" TEXT NOT NULL DEFAULT '其他', + "materialKind" TEXT NOT NULL DEFAULT 'IMAGE', + "version" INTEGER NOT NULL DEFAULT 1, + "fieldGroups" TEXT, + "displayConfig" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UploadTemplate_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "UploadField" ( + "id" TEXT NOT NULL, + "templateId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "label" TEXT NOT NULL, + "type" TEXT NOT NULL, + "required" BOOLEAN NOT NULL DEFAULT false, + "orderIndex" INTEGER NOT NULL DEFAULT 0, + "layoutWidth" TEXT, + "validations" TEXT, + "optionsSource" TEXT, + "optionsStatic" TEXT, + "allowedExtensions" TEXT, + "visibleOnHomeFilter" BOOLEAN NOT NULL DEFAULT false, + "isSearchable" BOOLEAN NOT NULL DEFAULT false, + "isReadOnly" BOOLEAN NOT NULL DEFAULT false, + "addToCategory" BOOLEAN NOT NULL DEFAULT false, + "conditions" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UploadField_pkey" PRIMARY KEY ("id") +); + +DO $$ +BEGIN + ALTER TABLE "UploadField" + ADD CONSTRAINT "UploadField_templateId_fkey" + FOREIGN KEY ("templateId") REFERENCES "UploadTemplate"("id") + ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +-- AlterTable(幂等,确保列存在) +ALTER TABLE "UploadField" ADD COLUMN IF NOT EXISTS "addToCategory" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20251123202325_add_hidden_builtin_templates/migration.sql b/prisma/migrations/20251123202325_add_hidden_builtin_templates/migration.sql new file mode 100644 index 0000000..f10fa5c --- /dev/null +++ b/prisma/migrations/20251123202325_add_hidden_builtin_templates/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "hiddenBuiltinTemplates" TEXT; diff --git a/prisma/migrations/20251124000000_rename_hidden_to_enabled_builtin_templates/migration.sql b/prisma/migrations/20251124000000_rename_hidden_to_enabled_builtin_templates/migration.sql new file mode 100644 index 0000000..b448566 --- /dev/null +++ b/prisma/migrations/20251124000000_rename_hidden_to_enabled_builtin_templates/migration.sql @@ -0,0 +1,69 @@ +-- 将 hiddenBuiltinTemplates 字段重命名为 enabledBuiltinTemplates +-- 逻辑反转:之前存储隐藏的列表,现在存储启用的列表 +-- 默认全部启用(空数组或null表示全部启用) + +-- 所有预制模板key +DO $$ +DECLARE + hidden_list TEXT; + enabled_list TEXT; + all_keys TEXT[] := ARRAY['3dmax-material', 'psd-material', 'ai-material', 'cad-material', 'c4d-material', 'video-material', 'texture-material', 'model-material', 'standard-accessory-material', 'other-material']; + hidden_array TEXT[]; + enabled_array TEXT[]; + key_item TEXT; +BEGIN + -- 检查字段是否存在 + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'system_settings' + AND column_name = 'hiddenBuiltinTemplates' + ) THEN + -- 获取当前的 hiddenBuiltinTemplates 值 + SELECT "hiddenBuiltinTemplates" INTO hidden_list FROM "system_settings" LIMIT 1; + + -- 如果 hiddenBuiltinTemplates 存在且有值,转换为启用列表 + IF hidden_list IS NOT NULL AND hidden_list != '' THEN + BEGIN + -- 解析隐藏列表 + SELECT ARRAY(SELECT json_array_elements_text(hidden_list::json)) INTO hidden_array; + + -- 将隐藏的列表转换为启用的列表(取反:所有key减去隐藏的key) + enabled_array := ARRAY( + SELECT unnest(all_keys) + EXCEPT + SELECT unnest(hidden_array) + ); + + -- 转换为JSON数组 + enabled_list := COALESCE( + (SELECT json_agg(value) FROM unnest(enabled_array) AS value), + '[]'::json + )::text; + EXCEPTION WHEN OTHERS THEN + -- 解析失败,设置为空数组(表示全部启用) + enabled_list := '[]'; + END; + ELSE + -- 如果没有隐藏列表,表示全部启用,设置为空数组 + enabled_list := '[]'; + END IF; + + -- 重命名字段 + ALTER TABLE "system_settings" + RENAME COLUMN "hiddenBuiltinTemplates" TO "enabledBuiltinTemplates"; + + -- 更新值 + UPDATE "system_settings" + SET "enabledBuiltinTemplates" = enabled_list; + ELSE + -- 如果字段不存在,直接创建新字段 + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'system_settings' + AND column_name = 'enabledBuiltinTemplates' + ) THEN + ALTER TABLE "system_settings" + ADD COLUMN "enabledBuiltinTemplates" TEXT DEFAULT '[]'; + END IF; + END IF; +END $$; diff --git a/prisma/migrations/20251124001000_add_material_type_to_templates/migration.sql b/prisma/migrations/20251124001000_add_material_type_to_templates/migration.sql new file mode 100644 index 0000000..baf4ed1 --- /dev/null +++ b/prisma/migrations/20251124001000_add_material_type_to_templates/migration.sql @@ -0,0 +1,7 @@ +-- 添加 materialType 字段到上传模板,默认值为“其他” +ALTER TABLE "UploadTemplate" +ADD COLUMN IF NOT EXISTS "materialType" TEXT NOT NULL DEFAULT '其他'; + +-- 为已有数据回填 materialType(保持默认值) +UPDATE "UploadTemplate" +SET "materialType" = COALESCE(NULLIF("materialType", ''), '其他'); diff --git a/prisma/migrations/20251124020000_add_category_managed_fields/migration.sql b/prisma/migrations/20251124020000_add_category_managed_fields/migration.sql new file mode 100644 index 0000000..71e3ef6 --- /dev/null +++ b/prisma/migrations/20251124020000_add_category_managed_fields/migration.sql @@ -0,0 +1,3 @@ +-- 为 system_settings 表新增分类字段管理配置列 +ALTER TABLE "system_settings" +ADD COLUMN IF NOT EXISTS "categoryManagedFields" TEXT; diff --git a/prisma/migrations/20251125040000_add_material_optional_fields/migration.sql b/prisma/migrations/20251125040000_add_material_optional_fields/migration.sql new file mode 100644 index 0000000..744be9c --- /dev/null +++ b/prisma/migrations/20251125040000_add_material_optional_fields/migration.sql @@ -0,0 +1,27 @@ +-- AlterTable +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "brandName" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "seriesName" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "skuCode" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "materialTexture" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "renderTime" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "imageResolution" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "colorMode" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "displayImages" TEXT NOT NULL DEFAULT ''; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "platformName" TEXT NOT NULL DEFAULT ''; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "previewUrls" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "metadata" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "templateId" TEXT; + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "materials_platformName_idx" ON "materials"("platformName"); + +-- AddForeignKey +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'materials_templateId_fkey' + ) THEN + ALTER TABLE "materials" ADD CONSTRAINT "materials_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "UploadTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE; + END IF; +END $$; diff --git a/prisma/migrations/20251126090000_add_field_pool/migration.sql b/prisma/migrations/20251126090000_add_field_pool/migration.sql new file mode 100644 index 0000000..13e2ac9 --- /dev/null +++ b/prisma/migrations/20251126090000_add_field_pool/migration.sql @@ -0,0 +1,38 @@ +-- CreateTable +CREATE TABLE "FieldDefinition" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "label" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'select', + "description" TEXT, + "source" TEXT NOT NULL DEFAULT 'custom', + "sourceTemplateIds" TEXT, + "isStandard" BOOLEAN NOT NULL DEFAULT false, + "isFilterEnabled" BOOLEAN NOT NULL DEFAULT false, + "filterDisplayName" TEXT, + "filterOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FieldDefinition_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FieldOption" ( + "id" TEXT NOT NULL, + "fieldId" TEXT NOT NULL, + "value" TEXT NOT NULL, + "label" TEXT NOT NULL, + "orderIndex" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FieldOption_pkey" PRIMARY KEY ("id"), + CONSTRAINT "FieldOption_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FieldDefinition"("id") ON DELETE CASCADE ON UPDATE NO ACTION +); + +-- CreateIndex +CREATE UNIQUE INDEX "FieldDefinition_key_key" ON "FieldDefinition"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "FieldOption_fieldId_value_key" ON "FieldOption"("fieldId", "value"); diff --git a/prisma/migrations/20251201090000_add_video_fields/migration.sql b/prisma/migrations/20251201090000_add_video_fields/migration.sql new file mode 100644 index 0000000..56d18ea --- /dev/null +++ b/prisma/migrations/20251201090000_add_video_fields/migration.sql @@ -0,0 +1,9 @@ +-- 为素材表增加视频相关字段,用于后续视频预览 / 转码能力 + +ALTER TABLE "materials" + ADD COLUMN "videoSourceUrl" TEXT, + ADD COLUMN "videoPreview540Url" TEXT, + ADD COLUMN "videoPreviewClipUrl" TEXT, + ADD COLUMN "videoCoverUrl" TEXT, + ADD COLUMN "videoDuration" INTEGER, + ADD COLUMN "videoProcessingStatus" TEXT; diff --git a/prisma/migrations/20251201133500_add_material_kind_to_templates/migration.sql b/prisma/migrations/20251201133500_add_material_kind_to_templates/migration.sql new file mode 100644 index 0000000..02f5d79 --- /dev/null +++ b/prisma/migrations/20251201133500_add_material_kind_to_templates/migration.sql @@ -0,0 +1,11 @@ +-- 为上传模板增加 materialKind 字段,用于区分图片/视频等素材大类 +ALTER TABLE "UploadTemplate" +ADD COLUMN IF NOT EXISTS "materialKind" TEXT NOT NULL DEFAULT 'IMAGE'; + +-- 依据 materialType 初步归类已有模板 +UPDATE "UploadTemplate" +SET "materialKind" = CASE + WHEN "materialType" = '视频' THEN 'VIDEO' + WHEN "materialType" = '其他' THEN 'OTHER' + ELSE 'IMAGE' +END; diff --git a/prisma/migrations/20251202000000_add_material_kind_to_materials/migration.sql b/prisma/migrations/20251202000000_add_material_kind_to_materials/migration.sql new file mode 100644 index 0000000..87d69df --- /dev/null +++ b/prisma/migrations/20251202000000_add_material_kind_to_materials/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "materialKind" TEXT NOT NULL DEFAULT 'IMAGE'; diff --git a/prisma/migrations/20251202091500_add_material_ownership_fields/migration.sql b/prisma/migrations/20251202091500_add_material_ownership_fields/migration.sql new file mode 100644 index 0000000..d28140d --- /dev/null +++ b/prisma/migrations/20251202091500_add_material_ownership_fields/migration.sql @@ -0,0 +1,18 @@ +-- Add ownership fields to materials for clean user/team attribution +-- ownerType: 素材归属类型(USER / TEAM),默认 TEAM +-- ownerId: 归属主体ID(团队空间ID或用户ID),默认 default-team +-- createdByUserId: 最初上传者用户ID +-- lastTransferredAt: 最近一次所有权移交时间 + +ALTER TABLE "materials" + ADD COLUMN IF NOT EXISTS "ownerType" TEXT NOT NULL DEFAULT 'TEAM', + ADD COLUMN IF NOT EXISTS "ownerId" TEXT NOT NULL DEFAULT 'default-team', + ADD COLUMN IF NOT EXISTS "createdByUserId" TEXT, + ADD COLUMN IF NOT EXISTS "lastTransferredAt" TIMESTAMPTZ; + +-- 索引:加速基于归属的查询 +CREATE INDEX IF NOT EXISTS "materials_ownerType_ownerId_idx" + ON "materials" ("ownerType", "ownerId"); + +CREATE INDEX IF NOT EXISTS "materials_createdByUserId_idx" + ON "materials" ("createdByUserId"); diff --git a/prisma/migrations/20251213073100_add_template_system_indexes/migration.sql b/prisma/migrations/20251213073100_add_template_system_indexes/migration.sql new file mode 100644 index 0000000..624f553 --- /dev/null +++ b/prisma/migrations/20251213073100_add_template_system_indexes/migration.sql @@ -0,0 +1,291 @@ +/* + Warnings: + + - You are about to drop the column `logoUrl` on the `system_settings` table. All the data in the column will be lost. + - A unique constraint covering the columns `[dingtalkUserId]` on the table `users` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[phone]` on the table `users` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "FieldDefinition" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "FieldOption" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "UploadField" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "UploadTemplate" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "categories" ADD COLUMN "options" TEXT; + +-- AlterTable +ALTER TABLE "materials" ADD COLUMN "deletedAt" TIMESTAMP(3), +ADD COLUMN "isPremium" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "zipFileName" SET DEFAULT '', +ALTER COLUMN "zipFilePath" SET DEFAULT '', +ALTER COLUMN "lastTransferredAt" SET DATA TYPE TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "scenes" ADD COLUMN "options" TEXT; + +-- AlterTable +ALTER TABLE "system_settings" DROP COLUMN "logoUrl", +ADD COLUMN "enableAiImageEdit" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "enableRegistration" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "enableTunnelService" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "enableUserActivation" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "headerLogoUrl" TEXT NOT NULL DEFAULT 'http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png', +ADD COLUMN "loginLogoUrl" TEXT NOT NULL DEFAULT 'http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png', +ADD COLUMN "premiumRatio" DOUBLE PRECISION NOT NULL DEFAULT 10.0, +ADD COLUMN "s3AccessKey" TEXT NOT NULL DEFAULT '', +ADD COLUMN "s3Bucket" TEXT NOT NULL DEFAULT '', +ADD COLUMN "s3Endpoint" TEXT NOT NULL DEFAULT '', +ADD COLUMN "s3PublicUrl" TEXT NOT NULL DEFAULT '', +ADD COLUMN "s3Region" TEXT NOT NULL DEFAULT 'us-east-1', +ADD COLUMN "s3SecretKey" TEXT NOT NULL DEFAULT ''; + +-- AlterTable +ALTER TABLE "users" ADD COLUMN "avatar" TEXT, +ADD COLUMN "department" TEXT NOT NULL DEFAULT '', +ADD COLUMN "dingtalkUserId" TEXT, +ADD COLUMN "isSuperAdmin" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "loginType" TEXT NOT NULL DEFAULT 'normal', +ADD COLUMN "managedDepartment" TEXT, +ADD COLUMN "parentId" TEXT, +ADD COLUMN "phone" TEXT, +ADD COLUMN "realName" TEXT; + +-- CreateTable +CREATE TABLE "FilterItem" ( + "id" TEXT NOT NULL, + "fieldKey" TEXT NOT NULL, + "label" TEXT NOT NULL, + "group" TEXT, + "orderIndex" INTEGER NOT NULL DEFAULT 0, + "multiSelect" BOOLEAN NOT NULL DEFAULT false, + "showOnHome" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "FilterItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "material_favorites" ( + "id" TEXT NOT NULL, + "materialId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "material_favorites_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "material_download_logs" ( + "id" TEXT NOT NULL, + "materialId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "downloadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "material_download_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "material_logs" ( + "id" TEXT NOT NULL, + "materialId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "changes" JSONB, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "material_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "platforms" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "options" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "platforms_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "departments" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "departments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "danmakus" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "username" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "danmakus_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "invitations" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "createdBy" TEXT NOT NULL, + "usedBy" TEXT, + "usedAt" TIMESTAMP(3), + "expiresAt" TIMESTAMP(3), + "maxUses" INTEGER NOT NULL DEFAULT 1, + "currentUses" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "invitations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "material_favorites_materialId_idx" ON "material_favorites"("materialId"); + +-- CreateIndex +CREATE INDEX "material_favorites_userId_idx" ON "material_favorites"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "material_favorites_materialId_userId_key" ON "material_favorites"("materialId", "userId"); + +-- CreateIndex +CREATE INDEX "material_download_logs_materialId_idx" ON "material_download_logs"("materialId"); + +-- CreateIndex +CREATE INDEX "material_download_logs_userId_idx" ON "material_download_logs"("userId"); + +-- CreateIndex +CREATE INDEX "material_download_logs_downloadedAt_idx" ON "material_download_logs"("downloadedAt"); + +-- CreateIndex +CREATE INDEX "material_download_logs_materialId_userId_downloadedAt_idx" ON "material_download_logs"("materialId", "userId", "downloadedAt"); + +-- CreateIndex +CREATE INDEX "material_logs_materialId_idx" ON "material_logs"("materialId"); + +-- CreateIndex +CREATE INDEX "material_logs_userId_idx" ON "material_logs"("userId"); + +-- CreateIndex +CREATE INDEX "material_logs_timestamp_idx" ON "material_logs"("timestamp"); + +-- CreateIndex +CREATE UNIQUE INDEX "platforms_name_key" ON "platforms"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "departments_name_key" ON "departments"("name"); + +-- CreateIndex +CREATE INDEX "danmakus_createdAt_idx" ON "danmakus"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "invitations_code_key" ON "invitations"("code"); + +-- CreateIndex +CREATE INDEX "invitations_code_idx" ON "invitations"("code"); + +-- CreateIndex +CREATE INDEX "invitations_createdBy_idx" ON "invitations"("createdBy"); + +-- CreateIndex +CREATE INDEX "FieldDefinition_source_idx" ON "FieldDefinition"("source"); + +-- CreateIndex +CREATE INDEX "FieldDefinition_isStandard_idx" ON "FieldDefinition"("isStandard"); + +-- CreateIndex +CREATE INDEX "FieldDefinition_isFilterEnabled_idx" ON "FieldDefinition"("isFilterEnabled"); + +-- CreateIndex +CREATE INDEX "FieldDefinition_filterOrder_idx" ON "FieldDefinition"("filterOrder"); + +-- CreateIndex +CREATE INDEX "FieldDefinition_isFilterEnabled_filterOrder_idx" ON "FieldDefinition"("isFilterEnabled", "filterOrder"); + +-- CreateIndex +CREATE INDEX "FieldOption_fieldId_idx" ON "FieldOption"("fieldId"); + +-- CreateIndex +CREATE INDEX "FieldOption_fieldId_orderIndex_idx" ON "FieldOption"("fieldId", "orderIndex"); + +-- CreateIndex +CREATE INDEX "UploadField_templateId_idx" ON "UploadField"("templateId"); + +-- CreateIndex +CREATE INDEX "UploadField_key_idx" ON "UploadField"("key"); + +-- CreateIndex +CREATE INDEX "UploadField_type_idx" ON "UploadField"("type"); + +-- CreateIndex +CREATE INDEX "UploadField_visibleOnHomeFilter_idx" ON "UploadField"("visibleOnHomeFilter"); + +-- CreateIndex +CREATE INDEX "UploadField_isSearchable_idx" ON "UploadField"("isSearchable"); + +-- CreateIndex +CREATE INDEX "UploadField_addToCategory_idx" ON "UploadField"("addToCategory"); + +-- CreateIndex +CREATE INDEX "UploadField_orderIndex_idx" ON "UploadField"("orderIndex"); + +-- CreateIndex +CREATE INDEX "UploadField_templateId_orderIndex_idx" ON "UploadField"("templateId", "orderIndex"); + +-- CreateIndex +CREATE INDEX "UploadTemplate_status_idx" ON "UploadTemplate"("status"); + +-- CreateIndex +CREATE INDEX "UploadTemplate_materialType_idx" ON "UploadTemplate"("materialType"); + +-- CreateIndex +CREATE INDEX "UploadTemplate_materialKind_idx" ON "UploadTemplate"("materialKind"); + +-- CreateIndex +CREATE INDEX "UploadTemplate_createdAt_idx" ON "UploadTemplate"("createdAt"); + +-- CreateIndex +CREATE INDEX "UploadTemplate_updatedAt_idx" ON "UploadTemplate"("updatedAt"); + +-- CreateIndex +CREATE INDEX "materials_deletedAt_idx" ON "materials"("deletedAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_dingtalkUserId_key" ON "users"("dingtalkUserId"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_phone_key" ON "users"("phone"); + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "material_favorites" ADD CONSTRAINT "material_favorites_materialId_fkey" FOREIGN KEY ("materialId") REFERENCES "materials"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "material_favorites" ADD CONSTRAINT "material_favorites_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "material_download_logs" ADD CONSTRAINT "material_download_logs_materialId_fkey" FOREIGN KEY ("materialId") REFERENCES "materials"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "material_download_logs" ADD CONSTRAINT "material_download_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "material_logs" ADD CONSTRAINT "material_logs_materialId_fkey" FOREIGN KEY ("materialId") REFERENCES "materials"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "material_logs" ADD CONSTRAINT "material_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251216000000_add_system_configs/migration.sql b/prisma/migrations/20251216000000_add_system_configs/migration.sql new file mode 100644 index 0000000..8d8a677 --- /dev/null +++ b/prisma/migrations/20251216000000_add_system_configs/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "system_configs" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "system_configs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "system_configs_key_key" ON "system_configs"("key"); diff --git a/prisma/migrations/20251217000000_add_audio_fields/migration.sql b/prisma/migrations/20251217000000_add_audio_fields/migration.sql new file mode 100644 index 0000000..e4ad6fd --- /dev/null +++ b/prisma/migrations/20251217000000_add_audio_fields/migration.sql @@ -0,0 +1,5 @@ +-- 添加音频素材相关字段 +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "audioDuration" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "audioCategory" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "audioScene" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "audioUrl" TEXT; diff --git a/prisma/migrations/20260524001000_local_default_logos/migration.sql b/prisma/migrations/20260524001000_local_default_logos/migration.sql new file mode 100644 index 0000000..2c3eb40 --- /dev/null +++ b/prisma/migrations/20260524001000_local_default_logos/migration.sql @@ -0,0 +1,14 @@ +-- 默认 Logo 不再指向旧对象存储,避免新安装继承旧环境地址 +ALTER TABLE "system_settings" +ALTER COLUMN "headerLogoUrl" SET DEFAULT '/logo/logo2.png'; + +ALTER TABLE "system_settings" +ALTER COLUMN "loginLogoUrl" SET DEFAULT '/logo/logo2.png'; + +UPDATE "system_settings" +SET "headerLogoUrl" = '/logo/logo2.png' +WHERE "headerLogoUrl" LIKE 'http://s3.1080p.hk/%'; + +UPDATE "system_settings" +SET "loginLogoUrl" = '/logo/logo2.png' +WHERE "loginLogoUrl" LIKE 'http://s3.1080p.hk/%'; diff --git a/prisma/migrations/20260525000000_empty_default_logos/migration.sql b/prisma/migrations/20260525000000_empty_default_logos/migration.sql new file mode 100644 index 0000000..ab1742e --- /dev/null +++ b/prisma/migrations/20260525000000_empty_default_logos/migration.sql @@ -0,0 +1,14 @@ +-- 默认 Logo 不再指向不存在的公共静态资源,避免新安装和旧数据继承死路径 +ALTER TABLE "system_settings" +ALTER COLUMN "headerLogoUrl" SET DEFAULT ''; + +ALTER TABLE "system_settings" +ALTER COLUMN "loginLogoUrl" SET DEFAULT ''; + +UPDATE "system_settings" +SET "headerLogoUrl" = '' +WHERE "headerLogoUrl" IN ('/logo/logo1.png', '/logo/logo2.png', '/logo/logo1', '/logo/logo2'); + +UPDATE "system_settings" +SET "loginLogoUrl" = '' +WHERE "loginLogoUrl" IN ('/logo/logo1.png', '/logo/logo2.png', '/logo/logo1', '/logo/logo2'); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 48a5637..1ac403c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -79,6 +79,7 @@ model MaterialShareToken { @@index([revokedAt]) @@map("material_share_tokens") } + model UploadTemplate { id String @id @default(uuid()) name String @@ -372,8 +373,8 @@ model SystemSettings { premiumRatio Float @default(10.0) updatedAt DateTime @updatedAt createdAt DateTime @default(now()) - headerLogoUrl String @default("http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png") - loginLogoUrl String @default("http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png") + headerLogoUrl String @default("") + loginLogoUrl String @default("") s3AccessKey String @default("") s3Bucket String @default("") s3Endpoint String @default("") diff --git a/prisma/seed.ts b/prisma/seed.ts index dbd1cb9..c1f6faf 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -17,113 +17,13 @@ import { PrismaClient } from '@prisma/client' import bcrypt from 'bcryptjs' +import { + BUILTIN_TEMPLATE_KEYS, + createBuiltinTemplate, +} from '../lib/upload-template-presets' const prisma = new PrismaClient() -// ============================================================================ -// 内置模板配置(与 lib/system-integrity.ts 保持一致) -// ============================================================================ - -const BUILTIN_TEMPLATE_KEYS = [ - '3dmax-material', - 'psd-material', - 'audio-material', - 'video-material', - 'model-material', - 'other-material', -] as const - -type BuiltinTemplateKey = (typeof BUILTIN_TEMPLATE_KEYS)[number] - -const BUILTIN_TEMPLATE_CONFIG: Record< - BuiltinTemplateKey, - { - name: string - description: string - materialType: string - materialKind: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER' - fields: Array<{ - key: string - label: string - type: string - required: boolean - layoutWidth: string - visibleOnHomeFilter: boolean - isSearchable: boolean - addToCategory?: boolean - }> - } -> = { - '3dmax-material': { - name: '3DMAX 素材模板', - description: '适用于 3D 模型类素材', - materialType: '3DMAX', - materialKind: 'IMAGE', - fields: [ - { key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - { key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'renderer', label: '渲染器', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true, addToCategory: true }, - ], - }, - 'psd-material': { - name: 'PSD 素材模板', - description: '针对平面设计 PSD 素材', - materialType: 'PSD', - materialKind: 'IMAGE', - fields: [ - { key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - { key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - ], - }, - 'audio-material': { - name: '音频素材模板', - description: '适用于音频文件素材,支持 MP3、WAV、FLAC、AAC 等格式', - materialType: '音频', - materialKind: 'AUDIO', - fields: [ - { key: 'productModel', label: '音频名称', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - { key: 'audioCategory', label: '音频分类', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'audioScene', label: '使用场景', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'authorName', label: '作者', type: 'text', required: false, layoutWidth: 'half', visibleOnHomeFilter: false, isSearchable: true }, - ], - }, - 'video-material': { - name: '视频素材模板', - description: '适用于视频文件素材', - materialType: '视频', - materialKind: 'VIDEO', - fields: [ - { key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - { key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - ], - }, - 'model-material': { - name: '模特素材模板', - description: '用于模特/人像素材', - materialType: '模特', - materialKind: 'IMAGE', - fields: [ - { key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - { key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - ], - }, - 'other-material': { - name: '其他素材模板', - description: '适用于其他类型的素材', - materialType: '其他', - materialKind: 'OTHER', - fields: [ - { key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - { key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - ], - }, -} - // ============================================================================ // 种子函数 // ============================================================================ @@ -134,8 +34,8 @@ async function seedBuiltinTemplates() { let skipped = 0 for (const key of BUILTIN_TEMPLATE_KEYS) { - const templateId = `builtin-${key}` - const config = BUILTIN_TEMPLATE_CONFIG[key] + const template = createBuiltinTemplate(key) + const templateId = template.id // 检查是否已存在 const existing = await prisma.uploadTemplate.findUnique({ @@ -153,17 +53,17 @@ async function seedBuiltinTemplates() { await tx.uploadTemplate.create({ data: { id: templateId, - name: config.name, - description: config.description, + name: template.name, + description: template.description, status: 'active', - materialType: config.materialType, - materialKind: config.materialKind, + materialType: template.materialType, + materialKind: template.materialKind, version: 1, }, }) - for (let i = 0; i < config.fields.length; i++) { - const field = config.fields[i] + for (let i = 0; i < template.fields.length; i++) { + const field = template.fields[i] await tx.uploadField.create({ data: { templateId, @@ -191,8 +91,8 @@ async function seedBuiltinTemplates() { async function seedDefaultAdmin() { console.log('正在检查默认管理员...') - const adminEmail = process.env.INIT_ADMIN_EMAIL || 'admin@flnxi.com' - const adminPassword = process.env.INIT_ADMIN_PASSWORD || '88888888' + const adminEmail = process.env.INIT_ADMIN_EMAIL || 'admin@local.flnxi' + const configuredPassword = process.env.INIT_ADMIN_PASSWORD?.trim() // 检查是否已有管理员 const existingAdmin = await prisma.user.findFirst({ @@ -204,8 +104,14 @@ async function seedDefaultAdmin() { return } + if (!configuredPassword) { + console.log(' 跳过: 未设置 INIT_ADMIN_PASSWORD,不在 seed 日志中生成或输出管理员密码') + console.log(' 请通过系统激活/初始化流程生成一次性管理员密码,或设置 INIT_ADMIN_PASSWORD 后重新执行 seed') + return + } + // 创建默认管理员 - const hashedPassword = await bcrypt.hash(adminPassword, 10) + const hashedPassword = await bcrypt.hash(configuredPassword, 10) const admin = await prisma.user.create({ data: { email: adminEmail, @@ -220,6 +126,29 @@ async function seedDefaultAdmin() { }) console.log(` 创建: 管理员 ${admin.email}`) + + await prisma.systemConfig.upsert({ + where: { key: 'initial_admin_state' }, + update: { + value: JSON.stringify({ + userId: admin.id, + email: admin.email, + mustChangePassword: true, + createdAt: new Date().toISOString(), + }), + }, + create: { + key: 'initial_admin_state', + value: JSON.stringify({ + userId: admin.id, + email: admin.email, + mustChangePassword: true, + createdAt: new Date().toISOString(), + }), + }, + }) + + console.log(' 初始管理员密码来源: 环境变量 INIT_ADMIN_PASSWORD(已写入哈希,不在日志中输出明文)') } async function seedDefaultData() { diff --git a/scripts/fix-missing-templates.ts b/scripts/fix-missing-templates.ts index 55f07ef..8f89a6c 100644 --- a/scripts/fix-missing-templates.ts +++ b/scripts/fix-missing-templates.ts @@ -9,74 +9,14 @@ */ import { PrismaClient } from '@prisma/client' +import { + BUILTIN_TEMPLATE_KEYS, + BUILTIN_TEMPLATE_NAMES, + createBuiltinTemplate, +} from '../lib/upload-template-presets' const prisma = new PrismaClient() -// 内置模板配置(直接定义,避免导入问题) -const BUILTIN_TEMPLATE_KEYS = [ - '3dmax-material', - 'psd-material', - 'audio-material', - 'video-material', - 'model-material', - 'other-material', -] as const - -const BUILTIN_TEMPLATE_NAMES: Record = { - '3dmax-material': '3DMAX 素材模板', - 'psd-material': 'PSD 素材模板', - 'audio-material': '音频素材模板', - 'video-material': '视频素材模板', - 'model-material': '模特素材模板', - 'other-material': '其他素材模板', -} - -const BUILTIN_TEMPLATE_DESCRIPTIONS: Record = { - '3dmax-material': '适用于 3D 模型类素材', - 'psd-material': '针对平面设计 PSD 素材', - 'audio-material': '适用于音频文件素材,支持 MP3、WAV、FLAC、AAC 等格式', - 'video-material': '适用于视频文件素材', - 'model-material': '用于模特/人像素材', - 'other-material': '适用于其他类型的素材', -} - -const BUILTIN_TEMPLATE_TYPES: Record = { - '3dmax-material': '3DMAX', - 'psd-material': 'PSD', - 'audio-material': '音频', - 'video-material': '视频', - 'model-material': '模特', - 'other-material': '其他', -} - -const BUILTIN_TEMPLATE_KINDS: Record = { - '3dmax-material': 'IMAGE', - 'psd-material': 'IMAGE', - 'audio-material': 'AUDIO', - 'video-material': 'VIDEO', - 'model-material': 'IMAGE', - 'other-material': 'OTHER', -} - -const BUILTIN_TEMPLATE_FIELDS: Record = { - '3dmax-material': ['productModel', 'categoryName', 'sceneName', 'renderer'], - 'psd-material': ['productModel', 'categoryName', 'sceneName'], - 'audio-material': ['productModel', 'audioCategory', 'audioScene', 'authorName'], - 'video-material': ['productModel', 'categoryName', 'sceneName'], - 'model-material': ['productModel', 'categoryName', 'sceneName'], - 'other-material': ['productModel', 'categoryName', 'sceneName'], -} - -const STANDARD_FIELDS: Record = { - productModel: { key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true }, - categoryName: { key: 'categoryName', label: '使用店铺', type: 'select', required: false, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - sceneName: { key: 'sceneName', label: '场景类别', type: 'select', required: false, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - renderer: { key: 'renderer', label: '渲染器', type: 'select', required: false, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true, addToCategory: true }, - audioCategory: { key: 'audioCategory', label: '音频分类', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - audioScene: { key: 'audioScene', label: '使用场景', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - authorName: { key: 'authorName', label: '作者', type: 'text', required: false, layoutWidth: 'half', visibleOnHomeFilter: false, isSearchable: true }, -} - async function fixMissingTemplates() { console.log('开始修复缺失的模板记录...\n') @@ -112,24 +52,21 @@ async function fixMissingTemplates() { console.log('\n步骤 2: 创建缺失的内置模板...') for (const key of missingKeys) { - const templateId = `builtin-${key}` - const name = BUILTIN_TEMPLATE_NAMES[key] - const description = BUILTIN_TEMPLATE_DESCRIPTIONS[key] - const materialType = BUILTIN_TEMPLATE_TYPES[key] - const materialKind = BUILTIN_TEMPLATE_KINDS[key] - const fieldKeys = BUILTIN_TEMPLATE_FIELDS[key] || [] + const presetTemplate = createBuiltinTemplate(key) + const templateId = presetTemplate.id + const name = presetTemplate.name console.log(`\n创建模板: ${name} (${templateId})`) // 创建模板记录 - const template = await prisma.uploadTemplate.create({ + const createdTemplate = await prisma.uploadTemplate.create({ data: { id: templateId, name, - description, + description: presetTemplate.description, status: 'active', - materialType, - materialKind, + materialType: presetTemplate.materialType, + materialKind: presetTemplate.materialKind, version: 1, }, }) @@ -137,50 +74,30 @@ async function fixMissingTemplates() { console.log(` ✓ 模板记录已创建`) // 创建字段记录 - const requireRenderer = key === '3dmax-material' - const fieldOverrides: Record> = { - productModel: { required: true }, - categoryName: { required: true }, - sceneName: { required: true }, - } - - if (requireRenderer) { - fieldOverrides.renderer = { required: true } - } - - for (let i = 0; i < fieldKeys.length; i++) { - const fieldKey = fieldKeys[i] - const fieldConfig = STANDARD_FIELDS[fieldKey] - - if (!fieldConfig) { - console.warn(` ⚠ 字段配置不存在: ${fieldKey}`) - continue - } - - const override = fieldOverrides[fieldKey] || {} - + for (let i = 0; i < presetTemplate.fields.length; i++) { + const field = presetTemplate.fields[i] await prisma.uploadField.create({ data: { - templateId: template.id, - key: fieldConfig.key, - label: fieldConfig.label, - type: fieldConfig.type, - required: override.required ?? fieldConfig.required, + templateId: createdTemplate.id, + key: field.key, + label: field.label, + type: field.type, + required: field.required, orderIndex: i, - layoutWidth: fieldConfig.layoutWidth, + layoutWidth: field.layoutWidth, validations: null, optionsSource: null, - optionsStatic: fieldConfig.optionsStatic || null, + optionsStatic: field.optionsStatic || null, allowedExtensions: null, - visibleOnHomeFilter: fieldConfig.visibleOnHomeFilter, - isSearchable: fieldConfig.isSearchable, - addToCategory: fieldConfig.addToCategory || false, + visibleOnHomeFilter: field.visibleOnHomeFilter, + isSearchable: field.isSearchable, + addToCategory: field.addToCategory || false, conditions: null, }, }) } - console.log(` ✓ ${fieldKeys.length} 个字段已创建`) + console.log(` ✓ ${presetTemplate.fields.length} 个字段已创建`) } // 3. 验证修复结果 diff --git a/scripts/update-audio-template-fields.ts b/scripts/update-audio-template-fields.ts index ccf3396..a879b99 100644 --- a/scripts/update-audio-template-fields.ts +++ b/scripts/update-audio-template-fields.ts @@ -20,7 +20,7 @@ const AUDIO_TEMPLATE_ID = 'builtin-audio-material' const AUDIO_FIELDS = [ { key: 'productModel', label: '音频名称', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true, addToCategory: false }, { key: 'audioCategory', label: '音频分类', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, - { key: 'audioScene', label: '使用场景', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, + { key: 'audioScene', label: '音频使用场景', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true }, { key: 'authorName', label: '作者', type: 'text', required: false, layoutWidth: 'half', visibleOnHomeFilter: false, isSearchable: true, addToCategory: false }, ]