财务发票系统设计与实现:开票、导入、OCR识别全流程

92 阅读24分钟

财务发票系统设计与实现:开票、导入、OCR识别全流程

前言

发票管理是企业财务系统的核心模块之一,涉及开票申请、发票开具、发票导入、OCR识别、发票验真、报销管理等多个环节。本文将详细介绍如何设计和实现一套完整的财务发票系统,涵盖系统架构、核心功能、代码实现等内容。


一、发票系统概述

1.1 发票类型

+------------------------------------------------------------------+
|                        发票类型分类                                |
+------------------------------------------------------------------+
|                                                                   |
|   按发票形式分类:                                                 |
|   +------------------+------------------------------------------+ |
|   |  增值税专用发票   |  可抵扣进项税,适用于一般纳税人            | |
|   +------------------+------------------------------------------+ |
|   |  增值税普通发票   |  不可抵扣,适用于小规模纳税人和个人消费    | |
|   +------------------+------------------------------------------+ |
|   |  增值税电子发票   |  电子版普通发票,具有同等法律效力          | |
|   +------------------+------------------------------------------+ |
|   |  增值税电子专票   |  电子版专用发票,可抵扣                    | |
|   +------------------+------------------------------------------+ |
|   |  全电发票        |  全面数字化电子发票(数电票)               | |
|   +------------------+------------------------------------------+ |
|                                                                   |
|   其他票据类型:                                                   |
|   +------------------+------------------------------------------+ |
|   |  机动车销售发票   |  购买机动车时开具                         | |
|   +------------------+------------------------------------------+ |
|   |  通行费发票      |  高速公路、桥闸通行费                      | |
|   +------------------+------------------------------------------+ |
|   |  火车票/机票     |  可作为增值税抵扣凭证                      | |
|   +------------------+------------------------------------------+ |
|   |  定额发票        |  固定金额的发票                           | |
|   +------------------+------------------------------------------+ |
|                                                                   |
+------------------------------------------------------------------+

1.2 发票核心要素

+------------------------------------------------------------------+
|                     增值税发票核心要素                             |
+------------------------------------------------------------------+
|                                                                   |
|   +------------------------------------------------------------+ |
|   |                    增值税电子普通发票                        | |
|   +------------------------------------------------------------+ |
|   |  发票代码: 011001900311          发票号码: 12345678         | |
|   |  开票日期: 2024-01-15            校验码: ABCD1234...        | |
|   +------------------------------------------------------------+ |
|   |  购买方信息:                                                | |
|   |    名称: XX科技有限公司                                     | |
|   |    纳税人识别号: 91110000MA12345678                        | |
|   |    地址电话: 北京市朝阳区... 010-12345678                   | |
|   |    开户行及账号: 工商银行北京分行 1234567890                | |
|   +------------------------------------------------------------+ |
|   |  商品明细:                                                  | |
|   |  +--------+------+------+------+------+------+------+       | |
|   |  | 商品名 | 规格 | 单位 | 数量 | 单价 | 金额 | 税率 |       | |
|   |  +--------+------+------+------+------+------+------+       | |
|   |  | 软件服务| -   | 项  | 1   |10000|10000| 6%  |       | |
|   |  +--------+------+------+------+------+------+------+       | |
|   +------------------------------------------------------------+ |
|   |  价税合计(大写): 壹万零陆佰元整                              | |
|   |  价税合计(小写): ¥10600.00                                  | |
|   |  税额: ¥600.00                                              | |
|   +------------------------------------------------------------+ |
|   |  销售方信息:                                                | |
|   |    名称: YY软件有限公司                                     | |
|   |    纳税人识别号: 91110000MA98765432                        | |
|   +------------------------------------------------------------+ |
|   |  备注:                                                      | |
|   |  开票人: 张三    复核: 李四    收款人: 王五                  | |
|   +------------------------------------------------------------+ |
|                                                                   |
+------------------------------------------------------------------+

1.3 业务流程

+------------------------------------------------------------------+
|                      发票业务全流程                                |
+------------------------------------------------------------------+
|                                                                   |
|   开票流程:                                                        |
|   +--------+    +--------+    +--------+    +--------+            |
|   | 开票   | -> | 审批   | -> | 开票   | -> | 交付   |            |
|   | 申请   |    | 流程   |    | 执行   |    | 发票   |            |
|   +--------+    +--------+    +--------+    +--------+            |
|                                                                   |
|   收票流程:                                                        |
|   +--------+    +--------+    +--------+    +--------+            |
|   | 发票   | -> | OCR    | -> | 发票   | -> | 入账   |            |
|   | 采集   |    | 识别   |    | 验真   |    | 报销   |            |
|   +--------+    +--------+    +--------+    +--------+            |
|       |                                                           |
|       +-- 手动录入                                                |
|       +-- 扫描上传                                                |
|       +-- 邮件导入                                                |
|       +-- Excel导入                                               |
|                                                                   |
+------------------------------------------------------------------+

二、系统架构设计

2.1 整体架构

+------------------------------------------------------------------------+
|                         发票系统整体架构                                 |
+------------------------------------------------------------------------+
|                                                                         |
|   接入层                                                                |
|   +------------------+  +------------------+  +-------------------+     |
|   |   Web 管理端     |  |   移动端 APP     |  |    开放 API       |     |
|   +------------------+  +------------------+  +-------------------+     |
|                                     |                                   |
|   网关层                            v                                   |
|   +---------------------------------------------------------------------+
|   |                        API Gateway                                  |
|   |            (认证、限流、路由、日志)                                  |
|   +---------------------------------------------------------------------+
|                                     |                                   |
|   服务层                            v                                   |
|   +---------------------------------------------------------------------+
|   |                                                                     |
|   |  +---------------+  +---------------+  +---------------+            |
|   |  |  开票服务     |  |  收票服务     |  |  验真服务     |            |
|   |  +---------------+  +---------------+  +---------------+            |
|   |                                                                     |
|   |  +---------------+  +---------------+  +---------------+            |
|   |  |  OCR服务      |  |  报销服务     |  |  统计服务     |            |
|   |  +---------------+  +---------------+  +---------------+            |
|   |                                                                     |
|   +---------------------------------------------------------------------+
|                                     |                                   |
|   中间件层                          v                                   |
|   +---------------------------------------------------------------------+
|   |  +----------+  +----------+  +----------+  +----------+            |
|   |  |  MySQL   |  |  Redis   |  | RocketMQ |  |   OSS    |            |
|   |  +----------+  +----------+  +----------+  +----------+            |
|   +---------------------------------------------------------------------+
|                                     |                                   |
|   外部服务                          v                                   |
|   +---------------------------------------------------------------------+
|   |  +---------------+  +---------------+  +---------------+            |
|   |  |  税控系统     |  |  税务验真API  |  |  OCR云服务    |            |
|   |  +---------------+  +---------------+  +---------------+            |
|   +---------------------------------------------------------------------+
|                                                                         |
+------------------------------------------------------------------------+

2.2 核心模块说明

+------------------------------------------------------------------+
|                       核心模块职责                                 |
+------------------------------------------------------------------+
|                                                                   |
|   开票服务:                                                        |
|   - 开票申请管理                                                  |
|   - 审批流程                                                      |
|   - 对接税控设备/电子发票平台                                      |
|   - 发票交付(邮件/短信/下载)                                     |
|                                                                   |
|   收票服务:                                                        |
|   - 发票采集(扫描/拍照/导入)                                     |
|   - 发票信息管理                                                  |
|   - 发票台账                                                      |
|   - 抵扣管理                                                      |
|                                                                   |
|   OCR服务:                                                         |
|   - 发票图片识别                                                  |
|   - 结构化数据提取                                                |
|   - 多类型发票支持                                                |
|                                                                   |
|   验真服务:                                                        |
|   - 调用税务局验真接口                                            |
|   - 发票真伪校验                                                  |
|   - 查重校验                                                      |
|                                                                   |
|   报销服务:                                                        |
|   - 报销单管理                                                    |
|   - 发票关联                                                      |
|   - 审批流程                                                      |
|                                                                   |
+------------------------------------------------------------------+

三、数据库设计

3.1 核心表结构

-- 1. 发票主表
CREATE TABLE `invoice` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    `invoice_code` VARCHAR(20) NOT NULL COMMENT '发票代码',
    `invoice_no` VARCHAR(20) NOT NULL COMMENT '发票号码',
    `invoice_type` VARCHAR(20) NOT NULL COMMENT '发票类型:SPECIAL/NORMAL/ELECTRONIC/FULL_DIGITAL',
    `invoice_status` TINYINT NOT NULL DEFAULT 0 COMMENT '发票状态:0-正常,1-已冲红,2-已作废',
    `invoice_date` DATE NOT NULL COMMENT '开票日期',
    `check_code` VARCHAR(50) COMMENT '校验码(后6位)',
    `machine_code` VARCHAR(20) COMMENT '机器编号',

    -- 购买方信息
    `buyer_name` VARCHAR(200) NOT NULL COMMENT '购买方名称',
    `buyer_tax_no` VARCHAR(20) COMMENT '购买方纳税人识别号',
    `buyer_address` VARCHAR(200) COMMENT '购买方地址电话',
    `buyer_bank` VARCHAR(200) COMMENT '购买方开户行及账号',

    -- 销售方信息
    `seller_name` VARCHAR(200) NOT NULL COMMENT '销售方名称',
    `seller_tax_no` VARCHAR(20) COMMENT '销售方纳税人识别号',
    `seller_address` VARCHAR(200) COMMENT '销售方地址电话',
    `seller_bank` VARCHAR(200) COMMENT '销售方开户行及账号',

    -- 金额信息
    `amount` DECIMAL(14,2) NOT NULL COMMENT '不含税金额',
    `tax_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '税额',
    `total_amount` DECIMAL(14,2) NOT NULL COMMENT '价税合计',
    `tax_rate` VARCHAR(20) COMMENT '税率',

    -- 其他信息
    `remark` VARCHAR(500) COMMENT '备注',
    `drawer` VARCHAR(50) COMMENT '开票人',
    `reviewer` VARCHAR(50) COMMENT '复核人',
    `payee` VARCHAR(50) COMMENT '收款人',

    -- 来源与附件
    `source_type` VARCHAR(20) NOT NULL COMMENT '来源类型:MANUAL/OCR/IMPORT/EMAIL',
    `file_url` VARCHAR(500) COMMENT '发票文件URL',
    `pdf_url` VARCHAR(500) COMMENT 'PDF文件URL',
    `ofd_url` VARCHAR(500) COMMENT 'OFD文件URL',

    -- 验真信息
    `verify_status` TINYINT DEFAULT 0 COMMENT '验真状态:0-未验真,1-验真通过,2-验真失败',
    `verify_time` DATETIME COMMENT '验真时间',
    `verify_result` TEXT COMMENT '验真结果JSON',

    -- 业务关联
    `company_id` BIGINT COMMENT '所属公司ID',
    `dept_id` BIGINT COMMENT '所属部门ID',
    `user_id` BIGINT COMMENT '录入人ID',
    `reimbursement_id` BIGINT COMMENT '关联报销单ID',
    `reimbursement_status` TINYINT DEFAULT 0 COMMENT '报销状态:0-未报销,1-报销中,2-已报销',

    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `deleted` TINYINT NOT NULL DEFAULT 0,

    UNIQUE KEY `uk_invoice` (`invoice_code`, `invoice_no`),
    KEY `idx_buyer_tax_no` (`buyer_tax_no`),
    KEY `idx_seller_tax_no` (`seller_tax_no`),
    KEY `idx_invoice_date` (`invoice_date`),
    KEY `idx_company_id` (`company_id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_reimbursement_status` (`reimbursement_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发票主表';

