feat(rvw): deliver tenant portal v4 flow and config foundation
Implement RVW V4.0 tenant-aware backend/frontend flow with tenant routing, config APIs, and full portal UX updates. Sync system/RVW/deployment docs to capture verified upload-review-report workflow and next-step admin configuration work. Made-with: Cursor
This commit is contained in:
994
docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊SaaS版开发计划.md
Normal file
994
docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 期刊SaaS版开发计划.md
Normal file
@@ -0,0 +1,994 @@
|
||||
# RVW V4.0 智能审稿 SaaS 开发计划
|
||||
|
||||
> **文档版本:** v1.1
|
||||
> **制定日期:** 2026-03-14
|
||||
> **最后更新:** 2026-03-14(架构审查报告修正 × 6)
|
||||
> **产品版本:** RVW V4.0 (Journal AI Review SaaS)
|
||||
> **当前基线:** RVW V3.0.2(方法学分治并行 + 20检查点 + 展示收敛)
|
||||
> **目标状态:** 多租户期刊 SaaS 平台,支持路径路由隔离、SSO统一账号、ADMIN配置中枢、Handlebars报告渲染、退修信 SSE 流式生成
|
||||
>
|
||||
> **⚠️ v1.1 架构前提变更(来自架构审查报告):**
|
||||
> MVP 阶段放弃子域名方案(个人证书不支持泛域名),改用 **URL 路径隔离**:`review.xunzhengyixue.com/jtim`。10 本期刊以内路径方案完全够用,超过 10 本后再迁移子域名架构。
|
||||
|
||||
---
|
||||
|
||||
## 一、升级背景与核心目标
|
||||
|
||||
### 1.1 为什么要升级
|
||||
|
||||
当前 V3.0.2 是面向"内部临床研究人员"的单租户审稿工具,已具备强大的 4 维审查能力(稿约规范性 / 方法学 / 数据验证 / 临床评估)。
|
||||
|
||||
**痛点:**
|
||||
1. **品牌混杂**:不同期刊编辑(JTIM、CMJ等)登录的是同一套通用界面,无法体现期刊个性
|
||||
2. **标准无弹性**:每个期刊有自己独特的稿约要求,但系统只有一套固定规则
|
||||
3. **缺乏完整工作流**:没有"人工复核确认 → 勾选缺陷 → 导出退修信"的编辑侧工作闭环
|
||||
|
||||
### 1.2 核心目标(PRD 定义的 4 个目标)
|
||||
|
||||
| 目标 | 说明 |
|
||||
|------|------|
|
||||
| **多租户隔离** | 每个期刊作为独立租户,拥有独立子域名(`jtim.review.xunzhengyixue.com`)、独立品牌登录页、数据硬隔离 |
|
||||
| **完整期刊工作流** | 作者投递预审 → 责编AI审查 → 人工复核勾选 → 导出内部报告/退修信 |
|
||||
| **ADMIN 配置中枢** | 不另建后台,直接扩展现有 ADMIN 端的 TenantDetailPage,实现"千刊千面"配置 |
|
||||
| **SSO 统一通行证** | 底层复用 `platform_schema.users`,同一账号可在主站与各期刊子域名间无缝漫游 |
|
||||
|
||||
### 1.3 MVP 策略
|
||||
|
||||
**Managed SaaS(代运营/配置)模式**:复杂配置由内部运营团队通过 ADMIN 端完成,期刊客户只需"开箱即用"业务端。这意味着 Phase 1-2 是内部工具建设,Phase 3 才是对外交付。
|
||||
|
||||
---
|
||||
|
||||
## 二、整体架构变更一览
|
||||
|
||||
```
|
||||
V3.0.2 现状 (单租户) V4.0 目标 (多租户 SaaS) — 路径隔离方案
|
||||
──────────────────── ──────────────────────────────────────
|
||||
单一登录页 review.xunzhengyixue.com/jtim (JTIM 专属)
|
||||
↓ review.xunzhengyixue.com/cmj (CMJ 专属)
|
||||
单一 ReviewTask 表 review.xunzhengyixue.com/... (更多期刊)
|
||||
↓ ↓
|
||||
固定 Skills 配置 前端路由提取 :tenantId → x-tenant-id Header
|
||||
↓ ↓
|
||||
固定 Prompt 内容 JWT Payload 注入 tenantId + 中间件双重校验
|
||||
↓ ↓
|
||||
固定 Word 导出格式 tenant_rvw_configs 表 (per-tenant 配置)
|
||||
↓
|
||||
SkillExecutor 按租户动态装配
|
||||
├── 静态系统协议 (RVW_xxx_SYSTEM_BASE)
|
||||
└── 动态业务层 (tenant_rvw_configs)
|
||||
↓
|
||||
Handlebars 渲染 expert_report_markdown
|
||||
(ADMIN 端支持模板实时预览)
|
||||
↓
|
||||
双轨导出: 内部审稿报告 + 作者退修信 (SSE 流式)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、三阶段里程碑
|
||||
|
||||
```
|
||||
Phase 1 (1周) Phase 2 (1.5周) Phase 3 (2周)
|
||||
多租户数据基础 ADMIN配置中枢 期刊业务端重构
|
||||
────────────── ───────────────── ──────────────────────
|
||||
DB Schema 扩展 → TenantDetailPage扩展 → 动态品牌登录页
|
||||
JWT 注入 tenantId RVW Config CRUD API 稿件管理池 Dashboard
|
||||
子域名路由检测 Skills 引擎按租户装配 沉浸式 4-Tab 审稿详情
|
||||
CORS 动态白名单 Handlebars 渲染引擎 人工复核 (HitL) 确认
|
||||
DevOps SSL证书 Zod Schema 校验 退修信一键生成
|
||||
Prompt 动静分离落地 双轨导出 (报告+退修信)
|
||||
```
|
||||
|
||||
**总工期预估:4~4.5 周**
|
||||
|
||||
---
|
||||
|
||||
## 四、Phase 1:多租户数据隔离与账号体系(预计 1 周)
|
||||
|
||||
### 4.1 数据库改造
|
||||
|
||||
> ⚠️ **【审查报告修正 P0】历史数据迁移两步走**(来自架构审查报告第 1 条):
|
||||
> 生产环境中已存在大量 V3.0 历史审稿任务。直接添加 `tenantId` 非空外键约束,Prisma Migrate 会因历史数据为空而报错中断。
|
||||
> **必须严格按以下两步执行:**
|
||||
> 1. 先在 `platform_schema.tenants` 中初始化"默认临床主站租户"(如 `tenant-default-yanjiu`)
|
||||
> 2. 在 Prisma migration SQL 中,先 `ALTER TABLE ADD COLUMN tenant_id TEXT NULL`,再 `UPDATE review_tasks SET tenant_id = 'tenant-default-yanjiu' WHERE tenant_id IS NULL`,最后 `ALTER TABLE ALTER COLUMN tenant_id SET NOT NULL`
|
||||
|
||||
**任务 1.1:新增 `tenant_rvw_configs` 扩展表**
|
||||
|
||||
```sql
|
||||
-- Prisma Schema 新增
|
||||
model TenantRvwConfig {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @unique
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
|
||||
-- 稿约规范评估配置
|
||||
editorialRules Json? -- JSONB: 规则数组 + fatal 标记
|
||||
|
||||
-- 方法学评估配置
|
||||
methodologyExpertPrompt String? -- Text: 专家的业务评判标准
|
||||
methodologyHandlebarsTemplate String? -- Text: 报告渲染模板
|
||||
|
||||
-- 数据验证配置
|
||||
dataForensicsLevel String @default("L2") -- L1/L2/L3
|
||||
|
||||
-- 临床评估配置
|
||||
finerWeights Json? -- JSONB: {feasibility, innovation, ethics, relevance, novelty}
|
||||
clinicalExpertPrompt String? -- Text: 临床评估补充要求
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@schema("platform_schema")
|
||||
}
|
||||
```
|
||||
|
||||
**执行方式:** `prisma migrate dev --name add_tenant_rvw_configs`
|
||||
|
||||
**任务 1.2:`review_tasks` 表添加 `tenantId` 字段**
|
||||
|
||||
当前 `review_tasks` 表未关联租户。需要:
|
||||
```sql
|
||||
ALTER TABLE rvw_schema.review_tasks ADD COLUMN tenant_id TEXT REFERENCES platform_schema.tenants(id);
|
||||
CREATE INDEX idx_review_tasks_tenant_id ON rvw_schema.review_tasks(tenant_id);
|
||||
```
|
||||
|
||||
执行方式:`prisma migrate dev --name add_tenant_id_to_review_tasks`
|
||||
|
||||
---
|
||||
|
||||
### 4.2 SSO 鉴权体系升级
|
||||
|
||||
**任务 1.3:JWT 身份信息(⚠️ 架构修正:JWT 不含 tenantId)**
|
||||
|
||||
> ⚠️ **【Sprint 审查修正 #1】JWT 中不应写入 tenantId**
|
||||
> 若 JWT 中写死 `tenantId: 'jtim'`,由于主站与期刊门户共用同一个 `localStorage`,用户在新标签页打开 `review.../cmj` 时,系统会携带含 `jtim` 的 Token,造成跨租户漫游验证失败。
|
||||
>
|
||||
> **正确的 SSO 统一通行证架构:**
|
||||
> - JWT Payload 中**仅包含** `userId`(个人身份,不含业务 tenantId)
|
||||
> - 中间件(任务 1.4)完全通过 `x-tenant-id` Header 获取租户上下文
|
||||
> - 后端实时查询 `tenant_members` 表核验 `userId` 是否属于该租户
|
||||
> - 优势:同一个 Token 可以在多期刊间无缝漫游,零冲突
|
||||
|
||||
修改 `backend/src/modules/auth/auth.service.ts`:
|
||||
- JWT Payload 格式:`{ userId, role, iat, exp }`(移除 tenantId 字段)
|
||||
- 登录接口本身不感知 tenantId(登录是个人行为,与租户无关)
|
||||
|
||||
**任务 1.4:中间件双重校验防线**
|
||||
|
||||
修改 `backend/src/modules/auth/auth.middleware.ts`:
|
||||
|
||||
> ⚠️ **【审查报告修正 P1-2】URL Slug → tenantId 映射**
|
||||
> 前端传来的 `x-tenant-id: jtim` 是 `tenants.code` 字段(业务缩写),而 Prisma 查询需要 `tenants.id`。
|
||||
> 查阅现有 Schema 确认:`tenants` 表已有 `code String @unique` 字段,`id` 为手动维护的字符串主键。
|
||||
>
|
||||
> **中间件处理逻辑(必须严格遵循):**
|
||||
> 1. 从 Header 中提取 `tenantSlug = request.headers['x-tenant-id']`(即 `code` 值,如 `'jtim'`)
|
||||
> 2. 查询 `tenant = await prisma.tenants.findUnique({ where: { code: tenantSlug } })`
|
||||
> **用 Redis 或内存 LRU 缓存此查询**,TTL 5 分钟,避免每次请求都打 DB
|
||||
> 3. 若查不到 → 返回 404(租户不存在)
|
||||
> 4. 将 `tenant.id` 挂载到 `request.tenantId`,将完整 `tenant` 对象挂载到 `request.tenant`
|
||||
> 5. **跨域校验**:校验该 `userId` 是否为 `tenant_members` 表中该租户的成员
|
||||
> 若不是 → 拒绝 403 Forbidden(内部运营人员持 `ops:user-ops` 权限除外)
|
||||
|
||||
**任务 1.5:Prisma 全局查询守护**
|
||||
|
||||
在 RVW 模块所有 Prisma 查询中强制附带 `where: { tenantId: request.tenantId }`,杜绝"裸查"。
|
||||
|
||||
---
|
||||
|
||||
### 4.3 前端路径路由(已调整为路径隔离方案)
|
||||
|
||||
> ⚠️ **【审查报告修正 P2】tenantId 提取方式变更**(来自架构审查报告第 6 条):
|
||||
> 路径方案下,后端无法从 `Host` 或 `Origin` 隐式推断租户,必须由前端显式携带 `x-tenant-id` Header。
|
||||
|
||||
**任务 1.6:前端 Tenant 识别逻辑**
|
||||
|
||||
修改 `frontend-v2/src/App.tsx`(或路由初始化):
|
||||
```typescript
|
||||
// MVP 路径隔离方案:review.xunzhengyixue.com/:tenantSlug
|
||||
const pathname = window.location.pathname;
|
||||
const match = pathname.match(/^\/([a-zA-Z0-9-]+)/);
|
||||
const tenantSlug = (match && match[1] !== 'api') ? match[1] : 'default';
|
||||
// 存入 Zustand store(tenantSlug 字段)
|
||||
useTenantStore.setState({ tenantSlug });
|
||||
```
|
||||
|
||||
> ⚠️ **【审查报告修正 P2-3】严禁使用 `axiosInstance.defaults.headers` 全局赋值**
|
||||
> SPA 中 Axios 实例是全局单例,用 `defaults.headers` 写死后,用户从 `/jtim` 返回主站 `/` 或切换至 `/cmj` 时,旧 Header 不会自动清除,导致"在主站发出了携带 JTIM Header 的请求"状态污染。
|
||||
> **必须使用请求拦截器动态读取:**
|
||||
|
||||
```typescript
|
||||
// 在 axiosInstance 初始化时注册一次,此后永远动态读取当前租户
|
||||
axiosInstance.interceptors.request.use(config => {
|
||||
const { tenantSlug } = useTenantStore.getState();
|
||||
if (tenantSlug && tenantSlug !== 'default') {
|
||||
config.headers['x-tenant-id'] = tenantSlug;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
**任务 1.6b:Zustand 租户 Store + 路由观察者 + RouteGuard 修正(✅ 已完成)**
|
||||
|
||||
> **解决的三个问题(对应 Sprint 审查 #3 React 状态陷阱):**
|
||||
> 1. SPA 内部跳转(`<Link>` 导航)不刷新页面,一次性初始化代码不重新执行
|
||||
> 2. Axios `defaults.headers` 全局写死导致租户状态污染
|
||||
> 3. RouteGuard 将主平台业务路径(`/rvw`、`/ai-qa`)误判为租户 slug
|
||||
|
||||
**新增文件:**
|
||||
|
||||
`frontend-v2/src/framework/tenant/useTenantStore.ts` — Zustand 全局租户 store:
|
||||
```typescript
|
||||
export const useTenantStore = create<TenantState>((set) => ({
|
||||
tenantSlug: null,
|
||||
setTenantSlug: (slug) => set({ tenantSlug: slug }),
|
||||
}))
|
||||
```
|
||||
|
||||
`frontend-v2/src/framework/tenant/useTenantObserver.ts` — 路由感知 hook(使用 `useLocation()`):
|
||||
```typescript
|
||||
// RESERVED_PATHS = 静态保留词 + MODULES 中所有注册路径首段(自动维护)
|
||||
export function useTenantObserver() {
|
||||
const location = useLocation()
|
||||
const setTenantSlug = useTenantStore(s => s.setTenantSlug)
|
||||
useEffect(() => {
|
||||
setTenantSlug(extractTenantSlug(location.pathname))
|
||||
}, [location.pathname, setTenantSlug])
|
||||
}
|
||||
```
|
||||
|
||||
**修改文件 `App.tsx`:**
|
||||
- 在 `<BrowserRouter>` 内挂载 `<TenantBootstrap />`(调用 `useTenantObserver()`)
|
||||
- Axios 拦截器改为实时读取 store,防止状态污染:
|
||||
```typescript
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const { tenantSlug } = useTenantStore.getState()
|
||||
if (tenantSlug) config.headers['x-tenant-id'] = tenantSlug
|
||||
return config
|
||||
})
|
||||
```
|
||||
|
||||
**修改文件 `RouteGuard.tsx`:**
|
||||
- 复用 `extractTenantSlug`(自动排除所有注册模块路径,防止 `/rvw` 误判)
|
||||
- 未登录跳转对齐 App.tsx 现有路由 `/t/:tenantCode/login`:
|
||||
```typescript
|
||||
// /jtim/dashboard 未登录 → /t/jtim/login?redirect=%2Fjtim%2Fdashboard
|
||||
// /rvw/tasks 未登录 → /login(行为不变)
|
||||
function buildLoginRedirect(pathname: string): string {
|
||||
const tenantSlug = extractTenantSlug(pathname)
|
||||
if (!tenantSlug) return '/login'
|
||||
return `/t/${tenantSlug}/login?redirect=${encodeURIComponent(pathname)}`
|
||||
}
|
||||
```
|
||||
|
||||
**任务 1.6b:路由守卫租户感知重定向(✅ 已完成)**
|
||||
|
||||
> **问题根因**:`RouteGuard.tsx` 原来写死 `<Navigate to="/login" />`,访问 `/jtim/*` 未登录时会跳到 `/login`,丢失 `jtim` 标识,登录页无法加载期刊品牌 UI。
|
||||
|
||||
已修改 `frontend-v2/src/framework/router/RouteGuard.tsx`:
|
||||
|
||||
```typescript
|
||||
// 新增两个辅助函数:
|
||||
|
||||
/** 不属于期刊租户的保留路径前缀 */
|
||||
const RESERVED_PATHS = new Set(['login', 'admin', 'org', 'api', ''])
|
||||
|
||||
/** 提取第一段路径为 tenantSlug,保留字返回 null */
|
||||
function extractTenantSlug(pathname: string): string | null {
|
||||
const firstSegment = pathname.split('/').filter(Boolean)[0] ?? ''
|
||||
return RESERVED_PATHS.has(firstSegment) ? null : firstSegment
|
||||
}
|
||||
|
||||
/** 构造携带租户标识的登录跳转目标 */
|
||||
function buildLoginRedirect(pathname: string): string {
|
||||
const tenantSlug = extractTenantSlug(pathname)
|
||||
if (!tenantSlug) return '/login' // 主站保持原有行为
|
||||
const redirectParam = encodeURIComponent(pathname)
|
||||
return `/login?tenant=${tenantSlug}&redirect=${redirectParam}`
|
||||
}
|
||||
|
||||
// 未登录跳转(原来):<Navigate to="/login" ... />
|
||||
// 未登录跳转(现在):<Navigate to="/login?tenant=jtim&redirect=%2Fjtim%2Fdashboard" ... />
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 访问 `/jtim/dashboard` 未登录 → 跳转 `/login?tenant=jtim&redirect=%2Fjtim%2Fdashboard`
|
||||
- 访问 `/login`、`/admin` 等主站路径 → 仍跳转 `/login`(行为不变)
|
||||
- 登录页可通过 `tenant` query param 加载 JTIM 品牌 UI,登录后可通过 `redirect` param 跳回原页面
|
||||
|
||||
**任务 1.7:模块动态过滤**
|
||||
|
||||
利用现有 `moduleRegistry.ts`:若当前 `tenantId` 为期刊租户,隐藏 AIA/DC/PKB 等非 RVW 模块入口。
|
||||
|
||||
---
|
||||
|
||||
### 4.4 DevOps 配置(路径方案简化版)
|
||||
|
||||
> ⚠️ **【审查报告修正 P1】前端打包 base 路径 + Nginx SPA 回退**(来自架构审查报告第 3 条):
|
||||
> 路径隔离方案下,若前端打包使用相对路径(`./`),访问 `/jtim` 时浏览器会错误请求 `/jtim/assets/...`,导致白屏。
|
||||
> 且用户在 `/jtim/dashboard` 直接刷新时,Nginx 找不到物理目录会报 404。
|
||||
> **修正:** Vite `vite.config.ts` 中设置 `base: '/'`;Nginx 必须配置 `try_files $uri $uri/ /index.html;`
|
||||
|
||||
**任务 1.8:Nginx + Vite 配置(运维 + 前端)**
|
||||
|
||||
| 项 | 内容 | 负责人 |
|
||||
|----|------|--------|
|
||||
| **DNS** | 无需泛解析,沿用现有 `review.xunzhengyixue.com` A 记录即可 | 运维 |
|
||||
| **SSL 证书** | 沿用现有 `*.xunzhengyixue.com` 证书,无需额外申购 | 运维 |
|
||||
| **Nginx 配置** | 确保 SPA 路由回退:`try_files $uri $uri/ /index.html;` | 运维 |
|
||||
| **Vite 打包** | `vite.config.ts` 中 `base: '/'`(绝对根路径,不可用相对路径)| 前端 |
|
||||
| **CORS 白名单** | 后端 Fastify CORS 配置中增加 `review.xunzhengyixue.com` | 后端 |
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Phase 1 验收标准
|
||||
|
||||
- [ ] `tenant_rvw_configs` 表成功创建并迁移
|
||||
- [ ] 历史 `review_tasks` 的 `tenant_id` 已批量刷新为默认租户 ID,无数据丢失
|
||||
- [ ] 访问 `review.xunzhengyixue.com/jtim` 可正常加载前端(不白屏、不 404)
|
||||
- [ ] 在 `/jtim/dashboard` 直接刷新,页面正常加载(Nginx SPA 回退生效)
|
||||
- [ ] 使用主站账号登录,JWT 中包含 `tenantId: 'jtim'`,Axios 自动携带 `x-tenant-id` Header
|
||||
- [ ] 用 JTIM 账号请求 CMJ 数据,后端返回 403
|
||||
- [ ] 前端侧边栏在期刊租户下隐藏非 RVW 模块
|
||||
|
||||
---
|
||||
|
||||
## 五、Phase 2:ADMIN 运营端配置中枢扩展(预计 1.5 周)
|
||||
|
||||
### 5.1 后端 API 新增
|
||||
|
||||
**任务 2.1:RVW Config CRUD 接口**
|
||||
|
||||
新文件:`backend/src/modules/admin/rvw-config/`
|
||||
- `rvwConfigController.ts`
|
||||
- `rvwConfigService.ts`
|
||||
- `rvwConfigRoutes.ts`
|
||||
|
||||
接口规范:
|
||||
```
|
||||
GET /api/admin/tenants/:id/rvw-config 获取租户审稿配置
|
||||
PUT /api/admin/tenants/:id/rvw-config 更新(UPSERT)租户审稿配置
|
||||
```
|
||||
|
||||
权限:复用现有 `ops:user-ops` 中间件,仅内部运营人员可访问。
|
||||
|
||||
---
|
||||
|
||||
### 5.2 前端 ADMIN 扩展
|
||||
|
||||
**任务 2.2:TenantDetailPage 新增"智能审稿配置" Tab**
|
||||
|
||||
修改 `frontend-v2/src/modules/admin/pages/TenantDetailPage.tsx`:
|
||||
|
||||
Tab 内包含 4 个子配置面板:
|
||||
|
||||
**Panel A — 稿约规范评估配置**
|
||||
- 规则列表(可增删改):每条规则含 `code / description / fatal(是否一票否决)`
|
||||
- 示例:`摘要字数 > 250` → fatal ✓
|
||||
|
||||
**Panel B — 方法学评估配置**
|
||||
- 文本区:业务评判标准(方法学专家语言,可自由编辑,不需懂JSON)
|
||||
- 文本区:Handlebars 报告展示模板(提供默认模板,可覆盖)
|
||||
- 变量说明:`{{system_metrics.overall_score}}`, `{{#each system_metrics.checkpoints}}` 等
|
||||
- > ⚠️ **【审查报告修正 P1】必须增加"测试渲染"预览**(来自架构审查报告第 5 条):
|
||||
> 若无预览功能,运营人员写错 Handlebars 语法后需上传真实 PDF 等待 5 分钟才能发现报错,试错成本极高。
|
||||
> **复用 ADMIN 现有的"测试渲染 (Test Render)"能力**:提供一份 Mock 好的 `system_metrics` JSON 假数据,点击"预览"按钮即可在右侧即时看到渲染效果,语法正确后再保存。
|
||||
|
||||
**Panel C — 数据验证配置**
|
||||
- 下拉选择验证深度:`L1 算术验证` / `L2 统计验证` / `L3 双通道核查`
|
||||
|
||||
**Panel D — 临床评估配置**
|
||||
- 5 个 FINER 权重输入框(总和校验 100%)
|
||||
- 文本区:专科特色补充要求
|
||||
|
||||
---
|
||||
|
||||
### 5.3 引擎改造(核心)
|
||||
|
||||
**任务 2.3:SkillExecutor 按租户动态装配**
|
||||
|
||||
修改 `backend/src/modules/rvw/skills/core/executor.ts`(或 `reviewWorker.ts`):
|
||||
- 在执行每个 Skill 前,通过 `tenantId` 查询 `TenantRvwConfig`
|
||||
- 将租户配置注入到 Skill 的执行上下文中
|
||||
|
||||
**任务 2.4:Hybrid Prompt 拼装机制**
|
||||
|
||||
修改各 Skill 的 Prompt 获取逻辑:
|
||||
|
||||
```
|
||||
最终发送给 LLM 的 Prompt =
|
||||
[静态系统协议层] ← promptService.get('RVW_METHODOLOGY_SYSTEM_BASE')
|
||||
+ [换行分隔]
|
||||
+ [动态业务层] ← tenantRvwConfig.methodologyExpertPrompt
|
||||
|
||||
其中 静态系统协议层 强制包含:
|
||||
- 必须以纯 JSON 输出(json_object 模式)
|
||||
- system_metrics 结构(含 20 项 checkpoints)
|
||||
- expert_report_markdown 字段(由LLM按专家要求自由撰写)
|
||||
```
|
||||
|
||||
**任务 2.5:LLM 调用升级 — Structured Outputs 强约束**
|
||||
|
||||
修改 `backend/src/common/llm/LLMFactory.ts`(或各 Skill 的 LLM 调用):
|
||||
- 调用 DeepSeek-V3 / GPT-4o 时,传入 `response_format: { type: "json_object" }`
|
||||
- 彻底摒弃仅靠 Prompt 文字要求 JSON 的"软性约束"
|
||||
|
||||
**任务 2.6:Handlebars 渲染引擎**
|
||||
|
||||
新文件:`backend/src/modules/rvw/services/reportRenderer.ts`
|
||||
|
||||
```typescript
|
||||
import Handlebars from 'handlebars';
|
||||
import Zod from 'zod';
|
||||
|
||||
// Zod 校验 LLM 结构化输出
|
||||
const methodologyOutputSchema = z.object({
|
||||
system_metrics: z.object({
|
||||
overall_score: z.number().min(0).max(100),
|
||||
conclusion: z.enum(['直接接收', '小修', '大修', '拒稿']),
|
||||
checkpoints: z.array(z.object({
|
||||
id: z.number(),
|
||||
item: z.string(),
|
||||
status: z.enum(['pass', 'minor_issue', 'major_issue', 'not_mentioned']),
|
||||
finding: z.string(),
|
||||
suggestion: z.string().optional()
|
||||
})).length(20)
|
||||
}),
|
||||
expert_report_markdown: z.string()
|
||||
});
|
||||
|
||||
// Handlebars 渲染(使用租户模板或默认模板)
|
||||
export function renderMethodologyReport(
|
||||
data: MethodologyLLMOutput,
|
||||
template?: string
|
||||
): string {
|
||||
const safeData = methodologyOutputSchema.parse(data); // Zod 兜底
|
||||
const compiledTemplate = Handlebars.compile(template ?? DEFAULT_METHODOLOGY_TEMPLATE);
|
||||
return compiledTemplate(safeData);
|
||||
}
|
||||
```
|
||||
|
||||
**任务 2.7:默认 Handlebars 模板(硬编码 + 可覆盖)**
|
||||
|
||||
为 4 个 Skill 各提供一套默认 Handlebars 模板(内嵌在代码中),租户可在 ADMIN 配置页覆盖:
|
||||
|
||||
```handlebars
|
||||
{{! 默认方法学报告模板 }}
|
||||
# 方法学评估报告
|
||||
|
||||
**综合评分:** {{system_metrics.overall_score}} 分 | **审稿结论:** {{system_metrics.conclusion}}
|
||||
|
||||
## 详细审查意见
|
||||
|
||||
{{expert_report_markdown}}
|
||||
|
||||
## 20 项检查点覆盖情况
|
||||
|
||||
{{#each system_metrics.checkpoints}}
|
||||
- [{{#if (eq status "pass")}}✅{{else if (eq status "minor_issue")}}🟡{{else if (eq status "major_issue")}}🔴{{else}}⬜{{/if}}] **{{id}}. {{item}}**:{{finding}}
|
||||
{{#if suggestion}}> 💡 建议:{{suggestion}}{{/if}}
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Phase 2 验收标准
|
||||
|
||||
- [ ] 内部运营人员在 `TenantDetailPage` 的"智能审稿配置"Tab 中,能为 JTIM 保存独立的方法学 Prompt 和 Handlebars 模板
|
||||
- [ ] 运行 JTIM 租户的审稿任务,后端日志显示 Prompt 为"静态协议 + JTIM 业务层"拼接版本
|
||||
- [ ] LLM 返回 `system_metrics` + `expert_report_markdown` 双字段的 JSON,不崩溃
|
||||
- [ ] Zod 校验可自动补全缺失的 `checkpoints` 字段(填入 `not_mentioned` 默认值)
|
||||
- [ ] Handlebars 渲染出最终报告文本,内容与 JTIM 模板格式一致
|
||||
|
||||
---
|
||||
|
||||
## 六、Phase 3:面向外部的期刊业务端重构(预计 2 周)
|
||||
|
||||
### 6.1 动态品牌登录页
|
||||
|
||||
**任务 3.1:公开租户信息接口**
|
||||
|
||||
新增接口(无需鉴权):
|
||||
```
|
||||
GET /api/v1/tenants/public-info/:tenantSlug
|
||||
Response: { logoUrl, brandColor, brandName, backgroundImageUrl }
|
||||
```
|
||||
|
||||
**任务 3.2:动态品牌登录页渲染**
|
||||
|
||||
> **前置条件**:任务 1.6b 已完成,未登录跳转现在携带 `?tenant=jtim&redirect=/jtim/dashboard`
|
||||
|
||||
修改现有 `frontend-v2/src/pages/LoginPage.tsx`(而非新建页面,避免重复):
|
||||
```typescript
|
||||
// 在 LoginPage 初始化时读取 tenant query param
|
||||
const [searchParams] = useSearchParams()
|
||||
const tenantSlug = searchParams.get('tenant') // e.g., 'jtim'
|
||||
const redirectPath = searchParams.get('redirect') // e.g., '/jtim/dashboard'
|
||||
|
||||
// 若 tenantSlug 非空,拉取公开配置,渲染期刊品牌
|
||||
useEffect(() => {
|
||||
if (tenantSlug) {
|
||||
fetch(`/api/v1/tenants/public-info/${tenantSlug}`)
|
||||
.then(r => r.json())
|
||||
.then(config => setBrandConfig(config))
|
||||
}
|
||||
}, [tenantSlug])
|
||||
|
||||
// 登录成功后
|
||||
onLoginSuccess(() => {
|
||||
navigate(redirectPath ?? '/')
|
||||
})
|
||||
```
|
||||
|
||||
**渲染逻辑**:
|
||||
- `tenantSlug` 有值 → 显示期刊专属品牌 UI(Logo、品牌色、背景图)
|
||||
- `tenantSlug` 为空 → 显示主站默认 UI(行为不变,向后兼容)
|
||||
|
||||
---
|
||||
|
||||
### 6.2 稿件管理池(Dashboard 重构)
|
||||
|
||||
**任务 3.3:稿件列表宽表升级**
|
||||
|
||||
修改 `frontend-v2/src/modules/rvw/pages/Dashboard.tsx`:
|
||||
|
||||
参考 `AI审稿V1.html` 原型设计:
|
||||
- **顶部统计卡片**:待预审 / AI 审查中 / 需人工复核 / 今日已退修
|
||||
- **稿件宽表**:稿件编号 / 标题 / 通讯作者 / 综合评分(环形) / 4 维状态图标 / 状态标签 / 操作按钮
|
||||
- 4 维状态图标:✅ pass / ⚠️ warn / ❌ error / ⏳ pending
|
||||
|
||||
---
|
||||
|
||||
### 6.3 沉浸式审稿详情页(核心)
|
||||
|
||||
**任务 3.4:分屏布局重构**
|
||||
|
||||
修改 `frontend-v2/src/modules/rvw/components/TaskDetail.tsx`:
|
||||
|
||||
参考原型,重构为两栏:
|
||||
- **左侧 3/5**:稿件原文预览(PDF/Word 渲染或纯文本),带浮动工具栏
|
||||
- **右侧 2/5**:4-Tab 审查工作台(稿约规范 / 方法学 / 数据验证 / 临床评估)
|
||||
|
||||
**任务 3.5:Human-in-the-Loop 问题确认**
|
||||
|
||||
修改各 Report 组件(`EditorialReport`、`MethodologyReport`等):
|
||||
|
||||
每个问题卡片底部增加复核操作:
|
||||
```
|
||||
[✓ 采纳此问题] [✗ 误报/忽略]
|
||||
```
|
||||
- 状态持久化到后端(新增 `review_task_confirmations` 字段或表)
|
||||
- 只有"已采纳"的问题才会进入退修信
|
||||
|
||||
**任务 3.6:主报告渲染升级**
|
||||
|
||||
修改 `MethodologyReport.tsx` 等:
|
||||
- 优先展示 `expert_report_markdown`(Handlebars 渲染后的纯文本/Markdown)
|
||||
- 不再依赖组件内部"拼接 JSON 字段展示"的旧逻辑
|
||||
|
||||
---
|
||||
|
||||
### 6.4 用户-租户绑定 SOP(无需新开发,但必须列入上线 Checklist)
|
||||
|
||||
> ⚠️ **【审查报告修正 P1】业务闭环缺失的关键补丁**(来自架构审查报告第 4 条):
|
||||
> 系统已有 `platform_schema.tenant_members` 表和用户管理 API,但计划中未说明"谁把责编拉进特定期刊租户"。
|
||||
> 此项**无需新开发**,但必须写入交付 SOP:
|
||||
> **上线 Checklist(必须完成):**
|
||||
> 1. 内部超管通过 ADMIN 端的【用户管理 → 租户成员管理】,将期刊责编账号添加至对应租户(如 JTIM)
|
||||
> 2. 赋予该用户 RVW 模块权限
|
||||
> 3. 验证责编登录 `review.xunzhengyixue.com/jtim` 后能看到该期刊的稿件列表(不为空、不报 403)
|
||||
|
||||
### 6.5 退修信生成(重点新功能)
|
||||
|
||||
**任务 3.7:退修信生成后端接口(SSE 流式方案)**
|
||||
|
||||
> ⚠️ **【审查报告修正 P0】退修信生成必须使用 SSE 流式,不可同步阻塞**(来自架构审查报告第 2 条):
|
||||
> 退修信涉及 LLM 生成长文本,耗时极易超过 SAE 网关 60 秒限制。
|
||||
> 系统已有 SSE 基础设施(ASL Deep Research V2.0 已验证),直接复用。
|
||||
>
|
||||
> ⚙️ **【Sprint 审查修正 #2】运维必须调整 ALB 空闲超时时间**
|
||||
> 阿里云 ALB 默认"连接空闲超时时间"为 **15 秒**。LLM 在生成退修信时若有思考停顿,超过 15 秒未推流,ALB 会强制切断 TCP 连接,前端报 SSE 中断。
|
||||
> **运维执行项**:进入 ALB 控制台 → 监听(443 端口)→ 高级配置 → 将"连接空闲超时时间"调大至 **300 秒**。
|
||||
|
||||
接口设计(两步):
|
||||
|
||||
```
|
||||
# 第一步:SSE 流式生成(打字机效果)
|
||||
GET /api/v2/rvw/tasks/:id/revision-letter/stream
|
||||
Body via query: confirmedIssueIds=xxx,yyy
|
||||
Response: text/event-stream,实时推送 letterContent 增量文本
|
||||
|
||||
# 第二步:生成完毕后触发 Word 导出(短耗时,同步可接受)
|
||||
POST /api/v2/rvw/tasks/:id/revision-letter/export
|
||||
Body: { letterContent: string }
|
||||
Response: { wordFileUrl: string }
|
||||
```
|
||||
|
||||
逻辑:
|
||||
1. 获取已确认的问题列表(按维度分组)
|
||||
2. 调用 LLM,以 stream 模式生成退修信,通过 SSE 推流到前端
|
||||
3. 前端生成完毕后,用户确认内容,点击"导出 Word"触发第二步接口
|
||||
|
||||
**任务 3.8:前端退修信弹窗(SSE 打字机效果)**
|
||||
|
||||
> ⚠️ **【审查报告修正 P0-1】严禁使用原生 `EventSource`**
|
||||
> 浏览器原生 `EventSource` 规范层面不支持设置任何自定义 HTTP Header(无法携带 `Authorization: Bearer xxx`,也无法携带 `x-tenant-id`)。直接使用会导致后端 401 / 403。
|
||||
> **必须使用 `@microsoft/fetch-event-source` 或手写 Fetch ReadableStream:**
|
||||
|
||||
```typescript
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
|
||||
// 安装:npm install @microsoft/fetch-event-source
|
||||
await fetchEventSource(`/api/v2/rvw/tasks/${taskId}/revision-letter/stream`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'x-tenant-id': tenantSlug,
|
||||
},
|
||||
onmessage(event) {
|
||||
setLetterContent(prev => prev + event.data); // 打字机效果
|
||||
},
|
||||
onerror(err) { /* 错误处理 */ }
|
||||
});
|
||||
```
|
||||
|
||||
新建 `RevisionLetterModal.tsx`:
|
||||
- 展示已采纳问题汇总(可再次勾选/取消)
|
||||
- "生成退修信"按钮
|
||||
- **打字机式实时渲染**:使用 `@microsoft/fetch-event-source`,字符逐步出现
|
||||
- 生成完毕后显示"导出 Word"按钮(调用第二步同步接口)
|
||||
|
||||
---
|
||||
|
||||
### 6.5 双轨导出升级
|
||||
|
||||
**任务 3.9:内部审稿报告导出**
|
||||
|
||||
升级现有 Word 导出功能:
|
||||
- 使用 Handlebars 渲染的 `expert_report_markdown` 替代旧的 JSON 拼接展示
|
||||
- 保留 4 个维度的分章节结构
|
||||
|
||||
**任务 3.10:作者退修信导出**
|
||||
|
||||
退修信 Word 文档格式:
|
||||
- 标题:`Revision Required - [稿件标题]`
|
||||
- 信头:期刊名、日期
|
||||
- 正文:按维度分组的问题与建议(敬语体)
|
||||
- 结尾:编辑署名
|
||||
|
||||
---
|
||||
|
||||
### 6.6 Phase 3 验收标准
|
||||
|
||||
- [ ] 访问 `review.xunzhengyixue.com/jtim` 显示 JTIM 品牌登录页(Logo/品牌色正确)
|
||||
- [ ] 登录后进入 JTIM 专属工作台,左侧导航只显示 RVW 相关功能
|
||||
- [ ] 上传稿件后,在 Dashboard 宽表中可看到 4 维状态图标实时更新
|
||||
- [ ] 审稿完成后,点击"人工复核"进入分屏详情页
|
||||
- [ ] 在每个问题卡片上点击"采纳"/"忽略",刷新后状态保持
|
||||
- [ ] 方法学报告优先展示 Handlebars 渲染的 `expert_report_markdown`(格式与 ADMIN 配置模板一致)
|
||||
- [ ] 点击"生成退修信",退修信以打字机效果实时渲染(SSE 流式),生成完毕后可导出 Word
|
||||
- [ ] 责编账号必须已通过 ADMIN 端绑定至 JTIM 租户,否则访问时正确提示 403
|
||||
|
||||
---
|
||||
|
||||
## 七、关键技术风险与防范
|
||||
|
||||
### 风险 1:JSON 转义风暴(最高优先级)
|
||||
|
||||
**风险**:LLM 在 `expert_report_markdown` 字段中输出含换行符、引号的长文本时,JSON.parse 崩溃。
|
||||
|
||||
**防范方案:**
|
||||
1. **API 层强约束**:所有 LLM 调用启用 `response_format: { type: "json_object" }`
|
||||
2. **引入 jsonrepair**:安装 `jsonrepair` 包,替换直接使用 `JSON.parse` 的地方
|
||||
3. **Partial 容灾**:继续保持 `Promise.allSettled` + `partial_completed` 机制
|
||||
|
||||
### 风险 2:Handlebars 模板变量错位
|
||||
|
||||
**风险**:运营人员配置的模板引用了 LLM 没有输出的字段,渲染报错。
|
||||
|
||||
**防范方案:**
|
||||
1. **Zod 校验先行**:在 Handlebars 渲染前,用 Zod Schema 对 LLM 输出进行校验并填充默认值
|
||||
2. **防御性模板规范**:模板中大量使用 `{{#if field}}...{{else}}N/A{{/if}}`,并在 ADMIN 端提示
|
||||
|
||||
### 风险 3:方法学超时(已有缓解措施)
|
||||
|
||||
**风险**:20 检查点 + 长文本生成,超过 8 分钟超时。
|
||||
|
||||
**防范方案(V3.0.2 已实施 + V4.0 增强):**
|
||||
1. ✅ 分治并行(A/B/C 三段)已实施
|
||||
2. **V4.0 增强**:`expert_report_markdown` 字段让 LLM 汇总输出一份完整报告,减少结构化字段填充压力
|
||||
3. 快速模式开关(`RVW_METHODOLOGY_FAST_MODE`)规划中
|
||||
|
||||
### 风险 4:多租户数据越权(安全红线)
|
||||
|
||||
**风险**:A 期刊编辑通过修改参数访问 B 期刊稿件。
|
||||
|
||||
**防范方案:**
|
||||
1. JWT Payload 中 tenantId 不可伪造(服务端签发)
|
||||
2. auth.middleware.ts 中双重校验(userId 有效性 + tenantId 归属)
|
||||
3. 所有 RVW Prisma 查询强制附带 `where: { tenantId: request.tenantId }`
|
||||
4. 开发规范:禁止跳过 tenantId 校验的裸查
|
||||
|
||||
---
|
||||
|
||||
## 八、各阶段任务清单(按人员分工)
|
||||
|
||||
### 后端工程师
|
||||
|
||||
| Phase | 任务编号 | 任务描述 | 工期 |
|
||||
|-------|---------|---------|------|
|
||||
| Phase 1 | 1.1 | 新增 `tenant_rvw_configs` + Prisma 迁移 | 0.5d |
|
||||
| Phase 1 | 1.2 | `review_tasks` 添加 `tenantId` + Prisma 迁移 | 0.5d |
|
||||
| Phase 1 | 1.3 | JWT 注入 tenantId | 1d |
|
||||
| Phase 1 | 1.4 | 中间件双重校验 | 1d |
|
||||
| Phase 1 | 1.5 | Prisma 查询全局守护 | 1d |
|
||||
| Phase 2 | 2.1 | RVW Config CRUD API | 1d |
|
||||
| Phase 2 | 2.3 | SkillExecutor 按租户动态装配 | 1d |
|
||||
| Phase 2 | 2.4 | Hybrid Prompt 拼装机制 | 1.5d |
|
||||
| Phase 2 | 2.5 | LLM Structured Outputs 强约束 | 0.5d |
|
||||
| Phase 2 | 2.6 | Handlebars 渲染引擎 + Zod 校验 | 1.5d |
|
||||
| Phase 2 | 2.7 | 4 个 Skill 默认 Handlebars 模板 | 1d |
|
||||
| Phase 3 | 3.1 | 公开租户信息接口 | 0.5d |
|
||||
| Phase 3 | 3.7 | 退修信生成接口 | 1.5d |
|
||||
| Phase 3 | 3.9 | 内部报告导出升级 | 1d |
|
||||
| Phase 3 | 3.10 | 退修信 Word 导出 | 1d |
|
||||
|
||||
### 前端工程师
|
||||
|
||||
| Phase | 任务编号 | 任务描述 | 工期 |
|
||||
|-------|---------|---------|------|
|
||||
| Phase 1 | 1.6 | Tenant 识别逻辑 + 全局 Header 注入 | 1d |
|
||||
| Phase 1 | 1.7 | 模块动态过滤 | 0.5d |
|
||||
| Phase 2 | 2.2 | TenantDetailPage 新增"智能审稿配置"Tab(4个Panel) | 3d |
|
||||
| Phase 3 | 3.2 | 动态品牌登录页 | 1.5d |
|
||||
| Phase 3 | 3.3 | Dashboard 稿件宽表升级 | 2d |
|
||||
| Phase 3 | 3.4 | 分屏布局审稿详情页 | 2d |
|
||||
| Phase 3 | 3.5 | Human-in-the-Loop 问题确认 | 1.5d |
|
||||
| Phase 3 | 3.6 | 主报告展示优先 expert_report_markdown | 1d |
|
||||
| Phase 3 | 3.8 | 退修信生成弹窗 | 1.5d |
|
||||
|
||||
### 运维工程师
|
||||
|
||||
| Phase | 任务编号 | 任务描述 | 工期 |
|
||||
|-------|---------|---------|------|
|
||||
| Phase 1 | 1.8a | 阿里云 DNS 泛解析 `*.review` 记录 | 0.5d |
|
||||
| Phase 1 | 1.8b | 申购 `*.review.xunzhengyixue.com` SSL 证书并挂载 | 1d |
|
||||
| Phase 1 | 1.8c | Nginx 配置子域名转发规则 | 0.5d |
|
||||
| Phase 1 | 1.8d | 后端 CORS 动态白名单配置 | 0.5d |
|
||||
|
||||
---
|
||||
|
||||
## 九、开发规范对齐要求
|
||||
|
||||
### 9.1 数据库规范(必须遵守)
|
||||
|
||||
> 对应:`docs/04-开发规范/09-数据库开发规范.md` 三条铁律
|
||||
|
||||
- **必须** 使用 `npx prisma migrate dev --name xxx` 生成迁移文件,**禁止** `prisma db push`
|
||||
- 每次 Prisma Schema 变更后,**立即** 在 `docs/05-部署文档/03-待部署变更清单.md` 追加变更记录
|
||||
- 本次 V4.0 涉及的迁移文件清单(需逐一追加):
|
||||
|
||||
| 迁移名 | 内容 | Schema |
|
||||
|--------|------|--------|
|
||||
| `add_tenant_rvw_configs` | 新增 `tenant_rvw_configs` 表 | `platform_schema` |
|
||||
| `add_tenant_id_to_review_tasks` | 历史数据平滑迁移(两步走)| `rvw_schema` |
|
||||
|
||||
### 9.2 OSS 存储规范(必须遵守)
|
||||
|
||||
> 对应:`docs/04-开发规范/11-OSS存储开发规范.md`
|
||||
|
||||
- 稿件文件上传/读取:**必须** 通过 `common/storage/` 统一接口,禁止前端直连 OSS
|
||||
- > ☁️ **【Sprint 审查修正 #4】OSS Bucket 必须配置 CORS 跨域规则**
|
||||
> 前端通过签名 URL 直连 OSS 下载/预览 PDF 时,浏览器会发起 OPTIONS 预检请求。若 Bucket 未配置 CORS,前端直接报 CORS Error 白屏。
|
||||
> **运维执行项**:阿里云 OSS 控制台 → 对应 Bucket → 权限管理 → 跨域设置,新增规则:
|
||||
> - 允许来源:`https://review.xunzhengyixue.com`
|
||||
> - 允许方法:`GET, PUT, POST, HEAD`
|
||||
> - 允许 Headers:`*`
|
||||
> - 暴露 Headers:`ETag, x-oss-request-id`
|
||||
- 退修信 Word 导出:文件存储 Key 遵循规范路径格式:
|
||||
|
||||
```
|
||||
rvw/{tenantCode}/{taskId}/manuscripts/{uuid}.docx # 原稿
|
||||
rvw/{tenantCode}/{taskId}/reports/{uuid}.docx # 审稿报告
|
||||
rvw/{tenantCode}/{taskId}/revision-letters/{uuid}.docx # 退修信
|
||||
```
|
||||
|
||||
- 报告/退修信下载链接:**必须** 使用带过期时间的签名 URL(Signed URL),禁止公开直链
|
||||
- 原始文件名(如 "张三投稿.docx")存数据库,OSS Key 使用 UUID
|
||||
|
||||
### 9.3 认证授权规范(必须遵守)
|
||||
|
||||
> 对应:`docs/04-开发规范/10-模块认证规范.md`
|
||||
|
||||
**后端路由层**(所有新增 API 必须):
|
||||
```typescript
|
||||
// RVW Config API:运营管理员才能访问
|
||||
fastify.put('/api/admin/tenants/:id/rvw-config', {
|
||||
preHandler: [authenticate, requirePermission('ops:user-ops')]
|
||||
}, handler);
|
||||
|
||||
// RVW 业务 API:普通用户 + 模块权限
|
||||
fastify.get('/api/v2/rvw/tasks', {
|
||||
preHandler: [authenticate, requireModule('RVW')]
|
||||
}, handler);
|
||||
```
|
||||
|
||||
**前端层**:
|
||||
- 普通业务 API:使用 `common/api/axios.ts` 的 `apiClient`(已内置 JWT)
|
||||
- SSE(退修信生成):使用 `@microsoft/fetch-event-source` + 手动从 `getAccessToken()` 获取 Token
|
||||
|
||||
**Controller 层**:
|
||||
```typescript
|
||||
// 必须从 request 提取,禁止硬编码
|
||||
const userId = getUserId(request);
|
||||
const tenantId = request.tenantId; // 由中间件翻译后挂载
|
||||
```
|
||||
|
||||
### 9.4 安全规范(必须遵守)
|
||||
|
||||
> 对应:`docs/04-开发规范/12-安全开发规范.md`
|
||||
|
||||
**IDOR 防护**(P0 级强制):
|
||||
```typescript
|
||||
// ✅ 所有 RVW 查询必须带 tenantId
|
||||
const task = await prisma.reviewTasks.findFirst({
|
||||
where: { id: taskId, tenantId: request.tenantId }
|
||||
});
|
||||
|
||||
// ❌ 禁止裸查
|
||||
const task = await prisma.reviewTasks.findUnique({ where: { id: taskId } });
|
||||
```
|
||||
|
||||
**公开 API 限流**(防爬):
|
||||
- `GET /api/v1/tenants/public-info/:tenantSlug`(无需鉴权的品牌配置接口)**必须** 加 Rate Limiting(建议 30次/分钟/IP)
|
||||
|
||||
**Handlebars 模板注入防护**:
|
||||
- 运营人员配置的 Handlebars 模板在服务端渲染时,**必须** 使用 `Handlebars.create()` 独立沙箱实例
|
||||
- 禁用危险 helpers(`{{raw}}` 等),防止模板注入攻击
|
||||
- 渲染时捕获所有异常,失败降级返回 `expert_report_markdown` 原始内容
|
||||
|
||||
### 9.5 与 ADMIN 模块融合要点
|
||||
|
||||
> 对应:`docs/03-业务模块/ADMIN-运营管理端/00-模块当前状态与开发指南.md`
|
||||
|
||||
| V4.0 需求 | ADMIN 现有能力 | 融合方式 |
|
||||
|-----------|--------------|---------|
|
||||
| Handlebars 模板编辑 + 预览 | Phase 3.5.4 `PromptEditorPage` 已有 Test Render | 在 RVW Config Tab 中复用该组件 |
|
||||
| 方法学业务 Prompt 配置 | Phase 3.5.1 `PromptService` + CodeMirror 编辑器 | 复用 `PromptEditor` 组件 |
|
||||
| 用户-租户绑定 | Phase 4.1 用户管理 + `tenant_members` 表 | 走已有流程,纳入上线 SOP |
|
||||
| 期刊品牌定制(Logo/主题色) | ADMIN P2 待开发:`品牌定制配置` + `租户专属登录页` | **主动对齐**:V4.0 中的动态登录页实现即为该 P2 项落地,需同步更新 ADMIN 开发状态文档 |
|
||||
| 运营数据统计 | Phase 5.0 `ActivityService` 埋点 | 新增退修信生成、配置变更等埋点,使用现有 fire-and-forget 模式 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、不在本次 V4.0 范围内(明确排除)
|
||||
|
||||
以下功能在本次 V4.0 中**不开发**,待后续版本(V4.1+)规划:
|
||||
|
||||
1. **投稿作者侧门户**:作者直接上传投稿、查看审稿意见(本次只做责编侧)
|
||||
2. **期刊主编仪表盘**:全刊数据统计(本次只做责编工作台)
|
||||
3. **外审专家模块**:分配外审、专家接收稿件(超出本次范围)
|
||||
4. **配置中心自助访问**:期刊客户自行修改配置(MVP 阶段由内部运营代劳)
|
||||
5. **微信登录**:认证网关方案规划中,MVP 阶段账密登录
|
||||
6. **PDF 渲染左栏**:Phase 3 的左侧稿件预览可先用纯文本展示,真正的 PDF 渲染后续迭代
|
||||
|
||||
---
|
||||
|
||||
## 十、全局技术依赖与选型
|
||||
|
||||
### 10.1 新增依赖(确需安装)
|
||||
|
||||
| 包 | 用途 | 安装命令 |
|
||||
|----|------|---------|
|
||||
| `@microsoft/fetch-event-source` | 支持自定义 Header 的 SSE 客户端 | `npm install @microsoft/fetch-event-source`(前端) |
|
||||
| `jsonrepair` | LLM JSON 容错解析 | `npm install jsonrepair`(后端) |
|
||||
| `zod` | LLM 输出 Schema 校验 | `npm install zod`(后端,若未安装) |
|
||||
|
||||
### 10.2 复用现有能力(禁止重复造轮子)
|
||||
|
||||
> ⚠️ 以下能力系统中已存在,**直接调用,严禁重新实现**
|
||||
|
||||
| 能力 | 现有位置 | RVW V4.0 使用场景 |
|
||||
|------|---------|-----------------|
|
||||
| **SSE 流式响应** | `backend/src/common/streaming/` `createStreamingService` | 退修信生成后端推流(替代自建 SSE) |
|
||||
| **缓存服务** | `backend/src/common/cache/` | 租户 slug → UUID 查询缓存(TTL 5min) |
|
||||
| **Prompt 管理 + Handlebars** | `backend/src/common/prompt/` `PromptService` | Handlebars 报告模板渲染(PromptService 已支持,无需新建 `reportRenderer.ts`) |
|
||||
| **LLM 网关** | `backend/src/common/llm/LLMFactory` | 所有 LLM 调用 |
|
||||
| **认证中间件** | `backend/src/common/auth/auth.middleware.ts` `authenticate` | 所有新增 RVW API 路由 |
|
||||
| **模块中间件** | `backend/src/common/auth/auth.middleware.ts` `requireModule` | RVW Config API 必须加 `requireModule('RVW')` |
|
||||
| **Axios 实例(含认证)** | `frontend-v2/src/common/api/axios.ts` | 所有前端 API 调用(含自动 JWT 注入) |
|
||||
| **Token 获取** | `frontend-v2/src/framework/auth/api.ts` `getAccessToken()` | SSE 手动鉴权(`@microsoft/fetch-event-source` 中使用) |
|
||||
| **运营埋点** | `backend/src/common/logging/ActivityService` | 退修信生成、配置变更等新行为需埋点 |
|
||||
| **存储服务** | `backend/src/common/storage/` | 稿件上传、报告/退修信 Word 导出存储 |
|
||||
| **Prompt 测试渲染** | ADMIN `PromptEditorPage` 已有 Test Render 功能(Phase 3.5.4) | Handlebars 模板预览直接复用 |
|
||||
| **租户详情页** | `frontend-v2/src/pages/admin/tenants/TenantDetailPage.tsx` | 新增"智能审稿配置"Tab,在此文件扩展 |
|
||||
| **用户-租户绑定** | ADMIN 用户管理(Phase 4.1 已完成)| 新期刊用户绑定,走已有流程,无需开发 |
|
||||
| **品牌定制能力** | ADMIN `tenants` 表 `config JSON` 字段 + `P2 - 品牌定制` 待开发项 | 动态登录页的 Logo/主题色配置,与 ADMIN 品牌定制对齐,避免冲突 |
|
||||
|
||||
### 10.3 LLM 调用规范
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| Structured Output | 所有新增 LLM 调用启用 `response_format: { type: "json_object" }` |
|
||||
| 通过 `LLMFactory` | 禁止直接 new SDK 客户端 |
|
||||
| maxTokens | 各 Section 分治调用使用 reduced budget(2048),防超时 |
|
||||
|
||||
---
|
||||
|
||||
## 十二、文档与设计参考
|
||||
|
||||
| 文档 | 路径 |
|
||||
|------|------|
|
||||
| PRD | `docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/AI智能审稿系统(期刊SaaS版)产品需求文档0314.md` |
|
||||
| UI 原型 | `docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/AI审稿V1.html` |
|
||||
| 技术风险指南 | `docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/核心技术难点与防范指南.md` |
|
||||
| 域名与多租户指南 | `docs/03-业务模块/RVW-稿件审查系统/00-系统设计/V3.0/AI智能审稿域名与多租户技术指南.md` |
|
||||
| 架构白皮书 | `docs/03-业务模块/RVW-稿件审查系统/08-技术架构建议/智能审稿终极稳定架构白皮书.md` |
|
||||
| 当前模块状态 | `docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md` |
|
||||
| 输出解耦计划 | `docs/03-业务模块/RVW-稿件审查系统/04-开发计划/RVW V4.0 智能审稿输出解耦开发计划.md` |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 十三、架构审查修正汇总(v1.0 → v1.3)
|
||||
|
||||
**第一轮审查修正(v1.0 → v1.1)**
|
||||
|
||||
| # | 等级 | 修正内容 | 影响章节 |
|
||||
|---|------|---------|---------|
|
||||
| 1 | 🔴 P0 | 历史 `review_tasks` 平滑迁移两步走脚本 | 第四章 4.1 |
|
||||
| 2 | 🔴 P0 | 退修信生成改为 SSE 流式,拆分为两步接口 | 第六章 6.5 |
|
||||
| 3 | 🟡 P1 | Vite `base: '/'` + Nginx `try_files` 明确要求 | 第四章 4.4 |
|
||||
| 4 | 🟡 P1 | 用户-租户绑定列入交付 SOP(无需新开发)| 第六章 6.4 |
|
||||
| 5 | 🟡 P1 | Handlebars 编辑器增加"测试渲染"预览按钮 | 第五章 5.2 |
|
||||
| 6 | 🟢 P2 | 前端携带 `x-tenant-id` Header + 后端从 Header 提取 | 第四章 4.3 |
|
||||
| - | 架构前提 | 域名方案从子域名改为路径隔离 `review.xunzhengyixue.com/:tenantId` | 全文 |
|
||||
|
||||
**第二轮审查修正(v1.1 → v1.2)**
|
||||
|
||||
| # | 等级 | 修正内容 | 影响章节 |
|
||||
|---|------|---------|---------|
|
||||
| 7 | 🔴 P0 | SSE 接口严禁使用原生 `EventSource`,必须用 `@microsoft/fetch-event-source` | 第六章 6.5 |
|
||||
| 8 | 🟡 P1 | URL Slug(`jtim`)→ `tenants.code` → 中间件翻译为 `tenants.id`,加 Redis 缓存 | 第四章 4.2 |
|
||||
| 9 | 🟢 P2 | Axios 全局 Header 改为请求拦截器动态读取,防状态污染 | 第四章 4.3 |
|
||||
|
||||
**第三轮规范对齐补丁(v1.2 → v1.3,已完成实现)**
|
||||
|
||||
| # | 类型 | 补丁内容 | 来源 |
|
||||
|---|------|---------|------|
|
||||
| A | 🔄 复用 | 退修信 SSE 后端用 `createStreamingService`,不自建 | 通用能力层:`common/streaming/` |
|
||||
| B | 🔄 复用 | 租户 slug 缓存用 `common/cache/` 服务,不自建 Redis 代码 | 通用能力层:`common/cache/` |
|
||||
| C | 🔄 复用 | Handlebars 渲染直接扩展 `PromptService`,不新建 `reportRenderer.ts` | 通用能力层:`common/prompt/` |
|
||||
| D | 🔄 复用 | 前端业务 API 用 `common/api/axios.ts`,SSE 用 `getAccessToken()` | 认证规范 §2 |
|
||||
| E | 🔄 复用 | Handlebars 预览复用 ADMIN `PromptEditorPage` Test Render | ADMIN Phase 3.5.4 |
|
||||
| F | 📏 规范 | 所有新增后端路由加 `authenticate` + `requireModule('RVW')` | 认证规范 §3 |
|
||||
| G | 📏 规范 | 所有新增 Prisma 查询必须带 `tenantId`,防 IDOR | 安全规范 §1 |
|
||||
| H | 📏 规范 | OSS 路径遵循 `rvw/{tenantCode}/{taskId}/xxx/` 格式,报告用签名 URL | OSS 规范 §2-3 |
|
||||
| I | 📏 规范 | DB 每次迁移更新 `docs/05-部署文档/03-待部署变更清单.md` | 数据库规范铁律 |
|
||||
| J | 📏 规范 | 公开接口加 Rate Limiting,Handlebars 渲染加沙箱防注入 | 安全规范 §1/§6 |
|
||||
| K | 🤝 融合 | 动态登录页实现 = ADMIN P2 `品牌定制配置` + `租户专属登录页` 落地,对齐两端 | ADMIN 待开发 P2 |
|
||||
|
||||
**第四轮:运行时 Bug 修复(v1.3 → v1.4,✅ 已完成实现)**
|
||||
|
||||
| # | 文件 | 修复内容 |
|
||||
|---|------|---------|
|
||||
| L | `RouteGuard.tsx` | 租户感知重定向:未登录时从 URL 提取 `tenantSlug`,跳转 `/t/jtim/login?redirect=/jtim/dashboard`,保留期刊上下文;主站路径行为不变 |
|
||||
|
||||
**第五轮:Sprint 冲刺会架构细节修正(v1.4 → v1.5,✅ 已完成实现)**
|
||||
|
||||
| # | 类型 | 内容 | 影响 |
|
||||
|---|------|------|------|
|
||||
| M | 🏗️ 架构修正 | JWT 不含 `tenantId`,后端实时通过 `x-tenant-id` Header + `tenant_members` 核验 | Task 1.3 |
|
||||
| N | ✅ 已实现 | `useTenantStore` + `useTenantObserver` + `TenantBootstrap` 全链路,解决 SPA 状态陷阱 | Task 1.6b |
|
||||
| O | ✅ 已实现 | `RouteGuard` 动态排除 MODULES 路径,对齐 `/t/:tenantCode/login` 路由 | Task 1.6b |
|
||||
| P | ⚙️ 运维 | ALB 空闲超时从 15s 调至 300s(SSE 退修信生成防切断)| Task 3.7 |
|
||||
| Q | ⚙️ 运维 | OSS Bucket 新增 CORS 规则,允许 `review.xunzhengyixue.com` | Task 9.2 |
|
||||
|
||||
---
|
||||
|
||||
**文档版本:** v1.5
|
||||
**制定日期:** 2026-03-14
|
||||
**最后更新:** 2026-03-14(第五轮:Sprint 冲刺会 4 条细节修正 + useTenantObserver 全链路已实现)
|
||||
**下一步行动:** Phase 1 启动 — DB 迁移脚本(含历史数据平滑迁移)+ 运维执行 ALB 超时 + OSS CORS 配置 + `@microsoft/fetch-event-source` 安装 + `/t/:tenantCode/login` 路由渲染期刊品牌 UI
|
||||
Reference in New Issue
Block a user