v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 数据库客户端:Bun.sql + Drizzle(bun-sql 适配器)。
|
||||
* 用法:import { db } from "~/db";
|
||||
*
|
||||
* 注意:db 采用惰性 Proxy。客户端组件会经由 server 函数模块间接引入本
|
||||
* 文件,顶层 `new SQL()` 在浏览器端无法解析 Bun 内置模块而崩溃;因此只在
|
||||
* 服务端首次访问时才初始化连接池(见 initDb / ensureDb)。
|
||||
*/
|
||||
import type { BunSQLDatabase } from "drizzle-orm/bun-sql";
|
||||
import * as schema from "./schema";
|
||||
|
||||
/** # 成员变量 惰性初始化的 Drizzle 实例缓存 **/
|
||||
let _db: BunSQLDatabase<typeof schema> | null = null;
|
||||
|
||||
/** # 成员变量 初始化 Promise(防并发重复构建) **/
|
||||
let _initing: Promise<BunSQLDatabase<typeof schema>> | null = null;
|
||||
|
||||
/** # 函数 构建 Drizzle 数据库实例(仅服务端执行)
|
||||
* 返回值 BunSQLDatabase 实例
|
||||
*/
|
||||
async function build(): Promise<BunSQLDatabase<typeof schema>> {
|
||||
// 变量指示符 + @vite-ignore:Vite/Rolldown 不做静态解析(客户端图不含
|
||||
// "bun"),服务端运行时由 Bun 原生解析内置模块
|
||||
const bunModule = "bun";
|
||||
const { SQL } = await import(/* @vite-ignore */ bunModule);
|
||||
const { drizzle } = await import("drizzle-orm/bun-sql");
|
||||
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
|
||||
return drizzle(sql, { schema }) as BunSQLDatabase<typeof schema>;
|
||||
}
|
||||
|
||||
/** # 函数 初始化数据库连接(幂等;仅服务端生效,浏览器端空操作)
|
||||
* 返回值 Drizzle 数据库实例(客户端返回空壳对象)
|
||||
*/
|
||||
export function initDb(): Promise<BunSQLDatabase<typeof schema>> {
|
||||
// start.ts 同时进客户端包,浏览器端绝不能触碰 Bun 内置模块
|
||||
if (typeof window !== "undefined") return Promise.resolve({} as BunSQLDatabase<typeof schema>);
|
||||
if (!_initing) _initing = build().then((d) => { _db = d; return d; });
|
||||
return _initing;
|
||||
}
|
||||
|
||||
/** # 函数 确保数据库已初始化(同步兜底:已初始化返回实例,否则抛出明确错误)
|
||||
* 返回值 Drizzle 数据库实例
|
||||
*/
|
||||
function ensureDb(): BunSQLDatabase<typeof schema> {
|
||||
if (!_db) throw new Error("db 未初始化:服务端入口需先 await initDb()(客户端不应访问 db)");
|
||||
return _db;
|
||||
}
|
||||
|
||||
/** # 成员变量 数据库连接代理(客户端导入安全:不触发连接初始化) **/
|
||||
export const db = new Proxy({} as BunSQLDatabase<typeof schema>, {
|
||||
get(_target, key) {
|
||||
const d = ensureDb();
|
||||
return Reflect.get(d, key, d);
|
||||
},
|
||||
}) as BunSQLDatabase<typeof schema>;
|
||||
|
||||
export { schema };
|
||||
export type Database = BunSQLDatabase<typeof schema>;
|
||||
@@ -0,0 +1,385 @@
|
||||
CREATE TABLE "audit_logs" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"actor_id" integer,
|
||||
"action" varchar(128) NOT NULL,
|
||||
"entity" varchar(64) NOT NULL,
|
||||
"entity_id" varchar(64),
|
||||
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"ip" varchar(64) DEFAULT '' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "cart_items" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"cart_id" integer NOT NULL,
|
||||
"product_id" integer NOT NULL,
|
||||
"qty" integer DEFAULT 1 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "carts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"user_id" integer,
|
||||
"session_key" varchar(64),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "categories" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"taxonomy" varchar(64) NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"parent_id" integer,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"image_media_id" integer,
|
||||
"sort" integer DEFAULT 0 NOT NULL,
|
||||
"legacy_id" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "content_revisions" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"content_id" integer NOT NULL,
|
||||
"lang" varchar(16) DEFAULT 'en' NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"html" text NOT NULL,
|
||||
"author_id" integer,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "content_terms" (
|
||||
"content_id" integer NOT NULL,
|
||||
"category_id" integer NOT NULL,
|
||||
CONSTRAINT "content_terms_content_id_category_id_pk" PRIMARY KEY("content_id","category_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "content_translations" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"content_id" integer NOT NULL,
|
||||
"lang" varchar(16) NOT NULL,
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"excerpt" text DEFAULT '' NOT NULL,
|
||||
"html" text DEFAULT '' NOT NULL,
|
||||
"status" varchar(16) DEFAULT 'draft' NOT NULL,
|
||||
"seo_title" text,
|
||||
"seo_description" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "contents" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"type" varchar(32) NOT NULL,
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"excerpt" text DEFAULT '' NOT NULL,
|
||||
"html" text DEFAULT '' NOT NULL,
|
||||
"status" varchar(16) DEFAULT 'published' NOT NULL,
|
||||
"author_id" integer,
|
||||
"featured_media_id" integer,
|
||||
"template" varchar(64) DEFAULT 'default' NOT NULL,
|
||||
"menu_order" integer DEFAULT 0 NOT NULL,
|
||||
"views" integer DEFAULT 0 NOT NULL,
|
||||
"sticky" boolean DEFAULT false NOT NULL,
|
||||
"seo_title" text,
|
||||
"seo_description" text,
|
||||
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"legacy_id" integer,
|
||||
"published_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "filters" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar(128) NOT NULL,
|
||||
"slug" varchar(128) NOT NULL,
|
||||
"type" varchar(24) DEFAULT 'attribute' NOT NULL,
|
||||
"attribute_id" integer,
|
||||
"taxonomy" varchar(64),
|
||||
"position" integer DEFAULT 0 NOT NULL,
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"legacy_id" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "inquiries" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"form_name" varchar(255) DEFAULT '' NOT NULL,
|
||||
"form_id" integer,
|
||||
"name" varchar(255) DEFAULT '' NOT NULL,
|
||||
"email" varchar(255) DEFAULT '' NOT NULL,
|
||||
"phone" varchar(128) DEFAULT '' NOT NULL,
|
||||
"company" varchar(255) DEFAULT '' NOT NULL,
|
||||
"country" varchar(128) DEFAULT '' NOT NULL,
|
||||
"message" text DEFAULT '' NOT NULL,
|
||||
"product_interest" varchar(255) DEFAULT '' NOT NULL,
|
||||
"fields" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"status" varchar(16) DEFAULT 'new' NOT NULL,
|
||||
"channel" text DEFAULT '' NOT NULL,
|
||||
"source_page" text DEFAULT '' NOT NULL,
|
||||
"ip" varchar(64) DEFAULT '' NOT NULL,
|
||||
"utm" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"legacy_id" integer,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "inquiry_forms" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"form_key" varchar(128) NOT NULL,
|
||||
"fields" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"mail_to" varchar(255),
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"legacy_id" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "jobs" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"type" varchar(64) NOT NULL,
|
||||
"payload" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"status" varchar(16) DEFAULT 'pending' NOT NULL,
|
||||
"attempts" integer DEFAULT 0 NOT NULL,
|
||||
"max_attempts" integer DEFAULT 3 NOT NULL,
|
||||
"run_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"last_error" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "languages" (
|
||||
"code" varchar(16) PRIMARY KEY NOT NULL,
|
||||
"name" varchar(64) NOT NULL,
|
||||
"is_default" boolean DEFAULT false NOT NULL,
|
||||
"enabled" boolean DEFAULT false NOT NULL,
|
||||
"machine" boolean DEFAULT false NOT NULL,
|
||||
"sort" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "legacy_id_mappings" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"entity" varchar(64) NOT NULL,
|
||||
"legacy_id" integer NOT NULL,
|
||||
"new_id" integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "media" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"path" varchar(512) NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"mime_type" varchar(128) DEFAULT '' NOT NULL,
|
||||
"width" integer,
|
||||
"height" integer,
|
||||
"size" bigint,
|
||||
"alt_text" text DEFAULT '' NOT NULL,
|
||||
"title" text DEFAULT '' NOT NULL,
|
||||
"ref_count" integer DEFAULT 0 NOT NULL,
|
||||
"legacy_id" integer,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "menu_items" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"menu_id" integer NOT NULL,
|
||||
"parent_id" integer,
|
||||
"title" varchar(255) NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"target" varchar(16) DEFAULT '_self' NOT NULL,
|
||||
"sort" integer DEFAULT 0 NOT NULL,
|
||||
"legacy_id" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "menus" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar(128) NOT NULL,
|
||||
"location" varchar(64) NOT NULL,
|
||||
"legacy_id" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "order_items" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order_id" integer NOT NULL,
|
||||
"product_id" integer,
|
||||
"name" text NOT NULL,
|
||||
"sku" varchar(128),
|
||||
"qty" integer DEFAULT 1 NOT NULL,
|
||||
"price" numeric(12, 2) DEFAULT '0' NOT NULL,
|
||||
"total" numeric(12, 2) DEFAULT '0' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "orders" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order_number" varchar(32) NOT NULL,
|
||||
"user_id" integer,
|
||||
"status" varchar(24) DEFAULT 'pending' NOT NULL,
|
||||
"currency" varchar(8) DEFAULT 'USD' NOT NULL,
|
||||
"subtotal" numeric(12, 2) DEFAULT '0' NOT NULL,
|
||||
"shipping_total" numeric(12, 2) DEFAULT '0' NOT NULL,
|
||||
"tax_total" numeric(12, 2) DEFAULT '0' NOT NULL,
|
||||
"total" numeric(12, 2) DEFAULT '0' NOT NULL,
|
||||
"payment_method" varchar(64),
|
||||
"payment_status" varchar(24) DEFAULT 'unpaid' NOT NULL,
|
||||
"transaction_id" varchar(128),
|
||||
"billing" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"shipping" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"customer_note" text DEFAULT '' NOT NULL,
|
||||
"legacy_id" integer,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "payments" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order_id" integer NOT NULL,
|
||||
"method" varchar(64) NOT NULL,
|
||||
"status" varchar(24) DEFAULT 'pending' NOT NULL,
|
||||
"transaction_id" varchar(128),
|
||||
"amount" numeric(12, 2) DEFAULT '0' NOT NULL,
|
||||
"raw" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product_attribute_map" (
|
||||
"product_id" integer NOT NULL,
|
||||
"value_id" integer NOT NULL,
|
||||
CONSTRAINT "product_attribute_map_product_id_value_id_pk" PRIMARY KEY("product_id","value_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product_attribute_values" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"attribute_id" integer NOT NULL,
|
||||
"value" varchar(255) NOT NULL,
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"sort" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product_attributes" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar(128) NOT NULL,
|
||||
"slug" varchar(128) NOT NULL,
|
||||
"filterable" boolean DEFAULT true NOT NULL,
|
||||
"sort" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product_terms" (
|
||||
"product_id" integer NOT NULL,
|
||||
"category_id" integer NOT NULL,
|
||||
CONSTRAINT "product_terms_product_id_category_id_pk" PRIMARY KEY("product_id","category_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product_translations" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"product_id" integer NOT NULL,
|
||||
"lang" varchar(16) NOT NULL,
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"short_description" text DEFAULT '' NOT NULL,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"status" varchar(16) DEFAULT 'draft' NOT NULL,
|
||||
"seo_title" text,
|
||||
"seo_description" text,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "products" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"sku" varchar(128),
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"short_description" text DEFAULT '' NOT NULL,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"price" numeric(12, 2),
|
||||
"regular_price" numeric(12, 2),
|
||||
"stock" integer,
|
||||
"stock_status" varchar(32) DEFAULT 'instock' NOT NULL,
|
||||
"featured_media_id" integer,
|
||||
"gallery" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"status" varchar(16) DEFAULT 'published' NOT NULL,
|
||||
"featured" boolean DEFAULT false NOT NULL,
|
||||
"views" integer DEFAULT 0 NOT NULL,
|
||||
"seo_title" text,
|
||||
"seo_description" text,
|
||||
"spec_url" text,
|
||||
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"legacy_id" integer,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "redirects" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"source" text NOT NULL,
|
||||
"target" text NOT NULL,
|
||||
"code" integer DEFAULT 301 NOT NULL,
|
||||
"is_regex" boolean DEFAULT false NOT NULL,
|
||||
"hits" integer DEFAULT 0 NOT NULL,
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"legacy_id" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "sessions" (
|
||||
"token" varchar(64) PRIMARY KEY NOT NULL,
|
||||
"user_id" integer NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "settings" (
|
||||
"key" varchar(128) PRIMARY KEY NOT NULL,
|
||||
"value" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"username" varchar(128),
|
||||
"display_name" varchar(255) DEFAULT '' NOT NULL,
|
||||
"password_hash" varchar(255),
|
||||
"legacy_password_hash" varchar(255),
|
||||
"role" varchar(32) DEFAULT 'customer' NOT NULL,
|
||||
"billing" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"shipping" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"legacy_id" integer,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX "audit_logs_time_idx" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE INDEX "cart_items_cart_idx" ON "cart_items" USING btree ("cart_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "categories_taxonomy_slug_idx" ON "categories" USING btree ("taxonomy","slug");--> statement-breakpoint
|
||||
CREATE INDEX "categories_parent_idx" ON "categories" USING btree ("parent_id");--> statement-breakpoint
|
||||
CREATE INDEX "content_terms_category_idx" ON "content_terms" USING btree ("category_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "content_translations_content_lang_idx" ON "content_translations" USING btree ("content_id","lang");--> statement-breakpoint
|
||||
CREATE INDEX "content_translations_lang_status_idx" ON "content_translations" USING btree ("lang","status");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "contents_slug_type_idx" ON "contents" USING btree ("slug","type");--> statement-breakpoint
|
||||
CREATE INDEX "contents_type_status_idx" ON "contents" USING btree ("type","status");--> statement-breakpoint
|
||||
CREATE INDEX "contents_legacy_idx" ON "contents" USING btree ("legacy_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "filters_slug_idx" ON "filters" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE INDEX "inquiries_status_idx" ON "inquiries" USING btree ("status","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "inquiries_email_idx" ON "inquiries" USING btree ("email");--> statement-breakpoint
|
||||
CREATE INDEX "inquiries_created_idx" ON "inquiries" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "inquiry_forms_key_idx" ON "inquiry_forms" USING btree ("form_key");--> statement-breakpoint
|
||||
CREATE INDEX "jobs_status_runat_idx" ON "jobs" USING btree ("status","run_at");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "legacy_map_entity_id_idx" ON "legacy_id_mappings" USING btree ("entity","legacy_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "media_path_idx" ON "media" USING btree ("path");--> statement-breakpoint
|
||||
CREATE INDEX "media_legacy_idx" ON "media" USING btree ("legacy_id");--> statement-breakpoint
|
||||
CREATE INDEX "menu_items_menu_idx" ON "menu_items" USING btree ("menu_id","sort");--> statement-breakpoint
|
||||
CREATE INDEX "order_items_order_idx" ON "order_items" USING btree ("order_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "orders_number_idx" ON "orders" USING btree ("order_number");--> statement-breakpoint
|
||||
CREATE INDEX "orders_status_idx" ON "orders" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "orders_user_idx" ON "orders" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "payments_order_idx" ON "payments" USING btree ("order_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "payments_txn_idx" ON "payments" USING btree ("transaction_id");--> statement-breakpoint
|
||||
CREATE INDEX "product_attr_map_value_idx" ON "product_attribute_map" USING btree ("value_id","product_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "product_attr_values_attr_slug_idx" ON "product_attribute_values" USING btree ("attribute_id","slug");--> statement-breakpoint
|
||||
CREATE INDEX "product_attr_values_attr_idx" ON "product_attribute_values" USING btree ("attribute_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "product_attributes_slug_idx" ON "product_attributes" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE INDEX "product_terms_category_idx" ON "product_terms" USING btree ("category_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "product_translations_product_lang_idx" ON "product_translations" USING btree ("product_id","lang");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "products_slug_idx" ON "products" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE INDEX "products_status_idx" ON "products" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "products_sku_idx" ON "products" USING btree ("sku");--> statement-breakpoint
|
||||
CREATE INDEX "products_legacy_idx" ON "products" USING btree ("legacy_id");--> statement-breakpoint
|
||||
CREATE INDEX "redirects_source_idx" ON "redirects" USING btree ("source");--> statement-breakpoint
|
||||
CREATE INDEX "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");--> statement-breakpoint
|
||||
CREATE INDEX "users_legacy_idx" ON "users" USING btree ("legacy_id");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1793295647791,
|
||||
"tag": "0000_worried_bill_hollister",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,863 @@
|
||||
/**
|
||||
* DusunIoT 重建版 PostgreSQL 数据模型(Drizzle ORM)
|
||||
*
|
||||
* 按业务域组织:content / products / commerce / inquiries / ops。
|
||||
* 每张表保留 legacy_id 以便对照原 WordPress 数据并支持增量同步。
|
||||
*/
|
||||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
varchar,
|
||||
text,
|
||||
integer,
|
||||
bigint,
|
||||
boolean,
|
||||
timestamp,
|
||||
numeric,
|
||||
jsonb,
|
||||
index,
|
||||
uniqueIndex,
|
||||
primaryKey,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
/* ============================= 内容域 ============================= */
|
||||
|
||||
/** # 数据库表
|
||||
* 内容主表(页面 page / 文章 post / 落地页 landing),默认语言(英文)的正本。
|
||||
* Elementor 原始 JSON 存于 meta.elementor,供 1:1 还原渲染器使用。
|
||||
*/
|
||||
export const contents = pgTable(
|
||||
"contents",
|
||||
{
|
||||
/** # 数据库字段 内容 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 内容类型:page | post | landing */
|
||||
type: varchar("type", { length: 32 }).notNull(),
|
||||
/** # 数据库字段 URL 别名(默认语言) */
|
||||
slug: varchar("slug", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 标题 */
|
||||
title: text("title").notNull(),
|
||||
/** # 数据库字段 摘要 */
|
||||
excerpt: text("excerpt").default("").notNull(),
|
||||
/** # 数据库字段 正文 HTML */
|
||||
html: text("html").default("").notNull(),
|
||||
/** # 数据库字段 状态:draft | review | published */
|
||||
status: varchar("status", { length: 16 }).default("published").notNull(),
|
||||
/** # 数据库字段 作者 */
|
||||
authorId: integer("author_id"),
|
||||
/** # 数据库字段 特色图片 */
|
||||
featuredMediaId: integer("featured_media_id"),
|
||||
/** # 数据库字段 页面模板名(home / product / blog / blank …) */
|
||||
template: varchar("template", { length: 64 }).default("default").notNull(),
|
||||
/** # 数据库字段 菜单排序 */
|
||||
menuOrder: integer("menu_order").default(0).notNull(),
|
||||
/** # 数据库字段 浏览次数 */
|
||||
views: integer("views").default(0).notNull(),
|
||||
/** # 数据库字段 是否置顶 */
|
||||
sticky: boolean("sticky").default(false).notNull(),
|
||||
/** # 数据库字段 SEO 标题 */
|
||||
seoTitle: text("seo_title"),
|
||||
/** # 数据库字段 SEO 描述 */
|
||||
seoDescription: text("seo_description"),
|
||||
/** # 数据库字段 扩展数据(elementor JSON 等) */
|
||||
meta: jsonb("meta").default({}).notNull(),
|
||||
/** # 数据库字段 原 WordPress post ID */
|
||||
legacyId: integer("legacy_id"),
|
||||
/** # 数据库字段 发布时间 */
|
||||
publishedAt: timestamp("published_at", { withTimezone: true }),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("contents_slug_type_idx").on(t.slug, t.type),
|
||||
index("contents_type_status_idx").on(t.type, t.status),
|
||||
index("contents_legacy_idx").on(t.legacyId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 内容多语言翻译版本(每种语言独立 slug/标题/正文/SEO;缺翻译时不生成伪语言页面)。
|
||||
*/
|
||||
export const contentTranslations = pgTable(
|
||||
"content_translations",
|
||||
{
|
||||
/** # 数据库字段 翻译 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 内容 ID */
|
||||
contentId: integer("content_id").notNull(),
|
||||
/** # 数据库字段 语言代码(BCP47,如 fr / de) */
|
||||
lang: varchar("lang", { length: 16 }).notNull(),
|
||||
/** # 数据库字段 该语言的 URL 别名 */
|
||||
slug: varchar("slug", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 标题 */
|
||||
title: text("title").notNull(),
|
||||
/** # 数据库字段 摘要 */
|
||||
excerpt: text("excerpt").default("").notNull(),
|
||||
/** # 数据库字段 正文 HTML */
|
||||
html: text("html").default("").notNull(),
|
||||
/** # 数据库字段 状态:draft | review | published */
|
||||
status: varchar("status", { length: 16 }).default("draft").notNull(),
|
||||
/** # 数据库字段 SEO 标题 */
|
||||
seoTitle: text("seo_title"),
|
||||
/** # 数据库字段 SEO 描述 */
|
||||
seoDescription: text("seo_description"),
|
||||
/** # 数据库字段 翻译时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("content_translations_content_lang_idx").on(t.contentId, t.lang),
|
||||
index("content_translations_lang_status_idx").on(t.lang, t.status),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 内容修订快照(发布前留档,支持回滚)。
|
||||
*/
|
||||
export const contentRevisions = pgTable("content_revisions", {
|
||||
/** # 数据库字段 修订 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 内容 ID */
|
||||
contentId: integer("content_id").notNull(),
|
||||
/** # 数据库字段 语言(缺省为默认语言) */
|
||||
lang: varchar("lang", { length: 16 }).default("en").notNull(),
|
||||
/** # 数据库字段 标题快照 */
|
||||
title: text("title").notNull(),
|
||||
/** # 数据库字段 正文 HTML 快照 */
|
||||
html: text("html").notNull(),
|
||||
/** # 数据库字段 修订人 */
|
||||
authorId: integer("author_id"),
|
||||
/** # 数据库字段 修订时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
/** # 数据库表
|
||||
* 分类/标签(taxonomy:category / post_tag / product_cat / product_tag / 属性 pa_*)。
|
||||
*/
|
||||
export const categories = pgTable(
|
||||
"categories",
|
||||
{
|
||||
/** # 数据库字段 分类 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 taxonomy 名称 */
|
||||
taxonomy: varchar("taxonomy", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 名称 */
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 URL 别名 */
|
||||
slug: varchar("slug", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 父级分类 */
|
||||
parentId: integer("parent_id"),
|
||||
/** # 数据库字段 描述 */
|
||||
description: text("description").default("").notNull(),
|
||||
/** # 数据库字段 分类图片 */
|
||||
imageMediaId: integer("image_media_id"),
|
||||
/** # 数据库字段 排序 */
|
||||
sort: integer("sort").default(0).notNull(),
|
||||
/** # 数据库字段 原 WordPress term_id */
|
||||
legacyId: integer("legacy_id"),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("categories_taxonomy_slug_idx").on(t.taxonomy, t.slug),
|
||||
index("categories_parent_idx").on(t.parentId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 内容与分类/标签的关联。
|
||||
*/
|
||||
export const contentTerms = pgTable(
|
||||
"content_terms",
|
||||
{
|
||||
/** # 数据库字段 内容 ID */
|
||||
contentId: integer("content_id").notNull(),
|
||||
/** # 数据库字段 分类 ID */
|
||||
categoryId: integer("category_id").notNull(),
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.contentId, t.categoryId] }),
|
||||
index("content_terms_category_idx").on(t.categoryId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 导航菜单定义(header / footer / account 等位置)。
|
||||
*/
|
||||
export const menus = pgTable("menus", {
|
||||
/** # 数据库字段 菜单 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 菜单名称 */
|
||||
name: varchar("name", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 位置别名(menu-1 / menu-2 / max_mega_menu_1 …) */
|
||||
location: varchar("location", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 原菜单 term_id */
|
||||
legacyId: integer("legacy_id"),
|
||||
});
|
||||
|
||||
/** # 数据库表
|
||||
* 菜单项(支持多级树)。
|
||||
*/
|
||||
export const menuItems = pgTable(
|
||||
"menu_items",
|
||||
{
|
||||
/** # 数据库字段 菜单项 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 所属菜单 */
|
||||
menuId: integer("menu_id").notNull(),
|
||||
/** # 数据库字段 父菜单项 */
|
||||
parentId: integer("parent_id"),
|
||||
/** # 数据库字段 显示文字 */
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 链接地址 */
|
||||
url: text("url").notNull(),
|
||||
/** # 数据库字段 打开方式:_self | _blank */
|
||||
target: varchar("target", { length: 16 }).default("_self").notNull(),
|
||||
/** # 数据库字段 排序 */
|
||||
sort: integer("sort").default(0).notNull(),
|
||||
/** # 数据库字段 原 menu_item post_id */
|
||||
legacyId: integer("legacy_id"),
|
||||
},
|
||||
(t) => [index("menu_items_menu_idx").on(t.menuId, t.sort)],
|
||||
);
|
||||
|
||||
/* ============================= 产品域 ============================= */
|
||||
|
||||
/** # 数据库表
|
||||
* 产品主表(WooCommerce 迁移)。
|
||||
*/
|
||||
export const products = pgTable(
|
||||
"products",
|
||||
{
|
||||
/** # 数据库字段 产品 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 SKU */
|
||||
sku: varchar("sku", { length: 128 }),
|
||||
/** # 数据库字段 URL 别名 */
|
||||
slug: varchar("slug", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 名称 */
|
||||
title: text("title").notNull(),
|
||||
/** # 数据库字段 短描述 */
|
||||
shortDescription: text("short_description").default("").notNull(),
|
||||
/** # 数据库字段 详细描述 HTML */
|
||||
description: text("description").default("").notNull(),
|
||||
/** # 数据库字段 现价 */
|
||||
price: numeric("price", { precision: 12, scale: 2 }),
|
||||
/** # 数据库字段 原价 */
|
||||
regularPrice: numeric("regular_price", { precision: 12, scale: 2 }),
|
||||
/** # 数据库字段 库存数量 */
|
||||
stock: integer("stock"),
|
||||
/** # 数据库字段 库存状态:instock | outofstock | onbackorder */
|
||||
stockStatus: varchar("stock_status", { length: 32 }).default("instock").notNull(),
|
||||
/** # 数据库字段 特色图片 */
|
||||
featuredMediaId: integer("featured_media_id"),
|
||||
/** # 数据库字段 图廊(媒体 ID 数组) */
|
||||
gallery: jsonb("gallery").default([]).notNull(),
|
||||
/** # 数据库字段 状态:draft | published */
|
||||
status: varchar("status", { length: 16 }).default("published").notNull(),
|
||||
/** # 数据库字段 是否主推产品 */
|
||||
featured: boolean("featured").default(false).notNull(),
|
||||
/** # 数据库字段 浏览次数 */
|
||||
views: integer("views").default(0).notNull(),
|
||||
/** # 数据库字段 SEO 标题 */
|
||||
seoTitle: text("seo_title"),
|
||||
/** # 数据库字段 SEO 描述 */
|
||||
seoDescription: text("seo_description"),
|
||||
/** # 数据库字段 规格书下载地址 */
|
||||
specUrl: text("spec_url"),
|
||||
/** # 数据库字段 Elementor 原始 JSON */
|
||||
meta: jsonb("meta").default({}).notNull(),
|
||||
/** # 数据库字段 原 WordPress post ID */
|
||||
legacyId: integer("legacy_id"),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("products_slug_idx").on(t.slug),
|
||||
index("products_status_idx").on(t.status),
|
||||
index("products_sku_idx").on(t.sku),
|
||||
index("products_legacy_idx").on(t.legacyId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 产品多语言翻译。
|
||||
*/
|
||||
export const productTranslations = pgTable(
|
||||
"product_translations",
|
||||
{
|
||||
/** # 数据库字段 翻译 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 产品 ID */
|
||||
productId: integer("product_id").notNull(),
|
||||
/** # 数据库字段 语言代码 */
|
||||
lang: varchar("lang", { length: 16 }).notNull(),
|
||||
/** # 数据库字段 该语言 slug */
|
||||
slug: varchar("slug", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 名称 */
|
||||
title: text("title").notNull(),
|
||||
/** # 数据库字段 短描述 */
|
||||
shortDescription: text("short_description").default("").notNull(),
|
||||
/** # 数据库字段 详细描述 */
|
||||
description: text("description").default("").notNull(),
|
||||
/** # 数据库字段 状态:draft | review | published */
|
||||
status: varchar("status", { length: 16 }).default("draft").notNull(),
|
||||
/** # 数据库字段 SEO 标题 */
|
||||
seoTitle: text("seo_title"),
|
||||
/** # 数据库字段 SEO 描述 */
|
||||
seoDescription: text("seo_description"),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [uniqueIndex("product_translations_product_lang_idx").on(t.productId, t.lang)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 产品属性定义(RAM / Flash / Chip Platform / Downlink 等,原 WooCommerce 全局属性)。
|
||||
*/
|
||||
export const productAttributes = pgTable(
|
||||
"product_attributes",
|
||||
{
|
||||
/** # 数据库字段 属性 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 属性名 */
|
||||
name: varchar("name", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 属性 slug(attribute_id,如 ram / chip-platform) */
|
||||
slug: varchar("slug", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 是否作为筛选条件 */
|
||||
filterable: boolean("filterable").default(true).notNull(),
|
||||
/** # 数据库字段 排序 */
|
||||
sort: integer("sort").default(0).notNull(),
|
||||
},
|
||||
(t) => [uniqueIndex("product_attributes_slug_idx").on(t.slug)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 产品属性可选值。
|
||||
*/
|
||||
export const productAttributeValues = pgTable(
|
||||
"product_attribute_values",
|
||||
{
|
||||
/** # 数据库字段 值 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 所属属性 */
|
||||
attributeId: integer("attribute_id").notNull(),
|
||||
/** # 数据库字段 显示值 */
|
||||
value: varchar("value", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 值 slug */
|
||||
slug: varchar("slug", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 排序 */
|
||||
sort: integer("sort").default(0).notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("product_attr_values_attr_slug_idx").on(t.attributeId, t.slug),
|
||||
index("product_attr_values_attr_idx").on(t.attributeId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 产品 ↔ 属性值 关联(筛选查询主路径,建立复合索引)。
|
||||
*/
|
||||
export const productAttributeMap = pgTable(
|
||||
"product_attribute_map",
|
||||
{
|
||||
/** # 数据库字段 产品 ID */
|
||||
productId: integer("product_id").notNull(),
|
||||
/** # 数据库字段 属性值 ID */
|
||||
valueId: integer("value_id").notNull(),
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.productId, t.valueId] }),
|
||||
index("product_attr_map_value_idx").on(t.valueId, t.productId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 产品 ↔ 产品分类 关联。
|
||||
*/
|
||||
export const productTerms = pgTable(
|
||||
"product_terms",
|
||||
{
|
||||
/** # 数据库字段 产品 ID */
|
||||
productId: integer("product_id").notNull(),
|
||||
/** # 数据库字段 分类 ID(taxonomy=product_cat) */
|
||||
categoryId: integer("category_id").notNull(),
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.productId, t.categoryId] }),
|
||||
index("product_terms_category_idx").on(t.categoryId),
|
||||
],
|
||||
);
|
||||
|
||||
/* ============================= 商城域 ============================= */
|
||||
|
||||
/** # 数据库表
|
||||
* 用户(后台账号 + 商城注册客户)。保留 WordPress phpass 密码哈希用于存量客户登录兼容。
|
||||
*/
|
||||
export const users = pgTable(
|
||||
"users",
|
||||
{
|
||||
/** # 数据库字段 用户 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 邮箱(登录名) */
|
||||
email: varchar("email", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 用户名 */
|
||||
username: varchar("username", { length: 128 }),
|
||||
/** # 数据库字段 显示名称 */
|
||||
displayName: varchar("display_name", { length: 255 }).default("").notNull(),
|
||||
/** # 数据库字段 密码哈希(新账号 bcrypt) */
|
||||
passwordHash: varchar("password_hash", { length: 255 }),
|
||||
/** # 数据库字段 WordPress 旧密码哈希(phpass $P$) */
|
||||
legacyPasswordHash: varchar("legacy_password_hash", { length: 255 }),
|
||||
/** # 数据库字段 角色:admin | editor | manager | customer */
|
||||
role: varchar("role", { length: 32 }).default("customer").notNull(),
|
||||
/** # 数据库字段 账单地址 */
|
||||
billing: jsonb("billing").default({}).notNull(),
|
||||
/** # 数据库字段 收货地址 */
|
||||
shipping: jsonb("shipping").default({}).notNull(),
|
||||
/** # 数据库字段 原 WP user ID */
|
||||
legacyId: integer("legacy_id"),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("users_email_idx").on(t.email),
|
||||
index("users_legacy_idx").on(t.legacyId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 服务端会话(HttpOnly Cookie → 会话记录)。
|
||||
*/
|
||||
export const sessions = pgTable(
|
||||
"sessions",
|
||||
{
|
||||
/** # 数据库字段 会话令牌 */
|
||||
token: varchar("token", { length: 64 }).primaryKey(),
|
||||
/** # 数据库字段 用户 ID */
|
||||
userId: integer("user_id").notNull(),
|
||||
/** # 数据库字段 过期时间 */
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [index("sessions_user_idx").on(t.userId)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 购物车(登录用户或匿名 session_key 维度)。
|
||||
*/
|
||||
export const carts = pgTable("carts", {
|
||||
/** # 数据库字段 购物车 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 用户 ID(登录用户) */
|
||||
userId: integer("user_id"),
|
||||
/** # 数据库字段 匿名会话键 */
|
||||
sessionKey: varchar("session_key", { length: 64 }),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
/** # 数据库表
|
||||
* 购物车明细。
|
||||
*/
|
||||
export const cartItems = pgTable(
|
||||
"cart_items",
|
||||
{
|
||||
/** # 数据库字段 明细 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 购物车 ID */
|
||||
cartId: integer("cart_id").notNull(),
|
||||
/** # 数据库字段 产品 ID */
|
||||
productId: integer("product_id").notNull(),
|
||||
/** # 数据库字段 数量 */
|
||||
qty: integer("qty").default(1).notNull(),
|
||||
},
|
||||
(t) => [index("cart_items_cart_idx").on(t.cartId)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 订单主表。
|
||||
*/
|
||||
export const orders = pgTable(
|
||||
"orders",
|
||||
{
|
||||
/** # 数据库字段 订单 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 订单号(对外展示) */
|
||||
orderNumber: varchar("order_number", { length: 32 }).notNull(),
|
||||
/** # 数据库字段 客户 ID */
|
||||
userId: integer("user_id"),
|
||||
/** # 数据库字段 状态:pending | on-hold | processing | completed | cancelled | refunded */
|
||||
status: varchar("status", { length: 24 }).default("pending").notNull(),
|
||||
/** # 数据库字段 币种 */
|
||||
currency: varchar("currency", { length: 8 }).default("USD").notNull(),
|
||||
/** # 数据库字段 商品小计 */
|
||||
subtotal: numeric("subtotal", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
/** # 数据库字段 运费 */
|
||||
shippingTotal: numeric("shipping_total", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
/** # 数据库字段 税费 */
|
||||
taxTotal: numeric("tax_total", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
/** # 数据库字段 订单总额 */
|
||||
total: numeric("total", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
/** # 数据库字段 支付方式 */
|
||||
paymentMethod: varchar("payment_method", { length: 64 }),
|
||||
/** # 数据库字段 支付状态 */
|
||||
paymentStatus: varchar("payment_status", { length: 24 }).default("unpaid").notNull(),
|
||||
/** # 数据库字段 支付交易号 */
|
||||
transactionId: varchar("transaction_id", { length: 128 }),
|
||||
/** # 数据库字段 账单信息 */
|
||||
billing: jsonb("billing").default({}).notNull(),
|
||||
/** # 数据库字段 收货信息 */
|
||||
shipping: jsonb("shipping").default({}).notNull(),
|
||||
/** # 数据库字段 客户备注 */
|
||||
customerNote: text("customer_note").default("").notNull(),
|
||||
/** # 数据库字段 原 WP order ID */
|
||||
legacyId: integer("legacy_id"),
|
||||
/** # 数据库字段 下单时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("orders_number_idx").on(t.orderNumber),
|
||||
index("orders_status_idx").on(t.status),
|
||||
index("orders_user_idx").on(t.userId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 订单明细(下单时快照商品名/价格)。
|
||||
*/
|
||||
export const orderItems = pgTable(
|
||||
"order_items",
|
||||
{
|
||||
/** # 数据库字段 明细 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 订单 ID */
|
||||
orderId: integer("order_id").notNull(),
|
||||
/** # 数据库字段 产品 ID */
|
||||
productId: integer("product_id"),
|
||||
/** # 数据库字段 商品名快照 */
|
||||
name: text("name").notNull(),
|
||||
/** # 数据库字段 SKU 快照 */
|
||||
sku: varchar("sku", { length: 128 }),
|
||||
/** # 数据库字段 数量 */
|
||||
qty: integer("qty").default(1).notNull(),
|
||||
/** # 数据库字段 单价 */
|
||||
price: numeric("price", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
/** # 数据库字段 小计 */
|
||||
total: numeric("total", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
},
|
||||
(t) => [index("order_items_order_idx").on(t.orderId)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 支付流水(含第三方回调原始报文,幂等键 transaction_id)。
|
||||
*/
|
||||
export const payments = pgTable(
|
||||
"payments",
|
||||
{
|
||||
/** # 数据库字段 支付 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 订单 ID */
|
||||
orderId: integer("order_id").notNull(),
|
||||
/** # 数据库字段 支付方式 */
|
||||
method: varchar("method", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 状态:succeeded | failed | refunded | pending */
|
||||
status: varchar("status", { length: 24 }).default("pending").notNull(),
|
||||
/** # 数据库字段 交易号(唯一,回调幂等) */
|
||||
transactionId: varchar("transaction_id", { length: 128 }),
|
||||
/** # 数据库字段 金额 */
|
||||
amount: numeric("amount", { precision: 12, scale: 2 }).default("0").notNull(),
|
||||
/** # 数据库字段 回调原始数据 */
|
||||
raw: jsonb("raw").default({}).notNull(),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
index("payments_order_idx").on(t.orderId),
|
||||
uniqueIndex("payments_txn_idx").on(t.transactionId),
|
||||
],
|
||||
);
|
||||
|
||||
/* ============================= 询盘域 ============================= */
|
||||
|
||||
/** # 数据库表
|
||||
* 询盘表单定义(Contact Form 7 迁移,字段结构存 JSON)。
|
||||
*/
|
||||
export const inquiryForms = pgTable(
|
||||
"inquiry_forms",
|
||||
{
|
||||
/** # 数据库字段 表单 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 表单名称 */
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
/** # 数据库字段 表单 key(slug) */
|
||||
formKey: varchar("form_key", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 字段定义 JSON */
|
||||
fields: jsonb("fields").default([]).notNull(),
|
||||
/** # 数据库字段 收件邮箱 */
|
||||
mailTo: varchar("mail_to", { length: 255 }),
|
||||
/** # 数据库字段 是否启用 */
|
||||
enabled: boolean("enabled").default(true).notNull(),
|
||||
/** # 数据库字段 原 CF7 form_id */
|
||||
legacyId: integer("legacy_id"),
|
||||
},
|
||||
(t) => [uniqueIndex("inquiry_forms_key_idx").on(t.formKey)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 询盘记录(CF7/Flamingo 与 Elementor Form 提交合并归一)。
|
||||
*/
|
||||
export const inquiries = pgTable(
|
||||
"inquiries",
|
||||
{
|
||||
/** # 数据库字段 询盘 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 来源表单名 */
|
||||
formName: varchar("form_name", { length: 255 }).default("").notNull(),
|
||||
/** # 数据库字段 表单 ID */
|
||||
formId: integer("form_id"),
|
||||
/** # 数据库字段 姓名 */
|
||||
name: varchar("name", { length: 255 }).default("").notNull(),
|
||||
/** # 数据库字段 邮箱 */
|
||||
email: varchar("email", { length: 255 }).default("").notNull(),
|
||||
/** # 数据库字段 电话 */
|
||||
phone: varchar("phone", { length: 128 }).default("").notNull(),
|
||||
/** # 数据库字段 公司 */
|
||||
company: varchar("company", { length: 255 }).default("").notNull(),
|
||||
/** # 数据库字段 国家/地区 */
|
||||
country: varchar("country", { length: 128 }).default("").notNull(),
|
||||
/** # 数据库字段 询盘内容 */
|
||||
message: text("message").default("").notNull(),
|
||||
/** # 数据库字段 感兴趣的产品 */
|
||||
productInterest: varchar("product_interest", { length: 255 }).default("").notNull(),
|
||||
/** # 数据库字段 完整原始字段(含 annualdemand、utm 等) */
|
||||
fields: jsonb("fields").default({}).notNull(),
|
||||
/** # 数据库字段 状态:new | read | replied | spam | archived */
|
||||
status: varchar("status", { length: 16 }).default("new").notNull(),
|
||||
/** # 数据库字段 来源渠道(channel / subject,可能很长用 text) */
|
||||
channel: text("channel").default("").notNull(),
|
||||
/** # 数据库字段 来源页面 */
|
||||
sourcePage: text("source_page").default("").notNull(),
|
||||
/** # 数据库字段 来源 IP */
|
||||
ip: varchar("ip", { length: 64 }).default("").notNull(),
|
||||
/** # 数据库字段 UTM 参数 */
|
||||
utm: jsonb("utm").default({}).notNull(),
|
||||
/** # 数据库字段 原 Flamingo/Elementor 记录 ID */
|
||||
legacyId: integer("legacy_id"),
|
||||
/** # 数据库字段 提交时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
index("inquiries_status_idx").on(t.status, t.createdAt),
|
||||
index("inquiries_email_idx").on(t.email),
|
||||
index("inquiries_created_idx").on(t.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
/* ============================= 运营域 ============================= */
|
||||
|
||||
/** # 数据库表
|
||||
* 媒体库(原 wp-content/uploads 附件)。
|
||||
*/
|
||||
export const media = pgTable(
|
||||
"media",
|
||||
{
|
||||
/** # 数据库字段 媒体 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 相对路径(uploads 下) */
|
||||
path: varchar("path", { length: 512 }).notNull(),
|
||||
/** # 数据库字段 访问 URL */
|
||||
url: text("url").notNull(),
|
||||
/** # 数据库字段 MIME 类型 */
|
||||
mimeType: varchar("mime_type", { length: 128 }).default("").notNull(),
|
||||
/** # 数据库字段 宽 */
|
||||
width: integer("width"),
|
||||
/** # 数据库字段 高 */
|
||||
height: integer("height"),
|
||||
/** # 数据库字段 文件大小 */
|
||||
size: bigint("size", { mode: "number" }),
|
||||
/** # 数据库字段 替代文本 */
|
||||
altText: text("alt_text").default("").notNull(),
|
||||
/** # 数据库字段 标题 */
|
||||
title: text("title").default("").notNull(),
|
||||
/** # 数据库字段 引用次数 */
|
||||
refCount: integer("ref_count").default(0).notNull(),
|
||||
/** # 数据库字段 原 WP attachment ID */
|
||||
legacyId: integer("legacy_id"),
|
||||
/** # 数据库字段 上传时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("media_path_idx").on(t.path),
|
||||
index("media_legacy_idx").on(t.legacyId),
|
||||
],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 301/302 重定向(Redirection 插件迁移)。
|
||||
*/
|
||||
export const redirects = pgTable(
|
||||
"redirects",
|
||||
{
|
||||
/** # 数据库字段 重定向 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 源路径(支持正则) */
|
||||
source: text("source").notNull(),
|
||||
/** # 数据库字段 目标地址 */
|
||||
target: text("target").notNull(),
|
||||
/** # 数据库字段 状态码 */
|
||||
code: integer("code").default(301).notNull(),
|
||||
/** # 数据库字段 是否正则匹配 */
|
||||
isRegex: boolean("is_regex").default(false).notNull(),
|
||||
/** # 数据库字段 命中次数 */
|
||||
hits: integer("hits").default(0).notNull(),
|
||||
/** # 数据库字段 是否启用 */
|
||||
enabled: boolean("enabled").default(true).notNull(),
|
||||
/** # 数据库字段 原 redirection item id */
|
||||
legacyId: integer("legacy_id"),
|
||||
},
|
||||
(t) => [index("redirects_source_idx").on(t.source)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 筛选配置(Filter Everything 迁移,前台产品筛选器)。
|
||||
*/
|
||||
export const filters = pgTable(
|
||||
"filters",
|
||||
{
|
||||
/** # 数据库字段 筛选 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 筛选名称 */
|
||||
name: varchar("name", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 URL 参数名(如 chip-platform) */
|
||||
slug: varchar("slug", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 类型:attribute | category | price */
|
||||
type: varchar("type", { length: 24 }).default("attribute").notNull(),
|
||||
/** # 数据库字段 关联属性 ID(type=attribute 时) */
|
||||
attributeId: integer("attribute_id"),
|
||||
/** # 数据库字段 关联 taxonomy */
|
||||
taxonomy: varchar("taxonomy", { length: 64 }),
|
||||
/** # 数据库字段 显示位置 */
|
||||
position: integer("position").default(0).notNull(),
|
||||
/** # 数据库字段 是否启用 */
|
||||
enabled: boolean("enabled").default(true).notNull(),
|
||||
/** # 数据库字段 原 filter-field post_id */
|
||||
legacyId: integer("legacy_id"),
|
||||
},
|
||||
(t) => [uniqueIndex("filters_slug_idx").on(t.slug)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 语言配置(GTranslate 迁移:启用的语言与默认语言)。
|
||||
*/
|
||||
export const languages = pgTable(
|
||||
"languages",
|
||||
{
|
||||
/** # 数据库字段 语言代码(BCP47) */
|
||||
code: varchar("code", { length: 16 }).primaryKey(),
|
||||
/** # 数据库字段 显示名 */
|
||||
name: varchar("name", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 是否默认语言 */
|
||||
isDefault: boolean("is_default").default(false).notNull(),
|
||||
/** # 数据库字段 是否启用(可发布真实页面) */
|
||||
enabled: boolean("enabled").default(false).notNull(),
|
||||
/** # 数据库字段 是否机器翻译(GTranslate 存量) */
|
||||
machine: boolean("machine").default(false).notNull(),
|
||||
/** # 数据库字段 排序 */
|
||||
sort: integer("sort").default(0).notNull(),
|
||||
},
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 持久化任务队列(独立于 Web 请求的后台任务,带重试)。
|
||||
*/
|
||||
export const jobs = pgTable(
|
||||
"jobs",
|
||||
{
|
||||
/** # 数据库字段 任务 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 任务类型(email.send / media.reindex …) */
|
||||
type: varchar("type", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 任务参数 */
|
||||
payload: jsonb("payload").default({}).notNull(),
|
||||
/** # 数据库字段 状态:pending | running | done | failed */
|
||||
status: varchar("status", { length: 16 }).default("pending").notNull(),
|
||||
/** # 数据库字段 已重试次数 */
|
||||
attempts: integer("attempts").default(0).notNull(),
|
||||
/** # 数据库字段 最大重试次数 */
|
||||
maxAttempts: integer("max_attempts").default(3).notNull(),
|
||||
/** # 数据库字段 计划执行时间 */
|
||||
runAt: timestamp("run_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 最近错误 */
|
||||
lastError: text("last_error"),
|
||||
/** # 数据库字段 创建时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [index("jobs_status_runat_idx").on(t.status, t.runAt)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 审计日志(后台敏感操作留痕)。
|
||||
*/
|
||||
export const auditLogs = pgTable(
|
||||
"audit_logs",
|
||||
{
|
||||
/** # 数据库字段 日志 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 操作人 */
|
||||
actorId: integer("actor_id"),
|
||||
/** # 数据库字段 动作(如 product.update) */
|
||||
action: varchar("action", { length: 128 }).notNull(),
|
||||
/** # 数据库字段 实体类型 */
|
||||
entity: varchar("entity", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 实体 ID */
|
||||
entityId: varchar("entity_id", { length: 64 }),
|
||||
/** # 数据库字段 附加数据 */
|
||||
meta: jsonb("meta").default({}).notNull(),
|
||||
/** # 数据库字段 操作 IP */
|
||||
ip: varchar("ip", { length: 64 }).default("").notNull(),
|
||||
/** # 数据库字段 操作时间 */
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => [index("audit_logs_time_idx").on(t.createdAt)],
|
||||
);
|
||||
|
||||
/** # 数据库表
|
||||
* 站点设置(键值 JSON:导航、邮件、支付、缓存开关等)。
|
||||
*/
|
||||
export const settings = pgTable("settings", {
|
||||
/** # 数据库字段 设置键 */
|
||||
key: varchar("key", { length: 128 }).primaryKey(),
|
||||
/** # 数据库字段 设置值 */
|
||||
value: jsonb("value").default({}).notNull(),
|
||||
/** # 数据库字段 更新时间 */
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
/** # 数据库表
|
||||
* 旧 ID 映射(WordPress ID → 新系统 ID,迁移校验与增量同步用)。
|
||||
*/
|
||||
export const legacyIdMappings = pgTable(
|
||||
"legacy_id_mappings",
|
||||
{
|
||||
/** # 数据库字段 映射 ID */
|
||||
id: serial("id").primaryKey(),
|
||||
/** # 数据库字段 实体类型(post/product/user/order/media …) */
|
||||
entity: varchar("entity", { length: 64 }).notNull(),
|
||||
/** # 数据库字段 原 WP ID */
|
||||
legacyId: integer("legacy_id").notNull(),
|
||||
/** # 数据库字段 新系统 ID */
|
||||
newId: integer("new_id").notNull(),
|
||||
},
|
||||
(t) => [uniqueIndex("legacy_map_entity_id_idx").on(t.entity, t.legacyId)],
|
||||
);
|
||||
Reference in New Issue
Block a user