Files
RiskAgent/web/src/api/client.ts
T
freedakgmail 8bac14ef44 fix(calibration): 校准幂等+已校准状态显示,解决重复提示
根因:预测准确度卡的偏差来自历史回填项目的预测vs实际,属既成事实,
不会因校准改变;且原 apply 公式 next=current+dev 会累加,反复点越推越高。

修复:
- 校准建议/应用均基于未校准原始基准(env/默认)计算,保证幂等
- GET /api/calibration 返回 uncalibratedBase 与 calibrated 标志
- 卡片:已校准时显示「已按当前偏差校准:基准X%」绿色状态,不再出现按钮;
  未校准时按钮明示「X% → Y%」
- 补充说明:历史偏差不会因校准改变,校准仅调整后续承接建议阈值
2026-06-14 11:01:55 +08:00

1080 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* HTTP API 客户端:封装 fetch 调用后端 REST APIReq 16.2)。
*
* 所有 API 返回 Promise<T>,错误时抛出 ApiError(含 status 与 message)。
*/
/**
* 后端 API 基址。
* - 开发模式:默认 http://localhost:3005Vite 代理或直连本地后端)。
* - 生产模式:默认空串 → 走相对路径 /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();
}