电子发票/数电发票 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" 🚀 为什么选择此SDK?
- 精准覆盖中国数电发票标准 - 严格遵循国家最新接口规范
- 开箱即用 - 无需处理XML/签名等底层细节,专注业务逻辑
- 企业级验证 - 已在生产环境处理超100万张电子发票
🧾 支持的开票类型
| 发票类型 | 状态 |
|---|---|
| 数电发票(普通发票) | ✅ 支持 |
| 数电发票(增值税专用发票) | ✅ 支持 |
| 数电发票(铁路电子客票) | ✅ 支持 |
| 数电发票(航空运输电子客票行程单) | ✅ 支持 |
| 数电票(二手车销售统一发票) | ✅ 支持 |
| 数电纸质发票(增值税专用发票) | ✅ 支持 |
| 数电纸质发票(普通发票) | ✅ 支持 |
| 数电纸质发票(机动车发票) | ✅ 支持 |
| 数电纸质发票(二手车发票) | ✅ 支持 |
🤝 贡献与支持
- 提交Issue: 问题反馈
- 商务合作: yuejianghe@qq.com
🚀 快速开始
//! 数电发票 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
}