-- 2. 发票明细表
CREATE TABLE `invoice_item` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `invoice_id` BIGINT NOT NULL COMMENT '发票ID',
    `line_no` INT NOT NULL COMMENT '行号',
    `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
    `specification` VARCHAR(100) COMMENT '规格型号',
    `unit` VARCHAR(20) COMMENT '单位',
    `quantity` DECIMAL(14,4) COMMENT '数量',
    `unit_price` DECIMAL(14,6) COMMENT '单价',
    `amount` DECIMAL(14,2) NOT NULL COMMENT '金额',
    `tax_rate` VARCHAR(20) COMMENT '税率',
    `tax_amount` DECIMAL(14,2) COMMENT '税额',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发票明细表';

-- 3. 开票申请表
CREATE TABLE `invoice_apply` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `apply_no` VARCHAR(32) NOT NULL COMMENT '申请编号',
    `apply_type` TINYINT NOT NULL COMMENT '申请类型:1-开票,2-冲红,3-作废',
    `apply_status` TINYINT NOT NULL DEFAULT 0 COMMENT '申请状态:0-待审批,1-审批中,2-已通过,3-已拒绝,4-已开票',

    -- 申请信息
    `invoice_type` VARCHAR(20) NOT NULL COMMENT '发票类型',
    `title_type` TINYINT NOT NULL DEFAULT 1 COMMENT '抬头类型:1-企业,2-个人',

    -- 购买方信息
    `buyer_name` VARCHAR(200) NOT NULL COMMENT '购买方名称',
    `buyer_tax_no` VARCHAR(20) COMMENT '购买方纳税人识别号',
    `buyer_address` VARCHAR(200) COMMENT '购买方地址电话',
    `buyer_bank` VARCHAR(200) COMMENT '购买方开户行及账号',
    `buyer_email` VARCHAR(100) COMMENT '购买方邮箱',
    `buyer_phone` VARCHAR(20) COMMENT '购买方电话',

    -- 商品信息
    `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
    `tax_code` VARCHAR(50) COMMENT '税收分类编码',
    `amount` DECIMAL(14,2) NOT NULL COMMENT '不含税金额',
    `tax_rate` VARCHAR(20) COMMENT '税率',
    `tax_amount` DECIMAL(14,2) COMMENT '税额',
    `total_amount` DECIMAL(14,2) NOT NULL COMMENT '价税合计',

    -- 关联信息
    `order_no` VARCHAR(64) COMMENT '关联订单号',
    `contract_no` VARCHAR(64) COMMENT '关联合同号',
    `remark` VARCHAR(500) COMMENT '备注',

    -- 申请人信息
    `apply_user_id` BIGINT NOT NULL COMMENT '申请人ID',
    `apply_user_name` VARCHAR(50) COMMENT '申请人姓名',
    `apply_dept_id` BIGINT COMMENT '申请部门ID',
    `apply_time` DATETIME NOT NULL COMMENT '申请时间',

    -- 审批信息
    `approve_user_id` BIGINT COMMENT '审批人ID',
    `approve_user_name` VARCHAR(50) COMMENT '审批人姓名',
    `approve_time` DATETIME COMMENT '审批时间',
    `approve_remark` VARCHAR(500) COMMENT '审批意见',

    -- 开票结果
    `invoice_id` BIGINT COMMENT '生成的发票ID',
    `invoice_code` VARCHAR(20) COMMENT '发票代码',
    `invoice_no` VARCHAR(20) COMMENT '发票号码',
    `invoice_time` DATETIME COMMENT '开票时间',

    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY `uk_apply_no` (`apply_no`),
    KEY `idx_apply_status` (`apply_status`),
    KEY `idx_apply_user_id` (`apply_user_id`),
    KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开票申请表';

-- 4. 发票抬头表(常用抬头管理)
CREATE TABLE `invoice_title` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `company_id` BIGINT NOT NULL COMMENT '所属公司ID',
    `title_type` TINYINT NOT NULL DEFAULT 1 COMMENT '抬头类型:1-企业,2-个人',
    `title_name` VARCHAR(200) NOT NULL COMMENT '抬头名称',
    `tax_no` VARCHAR(20) COMMENT '纳税人识别号',
    `address` VARCHAR(200) COMMENT '地址',
    `phone` VARCHAR(50) COMMENT '电话',
    `bank_name` VARCHAR(100) COMMENT '开户银行',
    `bank_account` VARCHAR(50) COMMENT '银行账号',
    `is_default` TINYINT DEFAULT 0 COMMENT '是否默认:0-否,1-是',
    `status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    KEY `idx_company_id` (`company_id`),
    KEY `idx_tax_no` (`tax_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发票抬头表';

-- 5. 发票验真记录表
CREATE TABLE `invoice_verify_log` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `invoice_id` BIGINT NOT NULL COMMENT '发票ID',
    `invoice_code` VARCHAR(20) NOT NULL COMMENT '发票代码',
    `invoice_no` VARCHAR(20) NOT NULL COMMENT '发票号码',
    `verify_type` VARCHAR(20) COMMENT '验真类型:TAX/THIRD_PARTY',
    `verify_status` TINYINT NOT NULL COMMENT '验真状态:1-成功,2-失败',
    `verify_message` VARCHAR(500) COMMENT '验真消息',
    `request_data` TEXT COMMENT '请求数据',
    `response_data` TEXT COMMENT '响应数据',
    `verify_time` DATETIME NOT NULL COMMENT '验真时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发票验真记录表';

