Deliver the RVW V4.0 journal configuration center across backend, frontend, migration, and docs with zh/en editorial baseline support and tenant-level prompt/template overrides. Unify tenant login to /:tenantCode/login and auto-enable RVW module when tenant type is JOURNAL to prevent post-login access gaps. Made-with: Cursor
354 lines
8.2 KiB
TypeScript
354 lines
8.2 KiB
TypeScript
/**
|
||
* 租户管理 API
|
||
*/
|
||
|
||
import { getAccessToken } from '../../../../framework/auth/api';
|
||
|
||
const API_BASE = '/api/admin/tenants';
|
||
const MODULES_API = '/api/admin/modules';
|
||
|
||
function getAuthHeaders(): HeadersInit {
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
};
|
||
const token = getAccessToken();
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
return headers;
|
||
}
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
export type TenantType = 'HOSPITAL' | 'PHARMA' | 'JOURNAL' | 'INTERNAL' | 'PUBLIC';
|
||
export type TenantStatus = 'ACTIVE' | 'SUSPENDED' | 'EXPIRED';
|
||
export type JournalLanguage = 'ZH' | 'EN' | 'OTHER';
|
||
|
||
export interface TenantInfo {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
type: TenantType;
|
||
journalLanguage?: JournalLanguage | null;
|
||
journalFullName?: string | null;
|
||
logoUrl?: string | null;
|
||
brandColor?: string | null;
|
||
loginBackgroundUrl?: string | null;
|
||
status: TenantStatus;
|
||
contactName?: string | null;
|
||
contactPhone?: string | null;
|
||
contactEmail?: string | null;
|
||
expiresAt?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface TenantModuleConfig {
|
||
code: string;
|
||
name: string;
|
||
enabled: boolean;
|
||
expiresAt?: string | null;
|
||
}
|
||
|
||
export interface TenantDetail extends TenantInfo {
|
||
modules: TenantModuleConfig[];
|
||
userCount: number;
|
||
}
|
||
|
||
export interface ModuleInfo {
|
||
code: string;
|
||
name: string;
|
||
description?: string | null;
|
||
icon?: string | null;
|
||
}
|
||
|
||
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;
|
||
expiresAt?: string;
|
||
modules?: string[];
|
||
}
|
||
|
||
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;
|
||
expiresAt?: string | null;
|
||
}
|
||
|
||
export interface TenantListResponse {
|
||
success: boolean;
|
||
data: TenantInfo[];
|
||
total: number;
|
||
page: number;
|
||
limit: number;
|
||
totalPages: number;
|
||
}
|
||
|
||
// ==================== API 函数 ====================
|
||
|
||
/**
|
||
* 获取租户列表
|
||
*/
|
||
export async function fetchTenantList(params?: {
|
||
type?: TenantType;
|
||
status?: TenantStatus;
|
||
search?: string;
|
||
page?: number;
|
||
limit?: number;
|
||
}): Promise<TenantListResponse> {
|
||
const searchParams = new URLSearchParams();
|
||
if (params?.type) searchParams.set('type', params.type);
|
||
if (params?.status) searchParams.set('status', params.status);
|
||
if (params?.search) searchParams.set('search', params.search);
|
||
if (params?.page) searchParams.set('page', String(params.page));
|
||
if (params?.limit) searchParams.set('limit', String(params.limit));
|
||
|
||
const url = `${API_BASE}?${searchParams.toString()}`;
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: getAuthHeaders(),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '获取租户列表失败');
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
/**
|
||
* 获取租户详情
|
||
*/
|
||
export async function fetchTenantDetail(id: string): Promise<TenantDetail> {
|
||
const response = await fetch(`${API_BASE}/${id}`, {
|
||
method: 'GET',
|
||
headers: getAuthHeaders(),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '获取租户详情失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
/**
|
||
* 创建租户
|
||
*/
|
||
export async function createTenant(data: CreateTenantRequest): Promise<TenantInfo> {
|
||
const response = await fetch(API_BASE, {
|
||
method: 'POST',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '创建租户失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
/**
|
||
* 更新租户信息
|
||
*/
|
||
export async function updateTenant(id: string, data: UpdateTenantRequest): Promise<TenantInfo> {
|
||
const response = await fetch(`${API_BASE}/${id}`, {
|
||
method: 'PUT',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '更新租户失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
/**
|
||
* 更新租户状态
|
||
*/
|
||
export async function updateTenantStatus(id: string, status: TenantStatus): Promise<void> {
|
||
const response = await fetch(`${API_BASE}/${id}/status`, {
|
||
method: 'PUT',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify({ status }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '更新租户状态失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除租户
|
||
*/
|
||
export async function deleteTenant(id: string): Promise<void> {
|
||
const response = await fetch(`${API_BASE}/${id}`, {
|
||
method: 'DELETE',
|
||
headers: getAuthHeaders(),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '删除租户失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 配置租户模块
|
||
*/
|
||
export async function configureModules(
|
||
tenantId: string,
|
||
modules: { code: string; enabled: boolean; expiresAt?: string | null }[]
|
||
): Promise<TenantModuleConfig[]> {
|
||
const response = await fetch(`${API_BASE}/${tenantId}/modules`, {
|
||
method: 'PUT',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify({ modules }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '配置租户模块失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
/**
|
||
* 获取所有可用模块列表
|
||
*/
|
||
export async function fetchModuleList(): Promise<ModuleInfo[]> {
|
||
const response = await fetch(MODULES_API, {
|
||
method: 'GET',
|
||
headers: getAuthHeaders(),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '获取模块列表失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
// ==================== RVW Config API ====================
|
||
|
||
/** 租户审稿配置 */
|
||
export interface TenantRvwConfig {
|
||
id: string;
|
||
tenantId: string;
|
||
editorialBaseStandard: 'zh' | 'en';
|
||
editorialExpertPrompt: string | null;
|
||
editorialHandlebarsTemplate: string | null;
|
||
methodologyExpertPrompt: string | null;
|
||
methodologyHandlebarsTemplate: string | null;
|
||
dataForensicsExpertPrompt: string | null;
|
||
dataForensicsHandlebarsTemplate: string | null;
|
||
clinicalExpertPrompt: string | null;
|
||
clinicalHandlebarsTemplate: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** 更新审稿配置请求 */
|
||
export interface UpdateRvwConfigRequest {
|
||
editorialBaseStandard?: 'zh' | 'en';
|
||
editorialExpertPrompt?: string | null;
|
||
editorialHandlebarsTemplate?: string | null;
|
||
methodologyExpertPrompt?: string | null;
|
||
methodologyHandlebarsTemplate?: string | null;
|
||
dataForensicsExpertPrompt?: string | null;
|
||
dataForensicsHandlebarsTemplate?: string | null;
|
||
clinicalExpertPrompt?: string | null;
|
||
clinicalHandlebarsTemplate?: string | null;
|
||
}
|
||
|
||
/**
|
||
* 获取租户智能审稿配置
|
||
*/
|
||
export async function fetchRvwConfig(tenantId: string): Promise<TenantRvwConfig | null> {
|
||
const response = await fetch(`${API_BASE}/${tenantId}/rvw-config`, {
|
||
headers: getAuthHeaders(),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '获取审稿配置失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
/**
|
||
* 保存(UPSERT)租户智能审稿配置
|
||
*/
|
||
export async function saveRvwConfig(tenantId: string, data: UpdateRvwConfigRequest): Promise<TenantRvwConfig> {
|
||
const response = await fetch(`${API_BASE}/${tenantId}/rvw-config`, {
|
||
method: 'PUT',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.message || '保存审稿配置失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.data;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|