From 83e395824bb4a89a497bf677d9b76262e804d90f Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Sun, 15 Mar 2026 11:51:35 +0800 Subject: [PATCH] feat(rvw): complete journal config center MVP and tenant login routing Deliver the RVW V4.0 journal configuration center across backend, frontend, migration, and docs with zh/en editorial baseline support and tenant-level prompt/template overrides. Unify tenant login to /:tenantCode/login and auto-enable RVW module when tenant type is JOURNAL to prevent post-login access gaps. Made-with: Cursor --- .../migration.sql | 49 ++ backend/prisma/schema.prisma | 35 +- backend/scripts/test-rvw-journal-e2e.ts | 496 ++++++++++++++++++ backend/src/common/prompt/prompt.fallbacks.ts | 46 ++ backend/src/index.ts | 4 +- .../journal-config/journalConfigController.ts | 75 +++ .../journal-config/journalConfigRoutes.ts | 29 + .../journal-config/journalConfigService.ts | 95 ++++ .../admin/rvw-config/rvwConfigController.ts | 24 +- .../admin/rvw-config/rvwConfigService.ts | 37 +- .../modules/admin/services/tenantService.ts | 81 ++- .../src/modules/admin/types/tenant.types.ts | 19 +- .../modules/rvw/services/clinicalService.ts | 17 +- .../modules/rvw/services/editorialService.ts | 29 +- backend/src/modules/rvw/skills/core/types.ts | 20 +- .../skills/library/ClinicalAssessmentSkill.ts | 8 +- .../rvw/skills/library/DataForensicsSkill.ts | 16 +- .../rvw/skills/library/EditorialSkill.ts | 12 +- .../src/modules/rvw/workers/reviewWorker.ts | 45 +- .../00-系统当前状态与开发指南.md | 15 +- .../RVW-稿件审查系统/00-模块当前状态与开发指南.md | 27 +- .../00-系统设计/V3.0/JTIM稿约规范与差异分析报告.md | 30 ++ .../V3.0/Medical Review期刊专属稿约提示词.md | 39 ++ .../V3.0/RVW V4.0 期刊租户配置中心开发需求.md | 131 ----- .../V3.0/RVW V4.0 期刊租户配置中心开发需求0315.md | 161 ++++++ .../V3.0/中文医学期刊2025新国标稿约审查提示词.md | 44 ++ .../00-系统设计/V3.0/通用英文期刊稿约审查提示词.md | 32 ++ .../04-开发计划/RVW V4.0 期刊配置中心MVP开发计划.md | 326 ++++++++++++ .../RVW-稿件审查系统/05-测试文档/Dongen 2003.pdf | Bin 0 -> 163043 bytes .../08-技术架构建议/ADMIN多业态租户架构演进评估 (1).md | 117 +++++ .../08-技术架构建议/期刊配置中心MVP计划审查报告.md | 76 +++ docs/05-部署文档/03-待部署变更清单.md | 17 +- frontend-v2/src/App.tsx | 11 +- .../src/framework/layout/AdminLayout.tsx | 1 + .../framework/layout/TenantPortalLayout.tsx | 2 +- .../src/framework/router/RouteGuard.tsx | 8 +- frontend-v2/src/pages/LoginPage.tsx | 6 +- frontend-v2/src/pages/TenantLoginPage.tsx | 2 +- .../JournalConfigDetailPage.tsx | 233 ++++++++ .../journal-configs/JournalConfigListPage.tsx | 162 ++++++ .../journal-configs/api/journalConfigApi.ts | 116 ++++ .../pages/admin/tenants/TenantDetailPage.tsx | 135 ++--- .../pages/admin/tenants/TenantListPage.tsx | 2 + .../src/pages/admin/tenants/api/tenantApi.ts | 37 +- 44 files changed, 2555 insertions(+), 312 deletions(-) create mode 100644 backend/prisma/migrations/20260315_journal_config_center_mvp/migration.sql create mode 100644 backend/scripts/test-rvw-journal-e2e.ts create mode 100644 backend/src/modules/admin/journal-config/journalConfigController.ts create mode 100644 backend/src/modules/admin/journal-config/journalConfigRoutes.ts create mode 100644 backend/src/modules/admin/journal-config/journalConfigService.ts create mode 100644 docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/JTIM稿约规范与差异分析报告.md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/Medical Review期刊专属稿约提示词.md delete mode 100644 docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求.md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求0315.md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/中文医学期刊2025新国标稿约审查提示词.md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/通用英文期刊稿约审查提示词.md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊配置中心MVP开发计划.md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/05-测试文档/Dongen 2003.pdf create mode 100644 docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/ADMIN多业态租户架构演进评估 (1).md create mode 100644 docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/期刊配置中心MVP计划审查报告.md create mode 100644 frontend-v2/src/pages/admin/journal-configs/JournalConfigDetailPage.tsx create mode 100644 frontend-v2/src/pages/admin/journal-configs/JournalConfigListPage.tsx create mode 100644 frontend-v2/src/pages/admin/journal-configs/api/journalConfigApi.ts diff --git a/backend/prisma/migrations/20260315_journal_config_center_mvp/migration.sql b/backend/prisma/migrations/20260315_journal_config_center_mvp/migration.sql new file mode 100644 index 00000000..f1586394 --- /dev/null +++ b/backend/prisma/migrations/20260315_journal_config_center_mvp/migration.sql @@ -0,0 +1,49 @@ +-- RVW V4.0 期刊配置中心 MVP +-- 一次性补齐 tenants(P0/P1/P2+language) 与 tenant_rvw_configs(4维Prompt+Template) + +-- 1) 枚举:JournalLanguage +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'JournalLanguage' + AND n.nspname = 'platform_schema' + ) THEN + CREATE TYPE "platform_schema"."JournalLanguage" AS ENUM ('ZH', 'EN', 'OTHER'); + END IF; +END $$; + +-- 2) 枚举:TenantType 增加 JOURNAL +DO $$ +BEGIN + ALTER TYPE "platform_schema"."TenantType" ADD VALUE IF NOT EXISTS 'JOURNAL'; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +-- 3) tenants 表新增字段 +ALTER TABLE "platform_schema"."tenants" + ADD COLUMN IF NOT EXISTS "journal_language" "platform_schema"."JournalLanguage", + ADD COLUMN IF NOT EXISTS "journal_full_name" TEXT, + ADD COLUMN IF NOT EXISTS "logo_url" TEXT, + ADD COLUMN IF NOT EXISTS "brand_color" TEXT, + ADD COLUMN IF NOT EXISTS "login_background_url" TEXT; + +ALTER TABLE "platform_schema"."tenants" + ALTER COLUMN "journal_language" SET DEFAULT 'ZH'; + +-- 4) tenant_rvw_configs 表升级为 Prompt/Template 统一结构 +ALTER TABLE "platform_schema"."tenant_rvw_configs" + ADD COLUMN IF NOT EXISTS "editorial_base_standard" TEXT NOT NULL DEFAULT 'en', + ADD COLUMN IF NOT EXISTS "editorial_expert_prompt" TEXT, + ADD COLUMN IF NOT EXISTS "editorial_handlebars_template" TEXT, + ADD COLUMN IF NOT EXISTS "data_forensics_expert_prompt" TEXT, + ADD COLUMN IF NOT EXISTS "data_forensics_handlebars_template" TEXT, + ADD COLUMN IF NOT EXISTS "clinical_handlebars_template" TEXT; + +ALTER TABLE "platform_schema"."tenant_rvw_configs" + DROP COLUMN IF EXISTS "editorial_rules", + DROP COLUMN IF EXISTS "data_forensics_level", + DROP COLUMN IF EXISTS "finer_weights"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4674fb6b..2de37ced 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1915,6 +1915,11 @@ model tenants { code String @unique name String type TenantType + journal_language JournalLanguage? @default(ZH) + journal_full_name String? + logo_url String? + brand_color String? + login_background_url String? status TenantStatus @default(ACTIVE) config Json? @default("{}") total_quota BigInt @default(0) @@ -1964,8 +1969,14 @@ model TenantRvwConfig { tenantId String @unique @map("tenant_id") tenant tenants @relation(fields: [tenantId], references: [id], onDelete: Cascade) - /// 稿约规范评估:规则数组,每条规则含 code/description/fatal - editorialRules Json? @map("editorial_rules") + /// 稿约规范评估:默认基线(zh/en),用于继承模式的系统默认 Prompt 路由 + editorialBaseStandard String @default("en") @map("editorial_base_standard") + + /// 稿约规范评估:租户自定义业务 Prompt(null 表示继承系统默认) + editorialExpertPrompt String? @map("editorial_expert_prompt") @db.Text + + /// 稿约规范评估:租户自定义报告模板(null 表示继承系统默认) + editorialHandlebarsTemplate String? @map("editorial_handlebars_template") @db.Text /// 方法学评估:专家业务评判标准(纯文本,可自由编辑,不需懂JSON) methodologyExpertPrompt String? @map("methodology_expert_prompt") @db.Text @@ -1973,15 +1984,18 @@ model TenantRvwConfig { /// 方法学评估:Handlebars 报告展示模板(可覆盖系统默认模板) methodologyHandlebarsTemplate String? @map("methodology_handlebars_template") @db.Text - /// 数据验证:验证深度 L1/L2/L3 - dataForensicsLevel String @default("L2") @map("data_forensics_level") + /// 数据验证:租户自定义业务 Prompt(null 表示继承系统默认) + dataForensicsExpertPrompt String? @map("data_forensics_expert_prompt") @db.Text - /// 临床评估:FINER 五维权重 {feasibility, innovation, ethics, relevance, novelty} - finerWeights Json? @map("finer_weights") + /// 数据验证:租户自定义报告模板(null 表示继承系统默认) + dataForensicsHandlebarsTemplate String? @map("data_forensics_handlebars_template") @db.Text /// 临床评估:专科特色补充要求(纯文本) clinicalExpertPrompt String? @map("clinical_expert_prompt") @db.Text + /// 临床评估:租户自定义报告模板(null 表示继承系统默认) + clinicalHandlebarsTemplate String? @map("clinical_handlebars_template") @db.Text + createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -2011,12 +2025,21 @@ enum TenantStatus { enum TenantType { HOSPITAL // 医院 PHARMA // 药企 + JOURNAL // 期刊 INTERNAL // 内部(公司自己) PUBLIC // 个人用户公共池 @@schema("platform_schema") } +enum JournalLanguage { + ZH + EN + OTHER + + @@schema("platform_schema") +} + enum UserRole { SUPER_ADMIN PROMPT_ENGINEER diff --git a/backend/scripts/test-rvw-journal-e2e.ts b/backend/scripts/test-rvw-journal-e2e.ts new file mode 100644 index 00000000..33f2b0a1 --- /dev/null +++ b/backend/scripts/test-rvw-journal-e2e.ts @@ -0,0 +1,496 @@ +/** + * RVW V4.0 期刊配置中心 E2E 脚本 + * + * 覆盖链路: + * 1) 登录(密码) + * 2) 期刊配置(journalLanguage + editorialBaseStandard) + * 3) 业务端上传稿件并执行审评 + * 4) 中英文场景使用不同 Skills + * 5) 轮询任务完成并输出结果快照 + * + * 运行方式(PowerShell): + * npx tsx scripts/test-rvw-journal-e2e.ts + * + * 可选环境变量: + * - RVW_E2E_BASE_URL=http://localhost:3001 + * - RVW_E2E_PHONE=13900139001 + * - RVW_E2E_PASSWORD=Test@1234 + * - RVW_E2E_ZH_TENANT_CODE=cmj + * - RVW_E2E_EN_TENANT_CODE=jtim + * - RVW_E2E_POLL_INTERVAL_MS=8000 + * - RVW_E2E_MAX_POLLS=180 + */ + +import axios, { AxiosError, AxiosInstance } from 'axios'; +import FormData from 'form-data'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { PrismaClient } from '@prisma/client'; + +type AgentType = 'editorial' | 'methodology' | 'clinical'; +type JournalLanguage = 'ZH' | 'EN' | 'OTHER'; +type EditorialBaseStandard = 'zh' | 'en'; +type TaskStatus = 'pending' | 'extracting' | 'reviewing' | 'completed' | 'partial_completed' | 'failed'; + +interface LoginResponse { + success: boolean; + data?: { + tokens?: { + accessToken?: string; + }; + }; + message?: string; +} + +interface JournalItem { + id: string; + code: string; + name: string; + journalLanguage?: JournalLanguage | null; +} + +interface JournalListResponse { + success: boolean; + data: JournalItem[]; +} + +interface CreateTaskResponse { + success: boolean; + data?: { + taskId: string; + }; + message?: string; +} + +interface RunTaskResponse { + success: boolean; + data?: { + taskId: string; + jobId: string; + }; + message?: string; +} + +interface TaskDetailResponse { + success: boolean; + data?: { + id: string; + status: TaskStatus; + errorMessage?: string | null; + overallScore?: number | null; + editorialScore?: number | null; + methodologyScore?: number | null; + modelUsed?: string | null; + completedAt?: string | null; + }; +} + +interface Scenario { + id: string; + name: string; + tenantCode: string; + journalLanguage: JournalLanguage; + editorialBaseStandard: EditorialBaseStandard; + filePath: string; + agents: AgentType[]; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..', '..'); + +const BASE_URL = process.env.RVW_E2E_BASE_URL || 'http://localhost:3001'; +const PHONE = process.env.RVW_E2E_PHONE || '13900139001'; +const PASSWORD = process.env.RVW_E2E_PASSWORD || 'Test@1234'; +const ZH_TENANT_CODE = (process.env.RVW_E2E_ZH_TENANT_CODE || 'cmj').toLowerCase(); +const EN_TENANT_CODE = (process.env.RVW_E2E_EN_TENANT_CODE || 'jtim').toLowerCase(); +const POLL_INTERVAL_MS = Number(process.env.RVW_E2E_POLL_INTERVAL_MS || 8000); +const MAX_POLLS = Number(process.env.RVW_E2E_MAX_POLLS || 180); +const ALLOW_DB_FALLBACK = (process.env.RVW_E2E_ALLOW_DB_FALLBACK || 'true') === 'true'; +const SCENARIO_FILTER = (process.env.RVW_E2E_SCENARIOS || '').trim(); + +const prisma = new PrismaClient(); + +const scenarios: Scenario[] = [ + { + id: 'zh-docx', + name: '中文期刊场景(稿约+方法学)', + tenantCode: ZH_TENANT_CODE, + journalLanguage: 'ZH', + editorialBaseStandard: 'zh', + filePath: path.resolve( + repoRoot, + 'docs/03-业务模块/RVW-稿件审查系统/05-测试文档/骶骨瘤患者围术期大量输血的术前危险因素分析及输血策略2月27 - 副本.docx' + ), + agents: ['editorial', 'methodology'], + }, + { + id: 'en-pdf', + name: '英文期刊场景(稿约+临床)', + tenantCode: EN_TENANT_CODE, + journalLanguage: 'EN', + editorialBaseStandard: 'en', + filePath: path.resolve( + repoRoot, + 'docs/03-业务模块/RVW-稿件审查系统/05-测试文档/Dongen 2003.pdf' + ), + agents: ['editorial', 'clinical'], + }, +]; + +function getEnabledScenarios(): Scenario[] { + if (!SCENARIO_FILTER) return scenarios; + const ids = new Set( + SCENARIO_FILTER + .split(',') + .map(s => s.trim()) + .filter(Boolean) + ); + return scenarios.filter(s => ids.has(s.id)); +} + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function createClient(token?: string): AxiosInstance { + const headers: Record = {}; + if (token) headers.Authorization = `Bearer ${token}`; + return axios.create({ + baseURL: BASE_URL, + timeout: 120000, + headers, + validateStatus: () => true, + }); +} + +function unwrapError(error: unknown): string { + if (axios.isAxiosError(error)) { + const e = error as AxiosError; + const status = e.response?.status; + const message = (e.response?.data as any)?.message || e.message; + return `[HTTP ${status ?? 'N/A'}] ${message}`; + } + return error instanceof Error ? error.message : String(error); +} + +async function loginByPassword(): Promise { + const client = createClient(); + const resp = await client.post('/api/v1/auth/login/password', { + phone: PHONE, + password: PASSWORD, + }); + if (resp.status !== 200 || !resp.data?.success || !resp.data?.data?.tokens?.accessToken) { + throw new Error(`登录失败: ${resp.status} ${resp.data?.message || ''}`.trim()); + } + return resp.data.data.tokens.accessToken; +} + +async function getJournalByCode(client: AxiosInstance, code: string): Promise { + const resp = await client.get('/api/admin/journal-configs', { + params: { search: code, page: 1, limit: 200 }, + }); + if (resp.status !== 200 || !resp.data?.success || !Array.isArray(resp.data.data)) { + throw new Error(`获取期刊列表失败: ${resp.status}`); + } + const exact = resp.data.data.find(item => item.code.toLowerCase() === code.toLowerCase()); + if (!exact) { + throw new Error(`未找到期刊租户 code=${code},请先在管理端创建并启用 JOURNAL 租户`); + } + return exact; +} + +async function getJournalByCodeFromDb(code: string): Promise { + const tenant = await prisma.tenants.findUnique({ + where: { code }, + select: { id: true, code: true, name: true, journal_language: true }, + }); + if (!tenant) { + throw new Error(`DB fallback 失败:未找到租户 code=${code}`); + } + return { + id: tenant.id, + code: tenant.code, + name: tenant.name, + journalLanguage: (tenant.journal_language as JournalLanguage | null) ?? null, + }; +} + +async function configureJournal( + client: AxiosInstance, + journalId: string, + journalLanguage: JournalLanguage, + editorialBaseStandard: EditorialBaseStandard +) { + const basicResp = await client.put(`/api/admin/journal-configs/${journalId}/basic-info`, { + journalLanguage, + }); + if (basicResp.status < 200 || basicResp.status >= 300) { + throw new Error(`更新期刊基础信息失败: ${basicResp.status} ${(basicResp.data as any)?.message || ''}`.trim()); + } + + const rvwResp = await client.put(`/api/admin/journal-configs/${journalId}/rvw-config`, { + editorialBaseStandard, + // 继承系统默认,验证 null 语义 + editorialExpertPrompt: null, + methodologyExpertPrompt: null, + dataForensicsExpertPrompt: null, + clinicalExpertPrompt: null, + }); + if (rvwResp.status < 200 || rvwResp.status >= 300) { + throw new Error(`更新期刊审稿配置失败: ${rvwResp.status} ${(rvwResp.data as any)?.message || ''}`.trim()); + } +} + +async function configureJournalByDb( + journalId: string, + journalLanguage: JournalLanguage, + editorialBaseStandard: EditorialBaseStandard +) { + await prisma.tenants.update({ + where: { id: journalId }, + data: { + journal_language: journalLanguage as any, + }, + }); + + await prisma.tenantRvwConfig.upsert({ + where: { tenantId: journalId }, + create: { + tenantId: journalId, + editorialBaseStandard, + editorialExpertPrompt: null, + methodologyExpertPrompt: null, + dataForensicsExpertPrompt: null, + clinicalExpertPrompt: null, + }, + update: { + editorialBaseStandard, + editorialExpertPrompt: null, + methodologyExpertPrompt: null, + dataForensicsExpertPrompt: null, + clinicalExpertPrompt: null, + updatedAt: new Date(), + }, + }); +} + +async function uploadTask( + client: AxiosInstance, + tenantCode: string, + filePath: string +): Promise { + if (!fs.existsSync(filePath)) { + throw new Error(`测试文件不存在: ${filePath}`); + } + + const form = new FormData(); + form.append('file', fs.createReadStream(filePath), path.basename(filePath)); + form.append('modelType', 'deepseek-v3'); + + const resp = await client.post('/api/v1/rvw/tasks', form, { + headers: { + ...form.getHeaders(), + 'x-tenant-id': tenantCode, + }, + maxBodyLength: Infinity, + maxContentLength: Infinity, + }); + + if (resp.status !== 200 || !resp.data?.success || !resp.data?.data?.taskId) { + throw new Error(`上传稿件失败: ${resp.status} ${(resp.data as any)?.message || ''}`.trim()); + } + return resp.data.data.taskId; +} + +async function runTask(client: AxiosInstance, tenantCode: string, taskId: string, agents: AgentType[]): Promise { + // 文档提取为异步,run 可能返回“尚未提取完成”,这里做重试以稳定 E2E + for (let i = 1; i <= 30; i += 1) { + const resp = await client.post( + `/api/v1/rvw/tasks/${taskId}/run`, + { agents }, + { headers: { 'x-tenant-id': tenantCode } } + ); + if (resp.status === 200 && resp.data?.success && resp.data?.data?.jobId) { + return resp.data.data.jobId; + } + + const message = String((resp.data as any)?.message || ''); + if (resp.status === 400 && message.includes('文档尚未提取完成')) { + await sleep(2000); + continue; + } + + throw new Error(`执行审评失败: ${resp.status} ${message}`.trim()); + } + throw new Error('执行审评超时:文档提取长时间未就绪'); +} + +async function pollTask(client: AxiosInstance, tenantCode: string, taskId: string) { + let lastStatus: TaskStatus | null = null; + for (let i = 1; i <= MAX_POLLS; i += 1) { + const resp = await client.get(`/api/v1/rvw/tasks/${taskId}`, { + headers: { 'x-tenant-id': tenantCode }, + }); + if (resp.status !== 200 || !resp.data?.success || !resp.data?.data) { + throw new Error(`查询任务失败: ${resp.status} ${(resp.data as any)?.message || ''}`.trim()); + } + + const data = resp.data.data; + if (data.status !== lastStatus) { + lastStatus = data.status; + console.log(` - 任务状态: ${data.status}`); + } + + if (data.status === 'completed' || data.status === 'partial_completed' || data.status === 'failed') { + return data; + } + + await sleep(POLL_INTERVAL_MS); + } + + throw new Error(`任务轮询超时(>${MAX_POLLS} 次)`); +} + +async function runScenario(client: AxiosInstance, scenario: Scenario) { + console.log(`\n▶ 场景: ${scenario.name}`); + console.log(` 期刊租户: ${scenario.tenantCode}`); + console.log(` 测试文件: ${scenario.filePath}`); + console.log(` Skills: ${scenario.agents.join(', ')}`); + + let journal: JournalItem; + let configMode: 'api' | 'db-fallback' = 'api'; + + try { + journal = await getJournalByCode(client, scenario.tenantCode); + } catch (error) { + const msg = unwrapError(error); + if (!ALLOW_DB_FALLBACK || !msg.includes('403')) throw error; + journal = await getJournalByCodeFromDb(scenario.tenantCode); + configMode = 'db-fallback'; + } + console.log(` 已找到期刊: ${journal.name} (${journal.code})`); + + try { + await configureJournal(client, journal.id, scenario.journalLanguage, scenario.editorialBaseStandard); + configMode = 'api'; + } catch (error) { + const msg = unwrapError(error); + if (!ALLOW_DB_FALLBACK || !msg.includes('403')) throw error; + await configureJournalByDb(journal.id, scenario.journalLanguage, scenario.editorialBaseStandard); + configMode = 'db-fallback'; + } + console.log(` 已配置期刊(${configMode}): journalLanguage=${scenario.journalLanguage}, editorialBaseStandard=${scenario.editorialBaseStandard}`); + + const taskId = await uploadTask(client, scenario.tenantCode, scenario.filePath); + console.log(` 上传成功,taskId=${taskId}`); + + const jobId = await runTask(client, scenario.tenantCode, taskId, scenario.agents); + console.log(` 已启动审评,jobId=${jobId}`); + + const finalTask = await pollTask(client, scenario.tenantCode, taskId); + console.log(` 场景完成,最终状态=${finalTask.status}`); + + let reportStatus = 'not-requested'; + if (finalTask.status !== 'failed') { + const reportResp = await client.get(`/api/v1/rvw/tasks/${taskId}/report`, { + headers: { 'x-tenant-id': scenario.tenantCode }, + validateStatus: () => true, + }); + reportStatus = `${reportResp.status}`; + } + + return { + scenarioId: scenario.id, + scenarioName: scenario.name, + tenantCode: scenario.tenantCode, + journalId: journal.id, + configMode, + journalLanguage: scenario.journalLanguage, + editorialBaseStandard: scenario.editorialBaseStandard, + filePath: scenario.filePath, + agents: scenario.agents, + taskId, + jobId, + finalStatus: finalTask.status, + overallScore: finalTask.overallScore ?? null, + editorialScore: finalTask.editorialScore ?? null, + methodologyScore: finalTask.methodologyScore ?? null, + modelUsed: finalTask.modelUsed ?? null, + completedAt: finalTask.completedAt ?? null, + errorMessage: finalTask.errorMessage ?? null, + reportStatus, + }; +} + +async function main() { + console.log('═══════════════════════════════════════════════════════'); + console.log('RVW V4.0 期刊配置中心 E2E 测试'); + console.log('═══════════════════════════════════════════════════════'); + console.log(`BASE_URL: ${BASE_URL}`); + console.log(`登录账号: ${PHONE}`); + console.log(`中文租户: ${ZH_TENANT_CODE} | 英文租户: ${EN_TENANT_CODE}`); + console.log(`轮询参数: interval=${POLL_INTERVAL_MS}ms, max=${MAX_POLLS}`); + console.log(`配置模式: API优先,DB fallback=${ALLOW_DB_FALLBACK ? '启用' : '禁用'}`); + console.log(`场景过滤: ${SCENARIO_FILTER || '全部'}`); + + const startedAt = new Date(); + const token = await loginByPassword(); + console.log('\n✅ 登录成功'); + + const client = createClient(token); + const results: any[] = []; + + const enabledScenarios = getEnabledScenarios(); + for (const scenario of enabledScenarios) { + try { + const result = await runScenario(client, scenario); + results.push({ success: true, ...result }); + } catch (error) { + const message = unwrapError(error); + console.error(` ❌ 场景失败: ${message}`); + results.push({ + success: false, + scenarioId: scenario.id, + scenarioName: scenario.name, + tenantCode: scenario.tenantCode, + filePath: scenario.filePath, + agents: scenario.agents, + error: message, + }); + } + } + + const endedAt = new Date(); + const outputDir = path.resolve(repoRoot, 'backend/scripts/output'); + await fs.promises.mkdir(outputDir, { recursive: true }); + const outputPath = path.resolve(outputDir, `rvw-journal-e2e-${Date.now()}.json`); + + const summary = { + startedAt: startedAt.toISOString(), + endedAt: endedAt.toISOString(), + durationSeconds: Math.round((endedAt.getTime() - startedAt.getTime()) / 1000), + baseUrl: BASE_URL, + operatorPhone: PHONE, + scenarios: results, + }; + + await fs.promises.writeFile(outputPath, JSON.stringify(summary, null, 2), 'utf-8'); + + const passed = results.filter(r => r.success).length; + const failed = results.length - passed; + console.log('\n═══════════════════════════════════════════════════════'); + console.log(`完成:通过 ${passed},失败 ${failed}`); + console.log(`结果文件:${outputPath}`); + console.log('═══════════════════════════════════════════════════════'); + + if (failed > 0) { + process.exitCode = 1; + } +} + +main().catch((error) => { + console.error('\n❌ 脚本执行失败:', unwrapError(error)); + process.exit(1); +}); diff --git a/backend/src/common/prompt/prompt.fallbacks.ts b/backend/src/common/prompt/prompt.fallbacks.ts index 77e569c9..02bbf47d 100644 --- a/backend/src/common/prompt/prompt.fallbacks.ts +++ b/backend/src/common/prompt/prompt.fallbacks.ts @@ -21,6 +21,52 @@ interface FallbackPrompt { * RVW 模块兜底 Prompt */ const RVW_FALLBACKS: Record = { + RVW_EDITORIAL_EN: { + content: `You are a rigorous medical journal editor. Review the manuscript against international ICMJE-style editorial compliance. + +Fatal checks (must flag as fatal if violated): +1) Ethics and informed consent statements for human/animal research. +2) Conflict of interest and funding statements (including "None declared" when absent). +3) AI authorship compliance (AI tools must not be listed as authors). +4) Blind-review leakage in main text (author/institution/funding identifiers). + +Major checks: +5) Abstract structure and length compliance by manuscript type. +6) Statistical expression standards (exact P values, notation consistency). +7) Citation-reference consistency and formatting integrity. +8) Figure/table editability and in-text cross-reference integrity. + +Minor checks: +9) Consistent English variant (US or UK). +10) Number spelling conventions. +11) Scientific term formatting (e.g., italics for species/gene symbols). +12) SI unit compliance. + +Return structured JSON only, including overall_score and items. +If no violation is found, clearly indicate all checks passed.`, + modelConfig: { model: 'deepseek-v3', temperature: 0.3 }, + }, + + RVW_EDITORIAL_ZH: { + content: `你是一位专业的医学期刊编辑,负责评估稿件的规范性。 + +【评估标准】 +1. 文稿科学性与实用性 +2. 文题(中文不超过20字,英文不超过10实词) +3. 作者格式 +4. 摘要(300-500字,含目的、方法、结果、结论) +5. 关键词(2-5个) +6. 医学名词和药物名称 +7. 缩略语 +8. 计量单位 +9. 图片格式 +10. 动态图像 +11. 参考文献 + +请输出JSON格式的评估结果,包含overall_score和items数组。`, + modelConfig: { model: 'deepseek-v3', temperature: 0.3 }, + }, + RVW_EDITORIAL: { content: `你是一位专业的医学期刊编辑,负责评估稿件的规范性。 diff --git a/backend/src/index.ts b/backend/src/index.ts index 212f7839..32f142d2 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -112,9 +112,11 @@ import { statsRoutes, userOverviewRoute } from './modules/admin/routes/statsRout import { systemKbRoutes } from './modules/admin/system-kb/index.js'; import { iitProjectRoutes, iitQcRuleRoutes, iitUserMappingRoutes, iitBatchRoutes, iitQcCockpitRoutes, iitEqueryRoutes } from './modules/admin/iit-projects/index.js'; import { rvwConfigRoutes } from './modules/admin/rvw-config/rvwConfigRoutes.js'; +import { journalConfigRoutes } from './modules/admin/journal-config/journalConfigRoutes.js'; import { authenticate, requireRoles } from './common/auth/auth.middleware.js'; await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' }); await fastify.register(rvwConfigRoutes, { prefix: '/api/admin/tenants' }); +await fastify.register(journalConfigRoutes, { prefix: '/api/admin/journal-configs' }); await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' }); await fastify.register(userRoutes, { prefix: '/api/admin/users' }); await fastify.register(statsRoutes, { prefix: '/api/admin/stats' }); @@ -133,7 +135,7 @@ await fastify.register(async (scope) => { await scope.register(iitEqueryRoutes); }, { prefix: '/api/v1/admin/iit-projects' }); -logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/modules, /api/admin/users, /api/admin/stats, /api/v1/admin/system-kb, /api/v1/admin/iit-projects (authenticated)'); +logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/journal-configs, /api/admin/modules, /api/admin/users, /api/admin/stats, /api/v1/admin/system-kb, /api/v1/admin/iit-projects (authenticated)'); // ============================================ // 【临时】平台基础设施测试API diff --git a/backend/src/modules/admin/journal-config/journalConfigController.ts b/backend/src/modules/admin/journal-config/journalConfigController.ts new file mode 100644 index 00000000..8c56c726 --- /dev/null +++ b/backend/src/modules/admin/journal-config/journalConfigController.ts @@ -0,0 +1,75 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { logger } from '../../../common/logging/index.js'; +import * as service from './journalConfigService.js'; +import type { TenantListQuery, UpdateTenantRequest } from '../types/tenant.types.js'; +import type { UpdateRvwConfigDto } from '../rvw-config/rvwConfigService.js'; + +interface TenantIdParams { + id: string; +} + +export async function listJournalConfigs( + request: FastifyRequest<{ Querystring: Omit }>, + reply: FastifyReply +) { + try { + const result = await service.listJournalConfigs(request.query); + return reply.send({ success: true, ...result }); + } catch (error: any) { + logger.error('[JournalConfig] 获取期刊配置列表失败', { error: error.message }); + return reply.status(500).send({ success: false, message: error.message || '获取期刊配置列表失败' }); + } +} + +export async function getJournalConfigDetail( + request: FastifyRequest<{ Params: TenantIdParams }>, + reply: FastifyReply +) { + try { + const detail = await service.getJournalConfigDetail(request.params.id); + if (!detail) { + return reply.status(404).send({ success: false, message: '期刊租户不存在' }); + } + return reply.send({ success: true, data: detail }); + } catch (error: any) { + logger.error('[JournalConfig] 获取详情失败', { error: error.message }); + return reply.status(500).send({ success: false, message: error.message || '获取详情失败' }); + } +} + +export async function updateJournalBasicInfo( + request: FastifyRequest<{ Params: TenantIdParams; Body: UpdateTenantRequest }>, + reply: FastifyReply +) { + try { + const dto = request.body || {}; + const updated = await service.updateJournalBasicInfo(request.params.id, dto); + return reply.send({ success: true, data: updated, message: '基础信息已保存' }); + } catch (error: any) { + if (error?.message?.includes('不存在')) { + return reply.status(404).send({ success: false, message: error.message }); + } + logger.error('[JournalConfig] 保存基础信息失败', { error: error.message }); + return reply.status(500).send({ success: false, message: error.message || '保存基础信息失败' }); + } +} + +export async function updateJournalRvwConfig( + request: FastifyRequest<{ Params: TenantIdParams; Body: UpdateRvwConfigDto }>, + reply: FastifyReply +) { + try { + const dto = request.body || {}; + if (dto.editorialBaseStandard && !['zh', 'en'].includes(dto.editorialBaseStandard)) { + return reply.status(400).send({ success: false, message: 'editorialBaseStandard 必须为 zh / en' }); + } + const updated = await service.updateJournalRvwConfig(request.params.id, dto); + return reply.send({ success: true, data: updated, message: '审稿配置已保存' }); + } catch (error: any) { + if (error?.message?.includes('不存在')) { + return reply.status(404).send({ success: false, message: error.message }); + } + logger.error('[JournalConfig] 保存审稿配置失败', { error: error.message }); + return reply.status(500).send({ success: false, message: error.message || '保存审稿配置失败' }); + } +} diff --git a/backend/src/modules/admin/journal-config/journalConfigRoutes.ts b/backend/src/modules/admin/journal-config/journalConfigRoutes.ts new file mode 100644 index 00000000..291a7b12 --- /dev/null +++ b/backend/src/modules/admin/journal-config/journalConfigRoutes.ts @@ -0,0 +1,29 @@ +import type { FastifyInstance } from 'fastify'; +import { authenticate, requireAnyPermission } from '../../../common/auth/auth.middleware.js'; +import * as controller from './journalConfigController.js'; + +/** + * 期刊配置中心路由 + * 前缀:/api/admin/journal-configs + */ +export async function journalConfigRoutes(fastify: FastifyInstance) { + fastify.get('/', { + preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')], + handler: controller.listJournalConfigs, + }); + + fastify.get('/:id', { + preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')], + handler: controller.getJournalConfigDetail, + }); + + fastify.put('/:id/basic-info', { + preHandler: [authenticate, requireAnyPermission('tenant:edit', 'ops:user-ops')], + handler: controller.updateJournalBasicInfo, + }); + + fastify.put('/:id/rvw-config', { + preHandler: [authenticate, requireAnyPermission('tenant:edit', 'ops:user-ops')], + handler: controller.updateJournalRvwConfig, + }); +} diff --git a/backend/src/modules/admin/journal-config/journalConfigService.ts b/backend/src/modules/admin/journal-config/journalConfigService.ts new file mode 100644 index 00000000..216204a0 --- /dev/null +++ b/backend/src/modules/admin/journal-config/journalConfigService.ts @@ -0,0 +1,95 @@ +import { prisma } from '../../../config/database.js'; +import { tenantService } from '../services/tenantService.js'; +import type { TenantListQuery, UpdateTenantRequest } from '../types/tenant.types.js'; +import type { UpdateRvwConfigDto } from '../rvw-config/rvwConfigService.js'; +import * as rvwConfigService from '../rvw-config/rvwConfigService.js'; + +export async function listJournalConfigs(query: Omit) { + return tenantService.listTenants({ + ...query, + type: 'JOURNAL', + }); +} + +export async function getJournalConfigDetail(tenantId: string) { + const tenant = await tenantService.getTenantDetail(tenantId); + if (!tenant || tenant.type !== 'JOURNAL') return null; + + const rvwConfig = await rvwConfigService.getRvwConfig(tenantId); + return { tenant, rvwConfig }; +} + +export async function updateJournalBasicInfo(tenantId: string, dto: UpdateTenantRequest) { + const tenant = await tenantService.getTenantDetail(tenantId); + if (!tenant || tenant.type !== 'JOURNAL') { + throw new Error('期刊租户不存在'); + } + return tenantService.updateTenant(tenantId, dto); +} + +export async function updateJournalRvwConfig(tenantId: string, dto: UpdateRvwConfigDto) { + const tenant = await tenantService.getTenantDetail(tenantId); + if (!tenant || tenant.type !== 'JOURNAL') { + throw new Error('期刊租户不存在'); + } + return rvwConfigService.upsertRvwConfig(tenantId, dto); +} + +/** + * 可选原子写接口:同时更新基础信息 + RVW 配置,避免跨表写入不一致 + */ +export async function saveJournalConfigAtomic( + tenantId: string, + basicInfo: UpdateTenantRequest, + rvwConfig: UpdateRvwConfigDto +) { + return prisma.$transaction(async (tx) => { + const tenant = await tx.tenants.update({ + where: { id: tenantId }, + data: { + name: basicInfo.name, + type: basicInfo.type as any, + journal_language: basicInfo.journalLanguage === null ? null : (basicInfo.journalLanguage as any), + journal_full_name: basicInfo.journalFullName === null ? null : basicInfo.journalFullName, + logo_url: basicInfo.logoUrl === null ? null : basicInfo.logoUrl, + brand_color: basicInfo.brandColor === null ? null : basicInfo.brandColor, + login_background_url: basicInfo.loginBackgroundUrl === null ? null : basicInfo.loginBackgroundUrl, + contact_name: basicInfo.contactName, + contact_phone: basicInfo.contactPhone, + contact_email: basicInfo.contactEmail, + expires_at: basicInfo.expiresAt === null ? null : (basicInfo.expiresAt ? new Date(basicInfo.expiresAt) : undefined), + updated_at: new Date(), + }, + }); + + const config = await tx.tenantRvwConfig.upsert({ + where: { tenantId }, + create: { + tenantId, + editorialBaseStandard: rvwConfig.editorialBaseStandard ?? 'en', + editorialExpertPrompt: rvwConfig.editorialExpertPrompt ?? null, + editorialHandlebarsTemplate: rvwConfig.editorialHandlebarsTemplate ?? null, + methodologyExpertPrompt: rvwConfig.methodologyExpertPrompt ?? null, + methodologyHandlebarsTemplate: rvwConfig.methodologyHandlebarsTemplate ?? null, + dataForensicsExpertPrompt: rvwConfig.dataForensicsExpertPrompt ?? null, + dataForensicsHandlebarsTemplate: rvwConfig.dataForensicsHandlebarsTemplate ?? null, + clinicalExpertPrompt: rvwConfig.clinicalExpertPrompt ?? null, + clinicalHandlebarsTemplate: rvwConfig.clinicalHandlebarsTemplate ?? null, + }, + update: { + ...(rvwConfig.editorialBaseStandard !== undefined && { editorialBaseStandard: rvwConfig.editorialBaseStandard }), + ...(rvwConfig.editorialExpertPrompt !== undefined && { editorialExpertPrompt: rvwConfig.editorialExpertPrompt }), + ...(rvwConfig.editorialHandlebarsTemplate !== undefined && { editorialHandlebarsTemplate: rvwConfig.editorialHandlebarsTemplate }), + ...(rvwConfig.methodologyExpertPrompt !== undefined && { methodologyExpertPrompt: rvwConfig.methodologyExpertPrompt }), + ...(rvwConfig.methodologyHandlebarsTemplate !== undefined && { methodologyHandlebarsTemplate: rvwConfig.methodologyHandlebarsTemplate }), + ...(rvwConfig.dataForensicsExpertPrompt !== undefined && { dataForensicsExpertPrompt: rvwConfig.dataForensicsExpertPrompt }), + ...(rvwConfig.dataForensicsHandlebarsTemplate !== undefined && { dataForensicsHandlebarsTemplate: rvwConfig.dataForensicsHandlebarsTemplate }), + ...(rvwConfig.clinicalExpertPrompt !== undefined && { clinicalExpertPrompt: rvwConfig.clinicalExpertPrompt }), + ...(rvwConfig.clinicalHandlebarsTemplate !== undefined && { clinicalHandlebarsTemplate: rvwConfig.clinicalHandlebarsTemplate }), + updatedAt: new Date(), + }, + }); + + return { tenant, config }; + }); +} diff --git a/backend/src/modules/admin/rvw-config/rvwConfigController.ts b/backend/src/modules/admin/rvw-config/rvwConfigController.ts index 02b1dbd8..d96344dc 100644 --- a/backend/src/modules/admin/rvw-config/rvwConfigController.ts +++ b/backend/src/modules/admin/rvw-config/rvwConfigController.ts @@ -50,9 +50,11 @@ export async function getRvwConfig( * * Body 示例: * { + * "editorialBaseStandard": "zh", + * "editorialExpertPrompt": null, * "methodologyExpertPrompt": "...", - * "dataForensicsLevel": "L2", - * "finerWeights": { "feasibility": 20, "innovation": 20, "ethics": 20, "relevance": 20, "novelty": 20 } + * "dataForensicsExpertPrompt": "...", + * "clinicalExpertPrompt": "..." * } */ export async function upsertRvwConfig( @@ -66,26 +68,14 @@ export async function upsertRvwConfig( const { id: tenantId } = request.params; const dto = request.body; - // 验证 dataForensicsLevel(若提供) - if (dto.dataForensicsLevel && !['L1', 'L2', 'L3'].includes(dto.dataForensicsLevel)) { + // 验证 editorialBaseStandard(若提供) + if (dto.editorialBaseStandard && !['zh', 'en'].includes(dto.editorialBaseStandard)) { return reply.status(400).send({ error: 'BadRequest', - message: 'dataForensicsLevel 必须为 L1 / L2 / L3', + message: 'editorialBaseStandard 必须为 zh / en', }); } - // 验证 finerWeights(若提供,权重之和应为 100) - if (dto.finerWeights && typeof dto.finerWeights === 'object') { - const weights = dto.finerWeights as Record; - const sum = Object.values(weights).reduce((a, b) => a + Number(b), 0); - if (Math.abs(sum - 100) > 1) { - return reply.status(400).send({ - error: 'BadRequest', - message: `FINER 权重之和应为 100,当前为 ${sum}`, - }); - } - } - const config = await rvwConfigService.upsertRvwConfig(tenantId, dto); return reply.status(200).send({ diff --git a/backend/src/modules/admin/rvw-config/rvwConfigService.ts b/backend/src/modules/admin/rvw-config/rvwConfigService.ts index e98128c9..a4729af1 100644 --- a/backend/src/modules/admin/rvw-config/rvwConfigService.ts +++ b/backend/src/modules/admin/rvw-config/rvwConfigService.ts @@ -9,30 +9,35 @@ import { prisma } from '../../../config/database.js'; import { logger } from '../../../common/logging/index.js'; -import type { Prisma } from '@prisma/client'; /** 审稿配置响应 DTO */ export interface RvwConfigDto { id: string; tenantId: string; - editorialRules: Prisma.JsonValue | null; + editorialBaseStandard: string; + editorialExpertPrompt: string | null; + editorialHandlebarsTemplate: string | null; methodologyExpertPrompt: string | null; methodologyHandlebarsTemplate: string | null; - dataForensicsLevel: string; - finerWeights: Prisma.JsonValue | null; + dataForensicsExpertPrompt: string | null; + dataForensicsHandlebarsTemplate: string | null; clinicalExpertPrompt: string | null; + clinicalHandlebarsTemplate: string | null; createdAt: Date; updatedAt: Date; } /** 审稿配置更新 DTO(所有字段可选,UPSERT 语义) */ export interface UpdateRvwConfigDto { - editorialRules?: Prisma.InputJsonValue | null; + editorialBaseStandard?: 'zh' | 'en'; + editorialExpertPrompt?: string | null; + editorialHandlebarsTemplate?: string | null; methodologyExpertPrompt?: string | null; methodologyHandlebarsTemplate?: string | null; - dataForensicsLevel?: string; - finerWeights?: Prisma.InputJsonValue | null; + dataForensicsExpertPrompt?: string | null; + dataForensicsHandlebarsTemplate?: string | null; clinicalExpertPrompt?: string | null; + clinicalHandlebarsTemplate?: string | null; } /** @@ -74,21 +79,27 @@ export async function upsertRvwConfig( where: { tenantId }, create: { tenantId, - editorialRules: dto.editorialRules ?? undefined, + editorialBaseStandard: dto.editorialBaseStandard ?? 'en', + editorialExpertPrompt: dto.editorialExpertPrompt ?? null, + editorialHandlebarsTemplate: dto.editorialHandlebarsTemplate ?? null, methodologyExpertPrompt: dto.methodologyExpertPrompt ?? null, methodologyHandlebarsTemplate: dto.methodologyHandlebarsTemplate ?? null, - dataForensicsLevel: dto.dataForensicsLevel ?? 'L2', - finerWeights: dto.finerWeights ?? undefined, + dataForensicsExpertPrompt: dto.dataForensicsExpertPrompt ?? null, + dataForensicsHandlebarsTemplate: dto.dataForensicsHandlebarsTemplate ?? null, clinicalExpertPrompt: dto.clinicalExpertPrompt ?? null, + clinicalHandlebarsTemplate: dto.clinicalHandlebarsTemplate ?? null, updatedAt: now, }, update: { - ...(dto.editorialRules !== undefined && { editorialRules: dto.editorialRules }), + ...(dto.editorialBaseStandard !== undefined && { editorialBaseStandard: dto.editorialBaseStandard }), + ...(dto.editorialExpertPrompt !== undefined && { editorialExpertPrompt: dto.editorialExpertPrompt }), + ...(dto.editorialHandlebarsTemplate !== undefined && { editorialHandlebarsTemplate: dto.editorialHandlebarsTemplate }), ...(dto.methodologyExpertPrompt !== undefined && { methodologyExpertPrompt: dto.methodologyExpertPrompt }), ...(dto.methodologyHandlebarsTemplate !== undefined && { methodologyHandlebarsTemplate: dto.methodologyHandlebarsTemplate }), - ...(dto.dataForensicsLevel !== undefined && { dataForensicsLevel: dto.dataForensicsLevel }), - ...(dto.finerWeights !== undefined && { finerWeights: dto.finerWeights }), + ...(dto.dataForensicsExpertPrompt !== undefined && { dataForensicsExpertPrompt: dto.dataForensicsExpertPrompt }), + ...(dto.dataForensicsHandlebarsTemplate !== undefined && { dataForensicsHandlebarsTemplate: dto.dataForensicsHandlebarsTemplate }), ...(dto.clinicalExpertPrompt !== undefined && { clinicalExpertPrompt: dto.clinicalExpertPrompt }), + ...(dto.clinicalHandlebarsTemplate !== undefined && { clinicalHandlebarsTemplate: dto.clinicalHandlebarsTemplate }), updatedAt: now, }, }); diff --git a/backend/src/modules/admin/services/tenantService.ts b/backend/src/modules/admin/services/tenantService.ts index fdc15294..52d5fbbb 100644 --- a/backend/src/modules/admin/services/tenantService.ts +++ b/backend/src/modules/admin/services/tenantService.ts @@ -17,6 +17,31 @@ import type { } from '../types/tenant.types.js'; class TenantService { + /** + * 期刊租户兜底策略:确保 RVW 模块已开通。 + * - 创建 JOURNAL 租户时自动开通 + * - 其他入口将租户改为 JOURNAL 时也自动补齐 + */ + private async ensureJournalRvwModule(tenantId: string): Promise { + await prisma.tenant_modules.upsert({ + where: { + tenant_id_module_code: { + tenant_id: tenantId, + module_code: 'RVW', + }, + }, + create: { + tenant_id: tenantId, + module_code: 'RVW', + is_enabled: true, + expires_at: null, + }, + update: { + is_enabled: true, + }, + }); + } + /** * 获取租户列表(分页) */ @@ -51,6 +76,11 @@ class TenantService { code: true, name: true, type: true, + journal_language: true, + journal_full_name: true, + logo_url: true, + brand_color: true, + login_background_url: true, status: true, contact_name: true, contact_phone: true, @@ -70,6 +100,11 @@ class TenantService { code: t.code, name: t.name, type: t.type as any, + journalLanguage: t.journal_language as any, + journalFullName: t.journal_full_name, + logoUrl: t.logo_url, + brandColor: t.brand_color, + loginBackgroundUrl: t.login_background_url, status: t.status as any, contactName: t.contact_name, contactPhone: t.contact_phone, @@ -120,6 +155,11 @@ class TenantService { code: tenant.code, name: tenant.name, type: tenant.type as any, + journalLanguage: tenant.journal_language as any, + journalFullName: tenant.journal_full_name, + logoUrl: tenant.logo_url, + brandColor: tenant.brand_color, + loginBackgroundUrl: tenant.login_background_url, status: tenant.status as any, contactName: tenant.contact_name, contactPhone: tenant.contact_phone, @@ -154,6 +194,11 @@ class TenantService { code: data.code, name: data.name, type: data.type as any, + journal_language: data.journalLanguage as any, + journal_full_name: data.journalFullName, + logo_url: data.logoUrl, + brand_color: data.brandColor, + login_background_url: data.loginBackgroundUrl, status: 'ACTIVE', contact_name: data.contactName, contact_phone: data.contactPhone, @@ -177,6 +222,11 @@ class TenantService { } } + // JOURNAL 租户自动开通 RVW(避免创建后无模块权限) + if (data.type === 'JOURNAL') { + await this.ensureJournalRvwModule(tenantId); + } + logger.info('[TenantService] 创建租户', { tenantId, code: data.code, @@ -189,6 +239,11 @@ class TenantService { code: tenant.code, name: tenant.name, type: tenant.type as any, + journalLanguage: tenant.journal_language as any, + journalFullName: tenant.journal_full_name, + logoUrl: tenant.logo_url, + brandColor: tenant.brand_color, + loginBackgroundUrl: tenant.login_background_url, status: tenant.status as any, contactName: tenant.contact_name, contactPhone: tenant.contact_phone, @@ -207,6 +262,12 @@ class TenantService { where: { id: tenantId }, data: { name: data.name, + type: data.type as any, + journal_language: data.journalLanguage === null ? null : (data.journalLanguage as any), + journal_full_name: data.journalFullName === null ? null : data.journalFullName, + logo_url: data.logoUrl === null ? null : data.logoUrl, + brand_color: data.brandColor === null ? null : data.brandColor, + login_background_url: data.loginBackgroundUrl === null ? null : data.loginBackgroundUrl, contact_name: data.contactName, contact_phone: data.contactPhone, contact_email: data.contactEmail, @@ -215,6 +276,11 @@ class TenantService { }, }); + // 当租户类型为 JOURNAL 时,自动兜底 RVW 模块 + if (tenant.type === 'JOURNAL') { + await this.ensureJournalRvwModule(tenantId); + } + logger.info('[TenantService] 更新租户', { tenantId, data }); return { @@ -222,6 +288,11 @@ class TenantService { code: tenant.code, name: tenant.name, type: tenant.type as any, + journalLanguage: tenant.journal_language as any, + journalFullName: tenant.journal_full_name, + logoUrl: tenant.logo_url, + brandColor: tenant.brand_color, + loginBackgroundUrl: tenant.login_background_url, status: tenant.status as any, contactName: tenant.contact_name, contactPhone: tenant.contact_phone, @@ -311,6 +382,7 @@ class TenantService { logo?: string; primaryColor: string; systemName: string; + backgroundImageUrl?: string; modules: string[]; isReviewOnly: boolean; } | null> { @@ -335,10 +407,11 @@ class TenantService { const isReviewOnly = modules.length === 1 && modules[0] === 'RVW'; return { - name: tenant.name, - logo: undefined, // TODO: 未来可从 tenant 扩展字段获取 - primaryColor: isReviewOnly ? '#6366f1' : '#1890ff', - systemName: isReviewOnly ? '智能审稿系统' : 'AI临床研究平台', + name: tenant.journal_full_name || tenant.name, + logo: tenant.logo_url || undefined, + primaryColor: tenant.brand_color || (isReviewOnly ? '#6366f1' : '#1890ff'), + systemName: tenant.journal_full_name || (isReviewOnly ? '智能审稿系统' : 'AI临床研究平台'), + backgroundImageUrl: tenant.login_background_url || undefined, modules, isReviewOnly, }; diff --git a/backend/src/modules/admin/types/tenant.types.ts b/backend/src/modules/admin/types/tenant.types.ts index cd526557..7a4ab841 100644 --- a/backend/src/modules/admin/types/tenant.types.ts +++ b/backend/src/modules/admin/types/tenant.types.ts @@ -2,8 +2,9 @@ * 租户管理类型定义 */ -export type TenantType = 'HOSPITAL' | 'PHARMA' | 'INTERNAL' | 'PUBLIC'; +export type TenantType = 'HOSPITAL' | 'PHARMA' | 'JOURNAL' | 'INTERNAL' | 'PUBLIC'; export type TenantStatus = 'ACTIVE' | 'SUSPENDED' | 'EXPIRED'; +export type JournalLanguage = 'ZH' | 'EN' | 'OTHER'; /** * 租户基本信息 @@ -14,6 +15,11 @@ export interface TenantInfo { name: string; type: TenantType; status: TenantStatus; + journalLanguage?: JournalLanguage | null; + journalFullName?: string | null; + logoUrl?: string | null; + brandColor?: string | null; + loginBackgroundUrl?: string | null; contactName?: string | null; contactPhone?: string | null; contactEmail?: string | null; @@ -47,6 +53,11 @@ export interface CreateTenantRequest { code: string; name: string; type: TenantType; + journalLanguage?: JournalLanguage; + journalFullName?: string; + logoUrl?: string; + brandColor?: string; + loginBackgroundUrl?: string; contactName?: string; contactPhone?: string; contactEmail?: string; @@ -59,6 +70,12 @@ export interface CreateTenantRequest { */ export interface UpdateTenantRequest { name?: string; + type?: TenantType; + journalLanguage?: JournalLanguage | null; + journalFullName?: string | null; + logoUrl?: string | null; + brandColor?: string | null; + loginBackgroundUrl?: string | null; contactName?: string; contactPhone?: string; contactEmail?: string; diff --git a/backend/src/modules/rvw/services/clinicalService.ts b/backend/src/modules/rvw/services/clinicalService.ts index 1a668315..37726650 100644 --- a/backend/src/modules/rvw/services/clinicalService.ts +++ b/backend/src/modules/rvw/services/clinicalService.ts @@ -28,15 +28,18 @@ export interface ClinicalReviewResult { export async function reviewClinical( text: string, modelType: ModelType = 'deepseek-v3', - userId?: string + userId?: string, + tenantExpertPrompt?: string | null ): Promise { try { - const promptService = getPromptService(prisma); - const { content: businessPrompt, isDraft } = await promptService.get( - 'RVW_CLINICAL', - {}, - { userId } - ); + let businessPrompt = tenantExpertPrompt?.trim() || ''; + let isDraft = false; + if (!businessPrompt) { + const promptService = getPromptService(prisma); + const result = await promptService.get('RVW_CLINICAL', {}, { userId }); + businessPrompt = result.content; + isDraft = result.isDraft; + } if (isDraft) { logger.info('[RVW:Clinical] 使用 DRAFT 版本 Prompt(调试模式)', { userId }); diff --git a/backend/src/modules/rvw/services/editorialService.ts b/backend/src/modules/rvw/services/editorialService.ts index 2af02365..4d2b4e69 100644 --- a/backend/src/modules/rvw/services/editorialService.ts +++ b/backend/src/modules/rvw/services/editorialService.ts @@ -65,16 +65,29 @@ async function repairEditorialToJson( export async function reviewEditorialStandards( text: string, modelType: ModelType = 'deepseek-v3', - userId?: string + userId?: string, + tenantExpertPrompt?: string | null, + editorialBaseStandard?: 'zh' | 'en' ): Promise { try { - // 1. 从 PromptService 获取系统Prompt(支持灰度预览) - const promptService = getPromptService(prisma); - const { content: businessPrompt, isDraft } = await promptService.get( - 'RVW_EDITORIAL', - {}, - { userId } - ); + // 1. 优先使用租户自定义 Prompt;否则按中英基线获取系统默认 Prompt + let businessPrompt = tenantExpertPrompt?.trim() || ''; + let isDraft = false; + + if (!businessPrompt) { + const promptService = getPromptService(prisma); + const promptCode = editorialBaseStandard === 'zh' ? 'RVW_EDITORIAL_ZH' : 'RVW_EDITORIAL_EN'; + try { + const result = await promptService.get(promptCode, {}, { userId }); + businessPrompt = result.content; + isDraft = result.isDraft; + } catch { + // 向后兼容:若新 code 尚未入库,降级到旧 code + const result = await promptService.get('RVW_EDITORIAL', {}, { userId }); + businessPrompt = result.content; + isDraft = result.isDraft; + } + } if (isDraft) { logger.info('[RVW:Editorial] 使用 DRAFT 版本 Prompt(调试模式)', { userId }); diff --git a/backend/src/modules/rvw/skills/core/types.ts b/backend/src/modules/rvw/skills/core/types.ts index 7eb2e65f..0eaaeacd 100644 --- a/backend/src/modules/rvw/skills/core/types.ts +++ b/backend/src/modules/rvw/skills/core/types.ts @@ -114,16 +114,24 @@ export interface ForensicsResult { * 字段与 TenantRvwConfig 对应,仅携带运行时所需的轻量子集 */ export interface TenantRvwConfigSnapshot { + /** 稿约规范评估:继承基线(zh/en) */ + editorialBaseStandard?: 'zh' | 'en'; + /** 稿约规范评估:租户自定义 Prompt */ + editorialExpertPrompt?: string | null; + /** 稿约规范评估:租户自定义模板 */ + editorialHandlebarsTemplate?: string | null; /** 方法学评估:专家业务评判标准(覆盖 PromptService 默认值) */ methodologyExpertPrompt?: string | null; + /** 方法学评估:租户自定义模板 */ + methodologyHandlebarsTemplate?: string | null; + /** 数据验证:租户自定义 Prompt */ + dataForensicsExpertPrompt?: string | null; + /** 数据验证:租户自定义模板 */ + dataForensicsHandlebarsTemplate?: string | null; /** 临床评估:专科特色补充要求 */ clinicalExpertPrompt?: string | null; - /** 稿约规范评估:规则数组 */ - editorialRules?: unknown; - /** 数据验证:深度级别 L1/L2/L3 */ - dataForensicsLevel?: string; - /** 临床评估:FINER 权重 */ - finerWeights?: unknown; + /** 临床评估:租户自定义模板 */ + clinicalHandlebarsTemplate?: string | null; } /** diff --git a/backend/src/modules/rvw/skills/library/ClinicalAssessmentSkill.ts b/backend/src/modules/rvw/skills/library/ClinicalAssessmentSkill.ts index 7b7a5e05..aeff4f6b 100644 --- a/backend/src/modules/rvw/skills/library/ClinicalAssessmentSkill.ts +++ b/backend/src/modules/rvw/skills/library/ClinicalAssessmentSkill.ts @@ -83,7 +83,13 @@ export class ClinicalAssessmentSkill extends BaseSkill { }); } - // 调用现有 editorialService - const result = await reviewEditorialStandards(content, 'deepseek-v3', context.userId); + // V4.0 Hybrid Prompt:优先租户自定义稿约 Prompt;否则按 editorialBaseStandard 路由系统默认中英基线 + const tenantEditorialPrompt = context.tenantRvwConfig?.editorialExpertPrompt ?? null; + const editorialBaseStandard = context.tenantRvwConfig?.editorialBaseStandard ?? 'en'; + const result = await reviewEditorialStandards( + content, + 'deepseek-v3', + context.userId, + tenantEditorialPrompt, + editorialBaseStandard + ); // 转换为 SkillResult 格式 const issues = this.convertToIssues(result); diff --git a/backend/src/modules/rvw/workers/reviewWorker.ts b/backend/src/modules/rvw/workers/reviewWorker.ts index 7d5c602c..7be58765 100644 --- a/backend/src/modules/rvw/workers/reviewWorker.ts +++ b/backend/src/modules/rvw/workers/reviewWorker.ts @@ -491,31 +491,60 @@ async function executeWithSkills( select: { tenantId: true, contextData: true }, }); let tenantRvwConfig: { + editorialBaseStandard?: 'zh' | 'en'; + editorialExpertPrompt?: string | null; + editorialHandlebarsTemplate?: string | null; methodologyExpertPrompt?: string | null; + methodologyHandlebarsTemplate?: string | null; + dataForensicsExpertPrompt?: string | null; + dataForensicsHandlebarsTemplate?: string | null; clinicalExpertPrompt?: string | null; - editorialRules?: unknown; - dataForensicsLevel?: string; - finerWeights?: unknown; + clinicalHandlebarsTemplate?: string | null; } | null = null; if (taskWithTenant?.tenantId) { - const cfg = await prisma.tenantRvwConfig.findUnique({ + const tenantMeta = await (prisma as any).tenants.findUnique({ + where: { id: taskWithTenant.tenantId }, + select: { journal_language: true }, + }) as { journal_language?: 'ZH' | 'EN' | 'OTHER' } | null; + const inferredEditorialBase: 'zh' | 'en' = + tenantMeta?.journal_language === 'ZH' ? 'zh' : 'en'; + + const cfg = await (prisma as any).tenantRvwConfig.findUnique({ where: { tenantId: taskWithTenant.tenantId }, select: { + editorialBaseStandard: true, + editorialExpertPrompt: true, + editorialHandlebarsTemplate: true, methodologyExpertPrompt: true, + methodologyHandlebarsTemplate: true, + dataForensicsExpertPrompt: true, + dataForensicsHandlebarsTemplate: true, clinicalExpertPrompt: true, - editorialRules: true, - dataForensicsLevel: true, - finerWeights: true, + clinicalHandlebarsTemplate: true, }, }); if (cfg) { - tenantRvwConfig = cfg; + tenantRvwConfig = { + ...cfg, + editorialBaseStandard: cfg.editorialBaseStandard === 'zh' ? 'zh' : inferredEditorialBase, + }; logger.info('[reviewWorker] 已加载租户审稿配置', { taskId, tenantId: taskWithTenant.tenantId, hasMethodologyPrompt: !!cfg.methodologyExpertPrompt, + hasEditorialPrompt: !!cfg.editorialExpertPrompt, + hasDataPrompt: !!cfg.dataForensicsExpertPrompt, hasClinicalPrompt: !!cfg.clinicalExpertPrompt, }); + } else { + tenantRvwConfig = { + editorialBaseStandard: inferredEditorialBase, + }; + logger.info('[reviewWorker] 未找到租户审稿配置,回退系统基线', { + taskId, + tenantId: taskWithTenant.tenantId, + editorialBaseStandard: inferredEditorialBase, + }); } } diff --git a/docs/00-系统总体设计/00-系统当前状态与开发指南.md b/docs/00-系统总体设计/00-系统当前状态与开发指南.md index 5dc32a60..4d444c89 100644 --- a/docs/00-系统总体设计/00-系统当前状态与开发指南.md +++ b/docs/00-系统总体设计/00-系统当前状态与开发指南.md @@ -1,10 +1,11 @@ # AIclinicalresearch 系统当前状态与开发指南 -> **文档版本:** v7.1 +> **文档版本:** v7.2 > **创建日期:** 2025-11-28 > **维护者:** 开发团队 -> **最后更新:** 2026-03-14 +> **最后更新:** 2026-03-15 > **🎉 重大里程碑:** +> - **🆕 2026-03-15:RVW V4.0 期刊配置中心 MVP 完成!** 新增 ADMIN 一级菜单「期刊配置中心」+ JOURNAL 租户能力 + 中英稿约基线 + 运行时最小配置适配;租户登录路径统一为 `/:tenantCode/login`,创建/更新 JOURNAL 自动开通 RVW 模块 > - **🆕 2026-03-14:RVW V4.0 租户门户 MVP 链路打通!** 期刊专属登录页 + 上传 + 执行审稿 + 过程页动态进度 + 报告查看闭环可用;租户列表状态图标展示修正(完成态不再误显示审稿中) > - **🆕 2026-03-13:RVW 方法学稳定性增强(V3.0.2)!** 方法学 20 检查点结构化增强 + A/B/C 分治并行评估 + 规则汇总器统一结论 + 前端展示口径收敛(按三大项分组展示检查点) > - **🆕 2026-03-09:认证模块接入阿里云短信验证码!** 登录验证码链路支持 `mock/aliyun` 双模式 + 后端短信服务封装 + 独立联调脚本(`npm run test:sms`)+ 实机发送验证通过 @@ -40,6 +41,9 @@ > - **2026-01-22:OSS 存储集成完成!** 阿里云 OSS 正式接入平台基础层 > > **🆕 最新进展(含认证短信集成 2026-03-09):** +> - ✅ **🆕 RVW V4.0 期刊配置中心 MVP 完成** — 运营管理端新增期刊配置中心,支持 JOURNAL 期刊中英基线配置(`editorial_base_standard`)与 4 维 Prompt/Template 管理;执行链路接入 `tenantCustom ?? systemDefault` 最小适配 +> - ✅ **🆕 RVW 登录路径统一** — 期刊登录统一为 `/:tenantCode/login`(开发/生产同路径,仅域名不同);不再保留 `/t/:tenantCode/login` +> - ✅ **🆕 JOURNAL 自动模块兜底** — 创建/更新为 `JOURNAL` 时自动开通 `RVW`,解决“用户能登录但无智能审稿模块权限”问题 > - ✅ **🆕 RVW V4.0 租户门户 MVP 联调通过** — `/:tenantSlug/rvw` 链路可用,执行审稿后回归旧版过程页交互(动态进度 + 动态Tab),上传按钮与状态图标问题修复 > - ✅ **🆕 RVW 方法学稳定性增强(V3.0.2)** — `checkpoints` 结构化输出(20项)+ 方法学 A/B/C 分治并行评估(1-9/10-14/15-20)+ 规则汇总器统一 `summary/conclusion` + 前端展示按三大项分组 > - ✅ **🆕 认证短信验证码接入完成** — `sendVerificationCode` 接入阿里云短信网关(保留 `mock`)+ 发送成功后再落库验证码 + 环境变量校验 + 联调脚本 `test:sms` + 实机发送验证通过 @@ -1352,6 +1356,7 @@ AIclinicalresearch/ | **2026-03-01** | **IIT GCP报表+Bug修复** 🎉 | ✅ 4张GCP标准报表+AI时间线增强+一键全量质控+6项Bug修复 | | **2026-02-27** | **DB文档+部署体系** 📚 | ✅ 6篇数据库文档+部署归档+统一操作手册+开发规范v3.0+Schema对齐 | | **2026-03-14** | **RVW V4.0 租户门户联调通过** 🎉 | ✅ 上传/审稿/过程页/报告闭环可用,状态图标与交互体验修正 | +| **2026-03-15** | **RVW V4.0 期刊配置中心 MVP 完成** 🚀 | ✅ ADMIN 期刊配置中心 + JOURNAL 中英基线 + 登录路径统一 + JOURNAL 自动开通 RVW | | **当前** | **PKB模块生产可用** | ✅ 核心功能全部实现(95%),自研RAG+OSS存储上线 | | **2026-01-07 晚** | **RVW模块开发完成** 🎉 | ✅ Phase 1-3完成(后端迁移+数据库扩展+前端重构) | @@ -1689,9 +1694,9 @@ if (items.length >= 50) { --- -**文档版本**:v7.1 -**最后更新**:2026-03-14 -**本次更新**:RVW V4.0 租户门户 MVP 联调通过(期刊专属登录 + 上传 + 执行审稿 + 过程页动态进度 + 报告查看)并修复租户列表维度状态显示误判问题;明日进入运营管理端期刊配置中心完善(中英期刊 Skills 匹配 + 租户登录风格 + URL 配置) +**文档版本**:v7.2 +**最后更新**:2026-03-15 +**本次更新**:RVW V4.0 期刊配置中心 MVP 完成(JOURNAL 模型与配置能力补齐、执行链路接入中英基线与继承覆盖、登录路径统一为 `/:tenantCode/login`、JOURNAL 自动开通 RVW 模块),进入生产部署与灰度验证阶段 --- diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md b/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md index e984bafb..4db68765 100644 --- a/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md +++ b/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md @@ -1,10 +1,10 @@ # RVW稿件审查模块 - 当前状态与开发指南 -> **文档版本:** v6.3 +> **文档版本:** v6.4 > **创建日期:** 2026-01-07 -> **最后更新:** 2026-03-14 +> **最后更新:** 2026-03-15 > **维护者:** 开发团队 -> **当前状态:** 🚀 **V4.0 租户门户 MVP 联调通过(上传/审稿/过程页/报告全链路可用)** +> **当前状态:** 🚀 **V4.0 期刊配置中心 MVP 开发完成(租户门户+配置中心+中英基线+权限兜底)** > **文档目的:** 快速了解RVW模块状态,为新AI助手提供上下文 > > **🎉 V3.0 进展(2026-03-07):** @@ -29,11 +29,18 @@ > - ✅ **前端展示口径统一**:方法学报告按“三大项->检查点”展示,去除重复占位文案并显示真实LLM内容 > > **🆕 V4.0 租户门户联调收敛(2026-03-14):** -> - ✅ **期刊专属登录页可用**:`/t/:tenantCode/login` 支持密码登录 + 验证码登录(与主站能力对齐,UI为期刊门户风格) +> - ✅ **期刊专属登录页可用**:`/:tenantCode/login` 支持密码登录 + 验证码登录(与主站能力对齐,UI为期刊门户风格) > - ✅ **上传链路修复**:租户工作台上传按钮稳定触发,`POST /api/v1/rvw/tasks` 创建任务成功 > - ✅ **执行审稿体验回归旧流程**:点击“开始审稿”后直接进入过程页(复用 `TaskDetail` 动态进度 + 动态Tab) > - ✅ **租户详情页全屏化复用**:租户路由下适配旧版过程页交互,无 Sidebar 干扰 > - ✅ **列表状态图标修复**:审稿完成后维度状态正确展示(绿色对钩/警告/错误),不再误显示“审稿中” +> +> **🆕 V4.0 期刊配置中心 MVP(2026-03-15):** +> - ✅ **运营管理端新增一级菜单「期刊配置中心」**:支持列表/详情/配置保存 +> - ✅ **JOURNAL 租户模型补齐**:`tenants` 增加 `journal_language/journal_full_name/logo_url/brand_color/login_background_url` +> - ✅ **审稿配置补齐**:`tenant_rvw_configs` 支持 4 维 Prompt + Handlebars 模板 + `editorial_base_standard(zh/en)` +> - ✅ **执行链路最小适配**:`finalPrompt = tenantCustom ?? systemDefault`,稿约支持中英基线路由 +> - ✅ **租户权限兜底**:创建/更新为 `JOURNAL` 时自动开通 `RVW` 模块,避免新用户登录后无模块权限 > > **V2.0 进展回顾:** > - ✅ L1 算术验证 + L2 统计验证 + L2.5 一致性取证 @@ -54,7 +61,7 @@ | **商业价值** | ⭐⭐⭐⭐⭐ 极高 | | **独立性** | ⭐⭐⭐⭐⭐ 极高(用户群完全不同) | | **目标用户** | 期刊初审编辑 | -| **开发状态** | 🚀 **V4.0 租户门户 MVP 已联调通过:上传→审稿→过程页→报告闭环可用** | +| **开发状态** | 🚀 **V4.0 期刊配置中心 MVP 已开发完成:配置中心→租户门户→上传→审稿→报告闭环可用** | ### 核心目标 @@ -104,7 +111,7 @@ | 状态筛选 | ❌ | ✅ | ✅ 已完成 | | 历史归档 | ❌ | ✅ | ⏸️ 数据库已支持,UI暂缓 | | 系统设置 | ❌ | ✅ | ⏸️ 暂不开发 | -| 登录页面 | ❌ | ⏸️ | ⏸️ 复用平台登录 | +| 期刊登录页面(单一路径) | ❌ | ✅ | ✅ `/:tenantCode/login` 已上线,开发/生产同路径 | | PICO卡片 | ❌ | ✅ | ⏸️ 数据库已支持,UI暂缓 | --- @@ -499,7 +506,7 @@ Content-Type: multipart/form-data --- -**文档版本:** v6.3 -**最后更新:** 2026-03-14 -**当前状态:** 🚀 V4.0 租户门户 MVP 联调通过(上传/审稿/过程页/报告闭环) -**下一步:** 完善运营管理端期刊配置中心(中英期刊 Skills 匹配 + 租户登录风格 + URL/品牌配置联动) +**文档版本:** v6.4 +**最后更新:** 2026-03-15 +**当前状态:** 🚀 V4.0 期刊配置中心 MVP 开发完成(配置中心 + 租户门户 + 中英基线 + 权限兜底) +**下一步:** 进入生产部署与灰度验证(Nginx 深链回退、前端缓存清理、期刊链路冒烟回归) diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/JTIM稿约规范与差异分析报告.md b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/JTIM稿约规范与差异分析报告.md new file mode 100644 index 00000000..ae49ceee --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/JTIM稿约规范与差异分析报告.md @@ -0,0 +1,30 @@ +# **JTIM 期刊稿约规范梳理** + +# **分析对象: *Journal of Translational Internal Medicine* (JTIM) 2026-03-10 最新版稿约** + +【JTIM 期刊全维稿约核查规则】 +作为严谨的医学期刊审稿人,请对稿件的结构、伦理及排版进行全面深度核查。必须严格遵守以下规则池: + +一、 🔴 致命错误与伦理规范(一票否决项,若违反请标记为 fatal) +1\. 双盲审查 (Double-blind):正文 (Main Text/Blinded Article) 中绝对不可出现作者姓名、机构、致谢、资助编号等身份信息。 +2\. 伦理与知情同意:涉及人类或动物实验必须有 IRB/IACUC 批准说明;涉及人类受试者(含病例报告)必须有知情同意书(Informed Consent)获取声明。 +3\. 声明完整性:正文或结尾必须包含利益冲突声明 (Conflict of Interest)、资金资助声明 (Funding)。若是临床干预试验必须有注册号。 +4\. AI 与署名:严禁 AI 工具作为作者署名。若声明使用了 AI 辅助必须予以记录。 + +二、 🟡 JTIM 专属体例与排版要求(重点核查项,若违反请标记为 major\_issue) +5\. 语言校验:全篇必须严格统一使用美式英语 (American English)。 +6\. 引言篇幅:Original articles(原创论著)的引言 (Introduction) 必须极其简短,严格校验是否在 75-100 个单词范围内。 +7\. 摘要合规性:论著必须为 Structured 格式(包含 Objectives, Methods, Results, Conclusions),限 250 字;综述限 200 字;病例限 150 字。 +8\. 参考文献与引文格式: + \- 正文引用:必须使用阿拉伯数字上标,用方括号包裹,且必须置于标点符号【之后】(例:...disease.^\[1\])。 + \- 列表格式:必须列出前 6 位作者,其余用 et al. 替代。 +9\. 统计与图表:必须提供 P 值的精确数值,且统计学字母必须为斜体(如:\*P\* \= 0.048);图表图例(Figure legends)不得超过 40 个单词。表格严禁为图片格式。 +10\. 作者数量警报:论著与综述共同作者(co-authors)超 3 人、其余类型超 2 人时,必须予以警告。 +11\. 标题要求:章节标题使用 Title case(实词首字母大写),绝对不能全部大写 (Not ALL CAPITALS)。Running title 限制在 50 个字符以内。 + +三、 🟢 通用学术写作规范(常规核查项,若违反请标记为 minor\_issue) +12\. 数字拼写:句首的数字必须拼写成英文单词(如 "Fifteen patients...");正文中 1-10 的数字通常要求拼写(带有度量衡单位的除外)。 +13\. 学术专有名词:种属名称(如 \*E. coli\*)、基因符号必须使用斜体。 +14\. 度量单位:必须使用标准的国际单位制 (SI Units)。 + +请系统性地输出上述规则的违反情况,如果没有任何违反,请回复“符合所有规范要求”。 diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/Medical Review期刊专属稿约提示词.md b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/Medical Review期刊专属稿约提示词.md new file mode 100644 index 00000000..c85088f8 --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/Medical Review期刊专属稿约提示词.md @@ -0,0 +1,39 @@ +# **Medical Review (MR) 期刊专属稿约审查提示词 (Prompt)** + +**使用场景:** 在 ADMIN 配置中心,当运营人员为 Medical Review 期刊配置稿约规范时,填入专家评判标准 (Expert Prompt) 文本框的内容。 + +**设计逻辑:** 依据 MR 2025-09-23 最新版 Author Guidelines 深度定制。重点加强了其特有的 Sentence case 标题规范、包容性语言要求、单盲属性(移除双盲核查)以及严格的文章类型硬指标。 + +请将以下内容直接复制到 MR 期刊配置的 Expert Prompt 中: + +【Medical Review (MR) 期刊全维稿约核查规则】 +作为严谨的国际医学期刊《Medical Review》的审稿人,请对稿件的结构、伦理及排版进行全面深度核查。必须严格遵守以下 MR 官方指南要求: + +一、 🔴 致命错误与伦理规范(一票否决项,若违反请标记为 fatal) +1\. 伦理与知情同意:涉及人类/动物实验必须有 IRB/IACUC 批准说明;涉及人类受试者必须有知情同意书(Informed consent)获取声明。 +2\. 声明完整性:正文末尾(或模板中)必须包含利益冲突声明 (Conflict of Interest)、资金资助声明 (Research funding) 以及临床试验注册号 (UTN, 若适用)。 +3\. 数据共享声明 (Data availability):MR 强制要求符合欧洲 GDPR 规范的数据共享政策,必须说明研究数据在哪里可以获取。 +4\. AI 与署名合规:严禁将大模型(如 ChatGPT, AI)列为作者。若研究方法或写作过程中使用了 AI 工具,必须在 Methods 或声明模板中清晰披露。 +\*(注:MR 采取单盲 Peer Review,正文可保留作者与机构信息,无需进行双盲排查)\* + +二、 🟡 MR 专属体例与排版要求(重点核查项,若违反请标记为 major\_issue) +5\. 标题与层级要求 (Sentence case): + \- 文章标题 (Title) 和所有章节小标题 (Headings) 必须使用 Sentence case(仅首字母大写,冒号后使用小写字母)。 + \- 短标题 (Running head) 限制在 75 个字符以内(含空格)。 + \- 章节标题不可带数字编号,最多不超过 4 级。 +6\. 摘要合规性 (严格限制 200 words 以内): + \- Research articles / Short Communications:必须为结构化摘要 (Objectives; Methods; Results; Conclusions)。 + \- Reviews:可为非结构化摘要,或特定结构的摘要 (Objectives; Content; Summary and Outlook)。 +7\. 各文章类型硬性指标: + \- Research Article:正文字数 3000-5000字;图表总数不超过 6 个;参考文献大于 50 篇。 + \- Review:正文字数大于 6000 字;图表总数 5-10 个;参考文献大于 80 篇。 + \- Systematic Review:正文字数小于 6000 字;图表总数 3-8 个;参考文献大于 50 篇。 +8\. 图表与引用独立性:表格必须是可编辑格式(Word/LaTeX),严禁使用图片格式的表格,避免使用垂直线和底纹;图表必须在正文中按阿拉伯数字顺序交叉引用(如 Figure 1, Table 1)。 + +三、 🟢 学术写作规范与细节(常规核查项,若违反请标记为 minor\_issue) +9\. 包容性语言 (Inclusive language):检查是否符合包容性原则,建议使用性别中立词汇(如尽量使用复数名词或 "they",避免使用 "he" 或 "she")。 +10\. 语言与拼写:全篇必须一致地使用美式英语或英式英语。 +11\. 关键词与缩写:提供 3-8 个全小写的关键词(用分号隔开);所有非常规缩写在摘要和正文首次出现时必须写出全称。 +12\. 格式规范:数字与国际标准单位 (SI Units) 之间必须保留一个窄空格(°C 和 % 除外);物种拉丁名(如 \*E. coli\*)和基因缩写必须使用斜体。 + +请系统性地输出上述规则的违反情况。对于没有明确违反规则的项,无需列出。如果通篇没有任何违反,请回复“符合 Medical Review 所有官方规范要求”。 diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求.md b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求.md deleted file mode 100644 index 8b854e16..00000000 --- a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求.md +++ /dev/null @@ -1,131 +0,0 @@ -# **RVW V4.0 期刊租户配置中心开发需求** - -**模块定位:** 依托现有 ADMIN 运营端 TenantDetailPage,为每个期刊客户建立独立的配置中枢。 - -**核心目标:** 实现期刊 SaaS 的“千刊千面”,包括基础品牌信息定制与底层 AI 审稿 Skills 规则的动态编排。 - -## **🎯 一、 前端 UI 与功能分区要求** - -在现有的 ADMIN 租户详情页(TenantDetailPage)中,构建两大核心配置区块(可作为独立的 Tab 或折叠面板): - -### **区块 1:期刊基础信息与门户配置 (Basic Info & Portal)** - -用于定义该期刊的对外面貌和访问入口。 - -| 配置项名称 | 字段类型 | UI 组件 | 功能说明 | -| :---- | :---- | :---- | :---- | -| **期刊全称** | String | 文本输入框 | 如:*Journal of Translational Internal Medicine*。显示在登录页及工作台顶部。 | -| **访问路径 (URL Slug)** | String | 文本输入框 (需正则校验) | **极重要**:用于生成 review.xunzhengyixue.com/{slug}。仅允许小写字母、数字和连字符。 | -| **期刊 Logo** | String | 图片上传组件 | 上传至 OSS 后保存 URL。用于替换默认的系统 Logo。 | -| **品牌主色调** | String | 颜色拾取器 (Color Picker) | 支持 HEX 色值(如 \#0284c7)。用于动态渲染按钮和高亮文字。 | -| **专属登录页背景图** | String | 图片上传组件 | (可选) 替换登录页的默认背景,增强品牌沉浸感。 | - -### **区块 2:智能审稿配置 (RVW Skills Config)** - -这是该模块的核心,分为 4 个独立的配置面板(Panel): - -#### **Panel A: 稿约规范评估 (Editorial)** - -* **规则列表维护**:支持增删改查。每条规则需包含 规则编号 (Code) 和 规则描述 (Description)。 -* **致命错误开关 (Fatal Flag)**:每条规则后跟随一个 Switch 开关。开启后,该项缺陷将作为“一票否决”项在前端红底高亮。 - -#### **Panel B: 方法学评估 (Methodology) \- 【需支持动静分离与预览】** - -* **专家评判标准 (Prompt)**:大文本框。供运营人员输入该期刊特有的统计学和方法学要求(大模型读取)。 -* **报告展示模板 (Handlebars)**:大文本框。用于配置最终输出给责编的 Markdown 格式。 -* ⚠️ **强依赖功能:测试渲染 (Test Render)**:**必须**在模板编辑区提供一个“预览”按钮。点击后,使用系统内置的假 JSON 数据(Mock Data)与当前模板进行结合,在右侧弹出抽屉实时渲染出 Markdown 结果,以防止语法错误。 - -#### **Panel C: 数据验证 (Data Forensics)** - -* **验证深度 (Level)**:提供三个单选按钮(Radio): - * L1 算术验证:仅核对行列加总、百分比。 - * L2 统计验证:包含 L1,增加 CI↔P 一致性逆向验证(推荐)。 - * L3 双通道核查:包含 L2,增加大模型智能深度核查(耗时较长)。 - -#### **Panel D: 临床专业评估 (Clinical)** - -* **FINER 权重占比**:提供 5 个数字输入框(或滑块),分别对应 Feasibility, Innovation, Novelty, Ethical, Relevant。前端需实时校验总和是否等于 100%。 -* **专科特色要求**:文本区,填写该期刊的专科倾向性偏好(如:心血管领域的特殊要求)。 - -## **💾 二、 数据库 Schema 设计 (Prisma)** - -本模块涉及两部分数据的存储:基础信息存入已有的 Tenants 表,审稿配置存入新增的 TenantRvwConfig 表。 - -### **1\. 基础信息存储 (platform\_schema.tenants)** - -利用现有的租户表。假设现有表结构已有 code (即 URL Slug) 和 name (期刊名称)。对于 Logo、颜色等品牌视觉资产,建议统一存入 config JSON 字段中。 - -// 无需新建表,在原 Tenants 表中合理利用字段 -model Tenant { - id String @id @default(uuid()) - code String @unique // 对应 URL Slug,如 'jtim' - name String // 期刊全称 - - // 品牌视觉信息存入此 JSON 字段,避免频繁改表结构 - config Json? // 格式:{ logoUrl: "...", brandColor: "\#...", bgImgUrl: "..." } - - rvwConfig TenantRvwConfig? // 关联 RVW 配置表 -} - -### **2\. RVW Skills 配置存储 (新增表)** - -执行 prisma migrate dev 创建以下 1对1 扩展表: - -model TenantRvwConfig { - id String @id @default(uuid()) - tenantId String @unique - tenant Tenant @relation(fields: \[tenantId\], references: \[id\]) - - // Panel A: 稿约规范 (存为 JSON 数组) - // 结构: \[{ code: 'ED-01', desc: '摘要需控制在250字内', isFatal: true }\] - editorialRules Json? - - // Panel B: 方法学评估 - methodologyExpertPrompt String? @db.Text - methodologyHandlebarsTemplate String? @db.Text - - // Panel C: 数据验证 - dataForensicsLevel String @default("L2") // 'L1', 'L2', 'L3' - - // Panel D: 临床评估 (存为 JSON) - // 结构: { f: 20, i: 20, n: 20, e: 20, r: 20 } - finerWeights Json? - clinicalExpertPrompt String? @db.Text - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@schema("platform\_schema") -} - -## **🔌 三、 后端 API 接口设计** - -新增统一的租户管理与 RVW 配置接口: - -### **1\. 内部 ADMIN 配置接口 (需要 ops:user-ops 权限)** - -* PUT /api/admin/tenants/:id/basic-info:更新期刊名称、Slug、品牌 Logo 等信息。 -* GET /api/admin/tenants/:id/rvw-config:获取指定期刊的 Skills 配置。 -* PUT /api/admin/tenants/:id/rvw-config:UPSERT (更新或创建) 期刊 Skills 配置。 - -### **2\. 公开业务接口 (无 Auth,用于动态登录页渲染)** - -* GET /api/v1/tenants/public-info/:slug - * **参数**:slug (例如 'jtim') - * **返回**:{ name, logoUrl, brandColor, bgImgUrl } - * **限流要求**:必须添加 Rate Limiting (防刷防爬)。 - -## **👥 四、 实施 SOP:用户与期刊绑定(无需开发)** - -**给开发团队的提示**:业务上,客户(责编)登录 /jtim 后能看到 JTIM 的稿件,依赖于正确的账号绑定。这部分**不需要开发新功能**,但需要通知实施团队遵循以下 SOP: - -1. 客户在主站注册账号(或由超管分配账号)。 -2. 内部超管在 ADMIN 端的 **【用户管理 \-\> 租户成员管理】** 页面,将该用户的账号添加到目标期刊租户(如 JTIM)下。 -3. 确保该用户拥有 RVW 模块的使用权限。 -4. **验证**:此后该用户访问 review.xunzhengyixue.com/jtim 时,JWT 中会自动带上该租户身份,接口方可放行。 - -## **✅ 五、 验收标准 (Definition of Done)** - -1. 运营人员可在后台顺滑配置 JTIM 和 CMJ 两套截然不同的方法学 Prompt 和 Handlebars 模板。 -2. 在 Handlebars 编辑器中点击“预览”,能立刻看到正确渲染的 Markdown 格式报告,不报错。 -3. 未登录用户访问 review.xunzhengyixue.com/jtim 时,登录页自动变更为 JTIM 的名称、Logo 和主色调。 \ No newline at end of file diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求0315.md b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求0315.md new file mode 100644 index 00000000..d11581c6 --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求0315.md @@ -0,0 +1,161 @@ +# **RVW V4.0 期刊租户配置中心开发需求** + +**模块定位:** 依托现有 ADMIN 运营端 TenantDetailPage,为每个期刊客户建立独立的配置中枢。 + +**核心目标:** 实现期刊 SaaS 的“千刊千面”。 + +**架构原则:** + +1. **配置继承模式 (Config Inheritance)**:系统提供【标准默认配置】。期刊默认继承系统标准;需要个性化时,可开启【自定义覆盖】。 +2. **中英双轨基线 (Dual-Baseline)**:针对稿约规范,系统提供“英文期刊”与“中文期刊”两套底层标准,供租户自主选择绑定。 +3. **全面 Prompt 化 (All-in-Prompt)**:取消细颗粒度表单,采用“专家评判标准 (Prompt) \+ 报告展示模板 (Handlebars)”的开放式极简架构。 + +## **🎯 一、 前端 UI 与功能分区要求** + +在现有的 ADMIN 租户详情页(TenantDetailPage)中,构建两大核心配置区块: + +### **区块 1:期刊基础信息与门户配置 (Basic Info & Portal)** + +用于定义该期刊的对外面貌和访问入口。 + +| 配置项名称 | 阶段 | 字段类型 | UI 组件 | 功能说明 | +| :---- | :---- | :---- | :---- | :---- | +| **期刊全称** | \[P0\] | String | 文本输入框 | 如:*Journal of Translational Internal Medicine*。显示在登录页及工作台顶部。 | +| **访问路径(Slug)** | \[P0\] | String | 文本输入框(需正则) | **极重要**:用于生成 review.xunzhengyixue.com/{slug}。仅允许小写字母、数字和连字符。 | +| **期刊 Logo** | \[P1\] | String | 图片上传组件 | 上传至 OSS 后保存 URL。用于替换默认的系统 Logo。 | +| **品牌主色调** | \[P1\] | String | 颜色拾取器 | 支持 HEX 色值。用于动态渲染按钮和高亮文字。 | +| **登录页背景图** | \[P2\] | String | 图片上传组件 | (暂不开发) 替换登录页的默认背景。 | + +### **区块 2:智能审稿配置 (RVW Skills Config) —— 【全维度 Prompt 架构】** + +这是该模块的核心。对于 4 个配置面板,顶部必须增加一个全局切换组件: + +* 🔘 **选项 A: 继承系统默认配置 (Inherit System Default)** \[P0\] —— 选中时,下方大文本框置灰只读。 +* 🔘 **选项 B: 启用个性化自定义 (Custom Override)** \[P0\] —— 选中时,解锁下方大文本框,允许大面积修改。 + +#### **Panel A: 稿约规范评估 (Editorial) —— 【支持中英标准选择】** + +作为特殊的规范模块,当选择 **“继承系统默认配置”** 时,必须提供一个**下拉单选框**供运营人员选择: + +* 🔘 **标准版英文期刊规范 (English Standard)** \[P0\]:自动绑定英文 SCI 级审查标准(严查伦理声明等)。 +* 🔘 **标准版中文稿约规范 (Chinese Standard)** \[P0\]:自动绑定中文核心期刊审查标准(查中英文摘要对照等)。 + +当选择 **“启用个性化自定义”** 时,展示下方的通用组件。 + +#### **统一的面板配置项(适用于 Panel B / C / D 及自定义状态下的 Panel A)** + +* **1\. 专家评判标准 (Expert Prompt)** \[P0\]:大文本框。自由输入该期刊特有的审查要求(大模型读取)。 +* **2\. 报告展示模板 (Handlebars Template)** \[P0\]:大文本框。用于配置最终输出给责编的 Markdown 格式。 +* ⚠️ **3\. 测试渲染 (Test Render)** \[P1\]:在模板编辑区提供“预览”按钮。使用假 JSON 数据在右侧实时渲染 Markdown 结果,防语法写错导致线上白屏。(MVP 阶段若来不及,可由运营人员去前台真实传 PDF 测试兜底)。 + +## **💾 二、 数据库 Schema 设计 (Prisma) \[P0\]** + +**DB 要求**:即便 UI 是分阶段开发,数据库字段必须在 \[P0\] 阶段一次性建好,避免后续频繁 Migrate。 + +利用数据库的 null 语义表达“继承”。取消原有的 JSON 数组,统一为 Text 类型的 Prompt 和 Template 字段。 + +重点新增 editorialBaseStandard 字段,用于持久化期刊的中英文基线选择。 + +model TenantRvwConfig { + id String @id @default(uuid()) + tenantId String @unique + tenant Tenant @relation(fields: \[tenantId\], references: \[id\]) + + // Panel A: 稿约规范评估 + // 'en' 代表选用标准英文规范,'zh' 代表选用标准中文规范。 + editorialBaseStandard String @default("en") + editorialExpertPrompt String? @db.Text + editorialHandlebarsTemplate String? @db.Text + + // Panel B: 方法学评估 + methodologyExpertPrompt String? @db.Text + methodologyHandlebarsTemplate String? @db.Text + + // Panel C: 数据验证评估 + dataForensicsExpertPrompt String? @db.Text + dataForensicsHandlebarsTemplate String? @db.Text + + // Panel D: 临床专业评估 + clinicalExpertPrompt String? @db.Text + clinicalHandlebarsTemplate String? @db.Text + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@schema("platform\_schema") +} + +## **🔌 三、 后端 API 与底层引擎继承逻辑 (核心)** + +### **1\. 内部 ADMIN 配置接口 (需要 ops:user-ops 权限) \[P0\]** + +* PUT /api/admin/tenants/:id/basic-info:更新期刊名称、Slug (P1阶段再接 Logo 等)。 +* GET /api/admin/tenants/:id/rvw-config:获取指定期刊的 Skills 配置(若不存在,返回 {})。 +* PUT /api/admin/tenants/:id/rvw-config:UPSERT 期刊 Skills 配置。**若前端选择“继承系统默认”,对应的 ExpertPrompt 和 Template 传 null,但 editorialBaseStandard 必须传选中的值('en'或'zh')**。 + +### **2\. 底层引擎运行时的“降级合并 (Fallback Merge)”逻辑 \[P0\]** + +后端重构 SkillExecutor 时,实施深度合并策略。以 **稿约规范 (EditorialSkill)** 为例: + +async function executeEditorialSkill(manuscript, tenantConfig) { + let promptToApply; + + // 1\. 如果租户自定义了稿约 Prompt,直接使用租户的开放式配置 (优先级最高) + if (tenantConfig && tenantConfig.editorialExpertPrompt) { + promptToApply \= tenantConfig.editorialExpertPrompt; + } + // 2\. 租户未自定义(为 null),触发系统默认降级逻辑 + else { + // 2.1 读取该期刊在后台配置的“标准语言基线” + const baseStandard \= tenantConfig?.editorialBaseStandard || 'en'; + + // 2.2 根据基线拉取对应的【系统标准版规范】Prompt + if (baseStandard \=== 'en') { + promptToApply \= await promptService.get('SYSTEM\_DEFAULT\_EDITORIAL\_EN'); + } else if (baseStandard \=== 'zh') { + promptToApply \= await promptService.get('SYSTEM\_DEFAULT\_EDITORIAL\_ZH'); + } + } + + // 3\. 将 promptToApply 喂给 LLM 执行,并使用对应 Handlebars 模板渲染... +} + +方法学、数据验证和临床评估的逻辑同理:最终提示词 \= 租户自定义 Prompt ?? 系统默认 Prompt。 + +## **👥 四、 实施 SOP:用户与期刊绑定(无需开发)** + +1. 客户在主站注册账号(或由超管分配账号)。 +2. 内部超管在 ADMIN 端的 **【用户管理 \-\> 租户成员管理】** 页面,将该用户的账号添加到目标期刊租户(如 JTIM)下。 +3. 确保该用户拥有 RVW 模块的使用权限。 +4. **验证**:此后该用户访问 review.xunzhengyixue.com/jtim 时,JWT 中会自动带上该租户身份,接口方可放行。 + +## **✅ 五、 验收标准 (Definition of Done \- P0 阶段)** + +1. ADMIN 后台成功渲染出只包含大文本框(Textarea)的极简配置界面。 +2. **中英基线验证**: + * 将 JTIM 的【稿约规范】设置为“继承系统默认”并选择\*\*【标准版英文期刊规范】\*\*,上传稿件时,系统严格核查伦理声明等英文规则。 + * 将 CMJ 设置为“继承系统默认”并选择\*\*【标准版中文稿约规范】\*\*,系统重点核查中英文摘要对应等中文规则。 +3. 当某期刊在【临床专业评估】开启自定义后,运营人员在大文本框中修改提示词并保存,新上传的稿件严格按该设定执行评估。 + +## **🚀 六、 分阶段实施与 MVP 裁剪指南 (给 PM 与研发团队)** + +为了保证项目快速上线,坚决执行 Managed SaaS (代运营) 和 All-in-Prompt 的裁剪策略。 + +### **🔴 第一阶段:MVP 必做清单 (P0) —— 目标:系统能跑通双语及个性化** + +* **数据库**:一次性建好所有 Schema 字段(包含全部 Text 和 BaseStandard 字段)。 +* **核心引擎**:必须完成 SkillExecutor 的降级合并逻辑(?? 合并)和中英文 Prompt 的自动路由。 +* **ADMIN 前端**:只要最基础、最“丑”的表单输入框。实现“继承/自定义”的单选切换,并暴露 Textarea。不做任何复杂的花哨控件。 +* **业务前端**:根据 URL 中的 /:tenantId 成功拦截并获取对应的【期刊名称】(纯文本展示即可)。 + +### **🟡 第二阶段:视觉与防呆增强 (P1) —— 目标:上线后 1-2 周内快反** + +* **模板预览 (Test Render)**:复用底层 PromptService 补齐预览按钮,防止运营在纯文本框写错 Handlebars 语法导致线上白屏。 +* **品牌视觉提升**:补齐期刊 Logo、主色调配置,并在客户端动态渲染,提升 SaaS 专属感。 +* **接口安全**:为公开的品牌信息拉取接口增加 Redis 缓存与 Rate Limiting 限流防刷。 + +### **🟢 第三阶段:暂缓或砍掉的伪需求 (P2) —— 目标:避免过度工程** + +* **细颗粒度表单控件**:既然已经全面 Prompt 化,坚决不再开发滑块(如权重分配)、下拉枚举(如验证级别)等细分 UI 控件。全部用 Prompt 解决。 +* **客户自助配置后台**:坚决不做。坚持代运营模式,期刊主编提需求,内部运营人员在 ADMIN 端修改,规避复杂的 B 端权限开发。 +* **登录页背景图定制**:暂不开发,统一使用系统默认干净纯色底即可。 \ No newline at end of file diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/中文医学期刊2025新国标稿约审查提示词.md b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/中文医学期刊2025新国标稿约审查提示词.md new file mode 100644 index 00000000..b65ccbc5 --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/中文医学期刊2025新国标稿约审查提示词.md @@ -0,0 +1,44 @@ +# **中国医学期刊2025新国标稿约审查提示词 (Prompt)** + +**使用场景:** 在 ADMIN 配置中心,当运营人员为某中文期刊租户选择 **“继承系统默认配置 \-\> 标准版中文稿约规范”** 时,系统底层默认调用的 Prompt。 + +**标准依据:** 深度融合 2025 年发布的最新三大国家标准:GB/T 7714-2025 (参考文献)、GB/T 6447-2025 (文献摘要)、GB/T 7713.1-2025 (学术体例)。 + +**设计逻辑:** 由于 LLM 缺乏 2025 年新规的先验知识,本 Prompt 采取“规则穷举显式化”设计,直接将判罚红线硬编码在提示词中。 + +请将以下内容直接复制到系统“中文版标准规范”的底层 Prompt 库中: + +【2025最新版中文医学期刊全维稿约核查规则】 +作为严谨的中国医学核心期刊审稿人,请对稿件的结构、学术规范及排版进行全面深度核查。你必须严格依据中国最新的三大国家标准(GB/T 6447-2025、GB/T 7714-2025、GB/T 7713.1)执行检查。请牢记并严格套用以下规则池: + +一、 🔴 摘要与关键词规范(依据 GB/T 6447-2025,若违反请标记为 major\_issue) +1\. 结构式摘要要求:原创论著的摘要必须为“结构式摘要”,强制包含【目的】、【方法】、【结果】、【结论】4个明确要素(或类似等效标识)。 +2\. 摘要内容禁区:摘要中严禁出现参考文献的引用(即不能带有 \[1\]、\[2\] 等引用角标);应尽量避免采用插图、表格、非公知公用的符号和缩写词。 +3\. 关键词数量:每篇论文必须提供 3个\~8个 关键词,多个关键词之间通常用分号 (;) 分隔。 +4\. 中英文一致性:必须同时提供中文摘要与英文摘要(Abstract),且英文摘要的要素和含义应与中文摘要保持对应一致。 + +二、 🟡 参考文献著录规范(依据 GB/T 7714-2025,重点核查项,若违反请标记为 major\_issue) +5\. 责任者(作者)著录规则: + \- 欧美作者名必须“姓在前,名在后”(如:Einstein A,不能写 A Einstein)。 + \- 中外文作者若不超过 3 个,必须全部照录;若超过 3 个,只著录前 3 个责任者,其后必须加“, 等”或“, et al.”(注意前置逗号)。 +6\. 文献类型与载体标识代码(新规重点,检查代码是否规范): + \- 必须准确标注类型:普通期刊 \[J\]、专著图书 \[M\]、学位论文 \[D\]、报告 \[R\]、会议录 \[C\]。 + \- \*\*2025新规必须支持并识别\*\*:预印本 \[PP\]、数据集 \[DS\]、网站/网页 \[EB/OL\] 或 \[EB\]。 + \- 电子资源必须标明载体,如 \[J/OL\]、\[M/OL\]、\[DS/OL\]。 +7\. 年卷期与页码格式: + \- 连续出版物(期刊)的标准格式必须为:\`年, 卷(期): 起讫页码.\`(例如:\`2024, 46(8): 102-111.\`)。 +8\. 日期格式:引用的电子资源、网站网页的发布日期、更新日期和引用日期,必须采用 \`YYYY-MM-DD\` 格式(如:\[2025-07-15\])。 +9\. DOI 永久标识符:强烈建议电子文献末尾带有持久标识符(如 DOI 或 CSTR)。格式示例:\`DOI:10.1038/nature13308.\` + +三、 🟢 学术体例与语言规范(依据 GB/T 7713.1-2025,常规核查项,若违反请标记为 minor\_issue) +10\. 图表规范: + \- 表格:应具有“自明性”,必须采用国际通行的“三线表”格式(避免使用复杂的内部网格线和垂直线)。 + \- 图表编号与图题:图题置于图的下方,表题置于表的上方。必须按出现顺采用阿拉伯数字连续编号(如:图1、表1)。 +11\. 计量单位与科学名词: + \- 必须严格采用国家法定的国际单位制(SI units)。 + \- 首次出现外文专业术语、缩略词时,必须用圆括号注明原词语全称。 +12\. 学术诚信与伦理(一票否决项,若违反请标记为 fatal): + \- 涉及人类受试者或动物的临床/实验研究,正文中必须明确提及“伦理委员会批准”以及“知情同意(Informed Consent)”。 + \- 严禁将 AI(如 ChatGPT 等大模型)列为共同作者。 + +请系统性地输出上述规则的违反情况(包含具体的缺陷位置和修改建议)。对于没有明确违反规则的项,无需列出。如果通篇没有任何违反,请回复“完全符合 2025 版中文医学期刊国家标准”。 diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/通用英文期刊稿约审查提示词.md b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/通用英文期刊稿约审查提示词.md new file mode 100644 index 00000000..7bfbc435 --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/通用英文期刊稿约审查提示词.md @@ -0,0 +1,32 @@ +# **通用英文医学期刊标准稿约审查提示词 (Prompt)** + +**使用场景:** 在 ADMIN 配置中心,当运营人员为某租户选择 **“继承系统默认配置 \-\> 标准版英文期刊规范”** 时,底层 SkillExecutor 默认调用的 Prompt。 + +**设计逻辑:** 覆盖主流 SCI 期刊及 ICMJE 规范的通用要求,作为英文期刊的“最大公约数”基线。 + +请将以下内容直接复制到系统的默认 Prompt 库中: + +【标准版英文医学期刊全维稿约核查规则】 +作为严谨的国际医学期刊审稿人,请对稿件的结构、伦理及学术排版进行全面深度核查。必须严格遵守以下通用国际规则池(ICMJE 规范): + +一、 🔴 致命错误与伦理规范(一票否决项,若违反请标记为 fatal) +1\. 伦理与知情同意:涉及人类或动物实验必须有 IRB/IACUC 伦理委员会批准说明;涉及人类受试者(含病例报告)必须有知情同意书(Informed Consent)获取声明。 +2\. 声明完整性:正文或结尾必须包含利益冲突声明 (Conflict of Interest / Competing Interests) 和资金资助声明 (Funding / Support)。即使没有,也必须声明 "None declared"。若是临床干预试验必须提供注册号。 +3\. AI 与署名:严禁 AI 工具(如 ChatGPT, LLM)作为作者署名。若声明使用了 AI 辅助写作或数据分析,必须予以清晰记录。 +4\. 盲审风险提示 (Blinded Article Check):请排查正文 (Main Text) 中是否遗留了作者姓名、具体机构名称、致谢或可溯源的资助编号(若期刊采取单盲则忽略,但通常建议分离至 Title Page)。 + +二、 🟡 核心体例与排版要求(重点核查项,若违反请标记为 major\_issue) +5\. 摘要合规性: + \- 论著 (Original Research) 必须为结构化摘要 (Structured Abstract,通常包含 Background/Objectives, Methods, Results, Conclusions),且字数一般在 200-250 字以内。 + \- 综述或病例报告通常为非结构化摘要,字数一般在 150-200 字以内。 +6\. 统计学表述规范:必须提供 P 值的精确数值(如 P \= 0.048),严禁简单表述为 "p \< 0.05"(极小值除外,如 P \< 0.001)。统计学参数字母通常需斜体。 +7\. 参考文献与引文一致性:检查正文引用(如上标方括号或作者-年份)是否全篇保持高度一致,并检查参考文献列表是否存在明显的格式错乱。 +8\. 图表独立性:表格严禁为不可编辑的图片格式(应为文本表格,多采用三线表);所有图表必须在正文中被按顺序交叉引用(Cross-referenced)。 + +三、 🟢 通用学术写作规范(常规核查项,若违反请标记为 minor\_issue) +9\. 语言一致性:全篇必须严格统一使用美式英语 (American English) 或英式英语 (British English),绝不可混用。 +10\. 数字拼写规则:句首的数字必须拼写成英文单词(如 "Fifteen patients...",严禁写 "15 patients");正文中 1-10 的数字通常要求使用英文拼写(带有度量衡单位的除外,如 5 mg)。 +11\. 学术专有名词:物种名称(如 \*E. coli\*)、基因符号等非英语的外来拉丁词汇必须使用斜体。 +12\. 度量单位:必须使用标准的国际单位制 (SI Units),且数字与单位之间通常需要保留一个空格(% 和 °C 除外)。 + +请系统性地输出上述规则的违反情况。对于没有明确违反规则的项,无需列出。如果通篇没有任何违反,请回复“符合所有国际通用规范要求”。 diff --git a/docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊配置中心MVP开发计划.md b/docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊配置中心MVP开发计划.md new file mode 100644 index 00000000..f65b36ed --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊配置中心MVP开发计划.md @@ -0,0 +1,326 @@ +# RVW V4.0 期刊配置中心 MVP 开发计划 + +> 文档版本: v1.0 +> 创建日期: 2026-03-15 +> 适用范围: RVW 多租户期刊配置中心(ADMIN 运营管理端) +> 约束前提: 严格遵循 `docs/04-开发规范/09-数据库开发规范.md` + +--- + +## 1. 目标与范围 + +本计划用于落地 RVW V4.0 的「期刊配置中心」MVP,满足以下目标: + +1. 在运营管理端新增独立一级模块「期刊配置中心」(类似 IIT 项目管理入口形态)。 +2. 系统默认配置支持区分: + - 中文-稿约规范性 + - 英文-稿约规范性 + - 方法学、数据验证、临床专业评估(通用默认) +3. 期刊(如 AAA、BBB)可选择: + - 继承系统默认配置 + - 在默认基础上进行覆盖 +4. 期刊基础信息新增字段:`期刊语言`(中文/英文/其他)。 +5. MVP 阶段功能可精简,但数据库字段一次性补齐(覆盖 P0/P1/P2 + 期刊语言)。 + +--- + +## 2. 关键输入文档 + +1. 需求基线 +`docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/RVW V4.0 期刊租户配置中心开发需求0315.md` + +2. 英文稿约默认规则(系统默认 EN 基线) +`docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/通用英文期刊稿约审查提示词.md` + +3. 数据库规范(强制约束) +`docs/04-开发规范/09-数据库开发规范.md` + +4. 既有总计划参考 +`docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊SaaS版开发计划.md` + +--- + +## 3. 架构策略(MVP 版) + +### 3.1 配置分层 + +- 第 1 层(系统默认层) + - Editorial: `ZH` / `EN` 双基线 + - Methodology / DataForensics / Clinical: 通用基线 +- 第 2 层(期刊继承层) + - 默认继承系统配置 +- 第 3 层(期刊覆盖层) + - 租户填入自定义 Prompt/Template 后覆盖系统默认 + +### 3.2 运行时合并规则 + +对任一技能维度,统一采用: + +`最终配置 = 期刊自定义值 ?? 系统默认值` + +Editorial 额外规则: + +- 若 `editorialExpertPrompt` 为空,则按 `editorialBaseStandard` 路由: + - `zh` -> 中文稿约默认 Prompt + - `en` -> 英文稿约默认 Prompt +- 若期刊未显式设置 `editorialBaseStandard`,按 `期刊语言` 映射默认值: + - 中文 -> `zh` + - 英文/其他 -> `en` + +--- + +## 4. 数据模型设计(一次性补齐) + +## 4.1 设计原则 + +- MVP 功能可分阶段启用,但字段一次性建完,避免重复迁移。 +- 使用 `null` 表达“继承系统默认”。 +- 严格使用 Prisma migration,不允许手工 DDL 改库。 + +## 4.2 表结构调整 + +### A. `platform_schema.tenants`(期刊基础信息) + +新增/补齐字段(若已有同义字段,统一命名后保留兼容映射): + +- `journal_language` (`ZH` | `EN` | `OTHER`) // P0 +- `journal_full_name` (String) // P0 +- `logo_url` (String?) // P1 +- `brand_color` (String?) // P1 +- `login_background_url` (String?) // P2 + +### B. `platform_schema.tenant_rvw_configs`(期刊审稿配置) + +一次性补齐四维 Prompt + Template: + +- `editorial_base_standard` (`zh` | `en`) default `en` +- `editorial_expert_prompt` (Text?) +- `editorial_handlebars_template` (Text?) +- `methodology_expert_prompt` (Text?) +- `methodology_handlebars_template` (Text?) +- `data_forensics_expert_prompt` (Text?) +- `data_forensics_handlebars_template` (Text?) +- `clinical_expert_prompt` (Text?) +- `clinical_handlebars_template` (Text?) + +> 兼容说明:历史结构化字段(如 `editorialRules`/`dataForensicsLevel`/`finerWeights`)如需保留,先保留读兼容;MVP 写入统一走 Prompt/Template 字段。 + +--- + +## 5. 数据库实施规范(必须执行) + +严格按 `09-数据库开发规范.md` 三条铁律执行: + +1. 所有变更通过 `npx prisma migrate dev --name xxx` 生成迁移。 +2. 禁止 `prisma db push`(含任何变体)。 +3. 每次迁移后,立即更新 `docs/05-部署文档/03-待部署变更清单.md`。 + +推荐本次迁移命名: + +- `20260315_add_journal_fields_to_tenants` +- `20260315_expand_tenant_rvw_configs_for_all_prompt_templates` + +Shadow DB 失败时,按规范第 10 章降级流程处理并 `migrate resolve --applied` 标记。 + +--- + +## 6. 可执行任务清单 + +## 6.1 后端任务 + +### BE-1 Schema 与迁移 +- 修改 `backend/prisma/schema.prisma` +- 生成并审查迁移 SQL +- 更新 Prisma Client + +交付物: +- `backend/prisma/migrations/*/migration.sql` +- `backend/prisma/schema.prisma` + +### BE-2 系统默认配置路由 +- 在 Prompt 默认库中明确: + - `SYSTEM_DEFAULT_EDITORIAL_EN` 使用英文默认规则(来源于指定文档) + - `SYSTEM_DEFAULT_EDITORIAL_ZH` 使用中文默认规则 + - 其余 3 个技能保留通用默认 + +### BE-3 管理端 API(期刊配置中心) +- 新增/扩展 API: + - `GET /api/admin/journal-configs` + - `GET /api/admin/journal-configs/:tenantId` + - `PUT /api/admin/journal-configs/:tenantId/basic-info` + - `PUT /api/admin/journal-configs/:tenantId/rvw-config` +- 权限:`authenticate + requirePermission('ops:user-ops')` + +### BE-4 RVW 执行链路接线 +- 在 Worker/Skill 装配处接入期刊配置 +- 按 `??` 规则完成 4 维配置合并 +- Editorial 增加 `zh/en` 基线路由 + +### BE-5 安全校验 +- `slug` 正则校验(小写字母/数字/连字符) +- `journalLanguage`、`editorialBaseStandard` 枚举校验 +- 日志记录配置来源(system/custom) + +--- + +## 6.2 前端任务 + +### FE-1 新增左侧一级模块 +- ADMIN 左侧导航新增 `期刊配置中心` +- 路由挂载与权限控制 + +### FE-2 列表页 +- 期刊列表(名称、slug、语言、状态、更新时间、操作) +- 支持搜索/筛选/分页 + +### FE-3 详情配置页 +- 基础信息区: + - 期刊全称 + - 访问路径 slug + - 期刊语言(中文/英文/其他) + - Logo/主色/背景图字段(先存储,MVP 可不渲染) +- 审稿配置区(4 Skills): + - 继承/自定义切换 + - Editorial 继承态支持 `zh/en` 基线选择 + - 每个维度 Prompt + Handlebars Template 文本框 + +### FE-4 API 类型对齐 +- 更新 TypeScript DTO 与后端一致 +- `null` 语义统一为“继承默认” + +--- + +## 6.3 联调任务 + +### INT-1 中英文默认稿约验证 +- JTIM 配置为 `en` 继承,触发英文稿约规则 +- CMJ 配置为 `zh` 继承,触发中文稿约规则 + +### INT-2 继承与覆盖验证 +- AAA 继承默认 +- BBB 在临床维度自定义 Prompt +- 对比两者结果差异应符合预期 + +### INT-3 兼容性验证 +- 历史任务查询不受影响 +- 现有租户登录/访问链路不回归 + +--- + +## 7. 验收标准(DoD) + +1. ADMIN 出现独立一级菜单「期刊配置中心」。 +2. 可在期刊维度配置并保存 `期刊语言`。 +3. 系统可明确区分中英文稿约默认规则。 +4. 期刊支持“继承默认 + 局部覆盖”两种模式。 +5. 数据库字段一次性补齐(P0/P1/P2 + 期刊语言)。 +6. 所有迁移已入库并追加到待部署变更清单。 + +--- + +## 8. 排期建议(MVP) + +- Day 1: BE-1 + BE-2(Schema/迁移/默认配置) +- Day 2: BE-3 + FE-1/FE-2(管理端骨架) +- Day 3: FE-3 + FE-4(配置页面联通) +- Day 4: BE-4 + 联调(INT-1/2/3) +- Day 5: 验收、文档补充、部署清单复核 + +--- + +## 9. 风险与规避 + +1. **风险:数据库 drift** + 规避:禁止 db push,迁移前后执行 status 与 SQL 审查。 + +2. **风险:字段多但 UI 尚未全部启用** + 规避:字段先入库,前端按阶段开放;未启用字段保持可空。 + +3. **风险:中英文规则路由错误** + 规避:加运行时日志(language/baseStandard/promptSource)与双租户回归用例。 + +--- + +## 10. 审查报告对齐结论(含 TenantType 现状) + +针对以下两份审查报告: + +- `docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/期刊配置中心MVP计划审查报告.md` +- `docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/ADMIN多业态租户架构演进评估 (1).md` + +本计划采用如下对齐结论: + +### 10.1 认可并吸收的观点 + +1. **前端独立模块方向正确**:在 ADMIN 左侧建立独立一级菜单“期刊配置中心”是正确演进方向。 +2. **后端数据层保持统一**:继续复用 `platform_schema.tenants`,不拆分新的“期刊主租户表”。 +3. **配置中心必须可运营**:Handlebars 模板能力应保留,避免后续“千刊千面”报告格式无法落地。 +4. **跨表一致性要求**:涉及 tenants 与 tenant_rvw_configs 同步修改的场景,应使用事务保证原子性。 + +### 10.2 需要修正的审查结论(以代码现状为准) + +两份报告中“系统当前没有 TenantType”的表述不准确。当前系统**已存在 TenantType**: + +- 后端 `backend/prisma/schema.prisma` 中已有 `enum TenantType` 与 `tenants.type` +- 当前枚举值:`HOSPITAL` / `PHARMA` / `INTERNAL` / `PUBLIC` + +因此,本项目不需要“从 0 引入 TenantType 机制”,而是需要在现有机制上评估扩展: + +- 方案 A(推荐):新增 `JOURNAL` 枚举值,用于期刊配置中心过滤与治理 +- 方案 B(兼容):暂不新增枚举,短期用 `INTERNAL` 或约定标签过渡(不推荐长期使用) + +本计划采用:**方案 A(新增 JOURNAL)**。 + +--- + +## 11. 最低成本实施策略(先跑通,不做大重构) + +### 11.1 决策原则 + +针对“当前 Prompt 是否硬编码、是否立刻大重构”的问题,本计划明确: + +1. **不做大重构**(避免阻塞交付) +2. **必须做最小配置适配**(保证配置中心不是“假配置”) +3. **Handlebars 保留并最小启用**(保留未来扩展能力,当前成本可控) + +### 11.2 最小配置适配(必须) + +在现有 RVW 链路上增加一层“薄适配”即可,不推翻当前实现: + +- 统一规则:`finalPrompt = tenantCustomPrompt ?? systemDefaultPrompt` +- Editorial 特殊规则: + - 租户未自定义时,按 `editorialBaseStandard` 选择 `zh/en` 默认 Prompt + - 若未显式设置 `editorialBaseStandard`,则按 `journalLanguage` 映射(`ZH -> zh`,其他 -> `en`) +- 目标:让“期刊配置中心”的保存结果立即影响审稿行为 + +### 11.3 Handlebars 最小启用策略 + +当前阶段不做复杂重构,但必须保留模板层: + +1. 字段保留:四维模板字段全部入库(已在第 4 章定义) +2. 运行时策略:模板为空时走系统默认模板 +3. 失败兜底:模板渲染失败时降级返回原始报告内容,防止线上中断 +4. MVP 可选项:测试渲染可先后置到 P1,但预留接口与校验能力 + +### 11.4 为什么不建议“完全按写死先跑” + +若只做 UI 不接运行时,配置中心会变成“可编辑但不生效”,风险更高: + +- 运营以为已生效,实际审稿仍走旧规则 +- 期刊 AAA/BBB 差异化配置无法验证 +- 后续回补执行链路时会引入二次返工和信任成本 + +因此本计划采用“**小步接线,先真生效**”策略,而非“先写死跑通”。 + +--- + +## 12. 本计划执行前检查清单 + +- [ ] 已确认英文默认稿约规则文本(指定文档) +- [ ] 已确认中文默认稿约规则文本(对应文档) +- [ ] 已同步研发团队数据库铁律(09 规范) +- [ ] 已确认 TenantType 扩展策略(新增 JOURNAL) +- [ ] 已确认“先最小适配,不做大重构”的实施口径 +- [ ] 已创建本次迁移命名并分配负责人 +- [ ] 已约定联调样例租户(至少 JTIM + CMJ) + diff --git a/docs/03-业务模块/RVW-稿件审查系统/05-测试文档/Dongen 2003.pdf b/docs/03-业务模块/RVW-稿件审查系统/05-测试文档/Dongen 2003.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f9d2024f6d21a4adbb2cc644517120178bb7ee1e GIT binary patch literal 163043 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�=FC2DRtI;k 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∈NYDt|6rIA77;*40&qb}O%79hSzN-?yxK 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�M~9 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 literal 0 HcmV?d00001 diff --git a/docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/ADMIN多业态租户架构演进评估 (1).md b/docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/ADMIN多业态租户架构演进评估 (1).md new file mode 100644 index 00000000..ce3ce84b --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/ADMIN多业态租户架构演进评估 (1).md @@ -0,0 +1,117 @@ +# **ADMIN 运营管理端:多业态租户架构演进评估与设计指南** + +**文档定位:** 针对平台未来面临“医院、药企、期刊”三大业态分化的战略级架构指导文件。 + +**核心主旨:** 摒弃“上帝级”通用租户配置页,走向\*\*“数据底层大一统,前端展现分而治之”\*\*的 SaaS 演进路线。 + +**评估结论:** 强烈建议将“期刊配置中心”在前端作为独立一级菜单和页面独立出来。 + +## **🔍 一、 业务形态分化趋势评估** + +平台目前服务的三类客户,其核心诉求与配置维度已经发生根本性分化: + +| 业态类型 | 核心业务模块 | ADMIN 端的配置侧重点 | 运营人员视角 | +| :---- | :---- | :---- | :---- | +| **医院 / 科研机构** | IIT (研究项目), DC (数据清洗) | REDCap 数据源打通、CRA 质控规则、伦理审查合规性、患者隐私脱敏级别。 | 偏向于“项目制”与“数据安全”管理。 | +| **药企 / 申办方** | PKB (企业知识库), AIA (智能体) | 专属知识库的向量化参数、RAG 检索阈值、多智能体协作 Prompt、合规审查界限。 | 偏向于“知识资产”与“AI 能力”调优。 | +| **医学期刊编辑部** | RVW (智能审稿) | 稿约规范双语基线、方法学/临床评估 Prompt 编排、Handlebars 退修信模板定制。 | 偏向于“审稿流水线”与“千刊千面”排版。 | + +**评估意见**:如果未来继续在同一个 TenantDetailPage.tsx 里通过 if-else 来堆砌这些表单,前端代码将迅速腐化,且运营人员在配置一家期刊时,可能会被医院的 REDCap 配置项干扰。**前端页面的物理拆分势在必行。** + +## **🏛️ 二、 核心架构建议:底层统一,表层分治** + +为了兼顾“开发成本”与“运营体验”,建议采用以下架构模式: + +### **1\. 底层大一统 (Unified Data Model)** + +**不能把租户表拆散**(不能建 journal\_tenants, hospital\_tenants)。 + +* **原因**:全平台的统一登录 (SSO)、统一计费 (Quotas)、基础信息 (Logo、名称) 必须保持全局唯一,复用现有的 platform\_schema.tenants。 +* **改造动作**:在 tenants 表中新增一个核心的枚举分类字段 tenant\_type。 + +### **2\. 1对1 扩展表 (1-to-1 Extension Tables)** + +针对不同业态的独特配置,采用垂直分表的模式,保持主表整洁。 + +* 期刊配置存入 tenant\_rvw\_configs +* 药企配置存入 tenant\_pkb\_configs (未来) +* 医院配置存入 tenant\_iit\_configs (未来) + +### **3\. 前端分治 (Decoupled Frontend Views)** + +在 ADMIN 端的左侧菜单,明确区分业务管理入口,为特定业态打造沉浸式的配置体验。 + +## **💾 三、 数据库 Schema 调整落地 (Prisma)** + +基于上述思路,在 MVP 阶段的数据库改造应如下进行: + +// 1\. 定义租户业态枚举 +enum TenantType { + CLINICAL // 医院/科研机构 (默认) + PHARMA // 药企/申办方 + JOURNAL // 医学期刊 + INTERNAL // 内部平台测试 +} + +// 2\. 改造基础租户表 +model Tenant { + id String @id @default(uuid()) + code String @unique // slug + name String + tenantType TenantType @default(CLINICAL) // 新增:业态分类 + journalLanguage String? // 'ZH' | 'EN' (仅当 type=JOURNAL 时有效) + + // 关联扩展表 + rvwConfig TenantRvwConfig? + // pkbConfig TenantPkbConfig? (未来扩展) + + @@schema("platform\_schema") +} + +// 3\. 期刊专属扩展表 (保持不变) +model TenantRvwConfig { + id String @id @default(uuid()) + tenantId String @unique + tenant Tenant @relation(fields: \[tenantId\], references: \[id\]) + // ... Prompt 和 Template 字段 +} + +## **🖥️ 四、 前端展现独立化设计方案 (ADMIN UI)** + +在之前的审查中,我曾建议“砍掉独立的期刊配置中心,回归租户详情页”。**在此,基于您的战略预判,我正式推翻该建议,支持您最初的构想:在前端建立独立的《期刊配置中心》。** + +### **1\. 左侧导航重构** + +ADMIN 端的导航栏应当按“业态垂直管理”和“平台横向管理”进行划分: + +▼ 平台横向管理 (横向能力) + \- 🏢 基础租户池 (所有租户的大盘,仅管理基础信息和计费) + \- 👥 全局用户与权限 + \- 🎨 Prompt 基础设施 + +▼ 垂直业态中心 (深度定制) + \- 🏥 临床研究中心配置 (过滤 tenantType=CLINICAL) + \- 💊 药企知识库配置 (过滤 tenantType=PHARMA) + \- 📖 期刊 SaaS 配置中心 (过滤 tenantType=JOURNAL) + +### **2\. “期刊配置中心”的专属交互设计** + +点击【期刊 SaaS 配置中心】后,前端交互应与普通租户管理区分开: + +* **列表页 (JournalConfigList)**: + * 后端 API 依然调用 GET /api/admin/tenants,但前端强制带上参数 ?type=JOURNAL。 + * 列表直接展示期刊专属的列:期刊名称、访问路径(Slug)、语言基线、AI 审查模块开关状态。 +* **新建期刊向导 (Creation Wizard)**: + * 点击“新增期刊”,不要弹出普通租户那种几十项配置的抽屉。 + * 弹出专属向导:第一步填名称和 Slug,第二步选“中文核心/英文 SCI”,第三步自动为其绑定 RVW 模块权限,一气呵成。 +* **沉浸式配置页 (JournalConfigDetail)**: + * 左侧是“稿约、方法学、数据、临床” 4 个锚点导航,右侧是您要求的“大文本框 Prompt \+ Handlebars 预览”,完全为期刊实施人员打造,没有任何多余的干扰信息。 + +## **🚀 五、 给 MVP 开发计划的修正建议** + +您可以直接把这份文档同步给开发团队,并对之前的《MVP 开发计划》做如下调整指示: + +1. **DB 调整**:必须在 tenants 表新增 tenant\_type (Enum) 字段,以此作为划分业态的唯一真理。 +2. **前端放行**:**恢复执行 FE-1 (新增左侧一级模块) 和 FE-2 (独立的期刊列表页) 任务!** 3\. **接口复用**:前端虽然拆了页面,但后端**不需要**写两套 CRUD。列表查询依然用 /api/admin/tenants?type=JOURNAL,配置更新依然用 /api/admin/tenants/:id/rvw-config。 + +**总结:** 您的这个决定避免了系统在未来半年内演变成一个庞大而混乱的“怪物”。通过**数据层的强内聚**(共享 Tenants 表和 SSO)与**前端页面的强解耦**(独立业务配置中心),平台将具备极强的横向扩展能力! \ No newline at end of file diff --git a/docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/期刊配置中心MVP计划审查报告.md b/docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/期刊配置中心MVP计划审查报告.md new file mode 100644 index 00000000..b63c557e --- /dev/null +++ b/docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/期刊配置中心MVP计划审查报告.md @@ -0,0 +1,76 @@ +# **RVW V4.0 期刊配置中心 MVP 开发计划 \- 架构与工程审查报告 (V2.0 战略更新版)** + +**审查目标:** 评估《RVW V4.0 期刊配置中心 MVP 开发计划 v1.0》的技术可行性、与平台多业态(临床/药企/期刊)演进架构的对齐度,以及潜在的生产环境风险。 + +**审查结论:** 计划整体逻辑清晰,**前端独立拆分模块(FE-1, FE-2)的战略方向极具前瞻性**。但在 **底层业态标识、数据库平滑迁移、防呆设计缺失** 等方面存在 5 个关键问题,需在进入开发前紧急修正。 + +## **🟢 核心架构亮点 (Architecture Gold Standard)** + +### **1\. 优秀的前端解耦:独立的期刊配置中心** + +* **计划现状**:任务 FE-1 和 FE-2 提出要“新增左侧一级模块(期刊配置中心)”和“新增列表页”。 +* **审查意见**:**极度赞同!** 随着平台演进,医院、药企、期刊的配置诉求将天差地别。将期刊配置从拥挤的通用 TenantDetailPage 中剥离出来,建立独立的沉浸式工作台,是极其健康的 SaaS 演进路线。 +* **⚠️ 后端防坑提醒**:虽然前端页面拆分了,但**后端绝对不能拆分新建独立的期刊租户表**。接口必须深度复用 GET /api/admin/tenants,通过下文提到的 tenant\_type 参数进行动态过滤。 + +## **🔴 高风险项 (P0级 \- 影响系统稳定与架构基线)** + +### **2\. 业态类型缺失:如何从底层区分“期刊”与“医院”?** + +* **计划现状**:将所有配置加在了全局的 tenants 表关联中,但没有明确区分租户种类的字段。 +* **冲突点**:前端要实现独立的“期刊列表页(FE-2)”,后端数据库中现有的临床租户和期刊租户目前混在一起,无法高效过滤。 +* **修正建议**: + 在 platform\_schema.tenants 表中,**必须**新增一个业态分类枚举字段: + enum TenantType { + CLINICAL + PHARMA + JOURNAL + } + // 在 tenants 表中新增: + tenantType TenantType @default(CLINICAL) + + 只有 tenantType \=== 'JOURNAL' 的租户才会出现在期刊配置中心的列表中。 + +### **3\. 数据库迁移雷区:向已有表强加非空字段导致 Migrate 崩溃** + +* **计划现状**:计划 4.2 A 节要求向已有的 platform\_schema.tenants 表新增 journal\_language 和 journal\_full\_name,且要求非空。 +* **冲突点**:线上数据库中 tenants 表已经存在历史数据(主站租户、IIT租户等)。如果直接执行 ALTER TABLE ADD COLUMN journal\_language TEXT NOT NULL,Prisma 会因为历史记录缺少该字段的默认值而直接报错中断。 +* **修正建议**: + 在 BE-1 的迁移策略中必须指明平滑过渡方案,建议二选一: + * **方案 A(推荐)**:在 Schema 中将该字段设为必填并给定默认值 @default("ZH"),或对于非期刊租户允许其具有特定默认值。 + * **方案 B**:设为可选字段 String?,在业务代码(Controller/Zod层)强制要求对于 tenantType \=== JOURNAL 的记录非空拦截。 + +## **🟡 中风险项 (P1级 \- 影响开发体验与业务闭环)** + +### **4\. 致命防呆设计丢失:Handlebars“测试渲染”预览功能被遗漏** + +* **计划现状**:任务 FE-3 仅规划了“每个维度 Prompt \+ Handlebars Template 文本框”。 +* **冲突点**:让运营人员在干巴巴的文本框里盲写 Handlebars 代码是灾难性的。一旦写错括号 {{ 导致线上渲染白屏,客诉极大。 +* **修正建议**: + 必须在 FE-3 中加回 **“测试渲染 (Test Render)”** 按钮。要求前端复用 ADMIN 现有的 PromptEditorPage 中的 Mock 预览机制,在右侧提供实时 Markdown 预览面板。(此功能是 All-in-Prompt 架构的风控底线)。 + +### **5\. API 事务一致性风险:跨表更新未强调 Transaction** + +* **计划现状**:任务 BE-3 设计了 PUT /.../basic-info 和 PUT /.../rvw-config。 +* **冲突点**:在“保存并发布”时,如果前端提交了一个大表单,同时修改了主表 tenants(如:期刊名称)和关联表 tenant\_rvw\_configs(如:方法学 Prompt)。若分开调用接口或在 Service 层不使用事务,极易造成数据不一致。 +* **修正建议**: + 在 BE-3 任务描述中增加硬性约束:**涉及跨 tenants 与 tenant\_rvw\_configs 表的同时写入操作,必须使用 Prisma $transaction 包裹,确保原子性。** + +## **🟢 低风险项 (P2级 \- 细节补齐)** + +### **6\. OTHER 语言的枚举映射与回退机制** + +* **计划现状**:计划 3.2 提到“英文/其他 \-\> en”。 +* **建议补充**:在后端 SkillExecutor 的合并逻辑中(BE-4),明确将这一点写进代码注释: + // 当 journalLanguage 为 OTHER 时,强制 fallback 到 EN 基线,以通用 SCI 标准兜底 + const langBase \= tenantConfig.editorialBaseStandard ?? + (tenant.journalLanguage \=== 'ZH' ? 'zh' : 'en'); + +## **🚀 最终行动建议 (Next Steps)** + +请开发团队在实施前对计划文档作如下微调: + +1. **推进独立前端**:落实 FE-1 和 FE-2,确保期刊业务拥有纯净的管理体验。 +2. **底层数据扩充**:在 Prisma Schema 中补齐 tenant\_type 枚举,并为 journal\_language 设定 @default 兜底(或允许为空)以解决数据迁移报错风险。 +3. **安全防线**:务必补上前端的 **“Handlebars 实时预览”** 按钮和后端的 **$transaction 事务一致性** 约束。 + +完成以上 3 点修正后,该 MVP 计划可立即进入研发阶段。 \ No newline at end of file diff --git a/docs/05-部署文档/03-待部署变更清单.md b/docs/05-部署文档/03-待部署变更清单.md index a5360092..b358b227 100644 --- a/docs/05-部署文档/03-待部署变更清单.md +++ b/docs/05-部署文档/03-待部署变更清单.md @@ -19,6 +19,7 @@ | DB-1 | SSA Agent 执行记录新增分步执行与种子审计字段(`step_results/current_step/seed_audit`) | `20260311_add_ssa_agent_step_seed_fields` | 高 | 按数据库规范生成;Shadow DB 失败后采用降级流程产出 SQL,并已人工收敛为仅本次字段变更 | | DB-2 | RVW V4.0:新增 `platform_schema.tenant_rvw_configs` 表(每期刊独立审稿配置,含4维提示词+Handlebars模板) | `20260314_add_tenant_rvw_configs` | 高 | ⚠️ 部署前无需前置条件;使用降级流程手动创建迁移SQL,需执行 `prisma migrate resolve --applied 20260314_add_tenant_rvw_configs` 标记为已应用 | | DB-3 | RVW V4.0:`rvw_schema.review_tasks` 新增 `tenant_id` 字段 + 索引(历史数据平滑回填两步走) | `20260314_add_tenant_id_to_review_tasks` | 高 | ⚠️ **部署前必须先确认** `platform_schema.tenants` 中存在 `code='yanjiu'` 的主站默认租户;迁移会自动将历史记录回填为该租户ID;需执行 `prisma migrate resolve --applied 20260314_add_tenant_id_to_review_tasks` 标记为已应用 | +| DB-4 | RVW V4.0:期刊配置中心 MVP 一次性补齐字段(`tenants` 新增期刊字段;`tenant_rvw_configs` 升级为 4 维 Prompt+Template;`TenantType` 新增 `JOURNAL`) | `20260315_journal_config_center_mvp` | 高 | ⚠️ `prisma migrate dev` 因历史 shadow DB 迁移失败触发降级流程,已按规范收敛手工 SQL;部署后执行 `prisma migrate resolve --applied 20260315_journal_config_center_mvp` | ### 后端变更 (Node.js) @@ -30,7 +31,9 @@ | BE-4 | R 代码语法修复器纠正 `} else` 处理策略,避免引入 `unexpected 'else'` | `backend/src/modules/ssa/services/CodeRunnerService.ts` | 重新构建镜像 | 修复线上语法错误噪声,减少重试失败 | | BE-5 | RVW 审稿通道改造:4 通道 Prompt 动静分离(业务提示词可编辑 + 系统协议固化)+ 方法学/稿约 JSON 结构化修复兜底 + DataForensics 默认切换为 LLM-only(规则验证默认关闭) | `backend/src/modules/rvw/services/promptProtocols.ts`, `backend/src/modules/rvw/services/editorialService.ts`, `backend/src/modules/rvw/services/methodologyService.ts`, `backend/src/modules/rvw/services/clinicalService.ts`, `backend/src/modules/rvw/skills/library/DataForensicsSkill.ts`, `backend/src/modules/rvw/skills/core/types.ts`, `backend/src/common/document/ExtractionClient.ts`, `backend/src/common/prompt/prompt.fallbacks.ts` | 重新构建镜像 | 解决运营端改 Prompt 导致 JSON 解析失败;数据侦探默认仅“表格提取+LLM判断”,规则代码保留可回切 | | BE-6 | RVW V4.0 Phase 1:Prisma Schema 新增 TenantRvwConfig 模型 + ReviewTask.tenantId 字段 + RVW租户中间件(rvwTenantMiddleware,slug到UUID解析+tenant_members校验+缓存)+ FastifyRequest 扩展 tenantId/tenant 字段 | `backend/prisma/schema.prisma`, `backend/src/modules/rvw/middleware/rvwTenantMiddleware.ts`, `backend/src/common/auth/auth.middleware.ts` | 重新构建镜像 + 执行 DB-2/DB-3 迁移 | 与 DB-2/DB-3 配套上线;Prisma Client 已重新生成 | -|| BE-7 | RVW V4.0 Phase 2:RVW Config CRUD API(GET/PUT `/api/admin/tenants/:id/rvw-config`)+ Handlebars 渲染引擎(Zod 校验 + 默认模板)+ SkillExecutor 按租户装配 Hybrid Prompt | `backend/src/modules/admin/rvw-config/rvwConfigController.ts`, `backend/src/modules/admin/rvw-config/rvwConfigService.ts`, `backend/src/modules/admin/rvw-config/rvwConfigRoutes.ts`, `backend/src/modules/rvw/services/rvwReportRenderer.ts`, `backend/src/modules/rvw/workers/reviewWorker.ts`, `backend/src/index.ts` | 重新构建镜像 | 与 BE-6/DB-2/DB-3 配套上线 | +| BE-7 | RVW V4.0 Phase 2:RVW Config CRUD API(GET/PUT `/api/admin/tenants/:id/rvw-config`)+ Handlebars 渲染引擎(Zod 校验 + 默认模板)+ SkillExecutor 按租户装配 Hybrid Prompt | `backend/src/modules/admin/rvw-config/rvwConfigController.ts`, `backend/src/modules/admin/rvw-config/rvwConfigService.ts`, `backend/src/modules/admin/rvw-config/rvwConfigRoutes.ts`, `backend/src/modules/rvw/services/rvwReportRenderer.ts`, `backend/src/modules/rvw/workers/reviewWorker.ts`, `backend/src/index.ts` | 重新构建镜像 | 与 BE-6/DB-2/DB-3 配套上线 | +| BE-8 | RVW V4.0 Phase 3:后端 CORS 配置新增 x-tenant-id 白名单(多租户 Header 跨域支持) | `backend/src/index.ts` | 重新构建镜像 | ⚠️ 缺少此项会导致浏览器预检请求拦截 x-tenant-id,整个多租户功能失效 | +| BE-9 | RVW V4.0 Phase 4(期刊配置中心 MVP):新增 `/api/admin/journal-configs` 专用 API;租户模型扩展期刊字段;RVW 执行链路接入最小配置适配(`tenantCustom ?? systemDefault`),Editorial 支持 `zh/en` 基线路由;创建/更新为 `JOURNAL` 时自动兜底开通 `RVW` 模块 | `backend/src/modules/admin/journal-config/*`, `backend/src/modules/admin/services/tenantService.ts`, `backend/src/modules/admin/types/tenant.types.ts`, `backend/src/modules/admin/rvw-config/*`, `backend/src/modules/rvw/workers/reviewWorker.ts`, `backend/src/modules/rvw/skills/library/*`, `backend/src/modules/rvw/services/editorialService.ts`, `backend/src/modules/rvw/services/clinicalService.ts`, `backend/src/common/prompt/prompt.fallbacks.ts`, `backend/src/index.ts` | 重新构建镜像 + 执行 DB-4 迁移 | 与 FE-7 联动上线,满足「独立一级菜单 + 中英文基线 + 继承/覆盖」MVP 范围,并避免新期刊用户登录后无 RVW 模块权限 | ### 前端变更 @@ -39,10 +42,11 @@ | FE-1 | Agent 通道接入 step_* SSE 事件并展示分步执行状态(兼容旧 code_* 事件) | `frontend-v2/src/modules/ssa/hooks/useSSAChat.ts`, `frontend-v2/src/modules/ssa/components/AgentCodePanel.tsx`, `frontend-v2/src/modules/ssa/types/index.ts`, `frontend-v2/src/modules/ssa/stores/ssaStore.ts` | 重新构建镜像 | 右侧工作区可见每步状态/错误/耗时,便于排障 | | FE-2 | Agent 计划阶段复用 QPER 变量编辑控件(单变量/多变量)并接入保存、确认前自动保存 | `frontend-v2/src/modules/ssa/components/AgentCodePanel.tsx`, `frontend-v2/src/modules/ssa/components/WorkflowTimeline.tsx`, `frontend-v2/src/modules/ssa/components/SSAWorkspacePane.tsx` | 重新构建镜像 | 对接 `PATCH /agent-executions/:executionId/plan-params`,实现 5A.5 前后端闭环 | | FE-3 | Agent 工作区增强:在分步状态下可展开查看每步已生成结果(`reportBlocks`),并兼容严格分步模式下的 `code_pending` 空代码预览 | `frontend-v2/src/modules/ssa/components/AgentCodePanel.tsx`, `frontend-v2/src/modules/ssa/hooks/useSSAChat.ts` | 重新构建镜像 | 修复“有结果但代码下方不可见”与状态显示误导问题 | -|| FE-4 | RVW V4.0 Phase 2:TenantDetailPage 新增「智能审稿配置」Tab(4 Panel:稿约规范占位 / 方法学Prompt+Handlebars模板 / 数据验证深度L1-L3 / 临床FINER权重)+ tenantApi 新增 fetchRvwConfig/saveRvwConfig | `frontend-v2/src/pages/admin/tenants/TenantDetailPage.tsx`, `frontend-v2/src/pages/admin/tenants/api/tenantApi.ts` | 重新构建镜像 | 与 BE-7/DB-2/DB-3 配套上线 | -|| BE-8 | RVW V4.0 Phase 3:后端 CORS 配置新增 x-tenant-id 白名单(多租户 Header 跨域支持) | `backend/src/index.ts` | 重新构建镜像 | ⚠️ 缺少此项会导致浏览器预检请求拦截 x-tenant-id,整个多租户功能失效 | -|| FE-5 | RVW V4.0 Phase 3:新增 TenantPortalLayout 极简期刊门户布局 + App.tsx 新增 /:tenantSlug/rvw/* 路由(期刊专属 URL)+ LoginPage.tsx 修复跳转逻辑(读取 ?redirect= 查询参数 + 租户默认落地页 /:tenantSlug/rvw) | `frontend-v2/src/framework/layout/TenantPortalLayout.tsx`, `frontend-v2/src/App.tsx`, `frontend-v2/src/pages/LoginPage.tsx` | 重新构建镜像 | 与 BE-6/BE-7/BE-8 配套上线;实现期刊租户完整访问路径 /jtim → /jtim/rvw | -| FE-6 | RVW V4.0 租户门户体验收敛:上传按钮稳定触发(原生文件选择器 API + input 回退)、执行审稿后跳转旧版过程页(复用 `TaskDetail`)、列表四维状态图标修复(完成态正确显示绿勾/警告) | `frontend-v2/src/modules/rvw/pages/TenantDashboard.tsx`, `frontend-v2/src/modules/rvw/pages/TenantTaskDetail.tsx` | 重新构建镜像 | 已本地联调通过;上线后重点回归 `/t/:tenant/login -> /:tenant/rvw` 主流程 | +| FE-4 | RVW V4.0 Phase 2:TenantDetailPage 新增「智能审稿配置」Tab(4 Panel:稿约规范占位 / 方法学Prompt+Handlebars模板 / 数据验证深度L1-L3 / 临床FINER权重)+ tenantApi 新增 fetchRvwConfig/saveRvwConfig | `frontend-v2/src/pages/admin/tenants/TenantDetailPage.tsx`, `frontend-v2/src/pages/admin/tenants/api/tenantApi.ts` | 重新构建镜像 | 与 BE-7/DB-2/DB-3 配套上线 | +| FE-5 | RVW V4.0 Phase 3:新增 TenantPortalLayout 极简期刊门户布局 + App.tsx 新增 /:tenantSlug/rvw/* 路由(期刊专属 URL)+ LoginPage.tsx 修复跳转逻辑(读取 ?redirect= 查询参数 + 租户默认落地页 /:tenantSlug/rvw) | `frontend-v2/src/framework/layout/TenantPortalLayout.tsx`, `frontend-v2/src/App.tsx`, `frontend-v2/src/pages/LoginPage.tsx` | 重新构建镜像 | 与 BE-6/BE-7/BE-8 配套上线;实现期刊租户完整访问路径 /jtim → /jtim/rvw | +| FE-6 | RVW V4.0 租户门户体验收敛:上传按钮稳定触发(原生文件选择器 API + input 回退)、执行审稿后跳转旧版过程页(复用 `TaskDetail`)、列表四维状态图标修复(完成态正确显示绿勾/警告) | `frontend-v2/src/modules/rvw/pages/TenantDashboard.tsx`, `frontend-v2/src/modules/rvw/pages/TenantTaskDetail.tsx` | 重新构建镜像 | 已本地联调通过;上线后重点回归 `/:tenant/login -> /:tenant/rvw` 主流程 | +| FE-7 | RVW V4.0 期刊配置中心 MVP:ADMIN 新增独立一级菜单“期刊配置中心”,新增列表页/详情页(基础信息 + 4 维审稿配置),并对齐新 API DTO(`journalLanguage`、`editorialBaseStandard`、Prompt/Handlebars 字段) | `frontend-v2/src/framework/layout/AdminLayout.tsx`, `frontend-v2/src/App.tsx`, `frontend-v2/src/pages/admin/journal-configs/*`, `frontend-v2/src/pages/admin/tenants/api/tenantApi.ts`, `frontend-v2/src/pages/admin/tenants/TenantDetailPage.tsx` | 重新构建镜像 | 与 BE-9/DB-4 配套上线;MVP 阶段先跑通配置闭环,品牌视觉字段先存储后逐步渲染 | +| FE-8 | 登录路径策略统一为单一路径:仅保留 `/:tenantCode/login`,未登录重定向与退出登录统一 `/:tenant/login`,开发/生产同路径仅域名不同 | `frontend-v2/src/App.tsx`, `frontend-v2/src/framework/router/RouteGuard.tsx`, `frontend-v2/src/framework/layout/TenantPortalLayout.tsx`, `frontend-v2/src/pages/TenantLoginPage.tsx`, `frontend-v2/src/pages/LoginPage.tsx` | 重新构建镜像 | 对齐生产目标链接 `review.xunzhengyixue.com/test-qikan01`;不再保留 `/t/:tenantCode/login` 路径,需同步更新测试文档/书签 | ### Python 微服务变更 @@ -66,7 +70,8 @@ | # | 变更内容 | 范围 | 备注 | |---|---------|------|------| -| — | *暂无* | | | +| INF-1 | 前端 Nginx 增加 SPA 深链回退(`try_files $uri /index.html`),并确保 `/api/` 路由优先反代后端(不能被 index.html 吞掉) | frontend-nginx-service / ingress | ⚠️ 否则直接访问 `review.xunzhengyixue.com/test-qikan01` 或 `.../test-qikan01/login` 会 404;部署后需在无缓存浏览器验证 | +| INF-2 | 前端 Docker 构建与发布注意:确认镜像内 Nginx 配置已包含深链规则、并清理旧静态资源缓存(CDN/浏览器)后再灰度 | frontend-v2 build/deploy pipeline | 建议发布后先执行硬刷新(Ctrl+F5)与隐身窗口验证,避免旧 bundle 缓存导致仍跳旧路径 | --- diff --git a/frontend-v2/src/App.tsx b/frontend-v2/src/App.tsx index 61371282..83621e51 100644 --- a/frontend-v2/src/App.tsx +++ b/frontend-v2/src/App.tsx @@ -39,6 +39,8 @@ import IitQcCockpitPage from './modules/admin/pages/IitQcCockpitPage' import IitMemberManagePage from './modules/admin/pages/IitMemberManagePage' // 运营日志 import ActivityLogsPage from './pages/admin/ActivityLogsPage' +import JournalConfigListPage from './pages/admin/journal-configs/JournalConfigListPage' +import JournalConfigDetailPage from './pages/admin/journal-configs/JournalConfigDetailPage' // 个人中心页面 import ProfilePage from './pages/user/ProfilePage' @@ -55,7 +57,7 @@ import ProfilePage from './pages/user/ProfilePage' * * 路由结构: * - /login - 通用登录页(个人用户) - * - /t/{tenantCode}/login - 租户专属登录页 + * - /{tenantCode}/login - 租户专属登录页 * - / - 首页(需要认证) * - /{module}/* - 业务模块(需要认证+权限) */ @@ -113,8 +115,8 @@ function App() { {/* 登录页面(无需认证) */} } /> - {/* 期刊租户专属登录页(全屏原型风格,独立于所有 Layout)*/} - } /> + {/* 期刊租户专属登录页(全屏原型风格,独立于所有 Layout) */} + } /> {/* 业务应用端 /app/* */} }> @@ -168,6 +170,9 @@ function App() { } /> {/* 运营日志 */} } /> + {/* 期刊配置中心 */} + } /> + } /> {/* 系统配置 */} 🚧 系统配置页面开发中...} /> diff --git a/frontend-v2/src/framework/layout/AdminLayout.tsx b/frontend-v2/src/framework/layout/AdminLayout.tsx index 3ada1419..c217612f 100644 --- a/frontend-v2/src/framework/layout/AdminLayout.tsx +++ b/frontend-v2/src/framework/layout/AdminLayout.tsx @@ -101,6 +101,7 @@ const AdminLayout = () => { type: 'group' as const, label: '商务运营', children: [ + { key: '/admin/journal-configs', icon: , label: '期刊配置中心' }, { key: '/admin/tenants', icon: , label: '租户管理' }, { key: '/admin/users', icon: , label: '用户管理' }, { key: '/admin/activity-logs', icon: , label: '运营日志' }, diff --git a/frontend-v2/src/framework/layout/TenantPortalLayout.tsx b/frontend-v2/src/framework/layout/TenantPortalLayout.tsx index 35e58398..a3450989 100644 --- a/frontend-v2/src/framework/layout/TenantPortalLayout.tsx +++ b/frontend-v2/src/framework/layout/TenantPortalLayout.tsx @@ -63,7 +63,7 @@ export default function TenantPortalLayout() { const handleLogout = async () => { await logout(); - window.location.href = `/t/${tenantSlug}/login`; + window.location.href = `/${tenantSlug}/login`; }; return ( diff --git a/frontend-v2/src/framework/router/RouteGuard.tsx b/frontend-v2/src/framework/router/RouteGuard.tsx index 8711a127..fb292e9d 100644 --- a/frontend-v2/src/framework/router/RouteGuard.tsx +++ b/frontend-v2/src/framework/router/RouteGuard.tsx @@ -14,7 +14,7 @@ import { LockOutlined } from '@ant-design/icons' * 3. 有权限→渲染子组件 * * @version 2026-03-14 V4.0:租户感知重定向 - * - 期刊租户路径(/jtim/*)→ /t/jtim/login?redirect=/jtim/dashboard + * - 期刊租户路径(/jtim/*)→ /jtim/login?redirect=/jtim/dashboard * - 主平台路径(/rvw/*, /ai-qa/* 等)→ /login(行为不变) * - extractTenantSlug 由 useTenantObserver 模块统一维护,自动排除所有注册模块路径 */ @@ -22,16 +22,16 @@ import { LockOutlined } from '@ant-design/icons' /** * 构造未登录时的登录跳转目标。 * - * 对齐 App.tsx 中已定义的路由: + * 对齐 App.tsx 中已定义的路由: * - * - 期刊租户下:/t/jtim/login?redirect=%2Fjtim%2Fdashboard + * - 期刊租户下:/jtim/login?redirect=%2Fjtim%2Fdashboard * - 主站下:/login(保持原有行为,向后兼容) */ function buildLoginRedirect(pathname: string): string { const tenantSlug = extractTenantSlug(pathname) if (!tenantSlug) return '/login' const redirectParam = encodeURIComponent(pathname) - return `/t/${tenantSlug}/login?redirect=${redirectParam}` + return `/${tenantSlug}/login?redirect=${redirectParam}` } interface RouteGuardProps { diff --git a/frontend-v2/src/pages/LoginPage.tsx b/frontend-v2/src/pages/LoginPage.tsx index e82f14bd..e343d339 100644 --- a/frontend-v2/src/pages/LoginPage.tsx +++ b/frontend-v2/src/pages/LoginPage.tsx @@ -7,7 +7,7 @@ * * 路由: * - /login - 通用登录(个人用户) - * - /t/{tenantCode}/login - 租户专属登录(机构用户) + * - /{tenantCode}/login - 租户专属登录(机构用户) */ import { useState, useEffect, useCallback } from 'react'; @@ -106,7 +106,7 @@ export default function LoginPage() { const userRole = user?.role; const userModules = user?.modules || []; - // 1. ?redirect= 查询参数优先(RouteGuard 对租户路由设置,如 /t/jtim/login?redirect=%2Fjtim%2Frvw) + // 1. ?redirect= 查询参数优先(RouteGuard 对租户路由设置,如 /jtim/login?redirect=%2Fjtim%2Frvw) const searchParams = new URLSearchParams(location.search); const redirectParam = searchParams.get('redirect'); if (redirectParam) { @@ -132,7 +132,7 @@ export default function LoginPage() { return from; } - // 3. 期刊租户专属登录页(/t/:tenantCode/login)且无 redirect 参数时,默认进入该租户审稿页 + // 3. 期刊租户专属登录页(/:tenantCode/login)且无 redirect 参数时,默认进入该租户审稿页 if (tenantCode) { return `/${tenantCode}/rvw`; } diff --git a/frontend-v2/src/pages/TenantLoginPage.tsx b/frontend-v2/src/pages/TenantLoginPage.tsx index b70dfec9..11017ead 100644 --- a/frontend-v2/src/pages/TenantLoginPage.tsx +++ b/frontend-v2/src/pages/TenantLoginPage.tsx @@ -3,7 +3,7 @@ * ─ 100% 独立全屏,无任何顶部导航/Layout 包裹 * ─ 设计 100% 还原 AI审稿V1.html 原型图 * ─ 功能:密码登录 + 验证码登录(与主站 LoginPage 逻辑相同) - * ─ 路由: /t/:tenantCode/login + * ─ 路由: /:tenantCode/login */ import { useState, useEffect, useCallback } from 'react'; diff --git a/frontend-v2/src/pages/admin/journal-configs/JournalConfigDetailPage.tsx b/frontend-v2/src/pages/admin/journal-configs/JournalConfigDetailPage.tsx new file mode 100644 index 00000000..afb2c742 --- /dev/null +++ b/frontend-v2/src/pages/admin/journal-configs/JournalConfigDetailPage.tsx @@ -0,0 +1,233 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Alert, Button, Card, Divider, Form, Input, Select, Space, Spin, Tabs, message } from 'antd'; +import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'; +import type { JournalLanguage, UpdateRvwConfigRequest } from '../tenants/api/tenantApi'; +import { + fetchJournalDetail, + saveJournalBasicInfo, + saveJournalRvwConfig, +} from './api/journalConfigApi'; + +const { TextArea } = Input; + +const LANGUAGE_OPTIONS: Array<{ value: JournalLanguage; label: string }> = [ + { value: 'ZH', label: '中文' }, + { value: 'EN', label: '英文' }, + { value: 'OTHER', label: '其他' }, +]; + +export default function JournalConfigDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [activeTab, setActiveTab] = useState('basic'); + const [basicForm] = Form.useForm(); + const [rvwForm] = Form.useForm(); + + const load = async () => { + if (!id) return; + setLoading(true); + try { + const detail = await fetchJournalDetail(id); + basicForm.setFieldsValue({ + name: detail.tenant.name, + journalFullName: detail.tenant.journalFullName || detail.tenant.name, + code: detail.tenant.code, + journalLanguage: detail.tenant.journalLanguage || 'ZH', + logoUrl: detail.tenant.logoUrl || '', + brandColor: detail.tenant.brandColor || '', + loginBackgroundUrl: detail.tenant.loginBackgroundUrl || '', + }); + + rvwForm.setFieldsValue({ + editorialBaseStandard: detail.rvwConfig?.editorialBaseStandard || 'en', + editorialExpertPrompt: detail.rvwConfig?.editorialExpertPrompt || '', + editorialHandlebarsTemplate: detail.rvwConfig?.editorialHandlebarsTemplate || '', + methodologyExpertPrompt: detail.rvwConfig?.methodologyExpertPrompt || '', + methodologyHandlebarsTemplate: detail.rvwConfig?.methodologyHandlebarsTemplate || '', + dataForensicsExpertPrompt: detail.rvwConfig?.dataForensicsExpertPrompt || '', + dataForensicsHandlebarsTemplate: detail.rvwConfig?.dataForensicsHandlebarsTemplate || '', + clinicalExpertPrompt: detail.rvwConfig?.clinicalExpertPrompt || '', + clinicalHandlebarsTemplate: detail.rvwConfig?.clinicalHandlebarsTemplate || '', + }); + } catch (e: any) { + message.error(e.message || '加载期刊配置失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [id]); + + const handleSaveBasic = async () => { + if (!id) return; + try { + const values = await basicForm.validateFields(); + setSaving(true); + await saveJournalBasicInfo(id, values); + message.success('基础信息已保存'); + await load(); + } catch (e: any) { + if (e?.errorFields) return; + message.error(e.message || '保存失败'); + } finally { + setSaving(false); + } + }; + + const handleSaveRvw = async () => { + if (!id) return; + try { + const values = await rvwForm.validateFields(); + const payload: UpdateRvwConfigRequest = { + editorialBaseStandard: values.editorialBaseStandard, + editorialExpertPrompt: values.editorialExpertPrompt || null, + editorialHandlebarsTemplate: values.editorialHandlebarsTemplate || null, + methodologyExpertPrompt: values.methodologyExpertPrompt || null, + methodologyHandlebarsTemplate: values.methodologyHandlebarsTemplate || null, + dataForensicsExpertPrompt: values.dataForensicsExpertPrompt || null, + dataForensicsHandlebarsTemplate: values.dataForensicsHandlebarsTemplate || null, + clinicalExpertPrompt: values.clinicalExpertPrompt || null, + clinicalHandlebarsTemplate: values.clinicalHandlebarsTemplate || null, + }; + + setSaving(true); + await saveJournalRvwConfig(id, payload); + message.success('审稿配置已保存'); + await load(); + } catch (e: any) { + if (e?.errorFields) return; + message.error(e.message || '保存失败'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + }, + { + key: 'rvw', + label: '智能审稿配置', + children: ( + <> + +
+ A. 稿约规范评估(中英基线) + + + + +