# OpenCheck 废标风控系统技术实现

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 #数据结构 #前端架构

相关推荐
rannn_1112 小时前
【力扣hot100】链表专题|21、2、19、24、92、25
java·数据结构·算法·leetcode·链表·开发
Gust of wind2 小时前
数据结构基础:线性表及其存储实现
数据结构
Rabitebla3 小时前
C++11 新特性详解(一):列表初始化、initializer_list 与右值引用
java·开发语言·数据结构·c++·算法·leetcode·list
zephyr054 小时前
链表实现O(n log n)时间复杂度的排序——归并排序详解
java·数据结构·算法
XWalnut5 小时前
LeetCode刷题 day37
java·数据结构·算法·leetcode
爱跳舞的烤冷面6 小时前
自学嵌入式第19天(数据结构篇——栈)
数据结构
星轨初途7 小时前
LeetCode 热题 100——day11 滑动窗口最大值
数据结构·c++·算法·leetcode·职场和发展
LuminousCPP20 小时前
数据结构-双向循环链表
c语言·数据结构·笔记·链表
白狐_79821 小时前
408数据结构第5章:树与二叉树②——遍历、线索树、森林与哈夫曼
数据结构·算法·深度优先