Skip to content

fapiaoapi/invoice-sdk-nodejs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

npm version License

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

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


📖 核心功能

基础认证

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

发票开具

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

发票红冲

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

🚀 快速安装

npm install tax-invoice

📦 查看npm最新版本


📚 查看完整中文文档


🔍 为什么选择此SDK?

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

📊 支持的开票类型

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

🤝 贡献与支持

开票

import readline from "readline";
const { TaxInvoice } = require('tax-invoice');
const qrcode = require('qrcode-terminal');

const { createClient } = require('redis');
const QRCode = require('qrcode');

// 配置信息
const appKey = '';
const appSecret = '';

const nsrsbh = ''; // 统一社会信用代码
const title = ''; // 名称(营业执照)
const username = ''; // 手机号码(电子税务局)
const password = ''; // 个人用户密码(电子税务局)
const type = '7'; //6基础 7 标准版
const xhdwdzdh = '重庆市渝北区龙溪街道丽园路2号XXXX 1325580XXXX' //地址和电话 空格隔开
const xhdwyhzh = '工商银行XXXX 15451211XXXX'  // 开户行和银行账号 空格隔开

let token = "";
let debug = false;

// 初始化SDK
const taxInvoice = new TaxInvoice({
  appKey: appKey,
  appSecret: appSecret,
  debug: debug
});
// 初始化redis客户端
const client = createClient({
  url: 'redis://:test123456@localhost:6379'
});


