feat(platform): Complete Postgres-Only architecture refactoring (Phase 1-7)
Major Changes: - Implement Platform-Only architecture pattern (unified task management) - Add PostgresCacheAdapter for unified caching (platform_schema.app_cache) - Add PgBossQueue for job queue management (platform_schema.job) - Implement CheckpointService using job.data (generic for all modules) - Add intelligent threshold-based dual-mode processing (THRESHOLD=50) - Add task splitting mechanism (auto chunk size recommendation) - Refactor ASL screening service with smart mode selection - Refactor DC extraction service with smart mode selection - Register workers for ASL and DC modules Technical Highlights: - All task management data stored in platform_schema.job.data (JSONB) - Business tables remain clean (no task management fields) - CheckpointService is generic (shared by all modules) - Zero code duplication (DRY principle) - Follows 3-layer architecture principle - Zero additional cost (no Redis needed, save 8400 CNY/year) Code Statistics: - New code: ~1750 lines - Modified code: ~500 lines - Test code: ~1800 lines - Documentation: ~3000 lines Testing: - Unit tests: 8/8 passed - Integration tests: 2/2 passed - Architecture validation: passed - Linter errors: 0 Files: - Platform layer: PostgresCacheAdapter, PgBossQueue, CheckpointService, utils - ASL module: screeningService, screeningWorker - DC module: ExtractionController, extractionWorker - Tests: 11 test files - Docs: Updated 4 key documents Status: Phase 1-7 completed, Phase 8-9 pending
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
-- ==================== Postgres-Only 改造:手动迁移 ====================
|
||||
-- 文件: 001_add_postgres_cache_and_checkpoint.sql
|
||||
-- 目的: 添加缓存表和断点续传字段
|
||||
-- 日期: 2025-12-13
|
||||
-- 说明: 避免Prisma migrate的shadow database问题,手动添加所需表和字段
|
||||
|
||||
-- ==================== 1. 创建缓存表 (AppCache) ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_schema.app_cache (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(500) UNIQUE NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 创建索引(优化过期查询和key查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_app_cache_expires
|
||||
ON platform_schema.app_cache(expires_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_app_cache_key_expires
|
||||
ON platform_schema.app_cache(key, expires_at);
|
||||
|
||||
-- ==================== 2. 为AslScreeningTask添加新字段 ====================
|
||||
|
||||
-- 任务拆分支持字段
|
||||
ALTER TABLE asl_schema.screening_tasks
|
||||
ADD COLUMN IF NOT EXISTS total_batches INTEGER DEFAULT 1,
|
||||
ADD COLUMN IF NOT EXISTS processed_batches INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS current_batch_index INTEGER DEFAULT 0;
|
||||
|
||||
-- 断点续传支持字段
|
||||
ALTER TABLE asl_schema.screening_tasks
|
||||
ADD COLUMN IF NOT EXISTS current_index INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS last_checkpoint TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS checkpoint_data JSONB;
|
||||
|
||||
-- ==================== 3. 验证创建结果 ====================
|
||||
|
||||
-- 查看app_cache表结构
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'platform_schema'
|
||||
AND table_name = 'app_cache'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- 查看screening_tasks新增字段
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'asl_schema'
|
||||
AND table_name = 'screening_tasks'
|
||||
AND column_name IN (
|
||||
'total_batches', 'processed_batches', 'current_batch_index',
|
||||
'current_index', 'last_checkpoint', 'checkpoint_data'
|
||||
)
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- ==================== 完成 ====================
|
||||
-- ✅ 缓存表已创建
|
||||
-- ✅ 任务拆分字段已添加
|
||||
-- ✅ 断点续传字段已添加
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 回滚迁移:删除业务表中的任务管理字段
|
||||
*
|
||||
* 原因:任务拆分和断点续传应由 platform_schema.job (pg-boss) 统一管理
|
||||
* 不应在各业务表中重复定义,符合3层架构原则
|
||||
*
|
||||
* 影响表:
|
||||
* - asl_schema.screening_tasks (删除 6 个字段)
|
||||
* - dc_schema.dc_extraction_tasks (无需添加)
|
||||
*/
|
||||
|
||||
-- 删除 ASL 表中的任务管理字段
|
||||
ALTER TABLE asl_schema.screening_tasks
|
||||
DROP COLUMN IF EXISTS total_batches,
|
||||
DROP COLUMN IF EXISTS processed_batches,
|
||||
DROP COLUMN IF EXISTS current_batch_index,
|
||||
DROP COLUMN IF EXISTS current_index,
|
||||
DROP COLUMN IF EXISTS last_checkpoint,
|
||||
DROP COLUMN IF EXISTS checkpoint_data;
|
||||
|
||||
84
backend/prisma/manual-migrations/run-migration-002.ts
Normal file
84
backend/prisma/manual-migrations/run-migration-002.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 执行回滚迁移脚本
|
||||
*
|
||||
* 删除业务表中的任务管理字段,统一由 platform_schema.job 管理
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function runMigration() {
|
||||
console.log('🚀 开始执行回滚迁移...\n');
|
||||
|
||||
try {
|
||||
// 读取 SQL 文件
|
||||
const sqlPath = path.join(__dirname, '002_rollback_to_platform_only.sql');
|
||||
const sql = fs.readFileSync(sqlPath, 'utf-8');
|
||||
|
||||
console.log('📄 SQL 文件已读取\n');
|
||||
|
||||
// 分段执行(按 -- ========== 分割)
|
||||
const sections = sql.split(/-- ={40,}/);
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i].trim();
|
||||
if (!section || section.startsWith('/**')) continue;
|
||||
|
||||
console.log(`📦 执行第 ${i} 段...\n`);
|
||||
|
||||
// 分行执行(按分号分割)
|
||||
const statements = section
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s && !s.startsWith('--'));
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.length > 10) {
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(statement);
|
||||
console.log(` ✅ 执行成功: ${statement.substring(0, 60)}...`);
|
||||
} catch (error: any) {
|
||||
// 忽略某些非致命错误
|
||||
if (error.message.includes('does not exist')) {
|
||||
console.log(` ⚠️ 字段不存在(已是正确状态): ${error.message}`);
|
||||
} else if (error.message.includes('✅')) {
|
||||
console.log(` ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 回滚迁移执行成功!');
|
||||
console.log('\n📊 验证结果:');
|
||||
console.log(' ✅ ASL 业务表:已删除 6 个任务管理字段');
|
||||
console.log(' ✅ DC 业务表:保持原状(无需添加)');
|
||||
console.log(' ✅ Platform 层:job 表统一管理所有任务');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ 迁移失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
runMigration()
|
||||
.then(() => {
|
||||
console.log('\n✅ 完成');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n❌ 错误:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
150
backend/prisma/manual-migrations/run-migration.ts
Normal file
150
backend/prisma/manual-migrations/run-migration.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 手动执行SQL迁移脚本
|
||||
* 用于Postgres-Only改造
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function runMigration() {
|
||||
try {
|
||||
console.log('🚀 开始执行手动迁移...\n');
|
||||
|
||||
// 步骤1: 创建app_cache表
|
||||
console.log('📦 [1/4] 创建 app_cache 表...');
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
CREATE TABLE IF NOT EXISTS platform_schema.app_cache (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(500) UNIQUE NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
console.log(' ✅ app_cache 表创建成功');
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('already exists')) {
|
||||
console.log(' ⚠️ 表已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤2: 创建索引
|
||||
console.log('\n📊 [2/4] 创建索引...');
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
CREATE INDEX IF NOT EXISTS idx_app_cache_expires
|
||||
ON platform_schema.app_cache(expires_at)
|
||||
`;
|
||||
console.log(' ✅ idx_app_cache_expires 创建成功');
|
||||
} catch (error: any) {
|
||||
console.log(' ⚠️ 索引可能已存在');
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
CREATE INDEX IF NOT EXISTS idx_app_cache_key_expires
|
||||
ON platform_schema.app_cache(key, expires_at)
|
||||
`;
|
||||
console.log(' ✅ idx_app_cache_key_expires 创建成功');
|
||||
} catch (error: any) {
|
||||
console.log(' ⚠️ 索引可能已存在');
|
||||
}
|
||||
|
||||
// 步骤3: 添加任务拆分字段
|
||||
console.log('\n🔧 [3/4] 添加任务拆分字段...');
|
||||
const splitFields = [
|
||||
{ name: 'total_batches', type: 'INTEGER', default: '1' },
|
||||
{ name: 'processed_batches', type: 'INTEGER', default: '0' },
|
||||
{ name: 'current_batch_index', type: 'INTEGER', default: '0' },
|
||||
];
|
||||
|
||||
for (const field of splitFields) {
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`
|
||||
ALTER TABLE asl_schema.screening_tasks
|
||||
ADD COLUMN IF NOT EXISTS ${field.name} ${field.type} DEFAULT ${field.default}
|
||||
`);
|
||||
console.log(` ✅ ${field.name} 添加成功`);
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('already exists') || error.message.includes('duplicate')) {
|
||||
console.log(` ⚠️ ${field.name} 已存在`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤4: 添加断点续传字段
|
||||
console.log('\n💾 [4/4] 添加断点续传字段...');
|
||||
const checkpointFields = [
|
||||
{ name: 'current_index', type: 'INTEGER', default: '0' },
|
||||
{ name: 'last_checkpoint', type: 'TIMESTAMP', default: null },
|
||||
{ name: 'checkpoint_data', type: 'JSONB', default: null },
|
||||
];
|
||||
|
||||
for (const field of checkpointFields) {
|
||||
try {
|
||||
const defaultClause = field.default !== null ? `DEFAULT ${field.default}` : '';
|
||||
await prisma.$executeRawUnsafe(`
|
||||
ALTER TABLE asl_schema.screening_tasks
|
||||
ADD COLUMN IF NOT EXISTS ${field.name} ${field.type} ${defaultClause}
|
||||
`);
|
||||
console.log(` ✅ ${field.name} 添加成功`);
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('already exists') || error.message.includes('duplicate')) {
|
||||
console.log(` ⚠️ ${field.name} 已存在`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 迁移执行完成!\n');
|
||||
|
||||
// 验证结果
|
||||
console.log('📊 验证缓存表...');
|
||||
const cacheCheck = await prisma.$queryRaw`
|
||||
SELECT table_name, column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'platform_schema'
|
||||
AND table_name = 'app_cache'
|
||||
ORDER BY ordinal_position
|
||||
`;
|
||||
console.log('app_cache表字段:', cacheCheck);
|
||||
|
||||
console.log('\n📊 验证任务表新字段...');
|
||||
const taskCheck = await prisma.$queryRaw`
|
||||
SELECT column_name, data_type, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'asl_schema'
|
||||
AND table_name = 'screening_tasks'
|
||||
AND column_name IN (
|
||||
'total_batches', 'processed_batches', 'current_batch_index',
|
||||
'current_index', 'last_checkpoint', 'checkpoint_data'
|
||||
)
|
||||
ORDER BY ordinal_position
|
||||
`;
|
||||
console.log('screening_tasks新字段:', taskCheck);
|
||||
|
||||
console.log('\n✅ 所有验证通过!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 迁移失败:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
runMigration();
|
||||
|
||||
@@ -11,3 +11,8 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,3 +38,8 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["multiSchema"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@@ -11,6 +12,23 @@ datasource db {
|
||||
schemas = ["platform_schema", "aia_schema", "pkb_schema", "asl_schema", "common_schema", "dc_schema", "rvw_schema", "admin_schema", "ssa_schema", "st_schema", "public"]
|
||||
}
|
||||
|
||||
// ==================== 平台基础设施 (Platform Infrastructure) ====================
|
||||
|
||||
/// 应用缓存表 - Postgres-Only架构
|
||||
/// 用于替代Redis缓存,支持LLM结果缓存、健康检查缓存等
|
||||
model AppCache {
|
||||
id Int @id @default(autoincrement())
|
||||
key String @unique @db.VarChar(500)
|
||||
value Json
|
||||
expiresAt DateTime @map("expires_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([expiresAt], name: "idx_app_cache_expires")
|
||||
@@index([key, expiresAt], name: "idx_app_cache_key_expires")
|
||||
@@map("app_cache")
|
||||
@@schema("platform_schema")
|
||||
}
|
||||
|
||||
// ==================== 用户模块 ====================
|
||||
|
||||
model User {
|
||||
@@ -576,6 +594,9 @@ model AslScreeningTask {
|
||||
failedItems Int @default(0) @map("failed_items")
|
||||
conflictItems Int @default(0) @map("conflict_items")
|
||||
|
||||
// ✅ 任务拆分和断点续传由 pg-boss (platform_schema.job.data) 统一管理
|
||||
// 不在业务表中存储这些信息,符合3层架构原则
|
||||
|
||||
// 时间信息
|
||||
startedAt DateTime? @map("started_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
@@ -798,6 +819,9 @@ model DCExtractionTask {
|
||||
// 错误信息
|
||||
error String? @map("error")
|
||||
|
||||
// ✅ 任务拆分和断点续传由 pg-boss (platform_schema.job.data) 统一管理
|
||||
// 不在业务表中存储这些信息,符合3层架构原则
|
||||
|
||||
// 时间戳
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
startedAt DateTime? @map("started_at")
|
||||
|
||||
Reference in New Issue
Block a user