This commit is contained in:
dlandy
2026-03-27 10:18:26 +08:00
commit 40be11adbf
116 changed files with 26138 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
-- CreateTable
CREATE TABLE "tools" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"category_id" TEXT NOT NULL,
"description" TEXT NOT NULL,
"rating" REAL NOT NULL DEFAULT 0,
"download_count" INTEGER NOT NULL DEFAULT 0,
"open_count" INTEGER NOT NULL DEFAULT 0,
"access_mode" TEXT NOT NULL DEFAULT 'download',
"open_url" TEXT,
"open_in_new_tab" BOOLEAN NOT NULL DEFAULT true,
"latest_artifact_id" TEXT,
"status" TEXT NOT NULL DEFAULT 'draft',
"updated_at" TEXT NOT NULL,
"is_deleted" BOOLEAN NOT NULL DEFAULT false,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"modified_at" DATETIME NOT NULL,
CONSTRAINT "tools_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "tools_latest_artifact_id_fkey" FOREIGN KEY ("latest_artifact_id") REFERENCES "tool_artifacts" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "tool_artifacts" (
"id" TEXT NOT NULL PRIMARY KEY,
"tool_id" TEXT NOT NULL,
"version" TEXT NOT NULL,
"file_name" TEXT NOT NULL,
"file_size_bytes" INTEGER NOT NULL,
"sha256" TEXT NOT NULL,
"mime_type" TEXT,
"gitlab_project_id" INTEGER NOT NULL,
"gitlab_package_name" TEXT NOT NULL,
"gitlab_package_version" TEXT NOT NULL,
"gitlab_file_path" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"release_notes" TEXT,
"uploaded_by" TEXT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tool_artifacts_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "tools" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "tool_artifacts_uploaded_by_fkey" FOREIGN KEY ("uploaded_by") REFERENCES "admin_users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "categories" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 100,
"is_deleted" BOOLEAN NOT NULL DEFAULT false
);
-- CreateTable
CREATE TABLE "tags" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"is_deleted" BOOLEAN NOT NULL DEFAULT false
);
-- CreateTable
CREATE TABLE "tool_tags" (
"tool_id" TEXT NOT NULL,
"tag_id" TEXT NOT NULL,
PRIMARY KEY ("tool_id", "tag_id"),
CONSTRAINT "tool_tags_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "tools" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "tool_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "tool_features" (
"id" TEXT NOT NULL PRIMARY KEY,
"tool_id" TEXT NOT NULL,
"feature_text" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 100,
CONSTRAINT "tool_features_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "tools" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "hot_keywords" (
"id" TEXT NOT NULL PRIMARY KEY,
"keyword" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 100,
"is_active" BOOLEAN NOT NULL DEFAULT true
);
-- CreateTable
CREATE TABLE "download_tickets" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"ticket" TEXT NOT NULL,
"tool_id" TEXT NOT NULL,
"artifact_id" TEXT NOT NULL,
"channel" TEXT,
"client_version" TEXT,
"request_ip" TEXT,
"expires_at" DATETIME NOT NULL,
"consumed_at" DATETIME,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "download_tickets_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "tools" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "download_tickets_artifact_id_fkey" FOREIGN KEY ("artifact_id") REFERENCES "tool_artifacts" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "download_records" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"tool_id" TEXT NOT NULL,
"artifact_id" TEXT NOT NULL,
"ticket" TEXT,
"downloaded_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"client_ip" TEXT,
"user_agent" TEXT,
"channel" TEXT,
"client_version" TEXT,
"status" TEXT NOT NULL DEFAULT 'success',
"error_message" TEXT,
CONSTRAINT "download_records_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "tools" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "download_records_artifact_id_fkey" FOREIGN KEY ("artifact_id") REFERENCES "tool_artifacts" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "open_records" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"tool_id" TEXT NOT NULL,
"opened_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"client_ip" TEXT,
"user_agent" TEXT,
"channel" TEXT,
"client_version" TEXT,
"referer" TEXT,
CONSTRAINT "open_records_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "tools" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "admin_users" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"display_name" TEXT,
"status" TEXT NOT NULL DEFAULT 'active',
"last_login_at" DATETIME,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"modified_at" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "admin_audit_logs" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"admin_user_id" TEXT,
"action" TEXT NOT NULL,
"resource_type" TEXT NOT NULL,
"resource_id" TEXT,
"request_method" TEXT NOT NULL,
"request_path" TEXT NOT NULL,
"request_body" TEXT,
"ip" TEXT,
"user_agent" TEXT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_audit_logs_admin_user_id_fkey" FOREIGN KEY ("admin_user_id") REFERENCES "admin_users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "tools_slug_key" ON "tools"("slug");
-- CreateIndex
CREATE INDEX "idx_tools_category_id" ON "tools"("category_id");
-- CreateIndex
CREATE INDEX "idx_tools_status" ON "tools"("status");
-- CreateIndex
CREATE INDEX "idx_tools_access_mode" ON "tools"("access_mode");
-- CreateIndex
CREATE INDEX "idx_tools_download_count" ON "tools"("download_count");
-- CreateIndex
CREATE INDEX "idx_tools_open_count" ON "tools"("open_count");
-- CreateIndex
CREATE INDEX "idx_tools_updated_at" ON "tools"("updated_at");
-- CreateIndex
CREATE INDEX "idx_tools_rating" ON "tools"("rating");
-- CreateIndex
CREATE INDEX "idx_tools_name" ON "tools"("name");
-- CreateIndex
CREATE INDEX "idx_artifact_tool_id" ON "tool_artifacts"("tool_id");
-- CreateIndex
CREATE INDEX "idx_artifact_status" ON "tool_artifacts"("status");
-- CreateIndex
CREATE UNIQUE INDEX "uk_tool_version" ON "tool_artifacts"("tool_id", "version");
-- CreateIndex
CREATE UNIQUE INDEX "categories_name_key" ON "categories"("name");
-- CreateIndex
CREATE UNIQUE INDEX "tags_name_key" ON "tags"("name");
-- CreateIndex
CREATE INDEX "idx_tool_feature_tool_id" ON "tool_features"("tool_id");
-- CreateIndex
CREATE UNIQUE INDEX "hot_keywords_keyword_key" ON "hot_keywords"("keyword");
-- CreateIndex
CREATE UNIQUE INDEX "download_tickets_ticket_key" ON "download_tickets"("ticket");
-- CreateIndex
CREATE INDEX "idx_download_tickets_tool_id" ON "download_tickets"("tool_id");
-- CreateIndex
CREATE INDEX "idx_download_tickets_expires_at" ON "download_tickets"("expires_at");
-- CreateIndex
CREATE INDEX "idx_download_records_tool_id" ON "download_records"("tool_id");
-- CreateIndex
CREATE INDEX "idx_download_records_artifact_id" ON "download_records"("artifact_id");
-- CreateIndex
CREATE INDEX "idx_open_records_tool_id" ON "open_records"("tool_id");
-- CreateIndex
CREATE UNIQUE INDEX "admin_users_username_key" ON "admin_users"("username");
-- CreateIndex
CREATE INDEX "idx_admin_audit_logs_user_id" ON "admin_audit_logs"("admin_user_id");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

246
server/prisma/schema.prisma Normal file
View File

@@ -0,0 +1,246 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
enum AccessMode {
web
download
}
enum ToolStatus {
draft
published
archived
}
enum ArtifactStatus {
active
deprecated
deleted
}
enum DownloadRecordStatus {
success
failed
}
enum AdminUserStatus {
active
disabled
}
model Tool {
id String @id
name String
slug String @unique
categoryId String @map("category_id")
description String
rating Float @default(0)
downloadCount Int @default(0) @map("download_count")
openCount Int @default(0) @map("open_count")
accessMode AccessMode @default(download) @map("access_mode")
openUrl String? @map("open_url")
openInNewTab Boolean @default(true) @map("open_in_new_tab")
latestArtifactId String? @map("latest_artifact_id")
status ToolStatus @default(draft)
updatedAt String @map("updated_at")
isDeleted Boolean @default(false) @map("is_deleted")
createdAt DateTime @default(now()) @map("created_at")
modifiedAt DateTime @updatedAt @map("modified_at")
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
latestArtifact ToolArtifact? @relation("ToolLatestArtifact", fields: [latestArtifactId], references: [id], onDelete: SetNull)
artifacts ToolArtifact[] @relation("ToolArtifacts")
tags ToolTag[]
features ToolFeature[]
downloadTickets DownloadTicket[]
downloadRecords DownloadRecord[]
openRecords OpenRecord[]
@@index([categoryId], map: "idx_tools_category_id")
@@index([status], map: "idx_tools_status")
@@index([accessMode], map: "idx_tools_access_mode")
@@index([downloadCount], map: "idx_tools_download_count")
@@index([openCount], map: "idx_tools_open_count")
@@index([updatedAt], map: "idx_tools_updated_at")
@@index([rating], map: "idx_tools_rating")
@@index([name], map: "idx_tools_name")
@@map("tools")
}
model ToolArtifact {
id String @id
toolId String @map("tool_id")
version String
fileName String @map("file_name")
fileSizeBytes Int @map("file_size_bytes")
sha256 String
mimeType String? @map("mime_type")
gitlabProjectId Int @map("gitlab_project_id")
gitlabPackageName String @map("gitlab_package_name")
gitlabPackageVersion String @map("gitlab_package_version")
gitlabFilePath String @map("gitlab_file_path")
status ArtifactStatus @default(active)
releaseNotes String? @map("release_notes")
uploadedBy String? @map("uploaded_by")
createdAt DateTime @default(now()) @map("created_at")
tool Tool @relation("ToolArtifacts", fields: [toolId], references: [id], onDelete: Restrict)
latestForTool Tool[] @relation("ToolLatestArtifact")
uploader AdminUser? @relation(fields: [uploadedBy], references: [id], onDelete: SetNull)
downloadTickets DownloadTicket[]
downloadRecords DownloadRecord[]
@@unique([toolId, version], map: "uk_tool_version")
@@index([toolId], map: "idx_artifact_tool_id")
@@index([status], map: "idx_artifact_status")
@@map("tool_artifacts")
}
model Category {
id String @id
name String @unique
sortOrder Int @default(100) @map("sort_order")
isDeleted Boolean @default(false) @map("is_deleted")
tools Tool[]
@@map("categories")
}
model Tag {
id String @id
name String @unique
isDeleted Boolean @default(false) @map("is_deleted")
tools ToolTag[]
@@map("tags")
}
model ToolTag {
toolId String @map("tool_id")
tagId String @map("tag_id")
tool Tool @relation(fields: [toolId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([toolId, tagId])
@@map("tool_tags")
}
model ToolFeature {
id String @id
toolId String @map("tool_id")
featureText String @map("feature_text")
sortOrder Int @default(100) @map("sort_order")
tool Tool @relation(fields: [toolId], references: [id], onDelete: Cascade)
@@index([toolId], map: "idx_tool_feature_tool_id")
@@map("tool_features")
}
model HotKeyword {
id String @id
keyword String @unique
sortOrder Int @default(100) @map("sort_order")
isActive Boolean @default(true) @map("is_active")
@@map("hot_keywords")
}
model DownloadTicket {
id Int @id @default(autoincrement())
ticket String @unique
toolId String @map("tool_id")
artifactId String @map("artifact_id")
channel String?
clientVersion String? @map("client_version")
requestIp String? @map("request_ip")
expiresAt DateTime @map("expires_at")
consumedAt DateTime? @map("consumed_at")
createdAt DateTime @default(now()) @map("created_at")
tool Tool @relation(fields: [toolId], references: [id], onDelete: Restrict)
artifact ToolArtifact @relation(fields: [artifactId], references: [id], onDelete: Restrict)
@@index([toolId], map: "idx_download_tickets_tool_id")
@@index([expiresAt], map: "idx_download_tickets_expires_at")
@@map("download_tickets")
}
model DownloadRecord {
id Int @id @default(autoincrement())
toolId String @map("tool_id")
artifactId String @map("artifact_id")
ticket String?
downloadedAt DateTime @default(now()) @map("downloaded_at")
clientIp String? @map("client_ip")
userAgent String? @map("user_agent")
channel String?
clientVersion String? @map("client_version")
status DownloadRecordStatus @default(success)
errorMessage String? @map("error_message")
tool Tool @relation(fields: [toolId], references: [id], onDelete: Restrict)
artifact ToolArtifact @relation(fields: [artifactId], references: [id], onDelete: Restrict)
@@index([toolId], map: "idx_download_records_tool_id")
@@index([artifactId], map: "idx_download_records_artifact_id")
@@map("download_records")
}
model OpenRecord {
id Int @id @default(autoincrement())
toolId String @map("tool_id")
openedAt DateTime @default(now()) @map("opened_at")
clientIp String? @map("client_ip")
userAgent String? @map("user_agent")
channel String?
clientVersion String? @map("client_version")
referer String?
tool Tool @relation(fields: [toolId], references: [id], onDelete: Restrict)
@@index([toolId], map: "idx_open_records_tool_id")
@@map("open_records")
}
model AdminUser {
id String @id
username String @unique
passwordHash String @map("password_hash")
displayName String? @map("display_name")
status AdminUserStatus @default(active)
lastLoginAt DateTime? @map("last_login_at")
createdAt DateTime @default(now()) @map("created_at")
modifiedAt DateTime @updatedAt @map("modified_at")
artifacts ToolArtifact[]
auditLogs AdminAuditLog[]
@@map("admin_users")
}
model AdminAuditLog {
id Int @id @default(autoincrement())
adminUserId String? @map("admin_user_id")
action String
resourceType String @map("resource_type")
resourceId String? @map("resource_id")
requestMethod String @map("request_method")
requestPath String @map("request_path")
requestBody String? @map("request_body")
ip String?
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
adminUser AdminUser? @relation(fields: [adminUserId], references: [id], onDelete: SetNull)
@@index([adminUserId], map: "idx_admin_audit_logs_user_id")
@@map("admin_audit_logs")
}

152
server/prisma/seed.ts Normal file
View File

@@ -0,0 +1,152 @@
import argon2 from 'argon2';
import { PrismaClient, AccessMode, ToolStatus, ArtifactStatus, AdminUserStatus } from '@prisma/client';
const prisma = new PrismaClient();
function todayDateString(): string {
return new Date().toISOString().slice(0, 10);
}
async function main() {
const nowDate = todayDateString();
await prisma.$transaction([
prisma.toolTag.deleteMany(),
prisma.toolFeature.deleteMany(),
prisma.downloadRecord.deleteMany(),
prisma.downloadTicket.deleteMany(),
prisma.openRecord.deleteMany(),
prisma.adminAuditLog.deleteMany(),
prisma.toolArtifact.deleteMany(),
prisma.tool.deleteMany(),
prisma.tag.deleteMany(),
prisma.category.deleteMany(),
prisma.hotKeyword.deleteMany(),
prisma.adminUser.deleteMany(),
]);
await prisma.category.createMany({
data: [
{ id: 'cat_ai', name: 'AI', sortOrder: 10 },
{ id: 'cat_dev', name: 'Developer', sortOrder: 20 },
{ id: 'cat_ops', name: 'Operations', sortOrder: 30 },
],
});
await prisma.tag.createMany({
data: [
{ id: 'tag_hot', name: 'Hot' },
{ id: 'tag_free', name: 'Free' },
{ id: 'tag_official', name: 'Official' },
],
});
const webToolId = 'tool_web_001';
const downloadToolId = 'tool_dl_001';
const artifactId = 'art_001';
await prisma.tool.create({
data: {
id: webToolId,
name: 'OpenAI Playground',
slug: 'openai-playground',
categoryId: 'cat_ai',
description: 'OpenAI web playground for prompt testing.',
rating: 4.8,
downloadCount: 0,
openCount: 128,
accessMode: AccessMode.web,
openUrl: 'https://platform.openai.com/playground',
openInNewTab: true,
status: ToolStatus.published,
updatedAt: nowDate,
features: {
createMany: {
data: [
{ id: 'feat_web_001', featureText: 'Prompt debugging', sortOrder: 10 },
{ id: 'feat_web_002', featureText: 'Model parameter tuning', sortOrder: 20 },
],
},
},
tags: {
create: [{ tagId: 'tag_hot' }, { tagId: 'tag_official' }],
},
},
});
await prisma.tool.create({
data: {
id: downloadToolId,
name: 'ToolsShow Desktop',
slug: 'toolsshow-desktop',
categoryId: 'cat_dev',
description: 'Desktop bundle for local workflows.',
rating: 4.6,
downloadCount: 52,
openCount: 0,
accessMode: AccessMode.download,
status: ToolStatus.published,
updatedAt: nowDate,
features: {
createMany: {
data: [
{ id: 'feat_dl_001', featureText: 'Offline usage', sortOrder: 10 },
{ id: 'feat_dl_002', featureText: 'Bundled plugins', sortOrder: 20 },
],
},
},
tags: {
create: [{ tagId: 'tag_free' }],
},
artifacts: {
create: {
id: artifactId,
version: '1.0.0',
fileName: 'toolsshow-desktop-1.0.0.zip',
fileSizeBytes: 12_345_678,
sha256: 'sample-sha256-not-real',
mimeType: 'application/zip',
gitlabProjectId: 0,
gitlabPackageName: 'toolsshow/toolsshow-desktop',
gitlabPackageVersion: '1.0.0',
gitlabFilePath: 'storage/toolsshow-desktop-1.0.0.zip',
status: ArtifactStatus.active,
releaseNotes: 'Initial release',
},
},
},
});
await prisma.tool.update({
where: { id: downloadToolId },
data: { latestArtifactId: artifactId },
});
await prisma.hotKeyword.createMany({
data: [
{ id: 'kw_001', keyword: 'agent', sortOrder: 10, isActive: true },
{ id: 'kw_002', keyword: 'automation', sortOrder: 20, isActive: true },
{ id: 'kw_003', keyword: 'open-source', sortOrder: 30, isActive: true },
],
});
const passwordHash = await argon2.hash('admin123456', { type: argon2.argon2id });
await prisma.adminUser.create({
data: {
id: 'u_admin_001',
username: 'admin',
passwordHash,
displayName: 'System Admin',
status: AdminUserStatus.active,
},
});
}
main()
.catch(async (error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});