发票管理链的核心不是把发票信息存成一条普通记录,而是让每次状态变更都变成不可篡改的历史事件。DeInvoice 基于 Node.js 构建,利用 SHA-256 哈希指针把发票从登记到归档的全过程串成链。这样数据库里即使有人修改某一笔流转记录,后续链上的哈希值也会对不上,审计时能立刻发现异常。

一、为什么要拆分发票主数据和链记录
发票本身有固定字段:发票代码、发票号码、购买方、销售方、金额、税额、开票日期。这些属性更新频率低,关系型数据库很适合存储和查询。如果直接在这些字段上增加状态列,虽然简单,但无法回答某个状态是谁在什么时间改的,也无法发现某条记录是否被后台直接修改。因此把主数据和流转记录拆开:invoices 表保存当前状态与业务属性,invoice_chain 表保存不可变事件。
invoice_chain 每条记录至少包含 id、invoiceId、previousHash、timestamp、action、operator、dataHash。previousHash 指向上一条链记录,dataHash 是对本条业务数据做哈希摘要,timestamp 记录操作时间,operator 记录操作人。创世记录 previousHash 可以固定为 000...0。这样的数据结构让查询当前发票状态仍然很快,而审计链可以独立导出和验证。
class Invoice {
constructor(invoiceCode, invoiceNumber, amount, tax, buyer, seller) {
this.invoiceCode = invoiceCode;
this.invoiceNumber = invoiceNumber;
this.amount = amount;
this.tax = tax;
this.buyer = buyer;
this.seller = seller;
this.status = 'DRAFT';
this.createdAt = Date.now();
}
}
class ChainRecord {
constructor(invoiceId, previousHash, action, operator, data) {
this.invoiceId = invoiceId;
this.previousHash = previousHash;
this.action = action;
this.operator = operator;
this.timestamp = Date.now();
this.data = data;
this.dataHash = '';
}
}
二、用Node.js实现哈希链写入与状态机
哈希计算使用 crypto.createHash('sha256')。生成 dataHash 时需要将数据规范化,避免对象属性顺序影响哈希。可以用 JSON.stringify 传入固定顺序的字段,或先构建规范对象。previousHash 与 dataHash 一起组成当前记录的 hash,再传给下一条。这样链上任何一条记录被改动,后续所有 hash 都会变化。
状态流转必须有状态机限制。合法路径可以定义为 DRAFT -> ISSUED -> VERIFIED -> REIMBURSED -> ARCHIVED。每一次流转创建一条新的 ChainRecord,previousHash 取当前 invoice 的 lastHash,action 记录状态转换,operator 记录操作者。如果传入非法转换,例如从 DRAFT 直接到 ARCHIVED,直接抛出异常,避免脏数据写入。
const crypto = require('crypto');
function sha256(input) {
return crypto.createHash('sha256').update(input).digest('hex');
}
function buildRecordHash(record) {
const raw = [
record.invoiceId,
record.previousHash,
record.action,
record.operator,
record.timestamp,
record.dataHash
].join('|');
return sha256(raw);
}
function createChainRecord(invoice, action, operator, data) {
const dataHash = sha256(JSON.stringify(data));
const record = new ChainRecord(invoice.id, invoice.lastHash, action, operator, data);
record.dataHash = dataHash;
record.hash = buildRecordHash(record);
invoice.lastHash = record.hash;
return record;
}
const allowedTransitions = {
DRAFT: ['ISSUED'],
ISSUED: ['VERIFIED'],
VERIFIED: ['REIMBURSED'],
REIMBURSED: ['ARCHIVED'],
ARCHIVED: []
};
function transition(invoice, nextStatus, operator) {
const validNext = allowedTransitions[invoice.status] || [];
if (!validNext.includes(nextStatus)) {
throw new Error('Invalid invoice transition from ' + invoice.status + ' to ' + nextStatus);
}
const record = createChainRecord(invoice, nextStatus, operator, {
from: invoice.status,
to: nextStatus
});
invoice.status = nextStatus;
return record;
}
三、REST API 与链完整性校验
用 Express 暴露接口。创建发票时自动写入创世记录:previousHash 使用固定值 0.repeat(64),action 为 CREATE。状态流转接口根据发票ID和下一状态调用 transition,把新记录追加到 invoice_chain 表。获取链接口返回该发票全部链记录,按时间升序排列。验证接口重新计算每条记录哈希,并检查当前记录 previousHash 是否等于上一条 hash。
验证逻辑不能只看最后一条,必须逐条遍历。即使攻击者同时修改某条记录和它的后继 previousHash,前一条 hash 与修改后的 previousHash 可能不匹配;要完全伪造必须重算整条链,代价较高。验证失败时返回具体第几条记录不匹配。生产环境还可以把链记录导出成只读文件或同步到对象存储,再做定期核对。
const express = require('express');
const app = express();
app.use(express.json());
const invoices = new Map();
const chains = new Map();
app.post('/invoices', (req, res) => {
const { invoiceCode, invoiceNumber, amount, tax, buyer, seller } = req.body;
const invoice = new Invoice(invoiceCode, invoiceNumber, amount, tax, buyer, seller);
invoice.id = 'INV-' + Date.now();
const genesis = createChainRecord(invoice, 'CREATE', 'system', {
invoiceCode, invoiceNumber, amount, tax, buyer, seller
});
invoices.set(invoice.id, invoice);
chains.set(invoice.id, [genesis]);
res.status(201).json({ invoiceId: invoice.id, status: invoice.status });
});
app.post('/invoices/:id/transition', (req, res) => {
const invoice = invoices.get(req.params.id);
if (!invoice) return res.status(404).json({ error: 'invoice not found' });
try {
const record = transition(invoice, req.body.nextStatus, req.body.operator);
chains.get(invoice.id).push(record);
res.json({ status: invoice.status, lastHash: invoice.lastHash });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
function verifyChain(records) {
for (let i = 0; i < records.length; i++) {
const record = records[i];
const expectedHash = buildRecordHash(record);
if (expectedHash !== record.hash) {
return { valid: false, brokenIndex: i, reason: 'hash mismatch' };
}
if (i > 0 && record.previousHash !== records[i - 1].hash) {
return { valid: false, brokenIndex: i, reason: 'previous hash mismatch' };
}
}
return { valid: true, totalRecords: records.length };
}
app.get('/invoices/:id/verify', (req, res) => {
const records = chains.get(req.params.id) || [];
res.json(verifyChain(records));
});
app.listen(3000, () => console.log('DeInvoice API listening on 3000'));
四、用签名和备份提升审计可信度
哈希链能发现数据库内部修改,但不能防止掌握数据库权限的人直接改完链后重新计算所有哈希。要增强可信度,可以在每条链记录写入时,由操作者用私钥对该记录 hash 做签名,验证接口再用对应公钥验签。Node.js 的 crypto.sign 和 crypto.verify 支持 RSA 或 Ed25519。签名数据包括 invoiceId、timestamp、dataHash 和前序 hash,避免签名被重放。
另一个容易忽略的是链尾风险。如果整个表被删除或覆盖,验证接口返回空数组,仍然可能被误以为正常。因此需要把最新的 lastHash 备份到独立存储或写入只读日志。企业场景可定期把链记录导出为 JSON 文件放到对象存储,并在调度任务里比对。DeInvoice 适合作为中小企业的发票台账模块,如果要达到财务级审计标准,还要加入权限隔离、操作日志和定期校验报告。
const crypto = require('crypto');
function signRecord(privateKey, record) {
const sign = crypto.createSign('SHA256');
sign.update(record.hash);
sign.end();
record.signature = sign.sign(privateKey, 'hex');
return record;
}
function verifyRecordSignature(publicKey, record) {
const verify = crypto.createVerify('SHA256');
verify.update(record.hash);
verify.end();
return verify.verify(publicKey, record.signature, 'hex');
}
DeInvoice 的实现思路可以推广到合同、工单、资产流转等需要审计链的场景。核心不是区块链共识,而是哈希指针加状态机加签名。Node.js 轻量、事件驱动,适合快速落地这类内部工具。后续可以接入前端看板,把链验证结果以可视化方式呈现给审计人员,让每一张发票的流转路径都清晰可查。