OpenCheck 废标风控系统技术实现
一、系统架构总览
scss
┌─────────────────────────────────────────────────────────┐
│ 形式合规总览面板 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 密封检查 │ │ 签章检测 │ │ 资质校验 │ │ 格式合规 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 规则解析引擎 (Rule Engine) │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 废标风控库 (Risk Control DB) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
二、废标风控库数据结构设计
2.1 核心表结构
sql
-- 废标规则主表
CREATE TABLE disqualification_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_code VARCHAR(32) UNIQUE NOT NULL, -- 规则编码: F001, F002...
category VARCHAR(32) NOT NULL, -- 分类: seal/signature/format/qualification
severity VARCHAR(16) DEFAULT 'critical', -- 严重程度: critical/warning/info
title TEXT NOT NULL, -- 规则标题
description TEXT, -- 规则描述
check_type VARCHAR(32) NOT NULL, -- 检查类型: regex/keyword/image/rules_engine
check_config JSONB NOT NULL, -- 检查配置(核心)
industry VARCHAR(64), -- 适用行业: all/construction/it/medical
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 规则检查配置 JSONB 结构示例
-- check_config = {
-- "type": "regex",
-- "pattern": "密封处.*(?:公章|印章)",
-- "required": true,
-- "error_msg": "密封处未加盖公章",
-- "page_scope": "last_page" // 检查范围: first_page/last_page/all
-- }
-- 废标检查结果表
CREATE TABLE disqualification_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
assessment_id UUID NOT NULL REFERENCES assessments(id),
rule_id UUID NOT NULL REFERENCES disqualification_rules(id),
status VARCHAR(16) NOT NULL, -- pass/fail/warning
evidence JSONB, -- 证据: {page, text, confidence}
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 包号一致性校验表
CREATE TABLE package_consistency (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
assessment_id UUID NOT NULL REFERENCES assessments(id),
package_no VARCHAR(32) NOT NULL, -- 包号: 01, 02, 03...
source_type VARCHAR(32) NOT NULL, -- 来源: tender_doc/bid_doc/qa_doc
field_name VARCHAR(64) NOT NULL, -- 字段名: project_name/bid_deadline
field_value TEXT, -- 字段值
consistent BOOLEAN NOT NULL, -- 是否一致
diff_detail TEXT, -- 不一致详情
created_at TIMESTAMPTZ DEFAULT NOW()
);
2.2 规则配置 JSONB 结构详解
typescript
// 规则配置类型定义
interface RuleConfig {
type: 'regex' | 'keyword' | 'image' | 'rules_engine' | 'combo';
// regex: 正则匹配
// keyword: 关键词检测
// image: 图像识别(签章)
// rules_engine: 规则引擎(组合逻辑)
// combo: 多条件组合
pattern?: string; // 正则表达式
keywords?: string[]; // 关键词列表
required?: boolean; // 是否必须存在
page_scope?: string; // 检查范围
error_msg?: string; // 错误提示
confidence?: number; // 置信度阈值(图像识别用)
// rules_engine 类型专用
logic?: 'AND' | 'OR' | 'NOT';
sub_rules?: RuleConfig[];
}
三、规则解析引擎核心代码
3.1 规则引擎主类
typescript
// 规则解析引擎:统一调度所有废标检查
class DisqualificationEngine {
private rules: DisqRule[];
private imageDetector: SignatureDetector;
constructor() {
this.rules = await this.loadActiveRules();
this.imageDetector = new SignatureDetector();
}
// 主入口:对一份招标文件执行所有规则检查
async check(assessment: Assessment): Promise<DisqResult[]> {
const results: DisqResult[] = [];
const pdfChunks = await this.parsePDF(assessment.fileUrl);
for (const rule of this.rules) {
// 行业过滤:非本行业的规则跳过
if (rule.industry !== 'all' && rule.industry !== assessment.industry) {
continue;
}
let result: DisqResult;
switch (rule.check_type) {
case 'regex':
result = this.checkRegex(rule, pdfChunks);
break;
case 'keyword':
result = this.checkKeyword(rule, pdfChunks);
break;
case 'image':
result = await this.imageDetector.check(rule, pdfChunks);
break;
case 'rules_engine':
result = await this.checkRulesEngine(rule, pdfChunks);
break;
case 'combo':
result = await this.checkCombo(rule, pdfChunks);
break;
}
results.push(result);
}
return results;
}
// 正则检查:在 PDF 文本中匹配规则
private checkRegex(rule: DisqRule, chunks: PDFChunk[]): DisqResult {
const config = rule.check_config as RegexConfig;
const regex = new RegExp(config.pattern, 'g');
// 按页码范围过滤
const targetChunks = this.filterByPageScope(chunks, config.page_scope);
for (const chunk of targetChunks) {
const match = chunk.text.match(regex);
if (match) {
return {
rule_id: rule.id,
status: config.required ? 'pass' : 'warning',
evidence: {
page: chunk.page_number,
text: match[0],
confidence: 1.0
}
};
}
}
// 未匹配到
return {
rule_id: rule.id,
status: config.required ? 'fail' : 'pass',
evidence: null
};
}
}
3.2 包号一致性校验逻辑
typescript
// 包号一致性校验:确保招标文件和投标文件的包号信息一致
class PackageConsistencyChecker {
// 校验入口
async check(assessmentId: string): Promise<ConsistencyResult[]> {
const results: ConsistencyResult[] = [];
// 1. 提取招标文件中的包号信息
const tenderPackages = await this.extractTenderPackages(assessmentId);
// 2. 提取投标文件中的包号信息
const bidPackages = await this.extractBidPackages(assessmentId);
// 3. 逐字段对比
const fieldsToCheck = [
'project_name',
'bid_deadline',
'bid_bond',
'qualification_req',
'technical_score_ratio'
];
for (const pkg of tenderPackages) {
const bidPkg = bidPackages.find(b => b.package_no === pkg.package_no);
if (!bidPkg) {
results.push({
package_no: pkg.package_no,
consistent: false,
diff_detail: `投标文件中未找到包号 ${pkg.package_no} 的响应`
});
continue;
}
for (const field of fieldsToCheck) {
const tenderVal = pkg[field];
const bidVal = bidPkg[field];
// 模糊对比:忽略空格、标点差异
const normalizedTender = this.normalize(tenderVal);
const normalizedBid = this.normalize(bidVal);
if (normalizedTender !== normalizedBid) {
results.push({
package_no: pkg.package_no,
field_name: field,
consistent: false,
diff_detail: `招标文件: "${tenderVal}" vs 投标文件: "${bidVal}"`
});
}
}
}
return results;
}
// 文本归一化:去除空格、标点、统一全角半角
private normalize(text: string): string {
return text
.replace(/[\s\u3000]+/g, '') // 去除所有空格
.replace(/[,。;:!?]/g, m => // 全角转半角
String.fromCharCode(m.charCodeAt(0) - 0xFEE0))
.toLowerCase();
}
}
四、签章检测技术方案
4.1 像素级图像识别签章检测
typescript
// 签章检测器:基于 OpenCV + 自训练模型
class SignatureDetector {
private model: ort.InferenceSession;
constructor() {
// 加载 ONNX 模型(自训练的签章检测模型)
this.model = await ort.InferenceSession.create(
'./models/signature_detector.onnx'
);
}
// 检测 PDF 页面中是否包含签章
async check(rule: DisqRule, chunks: PDFChunk[]): Promise<DisqResult> {
const config = rule.check_config as ImageConfig;
for (const chunk of chunks) {
if (!chunk.image_data) continue;
// 1. 预处理:转灰度、二值化、降噪
const processed = this.preprocess(chunk.image_data);
// 2. 推理:检测签章区域
const detections = await this.detect(processed);
// 3. 过滤:按置信度阈值
const validDetections = detections.filter(
d => d.confidence >= config.confidence
);
if (validDetections.length > 0) {
// 4. 分类:公章/合同章/法人章
const classified = await this.classify(validDetections);
return {
rule_id: rule.id,
status: 'pass',
evidence: {
page: chunk.page_number,
seal_type: classified.type,
confidence: classified.confidence,
bbox: classified.bounding_box
}
};
}
}
return {
rule_id: rule.id,
status: 'fail',
evidence: { error: '未检测到签章' }
};
}
// 图像预处理流水线
private preprocess(imageData: Buffer): Tensor {
// 1. 解码为 numpy array
const img = cv.imdecode(imageData);
// 2. 转灰度
const gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY);
// 3. 高斯降噪
const denoised = cv.GaussianBlur(gray, [5, 5], 0);
// 4. 自适应二值化(处理光照不均)
const binary = cv.adaptiveThreshold(
denoised, 255,
cv.ADAPTIVE_THRESH_GAUSSIAN_C,
cv.THRESH_BINARY, 11, 2
);
// 5. 转换为模型输入张量
return this.toTensor(binary);
}
}
4.2 密封检查逻辑
typescript
// 密封检查:检测投标文件密封处是否符合要求
async checkSeal(assessment: Assessment): Promise<SealCheckResult> {
const results: SealCheckResult = {
has_seal: false,
seal_position: null,
seal_type: null,
issues: []
};
// 1. 定位密封处(通常在最后一页)
const lastPage = await this.getLastPage(assessment.fileUrl);
// 2. 检查是否有密封字样
const sealTextPattern = /密封[处口线]|封口|密封章/g;
const hasSealText = sealTextPattern.test(lastPage.text);
// 3. 检查是否有公章/骑缝章
const sealDetection = await this.imageDetector.detectSeal(lastPage.image);
if (hasSealText && sealDetection.has_seal) {
results.has_seal = true;
results.seal_position = sealDetection.position;
results.seal_type = sealDetection.type;
} else {
if (!hasSealText) {
results.issues.push('未找到密封处标识文字');
}
if (!sealDetection.has_seal) {
results.issues.push('密封处未检测到公章');
}
}
// 4. 检查密封方式(密封条/密封章/密封袋)
const sealMethodPattern = /密封条|密封章|密封袋|火漆/g;
const sealMethod = lastPage.text.match(sealMethodPattern);
results.seal_method = sealMethod ? sealMethod[0] : '未识别';
return results;
}
五、数据流架构
scss
招标文件(PDF/Word)
│
▼
┌──────────────────┐
│ 文档解析层 │ pdf-parse / mammoth / tesseract
│ (PDF → Chunks) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 规则引擎层 │ DisqualificationEngine
│ (规则 → 检查) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 结果聚合层 │ 汇总所有检查结果
│ (结果 → 评分) │ 计算废标风险分数
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 前端展示层 │ 形式合规总览面板
│ (面板 → 操作) │ 一键导出检查报告
└──────────────────┘
标签:#废标风控 #规则引擎 #PDF解析 #签章检测 #OPENCHECK #数据结构 #前端架构