# invoice-sdk-rust **Repository Path**: fapiaoapi/invoice-sdk-rust ## Basic Information - **Project Name**: invoice-sdk-rust - **Description**: 发票sdk 发票api rust 电子发票 数电发票 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: https://fa-piao.com - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-07-14 - **Last Updated**: 2026-07-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 电子发票/数电发票 Rust SDK | 开票、验真、红冲一站式集成 [![Crates.io](https://img.shields.io/crates/v/tax-invoice.svg)](https://crates.io/crates/tax-invoice) [![Documentation](https://docs.rs/tax-invoice/badge.svg)](https://docs.rs/tax-invoice) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Rust 1.90+](https://img.shields.io/badge/rust-1.90%2B-orange.svg)](https://www.rust-lang.org) **发票 Rust SDK** 专为电子发票、数电发票(全电发票)场景设计,支持**开票、红冲、版式文件下载**等核心功能,快速对接税务平台API。 **关键词**: 电子发票SDK,数电票Rust SDK,开票接口,发票api,发票开具,发票红冲,全电发票集成 --- ## 📖 核心功能 ### 基础认证 - ✅ **获取授权** - 快速接入税务平台身份认证 - ✅ **人脸二维码登录** - 支持数电发票平台扫码登录 - ✅ **认证状态查询** - 实时获取纳税人身份状态 ### 发票开具 - 🎫 **数电蓝票开具** - 支持增值税普通/专用电子发票 - 📄 **版式文件下载** - 自动获取销项发票PDF/OFD/XML文件 ### 发票红冲 - 🔍 **红冲前蓝票查询** - 精确检索待红冲的电子发票 - 🛑 **红字信息表申请** - 生成红冲凭证 - 🔄 **负数发票开具** - 自动化红冲流程 --- ## 🚀 快速安装 ### Cargo add ```bash cargo add tax-invoice ``` ### Cargo.toml ```toml [dependencies] tax-invoice = "2.0.2" ``` [📦 查看Cargo.io最新版本](https://crates.io/crates/tax-invoice) [📚 查看中文文档](https://fa-piao.com/doc.html?source=gitee) | [💡 更多示例代码](https://gitee.com/fapiaoapi/invoice-sdk-rust/blob/master/examples) --- ## 🔍 为什么选择此SDK? - **精准覆盖中国数电发票标准** - 严格遵循国家最新接口规范 - **开箱即用** - 无需处理XML/签名等底层细节,专注业务逻辑 - **企业级验证** - 已在生产环境处理超100万张电子发票 --- ## 📊 支持的开票类型 | 发票类型 | 状态 | |----------------|--------| | 数电发票(普通发票) | ✅ 支持 | | 数电发票(增值税专用发票) | ✅ 支持 | | 数电发票(铁路电子客票) | ✅ 支持 | | 数电发票(航空运输电子客票行程单) | ✅ 支持 | | 数电票(二手车销售统一发票) | ✅ 支持 | | 数电纸质发票(增值税专用发票) | ✅ 支持 | | 数电纸质发票(普通发票) | ✅ 支持 | | 数电纸质发票(机动车发票) | ✅ 支持 | | 数电纸质发票(二手车发票) | ✅ 支持 | --- ## 🤝 贡献与支持 - 提交Issue: [问题反馈](https://github.com/fapiaoapi/invoice-sdk-rust/issues) - 商务合作: yuejianghe@qq.com ## 🎯 快速开始 ```rust //! 数电发票 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 = 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>> = 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=gitee */ 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=gitee */ // 1. 发短信验证码 /* * @see https://fa-piao.com/doc.html#api2?source=gitee */ 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=gitee */ 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=gitee */ // 1. 获取人脸二维码 /* * @see https://fa-piao.com/doc.html#api3?source=gitee */ 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=gitee */ 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=gitee */ 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 = 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>> { /* * 开票税额计算说明 demo * @see https://gitee.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=gitee */ CLIENT.blue_ticket(invoice_params) } fn download_pdf_ofd_xml(fphm: &str, kprq: &str) { /* * 获取销项数电版式文件 * @see https://fa-piao.com/doc.html#api7?source=gitee */ 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](https://gitee.com/fapiaoapi/invoice-sdk-rust/blob/master/examples/tax_example.rs "发票税额计算demo") [发票红冲demo](https://gitee.com/fapiaoapi/invoice-sdk-rust/blob/master/examples/red_invoice_example.rs "发票红冲demo")