外包风险评估系统:领域引擎+前端+服务端持久化与生产部署

- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
This commit is contained in:
freedakgmail
2026-06-13 01:06:39 +08:00
commit c670b9e454
404 changed files with 61820 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
/* eslint-disable */
/**
* 初始 schema:评估记录 / 工作流状态 / 操作记录 / 盈利分析。
* 使用 IF NOT EXISTS 以与早期 initSchema 已建表的环境幂等兼容。
*/
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS assessments (
id TEXT PRIMARY KEY,
assessment JSONB NOT NULL,
report JSONB,
saved_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_assessments_saved_at ON assessments(saved_at DESC);
CREATE TABLE IF NOT EXISTS workflow_status (
assessment_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS audit_logs (
id BIGSERIAL PRIMARY KEY,
assessment_id TEXT NOT NULL,
role TEXT NOT NULL,
username TEXT NOT NULL,
action TEXT NOT NULL,
comment TEXT,
ts TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_assessment ON audit_logs(assessment_id);
CREATE TABLE IF NOT EXISTS profitability (
assessment_id TEXT PRIMARY KEY,
result JSONB NOT NULL
);
`);
};
exports.down = (pgm) => {
pgm.sql(`
DROP TABLE IF EXISTS profitability;
DROP TABLE IF EXISTS audit_logs;
DROP TABLE IF EXISTS workflow_status;
DROP TABLE IF EXISTS assessments;
`);
};
@@ -0,0 +1,20 @@
/* eslint-disable */
/** 综合承接建议持久化表。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS recommendations (
assessment_id TEXT PRIMARY KEY,
level TEXT NOT NULL,
title TEXT NOT NULL,
note TEXT NOT NULL,
target_margin DOUBLE PRECISION NOT NULL,
net_margin DOUBLE PRECISION,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS recommendations;`);
};
+12
View File
@@ -0,0 +1,12 @@
/* eslint-disable */
/** 评估归档标记:归档项不在主列表显示,可单独查看。 */
exports.up = (pgm) => {
pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS archived BOOLEAN NOT NULL DEFAULT false;`);
pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_archived ON assessments(archived);`);
};
exports.down = (pgm) => {
pgm.sql(`DROP INDEX IF EXISTS idx_assessments_archived;`);
pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS archived;`);
};
+21
View File
@@ -0,0 +1,21 @@
/* eslint-disable */
/** 多报价方案对比:每个评估最多 5 套报价方案。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS scenarios (
id TEXT NOT NULL,
assessment_id TEXT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
label TEXT NOT NULL,
inputs JSONB NOT NULL,
result JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (assessment_id, id)
);
CREATE INDEX IF NOT EXISTS idx_scenarios_assessment ON scenarios(assessment_id);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS scenarios;`);
};
+24
View File
@@ -0,0 +1,24 @@
/* eslint-disable */
/** 费率表后台化:消除硬编码,支持按地域/分类管理费率,版本化发布。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS rate_tables (
id BIGSERIAL PRIMARY KEY,
region TEXT NOT NULL,
category TEXT NOT NULL,
key TEXT NOT NULL,
value DOUBLE PRECISION NOT NULL,
effective_date DATE NOT NULL DEFAULT CURRENT_DATE,
version INT NOT NULL DEFAULT 1,
reviewed BOOLEAN NOT NULL DEFAULT false,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(region, category, key, version)
);
CREATE INDEX IF NOT EXISTS idx_rate_tables_region ON rate_tables(region, category);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS rate_tables;`);
};
+21
View File
@@ -0,0 +1,21 @@
/* eslint-disable */
/** 运营指标实际值回填(评估闭环)。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS actuals (
id BIGSERIAL PRIMARY KEY,
assessment_id TEXT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
month INT NOT NULL,
metric_name TEXT NOT NULL,
actual_value DOUBLE PRECISION NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(assessment_id, month, metric_name)
);
CREATE INDEX IF NOT EXISTS idx_actuals_assessment ON actuals(assessment_id);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS actuals;`);
};
@@ -0,0 +1,26 @@
/* eslint-disable */
/** 合规红线库(可配置):按地域/业务类型条件启用,版本化管理。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS redline_rules (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
trigger_condition TEXT NOT NULL,
consequence TEXT NOT NULL,
region TEXT,
business_type TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
version INT NOT NULL DEFAULT 1,
regulation_ref TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_redline_rules_region ON redline_rules(region);
CREATE INDEX IF NOT EXISTS idx_redline_rules_biz ON redline_rules(business_type);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS redline_rules;`);
};
+23
View File
@@ -0,0 +1,23 @@
/* eslint-disable */
/** 客户信用与集中度风险:客户档案表。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS customers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
credit_rating TEXT NOT NULL DEFAULT '未评级',
avg_overdue_days DOUBLE PRECISION NOT NULL DEFAULT 0,
total_contract_amount DOUBLE PRECISION NOT NULL DEFAULT 0,
assessment_count INT NOT NULL DEFAULT 0,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS customers;`);
};
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable */
/** 评估有效期:默认 6 个月,到期可一键复评。 */
exports.up = (pgm) => {
pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;`);
pgm.sql(`UPDATE assessments SET expires_at = saved_at + INTERVAL '6 months' WHERE expires_at IS NULL;`);
pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_expires ON assessments(expires_at);`);
};
exports.down = (pgm) => {
pgm.sql(`DROP INDEX IF EXISTS idx_assessments_expires;`);
pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS expires_at;`);
};
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
/** 审批SLA:记录进入当前状态的时间,供超时计算。 */
exports.up = (pgm) => {
pgm.sql(`ALTER TABLE workflow_status ADD COLUMN IF NOT EXISTS entered_at TIMESTAMPTZ NOT NULL DEFAULT now();`);
};
exports.down = (pgm) => {
pgm.sql(`ALTER TABLE workflow_status DROP COLUMN IF EXISTS entered_at;`);
};
@@ -0,0 +1,20 @@
/* eslint-disable */
/** 驳回原因结构化:枚举表 + 审计关联。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS reject_reasons (
id BIGSERIAL PRIMARY KEY,
assessment_id TEXT NOT NULL,
reason_type TEXT NOT NULL,
detail TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_reject_reasons_assessment ON reject_reasons(assessment_id);
CREATE INDEX IF NOT EXISTS idx_reject_reasons_type ON reject_reasons(reason_type);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS reject_reasons;`);
};
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable */
/** 全文检索:用 PG tsvector 做相似历史项目检索。 */
exports.up = (pgm) => {
pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS tsv tsvector;`);
pgm.sql(`UPDATE assessments SET tsv = to_tsvector('simple', COALESCE(assessment->>'projectDescription','') || ' ' || COALESCE(assessment->>'businessType','') || ' ' || COALESCE(assessment->>'industry',''));`);
pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_tsv ON assessments USING gin(tsv);`);
};
exports.down = (pgm) => {
pgm.sql(`DROP INDEX IF EXISTS idx_assessments_tsv;`);
pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS tsv;`);
};
+23
View File
@@ -0,0 +1,23 @@
/* eslint-disable */
/** 附件管理:挂到评估/风险项,支持上传与审批时查证。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
assessment_id TEXT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
risk_item_id TEXT,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes BIGINT NOT NULL DEFAULT 0,
storage_path TEXT NOT NULL,
uploaded_by TEXT,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_attachments_assessment ON attachments(assessment_id);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS attachments;`);
};
+26
View File
@@ -0,0 +1,26 @@
/* eslint-disable */
/** LLM 经验库:沉淀综合研判 + 人工修正后的最终结论,供后续 few-shot。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS experience_library (
id BIGSERIAL PRIMARY KEY,
assessment_id TEXT NOT NULL,
business_type TEXT NOT NULL,
industry TEXT NOT NULL,
project_summary TEXT NOT NULL,
risk_grade TEXT,
acceptability TEXT,
recommendation_level TEXT,
lesson TEXT NOT NULL,
tags TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_experience_biz ON experience_library(business_type);
CREATE INDEX IF NOT EXISTS idx_experience_tags ON experience_library USING gin(tags);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS experience_library;`);
};
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable */
/** 向量搜索:用 pgvector 存储项目描述的 embedding,做语义相似检索。 */
exports.up = (pgm) => {
pgm.sql(`CREATE EXTENSION IF NOT EXISTS vector;`);
pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS embedding vector(1024);`);
pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_embedding ON assessments USING ivfflat (embedding vector_cosine_ops) WITH (lists = 10);`);
};
exports.down = (pgm) => {
pgm.sql(`DROP INDEX IF EXISTS idx_assessments_embedding;`);
pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS embedding;`);
};
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable */
/** 地域费率套:与引擎 RegionRates 结构对齐,复核后驱动评估盈利测算。 */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS region_rates (
region TEXT PRIMARY KEY,
rates JSONB NOT NULL,
reviewed BOOLEAN NOT NULL DEFAULT false,
updated_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS region_rates;`);
};
@@ -0,0 +1,37 @@
/* eslint-disable */
/**
* 红线可计算触发条件:将红线绑定到某项度量(月净利率/月毛利率/客户平均逾期天数/单客户集中度),
* 配合比较运算符与阈值,使红线可在评估时自动判定命中(而非一律"待核实")。
*
* - linked_metric: 关联度量键(netMargin / grossMargin / avgOverdueDays / concentration)
* 为空表示该红线仍需人工核实(合规/资质类红线)。
* - compare_op: 比较运算符(>=, <=, >, <)。
* - threshold: 阈值;百分比类度量(净利率/毛利率/集中度)按百分数填写(如 0 表示 0%、5 表示 5%)
* 逾期天数按天填写。
*/
exports.up = (pgm) => {
pgm.sql(`
ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS linked_metric TEXT;
ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS compare_op TEXT;
ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS threshold DOUBLE PRECISION;
`);
// 为内置数值型红线预置可计算条件(仅当字段为空时设置,避免覆盖人工配置)。
pgm.sql(`
UPDATE redline_rules SET linked_metric='netMargin', compare_op='<', threshold=0
WHERE id='negative-margin-3m' AND linked_metric IS NULL;
UPDATE redline_rules SET linked_metric='avgOverdueDays', compare_op='>', threshold=90
WHERE id='overdue-90days' AND linked_metric IS NULL;
UPDATE redline_rules SET linked_metric='concentration', compare_op='>', threshold=50
WHERE id='concentration-50pct' AND linked_metric IS NULL;
`);
};
exports.down = (pgm) => {
pgm.sql(`
ALTER TABLE redline_rules DROP COLUMN IF EXISTS linked_metric;
ALTER TABLE redline_rules DROP COLUMN IF EXISTS compare_op;
ALTER TABLE redline_rules DROP COLUMN IF EXISTS threshold;
`);
};
@@ -0,0 +1,13 @@
/* eslint-disable */
/**
* 保存盈利测算的原始输入(报价模式/合同周期/成本加成率/各项成本参数/岗位明细等),
* 以便编辑评估时完整带入原值(此前仅保存计算结果,导致加成率等输入项丢失)。
*/
exports.up = (pgm) => {
pgm.sql(`ALTER TABLE profitability ADD COLUMN IF NOT EXISTS inputs JSONB;`);
};
exports.down = (pgm) => {
pgm.sql(`ALTER TABLE profitability DROP COLUMN IF EXISTS inputs;`);
};
@@ -0,0 +1,21 @@
/* eslint-disable */
/**
* 红线复合条件:在主条件之外,支持一个可选的第二条件(与主条件 AND 组合)。
* 例如「月毛利率 < 0 且 月净利率 < 0」同时满足才命中。
*/
exports.up = (pgm) => {
pgm.sql(`
ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS linked_metric2 TEXT;
ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS compare_op2 TEXT;
ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS threshold2 DOUBLE PRECISION;
`);
};
exports.down = (pgm) => {
pgm.sql(`
ALTER TABLE redline_rules DROP COLUMN IF EXISTS linked_metric2;
ALTER TABLE redline_rules DROP COLUMN IF EXISTS compare_op2;
ALTER TABLE redline_rules DROP COLUMN IF EXISTS threshold2;
`);
};
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable */
/**
* 应用级设置(键值):用于持久化可调参数,如目标净利率基准(由预测准确度校准)。
*/
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value DOUBLE PRECISION NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS app_settings;`);
};
@@ -0,0 +1,24 @@
/* eslint-disable */
/**
* 客户回款记录:记录应收发票的到期日与实际回款日,用于自动计算客户平均逾期天数,
* 替代人工维护的 avg_overdue_days,并驱动"客户逾期超N天"红线。
*/
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS customer_payments (
id SERIAL PRIMARY KEY,
customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
invoice_amount DOUBLE PRECISION NOT NULL DEFAULT 0,
due_date DATE NOT NULL,
paid_date DATE,
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_customer_payments_cust ON customer_payments(customer_id);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS customer_payments;`);
};
+24
View File
@@ -0,0 +1,24 @@
/* eslint-disable */
/**
* 各地域月最低工资标准(可后台维护):驱动"低于最低工资"红线自动比对。
* 预置近似默认值,须经 HR/财务按当地官方标准复核更新。
*/
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS min_wages (
region TEXT PRIMARY KEY,
monthly_wage DOUBLE PRECISION NOT NULL,
updated_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO min_wages(region, monthly_wage) VALUES
('北京', 2420), ('上海', 2690), ('广东', 1900), ('深圳', 2360),
('江苏', 2490), ('浙江', 2490), ('四川', 2100), ('河北', 2200), ('中国大陆', 2000)
ON CONFLICT(region) DO NOTHING;
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS min_wages;`);
};
@@ -0,0 +1,25 @@
/* eslint-disable */
/**
* 评估向导草稿(服务端持久化,跨设备):保存未运行/未提交的向导填写进度。
* - source_assessment_id 为空:全新建评估的草稿
* - source_assessment_id 非空:编辑某既有评估时的草稿(中断不丢失)
* form 存完整向导快照(步骤/立项/描述/业务类型/指标作答/岗位/成本参数等)。
*/
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS wizard_drafts (
id TEXT PRIMARY KEY,
assessor_id TEXT,
source_assessment_id TEXT,
project_name TEXT,
form JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_wizard_drafts_assessor ON wizard_drafts(assessor_id);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS wizard_drafts;`);
};