Files
AIclinicalresearch/docs/04-开发规范/10-模块认证规范.md
HaHafeng 8f06d4f929 feat(asl): Complete Deep Research V2.0 core development
Backend:
- Add SSE streaming client (unifuncsSseClient) replacing async polling
- Add paragraph-based reasoning parser with mergeConsecutiveThinking
- Add requirement expansion service (DeepSeek-V3 PICOS+MeSH)
- Add Word export service with Pandoc, inline hyperlinks, reference link expansion
- Add deep research V2 worker with 2s log flush and Chinese source prompt
- Add 5 curated data sources config (PubMed/ClinicalTrials/Cochrane/CNKI/MedJournals)
- Add 4 API endpoints (generate-requirement/tasks/task-status/export-word)
- Update Prisma schema with 6 new V2.0 fields on AslResearchTask
- Add DB migration for V2.0 fields
- Simplify ASL_DEEP_RESEARCH_EXPANSION prompt (remove strategy section)

Frontend:
- Add waterfall-flow DeepResearchPage (phase 0-4 progressive reveal)
- Add LandingView, SetupPanel, StrategyConfirm, AgentTerminal, ResultsView
- Add react-markdown + remark-gfm for report rendering
- Add custom link component showing visible URLs after references
- Add useDeepResearchTask polling hook
- Add deep research TypeScript types

Tests:
- Add E2E test, smoke test, and Chinese data source test scripts

Docs:
- Update ASL module status (v2.0 - core features complete)
- Update system status (v6.1 - ASL V2.0 milestone)
- Update Unifuncs DeepSearch API guide (v2.0 - SSE mode + Chinese source results)
- Update module auth specification (test script guidelines)
- Update V2.0 development plan

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 13:21:52 +08:00

10 KiB
Raw Blame History

模块认证规范

本文档定义了业务模块如何正确使用平台认证能力,确保所有 API 都正确携带和验证用户身份。

1. 架构概览

┌─────────────────────────────────────────────────────────────┐
│                        前端                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  common/api/axios.ts  ← 带认证的 axios 实例         │   │
│  │  framework/auth/api.ts ← Token 管理 (getAccessToken)│   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ Authorization: Bearer <token>
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        后端                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  common/auth/auth.middleware.ts                      │   │
│  │  - authenticate: 验证 JWT Token                     │   │
│  │  - requirePermission: 权限检查                       │   │
│  │  - requireRoles: 角色检查                            │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2. 前端规范

2.1 使用带认证的 axios 实例(推荐)

// 导入带认证的 apiClient
import apiClient from '../../../common/api/axios';

// 使用方式与 axios 完全相同,自动携带 JWT Token
const response = await apiClient.get('/api/v2/xxx');
const response = await apiClient.post('/api/v2/xxx', data);

2.2 使用原生 fetch需手动添加 Token

import { getAccessToken } from '../../../framework/auth/api';

// 创建 getAuthHeaders 函数
function getAuthHeaders(): HeadersInit {
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
  };
  const token = getAccessToken();
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  return headers;
}

// 所有 fetch 请求使用 getAuthHeaders()
const response = await fetch(url, {
  headers: getAuthHeaders(),
});

