电子发票/数电发票 Rust SDK | 开票、验真、红冲一站式集成|云开票,开票系统

2026-07-15
电子发票/数电发票 Rust SDK | 开票、验真、红冲一站式集成|云开票,开票系统

电子发票/数电发票 Rust SDK | 开票、验真、红冲一站式集成

发票 Rust SDK 专为电子发票、数电发票(全电发票)场景设计,支持开票、红冲、版式文件下载等核心功能,快速对接税务平台API。

关键词: 电子发票SDK,数电票Rust SDK,开票接口,发票api,发票开具,发票红冲,全电发票集成


⚡ 核心功能

基础认证

  • 获取授权 - 快速接入税务平台身份认证
  • 人脸二维码登录 - 支持数电发票平台扫码登录
  • 认证状态查询 - 实时获取纳税人身份状态

发票开具

  • 🧾 数电蓝票开具 - 支持增值税普通/专用电子发票
  • 📥 版式文件下载 - 自动获取销项发票PDF/OFD/XML文件

发票红冲

  • 🔍 红冲前蓝票查询 - 精确检索待红冲的电子发票
  • 📋 红字信息表申请 - 生成红冲凭证
  • 负数发票开具 - 自动化红冲流程

📦 快速安装

Cargo add

cargo add tax-invoice

Cargo.toml

[dependencies]
 tax-invoice = "2.0.1"

📦查看Cargo.io最新版本

📚查看中文文档 | ?💡 更多示例代码


🚀 为什么选择此SDK?

  • 精准覆盖中国数电发票标准 - 严格遵循国家最新接口规范
  • 开箱即用 - 无需处理XML/签名等底层细节,专注业务逻辑
  • 企业级验证 - 已在生产环境处理超100万张电子发票

🧾 支持的开票类型

发票类型 状态
数电发票(普通发票) ✅ 支持
数电发票(增值税专用发票) ✅ 支持
数电发票(铁路电子客票) ✅ 支持
数电发票(航空运输电子客票行程单) ✅ 支持
数电票(二手车销售统一发票) ✅ 支持
数电纸质发票(增值税专用发票) ✅ 支持
数电纸质发票(普通发票) ✅ 支持
数电纸质发票(机动车发票) ✅ 支持
数电纸质发票(二手车发票) ✅ 支持

🤝 贡献与支持

🚀 快速开始