// 主函数
async function main() {
  try {

    //一 获取授权 可从缓存redis中获取Token
    await getToken(false);
    /*
     * 前端模拟数电发票/电子发票开具 (蓝字发票)
     * @link https://fa-piao.com/fapiao.html?source=github
     */

    //二 开具蓝票
    const invoiceResponse = await blueTicket()
    switch (invoiceResponse.code) {
      case 200:
        // 三 下载发票
        await downloadPdfOfdXml(invoiceResponse.data.Fphm);
        break;
      case 420:
        console.log("登录(短信认证)");
        /*
         * 前端模拟短信认证弹窗
         * @link https://fa-piao.com/fapiao.html?action=sms&source=github
         */
        // 1. 发短信验证码
        /*
        * @link https://fa-piao.com/doc.html#api2?source=github
        */
        const loginResponse = await taxInvoice.api.loginDppt({ nsrsbh, username, password, sms: "" });
        if (loginResponse.code === 200) {
          const smsCode = await new Promise<string>((resolve, reject) => {
            const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
            console.log('请在300秒内(' + getFutureTime(300) + '前)输入验证码:');
            const timer = setTimeout(() => {
              rl.close();
              const timeoutError: any = new Error('短信验证码输入超时');
              timeoutError.errorCode = 'TIMEOUT';
              reject(timeoutError);
            }, 300000);
            rl.once('line', (input) => {
              clearTimeout(timer);
              rl.close();
              resolve(input.trim());
            });
          });
          const smsLoginResponse = await taxInvoice.api.loginDppt({ nsrsbh, username, password, sms: smsCode });
          if (smsLoginResponse.code === 200) {
            console.log("短信验证成功");
            console.log("再次调用blueTicket");
            const retryInvoiceResponse = await blueTicket();
            if (retryInvoiceResponse.code === 200) {
              await downloadPdfOfdXml(retryInvoiceResponse.data.Fphm);
            } else {
              const invoiceError: any = new Error('开票失败');
              invoiceError.errorCode = retryInvoiceResponse.code;
              throw invoiceError;
            }
          } else {
            const authError: any = new Error('短信验证失败');
            authError.errorCode = smsLoginResponse.code;
            throw authError;
          }
        }
        break;
      case 430:
        console.log("人脸认证");
        /* 前端模拟人脸认证弹窗
         * @link https://fa-piao.com/fapiao.html?action=face&source=github
         */
        // 1. 获取人脸二维码
        /*
         * @link https://fa-piao.com/doc.html#api3?source=github
         */
        const qrCodeResponse = await taxInvoice.api.getFaceImg({ nsrsbh, username, type: "1" });
        if (qrCodeResponse.data.ewmly === 'swj') {
          console.log("请使用电子税务局app扫码");
        } else if (qrCodeResponse.data.ewmly === 'grsds') {
          console.log("个人所得税app扫码");
        }
        if (qrCodeResponse.data.ewm.length < 500) {
          // //字符串转二维码图片base64   安装 npm install qrcode
          // const base64 = await QRCode.toDataURL(qrCodeResponse.data.ewm);
          // console.log("二维码base64:", base64);
          // qrCodeResponse.data.ewm = base64;
          // // let base64Uri = 'data:image/png;base64,' + base64;
          // // 前端使用示例: <img src="base64Uri" />
        }
        console.log("成功做完人脸认证,请输入数字 1")
        console.log('请在300秒内(' + getFutureTime(300) + '前)输入:');
        qrcode.generate(qrCodeResponse.data.ewm, {
          small: true, // 关键设置:true 会让二维码缩小(使用半高字符)
          margin: 1    // 边距:设为 1 减少空白
        }, (qrCode: string, err: Error | null = null) => {
          console.log(qrCode);
        });
        await new Promise<void>((resolve, reject) => {
          const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
          const timer = setTimeout(() => {
            rl.close();
            const timeoutError: any = new Error('人脸认证等待输入超时');
            timeoutError.errorCode = 'TIMEOUT';
            reject(timeoutError);
          }, 300000);
          rl.once('line', (input) => {
            clearTimeout(timer);
            rl.close();
            console.log(`✅ 收到: ${input}`);
            resolve();
          });
        });
        // 2. 认证完成后获取人脸二维码认证状态
        /*
         * @link https://fa-piao.com/doc.html#api4?source=github
         */
        const rzid = qrCodeResponse.data.rzid;
        const faceStatusResponse = await taxInvoice.api.getFaceState({ nsrsbh, rzid, username, type: "1" });
        if (faceStatusResponse.data) {
          switch (faceStatusResponse.data.slzt) {
            case '1':
              console.log("人脸未认证");
              const faceStatusError: any = new Error('人脸未认证');
              faceStatusError.errorCode = faceStatusResponse.code;
              throw faceStatusError;
            case '2':
              console.log("人脸认证成功");
              const invoiceResponseAfterFace = await blueTicket();
              if (invoiceResponseAfterFace.code === 200) {
                await downloadPdfOfdXml(invoiceResponseAfterFace.data.Fphm);
              } else {
                const authError: any = new Error('人脸认证失败');
                authError.errorCode = invoiceResponseAfterFace.code;
                throw authError;
              }
              break;
            case '3':
              console.log("人脸认证二维码过期-->重新获取人脸二维码");
              const faceStatusError2: any = new Error('人脸认证二维码过期-->重新获取人脸二维码');
              faceStatusError2.errorCode = faceStatusResponse.code;
              throw faceStatusError2;
          }
        }
        break;
      case 401:
        //token过期 重新获取并缓存token
        console.log(`${invoiceResponse.code} 授权失败: ${invoiceResponse.msg}`);
        console.log("重新授权 获取token 缓存到redis");
         await getToken(true);
         console.log("再请求blueTicket");
         const invoiceResponseAfterToken = await blueTicket();
         if (invoiceResponseAfterToken.code === 200) {
            await downloadPdfOfdXml(invoiceResponseAfterToken.data.Fphm);
         } else {
            const invoiceError: any = new Error('开票失败');
            invoiceError.errorCode = invoiceResponseAfterToken.code;
            throw invoiceError;
         }
        break;
      default:
        console.log('其他错误:', `${invoiceResponse.code} ${invoiceResponse.msg}`);
        break;
    }
  } catch (error: unknown) {
    const err = error as any;
    const errorCode = err?.errorCode ?? err?.code ?? 'UNKNOWN';
    const errorMessage =
      err?.message ??
      (typeof error === 'string' ? error : JSON.stringify(error)) ??
      '未知异常';
    console.error(`错误码: ${errorCode}`);
    console.error(`错误信息: ${errorMessage}`);
    if (Array.isArray(err?.errors) && err.errors.length > 0) {
      console.error("根因列表:");
      err.errors.forEach((item: any, index: number) => {
        const rootCode = item?.code ?? item?.errorCode ?? 'UNKNOWN';
        const rootMessage = item?.message ?? String(item);
        console.error(`  ${index + 1}. [${rootCode}] ${rootMessage}`);
      });
    }
    if (err?.stack) {
      console.error("堆栈:");
      console.error(err.stack);
    }
    console.error("=========================================\n");
  }
}