// 文件上传(不设置 Content-Type
const token = getAccessToken();
const headers: HeadersInit = {};
if (token) {
  headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(url, {
  method: 'POST',
  headers,
  body: formData,
});

3. 后端规范

3.1 路由添加认证中间件

// 导入认证中间件
import { authenticate, requirePermission } from '../../../common/auth/auth.middleware.js';

// 添加到路由
fastify.get('/xxx', { preHandler: [authenticate] }, handler);

// 需要特定权限
fastify.post('/xxx', { 
  preHandler: [authenticate, requirePermission('module:action')] 
}, handler);

3.2 控制器获取用户 ID

/**
 * 获取用户ID从JWT Token中获取
 */
function getUserId(request: FastifyRequest): string {
  const userId = (request as any).user?.userId;
  if (!userId) {
    throw new Error('User not authenticated');
  }
  return userId;
}

// 在控制器方法中使用
async function myHandler(request: FastifyRequest, reply: FastifyReply) {
  const userId = getUserId(request);
  // ... 使用 userId
}

3.3 JWT Token 结构

interface DecodedToken {
  userId: string;      // 用户ID
  phone: string;       // 手机号
  role: string;        // 角色
  tenantId: string;    // 租户ID
  tenantCode?: string; // 租户Code
  iat: number;         // 签发时间
  exp: number;         // 过期时间
}

4. 检查清单

4.1 新模块开发检查清单

  • 前端 API 文件

    • 使用 apiClient 或添加 getAuthHeaders()
    • 文件上传单独处理(不设置 Content-Type
    • 导出函数不包含测试用 userId 参数
  • 后端路由文件

    • 导入 authenticate 中间件
    • 所有需要认证的路由添加 preHandler: [authenticate]
    • 公开 API如模板列表可不添加认证
  • 后端控制器文件

    • 添加 getUserId() 辅助函数
    • 移除所有 MOCK_USER_ID 或硬编码默认值
    • 使用 getUserId(request) 获取用户 ID

4.2 已完成模块状态

模块 前端 API 后端路由 后端控制器 状态
RVW apiClient authenticate getUserId
PKB 拦截器 authenticate getUserId
ASL getAuthHeaders authenticate getUserId
DC Tool B getAuthHeaders authenticate getUserId
DC Tool C apiClient authenticate getUserId
IIT N/A (企业微信) N/A 企业微信userId
Prompt管理 getAuthHeaders authenticate getUserId

5. 测试脚本认证规范

编写 API 测试脚本时,需要先通过登录接口获取 Token再携带到后续请求中。

5.1 登录接口

POST /api/v1/auth/login/password
Content-Type: application/json

{
  "phone": "13800000001",
  "password": "123456"
}

5.2 登录响应结构

{
  "success": true,
  "data": {
    "user": { "id": "...", "name": "...", "role": "SUPER_ADMIN" },
    "tokens": {
      "accessToken": "eyJhbGciOiJIUzI1NiIs...",
      "refreshToken": "..."
    }
  }
}

Token 提取路径res.data.data.tokens.accessToken

5.3 测试脚本通用模板

const BASE_URL = process.env.TEST_BASE_URL || 'http://localhost:3001';
const API_PREFIX = `${BASE_URL}/api/v1`;

const TEST_PHONE = process.env.TEST_PHONE || '13800000001';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456';

let authToken = '';

async function api(path: string, options: RequestInit = {}) {
  const res = await fetch(`${API_PREFIX}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
      ...(options.headers || {}),
    },
  });
  const contentType = res.headers.get('content-type') || '';
  const data = contentType.includes('json') ? await res.json() : await res.text();
  return { status: res.status, data };
}

// 登录获取 Token
async function login() {
  const res = await api('/auth/login/password', {
    method: 'POST',
    body: JSON.stringify({ phone: TEST_PHONE, password: TEST_PASSWORD }),
  });
  if (res.status !== 200 || !res.data.success) {
    throw new Error(`登录失败: ${JSON.stringify(res.data)}`);
  }
  authToken = res.data.data.tokens.accessToken;
}

5.4 测试账号说明

手机号 密码 角色 说明
13800000001 123456 SUPER_ADMIN 超级管理员,可访问所有模块
13800000000 123456 USER 普通测试用户

注意SUPER_ADMINPROMPT_ENGINEER 角色在 requireModule() 中间件中自动豁免模块权限检查,无需在 user_modules 表中分配模块。普通 USER 角色需要先在 platform_schema.user_modules 中分配对应模块才能访问。

5.5 运行测试脚本

# 前置条件:后端服务运行中 + PostgreSQL 运行中
cd backend

# 方式一:使用默认测试账号
npx tsx src/modules/xxx/__tests__/test-script.ts

# 方式二:通过环境变量指定账号
$env:TEST_PHONE="13800000001"; $env:TEST_PASSWORD="123456"; npx tsx src/modules/xxx/__tests__/test-script.ts

5.6 用户表说明

用户数据存储在 platform_schema.users 表中(不是 public.users。Prisma schema 中的 User model 映射到此表。关键字段:

字段 说明
phone 登录手机号(唯一)
password bcrypt 哈希密码
role 角色SUPER_ADMIN / HOSPITAL_ADMIN / DEPARTMENT_ADMIN / PROMPT_ENGINEER / USER 等
tenant_id 租户 ID
status active / disabled

6. 常见错误和解决方案

6.1 401 Unauthorized

原因: 前端没有携带 JWT Token 或 Token 过期

解决:

  1. 检查前端 API 是否使用 apiClientgetAuthHeaders()
  2. 检查 localStorage 中是否有 accessToken
  3. 如果 Token 过期,尝试刷新或重新登录

6.2 User not authenticated

原因: 后端路由没有添加 authenticate 中间件

解决: 在路由定义中添加 preHandler: [authenticate]

6.3 TypeError: Cannot read property 'userId' of undefined

原因: 使用了错误的属性名(request.user.id 而非 request.user.userId

解决: 使用 (request as any).user?.userId

6.4 测试脚本 Token 为 NULL

原因: 登录返回的 Token 路径不对

解决: Token 位于 res.data.data.tokens.accessToken,不是 res.data.data.accessTokenres.data.data.token

6.5 测试脚本登录成功但 API 返回 401

排查步骤:

  1. 确认 Token 提取路径:res.data.data.tokens.accessToken
  2. 确认 Authorization header 格式:Bearer <token>(注意空格)
  3. 确认后端服务使用的 JWT_SECRET 与签发时一致

7. 参考文件

  • 前端 axios 实例: frontend-v2/src/common/api/axios.ts
  • 前端 Token 管理: frontend-v2/src/framework/auth/api.ts
  • 后端认证中间件: backend/src/common/auth/auth.middleware.ts
  • 后端 JWT 服务: backend/src/common/auth/jwt.service.ts
  • 测试脚本示例: backend/src/modules/asl/__tests__/deep-research-v2-smoke.ts