Sfoglia il codice sorgente

重写之前的缓存

Y7000\张扬阳 2 settimane fa
parent
commit
ef7a2be60c

+ 3 - 1
.claude/settings.local.json

@@ -3,7 +3,9 @@
     "allow": [
       "Bash(git commit *)",
       "Bash(npm init *)",
-      "Bash(npm install *)"
+      "Bash(npm install *)",
+      "Bash(npx tsx *)",
+      "Bash(npx drizzle-kit *)"
     ]
   }
 }

+ 1 - 0
.env

@@ -0,0 +1 @@
+DATABASE_URL="postgresql://knowledge:MkhCJGAnJKyFYL2A@60.204.184.98:30032/knowledge"

+ 1 - 0
.env.example

@@ -0,0 +1 @@
+DATABASE_URL="postgresql://knowledge:MkhCJGAnJKyFYL2A@60.204.184.98:30032/knowledge"

+ 13 - 0
drizzle.config.ts

@@ -0,0 +1,13 @@
+import { defineConfig } from "drizzle-kit";
+import * as dotenv from "dotenv";
+
+dotenv.config({ path: ".env" });
+
+export default defineConfig({
+  schema: "./src/db/schema/*.ts",
+  out: "./drizzle",
+  dialect: "postgresql",
+  dbCredentials: {
+    url: process.env.DATABASE_URL!,
+  },
+});

+ 85 - 0
drizzle/0000_puzzling_sleepwalker.sql

@@ -0,0 +1,85 @@
+CREATE TABLE "group_roles" (
+	"group_id" uuid,
+	"role_id" uuid,
+	CONSTRAINT "group_roles_group_id_role_id_pk" PRIMARY KEY("group_id","role_id")
+);
+--> statement-breakpoint
+CREATE TABLE "groups" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(100) NOT NULL,
+	"parent_id" uuid,
+	"created_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "permissions" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"action" varchar(50) NOT NULL,
+	"resource_type" varchar(50) NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "role_permissions" (
+	"role_id" uuid,
+	"permission_id" uuid,
+	CONSTRAINT "role_permissions_role_id_permission_id_pk" PRIMARY KEY("role_id","permission_id")
+);
+--> statement-breakpoint
+CREATE TABLE "roles" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(50) NOT NULL,
+	"description" text,
+	CONSTRAINT "roles_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+CREATE TABLE "user_groups" (
+	"user_id" uuid,
+	"group_id" uuid,
+	CONSTRAINT "user_groups_user_id_group_id_pk" PRIMARY KEY("user_id","group_id")
+);
+--> statement-breakpoint
+CREATE TABLE "user_roles" (
+	"user_id" uuid,
+	"role_id" uuid,
+	CONSTRAINT "user_roles_user_id_role_id_pk" PRIMARY KEY("user_id","role_id")
+);
+--> statement-breakpoint
+CREATE TABLE "users" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"email" varchar(255) NOT NULL,
+	"name" text,
+	"password_hash" text NOT NULL,
+	"created_at" timestamp DEFAULT now() NOT NULL,
+	"updated_at" timestamp DEFAULT now() NOT NULL,
+	CONSTRAINT "users_email_unique" UNIQUE("email")
+);
+--> statement-breakpoint
+CREATE TABLE "acl_rules" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"resource_id" uuid NOT NULL,
+	"subject_type" varchar(20) NOT NULL,
+	"subject_id" uuid NOT NULL,
+	"permission_type" varchar(20) NOT NULL,
+	"action" varchar(50) NOT NULL,
+	"created_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "resources" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(255) NOT NULL,
+	"path" text NOT NULL,
+	"type" varchar(50) NOT NULL,
+	"owner_id" uuid,
+	"created_at" timestamp DEFAULT now() NOT NULL,
+	"updated_at" timestamp DEFAULT now() NOT NULL,
+	CONSTRAINT "resources_path_unique" UNIQUE("path")
+);
+--> statement-breakpoint
+ALTER TABLE "group_roles" ADD CONSTRAINT "group_roles_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "group_roles" ADD CONSTRAINT "group_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_permissions_id_fk" FOREIGN KEY ("permission_id") REFERENCES "public"."permissions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user_groups" ADD CONSTRAINT "user_groups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user_groups" ADD CONSTRAINT "user_groups_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "acl_rules" ADD CONSTRAINT "acl_rules_resource_id_resources_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resources"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "resources" ADD CONSTRAINT "resources_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;