//! 数电发票 SDK 使用示例 - 蓝票开具完整流程
 
 use std::io::{self, Write};
 use std::sync::{Arc, Mutex};
 use std::thread::sleep;
 use std::time::Duration;
 
 use redis::Commands;
 use serde_json::{json, Map, Value};
 use tax_invoice::{InvoiceClient, InvoiceError};
 
 use base64::{engine::general_purpose, Engine as _};
 use image::{GrayImage, ImageFormat, ImageReader, Luma};
 use qrcode::{Color, QrCode};
 
 // TODO: 替换为从 fa-piao.com 后台拿到的真实凭据
 pub static APP_KEY: &str = "";
 pub static APP_SECRET: &str = "";
 /// 统一社会信用代码
 pub static NSRSBH: &str = "";
 /// 名称(营业执照)
 pub static TITLE: &str = "";
 /// 手机号码(电子税务局)
 pub static USERNAME: &str = "";
 /// 个人用户密码(电子税务局)
 pub static PASSWORD: &str = "";
 /// 地址和电话 空格隔开
 pub static XHDWDZDH: &str = "";
 /// 开户行和银行账号 空格隔开
 pub static XHDWYHZH: &str = "";
 
 /// 是否打印日志
 pub static DEBUG: bool = true;
 
 /// Redis 配置
 pub const REDIS_HOST: &str = "127.0.0.1";
 pub const REDIS_PORT: u16 = 6379;
 pub const REDIS_PASSWORD: &str = "test123456";
 
 pub const REDIS_TOKEN_TTL_SECS: u64 = 3600 * 24 * 30;
 
 /// 创建客户端 (在 main 中初始化)
 pub static CLIENT: once_cell::sync::Lazy<InvoiceClient> = once_cell::sync::Lazy::new(|| {
     InvoiceClient::new_debug(APP_KEY, APP_SECRET, DEBUG).expect("failed to create InvoiceClient")
 });
 
 
 pub static REDIS_CLIENT: once_cell::sync::Lazy<Arc<Mutex<redis::Connection>>> =
     once_cell::sync::Lazy::new(|| {
         let client = redis::Client::open(format!("redis://{REDIS_HOST}:{REDIS_PORT}/"))
             .expect("failed to create Redis client");
         let mut conn = client
             .get_connection()
             .expect("failed to connect to Redis at 127.0.0.1:6379");
         let _: () = redis::cmd("AUTH")
             .arg(REDIS_PASSWORD)
             .query(&mut conn)
             .expect("Redis AUTH failed - check password");
         Arc::new(Mutex::new(conn))
     });
 
 fn main() -> Result<(), InvoiceError> {
     // ────────────── 一 获取 token ──────────────
     get_token(false);
 
     // ────────────── 二 开具蓝票 ──────────────
     /*
      * 前端模拟数电发票/电子发票开具 (蓝字发票)
      * @see https://fa-piao.com/fapiao.html?source=github
      */
     let invoice_response = match blue_ticket() {
         Ok(r) => r,
         Err(e) => {
             println!("开票异常: {e}");
             return Ok(());
         }
     };
     match invoice_response.code {
         200 => {
             // ────────── 三 下载发票 ──────────
             let data = invoice_response.data.unwrap_or_default();
             let fphm = data
                 .get("Fphm")
                 .and_then(|v| v.as_str())
                 .unwrap_or_default();
             let kprq = data
                 .get("Kprq")
                 .and_then(|v| v.as_str())
                 .unwrap_or_default();
             download_pdf_ofd_xml(fphm, kprq);
         }
         420 => {
             println!("420 登录(短信认证)");
             /*
              * 前端模拟短信认证弹窗
              * @see https://fa-piao.com/fapiao.html?action=sms&source=github
              */
             // 1. 发短信验证码
             /*
              * @see https://fa-piao.com/doc.html#api2?source=github
              */
             let login_response = match CLIENT.login_dppt(NSRSBH, USERNAME, PASSWORD, Some("")) {
                 Ok(r) => r,
                 Err(e) => {
                     println!("短信登录失败: {e}");
                     return Ok(());
                 }
             };
             if login_response.code == 200 {
                 println!("请输入验证码");
                 print!(
                     "300秒内({}前)输入内容: ",
                     chrono::Local::now()
                         .checked_add_signed(chrono::Duration::seconds(300))
                         .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string())
                         .unwrap_or_default()
                 );
                 let _ = io::stdout().flush();
                 let mut sms_code = String::new();
                 if io::stdin().read_line(&mut sms_code).is_err() {
                     println!("\n读取输入失败");
                     return Ok(());
                 }
                 let sms_code = sms_code.trim().to_string();
                 println!(
                     "? 成功获取输入: {} \n当前时间: {}",
                     sms_code,
                     chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
                 );
                 // 2. 输入验证码
                 /*
                  * @see https://fa-piao.com/doc.html#api2?source=github
                  */
                 let login_response2 = match CLIENT.login_dppt(NSRSBH, USERNAME, PASSWORD, Some(&sms_code)) {
                     Ok(r) => r,
                     Err(e) => {
                         println!("短信登录失败: {e}");
                         return Ok(());
                     }
                 };
                 if login_response2.code == 200 {
                     println!("验证成功");
                     println!("请再次调用blueTicket");
                     if let Ok(invoice_response2) = blue_ticket() {
                         if invoice_response2.code == 200 {
                             let data = invoice_response2.data.unwrap_or_default();
                             let fphm = data
                                 .get("Fphm")
                                 .and_then(|v| v.as_str())
                                 .unwrap_or_default();
                             let kprq = data
                                 .get("Kprq")
                                 .and_then(|v| v.as_str())
                                 .unwrap_or_default();
                             download_pdf_ofd_xml(fphm, kprq);
                         }
                     }
                 } else {
                     println!("{}验证失败: {}", login_response2.code, login_response2.msg);
                 }
             } else {
                 println!("{}发短信验证码失败: {}", login_response.code, login_response.msg);
             }
         }
         430 => {
             println!("430 人脸认证");
             /*
              * 前端模拟人脸认证弹窗
              * @see https://fa-piao.com/fapiao.html?action=face&source=github
              */
             // 1. 获取人脸二维码
             /*
              * @see https://fa-piao.com/doc.html#api3?source=github
              */
             let qr_code_response = match CLIENT.get_face_img(NSRSBH, Some(USERNAME), Some("1")) {
                 Ok(r) => r,
                 Err(e) => {
                     println!("人脸二维码请求失败: {e}");
                     return Ok(());
                 }
             };
             let qr_data = qr_code_response.data.clone().unwrap_or_default();
             if qr_data.is_empty() {
                 println!("人脸二维码返回为空: {}", qr_code_response.msg);
             } else {
                 let ewmly = qr_data
                     .get("ewmly")
                     .and_then(|v| v.as_str())
                     .unwrap_or("");
                 if ewmly == "swj" {
                     println!("请使用电子税务局app扫码");
                 } else {
                     println!("个人所得税app扫码");
                 }
                 // 字符串转二维码图片 - 命令行终端打印
                 if let Some(ewm) = qr_data.get("ewm").and_then(|v| v.as_str()) {
                     // println!("二维码内容长度: {}", ewm.len());
                     // 生成二维码图片base64编码
                     let b64 = qr_png_base64(ewm, 8);
                     // 打印二维码图片base64编码到终端
                     print_binary_image(&b64);
                 }
                 println!("成功做完人脸认证,请输入数字 1");
                 print!(
                     "300秒内({}前)输入内容: ",
                     chrono::Local::now()
                         .checked_add_signed(chrono::Duration::seconds(300))
                         .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string())
                         .unwrap_or_default()
                 );
                 let _ = io::stdout().flush();
                 let mut input_num = String::new();
                 if io::stdin().read_line(&mut input_num).is_err() {
                     println!("\n人脸认证输入超时!");
                     return Ok(());
                 }
                 let input_num = input_num.trim().to_string();
                 println!(
                     "? 成功获取输入: {} \n当前时间: {}",
                     input_num,
                     chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
                 );
                 // 2. 认证完成后获取人脸二维码认证状态
                 /*
                  * @see https://fa-piao.com/doc.html#api4?source=github
                  */
                 let rzid = qr_data
                     .get("rzid")
                     .and_then(|v| v.as_str())
                     .unwrap_or_default();
                 let face_status_response = match CLIENT.get_face_state(
                     NSRSBH,
                     rzid,
                     Some(USERNAME),
                     Some("1"),
                 ) {
                     Ok(r) => r,
                     Err(e) => {
                         println!("人脸状态查询失败: {e}");
                         return Ok(());
                     }
                 };
                 if let Some(face_data) = face_status_response.data {
                     if let Some(slzt) = face_data.get("slzt").and_then(|v| v.as_str()) {
                         if slzt == "2" {
                             println!("认证状态: 成功");
                             println!("请再次调用blueTicket");
                             if let Ok(invoice_response3) = blue_ticket() {
                                 if invoice_response3.code == 200 {
                                     let data = invoice_response3.data.unwrap_or_default();
                                     let fphm = data
                                         .get("Fphm")
                                         .and_then(|v| v.as_str())
                                         .unwrap_or_default();
                                     let kprq = data
                                         .get("Kprq")
                                         .and_then(|v| v.as_str())
                                         .unwrap_or_default();
                                     download_pdf_ofd_xml(fphm, kprq);
                                 }
                             }
                         } else if slzt == "1" {
                             println!("认证状态: 未认证");
                         } else {
                             println!("认证状态: 二维码过期");
                         }
                     }
                 }
             }
         }
         401 => {
             println!("{}授权失败:{}", invoice_response.code, invoice_response.msg);
             println!("401  token过期 重新获取并缓存token");
             sleep(Duration::from_millis(1000));
             get_token(true);
             println!("再调用blueTicket");
             if let Ok(invoice_response4) = blue_ticket() {
                 if invoice_response4.code == 200 {
                     let data = invoice_response4.data.unwrap_or_default();
                     let fphm = data
                         .get("Fphm")
                         .and_then(|v| v.as_str())
                         .unwrap_or_default();
                     let kprq = data
                         .get("Kprq")
                         .and_then(|v| v.as_str())
                         .unwrap_or_default();
                     download_pdf_ofd_xml(fphm, kprq);
                 }
             }
         }
         _ => {
             println!("{}异常{}", invoice_response.code, invoice_response.msg);
         }
     }
     Ok(())
 }
 
 // Redis 存储。
 fn get_token(force_update: bool) {
     let redis_key = format!("{}@{}@TOKEN", NSRSBH, USERNAME);
     let mut conn = REDIS_CLIENT.lock().expect("redis lock poisoned");
 
     if force_update {
         /*
          * 获取授权Token文档
          * @see https://fa-piao.com/doc.html#api1?source=github
          */
         if let Ok(auth_response) = CLIENT.get_authorization(NSRSBH) {
             if auth_response.is_success() {
                 if let Some(token) = auth_response
                     .data
                     .as_ref()
                     .map(|d| d.token().to_string())
                 {
  
                     let _: () = conn
                         .set_ex(&redis_key, &token, REDIS_TOKEN_TTL_SECS)
                         .expect("redis SETEX failed");
                     CLIENT.set_authorization(Some(token));
                 }
             }
         }
     } else {
 
         let cached: Option<String> = conn.get(&redis_key).expect("redis GET failed");
         if cached.is_none() || cached.as_ref().unwrap().is_empty() {
             if let Ok(auth_response) = CLIENT.get_authorization(NSRSBH) {
                 if auth_response.is_success() {
                     if let Some(token) =
                         auth_response.data.as_ref().map(|d| d.token().to_string())
                     {
                         let _: () = conn
                             .set_ex(&redis_key, &token, REDIS_TOKEN_TTL_SECS)
                             .expect("redis SETEX failed");
                         CLIENT.set_authorization(Some(token));
                     }
                 }
             }
         } else {
 
             CLIENT.set_authorization(cached);
         }
     }
 }
 
 //
 fn blue_ticket() -> tax_invoice::Result<tax_invoice::ApiResponse<Map<String, Value>>> {
     /*
      * 开票税额计算说明 demo
      * @see https://github.com/fapiaoapi/invoice-sdk-rust/blob/master/examples/tax_example.rs
      */
     let mut invoice_params = Map::new();
     invoice_params.insert("fplxdm".into(), Value::String("82".into()));
     invoice_params.insert(
         "fpqqlsh".into(),
         Value::String(format!("{}{}", APP_KEY, chrono::Utc::now().timestamp_millis())),
     );
     invoice_params.insert("ghdwmc".into(), Value::String("个人".into()));
     // invoice_params.insert("ghdwsbh".into(), Value::String("914208XXXXXXX".into()));
     invoice_params.insert("hjje".into(), json!(9.9));
     invoice_params.insert("hjse".into(), json!(0.1));
     invoice_params.insert("jshj".into(), json!(10));
     invoice_params.insert("kplx".into(), json!(0));
     invoice_params.insert("username".into(), Value::String(USERNAME.into()));
     invoice_params.insert("xhdwdzdh".into(), Value::String(XHDWDZDH.into()));
     invoice_params.insert("xhdwmc".into(), Value::String(TITLE.into()));
     invoice_params.insert("xhdwsbh".into(), Value::String(NSRSBH.into()));
     invoice_params.insert("xhdwyhzh".into(), Value::String(XHDWYHZH.into()));
     invoice_params.insert("zsfs".into(), json!(0));
     invoice_params.insert("fyxm[0][fphxz]".into(), json!(0));
     invoice_params.insert( "fyxm[0][spmc]".into(),Value::String("*软件维护服务*接口服务费".into()));
     // invoice_params.insert("fyxm[0][ggxh]".into(), Value::String("".into()));
     // invoice_params.insert("fyxm[0][dw]".into(), Value::String("个".into()));
     // invoice_params.insert("fyxm[0][spsl]".into(), json!(1));
     // invoice_params.insert("fyxm[0][dj]".into(), json!(10));
     invoice_params.insert("fyxm[0][je]".into(), json!(10));
     invoice_params.insert("fyxm[0][sl]".into(), json!(0.01));
     invoice_params.insert("fyxm[0][se]".into(), json!(0.1));
     invoice_params.insert("fyxm[0][hsbz]".into(), json!(1));
     invoice_params.insert("fyxm[0][spbm]".into(),Value::String("3040201030000000000".into()));
     /*
      * 开具数电发票文档
      * @see https://fa-piao.com/doc.html#api6?source=github
      */
     CLIENT.blue_ticket(invoice_params)
 }
 
 
 fn download_pdf_ofd_xml(fphm: &str, kprq: &str) {
     /*
      * 获取销项数电版式文件
      * @see https://fa-piao.com/doc.html#api7?source=github
      */
     let mut pdf_params = Map::new();
     pdf_params.insert("downflag".into(), Value::String("4".into()));
     pdf_params.insert("nsrsbh".into(), Value::String(NSRSBH.into()));
     pdf_params.insert("username".into(), Value::String(USERNAME.into()));
     pdf_params.insert("fphm".into(), Value::String(fphm.into()));
     pdf_params.insert("kprq".into(), Value::String(kprq.into()));
 
     if let Ok(pdf_response) = CLIENT.get_pdf_ofd_xml(pdf_params) {
         if pdf_response.is_success() {
             let data = pdf_response.data.unwrap_or_default();
             if let Some(pdf) = data.get("pdf").and_then(|v| v.as_str()) {
                 println!("发票PDF下载链接{}",pdf);
             }
             if let Some(ofd) = data.get("ofd").and_then(|v| v.as_str()) {
                 println!("发票OFD下载链接{}",ofd);
             }
         } else {
             println!("下载失败: code={} msg={}", pdf_response.code, pdf_response.msg);
         }
     }
 }
 
 /// 把字符串编码成 1 张 8 px/module 的灰度 PNG,再 base64 编码。
 /// 仅用于示例自动生成有效 base64 (Java 硬编码那份实际是损坏的 PNG)。
 fn qr_png_base64(content: &str, scale: u32) -> String {
     let code = QrCode::new(content.as_bytes()).expect("QR generation failed");
     let size = code.width() as u32;
     let margin = 4 * scale; // QR 标准 4 module 静默区
     let total = (size + 8) * scale;
     let mut img = GrayImage::from_pixel(total, total, Luma([255u8]));
     for y in 0..size {
         for x in 0..size {
             if code[(x as usize, y as usize)] == Color::Dark {
                 for dy in 0..scale {
                     for dx in 0..scale {
                         img.put_pixel(margin + x * scale + dx, margin + y * scale + dy, Luma([0]));
                     }
                 }
             }
         }
     }
     let mut buf = Vec::new();
     img.write_to(&mut io::Cursor::new(&mut buf), ImageFormat::Png)
         .expect("PNG encode failed");
     general_purpose::STANDARD.encode(&buf)
 }
 
 /// 然后按 1 module 宽 × 2 module 高的比例采样像素,用 `█▀▄` 块字符输出。
 fn print_binary_image(base64_data: &str) {
     let Ok(bytes) = general_purpose::STANDARD.decode(base64_data) else {
         eprintln!("[print_binary_image] base64 解码失败");
         return;
     };
     let Ok(reader) = ImageReader::new(io::Cursor::new(bytes)).with_guessed_format() else {
         eprintln!("[print_binary_image] 格式识别失败");
         return;
     };
     let Ok(img) = reader.decode() else {
         eprintln!("[print_binary_image] PNG 解码失败 (图片可能损坏)");
         return;
     };
     let gray = img.to_luma8();
     let (w, h) = (gray.width(), gray.height());
     if w < 8 || h < 8 {
         return;
     }
 
     // 1 字符代表 1 module 宽 × 2 module 高 (▀▄ 是半高块, 补偿终端字符 1:2 横纵比)
     // 目标 60 字符宽 (QR 一般 21~77 模块),适配任意尺寸 PNG
     let cols = 60u32.min(w / 2);
     if cols < 4 {
         return;
     }
     let step_x = w / cols;
     let step_y = step_x * 2;
 
     for y in (0..h).step_by(step_y as usize) {
         let mut line = String::new();
         for x in (0..w).step_by(step_x as usize) {
             // 采样上下半中心点, 比 corner 更稳定, 抗噪更好
             let top = is_black(&gray, x, (y + step_y / 4).min(h - 1));
             let bottom = is_black(&gray, x, (y + 3 * step_y / 4).min(h - 1));
             line.push(if top && bottom {
                 '█'
             } else if top {
                 '▀'
             } else if bottom {
                 '▄'
             } else {
                 ' '
             });
         }
         println!("{line}");
     }
 }
 
 fn is_black(img: &image::GrayImage, x: u32, y: u32) -> bool {
     img.get_pixel(x, y).0[0] < 128
 }

发票税额计算demo
发票红冲demo