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 00000000..f9d2024f Binary files /dev/null and b/docs/03-业务模块/RVW-稿件审查系统/05-测试文档/Dongen 2003.pdf differ 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. 稿约规范评估(中英基线) + + + + +