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 })
|
@Column({ name: 'output_tokens', type: 'int', nullable: true })
|
||||||
outputTokens: number | null;
|
outputTokens: number | null;
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz', nullable: true })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@ManyToOne(() => ConversationORM, (conversation) => conversation.messages)
|
@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);
|
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