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:
99
backend/scripts/test-data/01_seed_jtim_tenant.sql
Normal file
99
backend/scripts/test-data/01_seed_jtim_tenant.sql
Normal file
@@ -0,0 +1,99 @@
|
||||
-- =============================================================
|
||||
-- RVW V4.0 本地测试数据:创建 jtim 期刊租户及测试账号
|
||||
-- 执行方式:docker cp 此文件 到容器后 psql -f 执行
|
||||
-- =============================================================
|
||||
|
||||
-- 1. 创建 jtim 期刊租户
|
||||
INSERT INTO platform_schema.tenants (id, name, code, type, status, created_at, updated_at)
|
||||
VALUES (
|
||||
'tenant-jtim-001',
|
||||
'中华内科杂志',
|
||||
'jtim',
|
||||
'HOSPITAL', -- 暂用 HOSPITAL 类型(期刊类型未来可扩展枚举值 JOURNAL)
|
||||
'ACTIVE',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 创建 jtim 专属测试用户(密码 bcrypt hash = "Test@1234")
|
||||
-- 使用 bcrypt hash: $2b$10$hashed_value... 实际用已存在用户代替
|
||||
-- 注意:users.tenant_id 是主租户(用于主站),tenant_members 表决定跨租户访问权限
|
||||
INSERT INTO platform_schema.users (
|
||||
id, phone, password, email, name, tenant_id,
|
||||
role, status, is_default_password, is_trial, kb_quota, kb_used, token_version,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'user-jtim-reviewer-001',
|
||||
'13900139001',
|
||||
'$2a$10$OzlvWo.N5FKeU9BgBVuZ1e1DPonG7bF/WVO6PVW5d5di.fK7oxUnW', -- Test@1234
|
||||
'jtim-reviewer@test.com',
|
||||
'张审稿人',
|
||||
'tenant-jtim-001', -- 主租户 = jtim
|
||||
'USER',
|
||||
'active',
|
||||
false,
|
||||
false,
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. 绑定用户到 jtim 租户(tenant_members 表 - 用于 rvwTenantMiddleware 权限校验)
|
||||
INSERT INTO platform_schema.tenant_members (id, tenant_id, user_id, role, joined_at)
|
||||
VALUES (
|
||||
'member-jtim-reviewer-001',
|
||||
'tenant-jtim-001',
|
||||
'user-jtim-reviewer-001',
|
||||
'USER',
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
-- 4. 赋予 RVW 模块权限(user_modules 表)
|
||||
INSERT INTO platform_schema.user_modules (id, user_id, tenant_id, module_code, is_enabled, created_at, updated_at)
|
||||
VALUES (
|
||||
'umod-jtim-rvw-001',
|
||||
'user-jtim-reviewer-001',
|
||||
'tenant-jtim-001',
|
||||
'RVW',
|
||||
true,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (user_id, tenant_id, module_code) DO NOTHING;
|
||||
|
||||
-- 5. 创建 jtim 租户的智能审稿配置(使用系统默认,可后续在 ADMIN 修改)
|
||||
INSERT INTO platform_schema.tenant_rvw_configs (
|
||||
id, tenant_id, data_forensics_level,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'rvwcfg-jtim-001',
|
||||
'tenant-jtim-001',
|
||||
'L2',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 6. 验证数据
|
||||
SELECT
|
||||
t.id AS tenant_id,
|
||||
t.name AS tenant_name,
|
||||
t.code AS tenant_code,
|
||||
u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
u.phone,
|
||||
u.email,
|
||||
tm.role AS member_role,
|
||||
rc.data_forensics_level
|
||||
FROM platform_schema.tenants t
|
||||
JOIN platform_schema.users u ON u.tenant_id = t.id
|
||||
JOIN platform_schema.tenant_members tm ON tm.tenant_id = t.id AND tm.user_id = u.id
|
||||
LEFT JOIN platform_schema.tenant_rvw_configs rc ON rc.tenant_id = t.id
|
||||
WHERE t.code = 'jtim';
|
||||
161
backend/scripts/test-data/02_test_rvw_v4_api.ps1
Normal file
161
backend/scripts/test-data/02_test_rvw_v4_api.ps1
Normal file
@@ -0,0 +1,161 @@
|
||||
# =============================================================
|
||||
# RVW V4.0 多租户 API 测试脚本(PowerShell)
|
||||
# 测试目标:验证 x-tenant-id 多租户隔离全链路
|
||||
# 运行前提:仅需启动后端(npm run dev),无需启动前端
|
||||
# 运行方式:在项目根目录执行:.\backend\scripts\test-data\02_test_rvw_v4_api.ps1
|
||||
# =============================================================
|
||||
|
||||
$BASE_URL = "http://localhost:3001"
|
||||
$TENANT_SLUG = "jtim"
|
||||
$TEST_USER_PHONE = "13900139001"
|
||||
$TEST_USER_PASSWORD = "Test@1234"
|
||||
|
||||
# 输出工具函数
|
||||
function Print-Step($step, $msg) { Write-Host "`n[$step] $msg" -ForegroundColor Cyan }
|
||||
function Print-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
|
||||
function Print-FAIL($msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red }
|
||||
function Print-INFO($msg) { Write-Host " [INFO] $msg" -ForegroundColor Yellow }
|
||||
|
||||
# =====================================================================
|
||||
# STEP 0:检查后端是否已启动
|
||||
# =====================================================================
|
||||
Print-Step "0" "检查后端服务可用性 $BASE_URL/health"
|
||||
try {
|
||||
$health = Invoke-RestMethod -Uri "$BASE_URL/health" -Method GET -ErrorAction Stop
|
||||
Print-OK "后端健康检查通过: $($health | ConvertTo-Json -Compress)"
|
||||
} catch {
|
||||
Print-FAIL "后端未启动或不可达!请先运行:cd backend && npm run dev"
|
||||
Print-INFO "错误:$($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# STEP 1:登录获取 JWT Token
|
||||
# =====================================================================
|
||||
Print-Step "1" "登录测试账号 (phone: $TEST_USER_PHONE)"
|
||||
$loginBody = @{
|
||||
phone = $TEST_USER_PHONE
|
||||
password = $TEST_USER_PASSWORD
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$loginResp = Invoke-RestMethod -Uri "$BASE_URL/api/v1/auth/login/password" `
|
||||
-Method POST `
|
||||
-ContentType "application/json" `
|
||||
-Body $loginBody `
|
||||
-ErrorAction Stop
|
||||
|
||||
# 响应结构:data.tokens.accessToken
|
||||
if ($loginResp.success -and $loginResp.data.tokens.accessToken) {
|
||||
$TOKEN = $loginResp.data.tokens.accessToken
|
||||
$USER_ID = $loginResp.data.user.id
|
||||
Print-OK "登录成功!userId=$USER_ID tenantCode=$($loginResp.data.user.tenantCode)"
|
||||
Print-INFO "Token (前50字符): $($TOKEN.Substring(0, [Math]::Min(50, $TOKEN.Length)))..."
|
||||
} else {
|
||||
Print-FAIL "登录响应异常: $($loginResp | ConvertTo-Json)"
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Print-FAIL "登录失败:$($_.Exception.Message)"
|
||||
$errorBody = $_.ErrorDetails.Message
|
||||
if ($errorBody) { Print-INFO "响应体:$errorBody" }
|
||||
exit 1
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# STEP 2:不带 x-tenant-id,获取任务列表(主站单租户模式,应正常返回)
|
||||
# =====================================================================
|
||||
Print-Step "2" "不带 x-tenant-id Header - 获取任务列表(主站模式)"
|
||||
try {
|
||||
$listNoTenant = Invoke-RestMethod -Uri "$BASE_URL/api/v1/rvw/tasks" `
|
||||
-Method GET `
|
||||
-Headers @{ "Authorization" = "Bearer $TOKEN" } `
|
||||
-ErrorAction Stop
|
||||
|
||||
$total1 = if ($listNoTenant.data.total) { $listNoTenant.data.total } else { 0 }
|
||||
Print-OK "主站模式任务列表获取成功,共 $total1 条"
|
||||
} catch {
|
||||
$status = $_.Exception.Response.StatusCode.value__
|
||||
Print-FAIL "请求失败(HTTP $status):$($_.ErrorDetails.Message)"
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# STEP 3:携带 x-tenant-id: jtim,获取任务列表(期刊租户模式)
|
||||
# =====================================================================
|
||||
Print-Step "3" "携带 x-tenant-id: $TENANT_SLUG - 获取任务列表(期刊租户模式)"
|
||||
try {
|
||||
$listWithTenant = Invoke-RestMethod -Uri "$BASE_URL/api/v1/rvw/tasks" `
|
||||
-Method GET `
|
||||
-Headers @{
|
||||
"Authorization" = "Bearer $TOKEN"
|
||||
"x-tenant-id" = $TENANT_SLUG
|
||||
} `
|
||||
-ErrorAction Stop
|
||||
|
||||
$total2 = if ($listWithTenant.data.total) { $listWithTenant.data.total } else { 0 }
|
||||
Print-OK "期刊租户模式任务列表获取成功,共 $total2 条"
|
||||
} catch {
|
||||
$status = $_.Exception.Response.StatusCode.value__
|
||||
$body = $_.ErrorDetails.Message
|
||||
Print-FAIL "请求失败(HTTP $status):$body"
|
||||
if ($status -eq 401) { Print-INFO "401 说明 rvwTenantMiddleware 拦截成功(用户不在该租户,或中间件未注册)" }
|
||||
if ($status -eq 403) { Print-INFO "403 说明用户无 RVW 模块权限" }
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# STEP 4:携带不存在的租户 slug,应返回 404 或 401
|
||||
# =====================================================================
|
||||
Print-Step "4" "携带无效 x-tenant-id: nonexistent-journal - 应被拦截"
|
||||
try {
|
||||
$listBadTenant = Invoke-RestMethod -Uri "$BASE_URL/api/v1/rvw/tasks" `
|
||||
-Method GET `
|
||||
-Headers @{
|
||||
"Authorization" = "Bearer $TOKEN"
|
||||
"x-tenant-id" = "nonexistent-journal"
|
||||
} `
|
||||
-ErrorAction Stop
|
||||
|
||||
Print-FAIL "期望被拦截但未拦截!响应:$($listBadTenant | ConvertTo-Json -Compress)"
|
||||
} catch {
|
||||
$status = $_.Exception.Response.StatusCode.value__
|
||||
if ($status -eq 404 -or $status -eq 401 -or $status -eq 403) {
|
||||
Print-OK "已正确拦截无效租户!HTTP $status(符合预期)"
|
||||
} else {
|
||||
Print-FAIL "拦截状态码异常:HTTP $status,期望 401/403/404"
|
||||
}
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# STEP 5:用另一个用户 Token 访问 jtim 租户(非成员),应被拦截
|
||||
# =====================================================================
|
||||
Print-Step "5" "用其他用户(非 jtim 成员)访问 jtim 租户 - 应返回 401/403"
|
||||
Print-INFO "此步骤需手动提供另一个用户的 Token,跳过自动化测试"
|
||||
|
||||
# =====================================================================
|
||||
# STEP 6:查询 DB 验证本地数据一致性
|
||||
# =====================================================================
|
||||
Print-Step "6" "数据库验证 - 查询 jtim 租户配置"
|
||||
$sqlCheck = "SELECT t.code, rc.data_forensics_level, tm.role FROM platform_schema.tenants t JOIN platform_schema.tenant_rvw_configs rc ON rc.tenant_id = t.id JOIN platform_schema.tenant_members tm ON tm.tenant_id = t.id WHERE t.code = 'jtim';"
|
||||
$dbResult = docker exec -i ai-clinical-postgres psql -U postgres -d ai_clinical_research -c $sqlCheck 2>&1
|
||||
Print-INFO "DB 查询结果:`n$dbResult"
|
||||
|
||||
# =====================================================================
|
||||
# 测试总结
|
||||
# =====================================================================
|
||||
Write-Host "`n" -NoNewline
|
||||
Write-Host "======================================================" -ForegroundColor Magenta
|
||||
Write-Host " RVW V4.0 多租户 API 测试完成" -ForegroundColor Magenta
|
||||
Write-Host "======================================================" -ForegroundColor Magenta
|
||||
Write-Host ""
|
||||
Write-Host "测试账号信息(用于前端手动验证):" -ForegroundColor White
|
||||
Write-Host " 手机号:$TEST_USER_PHONE"
|
||||
Write-Host " 密码: $TEST_USER_PASSWORD"
|
||||
Write-Host " 租户: $TENANT_SLUG → http://localhost:5173/$TENANT_SLUG"
|
||||
Write-Host ""
|
||||
Write-Host "全流程前端测试步骤:" -ForegroundColor White
|
||||
Write-Host " 1. 启动后端:cd backend && npm run dev"
|
||||
Write-Host " 2. 启动前端:cd frontend-v2 && npm run dev"
|
||||
Write-Host " 3. 浏览器访问:http://localhost:5173/$TENANT_SLUG"
|
||||
Write-Host " 4. 期望重定向到:http://localhost:5173/t/$TENANT_SLUG/login"
|
||||
Write-Host " 5. 用上方账号登录,登录后应进入期刊审稿页面"
|
||||
Write-Host ""
|
||||
10
backend/scripts/test-data/03_fix_jtim_user_password.sql
Normal file
10
backend/scripts/test-data/03_fix_jtim_user_password.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- 修复 jtim 测试用户密码(Test@1234 的 bcrypt $2a$10$ hash)
|
||||
UPDATE platform_schema.users
|
||||
SET password = '$2a$10$OzlvWo.N5FKeU9BgBVuZ1e1DPonG7bF/WVO6PVW5d5di.fK7oxUnW',
|
||||
is_default_password = false
|
||||
WHERE id = 'user-jtim-reviewer-001';
|
||||
|
||||
-- 验证
|
||||
SELECT id, name, phone, LEFT(password, 7) AS hash_prefix, is_default_password
|
||||
FROM platform_schema.users
|
||||
WHERE id = 'user-jtim-reviewer-001';
|
||||
Reference in New Issue
Block a user