8bac14ef44
根因:预测准确度卡的偏差来自历史回填项目的预测vs实际,属既成事实, 不会因校准改变;且原 apply 公式 next=current+dev 会累加,反复点越推越高。 修复: - 校准建议/应用均基于未校准原始基准(env/默认)计算,保证幂等 - GET /api/calibration 返回 uncalibratedBase 与 calibrated 标志 - 卡片:已校准时显示「已按当前偏差校准:基准X%」绿色状态,不再出现按钮; 未校准时按钮明示「X% → Y%」 - 补充说明:历史偏差不会因校准改变,校准仅调整后续承接建议阈值
1080 lines
35 KiB
TypeScript
1080 lines
35 KiB
TypeScript
/**
|
||
* HTTP API 客户端:封装 fetch 调用后端 REST API(Req 16.2)。
|
||
*
|
||
* 所有 API 返回 Promise<T>,错误时抛出 ApiError(含 status 与 message)。
|
||
*/
|
||
|
||
/**
|
||
* 后端 API 基址。
|
||
* - 开发模式:默认 http://localhost:3005(Vite 代理或直连本地后端)。
|
||
* - 生产模式:默认空串 → 走相对路径 /api/...,由 nginx 同源反代到后端,避免硬编码域名/端口。
|
||
* - 可用环境变量 VITE_API_BASE 覆盖。
|
||
*/
|
||
export const API_BASE =
|
||
(import.meta.env.VITE_API_BASE as string | undefined) ??
|
||
(import.meta.env.DEV ? 'http://localhost:3005' : '');
|
||
|
||
/** 读取本地保存的鉴权令牌(生产 RBAC);演示模式下为空,请求不带令牌。 */
|
||
function authHeader(): Record<string, string> {
|
||
try {
|
||
const t = localStorage.getItem('risk-agent-token');
|
||
return t !== null && t !== '' ? { Authorization: `Bearer ${t}` } : {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/** 当前登录用户 ID(用于在记录中以 ID 关联操作人;演示模式下作为 body 兜底)。 */
|
||
export function currentUserId(): string | undefined {
|
||
try {
|
||
return localStorage.getItem('risk-agent-uid') ?? undefined;
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
/** API 调用错误。 */
|
||
export class ApiError extends Error {
|
||
constructor(
|
||
readonly status: number,
|
||
message: string,
|
||
) {
|
||
super(message);
|
||
this.name = 'ApiError';
|
||
}
|
||
}
|
||
|
||
async function request<T>(
|
||
method: 'GET' | 'POST',
|
||
path: string,
|
||
body?: unknown,
|
||
): Promise<T> {
|
||
const url = `${API_BASE}${path}`;
|
||
const init: RequestInit = {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json', ...authHeader() },
|
||
};
|
||
if (body !== undefined) {
|
||
init.body = JSON.stringify(body);
|
||
}
|
||
|
||
const res = await fetch(url, init);
|
||
const data = await res.json().catch(() => ({}));
|
||
|
||
if (!res.ok) {
|
||
throw new ApiError(
|
||
res.status,
|
||
typeof data.error === 'string' ? data.error : `HTTP ${res.status}`,
|
||
);
|
||
}
|
||
|
||
return data as T;
|
||
}
|
||
|
||
/** 分类结果(镜像后端 ClassificationResult)。 */
|
||
export interface ClassificationResult {
|
||
readonly businessType: string;
|
||
readonly businessTypeConfidence: number;
|
||
readonly businessTypeCandidates: ReadonlyArray<{ value: string; confidence: number }>;
|
||
readonly needsBusinessTypeConfirm: boolean;
|
||
readonly industry: string;
|
||
readonly industryConfidence: number;
|
||
readonly industryCandidates: ReadonlyArray<{ value: string; confidence: number }>;
|
||
readonly needsIndustryConfirm: boolean;
|
||
}
|
||
|
||
/** 评估运行结果(镜像后端 RunAssessment 响应)。 */
|
||
export interface RunAssessmentResult {
|
||
readonly assessmentId: string;
|
||
readonly assessment: unknown;
|
||
readonly report: unknown;
|
||
readonly classification: ClassificationResult;
|
||
readonly confirmed: { businessType: string; industry: string };
|
||
readonly riskScore: number;
|
||
readonly riskGrade: string;
|
||
readonly acceptability: string;
|
||
readonly residualGaps: readonly string[];
|
||
}
|
||
|
||
/** 工作流状态。 */
|
||
export type WorkflowStatus =
|
||
| 'draft'
|
||
| 'pending_risk_review'
|
||
| 'risk_reviewed'
|
||
| 'pending_management_approval'
|
||
| 'approved'
|
||
| 'rejected'
|
||
| 'abandoned';
|
||
|
||
/** 操作记录条目。 */
|
||
export interface AuditLogEntry {
|
||
readonly role: string;
|
||
readonly username: string;
|
||
readonly action: string;
|
||
readonly comment?: string;
|
||
readonly timestamp: string;
|
||
}
|
||
|
||
/** 评估列表项。 */
|
||
export interface AssessmentListItem {
|
||
readonly id: string;
|
||
readonly projectDescription: string;
|
||
readonly businessType: string;
|
||
readonly industry: string;
|
||
readonly region: string;
|
||
readonly riskScore?: number;
|
||
readonly riskGrade?: string;
|
||
readonly acceptability?: string;
|
||
readonly createdAt: string;
|
||
readonly assessorId: string;
|
||
/** 发起人显示名(按 assessorId 解析)。 */
|
||
readonly assessorName?: string;
|
||
readonly status: WorkflowStatus;
|
||
readonly auditLog: AuditLogEntry[];
|
||
readonly archived?: boolean;
|
||
readonly recommendation?: { level: string; title: string };
|
||
}
|
||
|
||
/** 综合承接建议。 */
|
||
export interface Recommendation {
|
||
readonly level: 'accept' | 'conditional' | 'caution' | 'reject';
|
||
readonly title: string;
|
||
readonly note: string;
|
||
readonly targetMargin: number;
|
||
readonly netMargin: number | null;
|
||
}
|
||
|
||
/** 运营控制指标——单条。 */
|
||
export interface ControlMetric {
|
||
readonly name: string;
|
||
readonly target: string;
|
||
readonly current: string | null;
|
||
readonly rationale: string;
|
||
readonly action: string;
|
||
}
|
||
|
||
/** 运营控制指标——分组。 */
|
||
export interface ControlGroup {
|
||
readonly category: '盈利管控' | '运营管控' | '质量管控';
|
||
readonly metrics: readonly ControlMetric[];
|
||
}
|
||
|
||
/** 运营控制指标集合(建议承接项目的盈利/运营/质量管控看板)。 */
|
||
export interface OperatingControls {
|
||
readonly groups: readonly ControlGroup[];
|
||
readonly note: string;
|
||
}
|
||
|
||
/** 模板列表项。 */
|
||
export interface TemplateListItem {
|
||
readonly id: string;
|
||
readonly name: string;
|
||
readonly businessType: string;
|
||
readonly industry: string;
|
||
readonly isDefault: boolean;
|
||
readonly dimensions: ReadonlyArray<{
|
||
readonly id: string;
|
||
readonly name: string;
|
||
readonly weight: number;
|
||
readonly enabled: boolean;
|
||
readonly indicators: ReadonlyArray<{
|
||
readonly id: string;
|
||
readonly name: string;
|
||
readonly weight: number;
|
||
readonly enabled: boolean;
|
||
}>;
|
||
}>;
|
||
readonly redlines: ReadonlyArray<{
|
||
readonly id: string;
|
||
readonly triggerCondition: string;
|
||
readonly consequence: string;
|
||
readonly enabled: boolean;
|
||
}>;
|
||
}
|
||
|
||
/** 输入项目描述,返回分类识别结果。 */
|
||
export async function classifyProject(
|
||
projectDescription: string,
|
||
): Promise<ClassificationResult> {
|
||
return request<ClassificationResult>('POST', '/api/assessments/classify', {
|
||
projectDescription,
|
||
});
|
||
}
|
||
|
||
/** 运行完整评估流程。 */
|
||
export async function runAssessment(
|
||
params: {
|
||
readonly projectDescription: string;
|
||
readonly confirmation?: { readonly businessType?: string; readonly industry?: string };
|
||
readonly region?: string;
|
||
readonly assessorId?: string;
|
||
readonly assessmentId?: string;
|
||
readonly expectedSavedAt?: string;
|
||
readonly clientTotalHeadcount?: number;
|
||
readonly knownData?: ReadonlyArray<readonly [string, number]>;
|
||
readonly costInputs?: Record<string, number>;
|
||
readonly useLlm?: boolean;
|
||
readonly profitabilityInputs?: ProfitabilityInputs;
|
||
},
|
||
): Promise<RunAssessmentResult> {
|
||
return request<RunAssessmentResult>('POST', '/api/assessments/run', params);
|
||
}
|
||
|
||
/** 单个指标的分级选项。 */
|
||
export interface IndicatorLevelOption {
|
||
readonly level: number;
|
||
readonly label: string;
|
||
readonly description: string;
|
||
}
|
||
|
||
/** LLM 对某指标的预填建议。 */
|
||
export interface IndicatorSuggestion {
|
||
readonly indicatorId: string;
|
||
readonly level: number;
|
||
readonly confidence: number;
|
||
readonly rationale: string;
|
||
}
|
||
|
||
/** 追问指标项(含话术、分级含义与 LLM 预填)。 */
|
||
export interface IndicatorQuestion {
|
||
readonly dimensionId: string;
|
||
readonly dimensionName: string;
|
||
readonly indicatorId: string;
|
||
readonly name: string;
|
||
readonly askPrompt: string;
|
||
readonly evidenceRequired: string;
|
||
readonly levels: readonly IndicatorLevelOption[];
|
||
readonly suggestion: IndicatorSuggestion | null;
|
||
}
|
||
|
||
/** questions 接口响应。 */
|
||
export interface QuestionsResponse {
|
||
readonly businessType: string;
|
||
readonly industry: string;
|
||
readonly llmEnabled: boolean;
|
||
readonly indicators: readonly IndicatorQuestion[];
|
||
}
|
||
|
||
/** 获取指定业务类型/行业下的指标清单(含 LLM 预填)。 */
|
||
export async function fetchQuestions(params: {
|
||
readonly projectDescription: string;
|
||
readonly businessType: string;
|
||
readonly industry?: string;
|
||
readonly skipPrefill?: boolean;
|
||
}): Promise<QuestionsResponse> {
|
||
return request<QuestionsResponse>('POST', '/api/assessments/questions', params);
|
||
}
|
||
|
||
/** 用 LLM 从项目描述抽取岗位明细(名称+人数)。失败时后端返回空数组。 */
|
||
export async function extractPositions(
|
||
projectDescription: string,
|
||
): Promise<Array<{ name: string; headcount: number }>> {
|
||
const resp = await request<{ positions?: Array<{ name: string; headcount: number }> }>(
|
||
'POST',
|
||
'/api/assessments/extract-positions',
|
||
{ projectDescription },
|
||
);
|
||
return resp.positions ?? [];
|
||
}
|
||
|
||
/** LLM 综合研判结果。 */
|
||
export interface SynthesisResult {
|
||
readonly suggestedGrade: string;
|
||
readonly confidence: number;
|
||
readonly overall: string;
|
||
readonly crossRisks: readonly string[];
|
||
readonly suggestedConditions: readonly string[];
|
||
readonly divergent: boolean;
|
||
}
|
||
|
||
/** 盈利分析——岗位明细输入。 */
|
||
export interface PositionInput {
|
||
name: string;
|
||
headcount: number;
|
||
monthlyGrossSalary: number;
|
||
socialInsuranceBase?: number;
|
||
housingFundBase?: number;
|
||
monthlyBenefits?: number;
|
||
unitPrice?: number;
|
||
}
|
||
|
||
/** 盈利分析输入。 */
|
||
export interface ProfitabilityInputs {
|
||
businessType: string;
|
||
region?: string;
|
||
pricingModel: 'per_head' | 'cost_plus' | 'fixed_total' | 'volume';
|
||
contractMonths?: number;
|
||
positions: PositionInput[];
|
||
managementFeePerHeadMonth?: number;
|
||
markupRate?: number;
|
||
contractTotal?: number;
|
||
attritionMonthlyRate?: number;
|
||
recruitingCostPerHire?: number;
|
||
trainingCostPerHead?: number;
|
||
employerLiabilityInsuranceRate?: number;
|
||
managementSpan?: number;
|
||
managementMonthlyCostPerManager?: number;
|
||
accountPeriodMonths?: number;
|
||
annualInterestRate?: number;
|
||
periodExpenseRate?: number;
|
||
utilizationRate?: number;
|
||
vatMode?: 'general' | 'simplified_diff';
|
||
}
|
||
|
||
/** 盈利分析结果(镜像后端 ProfitabilityResult 关键字段)。 */
|
||
export interface ProfitabilityResult {
|
||
businessType: string;
|
||
pricingModel: string;
|
||
region: string;
|
||
contractMonths: number;
|
||
vatMode: string;
|
||
positions: ReadonlyArray<{
|
||
name: string;
|
||
headcount: number;
|
||
perHead: {
|
||
grossSalary: number;
|
||
socialInsurance: number;
|
||
housingFund: number;
|
||
benefits: number;
|
||
employerInsurance: number;
|
||
recruitingAmortized: number;
|
||
trainingAmortized: number;
|
||
managementAllocated: number;
|
||
fullyLoaded: number;
|
||
};
|
||
loadingFactor: number;
|
||
monthlyCost: number;
|
||
monthlyRevenue: number;
|
||
}>;
|
||
totalHeadcount: number;
|
||
loadingFactor: number;
|
||
monthly: {
|
||
revenueGross: number;
|
||
revenueNet: number;
|
||
laborCost: number;
|
||
managementCost: number;
|
||
totalCost: number;
|
||
grossProfit: number;
|
||
grossMargin: number;
|
||
periodExpense: number;
|
||
financeCost: number;
|
||
vat: number;
|
||
surcharge: number;
|
||
riskReserve: number;
|
||
badDebtReserve: number;
|
||
netProfit: number;
|
||
netMargin: number;
|
||
};
|
||
contract: {
|
||
revenueGross: number;
|
||
revenueNet: number;
|
||
totalCost: number;
|
||
grossProfit: number;
|
||
netProfit: number;
|
||
};
|
||
breakeven: { unitPrice?: number; markupRate?: number; utilization?: number };
|
||
sensitivity: ReadonlyArray<{ variable: string; baseNetMargin: number; shockedNetMargin: number; deltaPct: number }>;
|
||
cashflow: {
|
||
maxAdvance: number;
|
||
peakMonth: number;
|
||
points: ReadonlyArray<{ month: number; cumulative: number }>;
|
||
};
|
||
marginCurve: ReadonlyArray<{ priceFactor: number; unitPrice: number | null; netMargin: number }>;
|
||
assumptions: readonly string[];
|
||
}
|
||
|
||
/** 盈利分析(无状态计算)。 */
|
||
export async function analyzeProfitability(inputs: ProfitabilityInputs): Promise<ProfitabilityResult> {
|
||
return request<ProfitabilityResult>('POST', '/api/assessments/profitability', inputs);
|
||
}
|
||
|
||
/** 对已完成评估生成 LLM 综合研判。 */
|
||
export async function fetchSynthesis(id: string): Promise<SynthesisResult> {
|
||
return request<SynthesisResult>('POST', `/api/assessments/${id}/synthesis`, {});
|
||
}
|
||
|
||
/** 重新生成综合承接建议(可指定目标净利率)。 */
|
||
export async function regenerateRecommendation(
|
||
id: string,
|
||
targetMargin?: number,
|
||
): Promise<Recommendation> {
|
||
return request<Recommendation>(
|
||
'POST',
|
||
`/api/assessments/${id}/recommendation`,
|
||
targetMargin !== undefined ? { targetMargin } : {},
|
||
);
|
||
}
|
||
|
||
/** 获取评估列表(全量,向后兼容)。 */
|
||
export async function fetchAssessments(): Promise<AssessmentListItem[]> {
|
||
return request<AssessmentListItem[]>('GET', '/api/assessments');
|
||
}
|
||
|
||
/** 分页列表响应。 */
|
||
export interface AssessmentPage {
|
||
readonly items: AssessmentListItem[];
|
||
readonly total: number;
|
||
readonly page: number;
|
||
readonly pageSize: number;
|
||
}
|
||
|
||
/** 服务端分页 + 状态过滤 + 关键词搜索(直接走 SQL)。 */
|
||
export async function fetchAssessmentsPage(params: {
|
||
readonly page: number;
|
||
readonly pageSize: number;
|
||
readonly status?: string;
|
||
readonly q?: string;
|
||
readonly archived?: 'active' | 'archived' | 'all';
|
||
readonly assessorId?: string;
|
||
}): Promise<AssessmentPage> {
|
||
const sp = new URLSearchParams();
|
||
sp.set('page', String(params.page));
|
||
sp.set('pageSize', String(params.pageSize));
|
||
if (params.status !== undefined && params.status !== '' && params.status !== 'all') {
|
||
sp.set('status', params.status);
|
||
}
|
||
if (params.q !== undefined && params.q.trim() !== '') {
|
||
sp.set('q', params.q.trim());
|
||
}
|
||
if (params.archived !== undefined && params.archived !== 'active') {
|
||
sp.set('archived', params.archived);
|
||
}
|
||
if (params.assessorId !== undefined && params.assessorId !== '') {
|
||
sp.set('assessorId', params.assessorId);
|
||
}
|
||
return request<AssessmentPage>('GET', `/api/assessments?${sp.toString()}`);
|
||
}
|
||
|
||
/** 归档 / 取消归档评估。 */
|
||
export async function archiveAssessment(
|
||
id: string,
|
||
archived: boolean,
|
||
user?: string,
|
||
): Promise<{ id: string; archived: boolean; status: WorkflowStatus }> {
|
||
return request('POST', `/api/assessments/${id}/archive`, {
|
||
archived,
|
||
...(user !== undefined ? { user } : {}),
|
||
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||
});
|
||
}
|
||
|
||
/** 工作台统计。 */
|
||
export interface AssessmentSummary {
|
||
readonly total: number;
|
||
readonly byStatus: Record<string, number>;
|
||
readonly archived?: number;
|
||
}
|
||
|
||
/** 获取各状态评估数统计。销售传本人 assessorId 则按本人统计(服务端对销售亦强制本人)。 */
|
||
export async function fetchSummary(assessorId?: string): Promise<AssessmentSummary> {
|
||
const q = assessorId !== undefined && assessorId !== '' ? `?assessorId=${encodeURIComponent(assessorId)}` : '';
|
||
return request<AssessmentSummary>('GET', `/api/assessments/summary${q}`);
|
||
}
|
||
|
||
/** 评估详情响应。 */
|
||
export interface AssessmentDetailResponse {
|
||
readonly assessment: unknown;
|
||
readonly report: unknown;
|
||
readonly savedAt: string;
|
||
readonly status: WorkflowStatus;
|
||
readonly auditLog: AuditLogEntry[];
|
||
readonly profitability?: ProfitabilityResult | null;
|
||
readonly recommendation?: Recommendation | null;
|
||
readonly operatingControls?: OperatingControls | null;
|
||
readonly archived?: boolean;
|
||
/** 红线 id → 中文标题映射(用于将结果中的红线 id 显示为中文)。 */
|
||
readonly redlineTitles?: Record<string, string>;
|
||
/** 盈利测算原始输入(供编辑时完整带入)。 */
|
||
readonly profitabilityInputs?: ProfitabilityInputs | null;
|
||
/** 评估有效期(到期需重新评估)。 */
|
||
readonly expiresAt?: string | null;
|
||
/** 审批人指派(按审批线,软约束)。 */
|
||
readonly assignment?: { riskReviewerName: string | null; managerName: string | null } | null;
|
||
/** 发起人显示名(按 assessorId 解析)。 */
|
||
readonly assessorName?: string | null;
|
||
}
|
||
|
||
/** 获取单条评估详情。 */
|
||
export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetailResponse> {
|
||
return request<AssessmentDetailResponse>('GET', `/api/assessments/${id}`);
|
||
}
|
||
|
||
/** 下载评估报告(json/html 自包含文件)。 */
|
||
export async function downloadReport(id: string, format: 'json' | 'html'): Promise<void> {
|
||
const res = await fetch(`${API_BASE}/api/assessments/${id}/report/export?format=${format}`, {
|
||
headers: authHeader(),
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json().catch(() => ({})) as { error?: string };
|
||
throw new ApiError(res.status, err.error ?? `HTTP ${res.status}`);
|
||
}
|
||
const blob = await res.blob();
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `risk-report-${id}.${format}`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
/** 风控审核。 */
|
||
export async function reviewAssessment(
|
||
id: string,
|
||
action: 'approve' | 'reject',
|
||
user: string,
|
||
comment?: string,
|
||
): Promise<{ status: WorkflowStatus }> {
|
||
return request('POST', `/api/assessments/${id}/review`, {
|
||
action,
|
||
user,
|
||
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
||
});
|
||
}
|
||
|
||
/** 管理层审批。通过=最终通过,放弃=终态,驳回可退回风控复审或退回销售。 */
|
||
export async function approveAssessment(
|
||
id: string,
|
||
action: 'approve' | 'reject' | 'abandon',
|
||
user: string,
|
||
comment?: string,
|
||
rejectTo?: 'risk' | 'origin',
|
||
): Promise<{ status: WorkflowStatus }> {
|
||
return request('POST', `/api/assessments/${id}/approve`, {
|
||
action,
|
||
user,
|
||
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
||
...(rejectTo !== undefined ? { rejectTo } : {}),
|
||
});
|
||
}
|
||
|
||
/** 重新提交评估。 */
|
||
export async function resubmitAssessment(id: string, user: string): Promise<{ status: WorkflowStatus }> {
|
||
return request('POST', `/api/assessments/${id}/resubmit`, { user, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) });
|
||
}
|
||
|
||
/** 申报:将草稿评估报送风控审核。 */
|
||
export async function submitAssessment(id: string, user: string): Promise<{ status: WorkflowStatus }> {
|
||
return request('POST', `/api/assessments/${id}/submit`, { user, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) });
|
||
}
|
||
|
||
/** 应用预测准确度校准(调整目标净利率基准,管理层)。 */
|
||
export async function applyCalibration(): Promise<{ appliedBase: number; previousBase: number; deviationPct: number }> {
|
||
return request('POST', '/api/calibration/apply', {});
|
||
}
|
||
|
||
/** 查询当前目标净利率基准与据偏差计算的建议基准、是否已校准。 */
|
||
export async function fetchCalibration(): Promise<{
|
||
currentBase: number;
|
||
suggestedBase: number;
|
||
uncalibratedBase: number;
|
||
calibrated: boolean;
|
||
bias: string | null;
|
||
deviationPct: number | null;
|
||
}> {
|
||
return request('GET', '/api/calibration');
|
||
}
|
||
|
||
/** 风控/管理层对「待核实」红线进行人工裁定(命中/未命中),闭环判定。 */
|
||
export async function submitRedlineVerdict(
|
||
id: string,
|
||
params: { redlineId: string; status: '命中' | '未命中'; note?: string; user: string; role?: string; title?: string },
|
||
): Promise<{ redlineId: string; status: string; acceptability: string }> {
|
||
return request('POST', `/api/assessments/${id}/redline-verdict`, { ...params, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) });
|
||
}
|
||
|
||
/** 管理层对「已通过」或「已放弃」项目直接调整工作流状态(留痕)。 */
|
||
export async function overrideAssessmentStatus(
|
||
id: string,
|
||
status: WorkflowStatus,
|
||
user: string,
|
||
comment?: string,
|
||
): Promise<{ status: WorkflowStatus }> {
|
||
return request('POST', `/api/assessments/${id}/override`, {
|
||
status,
|
||
user,
|
||
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
||
});
|
||
}
|
||
|
||
/** 获取默认模板列表。 */
|
||
export async function fetchTemplates(): Promise<TemplateListItem[]> {
|
||
return request<TemplateListItem[]>('GET', '/api/templates');
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* P1-P3 新增功能 API
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/** 费率条目。 */
|
||
export interface RateEntry {
|
||
id: number;
|
||
region: string;
|
||
category: string;
|
||
key: string;
|
||
value: number;
|
||
effectiveDate: string;
|
||
version: number;
|
||
reviewed: boolean;
|
||
}
|
||
|
||
export async function fetchAllRates(): Promise<RateEntry[]> {
|
||
return request<RateEntry[]>('GET', '/api/rates');
|
||
}
|
||
|
||
export async function createRate(entry: Omit<RateEntry, 'id'>): Promise<RateEntry> {
|
||
return request('POST', '/api/rates', entry);
|
||
}
|
||
|
||
export async function reviewRate(id: number): Promise<void> {
|
||
await request('POST', `/api/rates/${id}/review`, {});
|
||
}
|
||
|
||
export async function deleteRateEntry(id: number): Promise<void> {
|
||
await fetch(`${API_BASE}/api/rates/${id}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
/** 社保单位部分各险种费率。 */
|
||
export interface SocialInsuranceRates {
|
||
pension: number;
|
||
medical: number;
|
||
unemployment: number;
|
||
injury: number;
|
||
maternity: number;
|
||
}
|
||
|
||
/** 地域完整费率套(与引擎 RegionRates 对齐)。 */
|
||
export interface RegionRates {
|
||
regionName: string;
|
||
socialInsurance: SocialInsuranceRates;
|
||
housingFund: number;
|
||
vatGeneralRate: number;
|
||
vatSimplifiedRate: number;
|
||
surchargeRate: number;
|
||
}
|
||
|
||
/** 地域费率套记录(含复核状态)。 */
|
||
export interface RegionRateRecord {
|
||
region: string;
|
||
rates: RegionRates;
|
||
reviewed: boolean;
|
||
updatedBy: string | null;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export async function fetchEngineDefaults(): Promise<{ national: RegionRates; regions: Record<string, RegionRates> }> {
|
||
return request('GET', '/api/region-rates/engine-defaults');
|
||
}
|
||
|
||
export async function fetchRegionRates(): Promise<RegionRateRecord[]> {
|
||
return request<RegionRateRecord[]>('GET', '/api/region-rates');
|
||
}
|
||
|
||
export async function saveRegionRate(region: string, rates: RegionRates, updatedBy?: string): Promise<void> {
|
||
await request('POST', '/api/region-rates', { region, rates, ...(updatedBy ? { updatedBy } : {}) });
|
||
}
|
||
|
||
export async function reviewRegionRate(region: string): Promise<void> {
|
||
await request('POST', `/api/region-rates/${encodeURIComponent(region)}/review`, {});
|
||
}
|
||
|
||
export async function deleteRegionRate(region: string): Promise<void> {
|
||
await fetch(`${API_BASE}/api/region-rates/${encodeURIComponent(region)}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
/** 红线规则。 */
|
||
export interface RedlineRuleItem {
|
||
id: string;
|
||
title: string;
|
||
triggerCondition: string;
|
||
consequence: string;
|
||
region: string | null;
|
||
businessType: string | null;
|
||
enabled: boolean;
|
||
version: number;
|
||
regulationRef: string | null;
|
||
/** 关联度量(可计算红线):4 个数值度量,或 `ind:<指标id>`(绑定指标风险等级);null 表示人工核实。 */
|
||
linkedMetric?: string | null;
|
||
/** 比较运算符。 */
|
||
compareOp?: '>=' | '<=' | '>' | '<' | null;
|
||
/** 阈值(百分比类按百分数;逾期按天;指标等级按 1-5)。 */
|
||
threshold?: number | null;
|
||
/** 可选第二条件(与主条件 AND 组合)。 */
|
||
linkedMetric2?: string | null;
|
||
compareOp2?: '>=' | '<=' | '>' | '<' | null;
|
||
threshold2?: number | null;
|
||
}
|
||
|
||
export async function fetchRedlineRules(): Promise<RedlineRuleItem[]> {
|
||
return request<RedlineRuleItem[]>('GET', '/api/redline-rules');
|
||
}
|
||
|
||
export async function createRedlineRule(rule: RedlineRuleItem): Promise<RedlineRuleItem> {
|
||
return request('POST', '/api/redline-rules', rule);
|
||
}
|
||
|
||
export async function deleteRedlineRuleApi(id: string): Promise<void> {
|
||
await fetch(`${API_BASE}/api/redline-rules/${id}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
/** 客户档案。 */
|
||
export interface CustomerItem {
|
||
id: string;
|
||
name: string;
|
||
creditRating: string;
|
||
avgOverdueDays: number;
|
||
totalContractAmount: number;
|
||
assessmentCount: number;
|
||
notes: string | null;
|
||
}
|
||
|
||
export async function fetchCustomers(): Promise<CustomerItem[]> {
|
||
return request<CustomerItem[]>('GET', '/api/customers');
|
||
}
|
||
|
||
export async function createCustomer(c: CustomerItem): Promise<CustomerItem> {
|
||
return request('POST', '/api/customers', c);
|
||
}
|
||
|
||
export async function deleteCustomerApi(id: string): Promise<void> {
|
||
await fetch(`${API_BASE}/api/customers/${id}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
export async function fetchConcentration(id: string): Promise<{ concentration: number; warning: string | null }> {
|
||
return request('GET', `/api/customers/${id}/concentration`);
|
||
}
|
||
|
||
/** 客户回款记录。 */
|
||
export interface CustomerPayment {
|
||
id: number;
|
||
customerId: string;
|
||
invoiceAmount: number;
|
||
dueDate: string;
|
||
paidDate: string | null;
|
||
note: string | null;
|
||
}
|
||
|
||
export async function fetchPayments(customerId: string): Promise<CustomerPayment[]> {
|
||
return request<CustomerPayment[]>('GET', `/api/customers/${customerId}/payments`);
|
||
}
|
||
|
||
export async function addPayment(
|
||
customerId: string,
|
||
p: { invoiceAmount: number; dueDate: string; paidDate?: string | null; note?: string | null },
|
||
): Promise<{ saved: boolean; avgOverdueDays: number }> {
|
||
return request('POST', `/api/customers/${customerId}/payments`, p);
|
||
}
|
||
|
||
export async function deletePaymentApi(customerId: string, paymentId: number): Promise<{ avgOverdueDays: number }> {
|
||
const res = await fetch(`${API_BASE}/api/customers/${customerId}/payments/${paymentId}`, { method: 'DELETE', headers: authHeader() });
|
||
return res.json();
|
||
}
|
||
|
||
/** 各地域最低工资标准。 */
|
||
export interface MinWageItem {
|
||
region: string;
|
||
monthlyWage: number;
|
||
updatedBy: string | null;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export async function fetchMinWages(): Promise<MinWageItem[]> {
|
||
return request<MinWageItem[]>('GET', '/api/min-wages');
|
||
}
|
||
|
||
export async function saveMinWage(region: string, monthlyWage: number, updatedBy?: string): Promise<void> {
|
||
await request('POST', '/api/min-wages', { region, monthlyWage, ...(updatedBy ? { updatedBy } : {}) });
|
||
}
|
||
|
||
export async function deleteMinWageApi(region: string): Promise<void> {
|
||
await fetch(`${API_BASE}/api/min-wages/${encodeURIComponent(region)}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
/** 向导草稿(服务端持久化,跨设备)。列表项不含 form。 */
|
||
export interface DraftItem {
|
||
id: string;
|
||
assessorId: string | null;
|
||
sourceAssessmentId: string | null;
|
||
projectName: string | null;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** 草稿详情(含完整向导快照 form)。 */
|
||
export interface DraftRecord extends DraftItem {
|
||
form: unknown;
|
||
}
|
||
|
||
/** 列出草稿(可按评估人过滤)。 */
|
||
export async function listDrafts(assessorId?: string): Promise<DraftItem[]> {
|
||
const q = assessorId ? `?assessorId=${encodeURIComponent(assessorId)}` : '';
|
||
return request<DraftItem[]>('GET', `/api/drafts${q}`);
|
||
}
|
||
|
||
/** 取草稿详情(含 form)。 */
|
||
export async function getDraft(id: string): Promise<DraftRecord> {
|
||
return request<DraftRecord>('GET', `/api/drafts/${encodeURIComponent(id)}`);
|
||
}
|
||
|
||
/** 保存(新增/更新)草稿。 */
|
||
export async function saveDraft(input: {
|
||
id: string;
|
||
assessorId?: string | null;
|
||
sourceAssessmentId?: string | null;
|
||
projectName?: string | null;
|
||
form: unknown;
|
||
}): Promise<DraftRecord> {
|
||
return request<DraftRecord>('POST', '/api/drafts', input);
|
||
}
|
||
|
||
/** 删除草稿。 */
|
||
export async function deleteDraftApi(id: string): Promise<void> {
|
||
await fetch(`${API_BASE}/api/drafts/${encodeURIComponent(id)}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
/** 用户角色。 */
|
||
export type UserRole = '商务/销售' | '风控' | '管理层' | '系统管理员';
|
||
|
||
/** 用户账号(不含密码)。 */
|
||
export interface UserItem {
|
||
id: string;
|
||
username: string;
|
||
displayName: string | null;
|
||
role: UserRole;
|
||
active: boolean;
|
||
createdAt: string;
|
||
}
|
||
|
||
/** 列出全部用户(系统管理员)。 */
|
||
export async function listUsers(): Promise<UserItem[]> {
|
||
return request<UserItem[]>('GET', '/api/users');
|
||
}
|
||
|
||
/** 新增用户。 */
|
||
export async function createUserApi(input: { username: string; displayName?: string; password: string; role: UserRole }): Promise<UserItem> {
|
||
return request<UserItem>('POST', '/api/users', input);
|
||
}
|
||
|
||
/** 更新用户显示名/角色/启用状态。 */
|
||
export async function updateUserApi(id: string, patch: { displayName?: string | null; role?: UserRole; active?: boolean }): Promise<UserItem> {
|
||
const res = await fetch(`${API_BASE}/api/users/${encodeURIComponent(id)}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json', ...authHeader() },
|
||
body: JSON.stringify(patch),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new ApiError(res.status, typeof data.error === 'string' ? data.error : `HTTP ${res.status}`);
|
||
return data as UserItem;
|
||
}
|
||
|
||
/** 重置用户密码。 */
|
||
export async function resetUserPasswordApi(id: string, password: string): Promise<void> {
|
||
await request('POST', `/api/users/${encodeURIComponent(id)}/password`, { password });
|
||
}
|
||
|
||
/** 删除用户。 */
|
||
export async function deleteUserApi(id: string): Promise<void> {
|
||
await fetch(`${API_BASE}/api/users/${encodeURIComponent(id)}`, { method: 'DELETE', headers: authHeader() });
|
||
}
|
||
|
||
/* ----------------------------- 审批流程配置 ----------------------------- */
|
||
|
||
export type ApprovalField = 'riskGrade' | 'netMarginPct' | 'acceptability' | 'redlineHit' | 'contractAmount' | 'businessType';
|
||
export type ApprovalOp = '>=' | '<=' | '>' | '<' | '==' | '!=';
|
||
|
||
export interface ApprovalCondition {
|
||
field: ApprovalField;
|
||
op: ApprovalOp;
|
||
value: string | number | boolean;
|
||
}
|
||
export interface ApprovalRule {
|
||
id: string;
|
||
name: string;
|
||
enabled: boolean;
|
||
requireManagement: boolean;
|
||
conditions: ApprovalCondition[];
|
||
}
|
||
export interface ApprovalLine {
|
||
salesId: string;
|
||
riskReviewerId: string | null;
|
||
managerId: string | null;
|
||
}
|
||
export interface ApprovalAssignment {
|
||
enabled: boolean;
|
||
defaultRiskReviewerId: string | null;
|
||
defaultManagerId: string | null;
|
||
lines: ApprovalLine[];
|
||
}
|
||
export interface ApprovalConfig {
|
||
defaultRequireManagement: boolean;
|
||
slaRiskHours: number;
|
||
slaMgmtHours: number;
|
||
rejectTo: 'origin' | 'risk';
|
||
rules: ApprovalRule[];
|
||
assignment: ApprovalAssignment;
|
||
}
|
||
|
||
/** 审批人指派记录。 */
|
||
export interface AssignmentRecord {
|
||
assessmentId: string;
|
||
riskReviewerId: string | null;
|
||
riskReviewerName: string | null;
|
||
managerId: string | null;
|
||
managerName: string | null;
|
||
}
|
||
|
||
/** 全部评估的审批人指派(assessmentId → 记录)。 */
|
||
export async function fetchAssignments(): Promise<Record<string, AssignmentRecord>> {
|
||
return request<Record<string, AssignmentRecord>>('GET', '/api/assignments');
|
||
}
|
||
|
||
/** 读取审批流程配置。 */
|
||
export async function fetchApprovalConfig(): Promise<ApprovalConfig> {
|
||
return request<ApprovalConfig>('GET', '/api/approval-config');
|
||
}
|
||
|
||
/** 保存审批流程配置(系统管理员)。 */
|
||
export async function saveApprovalConfig(config: ApprovalConfig): Promise<ApprovalConfig> {
|
||
const res = await fetch(`${API_BASE}/api/approval-config`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json', ...authHeader() },
|
||
body: JSON.stringify(config),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new ApiError(res.status, typeof data.error === 'string' ? data.error : `HTTP ${res.status}`);
|
||
return data as ApprovalConfig;
|
||
}
|
||
|
||
/** 方案对比。 */
|
||
export interface ScenarioItem {
|
||
id: string;
|
||
label: string;
|
||
inputs: ProfitabilityInputs;
|
||
result: ProfitabilityResult;
|
||
}
|
||
|
||
export async function fetchScenarios(assessmentId: string): Promise<ScenarioItem[]> {
|
||
return request<ScenarioItem[]>('GET', `/api/assessments/${assessmentId}/scenarios`);
|
||
}
|
||
|
||
export async function createScenario(assessmentId: string, label: string, inputs: ProfitabilityInputs): Promise<ScenarioItem> {
|
||
return request('POST', `/api/assessments/${assessmentId}/scenarios`, { label, inputs });
|
||
}
|
||
|
||
/** 相似项目。 */
|
||
export interface SimilarProjectItem {
|
||
id: string;
|
||
projectDescription: string;
|
||
businessType: string;
|
||
industry: string;
|
||
riskScore: number | null;
|
||
riskGrade: string | null;
|
||
netMargin: number | null;
|
||
rank: number;
|
||
}
|
||
|
||
export async function fetchSimilarProjects(description: string): Promise<SimilarProjectItem[]> {
|
||
const q = encodeURIComponent(description);
|
||
return request<SimilarProjectItem[]>('GET', `/api/similar?description=${q}`);
|
||
}
|
||
|
||
/** 看板统计。 */
|
||
export interface DashboardStats {
|
||
byBusinessType: Array<{ dimension: string; count: number; avgRiskScore: number | null; passRate: number | null }>;
|
||
byIndustry: Array<{ dimension: string; count: number; avgRiskScore: number | null; passRate: number | null }>;
|
||
byRegion: Array<{ dimension: string; count: number; avgRiskScore: number | null; passRate: number | null }>;
|
||
riskDist: Array<{ grade: string; count: number }>;
|
||
trend: Array<{ month: string; count: number }>;
|
||
}
|
||
|
||
export async function fetchDashboardStats(): Promise<DashboardStats> {
|
||
return request<DashboardStats>('GET', '/api/dashboard/stats');
|
||
}
|
||
|
||
/** 系统操作审计日志项。 */
|
||
export interface SystemLogItem {
|
||
id: number;
|
||
ts: string;
|
||
actorId: string | null;
|
||
actorName: string | null;
|
||
role: string | null;
|
||
action: string;
|
||
method: string;
|
||
path: string;
|
||
targetId: string | null;
|
||
targetName: string | null;
|
||
status: number | null;
|
||
success: boolean | null;
|
||
durationMs: number | null;
|
||
ip: string | null;
|
||
query: string | null;
|
||
detail: unknown;
|
||
}
|
||
|
||
export interface SystemLogPage {
|
||
items: SystemLogItem[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
actions: string[];
|
||
roles: string[];
|
||
actors: Array<{ id: string; name: string }>;
|
||
}
|
||
|
||
/** 查询系统操作审计日志(系统管理员)。 */
|
||
export async function fetchSystemLogs(params: {
|
||
page: number; pageSize: number; actorId?: string; action?: string; role?: string; method?: string; q?: string; from?: string; to?: string; success?: 'true' | 'false';
|
||
}): Promise<SystemLogPage> {
|
||
const sp = new URLSearchParams();
|
||
sp.set('page', String(params.page));
|
||
sp.set('pageSize', String(params.pageSize));
|
||
if (params.action) sp.set('action', params.action);
|
||
if (params.actorId) sp.set('actorId', params.actorId);
|
||
if (params.role) sp.set('role', params.role);
|
||
if (params.method) sp.set('method', params.method);
|
||
if (params.q) sp.set('q', params.q);
|
||
if (params.from) sp.set('from', params.from);
|
||
if (params.to) sp.set('to', params.to);
|
||
if (params.success) sp.set('success', params.success);
|
||
return request<SystemLogPage>('GET', `/api/system-logs?${sp.toString()}`);
|
||
}
|
||
|
||
/** 经验库。 */
|
||
export interface ExperienceItem {
|
||
id: number;
|
||
assessmentId: string;
|
||
businessType: string;
|
||
industry: string;
|
||
projectSummary: string;
|
||
lesson: string;
|
||
tags: string[];
|
||
}
|
||
|
||
export async function fetchExperience(businessType?: string, industry?: string): Promise<ExperienceItem[]> {
|
||
const sp = new URLSearchParams();
|
||
if (businessType) sp.set('businessType', businessType);
|
||
if (industry) sp.set('industry', industry);
|
||
const qs = sp.toString();
|
||
return request<ExperienceItem[]>('GET', `/api/experience${qs ? '?' + qs : ''}`);
|
||
}
|
||
|
||
/** 销售修改项目资料(仅驳回状态)。 */
|
||
export async function updateAssessment(
|
||
id: string,
|
||
data: { projectDescription?: string; region?: string; profitabilityInputs?: ProfitabilityInputs; user?: string },
|
||
): Promise<{ updated: boolean; profitability: ProfitabilityResult | null; recommendation: Recommendation | null }> {
|
||
const res = await fetch(`${API_BASE}/api/assessments/${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json', ...authHeader() },
|
||
body: JSON.stringify({ ...data, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) }),
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json().catch(() => ({})) as { error?: string };
|
||
throw new ApiError(res.status, err.error ?? `HTTP ${res.status}`);
|
||
}
|
||
return res.json();
|
||
}
|