-- 6. OCR识别记录表
CREATE TABLE `invoice_ocr_log` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `batch_no` VARCHAR(32) COMMENT '批次号',
    `file_name` VARCHAR(200) COMMENT '文件名',
    `file_url` VARCHAR(500) NOT NULL COMMENT '文件URL',
    `file_type` VARCHAR(20) COMMENT '文件类型:IMAGE/PDF/OFD',
    `ocr_status` TINYINT NOT NULL DEFAULT 0 COMMENT 'OCR状态:0-待识别,1-识别中,2-识别成功,3-识别失败',
    `ocr_result` TEXT COMMENT 'OCR识别结果JSON',
    `invoice_id` BIGINT COMMENT '生成的发票ID',
    `error_msg` VARCHAR(500) COMMENT '错误信息',
    `ocr_time` DATETIME COMMENT 'OCR识别时间',
    `cost_time` INT COMMENT '耗时(毫秒)',
    `user_id` BIGINT COMMENT '操作用户ID',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_batch_no` (`batch_no`),
    KEY `idx_ocr_status` (`ocr_status`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OCR识别记录表';

-- 7. 报销单表
CREATE TABLE `reimbursement` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `reimburse_no` VARCHAR(32) NOT NULL COMMENT '报销单号',
    `reimburse_type` VARCHAR(20) NOT NULL COMMENT '报销类型:TRAVEL/DAILY/PROJECT',
    `reimburse_status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-草稿,1-待审批,2-审批中,3-已通过,4-已拒绝,5-已付款',
    `title` VARCHAR(200) NOT NULL COMMENT '报销主题',
    `total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '报销总金额',
    `invoice_count` INT NOT NULL DEFAULT 0 COMMENT '发票数量',
    `apply_user_id` BIGINT NOT NULL COMMENT '申请人ID',
    `apply_user_name` VARCHAR(50) COMMENT '申请人姓名',
    `apply_dept_id` BIGINT COMMENT '申请部门ID',
    `apply_time` DATETIME COMMENT '申请时间',
    `remark` VARCHAR(500) COMMENT '备注说明',
    `attachment_urls` TEXT COMMENT '附件URL列表JSON',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_reimburse_no` (`reimburse_no`),
    KEY `idx_apply_user_id` (`apply_user_id`),
    KEY `idx_reimburse_status` (`reimburse_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报销单表';

-- 8. 报销单发票关联表
CREATE TABLE `reimbursement_invoice` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `reimbursement_id` BIGINT NOT NULL COMMENT '报销单ID',
    `invoice_id` BIGINT NOT NULL COMMENT '发票ID',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_reimburse_invoice` (`reimbursement_id`, `invoice_id`),
    KEY `idx_invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报销单发票关联表';

-- 9. 导入批次表
CREATE TABLE `invoice_import_batch` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `batch_no` VARCHAR(32) NOT NULL COMMENT '批次号',
    `import_type` VARCHAR(20) NOT NULL COMMENT '导入类型:EXCEL/EMAIL/ZIP',
    `file_name` VARCHAR(200) COMMENT '文件名',
    `file_url` VARCHAR(500) COMMENT '文件URL',
    `total_count` INT DEFAULT 0 COMMENT '总记录数',
    `success_count` INT DEFAULT 0 COMMENT '成功数',
    `fail_count` INT DEFAULT 0 COMMENT '失败数',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待处理,1-处理中,2-已完成,3-失败',
    `error_file_url` VARCHAR(500) COMMENT '错误文件URL',
    `user_id` BIGINT COMMENT '操作用户ID',
    `start_time` DATETIME COMMENT '开始时间',
    `end_time` DATETIME COMMENT '结束时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_batch_no` (`batch_no`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发票导入批次表';

3.2 E-R 关系图

+------------------------------------------------------------------+
|                          E-R 关系图                               |
+------------------------------------------------------------------+
|                                                                   |
|   +---------------+      1:N      +----------------+              |
|   | invoice_apply |-------------->|    invoice     |              |
|   +---------------+               +----------------+              |
|                                          |                        |
|                                          | 1:N                    |
|                                          v                        |
|                                   +----------------+              |
|                                   | invoice_item   |              |
|                                   +----------------+              |
|                                          |                        |
|   +---------------+                      | 1:N                    |
|   | invoice_title |                      v                        |
|   +---------------+              +-------------------+            |
|                                  | invoice_verify_log|            |
|                                  +-------------------+            |
|                                                                   |
|   +----------------+      N:M     +----------------+              |
|   | reimbursement  |<------------>|    invoice     |              |
|   +----------------+              +----------------+              |
|          |                                                        |
|          v                                                        |
|   +------------------------+                                      |
|   | reimbursement_invoice  |                                      |
|   +------------------------+                                      |
|                                                                   |
|   +-------------------+           +---------------------+         |
|   | invoice_ocr_log   |           | invoice_import_batch|         |
|   +-------------------+           +---------------------+         |
|                                                                   |
+------------------------------------------------------------------+

四、核心代码实现

4.1 基础实体类

/**
 * 发票类型枚举
 */
public enum InvoiceType {
    SPECIAL("SPECIAL", "增值税专用发票"),
    NORMAL("NORMAL", "增值税普通发票"),
    ELECTRONIC("ELECTRONIC", "增值税电子普通发票"),
    ELECTRONIC_SPECIAL("ELECTRONIC_SPECIAL", "增值税电子专用发票"),
    FULL_DIGITAL("FULL_DIGITAL", "全电发票"),
    ROLL("ROLL", "增值税普通发票(卷票)"),
    VEHICLE("VEHICLE", "机动车销售统一发票"),
    TOLL("TOLL", "通行费发票"),
    TRAIN("TRAIN", "火车票"),
    FLIGHT("FLIGHT", "航空运输电子客票行程单"),
    QUOTA("QUOTA", "定额发票");

    private final String code;
    private final String desc;

    InvoiceType(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() { return code; }
    public String getDesc() { return desc; }
}

/**
 * 发票状态枚举
 */
public enum InvoiceStatus {
    NORMAL(0, "正常"),
    RED_FLUSHED(1, "已冲红"),
    CANCELLED(2, "已作废");

    private final int code;
    private final String desc;

    InvoiceStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() { return code; }
    public String getDesc() { return desc; }
}

/**
 * 验真状态枚举
 */
public enum VerifyStatus {
    NOT_VERIFIED(0, "未验真"),
    VERIFIED_SUCCESS(1, "验真通过"),
    VERIFIED_FAILED(2, "验真失败");

    private final int code;
    private final String desc;

    VerifyStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() { return code; }
    public String getDesc() { return desc; }
}
/**
 * 发票实体类
 */
@Data
@TableName("invoice")
public class Invoice {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String invoiceCode;
    private String invoiceNo;
    private String invoiceType;
    private Integer invoiceStatus;
    private LocalDate invoiceDate;
    private String checkCode;
    private String machineCode;

    // 购买方信息
    private String buyerName;
    private String buyerTaxNo;
    private String buyerAddress;
    private String buyerBank;

    // 销售方信息
    private String sellerName;
    private String sellerTaxNo;
    private String sellerAddress;
    private String sellerBank;

    // 金额信息
    private BigDecimal amount;
    private BigDecimal taxAmount;
    private BigDecimal totalAmount;
    private String taxRate;

    // 其他信息
    private String remark;
    private String drawer;
    private String reviewer;
    private String payee;

    // 来源与附件
    private String sourceType;
    private String fileUrl;
    private String pdfUrl;
    private String ofdUrl;

    // 验真信息
    private Integer verifyStatus;
    private LocalDateTime verifyTime;
    private String verifyResult;

    // 业务关联
    private Long companyId;
    private Long deptId;
    private Long userId;
    private Long reimbursementId;
    private Integer reimbursementStatus;

    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Integer deleted;
}

/**
 * 发票明细实体
 */
@Data
@TableName("invoice_item")
public class InvoiceItem {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long invoiceId;
    private Integer lineNo;
    private String goodsName;
    private String specification;
    private String unit;
    private BigDecimal quantity;
    private BigDecimal unitPrice;
    private BigDecimal amount;
    private String taxRate;
    private BigDecimal taxAmount;
    private LocalDateTime createTime;
}

/**
 * 发票DTO
 */
@Data
public class InvoiceDTO {
    private String invoiceCode;
    private String invoiceNo;
    private String invoiceType;
    private LocalDate invoiceDate;
    private String checkCode;

    private String buyerName;
    private String buyerTaxNo;
    private String buyerAddress;
    private String buyerBank;

    private String sellerName;
    private String sellerTaxNo;
    private String sellerAddress;
    private String sellerBank;

    private BigDecimal amount;
    private BigDecimal taxAmount;
    private BigDecimal totalAmount;
    private String taxRate;

    private String remark;
    private String drawer;
    private String reviewer;
    private String payee;

    private List<InvoiceItemDTO> items;
}

/**
 * 发票明细DTO
 */
@Data
public class InvoiceItemDTO {
    private Integer lineNo;
    private String goodsName;
    private String specification;
    private String unit;
    private BigDecimal quantity;
    private BigDecimal unitPrice;
    private BigDecimal amount;
    private String taxRate;
    private BigDecimal taxAmount;
}

4.2 OCR 识别服务

/**
 * OCR识别服务接口
 */
public interface InvoiceOcrService {

    /**
     * 识别发票图片
     */
    OcrResult recognize(String imageUrl);

    /**
     * 识别发票图片(Base64)
     */
    OcrResult recognizeBase64(String base64Image);

    /**
     * 批量识别
     */
    List<OcrResult> batchRecognize(List<String> imageUrls);
}

/**
 * OCR识别结果
 */
@Data
public class OcrResult {
    private boolean success;
    private String message;
    private String invoiceType;
    private InvoiceDTO invoiceData;
    private String rawJson;
    private Long costTime;
}

/**
 * 百度OCR实现
 */
@Slf4j
@Service
public class BaiduOcrServiceImpl implements InvoiceOcrService {

    @Value("${baidu.ocr.app-id}")
    private String appId;

    @Value("${baidu.ocr.api-key}")
    private String apiKey;

    @Value("${baidu.ocr.secret-key}")
    private String secretKey;

    private AipOcr client;

    @PostConstruct
    public void init() {
        client = new AipOcr(appId, apiKey, secretKey);
        client.setConnectionTimeoutInMillis(5000);
        client.setSocketTimeoutInMillis(60000);
    }

    @Override
    public OcrResult recognize(String imageUrl) {
        long startTime = System.currentTimeMillis();
        OcrResult result = new OcrResult();

        try {
            // 调用百度OCR增值税发票识别接口
            HashMap<String, String> options = new HashMap<>();
            options.put("type", "auto");  // 自动识别发票类型

            JSONObject response = client.vatInvoice(imageUrl, options);
            log.info("百度OCR响应: {}", response.toString(2));

            result.setRawJson(response.toString());

            if (response.has("error_code")) {
                result.setSuccess(false);
                result.setMessage(response.optString("error_msg", "识别失败"));
                return result;
            }

            // 解析识别结果
            InvoiceDTO invoiceDTO = parseOcrResponse(response);
            result.setSuccess(true);
            result.setInvoiceData(invoiceDTO);
            result.setInvoiceType(invoiceDTO.getInvoiceType());

        } catch (Exception e) {
            log.error("OCR识别异常", e);
            result.setSuccess(false);
            result.setMessage("OCR识别异常: " + e.getMessage());
        }

        result.setCostTime(System.currentTimeMillis() - startTime);
        return result;
    }

    @Override
    public OcrResult recognizeBase64(String base64Image) {
        long startTime = System.currentTimeMillis();
        OcrResult result = new OcrResult();

        try {
            byte[] imageBytes = Base64.getDecoder().decode(base64Image);
            HashMap<String, String> options = new HashMap<>();

            JSONObject response = client.vatInvoice(imageBytes, options);
            result.setRawJson(response.toString());

            if (response.has("error_code")) {
                result.setSuccess(false);
                result.setMessage(response.optString("error_msg", "识别失败"));
                return result;
            }

            InvoiceDTO invoiceDTO = parseOcrResponse(response);
            result.setSuccess(true);
            result.setInvoiceData(invoiceDTO);
            result.setInvoiceType(invoiceDTO.getInvoiceType());

        } catch (Exception e) {
            log.error("OCR识别异常", e);
            result.setSuccess(false);
            result.setMessage("OCR识别异常: " + e.getMessage());
        }

        result.setCostTime(System.currentTimeMillis() - startTime);
        return result;
    }

    @Override
    public List<OcrResult> batchRecognize(List<String> imageUrls) {
        return imageUrls.parallelStream()
                .map(this::recognize)
                .collect(Collectors.toList());
    }

    /**
     * 解析OCR响应
     */
    private InvoiceDTO parseOcrResponse(JSONObject response) {
        InvoiceDTO dto = new InvoiceDTO();

        JSONObject wordsResult = response.optJSONObject("words_result");
        if (wordsResult == null) {
            return dto;
        }

        // 基本信息
        dto.setInvoiceCode(getFieldValue(wordsResult, "InvoiceCode"));
        dto.setInvoiceNo(getFieldValue(wordsResult, "InvoiceNum"));
        dto.setInvoiceType(parseInvoiceType(response.optString("InvoiceType")));

        String dateStr = getFieldValue(wordsResult, "InvoiceDate");
        if (StringUtils.isNotBlank(dateStr)) {
            dto.setInvoiceDate(parseDate(dateStr));
        }

        dto.setCheckCode(getFieldValue(wordsResult, "CheckCode"));

        // 购买方信息
        dto.setBuyerName(getFieldValue(wordsResult, "PurchaserName"));
        dto.setBuyerTaxNo(getFieldValue(wordsResult, "PurchaserRegisterNum"));
        dto.setBuyerAddress(getFieldValue(wordsResult, "PurchaserAddress"));
        dto.setBuyerBank(getFieldValue(wordsResult, "PurchaserBank"));

        // 销售方信息
        dto.setSellerName(getFieldValue(wordsResult, "SellerName"));
        dto.setSellerTaxNo(getFieldValue(wordsResult, "SellerRegisterNum"));
        dto.setSellerAddress(getFieldValue(wordsResult, "SellerAddress"));
        dto.setSellerBank(getFieldValue(wordsResult, "SellerBank"));

        // 金额信息
        dto.setAmount(parseBigDecimal(getFieldValue(wordsResult, "TotalAmount")));
        dto.setTaxAmount(parseBigDecimal(getFieldValue(wordsResult, "TotalTax")));
        dto.setTotalAmount(parseBigDecimal(getFieldValue(wordsResult, "AmountInFiguers")));

        // 其他信息
        dto.setRemark(getFieldValue(wordsResult, "Remarks"));
        dto.setDrawer(getFieldValue(wordsResult, "NoteDrawer"));
        dto.setReviewer(getFieldValue(wordsResult, "Checker"));
        dto.setPayee(getFieldValue(wordsResult, "Payee"));

        // 解析商品明细
        dto.setItems(parseInvoiceItems(wordsResult));

        return dto;
    }

    private String getFieldValue(JSONObject wordsResult, String key) {
        JSONObject field = wordsResult.optJSONObject(key);
        if (field != null) {
            return field.optString("word", "");
        }
        return "";
    }

    private List<InvoiceItemDTO> parseInvoiceItems(JSONObject wordsResult) {
        List<InvoiceItemDTO> items = new ArrayList<>();

        JSONArray commodityInfos = wordsResult.optJSONArray("CommodityName");
        if (commodityInfos == null) {
            return items;
        }

        for (int i = 0; i < commodityInfos.length(); i++) {
            JSONObject commodity = commodityInfos.optJSONObject(i);
            if (commodity == null) continue;

            InvoiceItemDTO item = new InvoiceItemDTO();
            item.setLineNo(i + 1);
            item.setGoodsName(commodity.optString("word", ""));

            // 其他字段需要从对应数组获取
            items.add(item);
        }

        return items;
    }

    private String parseInvoiceType(String typeCode) {
        // 根据百度OCR返回的类型码转换
        switch (typeCode) {
            case "vat_invoice":
                return InvoiceType.SPECIAL.getCode();
            case "normal_invoice":
                return InvoiceType.NORMAL.getCode();
            case "elec_normal_invoice":
                return InvoiceType.ELECTRONIC.getCode();
            default:
                return InvoiceType.NORMAL.getCode();
        }
    }

    private LocalDate parseDate(String dateStr) {
        try {
            // 处理各种日期格式
            dateStr = dateStr.replaceAll("[年月]", "-").replace("日", "");
            return LocalDate.parse(dateStr);
        } catch (Exception e) {
            return null;
        }
    }

    private BigDecimal parseBigDecimal(String value) {
        if (StringUtils.isBlank(value)) {
            return BigDecimal.ZERO;
        }
        try {
            value = value.replace("¥", "").replace("¥", "")
                    .replace(",", "").replace(" ", "");
            return new BigDecimal(value);
        } catch (Exception e) {
            return BigDecimal.ZERO;
        }
    }
}
/**
 * 阿里云OCR实现
 */
@Slf4j
@Service
public class AliyunOcrServiceImpl implements InvoiceOcrService {

    @Value("${aliyun.ocr.access-key-id}")
    private String accessKeyId;

    @Value("${aliyun.ocr.access-key-secret}")
    private String accessKeySecret;

    @Value("${aliyun.ocr.endpoint}")
    private String endpoint;

    private Client client;

    @PostConstruct
    public void init() throws Exception {
        Config config = new Config()
                .setAccessKeyId(accessKeyId)
                .setAccessKeySecret(accessKeySecret)
                .setEndpoint(endpoint);
        client = new Client(config);
    }

    @Override
    public OcrResult recognize(String imageUrl) {
        long startTime = System.currentTimeMillis();
        OcrResult result = new OcrResult();

        try {
            RecognizeInvoiceRequest request = new RecognizeInvoiceRequest();
            request.setUrl(imageUrl);

            RecognizeInvoiceResponse response = client.recognizeInvoice(request);
            RecognizeInvoiceResponseBody body = response.getBody();

            result.setRawJson(JSON.toJSONString(body));

            if (body.getCode() != null && !"200".equals(body.getCode())) {
                result.setSuccess(false);
                result.setMessage(body.getMessage());
                return result;
            }

            // 解析识别结果
            InvoiceDTO invoiceDTO = parseAliyunResponse(body.getData());
            result.setSuccess(true);
            result.setInvoiceData(invoiceDTO);
            result.setInvoiceType(invoiceDTO.getInvoiceType());

        } catch (Exception e) {
            log.error("阿里云OCR识别异常", e);
            result.setSuccess(false);
            result.setMessage("OCR识别异常: " + e.getMessage());
        }

        result.setCostTime(System.currentTimeMillis() - startTime);
        return result;
    }

    @Override
    public OcrResult recognizeBase64(String base64Image) {
        // 阿里云OCR支持Base64
        long startTime = System.currentTimeMillis();
        OcrResult result = new OcrResult();

        try {
            RecognizeInvoiceRequest request = new RecognizeInvoiceRequest();
            request.setBody(new ByteArrayInputStream(
                    Base64.getDecoder().decode(base64Image)));

            RecognizeInvoiceResponse response = client.recognizeInvoice(request);
            // ... 处理响应

        } catch (Exception e) {
            log.error("阿里云OCR识别异常", e);
            result.setSuccess(false);
            result.setMessage("OCR识别异常: " + e.getMessage());
        }

        result.setCostTime(System.currentTimeMillis() - startTime);
        return result;
    }

    @Override
    public List<OcrResult> batchRecognize(List<String> imageUrls) {
        return imageUrls.parallelStream()
                .map(this::recognize)
                .collect(Collectors.toList());
    }

    private InvoiceDTO parseAliyunResponse(String data) {
        // 解析阿里云OCR响应数据
        JSONObject jsonData = JSON.parseObject(data);
        InvoiceDTO dto = new InvoiceDTO();

        // 根据阿里云返回格式解析
        dto.setInvoiceCode(jsonData.getString("invoiceCode"));
        dto.setInvoiceNo(jsonData.getString("invoiceNo"));
        // ... 其他字段

        return dto;
    }
}

/**
 * OCR服务门面(策略模式)
 */
@Slf4j
@Service
public class OcrServiceFacade {

    @Autowired
    private BaiduOcrServiceImpl baiduOcrService;

    @Autowired
    private AliyunOcrServiceImpl aliyunOcrService;

    @Value("${ocr.provider:baidu}")
    private String ocrProvider;

    /**
     * 识别发票
     */
    public OcrResult recognize(String imageUrl) {
        InvoiceOcrService ocrService = getOcrService();
        return ocrService.recognize(imageUrl);
    }

    /**
     * 识别发票(Base64)
     */
    public OcrResult recognizeBase64(String base64Image) {
        InvoiceOcrService ocrService = getOcrService();
        return ocrService.recognizeBase64(base64Image);
    }

    /**
     * 识别失败时自动切换备用OCR
     */
    public OcrResult recognizeWithFallback(String imageUrl) {
        // 先使用主OCR
        OcrResult result = getOcrService().recognize(imageUrl);

        // 失败时使用备用OCR
        if (!result.isSuccess()) {
            log.warn("主OCR识别失败,尝试备用OCR");
            InvoiceOcrService fallbackService = getFallbackOcrService();
            result = fallbackService.recognize(imageUrl);
        }

        return result;
    }

    private InvoiceOcrService getOcrService() {
        if ("aliyun".equalsIgnoreCase(ocrProvider)) {
            return aliyunOcrService;
        }
        return baiduOcrService;
    }

    private InvoiceOcrService getFallbackOcrService() {
        if ("aliyun".equalsIgnoreCase(ocrProvider)) {
            return baiduOcrService;
        }
        return aliyunOcrService;
    }
}

4.3 发票验真服务

/**
 * 发票验真服务接口
 */
public interface InvoiceVerifyService {

    /**
     * 验真发票
     */
    VerifyResult verify(InvoiceVerifyRequest request);

    /**
     * 批量验真
     */
    List<VerifyResult> batchVerify(List<InvoiceVerifyRequest> requests);
}

/**
 * 验真请求
 */
@Data
public class InvoiceVerifyRequest {
    private String invoiceCode;   // 发票代码
    private String invoiceNo;     // 发票号码
    private String invoiceDate;   // 开票日期 yyyy-MM-dd
    private String checkCode;     // 校验码后6位(普票必填)
    private String amount;        // 不含税金额(专票必填)
    private String invoiceType;   // 发票类型
}

/**
 * 验真结果
 */
@Data
public class VerifyResult {
    private boolean success;          // 是否成功
    private String message;           // 消息
    private boolean valid;            // 发票是否有效
    private String invoiceStatus;     // 发票状态
    private InvoiceDTO invoiceData;   // 发票详情(验真成功时返回)
    private String rawResponse;       // 原始响应
}

/**
 * 税务局验真服务实现
 */
@Slf4j
@Service
public class TaxVerifyServiceImpl implements InvoiceVerifyService {

    @Value("${tax.verify.api-url}")
    private String apiUrl;

    @Value("${tax.verify.app-key}")
    private String appKey;

    @Value("${tax.verify.app-secret}")
    private String appSecret;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public VerifyResult verify(InvoiceVerifyRequest request) {
        VerifyResult result = new VerifyResult();

        try {
            // 构建请求参数
            Map<String, Object> params = buildRequestParams(request);

            // 生成签名
            String sign = generateSign(params);
            params.put("sign", sign);

            // 调用验真接口
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(params, headers);

            ResponseEntity<String> response = restTemplate.exchange(
                    apiUrl, HttpMethod.POST, entity, String.class);

            result.setRawResponse(response.getBody());

            // 解析响应
            JSONObject jsonResponse = JSON.parseObject(response.getBody());
            String code = jsonResponse.getString("code");

            if ("0000".equals(code)) {
                result.setSuccess(true);
                result.setValid(true);

                // 解析发票详情
                JSONObject data = jsonResponse.getJSONObject("data");
                InvoiceDTO invoiceDTO = parseVerifyResponse(data);
                result.setInvoiceData(invoiceDTO);

                // 检查发票状态
                String status = data.getString("invoiceStatus");
                result.setInvoiceStatus(status);

                if (!"正常".equals(status)) {
                    result.setMessage("发票状态异常: " + status);
                }

            } else {
                result.setSuccess(false);
                result.setValid(false);
                result.setMessage(jsonResponse.getString("message"));
            }

        } catch (Exception e) {
            log.error("发票验真异常", e);
            result.setSuccess(false);
            result.setMessage("验真服务异常: " + e.getMessage());
        }

        return result;
    }

    @Override
    public List<VerifyResult> batchVerify(List<InvoiceVerifyRequest> requests) {
        // 限流:每秒最多10次
        return requests.stream()
                .map(this::verifyWithRateLimit)
                .collect(Collectors.toList());
    }

    private VerifyResult verifyWithRateLimit(InvoiceVerifyRequest request) {
        try {
            Thread.sleep(100);  // 简单限流
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return verify(request);
    }

    private Map<String, Object> buildRequestParams(InvoiceVerifyRequest request) {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("appKey", appKey);
        params.put("timestamp", System.currentTimeMillis());
        params.put("invoiceCode", request.getInvoiceCode());
        params.put("invoiceNo", request.getInvoiceNo());
        params.put("invoiceDate", request.getInvoiceDate());

        if (StringUtils.isNotBlank(request.getCheckCode())) {
            params.put("checkCode", request.getCheckCode());
        }
        if (StringUtils.isNotBlank(request.getAmount())) {
            params.put("amount", request.getAmount());
        }

        return params;
    }

    private String generateSign(Map<String, Object> params) {
        // 按key排序拼接
        StringBuilder sb = new StringBuilder();
        params.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> sb.append(entry.getKey())
                        .append("=")
                        .append(entry.getValue())
                        .append("&"));

        sb.append("appSecret=").append(appSecret);

        // MD5签名
        return DigestUtils.md5Hex(sb.toString()).toUpperCase();
    }

    private InvoiceDTO parseVerifyResponse(JSONObject data) {
        InvoiceDTO dto = new InvoiceDTO();
        dto.setInvoiceCode(data.getString("invoiceCode"));
        dto.setInvoiceNo(data.getString("invoiceNo"));
        dto.setBuyerName(data.getString("buyerName"));
        dto.setBuyerTaxNo(data.getString("buyerTaxNo"));
        dto.setSellerName(data.getString("sellerName"));
        dto.setSellerTaxNo(data.getString("sellerTaxNo"));
        dto.setAmount(data.getBigDecimal("amount"));
        dto.setTaxAmount(data.getBigDecimal("taxAmount"));
        dto.setTotalAmount(data.getBigDecimal("totalAmount"));
        // ... 其他字段
        return dto;
    }
}

4.4 发票导入服务

/**
 * 发票导入服务
 */
@Slf4j
@Service
public class InvoiceImportService {

    @Autowired
    private InvoiceMapper invoiceMapper;

    @Autowired
    private InvoiceImportBatchMapper batchMapper;

    @Autowired
    private OcrServiceFacade ocrService;

    @Autowired
    private InvoiceVerifyService verifyService;

    @Autowired
    private OssService ossService;

    /**
     * Excel导入发票
     */
    @Async
    public void importFromExcel(Long batchId, InputStream inputStream) {
        InvoiceImportBatch batch = batchMapper.selectById(batchId);
        batch.setStatus(1);  // 处理中
        batch.setStartTime(LocalDateTime.now());
        batchMapper.updateById(batch);

        List<InvoiceImportResult> results = new ArrayList<>();
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger failCount = new AtomicInteger(0);

        try {
            EasyExcel.read(inputStream, InvoiceExcelDTO.class, new ReadListener<InvoiceExcelDTO>() {
                @Override
                public void invoke(InvoiceExcelDTO data, AnalysisContext context) {
                    InvoiceImportResult result = processExcelRow(data);
                    results.add(result);

                    if (result.isSuccess()) {
                        successCount.incrementAndGet();
                    } else {
                        failCount.incrementAndGet();
                    }
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    log.info("Excel解析完成,共{}条", results.size());
                }
            }).sheet().doRead();

            // 更新批次状态
            batch.setStatus(2);  // 已完成
            batch.setTotalCount(results.size());
            batch.setSuccessCount(successCount.get());
            batch.setFailCount(failCount.get());

            // 如果有失败记录,生成错误文件
            if (failCount.get() > 0) {
                String errorFileUrl = generateErrorFile(results);
                batch.setErrorFileUrl(errorFileUrl);
            }

        } catch (Exception e) {
            log.error("Excel导入异常", e);
            batch.setStatus(3);  // 失败
        }

        batch.setEndTime(LocalDateTime.now());
        batchMapper.updateById(batch);
    }

    /**
     * 图片批量导入(OCR识别)
     */
    @Async
    public void importFromImages(Long batchId, List<String> imageUrls) {
        InvoiceImportBatch batch = batchMapper.selectById(batchId);
        batch.setStatus(1);
        batch.setStartTime(LocalDateTime.now());
        batch.setTotalCount(imageUrls.size());
        batchMapper.updateById(batch);

        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger failCount = new AtomicInteger(0);

        for (String imageUrl : imageUrls) {
            try {
                // OCR识别
                OcrResult ocrResult = ocrService.recognizeWithFallback(imageUrl);

                if (ocrResult.isSuccess()) {
                    // 保存发票
                    Invoice invoice = convertToInvoice(ocrResult.getInvoiceData());
                    invoice.setSourceType("OCR");
                    invoice.setFileUrl(imageUrl);

                    // 查重
                    if (checkDuplicate(invoice.getInvoiceCode(), invoice.getInvoiceNo())) {
                        log.warn("发票已存在: {}-{}", invoice.getInvoiceCode(), invoice.getInvoiceNo());
                        failCount.incrementAndGet();
                        continue;
                    }

                    invoiceMapper.insert(invoice);
                    successCount.incrementAndGet();

                    // 记录OCR日志
                    saveOcrLog(batchId, imageUrl, ocrResult, invoice.getId());
                } else {
                    failCount.incrementAndGet();
                    saveOcrLog(batchId, imageUrl, ocrResult, null);
                }

            } catch (Exception e) {
                log.error("处理图片失败: {}", imageUrl, e);
                failCount.incrementAndGet();
            }
        }

        // 更新批次状态
        batch.setStatus(2);
        batch.setSuccessCount(successCount.get());
        batch.setFailCount(failCount.get());
        batch.setEndTime(LocalDateTime.now());
        batchMapper.updateById(batch);
    }

    /**
     * 邮件发票导入
     */
    @Async
    public void importFromEmail(Long batchId, String emailContent, List<String> attachmentUrls) {
        InvoiceImportBatch batch = batchMapper.selectById(batchId);
        batch.setStatus(1);
        batch.setStartTime(LocalDateTime.now());
        batchMapper.updateById(batch);

        int successCount = 0;
        int failCount = 0;

        for (String attachmentUrl : attachmentUrls) {
            try {
                String fileType = getFileType(attachmentUrl);

                if ("PDF".equals(fileType) || "OFD".equals(fileType)) {
                    // PDF/OFD 文件直接解析
                    InvoiceDTO invoiceDTO = parseElectronicInvoice(attachmentUrl, fileType);
                    if (invoiceDTO != null) {
                        Invoice invoice = convertToInvoice(invoiceDTO);
                        invoice.setSourceType("EMAIL");
                        invoice.setPdfUrl("PDF".equals(fileType) ? attachmentUrl : null);
                        invoice.setOfdUrl("OFD".equals(fileType) ? attachmentUrl : null);

                        if (!checkDuplicate(invoice.getInvoiceCode(), invoice.getInvoiceNo())) {
                            invoiceMapper.insert(invoice);
                            successCount++;
                        }
                    }
                } else if ("IMAGE".equals(fileType)) {
                    // 图片用OCR识别
                    OcrResult ocrResult = ocrService.recognize(attachmentUrl);
                    if (ocrResult.isSuccess()) {
                        Invoice invoice = convertToInvoice(ocrResult.getInvoiceData());
                        invoice.setSourceType("EMAIL");
                        invoice.setFileUrl(attachmentUrl);

                        if (!checkDuplicate(invoice.getInvoiceCode(), invoice.getInvoiceNo())) {
                            invoiceMapper.insert(invoice);
                            successCount++;
                        }
                    } else {
                        failCount++;
                    }
                }

            } catch (Exception e) {
                log.error("处理邮件附件失败: {}", attachmentUrl, e);
                failCount++;
            }
        }

        batch.setStatus(2);
        batch.setTotalCount(attachmentUrls.size());
        batch.setSuccessCount(successCount);
        batch.setFailCount(failCount);
        batch.setEndTime(LocalDateTime.now());
        batchMapper.updateById(batch);
    }

    /**
     * 处理Excel行数据
     */
    private InvoiceImportResult processExcelRow(InvoiceExcelDTO data) {
        InvoiceImportResult result = new InvoiceImportResult();
        result.setRowData(data);

        try {
            // 数据校验
            List<String> errors = validateExcelData(data);
            if (!errors.isEmpty()) {
                result.setSuccess(false);
                result.setErrorMessage(String.join("; ", errors));
                return result;
            }

            // 查重
            if (checkDuplicate(data.getInvoiceCode(), data.getInvoiceNo())) {
                result.setSuccess(false);
                result.setErrorMessage("发票已存在");
                return result;
            }

            // 转换并保存
            Invoice invoice = convertExcelToInvoice(data);
            invoice.setSourceType("IMPORT");
            invoiceMapper.insert(invoice);

            result.setSuccess(true);
            result.setInvoiceId(invoice.getId());

        } catch (Exception e) {
            log.error("处理Excel行数据失败", e);
            result.setSuccess(false);
            result.setErrorMessage("处理失败: " + e.getMessage());
        }

        return result;
    }

    /**
     * 校验Excel数据
     */
    private List<String> validateExcelData(InvoiceExcelDTO data) {
        List<String> errors = new ArrayList<>();

        if (StringUtils.isBlank(data.getInvoiceCode())) {
            errors.add("发票代码不能为空");
        }
        if (StringUtils.isBlank(data.getInvoiceNo())) {
            errors.add("发票号码不能为空");
        }
        if (data.getInvoiceDate() == null) {
            errors.add("开票日期不能为空");
        }
        if (data.getTotalAmount() == null || data.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
            errors.add("金额无效");
        }

        return errors;
    }

    /**
     * 查重
     */
    private boolean checkDuplicate(String invoiceCode, String invoiceNo) {
        return invoiceMapper.selectByCodeAndNo(invoiceCode, invoiceNo) != null;
    }

    private Invoice convertToInvoice(InvoiceDTO dto) {
        Invoice invoice = new Invoice();
        BeanUtils.copyProperties(dto, invoice);
        invoice.setInvoiceStatus(InvoiceStatus.NORMAL.getCode());
        invoice.setVerifyStatus(VerifyStatus.NOT_VERIFIED.getCode());
        invoice.setReimbursementStatus(0);
        return invoice;
    }

    private Invoice convertExcelToInvoice(InvoiceExcelDTO dto) {
        Invoice invoice = new Invoice();
        invoice.setInvoiceCode(dto.getInvoiceCode());
        invoice.setInvoiceNo(dto.getInvoiceNo());
        invoice.setInvoiceType(dto.getInvoiceType());
        invoice.setInvoiceDate(dto.getInvoiceDate());
        invoice.setCheckCode(dto.getCheckCode());
        invoice.setBuyerName(dto.getBuyerName());
        invoice.setBuyerTaxNo(dto.getBuyerTaxNo());
        invoice.setSellerName(dto.getSellerName());
        invoice.setSellerTaxNo(dto.getSellerTaxNo());
        invoice.setAmount(dto.getAmount());
        invoice.setTaxAmount(dto.getTaxAmount());
        invoice.setTotalAmount(dto.getTotalAmount());
        invoice.setInvoiceStatus(InvoiceStatus.NORMAL.getCode());
        invoice.setVerifyStatus(VerifyStatus.NOT_VERIFIED.getCode());
        return invoice;
    }

    private void saveOcrLog(Long batchId, String imageUrl, OcrResult result, Long invoiceId) {
        // 保存OCR识别日志
    }

    private String generateErrorFile(List<InvoiceImportResult> results) {
        // 生成错误文件并上传OSS
        return null;
    }

    private String getFileType(String url) {
        String lowerUrl = url.toLowerCase();
        if (lowerUrl.endsWith(".pdf")) return "PDF";
        if (lowerUrl.endsWith(".ofd")) return "OFD";
        if (lowerUrl.matches(".*\\.(jpg|jpeg|png|gif|bmp)$")) return "IMAGE";
        return "UNKNOWN";
    }

    private InvoiceDTO parseElectronicInvoice(String fileUrl, String fileType) {
        // 解析电子发票文件
        return null;
    }

    /**
     * Excel导入DTO
     */
    @Data
    public static class InvoiceExcelDTO {
        @ExcelProperty("发票代码")
        private String invoiceCode;

        @ExcelProperty("发票号码")
        private String invoiceNo;

        @ExcelProperty("发票类型")
        private String invoiceType;

        @ExcelProperty("开票日期")
        private LocalDate invoiceDate;

        @ExcelProperty("校验码")
        private String checkCode;

        @ExcelProperty("购买方名称")
        private String buyerName;

        @ExcelProperty("购买方税号")
        private String buyerTaxNo;

        @ExcelProperty("销售方名称")
        private String sellerName;

        @ExcelProperty("销售方税号")
        private String sellerTaxNo;

        @ExcelProperty("不含税金额")
        private BigDecimal amount;

        @ExcelProperty("税额")
        private BigDecimal taxAmount;

        @ExcelProperty("价税合计")
        private BigDecimal totalAmount;
    }

    @Data
    public static class InvoiceImportResult {
        private InvoiceExcelDTO rowData;
        private boolean success;
        private String errorMessage;
        private Long invoiceId;
    }
}

4.5 开票服务

/**
 * 开票服务
 */
@Slf4j
@Service
public class InvoiceIssueService {

    @Autowired
    private InvoiceApplyMapper applyMapper;

    @Autowired
    private InvoiceMapper invoiceMapper;

    @Autowired
    private InvoiceItemMapper itemMapper;

    @Autowired
    private TaxControlService taxControlService;

    @Autowired
    private NotificationService notificationService;

    /**
     * 创建开票申请
     */
    @Transactional
    public InvoiceApply createApply(InvoiceApplyDTO dto, Long userId) {
        // 校验购买方信息
        validateBuyerInfo(dto);

        InvoiceApply apply = new InvoiceApply();
        apply.setApplyNo(generateApplyNo());
        apply.setApplyType(1);  // 开票
        apply.setApplyStatus(0);  // 待审批

        // 设置发票信息
        apply.setInvoiceType(dto.getInvoiceType());
        apply.setTitleType(dto.getTitleType());
        apply.setBuyerName(dto.getBuyerName());
        apply.setBuyerTaxNo(dto.getBuyerTaxNo());
        apply.setBuyerAddress(dto.getBuyerAddress());
        apply.setBuyerBank(dto.getBuyerBank());
        apply.setBuyerEmail(dto.getBuyerEmail());
        apply.setBuyerPhone(dto.getBuyerPhone());

        // 设置商品信息
        apply.setGoodsName(dto.getGoodsName());
        apply.setTaxCode(dto.getTaxCode());
        apply.setAmount(dto.getAmount());
        apply.setTaxRate(dto.getTaxRate());

        // 计算税额
        BigDecimal taxRate = new BigDecimal(dto.getTaxRate().replace("%", ""))
                .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        BigDecimal taxAmount = dto.getAmount().multiply(taxRate)
                .setScale(2, RoundingMode.HALF_UP);
        apply.setTaxAmount(taxAmount);
        apply.setTotalAmount(dto.getAmount().add(taxAmount));

        apply.setOrderNo(dto.getOrderNo());
        apply.setContractNo(dto.getContractNo());
        apply.setRemark(dto.getRemark());

        // 申请人信息
        apply.setApplyUserId(userId);
        apply.setApplyTime(LocalDateTime.now());

        applyMapper.insert(apply);

        return apply;
    }

    /**
     * 审批开票申请
     */
    @Transactional
    public void approveApply(Long applyId, boolean approved, String remark, Long approveUserId) {
        InvoiceApply apply = applyMapper.selectById(applyId);
        if (apply == null) {
            throw new RuntimeException("申请不存在");
        }

        if (apply.getApplyStatus() != 0 && apply.getApplyStatus() != 1) {
            throw new RuntimeException("申请状态不允许审批");
        }

        apply.setApproveUserId(approveUserId);
        apply.setApproveTime(LocalDateTime.now());
        apply.setApproveRemark(remark);

        if (approved) {
            apply.setApplyStatus(2);  // 已通过
            // 自动开票
            issueInvoice(apply);
        } else {
            apply.setApplyStatus(3);  // 已拒绝
        }

        applyMapper.updateById(apply);
    }

    /**
     * 执行开票
     */
    @Transactional
    public Invoice issueInvoice(InvoiceApply apply) {
        log.info("开始开票,申请编号: {}", apply.getApplyNo());

        try {
            // 1. 调用税控系统开票
            TaxInvoiceRequest taxRequest = buildTaxRequest(apply);
            TaxInvoiceResponse taxResponse = taxControlService.issueInvoice(taxRequest);

            if (!taxResponse.isSuccess()) {
                throw new RuntimeException("税控开票失败: " + taxResponse.getMessage());
            }

            // 2. 保存发票信息
            Invoice invoice = new Invoice();
            invoice.setInvoiceCode(taxResponse.getInvoiceCode());
            invoice.setInvoiceNo(taxResponse.getInvoiceNo());
            invoice.setInvoiceType(apply.getInvoiceType());
            invoice.setInvoiceStatus(InvoiceStatus.NORMAL.getCode());
            invoice.setInvoiceDate(LocalDate.now());
            invoice.setCheckCode(taxResponse.getCheckCode());

            invoice.setBuyerName(apply.getBuyerName());
            invoice.setBuyerTaxNo(apply.getBuyerTaxNo());
            invoice.setBuyerAddress(apply.getBuyerAddress());
            invoice.setBuyerBank(apply.getBuyerBank());

            // 销售方信息从配置获取
            invoice.setSellerName(getSellerName());
            invoice.setSellerTaxNo(getSellerTaxNo());
            invoice.setSellerAddress(getSellerAddress());
            invoice.setSellerBank(getSellerBank());

            invoice.setAmount(apply.getAmount());
            invoice.setTaxAmount(apply.getTaxAmount());
            invoice.setTotalAmount(apply.getTotalAmount());
            invoice.setTaxRate(apply.getTaxRate());

            invoice.setRemark(apply.getRemark());
            invoice.setSourceType("ISSUE");
            invoice.setPdfUrl(taxResponse.getPdfUrl());
            invoice.setVerifyStatus(VerifyStatus.VERIFIED_SUCCESS.getCode());

            invoiceMapper.insert(invoice);

            // 3. 保存发票明细
            InvoiceItem item = new InvoiceItem();
            item.setInvoiceId(invoice.getId());
            item.setLineNo(1);
            item.setGoodsName(apply.getGoodsName());
            item.setAmount(apply.getAmount());
            item.setTaxRate(apply.getTaxRate());
            item.setTaxAmount(apply.getTaxAmount());
            itemMapper.insert(item);

            // 4. 更新申请状态
            apply.setApplyStatus(4);  // 已开票
            apply.setInvoiceId(invoice.getId());
            apply.setInvoiceCode(invoice.getInvoiceCode());
            apply.setInvoiceNo(invoice.getInvoiceNo());
            apply.setInvoiceTime(LocalDateTime.now());
            applyMapper.updateById(apply);

            // 5. 发送发票通知
            sendInvoiceNotification(apply, invoice);

            log.info("开票成功,发票代码: {}, 发票号码: {}", invoice.getInvoiceCode(), invoice.getInvoiceNo());
            return invoice;

        } catch (Exception e) {
            log.error("开票失败", e);
            throw new RuntimeException("开票失败: " + e.getMessage());
        }
    }

    /**
     * 发票冲红
     */
    @Transactional
    public Invoice redFlush(Long invoiceId, String reason) {
        Invoice originalInvoice = invoiceMapper.selectById(invoiceId);
        if (originalInvoice == null) {
            throw new RuntimeException("原发票不存在");
        }

        if (originalInvoice.getInvoiceStatus() != InvoiceStatus.NORMAL.getCode()) {
            throw new RuntimeException("发票状态不允许冲红");
        }

        // 调用税控系统开红票
        TaxRedFlushRequest request = new TaxRedFlushRequest();
        request.setOriginalInvoiceCode(originalInvoice.getInvoiceCode());
        request.setOriginalInvoiceNo(originalInvoice.getInvoiceNo());
        request.setReason(reason);

        TaxInvoiceResponse response = taxControlService.redFlush(request);
        if (!response.isSuccess()) {
            throw new RuntimeException("冲红失败: " + response.getMessage());
        }

        // 更新原发票状态
        originalInvoice.setInvoiceStatus(InvoiceStatus.RED_FLUSHED.getCode());
        invoiceMapper.updateById(originalInvoice);

        // 保存红字发票
        Invoice redInvoice = new Invoice();
        BeanUtils.copyProperties(originalInvoice, redInvoice);
        redInvoice.setId(null);
        redInvoice.setInvoiceCode(response.getInvoiceCode());
        redInvoice.setInvoiceNo(response.getInvoiceNo());
        redInvoice.setAmount(originalInvoice.getAmount().negate());
        redInvoice.setTaxAmount(originalInvoice.getTaxAmount().negate());
        redInvoice.setTotalAmount(originalInvoice.getTotalAmount().negate());
        redInvoice.setRemark("冲红原因: " + reason);
        redInvoice.setInvoiceDate(LocalDate.now());

        invoiceMapper.insert(redInvoice);

        return redInvoice;
    }

    private void validateBuyerInfo(InvoiceApplyDTO dto) {
        if (dto.getTitleType() == 1) {  // 企业
            if (StringUtils.isBlank(dto.getBuyerTaxNo())) {
                throw new RuntimeException("企业抬头必须填写纳税人识别号");
            }
        }
    }

    private String generateApplyNo() {
        return "INV" + System.currentTimeMillis() +
                String.format("%04d", new Random().nextInt(10000));
    }

    private TaxInvoiceRequest buildTaxRequest(InvoiceApply apply) {
        TaxInvoiceRequest request = new TaxInvoiceRequest();
        // 构建税控请求
        return request;
    }

    private void sendInvoiceNotification(InvoiceApply apply, Invoice invoice) {
        // 发送邮件
        if (StringUtils.isNotBlank(apply.getBuyerEmail())) {
            notificationService.sendInvoiceEmail(
                    apply.getBuyerEmail(),
                    invoice.getInvoiceCode(),
                    invoice.getInvoiceNo(),
                    invoice.getPdfUrl()
            );
        }

        // 发送短信
        if (StringUtils.isNotBlank(apply.getBuyerPhone())) {
            notificationService.sendInvoiceSms(
                    apply.getBuyerPhone(),
                    invoice.getInvoiceCode(),
                    invoice.getInvoiceNo()
            );
        }
    }

    private String getSellerName() {
        return "XX科技有限公司";
    }

    private String getSellerTaxNo() {
        return "91110000MA12345678";
    }

    private String getSellerAddress() {
        return "北京市朝阳区XX路XX号";
    }

    private String getSellerBank() {
        return "工商银行北京分行 1234567890";
    }
}

/**
 * 开票申请DTO
 */
@Data
public class InvoiceApplyDTO {
    private String invoiceType;
    private Integer titleType;
    private String buyerName;
    private String buyerTaxNo;
    private String buyerAddress;
    private String buyerBank;
    private String buyerEmail;
    private String buyerPhone;
    private String goodsName;
    private String taxCode;
    private BigDecimal amount;
    private String taxRate;
    private String orderNo;
    private String contractNo;
    private String remark;
}

4.6 报销服务

/**
 * 报销服务
 */
@Slf4j
@Service
public class ReimbursementService {

    @Autowired
    private ReimbursementMapper reimbursementMapper;

    @Autowired
    private ReimbursementInvoiceMapper reimbursementInvoiceMapper;

    @Autowired
    private InvoiceMapper invoiceMapper;

    @Autowired
    private InvoiceVerifyService verifyService;

    /**
     * 创建报销单
     */
    @Transactional
    public Reimbursement createReimbursement(ReimbursementDTO dto, Long userId) {
        // 校验发票
        List<Invoice> invoices = validateAndGetInvoices(dto.getInvoiceIds());

        // 计算总金额
        BigDecimal totalAmount = invoices.stream()
                .map(Invoice::getTotalAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // 创建报销单
        Reimbursement reimbursement = new Reimbursement();
        reimbursement.setReimburseNo(generateReimburseNo());
        reimbursement.setReimburseType(dto.getReimburseType());
        reimbursement.setReimburseStatus(0);  // 草稿
        reimbursement.setTitle(dto.getTitle());
        reimbursement.setTotalAmount(totalAmount);
        reimbursement.setInvoiceCount(invoices.size());
        reimbursement.setApplyUserId(userId);
        reimbursement.setRemark(dto.getRemark());

        if (dto.getAttachmentUrls() != null) {
            reimbursement.setAttachmentUrls(JSON.toJSONString(dto.getAttachmentUrls()));
        }

        reimbursementMapper.insert(reimbursement);

        // 关联发票
        for (Long invoiceId : dto.getInvoiceIds()) {
            ReimbursementInvoice ri = new ReimbursementInvoice();
            ri.setReimbursementId(reimbursement.getId());
            ri.setInvoiceId(invoiceId);
            reimbursementInvoiceMapper.insert(ri);

            // 更新发票报销状态
            Invoice invoice = invoiceMapper.selectById(invoiceId);
            invoice.setReimbursementId(reimbursement.getId());
            invoice.setReimbursementStatus(1);  // 报销中
            invoiceMapper.updateById(invoice);
        }

        return reimbursement;
    }

    /**
     * 提交报销单
     */
    @Transactional
    public void submitReimbursement(Long reimbursementId, Long userId) {
        Reimbursement reimbursement = reimbursementMapper.selectById(reimbursementId);
        if (reimbursement == null) {
            throw new RuntimeException("报销单不存在");
        }

        if (reimbursement.getReimburseStatus() != 0) {
            throw new RuntimeException("报销单状态不允许提交");
        }

        // 验真所有关联发票
        List<Long> invoiceIds = reimbursementInvoiceMapper.selectInvoiceIds(reimbursementId);
        for (Long invoiceId : invoiceIds) {
            Invoice invoice = invoiceMapper.selectById(invoiceId);
            if (invoice.getVerifyStatus() != VerifyStatus.VERIFIED_SUCCESS.getCode()) {
                // 执行验真
                InvoiceVerifyRequest request = buildVerifyRequest(invoice);
                VerifyResult result = verifyService.verify(request);

                invoice.setVerifyStatus(result.isValid()
                        ? VerifyStatus.VERIFIED_SUCCESS.getCode()
                        : VerifyStatus.VERIFIED_FAILED.getCode());
                invoice.setVerifyTime(LocalDateTime.now());
                invoice.setVerifyResult(result.getRawResponse());
                invoiceMapper.updateById(invoice);

                if (!result.isValid()) {
                    throw new RuntimeException("发票验真失败: " + invoice.getInvoiceNo());
                }
            }
        }

        // 更新报销单状态
        reimbursement.setReimburseStatus(1);  // 待审批
        reimbursement.setApplyTime(LocalDateTime.now());
        reimbursementMapper.updateById(reimbursement);
    }

    /**
     * 审批报销单
     */
    @Transactional
    public void approveReimbursement(Long reimbursementId, boolean approved,
                                      String remark, Long approveUserId) {
        Reimbursement reimbursement = reimbursementMapper.selectById(reimbursementId);
        if (reimbursement == null) {
            throw new RuntimeException("报销单不存在");
        }

        if (approved) {
            reimbursement.setReimburseStatus(3);  // 已通过

            // 更新发票报销状态
            List<Long> invoiceIds = reimbursementInvoiceMapper.selectInvoiceIds(reimbursementId);
            for (Long invoiceId : invoiceIds) {
                Invoice invoice = invoiceMapper.selectById(invoiceId);
                invoice.setReimbursementStatus(2);  // 已报销
                invoiceMapper.updateById(invoice);
            }
        } else {
            reimbursement.setReimburseStatus(4);  // 已拒绝

            // 释放发票
            List<Long> invoiceIds = reimbursementInvoiceMapper.selectInvoiceIds(reimbursementId);
            for (Long invoiceId : invoiceIds) {
                Invoice invoice = invoiceMapper.selectById(invoiceId);
                invoice.setReimbursementId(null);
                invoice.setReimbursementStatus(0);  // 未报销
                invoiceMapper.updateById(invoice);
            }
        }

        reimbursementMapper.updateById(reimbursement);
    }

    /**
     * 校验并获取发票
     */
    private List<Invoice> validateAndGetInvoices(List<Long> invoiceIds) {
        List<Invoice> invoices = new ArrayList<>();

        for (Long invoiceId : invoiceIds) {
            Invoice invoice = invoiceMapper.selectById(invoiceId);
            if (invoice == null) {
                throw new RuntimeException("发票不存在: " + invoiceId);
            }

            // 检查发票状态
            if (invoice.getInvoiceStatus() != InvoiceStatus.NORMAL.getCode()) {
                throw new RuntimeException("发票状态异常: " + invoice.getInvoiceNo());
            }

            // 检查是否已报销
            if (invoice.getReimbursementStatus() == 2) {
                throw new RuntimeException("发票已报销: " + invoice.getInvoiceNo());
            }

            // 检查是否已关联其他报销单
            if (invoice.getReimbursementId() != null) {
                throw new RuntimeException("发票已关联其他报销单: " + invoice.getInvoiceNo());
            }

            invoices.add(invoice);
        }

        return invoices;
    }

    private InvoiceVerifyRequest buildVerifyRequest(Invoice invoice) {
        InvoiceVerifyRequest request = new InvoiceVerifyRequest();
        request.setInvoiceCode(invoice.getInvoiceCode());
        request.setInvoiceNo(invoice.getInvoiceNo());
        request.setInvoiceDate(invoice.getInvoiceDate().toString());
        request.setCheckCode(invoice.getCheckCode());
        request.setAmount(invoice.getAmount().toString());
        request.setInvoiceType(invoice.getInvoiceType());
        return request;
    }

    private String generateReimburseNo() {
        return "RB" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) +
                String.format("%06d", new Random().nextInt(1000000));
    }
}

@Data
public class ReimbursementDTO {
    private String reimburseType;
    private String title;
    private List<Long> invoiceIds;
    private String remark;
    private List<String> attachmentUrls;
}

五、API 接口设计

5.1 发票管理接口

/**
 * 发票管理控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/invoice")
public class InvoiceController {

    @Autowired
    private InvoiceService invoiceService;

    @Autowired
    private InvoiceImportService importService;

    @Autowired
    private OcrServiceFacade ocrService;

    @Autowired
    private InvoiceVerifyService verifyService;

    /**
     * 发票列表查询
     */
    @GetMapping("/list")
    public Result<PageInfo<Invoice>> list(InvoiceQueryDTO query) {
        PageHelper.startPage(query.getPageNum(), query.getPageSize());
        List<Invoice> invoices = invoiceService.queryList(query);
        return Result.success(new PageInfo<>(invoices));
    }

    /**
     * 发票详情
     */
    @GetMapping("/{id}")
    public Result<InvoiceDetailVO> detail(@PathVariable Long id) {
        InvoiceDetailVO detail = invoiceService.getDetail(id);
        return Result.success(detail);
    }

    /**
     * 手动录入发票
     */
    @PostMapping("/manual")
    public Result<Invoice> manualCreate(@RequestBody @Valid InvoiceDTO dto) {
        Invoice invoice = invoiceService.manualCreate(dto, getCurrentUserId());
        return Result.success(invoice);
    }

    /**
     * OCR识别发票
     */
    @PostMapping("/ocr")
    public Result<OcrResult> ocrRecognize(@RequestParam("file") MultipartFile file) {
        try {
            // 上传文件
            String imageUrl = uploadFile(file);

            // OCR识别
            OcrResult result = ocrService.recognizeWithFallback(imageUrl);
            return Result.success(result);

        } catch (Exception e) {
            log.error("OCR识别失败", e);
            return Result.fail("识别失败: " + e.getMessage());
        }
    }

    /**
     * OCR识别并保存发票
     */
    @PostMapping("/ocr/save")
    public Result<Invoice> ocrAndSave(@RequestParam("file") MultipartFile file) {
        try {
            String imageUrl = uploadFile(file);
            OcrResult result = ocrService.recognizeWithFallback(imageUrl);

            if (!result.isSuccess()) {
                return Result.fail("识别失败: " + result.getMessage());
            }

            // 保存发票
            Invoice invoice = invoiceService.saveFromOcr(result.getInvoiceData(),
                    imageUrl, getCurrentUserId());

            return Result.success(invoice);

        } catch (Exception e) {
            log.error("OCR保存失败", e);
            return Result.fail("保存失败: " + e.getMessage());
        }
    }

    /**
     * 批量上传发票图片
     */
    @PostMapping("/upload/batch")
    public Result<String> batchUpload(@RequestParam("files") List<MultipartFile> files) {
        try {
            // 上传文件
            List<String> imageUrls = new ArrayList<>();
            for (MultipartFile file : files) {
                String imageUrl = uploadFile(file);
                imageUrls.add(imageUrl);
            }

            // 创建导入批次
            String batchNo = importService.createBatch("IMAGE", getCurrentUserId());

            // 异步处理
            Long batchId = importService.getBatchId(batchNo);
            importService.importFromImages(batchId, imageUrls);

            return Result.success(batchNo);

        } catch (Exception e) {
            log.error("批量上传失败", e);
            return Result.fail("上传失败: " + e.getMessage());
        }
    }

    /**
     * Excel导入发票
     */
    @PostMapping("/import/excel")
    public Result<String> importExcel(@RequestParam("file") MultipartFile file) {
        try {
            // 创建导入批次
            String batchNo = importService.createBatch("EXCEL", getCurrentUserId());
            Long batchId = importService.getBatchId(batchNo);

            // 异步导入
            importService.importFromExcel(batchId, file.getInputStream());

            return Result.success(batchNo);

        } catch (Exception e) {
            log.error("Excel导入失败", e);
            return Result.fail("导入失败: " + e.getMessage());
        }
    }

    /**
     * 查询导入进度
     */
    @GetMapping("/import/progress/{batchNo}")
    public Result<InvoiceImportBatch> importProgress(@PathVariable String batchNo) {
        InvoiceImportBatch batch = importService.getBatch(batchNo);
        return Result.success(batch);
    }

    /**
     * 发票验真
     */
    @PostMapping("/verify")
    public Result<VerifyResult> verify(@RequestBody InvoiceVerifyRequest request) {
        VerifyResult result = verifyService.verify(request);
        return Result.success(result);
    }

    /**
     * 批量验真
     */
    @PostMapping("/verify/batch")
    public Result<?> batchVerify(@RequestBody List<Long> invoiceIds) {
        invoiceService.batchVerify(invoiceIds);
        return Result.success();
    }

    /**
     * 删除发票
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Long id) {
        invoiceService.delete(id, getCurrentUserId());
        return Result.success();
    }

    /**
     * 导出发票
     */
    @GetMapping("/export")
    public void export(InvoiceQueryDTO query, HttpServletResponse response) throws Exception {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment;filename=invoices.xlsx");

        invoiceService.export(query, response.getOutputStream());
    }

    private String uploadFile(MultipartFile file) throws Exception {
        // 上传到OSS
        return null;
    }

    private Long getCurrentUserId() {
        return 1L;  // 从上下文获取
    }
}

/**
 * 开票申请控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/invoice/apply")
public class InvoiceApplyController {

    @Autowired
    private InvoiceIssueService issueService;

    /**
     * 创建开票申请
     */
    @PostMapping
    public Result<InvoiceApply> create(@RequestBody @Valid InvoiceApplyDTO dto) {
        InvoiceApply apply = issueService.createApply(dto, getCurrentUserId());
        return Result.success(apply);
    }

    /**
     * 开票申请列表
     */
    @GetMapping("/list")
    public Result<PageInfo<InvoiceApply>> list(InvoiceApplyQueryDTO query) {
        // ...
        return null;
    }

    /**
     * 审批开票申请
     */
    @PostMapping("/{id}/approve")
    public Result<?> approve(@PathVariable Long id,
                             @RequestParam boolean approved,
                             @RequestParam(required = false) String remark) {
        issueService.approveApply(id, approved, remark, getCurrentUserId());
        return Result.success();
    }

    /**
     * 发票冲红
     */
    @PostMapping("/red-flush")
    public Result<Invoice> redFlush(@RequestParam Long invoiceId,
                                     @RequestParam String reason) {
        Invoice redInvoice = issueService.redFlush(invoiceId, reason);
        return Result.success(redInvoice);
    }

    private Long getCurrentUserId() {
        return 1L;
    }
}

/**
 * 报销控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/reimbursement")
public class ReimbursementController {

    @Autowired
    private ReimbursementService reimbursementService;

    /**
     * 创建报销单
     */
    @PostMapping
    public Result<Reimbursement> create(@RequestBody @Valid ReimbursementDTO dto) {
        Reimbursement reimbursement = reimbursementService.createReimbursement(dto, getCurrentUserId());
        return Result.success(reimbursement);
    }

    /**
     * 报销单列表
     */
    @GetMapping("/list")
    public Result<PageInfo<Reimbursement>> list(ReimbursementQueryDTO query) {
        // ...
        return null;
    }

    /**
     * 提交报销单
     */
    @PostMapping("/{id}/submit")
    public Result<?> submit(@PathVariable Long id) {
        reimbursementService.submitReimbursement(id, getCurrentUserId());
        return Result.success();
    }

    /**
     * 审批报销单
     */
    @PostMapping("/{id}/approve")
    public Result<?> approve(@PathVariable Long id,
                             @RequestParam boolean approved,
                             @RequestParam(required = false) String remark) {
        reimbursementService.approveReimbursement(id, approved, remark, getCurrentUserId());
        return Result.success();
    }

    private Long getCurrentUserId() {
        return 1L;
    }
}

