fix(db): add multi-tenancy migration for conversation-service tables
- Add tenant_id column to conversations, messages, token_usages tables - Create standalone migration SQL script for production deployment - Add agent_executions table to init-db.sql for new installations - Fix MessageORM created_at nullable mismatch with database schema - Backfill existing data with default tenant ID Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51c05f98ee
commit
4ac1fc4f88
|
|
@ -46,7 +46,7 @@ export class MessageORM {
|
|||
@Column({ name: 'output_tokens', type: 'int', nullable: true })
|
||||
outputTokens: number | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz', nullable: true })
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => ConversationORM, (conversation) => conversation.messages)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 为 conversation-service 的三张表添加 tenant_id 列
|
||||
*
|
||||
* 影响的表:
|
||||
* - conversations
|
||||
* - messages
|
||||
* - token_usages
|
||||
*
|
||||
* 使用默认租户 ID 回填现有数据
|
||||
*/
|
||||
export class AddMultiTenancyToConversationService1738900000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddMultiTenancyToConversationService1738900000000';
|
||||
|
||||
private readonly DEFAULT_TENANT_ID =
|
||||
'00000000-0000-0000-0000-000000000001';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// ========== 1. conversations 表 ==========
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "conversations" ADD COLUMN IF NOT EXISTS "tenant_id" UUID`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "conversations" SET "tenant_id" = '${this.DEFAULT_TENANT_ID}' WHERE "tenant_id" IS NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "conversations" ALTER COLUMN "tenant_id" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "conversations" ALTER COLUMN "tenant_id" SET DEFAULT '${this.DEFAULT_TENANT_ID}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "idx_conversations_tenant" ON "conversations" ("tenant_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "idx_conversations_tenant_user" ON "conversations" ("tenant_id", "user_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "idx_conversations_tenant_status" ON "conversations" ("tenant_id", "status")`,
|
||||
);
|
||||
|
||||
// ========== 2. messages 表 ==========
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "messages" ADD COLUMN IF NOT EXISTS "tenant_id" UUID`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "messages" SET "tenant_id" = '${this.DEFAULT_TENANT_ID}' WHERE "tenant_id" IS NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "messages" ALTER COLUMN "tenant_id" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "messages" ALTER COLUMN "tenant_id" SET DEFAULT '${this.DEFAULT_TENANT_ID}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "idx_messages_tenant" ON "messages" ("tenant_id")`,
|
||||
);
|
||||
|
||||
// ========== 3. token_usages 表 ==========
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "token_usages" ADD COLUMN IF NOT EXISTS "tenant_id" UUID`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "token_usages" SET "tenant_id" = '${this.DEFAULT_TENANT_ID}' WHERE "tenant_id" IS NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "token_usages" ALTER COLUMN "tenant_id" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "token_usages" ALTER COLUMN "tenant_id" SET DEFAULT '${this.DEFAULT_TENANT_ID}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "idx_token_usages_tenant" ON "token_usages" ("tenant_id")`,
|
||||
);
|
||||
|
||||
// ========== 4. 确保 tenants 表存在并有默认租户 ==========
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "tenants" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"name" VARCHAR(100) NOT NULL UNIQUE,
|
||||
"slug" VARCHAR(50) NOT NULL UNIQUE,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
"plan" VARCHAR(30) NOT NULL DEFAULT 'STANDARD',
|
||||
"max_users" INT NOT NULL DEFAULT 100,
|
||||
"max_conversations_per_month" INT NOT NULL DEFAULT 10000,
|
||||
"max_storage_mb" INT NOT NULL DEFAULT 5120,
|
||||
"current_user_count" INT NOT NULL DEFAULT 0,
|
||||
"current_conversation_count" INT NOT NULL DEFAULT 0,
|
||||
"current_storage_bytes" BIGINT NOT NULL DEFAULT 0,
|
||||
"config" JSONB NOT NULL DEFAULT '{}',
|
||||
"billing_email" VARCHAR(255),
|
||||
"billing_name" VARCHAR(100),
|
||||
"billing_phone" VARCHAR(20),
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"suspended_at" TIMESTAMPTZ
|
||||
)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "tenants" ("id", "name", "slug", "status", "plan")
|
||||
VALUES ('${this.DEFAULT_TENANT_ID}', 'Default Tenant', 'default', 'ACTIVE', 'ENTERPRISE')
|
||||
ON CONFLICT ("id") DO NOTHING
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// token_usages
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "idx_token_usages_tenant"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "token_usages" DROP COLUMN IF EXISTS "tenant_id"`,
|
||||
);
|
||||
|
||||
// messages
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "idx_messages_tenant"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "messages" DROP COLUMN IF EXISTS "tenant_id"`,
|
||||
);
|
||||
|
||||
// conversations
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "idx_conversations_tenant_status"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "idx_conversations_tenant_user"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "idx_conversations_tenant"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "tenant_id"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1710,6 +1710,50 @@ CREATE INDEX idx_token_usages_created_brin ON token_usages USING BRIN(created_at
|
|||
-- 租户索引
|
||||
CREATE INDEX idx_token_usages_tenant ON token_usages(tenant_id);
|
||||
|
||||
-- ===========================================
|
||||
-- Agent执行记录表 (agent_executions)
|
||||
-- 记录每次 Agent 调用的执行数据,用于运营分析
|
||||
-- ===========================================
|
||||
CREATE TABLE agent_executions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- 租户ID
|
||||
tenant_id UUID NOT NULL,
|
||||
-- 所属对话ID
|
||||
conversation_id UUID NOT NULL,
|
||||
-- 关联的消息ID
|
||||
message_id UUID,
|
||||
-- 所属用户ID
|
||||
user_id UUID,
|
||||
-- Agent类型: coordinator, policy_expert, assessment_expert, strategist, objection_handler, case_analyst, memory_manager
|
||||
agent_type VARCHAR(30) NOT NULL,
|
||||
-- Agent名称(中文显示名)
|
||||
agent_name VARCHAR(50) NOT NULL,
|
||||
-- 执行耗时(毫秒)
|
||||
duration_ms INT NOT NULL DEFAULT 0,
|
||||
-- 是否成功
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
-- 错误信息(失败时记录)
|
||||
error_message TEXT,
|
||||
-- 工具调用次数
|
||||
tool_calls_count INT NOT NULL DEFAULT 0,
|
||||
-- 输入Token数
|
||||
input_tokens INT NOT NULL DEFAULT 0,
|
||||
-- 输出Token数
|
||||
output_tokens INT NOT NULL DEFAULT 0,
|
||||
-- 创建时间
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE agent_executions IS 'Agent执行记录表 - 记录每次Agent调用的执行数据';
|
||||
COMMENT ON COLUMN agent_executions.agent_type IS 'Agent类型标识符';
|
||||
COMMENT ON COLUMN agent_executions.duration_ms IS '执行耗时,单位毫秒';
|
||||
|
||||
CREATE INDEX idx_agent_executions_tenant ON agent_executions(tenant_id);
|
||||
CREATE INDEX idx_agent_executions_conversation ON agent_executions(conversation_id);
|
||||
CREATE INDEX idx_agent_executions_agent_type ON agent_executions(agent_type);
|
||||
CREATE INDEX idx_agent_executions_created ON agent_executions(created_at);
|
||||
CREATE INDEX idx_agent_executions_tenant_date ON agent_executions(tenant_id, created_at);
|
||||
|
||||
-- ===========================================
|
||||
-- 结束
|
||||
-- ===========================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
-- ===========================================
|
||||
-- 多租户迁移: conversation-service 表
|
||||
-- 为 conversations, messages, token_usages 添加 tenant_id
|
||||
-- 创建 agent_executions 表
|
||||
-- 日期: 2026-02-06
|
||||
-- ===========================================
|
||||
|
||||
-- ========== 1. 确保 tenants 表和默认租户存在 ==========
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
plan VARCHAR(30) NOT NULL DEFAULT 'STANDARD',
|
||||
max_users INT NOT NULL DEFAULT 100,
|
||||
max_conversations_per_month INT NOT NULL DEFAULT 10000,
|
||||
max_storage_mb INT NOT NULL DEFAULT 5120,
|
||||
current_user_count INT NOT NULL DEFAULT 0,
|
||||
current_conversation_count INT NOT NULL DEFAULT 0,
|
||||
current_storage_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
config JSONB NOT NULL DEFAULT '{}',
|
||||
billing_email VARCHAR(255),
|
||||
billing_name VARCHAR(100),
|
||||
billing_phone VARCHAR(20),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
suspended_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
INSERT INTO tenants (id, name, slug, status, plan)
|
||||
VALUES ('00000000-0000-0000-0000-000000000001', 'Default Tenant', 'default', 'ACTIVE', 'ENTERPRISE')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 2. conversations 表 ==========
|
||||
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||
UPDATE conversations SET tenant_id = '00000000-0000-0000-0000-000000000001' WHERE tenant_id IS NULL;
|
||||
ALTER TABLE conversations ALTER COLUMN tenant_id SET NOT NULL;
|
||||
ALTER TABLE conversations ALTER COLUMN tenant_id SET DEFAULT '00000000-0000-0000-0000-000000000001';
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_tenant ON conversations(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_tenant_user ON conversations(tenant_id, user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_tenant_status ON conversations(tenant_id, status);
|
||||
|
||||
-- ========== 3. messages 表 ==========
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||
UPDATE messages SET tenant_id = '00000000-0000-0000-0000-000000000001' WHERE tenant_id IS NULL;
|
||||
ALTER TABLE messages ALTER COLUMN tenant_id SET NOT NULL;
|
||||
ALTER TABLE messages ALTER COLUMN tenant_id SET DEFAULT '00000000-0000-0000-0000-000000000001';
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_tenant ON messages(tenant_id);
|
||||
|
||||
-- ========== 4. token_usages 表 ==========
|
||||
ALTER TABLE token_usages ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||
UPDATE token_usages SET tenant_id = '00000000-0000-0000-0000-000000000001' WHERE tenant_id IS NULL;
|
||||
ALTER TABLE token_usages ALTER COLUMN tenant_id SET NOT NULL;
|
||||
ALTER TABLE token_usages ALTER COLUMN tenant_id SET DEFAULT '00000000-0000-0000-0000-000000000001';
|
||||
CREATE INDEX IF NOT EXISTS idx_token_usages_tenant ON token_usages(tenant_id);
|
||||
|
||||
-- ========== 5. agent_executions 表 ==========
|
||||
CREATE TABLE IF NOT EXISTS agent_executions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
conversation_id UUID NOT NULL,
|
||||
message_id UUID,
|
||||
user_id UUID,
|
||||
agent_type VARCHAR(30) NOT NULL,
|
||||
agent_name VARCHAR(50) NOT NULL,
|
||||
duration_ms INT NOT NULL DEFAULT 0,
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
error_message TEXT,
|
||||
tool_calls_count INT NOT NULL DEFAULT 0,
|
||||
input_tokens INT NOT NULL DEFAULT 0,
|
||||
output_tokens INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_executions_tenant ON agent_executions(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_executions_conversation ON agent_executions(conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_executions_agent_type ON agent_executions(agent_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_executions_created ON agent_executions(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_executions_tenant_date ON agent_executions(tenant_id, created_at);
|
||||
Loading…
Reference in New Issue