feat: 数据库 schema 扩展(工具字段/收藏/Newsletter/联系表单)
- tool 表新增 pricing(free/freemium/paid)、featured、viewCount 字段 - 新增 favorite、newsletter_subscription、contact_message 表 - auth 表拆分至 schema/auth.ts,schema.ts 改为纯汇出(表定义未变) - seed-tools.ts 改为直接导入项目 schema,pricing/featured 从标签推导 - 删除 seed-tools-data.sql,种子数据以 seed-tools.ts 为唯一来源(部署本项目必有 Node 环境,pnpm seed:tools 始终可用) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8976caaaad
commit
7fd9338f80
@@ -0,0 +1,40 @@
|
||||
CREATE TABLE "favorite" (
|
||||
"user_id" text NOT NULL,
|
||||
"tool_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "favorite_user_id_tool_id_pk" PRIMARY KEY("user_id","tool_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "contact_message" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"company" text,
|
||||
"message" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "newsletter_subscription" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"user_id" text,
|
||||
"status" text DEFAULT 'active' NOT NULL,
|
||||
"unsubscribe_token" text NOT NULL,
|
||||
"subscribed_at" timestamp DEFAULT now() NOT NULL,
|
||||
"unsubscribed_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "newsletter_subscription_email_unique" UNIQUE("email"),
|
||||
CONSTRAINT "newsletter_subscription_unsubscribe_token_unique" UNIQUE("unsubscribe_token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "tool" ADD COLUMN "pricing" text;--> statement-breakpoint
|
||||
ALTER TABLE "tool" ADD COLUMN "featured" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "tool" ADD COLUMN "view_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "favorite" ADD CONSTRAINT "favorite_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "favorite" ADD CONSTRAINT "favorite_tool_id_tool_id_fk" FOREIGN KEY ("tool_id") REFERENCES "public"."tool"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "newsletter_subscription" ADD CONSTRAINT "newsletter_subscription_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "favorite_tool_id_idx" ON "favorite" USING btree ("tool_id");--> statement-breakpoint
|
||||
CREATE INDEX "newsletter_subscription_status_idx" ON "newsletter_subscription" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "tool_featured_idx" ON "tool" USING btree ("featured");--> statement-breakpoint
|
||||
CREATE INDEX "tool_pricing_idx" ON "tool" USING btree ("pricing");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,13 @@
|
||||
"when": 1767366953512,
|
||||
"tag": "0000_many_tomas",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1785552096235,
|
||||
"tag": "0001_lumpy_proudstar",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-67
@@ -1,70 +1,8 @@
|
||||
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
// user role: 'admin' | 'user'
|
||||
role: text("role").default("user").notNull(),
|
||||
// ban status
|
||||
banned: boolean("banned").default(false).notNull(),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
// ========== 认证模块 ==========
|
||||
export * from "./schema/auth";
|
||||
|
||||
// ========== 工具模块 ==========
|
||||
export * from "./schema/tool";
|
||||
|
||||
// ========== 营销模块 (Newsletter / 联系表单) ==========
|
||||
export * from "./schema/marketing";
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
// user role: 'admin' | 'user'
|
||||
role: text("role").default("user").notNull(),
|
||||
// ban status
|
||||
banned: boolean("banned").default(false).notNull(),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { pgTable, text, timestamp, index } from "drizzle-orm/pg-core";
|
||||
import { user } from "./auth";
|
||||
|
||||
// ========== Newsletter 订阅表 ==========
|
||||
export const newsletterSubscription = pgTable("newsletter_subscription", {
|
||||
id: text("id").primaryKey(),
|
||||
email: text("email").notNull().unique(),
|
||||
// 已登录用户订阅时关联,游客订阅为空
|
||||
userId: text("user_id").references(() => user.id, { onDelete: "set null" }),
|
||||
// 状态: active | unsubscribed
|
||||
status: text("status").notNull().default("active"),
|
||||
unsubscribeToken: text("unsubscribe_token").notNull().unique(),
|
||||
subscribedAt: timestamp("subscribed_at").defaultNow().notNull(),
|
||||
unsubscribedAt: timestamp("unsubscribed_at"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
}, (table) => [
|
||||
index("newsletter_subscription_status_idx").on(table.status),
|
||||
]);
|
||||
|
||||
// ========== 联系表单留言表 ==========
|
||||
export const contactMessage = pgTable("contact_message", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
company: text("company"),
|
||||
message: text("message").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { pgTable, text, timestamp, integer, primaryKey, index } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, timestamp, integer, boolean, primaryKey, index } from "drizzle-orm/pg-core";
|
||||
import { user } from "./auth";
|
||||
|
||||
// ========== 工具表 ==========
|
||||
export const tool = pgTable("tool", {
|
||||
@@ -17,6 +18,15 @@ export const tool = pgTable("tool", {
|
||||
nameZh: text("name_zh"),
|
||||
descriptionZh: text("description_zh"),
|
||||
|
||||
// 定价: free | freemium | paid
|
||||
pricing: text("pricing"),
|
||||
|
||||
// 首页精选标记
|
||||
featured: boolean("featured").notNull().default(false),
|
||||
|
||||
// 浏览量
|
||||
viewCount: integer("view_count").notNull().default(0),
|
||||
|
||||
// 状态: draft | published
|
||||
status: text("status").notNull().default("draft"),
|
||||
|
||||
@@ -28,6 +38,8 @@ export const tool = pgTable("tool", {
|
||||
}, (table) => [
|
||||
index("tool_slug_idx").on(table.slug),
|
||||
index("tool_status_idx").on(table.status),
|
||||
index("tool_featured_idx").on(table.featured),
|
||||
index("tool_pricing_idx").on(table.pricing),
|
||||
]);
|
||||
|
||||
// ========== 分类表 ==========
|
||||
@@ -89,6 +101,20 @@ export const toolCategory = pgTable("tool_category", {
|
||||
index("tool_category_category_id_idx").on(table.categoryId),
|
||||
]);
|
||||
|
||||
// ========== 用户收藏表 ==========
|
||||
export const favorite = pgTable("favorite", {
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
toolId: text("tool_id")
|
||||
.notNull()
|
||||
.references(() => tool.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
primaryKey({ columns: [table.userId, table.toolId] }),
|
||||
index("favorite_tool_id_idx").on(table.toolId),
|
||||
]);
|
||||
|
||||
// ========== 工具-标签关联表 ==========
|
||||
export const toolTag = pgTable("tool_tag", {
|
||||
toolId: text("tool_id")
|
||||
|
||||
Reference in New Issue
Block a user