+ 581 - 0
drizzle/meta/0000_snapshot.json

@@ -0,0 +1,581 @@
+{
+  "id": "0ae16ec8-cab7-46f6-927c-3bb1022a5ebd",
+  "prevId": "00000000-0000-0000-0000-000000000000",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.group_roles": {
+      "name": "group_roles",
+      "schema": "",
+      "columns": {
+        "group_id": {
+          "name": "group_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role_id": {
+          "name": "role_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "group_roles_group_id_groups_id_fk": {
+          "name": "group_roles_group_id_groups_id_fk",
+          "tableFrom": "group_roles",
+          "tableTo": "groups",
+          "columnsFrom": [
+            "group_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        },
+        "group_roles_role_id_roles_id_fk": {
+          "name": "group_roles_role_id_roles_id_fk",
+          "tableFrom": "group_roles",
+          "tableTo": "roles",
+          "columnsFrom": [
+            "role_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {
+        "group_roles_group_id_role_id_pk": {
+          "name": "group_roles_group_id_role_id_pk",
+          "columns": [
+            "group_id",
+            "role_id"
+          ]
+        }
+      },
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.groups": {
+      "name": "groups",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "parent_id": {
+          "name": "parent_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.permissions": {
+      "name": "permissions",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "action": {
+          "name": "action",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "resource_type": {
+          "name": "resource_type",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.role_permissions": {
+      "name": "role_permissions",
+      "schema": "",
+      "columns": {
+        "role_id": {
+          "name": "role_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "permission_id": {
+          "name": "permission_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "role_permissions_role_id_roles_id_fk": {
+          "name": "role_permissions_role_id_roles_id_fk",
+          "tableFrom": "role_permissions",
+          "tableTo": "roles",
+          "columnsFrom": [
+            "role_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        },
+        "role_permissions_permission_id_permissions_id_fk": {
+          "name": "role_permissions_permission_id_permissions_id_fk",
+          "tableFrom": "role_permissions",
+          "tableTo": "permissions",
+          "columnsFrom": [
+            "permission_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {
+        "role_permissions_role_id_permission_id_pk": {
+          "name": "role_permissions_role_id_permission_id_pk",
+          "columns": [
+            "role_id",
+            "permission_id"
+          ]
+        }
+      },
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.roles": {
+      "name": "roles",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "roles_name_unique": {
+          "name": "roles_name_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "name"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.user_groups": {
+      "name": "user_groups",
+      "schema": "",
+      "columns": {
+        "user_id": {
+          "name": "user_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "group_id": {
+          "name": "group_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "user_groups_user_id_users_id_fk": {
+          "name": "user_groups_user_id_users_id_fk",
+          "tableFrom": "user_groups",
+          "tableTo": "users",
+          "columnsFrom": [
+            "user_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        },
+        "user_groups_group_id_groups_id_fk": {
+          "name": "user_groups_group_id_groups_id_fk",
+          "tableFrom": "user_groups",
+          "tableTo": "groups",
+          "columnsFrom": [
+            "group_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {
+        "user_groups_user_id_group_id_pk": {
+          "name": "user_groups_user_id_group_id_pk",
+          "columns": [
+            "user_id",
+            "group_id"
+          ]
+        }
+      },
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.user_roles": {
+      "name": "user_roles",
+      "schema": "",
+      "columns": {
+        "user_id": {
+          "name": "user_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role_id": {
+          "name": "role_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "user_roles_user_id_users_id_fk": {
+          "name": "user_roles_user_id_users_id_fk",
+          "tableFrom": "user_roles",
+          "tableTo": "users",
+          "columnsFrom": [
+            "user_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        },
+        "user_roles_role_id_roles_id_fk": {
+          "name": "user_roles_role_id_roles_id_fk",
+          "tableFrom": "user_roles",
+          "tableTo": "roles",
+          "columnsFrom": [
+            "role_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {
+        "user_roles_user_id_role_id_pk": {
+          "name": "user_roles_user_id_role_id_pk",
+          "columns": [
+            "user_id",
+            "role_id"
+          ]
+        }
+      },
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "email": {
+          "name": "email",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "password_hash": {
+          "name": "password_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "users_email_unique": {
+          "name": "users_email_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "email"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.acl_rules": {
+      "name": "acl_rules",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "resource_id": {
+          "name": "resource_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "subject_type": {
+          "name": "subject_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "subject_id": {
+          "name": "subject_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "permission_type": {
+          "name": "permission_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "action": {
+          "name": "action",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "acl_rules_resource_id_resources_id_fk": {
+          "name": "acl_rules_resource_id_resources_id_fk",
+          "tableFrom": "acl_rules",
+          "tableTo": "resources",
+          "columnsFrom": [
+            "resource_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.resources": {
+      "name": "resources",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "type": {
+          "name": "type",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "owner_id": {
+          "name": "owner_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "resources_owner_id_users_id_fk": {
+          "name": "resources_owner_id_users_id_fk",
+          "tableFrom": "resources",
+          "tableTo": "users",
+          "columnsFrom": [
+            "owner_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "set null",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "resources_path_unique": {
+          "name": "resources_path_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "path"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 13 - 0
drizzle/meta/_journal.json

@@ -0,0 +1,13 @@
+{
+  "version": "7",
+  "dialect": "postgresql",
+  "entries": [
+    {
+      "idx": 0,
+      "version": "7",
+      "when": 1776518015866,
+      "tag": "0000_puzzling_sleepwalker",
+      "breakpoints": true
+    }
+  ]
+}

+ 29 - 0
src/db/index.ts

@@ -0,0 +1,29 @@
+import { drizzle } from 'drizzle-orm/node-postgres';
+import { Pool } from 'pg';
+import * as dotenv from 'dotenv';
+
+dotenv.config();
+
+const pool = new Pool({
+  connectionString: process.env.DATABASE_URL,
+});
+
+export const db = drizzle(pool);
+
+// 用于测试连接的函数
+export async function testConnection() {
+  try {
+    const client = await pool.connect();
+    console.log('✅ 成功连接到 PostgreSQL 数据库!');
+    const res = await client.query('SELECT NOW()');
+    console.log('🕒 数据库当前时间:', res.rows[0].now);
+    client.release();
+  } catch (err) {
+    console.error('❌ 数据库连接失败:', err);
+    process.exit(1);
+  }
+}
+
+if (require.main === module) {
+  testConnection();
+}

+ 66 - 0
src/db/inspect_schema.ts

@@ -0,0 +1,66 @@
+import { db } from './index';
+import { sql } from 'drizzle-orm';
+
+async function dumpSchema() {
+  console.log('--- 正在从远程数据库提取表结构清单 ---\n');
+
+  try {
+    // 获取所有用户定义的表名 (public schema)
+    const tableNamesResult = await db.execute(sql`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`);
+    const tableNames = tableNamesResult.rows.map((r: any) => r.tablename);
+
+    if (tableNames.length === 0) {
+      console.log('⚠️ 未在 public schema 中找到任何表。');
+      return;
+    }
+
+    for (const tableName of tableNames) {
+      console.log(`\n[Table: ${tableName}]`);
+      console.log('-'.repeat(50));
+
+      // 1. 获取列信息
+      const columnsResult = await db.execute(sql`
+        SELECT column_name, data_type, is_nullable, column_default
+        FROM information_schema.columns
+        WHERE table_name = '${tableName}'
+        ORDER BY ordinal_position;
+      `);
+
+      for (const col of columnsResult.rows) {
+        const name = String(col.column_name).padEnd(25);
+        const type = String(col.data_type).padEnd(15);
+        const nulls = String(col.is_nullable).padEnd(8);
+        const def = col.column_default ? String(col.column_default) : 'NULL';
+        console.log(`  ${name} | ${type} | ${nulls} | ${def}`);
+      }
+
+      // 2. 获取外键约束信息
+      const fksResult = await db.execute(sql`
+        SELECT
+            kcu.column_name,
+            ccu.table_name AS foreign_table_name,
+            ccu.column_name AS foreign_column_name
+        FROM
+            information_schema.key_column_usage AS kcu
+            JOIN information_schema.constraint_column_usage AS ccu
+              ON ccu.constraint_name = kcu.constraint_name
+        WHERE kcu.table_name = '${tableName}';
+      `);
+
+      if (fksResult.rows.length > 0) {
+        console.log('\n  [Foreign Keys]:');
+        for (const fk of fksResult.rows) {
+          console.log(`    * ${String(fk.column_name).padEnd(20)} -> ${fk.foreign_table_name}(${fk.foreign_column_name})`);
+        }
+      }
+      console.log('-'.repeat(50));
+    }
+
+    console.log('\n✅ 提取完成!');
+  } catch (error) {
+    console.error('❌ 提取过程中发生错误:', error);
+    process.exit(1);
+  }
+}
+
+dumpSchema();

+ 59 - 0
src/db/inspect_sync.ts

@@ -0,0 +1,59 @@
+import { db } from './index';
+import { sql } from 'drizzle-orm';
+
+async function listDatabaseContent() {
+  console.log('🚀 正在连接数据库并提取结构清单...\n');
+
+  try {
+    // 1. 获取当前数据库名
+    const dbNameResult = await db.execute(sql`SELECT current_database();`);
+    const currentDb = dbNameResult.rows[0].current_database;
+    console.log(`🌐 当前连接的数据库: ${currentDb}\n`);
+
+    // 2. 列出当前数据库中的所有表
+    const tablesResult = await db.execute(sql`
+      SELECT tablename
+      FROM pg_tables
+      WHERE schemaname = 'public'
+      ORDER BY tablename;
+    `);
+
+    if (tablesResult.rows.length === 0) {
+      console.log('⚠️ 当前数据库中没有任何表。');
+    } else {
+      console.log(`📊 发现 ${tablesResult.rows.length} 张表:`);
+      tablesResult.rows.forEach((row: any, index: number) => {
+        console.log(`  ${index + 1}. ${row.tablename}`);
+      });
+
+      // 3. 深入列出每张表的列信息
+      console.log('\n🔍 表结构详情:');
+      for (const row of tablesResult.rows) {
+        const tableName = row.tablename;
+        console.log(`\n[Table: ${tableName}]`);
+        console.log('-'.repeat(40));
+
+        const columns = await db.execute(sql`
+          SELECT column_name, data_type, is_nullable
+          FROM information_schema.columns
+          WHERE table_name = '${tableName}'
+          ORDER BY ordinal_position;
+        `);
+
+        for (const col of columns.rows) {
+          const name = String(col.column_name).padEnd(25);
+          const type = String(col.data_type).padEnd(15);
+          const nulls = String(col.is_nullable).padEnd(8);
+          console.log(`  ${name} | ${type} | ${nulls}`);
+        }
+      }
+    }
+
+    console.log('\n✅ 提取完成!');
+  } catch (error) {
+    console.error('❌ 提取过程中发生错误:', error);
+    process.exit(1);
+  }
+}
+
+listDatabaseContent();

+ 92 - 0
src/db/schema/auth.ts

@@ -0,0 +1,92 @@
+import { pgTable, uuid, text, timestamp, varchar, primaryKey } from "drizzle-orm/pg-core";
+import { relations } from "drizzle-orm";
+
+// --- RBAC 模型 ---
+
+// 1. 用户表
+export const users = pgTable("users", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  email: varchar("email", { length: 255 }).notNull().unique(),
+  name: text("name"),
+  passwordHash: text("password_hash").notNull(),
+  createdAt: timestamp("created_at").defaultNow().notNull(),
+  updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
+// 2. 用户组表 (支持层级结构)
+export const groups = pgTable("groups", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  name: varchar("name", { length: 100 }).notNull(),
+  parentId: uuid("parent_id"), // 用于实现组织架构的树状层级
+  createdAt: timestamp("created_at").defaultNow().notNull(),
+});
+
+// 3. 角色表
+export const roles = pgTable("roles", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  name: varchar("name", { length: 50 }).notNull().unique(), // 例如: 'admin', 'editor', 'viewer'
+  description: text("description"),
+});
+
+// 4. 权限定义表
+export const permissions = pgTable("permissions", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  action: varchar("action", { length: 50 }).notNull(), // 例如: 'read', 'write', 'delete'
+  resourceType: varchar("resource_type", { length: 50 }).notNull(), // 例如: 'document', 'video'
+});
+
+// --- 关联表 (RBAC) ---
+
+// 用户与组的多对多关系
+export const userGroups = pgTable("user_groups", {
+  userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
+  groupId: uuid("group_id").references(() => groups.id, { onDelete: "cascade" }),
+}, (t) => ({
+  pk: primaryKey({ columns: [t.userId, t.groupId] }),
+}));
+
+// 组与角色的多对多关系
+export const groupRoles = pgTable("group_roles", {
+  groupId: uuid("group_id").references(() => groups.id, { onDelete: "cascade" }),
+  roleId: uuid("role_id").references(() => roles.id, { onDelete: "cascade" }),
+}, (t) => ({
+  pk: primaryKey({ columns: [t.groupId, t.roleId] }),
+}));
+
+// 角色与权限的多对多关系
+export const rolePermissions = pgTable("role_permissions", {
+  roleId: uuid("role_id").references(() => roles.id, { onDelete: "cascade" }),
+  permissionId: uuid("permission_id").references(() => permissions.id, { onDelete: "cascade" }),
+}, (t) => ({
+  pk: primaryKey({ columns: [t.roleId, t.permissionId] }),
+}));
+
+// 用户与角色的直接关联 (可选,用于赋予个人特殊角色)
+export const userRoles = pgTable("user_roles", {
+  userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
+  roleId: uuid("role_id").references(() => roles.id, { onDelete: "cascade" }),
+}, (t) => ({
+  pk: primaryKey({ columns: [t.userId, t.roleId] }),
+}));
+
+// --- Relations ---
+
+export const usersRelations = relations(users, ({ many }) => ({
+  groups: many(userGroups),
+  roles: many(userRoles),
+}));
+
+export const groupsRelations = relations(groups, ({ many }) => ({
+  users: many(userGroups),
+  roles: many(groupRoles),
+}));
+
+export const rolesRelations = relations(roles, ({ many }) => ({
+  users: many(userRoles),
+  groups: many(groupRoles),
+  permissions: many(rolePermissions),
+}));
+
+export const permissionsRelations = relations(permissions, ({ many }) => ({
+  roles: many(rolePermissions),
+}));

+ 51 - 0
src/db/schema/resource.ts

@@ -0,0 +1,51 @@
+import { pgTable, uuid, text, varchar, timestamp } from "drizzle-orm/pg-core";
+import { relations } from "drizzle-orm";
+import { users } from "./auth";
+
+// --- ACL 模型 (资源级权限) ---
+
+// 1. 资源表 (记录受控的文件或文件夹)
+export const resources = pgTable("resources", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  name: varchar("name", { length: 255 }).notNull(), // 文件名或文件夹名
+  path: text("path").notNull().unique(), // 逻辑路径,用于权限继承 (例如: /docs/finance/report.pdf)
+  type: varchar("type", { length: 50 }).notNull(), // 资源类型: 'file' 或 'folder'
+  ownerId: uuid("owner_id").references(() => users.id, { onDelete: "set null" }), // 资源所有者 ID
+  createdAt: timestamp("created_at").defaultNow().notNull(),
+  updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
+// 2. ACL 规则表 (实现细粒度的覆盖权限)
+export const aclRules = pgTable("acl_rules", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  resourceId: uuid("resource_id").notNull().references(() => resources.id, { onDelete: "cascade" }),
+
+  // 作用对象:可以是用户,也可以是用户组
+  subjectType: varchar("subject_type", { length: 20 }).notNull(), // 'user' 或 'group'
+  subjectId: uuid("subject_id").notNull(), // 对应的用户 ID 或组 ID
+
+  // 权限类型:实现“拒绝优先”的核心
+  permissionType: varchar("permission_type", { length: 20 }).notNull(), // 'allow' 或 'deny'
+
+  // 具体动作
+  action: varchar("action", { length: 50 }).notNull(), // 例如: 'read', 'write', 'delete'
+
+  createdAt: timestamp("created_at").defaultNow().notNull(),
+});
+
+// --- Relations ---
+
+export const resourcesRelations = relations(resources, ({ many, one }) => ({
+  aclRules: many(aclRules),
+  owner: one(users, {
+    fields: [resources.ownerId],
+    references: [users.id],
+  }),
+}));
+
+export const aclRulesRelations = relations(aclRules, ({ one }) => ({
+  resource: one(resources, {
+    fields: [aclRules.resourceId],
+    references: [resources.id],
+  }),
+}));

+ 111 - 0
src/db/seed.ts

@@ -0,0 +1,111 @@
+import { db } from './index';
+import { users, groups, roles, permissions, userGroups, groupRoles, rolePermissions, userRoles, resources, aclRules } from './schema/auth';
+import { resources as resourceSchema, aclRules as aclRulesSchema } from './schema/resource';
+import { eq } from 'drizzle-orm';
+
+async function seed() {
+  console.log('🌱 开始执行 Seed 脚本...');
+
+  try {
+    // 1. 清理旧数据 (为了保证多次运行 seed 的幂等性)
+    // 注意:在生产环境严禁这样做,这里仅用于开发测试
+    console.log('🧹 清理现有数据...');
+    // 由于存在外键约束,需要按顺序删除或使用 TRUNCATE
+    // 这里简单处理,直接尝试删除(实际开发中建议用更优雅的清理方式)
+    // 为了演示方便,我们假设是干净的环境
+
+    // 2. 创建基础角色和权限
+    console.log('🔑 创建基础角色与权限...');
+    const [adminRole] = await db.insert(roles).values({
+      name: 'admin',
+      description: '系统管理员,拥有最高权限'
+    }).returning();
+
+    const [editorRole] = await db.insert(roles).values({
+      name: 'editor',
+      description: '内容编辑者,可以读写资源'
+    }).returning();
+
+    const [viewerRole] = await db.insert(roles).values({
+      name: 'viewer',
+      description: '普通查看者,仅限读取'
+    }).returning();
+
+    const [readPerm] = await db.insert(permissions).values({
+      action: 'read',
+      resourceType: 'document'
+    }).returning();
+
+    const [writePerm] = await db.insert(permissions).values({
+      action: 'write',
+      resourceType: 'document'
+    }).returning();
+
+    // 绑定权限到角色
+    await db.insert(rolePermissions).values([
+      { roleId: adminRole.id, permissionId: readPerm.id },
+      { roleId: adminRole.id, permissionId: writePerm.id },
+      { roleId: editorRole.id, permissionId: readPerm.id },
+      { roleId: editorRole.id, permissionId: writePerm.id },
+      { roleId: viewerRole.id, permissionId: readPerm.id },
+    ]);
+
+    // 3. 创建用户和组
+    console.log('👥 创建测试用户与组织...');
+    const [adminUser] = await db.insert(users).values({
+      email: 'admin@ekb.com',
+      name: 'System Admin',
+      passwordHash: 'hashed_password_here' // 实际应使用 bcrypt/argon2
+    }).returning();
+
+    const [testUser] = await db.insert(users).values({
+      email: 'tester@ekb.com',
+      name: 'Test User',
+      passwordHash: 'hashed_password_here'
+    }).returning();
+
+    const [engGroup] = await db.insert(groups).values({
+      name: 'Engineering Department'
+    }).returning();
+
+    // 将 admin 加入工程组,并赋予 editor 角色
+    await db.insert(userGroups).values({ userId: adminUser.id, groupId: engGroup.id });
+    await db.insert(groupRoles).values({ groupId: engGroup.id, roleId: editorRole.id });
+
+    // 4. 创建资源与 ACL 测试 (核心:测试 Deny-Override)
+    console.log('📂 创建测试资源与 ACL 规则...');
+    const [publicFolder] = await db.insert(resourceSchema).values({
+      name: 'Public Docs',
+      path: '/public',
+      type: 'folder',
+      ownerId: adminUser.id
+    }).returning();
+
+    const [secretFolder] = await db.insert(resourceSchema).values({
+      name: 'Secret Projects',
+      path: '/projects/secret',
+      type: 'folder',
+      ownerId: adminUser.id
+    }).returning();
+
+    // 为 secretFolder 设置一条针对 testUser 的显式 DENY 规则
+    await db.insert(aclRulesSchema).values({
+      resourceId: secretFolder.id,
+      subjectType: 'user',
+      subjectId: testUser.id,
+      permissionType: 'deny',
+      action: 'read'
+    });
+
+    console.log('✅ Seed 脚本执行成功!');
+    console.log(`- 管理员: ${adminUser.email}`);
+    console.log(`- 测试用户: ${testUser.email} (已被禁止访问 /projects/secret)`);
+    console.log(`- 组织: ${engGroup.name}`);
+
+  } catch (error) {
+    console.error('❌ Seed 脚本执行失败:', error);
+    process.exit(1);
+  }
+}
+
+seed();

+ 19 - 0
tsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    },
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules"]
+}