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:
CoolChange.AI
2026-07-31 20:35:17 -07:00
co-authored by Claude Fable 5
parent 8976caaaad
commit 7fd9338f80
9 changed files with 1356 additions and 605 deletions
+22 -68
View File
@@ -16,76 +16,11 @@ import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import dotenv from "dotenv";
import { resolve } from "path";
import { pgTable, text, timestamp, integer, primaryKey, index } from "drizzle-orm/pg-core";
import { tool, category, tag, toolCategory, toolTag } from "../src/lib/db/schema";
// 加载环境变量
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
// ========== Schema 定义 ==========
const tool = pgTable("tool", {
id: text("id").primaryKey(),
slug: text("slug").notNull().unique(),
domain: text("domain"),
websiteUrl: text("website_url"),
coverImageUrl: text("cover_image_url"),
logoUrl: text("logo_url"),
nameEn: text("name_en").notNull(),
descriptionEn: text("description_en"),
nameZh: text("name_zh"),
descriptionZh: text("description_zh"),
status: text("status").notNull().default("draft"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
}, (table) => [
index("tool_slug_idx").on(table.slug),
index("tool_status_idx").on(table.status),
]);
const category = pgTable("category", {
id: text("id").primaryKey(),
slug: text("slug").notNull().unique(),
icon: text("icon"),
sortOrder: integer("sort_order").notNull().default(0),
nameEn: text("name_en").notNull(),
descriptionEn: text("description_en"),
nameZh: text("name_zh"),
descriptionZh: text("description_zh"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
}, (table) => [
index("category_slug_idx").on(table.slug),
index("category_sort_order_idx").on(table.sortOrder),
]);
const tag = pgTable("tag", {
id: text("id").primaryKey(),
slug: text("slug").notNull().unique(),
nameEn: text("name_en").notNull(),
nameZh: text("name_zh"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
}, (table) => [
index("tag_slug_idx").on(table.slug),
]);
const toolCategory = pgTable("tool_category", {
toolId: text("tool_id").notNull().references(() => tool.id, { onDelete: "cascade" }),
categoryId: text("category_id").notNull().references(() => category.id, { onDelete: "cascade" }),
}, (table) => [
primaryKey({ columns: [table.toolId, table.categoryId] }),
index("tool_category_tool_id_idx").on(table.toolId),
index("tool_category_category_id_idx").on(table.categoryId),
]);
const toolTag = pgTable("tool_tag", {
toolId: text("tool_id").notNull().references(() => tool.id, { onDelete: "cascade" }),
tagId: text("tag_id").notNull().references(() => tag.id, { onDelete: "cascade" }),
}, (table) => [
primaryKey({ columns: [table.toolId, table.tagId] }),
index("tool_tag_tool_id_idx").on(table.toolId),
index("tool_tag_tag_id_idx").on(table.tagId),
]);
// ========== 种子数据 ==========
const categoriesData = [
@@ -581,10 +516,29 @@ async function seedTools() {
}
console.log("✅ 标签数据导入完成\n");
// 3. 导入工具
// 3. 导入工具(pricing 从定价标签推导,featured 从"编辑推荐"标签推导)
console.log("🔧 导入工具数据 (50 个)...");
const pricingTagMap: Record<string, string> = {
tag_free: "free",
tag_freemium: "freemium",
tag_paid: "paid",
};
const pricingByTool = new Map<string, string>();
const featuredToolIds = new Set<string>();
for (const tt of toolTagData) {
const pricing = pricingTagMap[tt.tagId];
if (pricing) pricingByTool.set(tt.toolId, pricing);
if (tt.tagId === "tag_editors_choice") featuredToolIds.add(tt.toolId);
}
for (const t of toolsData) {
await db.insert(tool).values(t).onConflictDoNothing();
await db
.insert(tool)
.values({
...t,
pricing: pricingByTool.get(t.id) ?? null,
featured: featuredToolIds.has(t.id),
})
.onConflictDoNothing();
}
console.log("✅ 工具数据导入完成\n");