// 获取token 并缓存到redis
async function getToken(forceUpdate = false) {
  await client.connect();
  // 从缓存redis中获取Token 安装npm install redis
  const key = nsrsbh + "@" + username + "@TOKEN";
  if (forceUpdate) {
    /*
    * 获取授权Token文档
    * @link https://fa-piao.com/doc.html#api1?source=github
    */
    const authResult = await taxInvoice.api.getAuthorization(nsrsbh, type);
    // const authResult = await taxInvoice.api.getAuthorization(nsrsbh,type,username,password);
    if (authResult.code === 200) {
      const newToken = authResult.data.token;
      taxInvoice.setToken(newToken);
      await client.set(key, authResult.data.token, { EX: 3600 * 24 * 30 });// 设置过期时间为30
    } else {
      const authError: any = new Error(`授权失败: ${authResult.msg}`);
      authError.errorCode = authResult.code;
      throw authError;
    }
  } else {
    token = await client.get(key);
    if (token) {
      taxInvoice.setToken(token);
      console.log("Token From Redis");
    } else {
      const authResult = await taxInvoice.api.getAuthorization(nsrsbh, type);
      // const authResult = await taxInvoice.api.getAuthorization(nsrsbh,type,username,password);
      if (authResult.code === 200) {
        const newToken = authResult.data.token;
        taxInvoice.setToken(newToken);
        await client.set(key, authResult.data.token, { EX: 3600 * 24 * 30 });// 设置过期时间为30
      } else {
        const authError: any = new Error(`授权失败: ${authResult.msg}`);
        authError.errorCode = authResult.code;
        throw authError;
      }
    }
  }
}
// 开票
async function blueTicket() {
      /*
     * 开具数电发票文档
     * @link https://fa-piao.com/doc.html#api6?source=github
     *
     * 税额计算demo
     * @link https://github.com/fapiaoapi/invoice-sdk-nodejs/blob/master/examples/tax_example.ts
     */
  const invoiceParams = {
    username: username,
    fpqqlsh: appKey + Date.now(),// 建议用你的订单号
    fplxdm: '82',
    kplx: '0',
    xhdwsbh: nsrsbh,
    xhdwmc: title,
    xhdwdzdh: xhdwdzdh,
    xhdwyhzh: xhdwyhzh,
    ghdwmc: '个人',
    zsfs: '0',
    fyxm: [
      {
        "fphxz": 0,
        "hsbz": 1,
        "spmc": "*软件维护服务*接口服务费",
        "spbm": "3040201030000000000",
        "je": 100,
        "sl": 0.01,
        "se": 0.99,
        "spsl": 100,
        "dj": 1,
        "dw": "次"
      },
      {
        "fphxz": 0,
        "hsbz": 1,
        "spmc": "*软件维护服务*接口服务费",
        "spbm": "3040201030000000000",
        "je": 100,
        "sl": 0.01,
        "se": 0.99,
        "spsl": 200,
        "dj": 0.5,
        "dw": "次"
      }
    ],
    hjje: 198.02,
    hjse: 1.98,
    jshj: 200
  };

  /*
   * 数电蓝票开具接口 文档
   * @link https://fa-piao.com/doc.html#api6?source=github
   */
  return await taxInvoice.api.blueTicket(invoiceParams);
}

/**
 * 获取pdf ofd xml 下载链接
 * @param fphm 发票号
 */
async function downloadPdfOfdXml(fphm: string) {
  /*
   * 获取销项数电版式文件 文档 PDF/OFD/XML
   * @link https://fa-piao.com/doc.html#api7?source=github
   */
  const pdfResponse = await taxInvoice.api.getPdfOfdXml({
    "nsrsbh": nsrsbh,
    "username": username,
    "fphm": fphm,
    "downflag": "4"
  });
  if (pdfResponse.code === 200) {
    console.log("发票下载结果:", pdfResponse.data);
  }
}

/**
 * 获取指定秒数后的时间字符串 (格式: YYYY-MM-DD HH:mm:ss)
 * @param addSeconds 需要增加的秒数 (例如: 300)
 */
function getFutureTime(addSeconds: number): string {
  const d = new Date();
  d.setSeconds(d.getSeconds() + addSeconds); // 在当前时间基础上增加秒数

  // 辅助函数:补零
  const pad = (n: number) => n.toString().padStart(2, '0');

  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

// 执行主函数
main();

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

About

发票sdk nodejs 电子发票/数电发票/全电发票/数电票/开票

Topics

Resources

License

Stars

Watchers

Forks

Packages