六、统计分析

6.1 发票统计服务

/**
 * 发票统计服务
 */
@Slf4j
@Service
public class InvoiceStatisticsService {

    @Autowired
    private InvoiceMapper invoiceMapper;

    /**
     * 获取发票统计概览
     */
    public InvoiceStatisticsVO getStatisticsOverview(Long companyId, LocalDate startDate, LocalDate endDate) {
        InvoiceStatisticsVO vo = new InvoiceStatisticsVO();

        // 收票统计
        vo.setReceiveCount(invoiceMapper.countBySource(companyId, startDate, endDate, null));
        vo.setReceiveAmount(invoiceMapper.sumAmountBySource(companyId, startDate, endDate, null));

        // 开票统计
        vo.setIssueCount(invoiceMapper.countBySource(companyId, startDate, endDate, "ISSUE"));
        vo.setIssueAmount(invoiceMapper.sumAmountBySource(companyId, startDate, endDate, "ISSUE"));

        // 验真统计
        vo.setVerifiedCount(invoiceMapper.countByVerifyStatus(companyId, startDate, endDate,
                VerifyStatus.VERIFIED_SUCCESS.getCode()));
        vo.setUnverifiedCount(invoiceMapper.countByVerifyStatus(companyId, startDate, endDate,
                VerifyStatus.NOT_VERIFIED.getCode()));

        // 报销统计
        vo.setReimbursedCount(invoiceMapper.countByReimbursementStatus(companyId, startDate, endDate, 2));
        vo.setReimbursedAmount(invoiceMapper.sumAmountByReimbursementStatus(companyId, startDate, endDate, 2));
        vo.setPendingReimbursementCount(invoiceMapper.countByReimbursementStatus(companyId, startDate, endDate, 0));

        // 税额统计
        vo.setTotalTaxAmount(invoiceMapper.sumTaxAmount(companyId, startDate, endDate));

        // 按类型统计
        vo.setTypeStatistics(invoiceMapper.groupByType(companyId, startDate, endDate));

        // 按月份趋势
        vo.setMonthlyTrend(invoiceMapper.groupByMonth(companyId, startDate, endDate));

        return vo;
    }

