跳到正文

超越创新。超越边界。抵达实效。

语言
外观

那张删不掉的小票

德国税法要求收银台的每一笔交易都被签名并保存十年。没有删除。这一句话对数据库的塑造,超过任何一条产品需求。

作者
发布时间
阅读时长
4分钟

多数合规工作是搁在软件旁边的文书。德国的收银法不是这样。它会伸进数据库结构里,把它重新排布。

规则很短。收银台的每一笔交易都必须由一台经认证的技术安全设备——TSE——签名,而这条签名记录必须存活十年,税务机关随时索取时,都要能以规定的导出格式交出。

用工程师的眼睛再读一遍。没有删除这回事。

「不能删」对一个设计意味着什么

想一件再普通不过的事:一笔测试交易。每个系统都有一个预发布模式,每个开发者都曾在某个时刻把一笔测试订单放进生产环境,看看它能不能跑通。

在这里这么做,那笔测试订单就会在税务记录里待上十年。不是尴尬——是永久。它拿不掉,因为这台设备存在的全部意义就是任何东西都拿不掉。

于是「测试与生产是分开的」不再是一种约定,而变成一种结构。在这个项目里,一台收银设备要声明自己的环境是测试还是生产,而一条 CHECK 约束加上一个部分唯一索引,把同一时刻只能有一台生产设备变成数据库强制执行的保证。不是操作手册里的一条规矩。不是一个习惯。是数据库根本不会让您做的事。

这就是十年带来的差别。平常,您写的是能抓住 bug 的约束。在这里,您写的是能抓住那十年的约束。

从规范里拼装,不要从记忆里拼装

税务报文有一套 schema,而那套 schema 是公开的。这里的客户端是从 API 自己提供的规范里拼装它,而不是从任何人对字段名的印象里。

在您看到另一种做法之前,这听着像迂腐:一个开发者把规范读过一遍,凭记忆写出来,然后发布。测试通过了,因为测试也是凭同一份记忆写的。它在多年之后的稽查中失败,而且以一种谁也无法再复原的方式。

只有生产环境才会教你的细节

这里有一个值回票价的。

签名时间戳从设备返回时,用的是安全模块自己声明的格式——而那是模块的属性,不是标准的常量。这个项目里的模块报的是 unixTime:以数字表示的纪元秒。另一台配置不同的模块,完全可能发来一个 ISO 字符串。

所以代码两种都接受。没有人会预先设计出这一点。您是在某张凭证被打上 1970 年的日期时第一次学到的,然后把为什么写进文件里,好让下一个人不会出于好意又把它「简化」回去。

为稽查员写,不是为测试套件写

导出件里带着安全模块的证书和公钥。这不是因为格式要求某个字段必须填满,而是为了让稽查员能够离线、对照可信根来验证签名,而不必请被稽查的系统为自己作保。

把它省掉,验证签名就等于去信任正在被验证的那个东西。格式允许您省。这件事的意义不允许。

这条原则塑造了整份导出件。它是按税务机关列出的顺序打包的十六张表——先主数据,再日结,再逐条记录——而当它无法被拼装时,它会说出原因。一个未冻结的增值税率、一个未映射的税码、一个缺失的税号:每一个都成为结果上被记录下来的问题,而不是从调用栈某处抛出的异常。调用方的职责是记下这次失败的尝试,而不是崩溃。一份悄无声息失败的导出件,比一份明说自己失败的更糟。

为什么这不只是德国的问题

我们为埃及的电子发票、为沙特的 ZATCA 做同样的工作。格式不同,机关不同,期限不同。

那份纪律并无不同。凡是记录比产出它的软件活得更久的地方,记录本身就是产品——而写下它的代码,应当让多年以后、在一场争论之中、您并不在场解释时,仍然读得懂。

聊聊一个合规攸关的项目

全部文章

本页内容

那么,您的系统该往哪儿走?

一篇文章可以讲清某样东西怎么运作;它说不了这对您已经在跑的那套系统意味着什么。与一位工程师聊三十分钟,不是销售,也没有跟进序列。