@allbluecn/database 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/migrations/20260804174501_init/migration.sql +1764 -0
  2. package/migrations/20260805174545_add_story_activity_is_ai/migration.sql +2 -0
  3. package/migrations/20260806035202_story_archived/migration.sql +6 -0
  4. package/migrations/20260806061157_builtin_independent_project/migration.sql +26 -0
  5. package/migrations/20260806144250_tanstack_ai_persistence/migration.sql +129 -0
  6. package/migrations/20260809080004_add_monitoring_observability/migration.sql +75 -0
  7. package/migrations/20260811112502_add_workspace/migration.sql +89 -0
  8. package/migrations/20260811153719_add_hosted_ai_tables/migration.sql +119 -0
  9. package/migrations/20260812043944_add_billing_tables/migration.sql +86 -0
  10. package/migrations/20260813040200_sub3_stripe_subscription_unique/migration.sql +6 -0
  11. package/migrations/20260813061345_add_enterprise_license_sso_audit_permissions/migration.sql +169 -0
  12. package/migrations/20260813132026_add_user_locale/migration.sql +2 -0
  13. package/migrations/20260814000000_add_story_composite_index/migration.sql +2 -0
  14. package/migrations/20260814161622_add_user_default_workspace_id/migration.sql +2 -0
  15. package/migrations/20260815025746_p2_seat_addon/migration.sql +15 -0
  16. package/migrations/20260815120000_creem_billing_neutralization/migration.sql +122 -0
  17. package/migrations/20260816212027_compliance_content/migration.sql +3 -0
  18. package/migrations/20260817204928_p6_user_agents_skills/migration.sql +57 -0
  19. package/migrations/20260818094327_user_level_subscription/migration.sql +43 -0
  20. package/migrations/20260819014924_team_membership_invitation/migration.sql +65 -0
  21. package/migrations/migration_lock.toml +3 -0
  22. package/package.json +8 -3
  23. package/prisma.config.ts +18 -0
  24. package/schema.prisma +2014 -0
  25. package/schema.sqlite.prisma +1584 -0
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE "story_activities" ADD COLUMN "isAI" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,6 @@
1
+ -- AlterTable
2
+ ALTER TABLE "stories" ADD COLUMN "archived" BOOLEAN NOT NULL DEFAULT false,
3
+ ADD COLUMN "archivedAt" TIMESTAMP(3);
4
+
5
+ -- CreateIndex
6
+ CREATE INDEX "stories_archived_idx" ON "stories"("archived");
@@ -0,0 +1,26 @@
1
+ -- 1. 为每个有 projectId IS NULL 的 Story 的 createdById 用户补建内置项目
2
+ DO $$
3
+ DECLARE uid text; pid text;
4
+ BEGIN
5
+ FOR uid IN SELECT DISTINCT "createdById" FROM "stories" WHERE "projectId" IS NULL LOOP
6
+ INSERT INTO "projects" ("id", "displayId", "name", "icon", "ownerId", "createdAt", "updatedAt")
7
+ VALUES (lower(gen_random_uuid()::text), 'STORY', '独立需求', 'sprout', uid, now(), now())
8
+ ON CONFLICT ("ownerId", "displayId") DO NOTHING;
9
+
10
+ SELECT "id" INTO pid FROM "projects" WHERE "ownerId" = uid AND "displayId" = 'STORY';
11
+
12
+ INSERT INTO "project_members" ("id", "projectId", "userId", "role", "status", "joinedAt")
13
+ VALUES (lower(gen_random_uuid()::text), pid, uid, 'owner', 'active', now())
14
+ ON CONFLICT ("projectId", "userId") DO NOTHING;
15
+
16
+ UPDATE "stories" SET "projectId" = pid WHERE "projectId" IS NULL AND "createdById" = uid;
17
+ END LOOP;
18
+ END $$;
19
+
20
+ -- 2. 添加 NOT NULL 约束
21
+ ALTER TABLE "stories" ALTER COLUMN "projectId" SET NOT NULL;
22
+
23
+ -- 3. 改外键 onDelete: SET NULL → RESTRICT
24
+ ALTER TABLE "stories" DROP CONSTRAINT "stories_projectId_fkey";
25
+ ALTER TABLE "stories" ADD CONSTRAINT "stories_projectId_fkey"
26
+ FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,129 @@
1
+ -- DropForeignKey
2
+ ALTER TABLE "conversations" DROP CONSTRAINT "conversations_userId_fkey";
3
+
4
+ -- DropForeignKey
5
+ ALTER TABLE "messages" DROP CONSTRAINT "messages_conversationId_fkey";
6
+
7
+ -- DropTable
8
+ DROP TABLE "conversations";
9
+
10
+ -- DropTable
11
+ DROP TABLE "messages";
12
+
13
+ -- CreateTable
14
+ CREATE TABLE "chat_threads" (
15
+ "id" TEXT NOT NULL,
16
+ "userId" TEXT NOT NULL,
17
+ "title" TEXT,
18
+ "isArchived" BOOLEAN NOT NULL DEFAULT false,
19
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ "updatedAt" TIMESTAMP(3) NOT NULL,
21
+
22
+ CONSTRAINT "chat_threads_pkey" PRIMARY KEY ("id")
23
+ );
24
+
25
+ -- CreateTable
26
+ CREATE TABLE "chat_messages" (
27
+ "id" TEXT NOT NULL,
28
+ "threadId" TEXT NOT NULL,
29
+ "role" TEXT NOT NULL,
30
+ "content" TEXT NOT NULL,
31
+ "parts" JSONB,
32
+ "thinking" TEXT,
33
+ "citations" JSONB,
34
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
35
+
36
+ CONSTRAINT "chat_messages_pkey" PRIMARY KEY ("id")
37
+ );
38
+
39
+ -- CreateTable
40
+ CREATE TABLE "chat_runs" (
41
+ "id" TEXT NOT NULL,
42
+ "threadId" TEXT NOT NULL,
43
+ "userId" TEXT NOT NULL,
44
+ "status" TEXT NOT NULL,
45
+ "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
46
+ "finishedAt" TIMESTAMP(3),
47
+ "detachedSince" TIMESTAMP(3),
48
+ "usage" JSONB,
49
+ "error" TEXT,
50
+
51
+ CONSTRAINT "chat_runs_pkey" PRIMARY KEY ("id")
52
+ );
53
+
54
+ -- CreateTable
55
+ CREATE TABLE "chat_interrupts" (
56
+ "id" TEXT NOT NULL,
57
+ "runId" TEXT NOT NULL,
58
+ "threadId" TEXT NOT NULL,
59
+ "userId" TEXT NOT NULL,
60
+ "kind" TEXT NOT NULL,
61
+ "toolName" TEXT,
62
+ "state" TEXT NOT NULL,
63
+ "payload" JSONB,
64
+ "resolution" JSONB,
65
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
66
+ "resolvedAt" TIMESTAMP(3),
67
+
68
+ CONSTRAINT "chat_interrupts_pkey" PRIMARY KEY ("id")
69
+ );
70
+
71
+ -- CreateTable
72
+ CREATE TABLE "generation_runs" (
73
+ "id" TEXT NOT NULL,
74
+ "threadId" TEXT NOT NULL,
75
+ "userId" TEXT NOT NULL,
76
+ "kind" TEXT NOT NULL,
77
+ "status" TEXT NOT NULL,
78
+ "input" JSONB,
79
+ "result" JSONB,
80
+ "artifactId" TEXT,
81
+ "error" TEXT,
82
+ "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
83
+ "finishedAt" TIMESTAMP(3),
84
+
85
+ CONSTRAINT "generation_runs_pkey" PRIMARY KEY ("id")
86
+ );
87
+
88
+ -- CreateIndex
89
+ CREATE INDEX "chat_threads_userId_updatedAt_idx" ON "chat_threads"("userId", "updatedAt");
90
+
91
+ -- CreateIndex
92
+ CREATE INDEX "chat_messages_threadId_createdAt_idx" ON "chat_messages"("threadId", "createdAt");
93
+
94
+ -- CreateIndex
95
+ CREATE INDEX "chat_runs_threadId_startedAt_idx" ON "chat_runs"("threadId", "startedAt");
96
+
97
+ -- CreateIndex
98
+ CREATE INDEX "chat_runs_userId_status_idx" ON "chat_runs"("userId", "status");
99
+
100
+ -- CreateIndex
101
+ CREATE INDEX "chat_interrupts_runId_idx" ON "chat_interrupts"("runId");
102
+
103
+ -- CreateIndex
104
+ CREATE INDEX "chat_interrupts_threadId_state_idx" ON "chat_interrupts"("threadId", "state");
105
+
106
+ -- CreateIndex
107
+ CREATE INDEX "generation_runs_threadId_startedAt_idx" ON "generation_runs"("threadId", "startedAt");
108
+
109
+ -- CreateIndex
110
+ CREATE INDEX "generation_runs_userId_status_idx" ON "generation_runs"("userId", "status");
111
+
112
+ -- AddForeignKey
113
+ ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
114
+
115
+ -- AddForeignKey
116
+ ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "chat_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
117
+
118
+ -- AddForeignKey
119
+ ALTER TABLE "chat_runs" ADD CONSTRAINT "chat_runs_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "chat_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
120
+
121
+ -- AddForeignKey
122
+ ALTER TABLE "chat_interrupts" ADD CONSTRAINT "chat_interrupts_runId_fkey" FOREIGN KEY ("runId") REFERENCES "chat_runs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
123
+
124
+ -- AddForeignKey
125
+ ALTER TABLE "chat_interrupts" ADD CONSTRAINT "chat_interrupts_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "chat_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
126
+
127
+ -- AddForeignKey
128
+ ALTER TABLE "generation_runs" ADD CONSTRAINT "generation_runs_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "chat_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
129
+
@@ -0,0 +1,75 @@
1
+ -- CreateTable
2
+ CREATE TABLE "monitoring_request_logs" (
3
+ "id" TEXT NOT NULL,
4
+ "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
5
+ "method" VARCHAR(10) NOT NULL,
6
+ "path" VARCHAR(512) NOT NULL,
7
+ "status" INTEGER NOT NULL,
8
+ "duration" INTEGER NOT NULL,
9
+ "ip" VARCHAR(45),
10
+ "userAgent" TEXT,
11
+
12
+ CONSTRAINT "monitoring_request_logs_pkey" PRIMARY KEY ("id")
13
+ );
14
+
15
+ -- CreateTable
16
+ CREATE TABLE "monitoring_error_reports" (
17
+ "id" TEXT NOT NULL,
18
+ "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ "errorName" VARCHAR(128) NOT NULL,
20
+ "errorMessage" TEXT NOT NULL,
21
+ "stackTrace" TEXT,
22
+ "count" INTEGER NOT NULL DEFAULT 1,
23
+ "firstSeen" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+ "lastSeen" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+ "context" JSONB NOT NULL DEFAULT '{}',
26
+
27
+ CONSTRAINT "monitoring_error_reports_pkey" PRIMARY KEY ("id")
28
+ );
29
+
30
+ -- CreateTable
31
+ CREATE TABLE "monitoring_metrics_snapshots" (
32
+ "id" TEXT NOT NULL,
33
+ "recordedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
34
+ "metricName" VARCHAR(256) NOT NULL,
35
+ "count" INTEGER NOT NULL,
36
+ "avg" DOUBLE PRECISION NOT NULL,
37
+ "p50" DOUBLE PRECISION NOT NULL,
38
+ "p95" DOUBLE PRECISION NOT NULL,
39
+ "p99" DOUBLE PRECISION NOT NULL,
40
+ "min" DOUBLE PRECISION NOT NULL,
41
+ "max" DOUBLE PRECISION NOT NULL,
42
+
43
+ CONSTRAINT "monitoring_metrics_snapshots_pkey" PRIMARY KEY ("id")
44
+ );
45
+
46
+ -- CreateTable
47
+ CREATE TABLE "monitoring_health_snapshots" (
48
+ "id" TEXT NOT NULL,
49
+ "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
50
+ "status" VARCHAR(16) NOT NULL,
51
+ "uptimeSeconds" DOUBLE PRECISION NOT NULL,
52
+ "memoryUsage" JSONB NOT NULL,
53
+ "dbStatus" VARCHAR(16) NOT NULL,
54
+ "dbLatencyMs" INTEGER,
55
+
56
+ CONSTRAINT "monitoring_health_snapshots_pkey" PRIMARY KEY ("id")
57
+ );
58
+
59
+ -- CreateIndex
60
+ CREATE INDEX "idx_request_log_timestamp" ON "monitoring_request_logs"("timestamp");
61
+
62
+ -- CreateIndex
63
+ CREATE INDEX "idx_request_log_path_ts" ON "monitoring_request_logs"("path", "timestamp");
64
+
65
+ -- CreateIndex
66
+ CREATE INDEX "idx_error_report_name" ON "monitoring_error_reports"("errorName");
67
+
68
+ -- CreateIndex
69
+ CREATE INDEX "idx_error_report_last_seen" ON "monitoring_error_reports"("lastSeen");
70
+
71
+ -- CreateIndex
72
+ CREATE INDEX "idx_metrics_name_ts" ON "monitoring_metrics_snapshots"("metricName", "recordedAt");
73
+
74
+ -- CreateIndex
75
+ CREATE INDEX "idx_health_snapshot_ts" ON "monitoring_health_snapshots"("timestamp");
@@ -0,0 +1,89 @@
1
+ -- AlterTable
2
+ ALTER TABLE "projects" ADD COLUMN "workspaceId" TEXT;
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "workspaces" (
6
+ "id" TEXT NOT NULL,
7
+ "name" TEXT NOT NULL,
8
+ "icon" TEXT NOT NULL DEFAULT 'folder',
9
+ "description" TEXT,
10
+ "ownerId" TEXT NOT NULL,
11
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
+ "updatedAt" TIMESTAMP(3) NOT NULL,
13
+
14
+ CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
15
+ );
16
+
17
+ -- CreateTable
18
+ CREATE TABLE "workspace_members" (
19
+ "id" TEXT NOT NULL,
20
+ "workspaceId" TEXT NOT NULL,
21
+ "userId" TEXT NOT NULL,
22
+ "role" TEXT NOT NULL DEFAULT 'member',
23
+ "status" TEXT NOT NULL DEFAULT 'invited',
24
+ "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+
26
+ CONSTRAINT "workspace_members_pkey" PRIMARY KEY ("id")
27
+ );
28
+
29
+ -- CreateIndex
30
+ CREATE INDEX "workspaces_ownerId_idx" ON "workspaces"("ownerId");
31
+
32
+ -- CreateIndex
33
+ CREATE INDEX "workspace_members_workspaceId_idx" ON "workspace_members"("workspaceId");
34
+
35
+ -- CreateIndex
36
+ CREATE INDEX "workspace_members_userId_idx" ON "workspace_members"("userId");
37
+
38
+ -- CreateIndex
39
+ CREATE UNIQUE INDEX "workspace_members_workspaceId_userId_key" ON "workspace_members"("workspaceId", "userId");
40
+
41
+ -- CreateIndex
42
+ CREATE INDEX "projects_workspaceId_idx" ON "projects"("workspaceId");
43
+
44
+ -- AddForeignKey
45
+ ALTER TABLE "projects" ADD CONSTRAINT "projects_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE SET NULL ON UPDATE CASCADE;
46
+
47
+ -- AddForeignKey
48
+ ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
49
+
50
+ -- AddForeignKey
51
+ ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
52
+
53
+ -- AddForeignKey
54
+ ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
55
+
56
+ -- Backfill: 为每个有项目且 workspaceId 为 null 的 owner 创建一个默认工作空间
57
+ INSERT INTO "workspaces" ("id", "name", "icon", "description", "ownerId", "createdAt", "updatedAt")
58
+ SELECT
59
+ gen_random_uuid()::text,
60
+ COALESCE(u."username", split_part(u."email", '@', 1)) || ' 的工作空间',
61
+ 'folder',
62
+ '系统自动创建的默认工作空间',
63
+ p."ownerId",
64
+ NOW(),
65
+ NOW()
66
+ FROM (SELECT DISTINCT "ownerId" FROM "projects" WHERE "workspaceId" IS NULL) p
67
+ JOIN "users" u ON u."id" = p."ownerId"
68
+ WHERE NOT EXISTS (
69
+ SELECT 1 FROM "workspaces" w
70
+ WHERE w."ownerId" = p."ownerId" AND w."description" = '系统自动创建的默认工作空间'
71
+ );
72
+
73
+ -- Backfill: 回填 project.workspaceId
74
+ UPDATE "projects"
75
+ SET "workspaceId" = (
76
+ SELECT "id" FROM "workspaces"
77
+ WHERE "ownerId" = "projects"."ownerId"
78
+ AND "description" = '系统自动创建的默认工作空间'
79
+ LIMIT 1
80
+ )
81
+ WHERE "workspaceId" IS NULL;
82
+
83
+ -- Backfill: 把每个默认工作空间的 owner 加入为 owner 角色
84
+ INSERT INTO "workspace_members" ("id", "workspaceId", "userId", "role", "status", "joinedAt")
85
+ SELECT gen_random_uuid()::text, w."id", w."ownerId", 'owner', 'active', NOW()
86
+ FROM "workspaces" w
87
+ WHERE NOT EXISTS (
88
+ SELECT 1 FROM "workspace_members" wm WHERE wm."workspaceId" = w."id"
89
+ );
@@ -0,0 +1,119 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'EXPIRED', 'UNPAID');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "UsageMetric" AS ENUM ('AI_CALL', 'STORAGE_MB', 'SEAT');
6
+
7
+ -- CreateTable
8
+ CREATE TABLE "plans" (
9
+ "id" TEXT NOT NULL,
10
+ "name" TEXT NOT NULL,
11
+ "displayName" TEXT NOT NULL,
12
+ "description" TEXT,
13
+ "priceMonthly" INTEGER NOT NULL DEFAULT 0,
14
+ "priceYearly" INTEGER,
15
+ "currency" TEXT NOT NULL DEFAULT 'usd',
16
+ "maxSeats" INTEGER,
17
+ "maxWorkspaces" INTEGER,
18
+ "aiQuotaMonthly" INTEGER,
19
+ "features" JSONB NOT NULL,
20
+ "isPublic" BOOLEAN NOT NULL DEFAULT true,
21
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
22
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
23
+ "updatedAt" TIMESTAMP(3) NOT NULL,
24
+
25
+ CONSTRAINT "plans_pkey" PRIMARY KEY ("id")
26
+ );
27
+
28
+ -- CreateTable
29
+ CREATE TABLE "subscriptions" (
30
+ "id" TEXT NOT NULL,
31
+ "workspaceId" TEXT NOT NULL,
32
+ "planId" TEXT NOT NULL,
33
+ "status" "SubscriptionStatus" NOT NULL DEFAULT 'ACTIVE',
34
+ "seats" INTEGER NOT NULL DEFAULT 1,
35
+ "stripeCustomerId" TEXT,
36
+ "stripeSubscriptionId" TEXT,
37
+ "stripePriceId" TEXT,
38
+ "currentPeriodStart" TIMESTAMP(3),
39
+ "currentPeriodEnd" TIMESTAMP(3),
40
+ "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
41
+ "canceledAt" TIMESTAMP(3),
42
+ "trialEndsAt" TIMESTAMP(3),
43
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
44
+ "updatedAt" TIMESTAMP(3) NOT NULL,
45
+
46
+ CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
47
+ );
48
+
49
+ -- CreateTable
50
+ CREATE TABLE "usage_records" (
51
+ "id" TEXT NOT NULL,
52
+ "workspaceId" TEXT NOT NULL,
53
+ "metric" "UsageMetric" NOT NULL,
54
+ "period" TEXT NOT NULL,
55
+ "count" INTEGER NOT NULL DEFAULT 0,
56
+ "resetAt" TIMESTAMP(3) NOT NULL,
57
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
58
+ "updatedAt" TIMESTAMP(3) NOT NULL,
59
+
60
+ CONSTRAINT "usage_records_pkey" PRIMARY KEY ("id")
61
+ );
62
+
63
+ -- CreateTable
64
+ CREATE TABLE "ai_usage_logs" (
65
+ "id" TEXT NOT NULL,
66
+ "workspaceId" TEXT NOT NULL,
67
+ "userId" TEXT NOT NULL,
68
+ "source" TEXT NOT NULL DEFAULT 'byok',
69
+ "provider" TEXT NOT NULL,
70
+ "model" TEXT NOT NULL,
71
+ "inputTokens" INTEGER NOT NULL DEFAULT 0,
72
+ "outputTokens" INTEGER NOT NULL DEFAULT 0,
73
+ "costUsd" DECIMAL(10,6),
74
+ "success" BOOLEAN NOT NULL DEFAULT true,
75
+ "errorMessage" TEXT,
76
+ "durationMs" INTEGER,
77
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
78
+
79
+ CONSTRAINT "ai_usage_logs_pkey" PRIMARY KEY ("id")
80
+ );
81
+
82
+ -- CreateIndex
83
+ CREATE UNIQUE INDEX "plans_name_key" ON "plans"("name");
84
+
85
+ -- CreateIndex
86
+ CREATE UNIQUE INDEX "subscriptions_workspaceId_key" ON "subscriptions"("workspaceId");
87
+
88
+ -- CreateIndex
89
+ CREATE INDEX "usage_records_workspaceId_metric_idx" ON "usage_records"("workspaceId", "metric");
90
+
91
+ -- CreateIndex
92
+ CREATE UNIQUE INDEX "usage_records_workspaceId_metric_period_key" ON "usage_records"("workspaceId", "metric", "period");
93
+
94
+ -- CreateIndex
95
+ CREATE INDEX "ai_usage_logs_workspaceId_createdAt_idx" ON "ai_usage_logs"("workspaceId", "createdAt");
96
+
97
+ -- CreateIndex
98
+ CREATE INDEX "ai_usage_logs_userId_createdAt_idx" ON "ai_usage_logs"("userId", "createdAt");
99
+
100
+ -- CreateIndex
101
+ CREATE INDEX "ai_usage_logs_workspaceId_source_createdAt_idx" ON "ai_usage_logs"("workspaceId", "source", "createdAt");
102
+
103
+ -- CreateIndex
104
+ CREATE INDEX "ai_usage_logs_workspaceId_success_createdAt_idx" ON "ai_usage_logs"("workspaceId", "success", "createdAt");
105
+
106
+ -- AddForeignKey
107
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
108
+
109
+ -- AddForeignKey
110
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planId_fkey" FOREIGN KEY ("planId") REFERENCES "plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
111
+
112
+ -- AddForeignKey
113
+ ALTER TABLE "usage_records" ADD CONSTRAINT "usage_records_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
114
+
115
+ -- AddForeignKey
116
+ ALTER TABLE "ai_usage_logs" ADD CONSTRAINT "ai_usage_logs_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
117
+
118
+ -- AddForeignKey
119
+ ALTER TABLE "ai_usage_logs" ADD CONSTRAINT "ai_usage_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,86 @@
1
+ -- 幂等化迁移:子项目 3 新增 Invoice/StripeEvent 表 + Plan/User/Subscription 字段
2
+
3
+ -- 为 Plan 表添加 Stripe 字段(幂等:列已存在则跳过)
4
+ DO $$
5
+ BEGIN
6
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'plans' AND column_name = 'stripeProductId') THEN
7
+ ALTER TABLE "plans" ADD COLUMN "stripeProductId" TEXT;
8
+ END IF;
9
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'plans' AND column_name = 'stripePriceIdUsd') THEN
10
+ ALTER TABLE "plans" ADD COLUMN "stripePriceIdUsd" TEXT;
11
+ END IF;
12
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'plans' AND column_name = 'stripePriceIdCny') THEN
13
+ ALTER TABLE "plans" ADD COLUMN "stripePriceIdCny" TEXT;
14
+ END IF;
15
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'plans' AND column_name = 'stripePriceIdUsdYearly') THEN
16
+ ALTER TABLE "plans" ADD COLUMN "stripePriceIdUsdYearly" TEXT;
17
+ END IF;
18
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'plans' AND column_name = 'stripePriceIdCnyYearly') THEN
19
+ ALTER TABLE "plans" ADD COLUMN "stripePriceIdCnyYearly" TEXT;
20
+ END IF;
21
+ END $$;
22
+
23
+ -- 为 User 表添加 preferredCurrency(幂等)
24
+ DO $$
25
+ BEGIN
26
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'preferredCurrency') THEN
27
+ ALTER TABLE "users" ADD COLUMN "preferredCurrency" TEXT NOT NULL DEFAULT 'usd';
28
+ END IF;
29
+ END $$;
30
+
31
+ -- 创建 Invoice 表(幂等)
32
+ CREATE TABLE IF NOT EXISTS "invoices" (
33
+ "id" TEXT NOT NULL,
34
+ "subscriptionId" TEXT NOT NULL,
35
+ "stripeInvoiceId" TEXT NOT NULL,
36
+ "stripeEventId" TEXT,
37
+ "amountDue" INTEGER NOT NULL,
38
+ "amountPaid" INTEGER NOT NULL,
39
+ "currency" TEXT NOT NULL DEFAULT 'usd',
40
+ "status" TEXT NOT NULL,
41
+ "pdfUrl" TEXT,
42
+ "hostedInvoiceUrl" TEXT,
43
+ "periodStart" TIMESTAMP(3) NOT NULL,
44
+ "periodEnd" TIMESTAMP(3) NOT NULL,
45
+ "paidAt" TIMESTAMP(3),
46
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
47
+
48
+ CONSTRAINT "invoices_pkey" PRIMARY KEY ("id")
49
+ );
50
+
51
+ -- 创建 StripeEvent 表(幂等)
52
+ CREATE TABLE IF NOT EXISTS "stripe_events" (
53
+ "id" TEXT NOT NULL,
54
+ "stripeEventId" TEXT NOT NULL,
55
+ "type" TEXT NOT NULL,
56
+ "status" TEXT NOT NULL DEFAULT 'processing',
57
+ "payload" JSONB,
58
+ "errorMessage" TEXT,
59
+ "processedAt" TIMESTAMP(3),
60
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
61
+
62
+ CONSTRAINT "stripe_events_pkey" PRIMARY KEY ("id")
63
+ );
64
+
65
+ -- 创建唯一索引(幂等)
66
+ CREATE UNIQUE INDEX IF NOT EXISTS "invoices_stripeInvoiceId_key" ON "invoices"("stripeInvoiceId");
67
+ CREATE UNIQUE INDEX IF NOT EXISTS "stripe_events_stripeEventId_key" ON "stripe_events"("stripeEventId");
68
+
69
+ -- 创建普通索引(幂等)
70
+ CREATE INDEX IF NOT EXISTS "invoices_subscriptionId_createdAt_idx" ON "invoices"("subscriptionId", "createdAt");
71
+ CREATE INDEX IF NOT EXISTS "stripe_events_type_createdAt_idx" ON "stripe_events"("type", "createdAt");
72
+
73
+ -- 添加外键(幂等:已存在则跳过)
74
+ DO $$
75
+ BEGIN
76
+ IF NOT EXISTS (
77
+ SELECT 1 FROM information_schema.table_constraints
78
+ WHERE constraint_name = 'invoices_subscriptionId_fkey'
79
+ ) THEN
80
+ ALTER TABLE "invoices"
81
+ ADD CONSTRAINT "invoices_subscriptionId_fkey"
82
+ FOREIGN KEY ("subscriptionId")
83
+ REFERENCES "subscriptions"("id")
84
+ ON DELETE RESTRICT ON UPDATE CASCADE;
85
+ END IF;
86
+ END $$;
@@ -0,0 +1,6 @@
1
+ -- 子项目 3: stripeSubscriptionId 添加 @unique 约束
2
+ -- 用于 webhook 反查订阅(subscription.ts syncSubscriptionFromStripe)
3
+ -- 字段可空(free plan 无 Stripe 订阅),但非空值必须唯一
4
+
5
+ CREATE UNIQUE INDEX IF NOT EXISTS "subscriptions_stripeSubscriptionId_key"
6
+ ON "subscriptions"("stripeSubscriptionId");