    /**
     * 进项税额统计(可抵扣)
     */
    public BigDecimal getDeductibleTaxAmount(Long companyId, LocalDate startDate, LocalDate endDate) {
        // 只统计专用发票的税额
        return invoiceMapper.sumTaxAmountByType(companyId, startDate, endDate,
                Arrays.asList(InvoiceType.SPECIAL.getCode(), InvoiceType.ELECTRONIC_SPECIAL.getCode()));
    }

    /**
     * 获取即将过期发票(电子发票有效期)
     */
    public List<Invoice> getExpiringInvoices(Long companyId, int days) {
        LocalDate expiryDate = LocalDate.now().plusDays(days);
        return invoiceMapper.selectExpiringInvoices(companyId, expiryDate);
    }

    @Data
    public static class InvoiceStatisticsVO {
        private Long receiveCount;
        private BigDecimal receiveAmount;
        private Long issueCount;
        private BigDecimal issueAmount;
        private Long verifiedCount;
        private Long unverifiedCount;
        private Long reimbursedCount;
        private BigDecimal reimbursedAmount;
        private Long pendingReimbursementCount;
        private BigDecimal totalTaxAmount;
        private List<TypeStatistics> typeStatistics;
        private List<MonthlyTrend> monthlyTrend;
    }

    @Data
    public static class TypeStatistics {
        private String invoiceType;
        private String invoiceTypeName;
        private Long count;
        private BigDecimal amount;
        private BigDecimal taxAmount;
    }

