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:
hailin 2026-02-06 08:39:10 -08:00
parent 51c05f98ee
commit 4ac1fc4f88
4 changed files with 265 additions and 1 deletions

View File

@ -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)

View File

@ -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"`,
);
}
}

View File

@ -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);
-- =========================================== -- ===========================================
-- 结束 -- 结束
-- =========================================== -- ===========================================

View File

@ -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);