本文为【金融科技工程】系列第 10 篇。前一篇讨论了支付网关如何把一次「扣款」安全送达,本篇向前推一步:在什么规则下计算「应扣多少」、以什么形式「开出账单和发票」、如何把「出海的税」算对。
引子:为什么计费比支付还难
如果说支付解决的是「钱如何安全流转」,那么订阅与计费解决的是「在复杂的商业规则下,该收多少、向谁收、什么时候收、开什么票」。对大多数工程师来说,接支付接口是一个有限问题——文档、签名、回调、对账,半年之内能做到熟练。但计费系统不一样:它是产品、销售、财务、税务、法务、合规同时下场的系统,是公司对外「商业模式」的直接投射。每一次打折、每一次涨价、每一次加入一个新的国家市场、每一次推出一个新的用量维度,都会在计费系统里落下一块伤疤。
几个具体的难点:
- 时间维度:订阅有「账期」(Billing Period),用量有「计量窗口」(Metering Window),发票有「开票期」(Invoice Period),税务有「申报期」(Tax Period)。四种时间都不一样,还要考虑时区、夏令时、闰秒。
- 金额维度:一个账单里既有固定费(订阅)、又有浮动费(用量),还有折扣、Credit、退款、代金券、税。这些都要按规则分摊(Proration)和凑整(Rounding),结果还要反过来校验到分。
- 合规维度:中国要开增值税发票(专票、普票),欧盟要区分 B2B/B2C 并判断是否 Reverse Charge,美国 sales tax 按 state+city+zip 三级,日本要考虑「适格请求书」制度,澳洲要区分 GST-free。
- 历史维度:价格一旦开出去,除极个别情况,永远不能回溯修改。一个 2022 年签的三年合同,必须用 2022 年那一版价格算到 2025 年。
这篇文章面向的是「计划或正在构建 SaaS、API、云服务计费系统」的工程团队——可能你们正在评估 Stripe Billing 和自研的取舍,可能你们正在把产品卖到第一个海外国家,也可能你们被财务催着解决「发票号跳号」的陈年老账。下文不会教你「怎么设计一张漂亮的定价页」,而是把引擎盖打开,看看 billing engine 内部这台机器是怎么运转的。
一、计费模型分类:从 Flat 到 Usage-Based
1.1 五种基本形态
在设计数据模型之前,先把业界常见的计费形态梳理清楚。不同计费形态决定了订阅记录、用量记录、账单生成时机完全不同。
| 模型 | 中文 | 典型产品 | 计算逻辑 |
|---|---|---|---|
| Flat-rate | 固定订阅 | Netflix、Notion Team | price_per_period |
| Per-seat | 按席位 | Slack、GitHub、Salesforce | seats × unit_price |
| Tiered | 分层 | CDN 流量阶梯价 | 落入某一层就整单按该层单价 |
| Graduated / Volume | 阶梯累进 | AWS、GCP、阿里云 | 每一层单价不同,用量跨层分段计算 |
| Usage-based (Pay-as-you-go) | 纯按量 | Twilio、Stripe API、OpenAI | usage × unit_price |
| Package + Overage | 套餐 + 超额 | 运营商流量套餐、API 配额 | 套餐内固定费,超出部分按量 |
Tiered 和 Graduated 在中文里都被翻译为「阶梯计价」,但计算方式差别极大,这是最容易搞错的一对概念。举一个 API 调用的例子:
- 0–10,000 次:¥0.10 / 次
- 10,001–100,000 次:¥0.08 / 次
- 100,001+ 次:¥0.05 / 次
用户用了 50,000 次:
- Tiered(分层):整单按第二层,50,000 × 0.08 = ¥4,000
- Graduated(阶梯累进):10,000 × 0.10 + 40,000 × 0.08 = ¥1,000 + ¥3,200 = ¥4,200
销售说「阶梯价」的时候,工程师一定要追问一句:是 tiered 还是 graduated。合同里、定价页里、计费引擎里这三处必须一致,否则 QA 的测试用例和销售谈出来的数永远对不上。
1.2 Prepaid vs Postpaid
正交于计费模型的另一个维度是「先付还是后付」:
- Prepaid(预付):用户先充值成「余额」或「Credits」,使用时扣减。好处是资金安全、坏账风险低;坏处是用户决策门槛高。OpenAI 在 2024 年把 API 从后付迁到了预付 Credits,一部分原因是为了解决全球范围的坏账。
- Postpaid(后付):先用后付,月底生成账单。好处是体验无感;坏处是坏账和 dunning 流程(催收)要做得非常扎实。AWS、GCP 默认都是 postpaid。
很多公司会混用:小额账户 postpaid(坏账风险可控),大企业账户 Annual Prepaid(一次性预付一年,给折扣),超高用量用户可能同时有 Commitment(承诺量预付)+ Overage(超量后付)。
1.3 折扣、Credit、Coupon 的优先级
真实的账单不是「单价 × 数量」那么简单,至少有四层价格变换:
- Catalog Price(目录价)
- Contract Price(合同协商价,针对大客户)
- Coupon / Promotion(活动折扣,通常带有时效)
- Credits(余额抵扣,可能来自充值、SLA 赔偿、运营赠送)
顺序不能搞反。工程上常见的错是「先减 credits 再算税」,结果税务口径下应纳税额偏低,审计时被认定为「漏税」。正确的顺序是先在 pre-tax 金额上做折扣,然后基于 post-discount 金额计算税,最后用 credits 冲抵 total-with-tax。Stripe、Chargebee 的实现都遵循这个顺序。
二、计量系统:Metering 的工程深坑
对于 usage-based 计费,计量系统是整个计费链路的最上游,也是最容易出错的地方。一次「漏计」或「重复计」在单次调用里看不出来,在月底账单上会被放大 N 万倍。
2.1 计量事件的基本模型
message UsageEvent {
string event_id = 1; // 幂等键,事件唯一 ID
string customer_id = 2; // 账户 ID
string resource_id = 3; // 订阅 ID / API Key / 实例 ID
string metric = 4; // api_calls / gb_transferred / cpu_seconds
double quantity = 5; // 用量值
int64 event_ts_ms = 6; // 业务发生时间(UTC ms)
int64 ingest_ts_ms = 7; // 接收时间
map<string,string> dimensions = 8; // region / sku / tier 等维度
string idempotency_key = 9; // 上游幂等键
}几个设计要点:
- event_ts vs ingest_ts 分离:前者决定「归属哪个账期」,后者决定「何时能开账」。两者可以相差小时级甚至天级(比如边缘节点断网后补传)。
- event_id 必须全局唯一且幂等:否则重传时会重复计费。
- dimensions 必须可扩展:计费维度会不断增加(比如从一个 region 变成 20 个 region、新增不同硬件规格)。
- quantity 用 double 还是 int64?——调用次数用 int64,字节数、秒数、token 数用 int64 更安全。涉及小数(比如按 GB-hour 记账)建议存放放大后的整数(如微秒、字节、兆字节整数),见本系列第 2 篇《钱的建模》的同类讨论。
2.2 管道架构:采集 → 去重 → 聚合
flowchart LR
A[业务服务<br/>API Gateway] -- UsageEvent --> B[Kafka<br/>raw-usage]
B --> C[Flink<br/>去重 + 窗口聚合]
C --> D[时序库<br/>ClickHouse/Druid]
C --> E[聚合结果<br/>aggregated-usage]
E --> F[计费引擎<br/>Rating]
F --> G[Billing DB<br/>invoice_lines]
D --> H[用户 Dashboard<br/>实时用量可视化]
B -.补偿.-> I[Late Event Handler]
I --> C
- raw 层:保留原始事件至少 13 个月(覆盖「年度账单 + 一个争议期」),用于审计、重算、纠纷回溯。
- 去重:基于
event_id用 RocksDB state 或 Redis SET 做 windowed dedup;窗口一般 24–72 小时,覆盖大部分重传场景。 - 聚合窗口:
- 滚动窗口(Tumbling):1 分钟/1 小时,用于实时大盘。
- 会话窗口(Session):比如「一次 WebSocket 连接计一次」「一次 VM 启动到停机算一个 session」。
- 计费窗口(Billing Window):按账期切分,跨账期事件单独处理。
- 迟到事件(Late Arrival)补偿:
- 设定 watermark 允许延迟(如 6 小时)。
- 超过 watermark 的事件写入「补偿账户」,下一期账单里以
[Prior Period Adjustment]明细出现。 - 如果事件极端迟到(比如月底后一周才到),在中国会计准则下属于「跨期收入」,需要做调整分录,不能直接塞到本月账单里。
2.3 OLAP + 时序:为什么要两个库
很多团队只用一个 ClickHouse 就想解决所有问题,跑一段时间会遇到:
- 用户 Dashboard 要
近 1 小时 / 1 分钟粒度的实时曲线 → 时序库强项。 - 计费引擎需要
按账期 × 账户 × metric × dimension的聚合结果 → OLAP 强项。 - 财务要历史数据的多维下钻分析 → OLAP/数仓强项。
拆成两条链路更合理:
| 用途 | 存储 | 粒度 | 保留期 |
|---|---|---|---|
| 实时 Dashboard | Prometheus / Druid | 秒—分钟 | 30–90 天 |
| 计费聚合结果 | Postgres / TiDB | 小时/天 + 账期 | 永久 |
| 原始事件 | S3/OSS + ClickHouse | 单条 | 13–36 个月 |
2.4 真实案例:AWS 和 Google 的 Metering 差别
AWS 的计费有一个众所周知的特性:账单不是实时的。你在 Console 里看到的「今日用量」通常延迟 24–48 小时,月底之后还会有最多 7–10 天的「Finalization Period」,这期间金额会微调。AWS 公开披露过其内部 billing pipeline 会批处理大约 每秒数百万条 metering 事件,并在 month-end close 时做最终对账(参考 AWS re:Invent 历年 ARC/FIN 专题)。
Google Cloud 更激进一点,Cloud Billing 的「near real-time」延迟约数小时,但月底同样有 finalization。阿里云、腾讯云的明细账单也有类似的「T+1 出账」机制。
这个延迟不是技术做不到实时,而是故意留出来的修正窗口:任何一个上游数据源(比如某 region 的网关)宕机延迟上报,都可能引起账单重算。如果你做一个号称「实时精确」的计费系统,反而会在第一次出故障时陷入巨大的对账噩梦。
2.5 一个关于「可重放」的工程原则
计量聚合的最高原则是可重放(Replayable):给定一批原始事件和一份定价版本,系统任意时刻重跑都应该得到一模一样的 invoice line。这个性质在以下场景救命:
- 用户投诉「你们多算了我 500 次 API 调用」。客服要能在不动生产的前提下,重放一段事件流给客户看清单。
- Bug 发生后,要能「拉回过去 30 天事件,用修好的代码重跑,和线上结果 diff」。
- 跨云迁移或底层存储换代时,新旧两套计量链要能对齐小数点后两位。
为了做到这点:
- Rating 必须是纯函数:输入事件 + plan + tax 版本,输出明细。不读当前时间、不查随机 DB 状态。
- 计费决策引用的任何状态都要版本化:plan、tax
rate、exchange rate、coupon、credits 状态都要带
as_of时间戳。 - 原始事件 13+ 个月内可检索:别只存聚合结果。
- Replay 工具是 Day-1 交付物:不要等出事再写。
三、订阅生命周期
3.1 状态机
stateDiagram-v2
[*] --> Trialing: 开始试用
Trialing --> Active: 试用转正
Trialing --> Canceled: 试用期内取消
Active --> PastDue: 账单支付失败
PastDue --> Active: Dunning 成功
PastDue --> Unpaid: Dunning 失败
Unpaid --> Canceled: 超出宽限期
Active --> Paused: 用户暂停
Paused --> Active: 恢复
Active --> Canceled: 用户主动取消
Canceled --> Active: 重新订阅 (Reactivate)
Canceled --> [*]
几个关键状态的业务含义:
- Trialing:试用期内,通常不收费但可以使用。要决定:试用期结束是「自动续费」还是「停止」。自动续费要在 T-3 天发邮件提醒(欧盟 Digital Services Act、美国 FTC 已在收紧此类要求)。
- Active:正常订阅状态。
- PastDue:账单到期未付。服务是否降级在这里是一个产品决策:SaaS 常见做法是保留 7–30 天访问权限继续 dunning,超过则降级为只读/只导出。
- Unpaid / Canceled:最终状态。Canceled 有「立即生效」和「账期末生效」两种,合同和 UI 必须说清楚。
- Paused:不收费、不使用。健身 App、教育 App 常用。注意 pause 的会计处理:本质是订阅期延长,不是新合同。
3.2 Proration:升降级的按比例扣费
假设用户在账期的第 10 天(总 30 天)从 Plan A(¥100/月)升级到 Plan B(¥300/月),proration 怎么算?业界几种做法:
Approach 1:Credit + Charge - 退还 Plan A 未使用部分:100 × (20/30) = 66.67 - 收取 Plan B 剩余部分:300 × (20/30) = 200.00 - 本次补差:200.00 - 66.67 = 133.33
Approach 2:账期末出一个合并账单 -
本账期账单里,Plan A 行
100 × 10/30 = 33.33,Plan B 行
300 × 20/30 = 200.00,合计 233.33。 -
用户原本已付 100,credit 里挂 -100,本月实际扣
133.33。
Approach 3:No Proration(不按比例) - 本期不找零,Plan B 下个账期开始生效。 - 仅在营销活动(试用转正)时使用。
工程上推荐 Approach 2,原因是每一行 invoice line 都是独立的账务凭证,退款、税务都可追溯。Stripe Billing、Chargebee 默认都是 Approach 2。
Python 示例:
from decimal import Decimal, ROUND_HALF_UP
from datetime import date
def prorate_amount(
full_price: Decimal,
period_start: date,
period_end: date,
effective_from: date,
effective_to: date | None = None,
) -> Decimal:
"""按天数比例计算。period 为账期,effective 为实际使用区间。"""
effective_to = effective_to or period_end
total_days = (period_end - period_start).days
used_days = (effective_to - effective_from).days
if total_days <= 0 or used_days <= 0:
return Decimal("0.00")
ratio = Decimal(used_days) / Decimal(total_days)
return (full_price * ratio).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
# 升级补差
credit_a = prorate_amount(Decimal("100"), date(2026,4,1), date(2026,5,1),
date(2026,4,10), date(2026,5,1)) # 66.67
charge_b = prorate_amount(Decimal("300"), date(2026,4,1), date(2026,5,1),
date(2026,4,10), date(2026,5,1)) # 200.00
delta = charge_b - credit_a # 133.33几个坑:
- 按秒还是按天 prorate?云厂商多半按秒(AWS EC2 per-second billing),SaaS 通常按天。合同要白纸黑字写清楚。
- Rounding 顺序:每一行各自 round 还是最后一次性 round?要和会计确认后写进 coding guideline。
- 多币种:升级时换了币种怎么处理?一般规则是「只允许 upgrade 不换币种」,换币种要走「取消 + 新建」。
3.3 升降级的 Anchor Date
如果用户在 4 月 10 日订阅(anchor = 10 日),那么账期是 4/10–5/10、5/10–6/10……
几个常见决策: - 是否把 anchor
对齐到月初?:财务更喜欢对齐(好对账),用户不一定喜欢(因为第一期要么短、要么被
prorate)。 - 2 月 31 日问题:1 月 31
日订阅的,2 月 anchor 是哪天?惯例是「每月最后一天」,即 2
月 28/29、3 月 31、4 月 30。 - 跨时区
anchor:建议所有 anchor 保存为 UTC
timestamp,按商户时区展示。闰秒在计费里实际很少引起问题(因为都
round 到天了),但测试环境一定要能注入「闰秒日
23:59:60」,避免出现 Java 某些版本 Instant
解析报错。
四、账单(Invoice)的生成与推送
4.1 账期触发与账单生成流水线
sequenceDiagram
participant Cron as Billing Scheduler
participant Agg as Usage Aggregator
participant Eng as Rating Engine
participant Tax as Tax Engine
participant Inv as Invoice Service
participant Pay as Payment Gateway
participant Mail as Email/Notification
Cron->>Agg: 关账期 (period cutoff)
Agg-->>Cron: usage_summary
Cron->>Eng: 生成 invoice draft
Eng->>Eng: 合并 subscription + usage + proration
Eng->>Eng: 应用 discount / coupon / credit
Eng->>Tax: 计算税
Tax-->>Eng: tax_lines
Eng->>Inv: invoice_draft
Inv->>Inv: 内部 QA/人工复核(可选)
Inv-->>Mail: 推送 PDF/通知
Inv->>Pay: 扣款(auto-collect)
Pay-->>Inv: 支付结果
Inv-->>Mail: 收据 / dunning
账单生成是个幂等、可重入、可回滚的流程。几点实现经验:
- Draft / Finalized 两阶段:draft 可以修改、删除;finalized 不可变。finalized 后才分配发票号、才推送支付。
- 每个订阅独立出账还是合并?大企业客户会要求「每个
BU 一张账单」,个人用户希望「全家桶合并一张」。数据模型里
invoice和subscription是 1:N,不要锁死成 1:1。 - 幂等键:
(customer_id, period_start, period_end)做唯一索引,防止定时任务重复跑导致一个账期两张账单。 - 预览(Upcoming Invoice):用户在月中改计划时,应该能看到「下期账单预计 X 元」。这需要把账单生成逻辑拆成纯函数,不要一路写库。
4.2 Dunning:支付失败的重试与催收
自动扣款失败是日常现象(信用卡到期、额度不足、银行 3DS 验证超时)。Dunning(催缴)策略一般长这样:
flowchart TD
A[Invoice 到期扣款失败] --> B{第 1 次重试<br/>+3 天}
B -->|成功| OK[Active]
B -->|失败| C{第 2 次重试<br/>+5 天}
C -->|成功| OK
C -->|失败| D[邮件通知用户<br/>更新支付方式]
D --> E{第 3 次重试<br/>+7 天}
E -->|成功| OK
E -->|失败| F[降级服务<br/>PastDue]
F --> G{人工介入 / 最终通知<br/>+14 天}
G -->|成功| OK
G -->|失败| H[取消订阅<br/>Unpaid]
H --> I[转催收 / 坏账核销]
重试策略有几种:
- Smart Retry:根据信用卡 decline reason
决定——
insufficient_funds在月初工资日重试;expired_card直接不重试,让用户主动换卡;do_not_honor尝试不同地区的收单行。 - Retry Schedule:经验值是「3–5–7 天」或「1–3–5–7–14 天」,不是越频繁越好;太频繁会被发卡行标记为可疑。
- Grace Period:7–30 天不等。B2B 大客户一般长,消费者短。
Stripe 官方文档披露了他们的 “Smart Retries”:根据机器学习模型选择最可能成功的重试时间,公开披露可把回收率提高 10% 左右。
4.3 账单数据模型
-- 订阅
CREATE TABLE subscriptions (
id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
plan_id BIGINT NOT NULL,
plan_version INT NOT NULL, -- 定价版本,不可变
status VARCHAR(16) NOT NULL, -- trialing/active/past_due/...
currency CHAR(3) NOT NULL,
anchor_at TIMESTAMP NOT NULL,
current_period_start TIMESTAMP NOT NULL,
current_period_end TIMESTAMP NOT NULL,
cancel_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
-- 账单
CREATE TABLE invoices (
id BIGINT PRIMARY KEY,
number VARCHAR(32) UNIQUE, -- 发票流水号,finalize 时分配
customer_id BIGINT NOT NULL,
status VARCHAR(16) NOT NULL, -- draft/open/paid/void/uncollectible
currency CHAR(3) NOT NULL,
period_start TIMESTAMP NOT NULL,
period_end TIMESTAMP NOT NULL,
subtotal BIGINT NOT NULL, -- 分
discount_total BIGINT NOT NULL DEFAULT 0,
tax_total BIGINT NOT NULL DEFAULT 0,
total BIGINT NOT NULL,
amount_paid BIGINT NOT NULL DEFAULT 0,
amount_due BIGINT NOT NULL,
due_at TIMESTAMP,
finalized_at TIMESTAMP,
paid_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
UNIQUE KEY uk_customer_period (customer_id, period_start, period_end)
);
-- 账单明细
CREATE TABLE invoice_lines (
id BIGINT PRIMARY KEY,
invoice_id BIGINT NOT NULL,
subscription_id BIGINT, -- 可为 null(一次性费用)
description VARCHAR(255),
quantity DECIMAL(20,6),
unit_amount BIGINT, -- 分
amount BIGINT NOT NULL, -- 分
period_start TIMESTAMP,
period_end TIMESTAMP,
proration BOOLEAN DEFAULT FALSE,
metric VARCHAR(64), -- 仅 usage 行
tax_rate_id BIGINT,
discount_ids JSON,
metadata JSON
);
-- 用量事件(归档到 OLAP,这里只放日聚合)
CREATE TABLE usage_daily (
customer_id BIGINT NOT NULL,
subscription_id BIGINT NOT NULL,
metric VARCHAR(64) NOT NULL,
day DATE NOT NULL,
quantity DECIMAL(24,6) NOT NULL,
dimensions JSON,
PRIMARY KEY (customer_id, subscription_id, metric, day)
);
-- 账期(显式物化有助于幂等)
CREATE TABLE billing_periods (
subscription_id BIGINT NOT NULL,
seq INT NOT NULL, -- 第几期
period_start TIMESTAMP NOT NULL,
period_end TIMESTAMP NOT NULL,
status VARCHAR(16) NOT NULL, -- open/closed/billed
invoice_id BIGINT,
PRIMARY KEY (subscription_id, seq)
);
-- 税率
CREATE TABLE tax_rates (
id BIGINT PRIMARY KEY,
jurisdiction VARCHAR(64) NOT NULL, -- 'DE'/'US-CA-SF'/'CN'
tax_type VARCHAR(32) NOT NULL, -- VAT/GST/SALES_TAX/CT
rate_bps INT NOT NULL, -- 基点:2000 = 20%
inclusive BOOLEAN NOT NULL, -- 价内 / 价外
effective_from DATE NOT NULL,
effective_to DATE
);几个设计点:
- 金额全部存分(integer):不要存 decimal 的元。浮点绝对不行,参见第 2 篇。
plan_version显式字段:plans 表可以改,但 subscription 必须记住签约时的版本。billing_periods物化:不要每次用 date 函数算,显式存下来,方便幂等与审计。invoice.number必须连续递增:中国税法、欧盟 VAT Directive 都要求「发票号不得跳号」。做分布式生成时不能简单用雪花算法,要做「号段 + 事务」。
4.4 发票号的分配
# 伪代码:发票号段分配
class InvoiceNumberService:
def __init__(self, db):
self.db = db
def allocate(self, country: str, series: str, year: int) -> str:
# SELECT ... FOR UPDATE 串行化
with self.db.transaction():
row = self.db.query(
"SELECT next_seq FROM invoice_number_seq "
"WHERE country=%s AND series=%s AND year=%s FOR UPDATE",
country, series, year,
)
seq = row["next_seq"]
self.db.execute(
"UPDATE invoice_number_seq SET next_seq = next_seq + 1 "
"WHERE country=%s AND series=%s AND year=%s",
country, series, year,
)
return f"{country}-{series}-{year}-{seq:08d}"为什么不能用雪花/UUID?因为税务局要求「按时间顺序连续」。为什么不能提前批量分配号段?因为分配了没用会留空洞;欧盟 VAT 审计会让你解释每一个缺失号。
五、发票(Fapiao / Tax Invoice)
在中国之外,“invoice” 通常指上一节的账单。在中国大陆语境下,需要把账单(Invoice / 对账单)和发票(Fapiao / 税务发票)严格区分开。
5.1 中国大陆:增值税发票
- 增值税专用发票(专票):B2B 交易用,购方可以抵扣进项税。对开票方要求严格:客户必须有一般纳税人资格、提供完整税号、银行账户等信息。
- 增值税普通发票(普票):B2C 或小规模纳税人用,不能抵扣。
- 数字化电子发票(数电票):2021 年起推广,2023 年之后大范围铺开。取消了税控盘,由「乐企」「税务 UKey」等平台对接。
- 税率:SaaS 服务属「信息技术服务」或「软件服务」,一般纳税人 6%,小规模纳税人 3%(阶段性减按 1%,以当年政策为准)。
工程对接要点:
- 发票通常不是计费系统直接开,而是调用第三方服务(航信、百望、高灯、亿企赢)或对接省级电子税务局。
- 开票接口是异步的(几分钟—几小时),要有状态机:
requested → processing → issued → failed → reverted。 - 红冲(作废/退款)有严格流程:当月可作废,跨月必须红冲。红冲也是异步。
- 一张 invoice(账单)可能对应多张 fapiao(一次开一张不够抬头长度、或按项目拆票)。模型上是 1:N。
sequenceDiagram
participant U as 用户
participant B as Billing
participant F as 开票服务
participant T as 税局/数电
U->>B: 提交开票信息(抬头/税号)
B->>F: 开票请求(idempotency_key)
F->>T: 上传
T-->>F: 发票代码/号码
F-->>B: issued
B-->>U: 发票 PDF/OFD
U->>B: 申请退款
B->>F: 红冲请求
F->>T: 红冲
T-->>F: 红冲完成
F-->>B: reverted
5.2 海外:VAT Invoice
英国、欧盟、澳洲等地的「税务发票(Tax Invoice)」本质是一张合规的 PDF,自己生成即可,不需要走国家平台。但内容有硬性要求:
- 开票方完整公司名、地址、税号(VAT number / ABN)
- 购方名字、地址(B2B 还要购方 VAT 号)
- 发票号(连续递增)、开票日期、供货日期(tax point)
- 每一行:品名、数量、单价、税前金额、税率、税额
- 合计:subtotal、tax total、grand total
- 若为 Reverse
Charge,应显式标注:
"Reverse charge, VAT to be accounted for by the recipient"
这类发票无需提交到政府平台(除了个别要求 e-invoicing 的国家,如意大利 FatturaPA、法国 2026 年起强制 e-invoicing、沙特 ZATCA phase 2),生成好直接邮件/ Dashboard 下载即可。
5.3 发票 PDF 生成
工程实现一般有两种:
- HTML → PDF:Chromium headless(puppeteer / playwright)或 wkhtmltopdf。好处是模板维护方便,坏处是渲染一致性差、字体嵌入需配置。
- 直接用 PDF 库:Python
reportlab、Gogofpdf、JavaiText。好处是可控、二进制体积小,坏处是排版代码写起来冗长。
规模一大,PDF 生成会成为瓶颈。建议:
- 异步化:finalize invoice → enqueue PDF
任务 → worker 拉取渲染 → 上传对象存储 → 写回
invoice.pdf_url。 - 模板引擎 + 字体预加载:中文字体 5–10MB,冷启动慢。
- 打防篡改水印:简单做法是 PDF 里嵌入签名(PAdES),或发票号对应一个可验证的 URL/二维码。
六、出海税务:VAT / GST / Sales Tax 全景
对出海的 SaaS 公司来说,税务是一个一旦犯错就非常昂贵的坑。2015 年欧盟 VAT MOSS 规则正式实施、2018 年美国最高法院 South Dakota v. Wayfair 判决、2023 年起多国纷纷实施 “digital services tax”——近十年海外税务对 SaaS 越来越不友好,也越来越复杂。
下文旨在给出工程视角的税务决策树,不构成税务建议。跨境税务问题请咨询持牌税务顾问。
6.1 欧盟 VAT(含 OSS/IOSS)
- 2015 年规则:电子服务按消费者所在国征税(destination principle),而不是供应者所在国。
- MOSS → OSS:2021 年 Mini-One-Stop-Shop 扩展为 OSS(One-Stop-Shop),企业在一个成员国注册 OSS 即可统一申报全欧盟销售给消费者的 VAT。
- B2B Reverse Charge:若购方是欧盟境内另一国的企业且提供了有效 VAT 号,供应方不收 VAT,而是由购方在自己国家做「反向征收」(自己申报销项同时抵扣进项)。这是欧盟单一市场的核心机制。
- VIES 校验:购方提供的 VAT 号必须通过欧盟委员会 VIES 系统校验(API 可用)。
伪代码:
def eu_vat(seller_country, buyer_country, buyer_vat_number, amount):
if buyer_country not in EU_COUNTRIES:
return TaxResult(rate=0, note="non-EU, zero-rated for goods export rules")
if buyer_country == seller_country:
# 国内销售,按本国 VAT
return TaxResult(rate=VAT_RATES[seller_country], note="domestic")
if buyer_vat_number and vies_is_valid(buyer_country, buyer_vat_number):
return TaxResult(rate=0, note="reverse charge")
# B2C 跨境:OSS,按买方国 VAT
return TaxResult(rate=VAT_RATES[buyer_country], note="OSS destination")6.2 英国 VAT
2020 年 Brexit 之后,英国独立于欧盟 VAT 体系:
- 英国本土 VAT 标准税率 20%。
- B2B Reverse Charge 机制类似欧盟,但要验证 GB VAT 号(HMRC 有公开 API)。
- 非英国境内企业卖 digital services 给英国消费者需注册 UK VAT(无起征点阈值)。
- MTD(Making Tax Digital)要求通过认证软件申报。
6.3 美国 sales tax:nexus 体系
美国没有联邦增值税。sales tax 是 50 个州各自立法,加上县、市、特区,全美组合出 10,000+ 个独立税率。
- Nexus:你必须在某州有「充分联系」才在该州有征税义务。传统上是「实体存在」(办公室、员工、仓库)。
- Wayfair 判决(2018):允许各州对 remote sellers 征税。目前大部分州采用 经济 nexus 阈值:过去 12 个月销售额超 $100,000 或交易数超 200 单,就需要在该州注册 sales tax 并开始征收。
- Source-based vs Destination-based:绝大多数州按买方所在地的税率(destination),少数州(如 AZ、TX 内州销售)按卖方所在地。
- Product taxability:SaaS 在某些州应税(NY、TX、WA),某些州不应税(CA、FL 对 pure SaaS 一般免税,但 2026 年前后多州仍在频繁调整)。
真实情况是:几乎没有一家出海 SaaS 公司会自研美国 sales tax 引擎,而是接入 Avalara / TaxJar(现 Stripe 旗下)/ Vertex 这类专业服务,每一次 invoice 生成时调用它们的 API 拿税率。
6.4 其他主要司法辖区
| 国家/地区 | 税种 | 税率(截至 2026 Q2,以当年法规为准) | 特点 |
|---|---|---|---|
| 日本 | 消費税 | 10%(标准)/ 8%(食品) | 2023 年起适格請求書(Qualified Invoice)制度 |
| 澳大利亚 | GST | 10% | 年销售额 A$75,000 以上强制注册 |
| 新西兰 | GST | 15% | 对非居民数字服务商有强制注册阈值 |
| 新加坡 | GST | 9%(2024 起) | Overseas Vendor Registration 制度 |
| 印度 | GST | 18%(SaaS) | 需注册 GSTIN,B2B reverse charge 类似 |
| 巴西 | ICMS + PIS/COFINS + ISS | 多税种叠加 | 极其复杂,基本必须找本地服务商 |
| 沙特 | VAT | 15% | ZATCA phase 2 e-invoicing 强制 |
6.5 税务引擎的工程架构
一个可用的海外税务引擎,至少要具备以下能力:
flowchart LR
A[Invoice Draft] --> B[Tax Resolver]
B --> C{客户画像}
C -->|B2B + valid VAT| D[Reverse Charge]
C -->|B2C 本国| E[本国税率]
C -->|B2C 跨境 EU| F[OSS]
C -->|US 客户| G[Avalara/TaxJar]
C -->|其他国家| H[国家税率表]
D --> I[生成 tax_lines]
E --> I
F --> I
G --> I
H --> I
I --> J[写回 invoice]
关键点:
- Tax Determination 必须独立服务:不要嵌进 billing engine,否则税规则一变要重发计费代码。
- 税率要版本化:和价格一样,一张已开出的发票即使税率后来调整,也不能重算。
- VAT 号校验要缓存:VIES 偶尔挂机,要有降级策略(允许 soft-fail 但标记待复核)。
- Place of Supply 判定证据:欧盟要求保留两项非矛盾证据(IP 地址 + 账单地址 / 信用卡签发国 / SIM 归属地)以证明客户所在国,并保存 10 年。
七、定价引擎设计:规则、DSL 还是硬编码
7.1 三种实现方式对比
| 方案 | 硬编码 | 规则引擎(Drools 等) | DSL(自研) |
|---|---|---|---|
| 开发速度 | 起步最快 | 中 | 最慢 |
| 业务自助改价 | 不行 | 一般(需要业务懂语法) | 好(可视化编辑) |
| 可测试性 | 好 | 一般 | 好 |
| 性能 | 最好 | 较差 | 好 |
| 适用阶段 | 0–1,业务单一 | 1–10,规则多变 | 10+,有专职 pricing 团队 |
多数中型 SaaS 最终会落到一个折中方案:
- Plan 结构用 DSL / JSON Schema 配置(如
{type:"graduated", tiers:[{up_to:1000, unit_price:10},...]})。 - Coupon / Credit / Promotion 用规则引擎(条件 + 动作)。
- 计算内核用纯函数 Go/Java/Rust,保证可单测、可重放、无副作用。
7.2 定价版本化:不可回溯
核心原则:任何已生效的订阅必须使用签约时的定价,后续任何改动都是新版本。
CREATE TABLE plans (
id BIGINT PRIMARY KEY,
code VARCHAR(64) NOT NULL,
version INT NOT NULL,
currency CHAR(3) NOT NULL,
spec JSON NOT NULL, -- 完整定价 DSL
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMP NOT NULL,
UNIQUE KEY uk_code_version (code, version)
);改价的流程应该是:
- 新建
(code, version=N+1)记录。 - 对老订阅:继续用
version=N。可选策略「老客户 X 个月内也涨价」→ 需要邮件通知、给机会取消,并且从 UI 告知。 - 新订阅:默认用
version=N+1。 - 历史账单的重新出具(比如客户投诉多算了):只能用
version=N重算。
7.3 促销与 Coupon 模型
CREATE TABLE coupons (
id BIGINT PRIMARY KEY,
code VARCHAR(64) UNIQUE NOT NULL,
kind VARCHAR(16) NOT NULL, -- percent/fixed/free_trial/free_units
value_bps INT, -- percent=1000 表示 10%
value_amount BIGINT, -- fixed=多少分
currency CHAR(3),
max_redemptions INT,
redeemed_count INT NOT NULL DEFAULT 0,
valid_from DATE,
valid_to DATE,
applies_to JSON, -- plan ids / products
duration VARCHAR(16), -- once/repeating/forever
duration_in_months INT,
metadata JSON
);
CREATE TABLE customer_credits (
id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
amount BIGINT NOT NULL, -- 分(正=余额,负=欠)
currency CHAR(3) NOT NULL,
source VARCHAR(32), -- topup/refund/gift/sla
applied_invoice_id BIGINT, -- 已用于哪张账单
expires_at TIMESTAMP,
created_at TIMESTAMP NOT NULL
);Credits 本质是账户的「预收款余额」。它和发票、退款、坏账都有复杂互动(详见《复式记账工程化》):
- 充值 Credits:
Dr 银行 / Cr 预收账款。 - 用 Credits 付
invoice:
Dr 预收账款 / Cr 主营业务收入 + 应交税费。 - Credits
过期:
Dr 预收账款 / Cr 营业外收入(通常收入确认有特殊规则)。
八、计量聚合与定价的代码示例
8.1 Go:Graduated(阶梯累进)Rating 引擎
package billing
import "math/big"
type Tier struct {
UpTo *int64 // nil 表示无上限
FlatFee int64 // 每 tier 固定费(单位:分)
UnitAmt *big.Rat // 每单位价格(分)
}
type GraduatedPlan struct {
Tiers []Tier
}
// Rate 计算 graduated 计费:usage 跨多个 tier 分段计算。
func (p *GraduatedPlan) Rate(usage int64) int64 {
remaining := usage
total := big.NewRat(0, 1)
var prevCap int64
for _, t := range p.Tiers {
if remaining <= 0 {
break
}
var take int64
if t.UpTo == nil {
take = remaining
} else {
cap := *t.UpTo - prevCap
if remaining < cap {
take = remaining
} else {
take = cap
}
prevCap = *t.UpTo
}
tierAmt := new(big.Rat).Mul(t.UnitAmt, big.NewRat(take, 1))
total.Add(total, tierAmt)
total.Add(total, big.NewRat(t.FlatFee, 1))
remaining -= take
}
// 最后四舍五入到分
num, denom := total.Num(), total.Denom()
q := new(big.Int).Quo(num, denom)
rem := new(big.Int).Mod(num, denom)
two := big.NewInt(2)
if new(big.Int).Mul(rem, two).Cmp(denom) >= 0 {
q.Add(q, big.NewInt(1))
}
return q.Int64()
}单测必须覆盖:
- 恰好落在 tier 边界(9999 / 10000 / 10001)。
- 单 tier、跨两个 tier、跨所有 tier。
- 0 用量、负用量(正常不会出现,但测试要能 fail fast)。
8.2 Python:从事件到日聚合
from dataclasses import dataclass
from collections import defaultdict
from typing import Iterable
@dataclass(frozen=True)
class Event:
event_id: str
customer_id: str
subscription_id: str
metric: str
quantity: float
event_ts_ms: int
def aggregate_daily(events: Iterable[Event], seen_ids: set[str]) -> dict:
"""
对事件按 (customer, subscription, metric, day) 聚合。
seen_ids 用于幂等去重,外部传入(例如 Redis SET / RocksDB)。
"""
buckets = defaultdict(float)
for e in events:
if e.event_id in seen_ids:
continue
seen_ids.add(e.event_id)
day = e.event_ts_ms // (86400 * 1000)
key = (e.customer_id, e.subscription_id, e.metric, day)
buckets[key] += e.quantity
return dict(buckets)生产环境中 seen_ids 通常用 Bloom
filter + Redis SET:Bloom filter
过滤大部分重复(99.9%),剩下的用 Redis SET
做精确去重。Bloom filter 带 1–7 天 TTL 滚动。
8.3 Python:Invoice 生成(简化版)
from decimal import Decimal
from datetime import date
from typing import List
def build_invoice(
subscription,
usage_summary: List[dict],
proration_lines: List[dict],
discounts: List[dict],
credits: Decimal,
tax_resolver,
period_start: date,
period_end: date,
) -> dict:
lines = []
# 1. 订阅固定费
lines.append({
"desc": f"{subscription['plan_code']} subscription",
"quantity": 1,
"unit_amount": subscription["flat_price_cents"],
"amount": subscription["flat_price_cents"],
"period_start": period_start,
"period_end": period_end,
})
# 2. Proration 行(升降级)
for p in proration_lines:
lines.append(p)
# 3. 用量行
for u in usage_summary:
amt = rate_graduated(u["plan"], u["quantity"]) # 见 8.1
lines.append({
"desc": f"{u['metric']} usage",
"quantity": u["quantity"],
"unit_amount": None,
"amount": amt,
"metric": u["metric"],
"period_start": period_start,
"period_end": period_end,
})
subtotal = sum(l["amount"] for l in lines)
# 4. 折扣(pre-tax)
discount_total = 0
for d in discounts:
if d["kind"] == "percent":
cut = int(subtotal * d["value_bps"] / 10000)
else:
cut = d["value_amount"]
discount_total += cut
pre_tax = subtotal - discount_total
# 5. 税
tax_lines = tax_resolver.resolve(
buyer=subscription["customer"],
seller=subscription["seller_entity"],
amount=pre_tax,
lines=lines,
)
tax_total = sum(t["amount"] for t in tax_lines)
total_with_tax = pre_tax + tax_total
# 6. Credits(post-tax 冲抵)
applied_credit = min(credits, Decimal(total_with_tax))
amount_due = total_with_tax - int(applied_credit)
return {
"lines": lines,
"subtotal": subtotal,
"discount_total": discount_total,
"tax_lines": tax_lines,
"tax_total": tax_total,
"total": total_with_tax,
"credit_applied": int(applied_credit),
"amount_due": amount_due,
"period_start": period_start,
"period_end": period_end,
"status": "draft",
}注意几个顺序:
- Discount 一定在 pre-tax 上做(除非合同另有约定)。
- Tax 基于 post-discount 金额。
- Credits 在 total-with-tax 上冲抵。
九、对接第三方:Stripe Billing、Chargebee、Recurly、Zuora
9.1 对比
| 平台 | 强项 | 弱项 | 典型客户 |
|---|---|---|---|
| Stripe Billing | 开发者体验最好、与 Stripe 支付一体化、Smart Retries、Tax 内建 | 企业级合同谈判、ERP 集成稍弱 | 中小 SaaS、开发者工具 |
| Chargebee | 订阅业务特性最丰富、多网关、多币种、多实体、发票定制 | 价格、仪表盘有时慢 | 成长期 SaaS,尤其 B2B |
| Recurly | 信用卡回收率模型成熟、Revenue Recognition 强 | 国际化略弱 | 北美订阅媒体 |
| Zuora | 企业级、ASC 606 收入确认、复杂合同能力 | 贵、重、部署周期长 | 上市公司、复杂合同(电信、IoT) |
| Maxio (ex-Chartmogul + SaaSOptics) | SaaS metrics + billing 整合 | 品牌认知 | 中型 SaaS 财务导向 |
9.2 国内为什么没有成熟的通用 SaaS 计费方案
可以列出几条原因:
- SaaS 市场本身成熟度不够:国内 SaaS 营收规模相对较小(2023 年中国 SaaS 市场约为美国市场的十分之一,多家市场研究机构口径略有差异),通用计费平台的商业天花板不足以支撑一个「中国版 Stripe Billing」。
- 发票体系的深度耦合:数电票对接是强本地化工作,每家公司的财务流程定制化极重。
- 支付多样性:微信、支付宝、对公转账、分期、花呗、白条……真正能做到一站式接入的寥寥。
- 合规个案多:每个行业(教育、医疗、跨境电商)都有自己的开票细则,通用产品难以覆盖。
结果是:国内 SaaS 企业大多自研计费 + 接入本地开票服务商(航信、百望、高灯),或者使用有赞、微盟等偏零售电商方向的订阅付费模块。
9.3 自研 vs SaaS:决策模型
评估 Stripe Billing / Chargebee 等 SaaS 计费产品 vs 自研,常见的误区是只比「License 费」。真正的成本结构至少要算上:
- License 成本:Stripe Billing 按交易量收 0.4–0.7%(2026 Q2 官网披露,以签约条款为准)。当 GMV 上到千万美元级,年费就会显著。
- 集成成本:用 Stripe 大约 1–2 个工程师季度;从零自研需要 4–6 人年起,且要踩遍后文「坑点」。
- 二次开发成本:Stripe / Chargebee 的 webhook、metadata、扩展能力是有限的。一个罕见定价模式(如「承诺量 + 双阶梯 + 跨 region 合并」)SaaS 可能做不到。
- 数据主权与锁定:一旦在 Stripe Billing 里积累了百万订阅记录,迁出成本极高——客户托管 token、发票历史、税务 ID、credits 都绑着。建议从第一天就把 Stripe 的对象镜像到自家库。
- 合规责任转移:Stripe Tax、TaxJar 承担 tax 计算正确性,出错由服务商兜底部分责任。自研出错由公司自己承担税务罚款。
决策原则简化为一句话:业务形态收敛时 SaaS 优先;业务形态仍在剧烈演化时留自研的接口。
9.4 计费系统与财务系统的边界
一个容易混淆的问题:计费(Billing)、账务(Ledger/General Ledger)、收入确认(Revenue Recognition) 三者什么关系?
- Billing:向客户收多少钱、什么时候收、开什么票。看客户。
- Ledger:公司内部的复式记账,每笔经济业务记到科目。见第 3 篇。看公司资产负债表。
- Rev Rec:GAAP (ASC 606) / IFRS 15 规定的收入确认规则——现金收到不等于收入确认。比如一次性预付 12 个月 $1200,现金当月入账,但收入每月按 $100 分摊确认。看利润表。
这三者在工程上应该是三套独立的数据流,由 billing 作为事件源向 ledger 和 rev rec 发事件:
flowchart LR
B[Billing System] -->|invoice.finalized| L[Ledger]
B -->|payment.succeeded| L
B -->|invoice.finalized| R[Rev Rec Engine]
R -->|journal entries| L
L --> GL[总账 / 财务报表]
不要让 billing 系统直接生成会计分录;也不要让 ledger 倒推账单。
十、工程坑点(真实故事)
- 时区漂移:Billing 定时任务用了 JVM
默认时区,部署到新加坡机房
Asia/Singapore,所有账期偏移 8 小时;月底出账比合同晚了半天,大客户投诉「过期用量被算进下月」。修复:所有时间字段一律 UTC 存,仅展示层按customer.display_tz格式化;定时任务注解@Scheduled(zone = "UTC")。 - 跨月事件归属:Redis 延迟 30 分钟,一批
23:59 的事件到达 Kafka 已是
00:15,被归到次月。修复:以
event_ts(业务时间)归属账期,不用ingest_ts。账期结账必须显式等待 watermark(常用 6h)。 - 多币种:客户把账单货币从 USD 换到 EUR,历史 credits 是 USD 记账,抵扣时按「实时汇率」还是「入账汇率」?选择:保留原币种 credits,展示时给出「按当前汇率约合 X 元」,实际结算时按冻结汇率(ECB/PBoC 中间价 T-1)。
- 退款对 invoice 的影响:全额退款 → invoice 整单作废还是保留+挂 Credit Note?推荐:保留原 invoice,生成 Credit Note 冲销;对账更清晰,中国红冲制度也要求这么做。
- Delayed Usage:海外 region
的边缘节点断网 3 天,3
天的用量集中在恢复后涌入。直接塞进当月会导致突刺,还可能跨了账期。解决:按
event_ts分发到正确账期;已关账的账期产生的用量自动挂「Prior Period Adjustment」到下期账单明细。 - Rounding 悖论:10 个用户每人 $9.999 / 月,分别 round 是 10.00,合计 100.00;整单合计 99.99 后 round 还是 99.99。规则:逐行 round,subtotal 为各行之和;绝不能「先加总再 round」,否则会和明细对不上。
- 发票号跳号:finalize 时分配了号,PDF 生成失败回滚;下次重试又分配了新号,空档出来一个。修复:号段只分配一次;PDF 失败不影响号段;中国口径下缺口需要年底向税局报备或红冲冲销。
- Plan 改名引起的账单迷惑:市场把 “Pro”
改名为 “Business”,工程师在
plans.code就地 update。老客户账单开出来品名和合同不符。修复:plans.display_name字段和code分离;code不可变。 - 促销叠加爆炸:coupon A 八折、coupon B 减 50、credits 余额 200,用户按不同顺序组合,产生四种不同 total。修复:产品侧明确「单个 invoice 仅可用一个 coupon」;credits 在最后一步、对税后金额冲抵。所有折扣计算在服务端,前端只展示。
- 数据保留合规:欧盟 VAT 要求发票保存 10 年,中国税法至少 30 年(账簿、凭证),GDPR 又要求用户注销后删除 PII。妥协:PII 可擦除,但发票上的「抬头、税号、地址」保留(有税务抗辩法律依据)。
十一、选型与落地清单
起步阶段(0–10k MRR): - 直接用 Stripe Billing + Stripe Tax,出海侧几乎全覆盖。 - 国内侧用微信/支付宝常规接入 + 第三方开票。 - 不自研计费引擎。
成长阶段(10k–1M MRR): - 用 Chargebee / Stripe Billing 作为计费 SaaS,但自己存一份 shadow DB:订阅、发票、事件全量拷贝到自家库,方便对账、报表、审计。 - 海外税交给 Avalara / TaxJar。 - 自研 metering(raw events + Flink 聚合),只把聚合结果推给 Stripe/Chargebee。
规模阶段(1M MRR 以上): - 自研 rating engine、invoice engine,Stripe 仅做支付网关。 - 引入专业 Revenue Recognition 工具(如 Sage Intacct、NetSuite、Zuora Revenue)。 - 税务用 Avalara/Vertex,但内部有 fallback 税率表。 - 审计 trail:每一次定价修改、手工账单调整都要有记录,走审批流。
落地检查表:
十二、与本系列其他文章的交叉引用
- 金额与币种:《钱的建模》;多币种 invoice 的根基。
- 复式记账:《复式记账工程化》;invoice/credit 如何在总账里落账。
- 幂等与事务:《幂等、事务与一致性》;metering 去重、invoice 重跑的理论支撑。
- 支付网关:《支付网关设计》;自动扣款与回调。
- 清算结算:《清算 vs 结算》;收到钱到入账的时间差。
- 对账:《对账系统工程》;invoice/payment/ledger 三方对账。
十三、参考资料
- Stripe Docs – Billing: https://stripe.com/docs/billing
- Stripe Docs – Smart Retries & Automatic Collection: https://stripe.com/docs/billing/revenue-recovery/smart-retries
- Stripe Tax: https://stripe.com/docs/tax
- Chargebee Product Docs: https://www.chargebee.com/docs/2.0/
- AWS Billing and Cost Management: https://docs.aws.amazon.com/awsaccountbilling/
- Google Cloud Billing: https://cloud.google.com/billing/docs
- EU Commission – VAT OSS: https://taxation-customs.ec.europa.eu/online-services/online-services-and-databases-taxation/oss_en
- EU VIES VAT number validation: https://ec.europa.eu/taxation_customs/vies/
- HMRC – VAT for digital services: https://www.gov.uk/guidance/vat-on-digital-services
- South Dakota v. Wayfair, Inc. (2018): https://www.supremecourt.gov/opinions/17pdf/17-494_j4el.pdf
- Avalara Developer Docs: https://developer.avalara.com/
- 国家税务总局《全面数字化的电子发票试点有关事项》: https://www.chinatax.gov.cn/
- Japan NTA – Qualified Invoice System: https://www.nta.go.jp/english/taxes/consumption_tax/
- ATO – GST on digital services: https://www.ato.gov.au/Business/International-tax-for-business/GST-on-imported-services-and-digital-products/
- ZATCA e-invoicing (Saudi Arabia): https://zatca.gov.sa/en/E-Invoicing/Pages/default.aspx
上一篇:《支付网关设计:路由、限流、补单、异步通知、签名与防重放》
下一篇:《清算 vs 结算 vs 资金归集:T+0/T+1、NDS、PvP/DvP》
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【金融科技工程】金融科技工程全景:从支付到交易所的系统分类与读图
金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。
【金融科技工程】钱的建模:金额精度、币种、会计单位、多语言金额
在代码里正确地表示"一笔钱"远比看起来难。本文系统梳理金额的数值建模(浮点、定点、Decimal、最小单位)、币种标准(ISO 4217)、本地化显示、汇率换算与数据库存储,并给出 Go、Python、Java、Rust 的工程化示例。
【金融科技工程】复式记账工程化:科目、分录、余额、对账
把 500 年历史的复式记账翻译成工程师可以落地的数据模型、SQL 表结构与余额计算策略,覆盖充值、下单、退款、分润、红包、多币种与冲销的真实场景,并对比 TigerBeetle、beancount、Ledger CLI、Square LedgerDB、Stripe Ledger 等开源与工业实现。
【金融科技工程】账务数据库设计:TiDB/OceanBase/Postgres 下的分片、索引、热点账户
账务(Ledger)数据库是金融系统最硬的那块骨头。本文从 RPO/RTO 目标出发,对比 PostgreSQL、MySQL、OceanBase、TiDB、CockroachDB、Oracle、TigerBeetle 等主流选型,讲分片维度、热点账户拆解、索引设计、冷热归档、MVCC 并发控制与审计合规,辅以蚂蚁、Stripe、PayPal、Square 的真实演进路径。