    @Data
    public static class MonthlyTrend {
        private String month;
        private Long receiveCount;
        private BigDecimal receiveAmount;
        private Long issueCount;
        private BigDecimal issueAmount;
    }
}

七、最佳实践

7.1 发票系统设计要点

+------------------------------------------------------------------+
|                    发票系统设计要点                                |
+------------------------------------------------------------------+
|                                                                   |
|   1. 数据准确性                                                    |
|   - OCR识别后人工复核                                              |
|   - 多渠道验真(税务局/第三方)                                     |
|   - 查重机制防止重复录入                                           |
|                                                                   |
|   2. 安全性                                                        |
|   - 敏感信息加密存储                                               |
|   - 操作日志完整记录                                               |
|   - 权限控制细粒度管理                                             |
|                                                                   |
|   3. 合规性                                                        |
|   - 符合税务法规要求                                               |
|   - 电子发票存档要求                                               |
|   - 报销审批流程规范                                               |
|                                                                   |
|   4. 易用性                                                        |
|   - 多种导入方式(扫描/拍照/上传)                                  |
|   - 智能识别减少手动输入                                           |
|   - 批量操作提高效率                                               |
|                                                                   |
|   5. 扩展性                                                        |
|   - 支持新发票类型                                                 |
|   - 对接多个OCR服务                                                |
|   - 灵活的审批流程配置                                             |
|                                                                   |
+------------------------------------------------------------------+

7.2 常见问题处理

问题原因解决方案
OCR识别率低图片质量差/角度偏斜图片预处理/提供拍摄指引
验真失败发票信息有误/网络问题人工复核/重试机制
重复报销发票查重不严格代码+号码唯一索引/历史查重
金额计算错误含税/不含税混淆明确金额类型/自动计算校验
发票过期未及时报销到期提醒/自动预警

7.3 性能优化建议

  1. OCR并行处理:批量识别时使用多线程
  2. 缓存常用数据:发票抬头、税收编码等
  3. 异步处理:导入、验真等耗时操作异步化
  4. 分表策略:按时间分表存储历史发票
  5. 索引优化:针对常用查询条件建立索引

八、总结

本文详细介绍了财务发票系统的设计与实现,涵盖以下核心内容:

  1. 系统架构:微服务架构,模块化设计
  2. 数据模型:完整的发票、开票、报销数据模型
  3. OCR识别:对接百度/阿里云OCR,支持多种发票类型
  4. 发票验真:对接税务局接口,确保发票真实有效
  5. 多渠道导入:支持Excel、图片、邮件等多种导入方式
  6. 开票功能:开票申请、审批、开具、交付全流程
  7. 报销管理:报销单创建、发票关联、审批流程

通过本系统,企业可以实现发票的全生命周期管理,提高财务工作效率,降低合规风险。