D08 发票域 · 设计文档

本域总览见《账号计费中心 · 结构大纲》。本文为该域完整设计:定位与边界 / 角色与依赖 / 数据模型 / 核心流程 / 接口契约 / 关键约束与验收标准。 章节顺序理由:先数据模型(事实结构)→ 再核心流程(结构之上的运行路径)→ 最后接口契约(流程的对外投影)。


1. 定位与边界

定位:账期费用的收据化环节。回答三个问题——「这个账期该开多少」(账期费用汇总对量)、「开出来的是哪一张票」(发票实体与发票号)、「票开错了怎么退回去」(作废重开)。申请流与发票实体严格分离:申请是请求与处置的过程,发票是开票结果,一张申请可以被驳回零次或多次开票,也可以在作废后退回重开。

关键口径

概念 承载 语义
申请 invoice_applications 请求流:收票主体 + 账期 + 金额 + 票种 + 抬头;状态机主体
发票 customer_invoices 开票结果:发票号、开票时刻、抬头快照;不出现在申请列表以外
账期 period_start / period_end 日粒度闭区间,含首含尾
金额 amount_cents 分、整数;与跨域汇总源的元口径按 1 元 = 100 分换算
抬头 title_info 抬头与税号;缺省取组织企业资料
票种 invoice_type 仅两值:普票 / 专票
收票主体 organization_id 收票主体是组织;资金主体是钱包,二者一一对应
欠费态 customers.overdue_since 开票前置拦截的读侧判据(口径见 §4.3)

边界

归属口径:本域端点 = 内部运营链 /internal/invoice-applications(5 个)+ 运营面 /api/account/ops/invoices/applications(5 个)。两通道共用同一实现。


2. 角色与依赖

能力矩阵

能力 运营面 内部运营链(持运营令牌的调用方) 门户用户 计费路由与计量方
代客建申请 ✓(scope saas:ops:invoice) ✓ ✗ ✗
客户侧提交申请 ✗ ✗ ✓(申请人落 applicant_user_id) ✗
查申请台账(筛选 + 附组织名与发票号) ✓ ✓ ✗ ✗
开票 ✓ ✓ ✗ ✗
驳回 ✓ ✓ ✗ ✗
作废重开 ✓ ✓ ✗ ✗

上游依赖

上游 依赖内容 语义
租户与组织域 组织存在性、组织名、信用代码、实名等级、主体性质 收票主体与抬头缺省值、票种门禁的判定输入
钱包与账务域 由组织反查钱包、欠费态标记 发票实体挂钱包主体;欠费账期不可开
计量与计费域 账期用量与金额汇总 申请金额的核对数据源(人工核对)
实名与合规域 开票实名门禁判定 普票需主体达 L1、专票需企业达 L2

下游被引用

引用方 引用内容 语义
运营管理域 变更动作与操作人审计行 申请与发票的全部处置动作留痕,供运营台与合规追溯
用户自助与门户域 发票列表与票面字段 客户视角的发票展示(金额按分口径的展示规范详见 D12)
账期账单消费方 发票金额与账期区间 账期账单的「票面」一侧;金额与用量对量以计量与计费域口径为准

3. 数据模型

3.1 域内关联总览

收票主体,删除受限收票主体,删除受限资金主体,删除受限客户侧申请人,可空开票回填,可空

organizations

invoice_applications

uuid

id

PK

uuid

organization_id

FK

收票主体

uuid

applicant_user_id

FK

客户侧申请人,可空

date

period_start

账期首日

date

period_end

账期尾日

bigint

amount_cents

金额,分

string

invoice_type

普票 / 专票

jsonb

title_info

抬头与税号

string

status

submitted / issued / rejected

string

handler_note

人工处理记录,上限 500

uuid

invoice_id

FK

开票后回填,作废时清空

timestamp

created_at

timestamp

handled_at

最近一次处置时刻

timestamp

updated_at

customer_invoices

uuid

id

PK

uuid

customer_id

FK

资金主体,删除受限

uuid

organization_id

FK

收票主体,删除受限

string

invoice_no

全局唯一,开票时生成

date

period_start

date

period_end

bigint

amount_cents

金额,分

string

invoice_type

普票 / 专票

jsonb

title_info

抬头快照

string

status

issued / void

timestamp

created_at

timestamp

issued_at

开票时刻

customers

users

图 1

关联线是单向指针:申请可指向一张发票,发票不反向持有申请。作废重开时申请指针被清空并重新指向新发票,旧发票行原样留存供追溯。

3.2 域间引用

收票主体与抬头缺省值收票主体与资金主体资金主体反查客户侧申请人,弱引用可空金额核对数据源(只读)欠费态只读判定

invoice_applications
申请流

租户与组织域
organizations

customer_invoices
发票实体

钱包与账务域
customers

账号与身份域
users

计量与计费域
usage_daily

图 2

3.3 表清单(2 表)

表 职责 关键字段
invoice_applications 申请流与状态机主体:账期 + 金额 + 票种 + 抬头 + 处置结果 organization_id / applicant_user_id / period_start / period_end / amount_cents / invoice_type / title_info / status / handler_note / invoice_id / handled_at / updated_at
customer_invoices 开票结果:发票号 + 票面快照 + 生命周期状态 customer_id / organization_id / invoice_no / period_start / period_end / amount_cents / invoice_type / title_info / status / issued_at

3.4 设计约束

  1. 金额为分整数:amount_cents 为分口径整数列,不接受小数;跨域汇总源为元、6 位小数,两者在开票前按 1 元 = 100 分对齐(§4.8)。
  2. 发票号全局唯一:invoice_no 上建唯一约束,跨组织、跨账期、跨票种一律不重复;唯一约束是最终防线,生成规则只负责低撞号概率(§4.3)。
  3. 发票号可空:invoice_no 在发票实体建立时即写入,可空仅为兼容历史基线缺省;业务上不存在「已开票而无号」的发票。
  4. 申请状态缺省为 submitted:建申请即入可处理态;两表的状态列都是字符串列,取值由服务层枚举把关,不依赖数据库层检查约束。
  5. 发票状态缺省为 pending:本域写入路径只产生 issued 与 void 两值;pending 是表级缺省的历史基线值,不表述业务态。
  6. 收票主体与资金主体删除受限:申请与发票对组织的引用、发票对钱包的引用均为受限删除;不允许删组织或删钱包连带抹掉票据。
  7. 申请人对用户表置空删除、申请对发票置空删除:申请人离场不影响申请与票据;发票行被删(受控维护场景)时申请指针置空,不连带删除申请。
  8. 抬头是快照:title_info 为文档型列;申请上的抬头在开票时复制到发票实体,此后组织企业资料变更不影响已开发票票面。
  9. 申请不设账期唯一约束:同一收票主体、同一账期允许存在多条申请(换票种、换抬头、拆分金额),冲突由处置层的状态机与人工核对消解,而不由数据库唯一约束隐式拒绝。
  10. 处置时刻单一:handled_at 记录最近一次处置时刻,三个处置动作都会推进它;申请列表按 created_at 降序返回,行附组织名与已开发票号。

4. 核心流程

4.1 申请创建(两条入口合一)

关系库实名与合规域发票域运营面 / 内部运营链关系库实名与合规域发票域运营面 / 内部运营链alt[票种与实名等级不匹配][门禁通过]alt[主体不存在][主体存在]建申请:收票主体 / 账期 / 金额(分)/ 票种 / 抬头 / 操作人读收票主体404 organization_not_found按主体性质与实名等级判定票种门禁拒绝(专票需企业 L2;普票需 L1;个人不可专票)422 company_real_name_required / personal_real_name_required / 400 personal_cannot_vat_invoice落申请行:status = submitted、抬头缺省取组织资料同事务落审计:申请已创建 + 操作人线索200 申请行(含组织名,发票号为空)
图 3

4.2 创建前置判定链

否是否是是否否是否是

建申请请求

金额是否大于 0

422 入参拒绝(金额须为正整数分)

收票主体是否存在

404 organization_not_found

尾日是否早于首日

400 end_date_before_start

票种是否为首尾两值之一

400 invalid_invoice_type

实名等级是否满足票种

422 / 400 实名门禁拒绝:专票需企业 L2,普票需 L1

落申请:status = submitted

图 4

4.3 开票

关系库发票域运营面 / 内部运营链关系库发票域运营面 / 内部运营链alt[资金主体不存在][资金主体欠费(欠费态标记非空)][可开]alt[申请不存在][申请不处于可处理态][申请可处理]开票:申请标识 + 处理记录 + 操作人按申请标识取行锁(FOR UPDATE)404 invoice_application_not_found409 invoice_application_already_processed由收票主体反查资金主体404 customer_not_found400 overdue_not_invoiceable,申请保持可处理态生成发票号:前缀 + 账期尾日 + 随机段落发票实体:票面快照 + status = issued + 开票时刻回写申请:status = issued、发票指针、处置时刻、处理记录同事务落审计:发票已开 + 发票号 + 金额 + 操作人线索200 申请行(含发票号与发票标识)
图 5

4.4 申请状态机

开票驳回作废重开

submitted
可处理

issued
已开票

rejected
已驳回

图 6

4.5 作废重开

关系库发票域运营面 / 内部运营链关系库发票域运营面 / 内部运营链alt[申请不存在][申请非已开票态或发票指针为空][指向的发票行不存在][可作废]作废重开:申请标识 + 处理记录 + 操作人按申请标识取行锁(FOR UPDATE)404 invoice_application_not_found409 invoice_application_not_voidable409 invoice_application_not_voidable发票行置作废态(原号保留,票据留存可追溯)申请回退:status = submitted、清空发票指针、推进处置时刻同事务落审计:发票已作废 + 原发票号 + 操作人线索200 申请行(发票号为空,可再次开票)
图 7

4.6 驳回

否是

驳回请求:申请标识 + 处理记录

按申请标识取行锁

申请是否可处理态

409 invoice_application_already_processed

申请置已驳回 + 推进处置时刻 + 记处理记录

同事务落审计:申请已驳回

不生成任何发票实体、不生成发票号

图 8

4.7 操作人归属与外键安全

否是是否

处置请求携带操作人标识

标识是否为合法 UUID

操作人按空处理(不报错)

该标识是否存在于本仓用户表

操作人落审计的实外键引用

操作人外键置空 + 跨服务操作人线索记入审计明细

业务变更照常提交

图 9

4.8 账期费用汇总作为开票数据源

否是

账期区间:首日与尾日(日粒度闭区间)

按日聚合用量:日用量与计量金额

按类型聚合钱包流水:费用构成

量钱是否能对上

人工核对差异:先查推送窗口与对账重跑,不开票

取该账期应付金额

由元换算为分:1 元 = 100 分,对齐后写入申请金额

人工复核申请金额不高于账期实际消耗

申请进入可处理态,等待开票

图 10

4.9 幂等与并发收敛

并发同申请串行重复开票而状态非可处理驳回而状态非可处理作废而状态非已开票匹配

同一申请被重复处置 / 并发处置

是否持有申请行锁

后到者阻塞在行锁上

先到者提交后锁重估:读到最新已处理状态

409 已处理 / 不可作废

动作与当前状态是否匹配

409 invoice_application_already_processed

409 invoice_application_already_processed

409 invoice_application_not_voidable

执行并把状态推进为确定值

图 11

4.10 抬头与开票主体

否是未开票已开票

建申请:抬头信息

提交方是否显式给出抬头

缺省取组织企业资料:组织名 + 信用代码

按提交值落申请抬头

开票时复制到发票实体(票面快照)

抬头是否需要更正

另建申请(申请抬头无单独编辑动作)

作废重开:旧票作废后按新申请抬头重开

图 12

5. 接口契约

5.1 内部运营链(内部令牌的运营范围,5 个端点)

方法 路径 语义
GET /internal/invoice-applications 申请台账:按创建时刻倒序,可按收票主体与状态筛选,分页上限 200;行附组织名与已开发票号
POST /internal/invoice-applications 建申请(代客):校验主体 / 金额 / 账期 / 票种 / 实名门禁 → submitted
POST /internal/invoice-applications/{application_id}/issue 开票:欠费账期不可开;同事务落发票实体(issued + 发票号)并回写申请
POST /internal/invoice-applications/{application_id}/reject 驳回:submitted → rejected,不产生发票实体
POST /internal/invoice-applications/{application_id}/void 作废重开:原发票置 void,申请退回 submitted,可重新开票

5.2 运营面(saas:ops:invoice 权限范围,5 个端点)

方法 路径 语义
GET /api/account/ops/invoices/applications 发票申请台账(与内部通道同源同实现),行附组织名与发票号
POST /api/account/ops/invoices/applications 代客建申请(运营代客户发起 → submitted)
POST /api/account/ops/invoices/applications/{application_id}/issue 开票(欠费账期不可开)
POST /api/account/ops/invoices/applications/{application_id}/reject 驳回(无发票实体产生)
POST /api/account/ops/invoices/applications/{application_id}/void 作废重开(发票作废 + 申请退回)

5.3 客户侧入口(归属说明)

方向 承载 语义
客户提交 申请行上的客户侧申请人列 客户侧提交的申请携带申请人;代客申请该列为空,操作人在审计
客户查看 用户自助与门户域 发票列表与票面字段的展示由门户承载(展示规范详见 D12)

6. 关键约束与验收标准

6.1 约束

  1. 申请与发票分离:申请是流,发票是结果;发票实体只由开票动作创建,被驳回的申请永不产生发票实体。
  2. 可处理态唯一:仅 submitted 可被开票或驳回;已开票的申请不可再驳回,已驳回的申请不可再开票或再驳回(一律 409)。
  3. 一申请一有效票:开票与回写同事务,发票指针同事务建立;作废时指针与发票状态同事务一并变更。
  4. 作废可追溯:作废只改发票状态,发票行与发票号整体留存;重开必然生成新号,旧号不复用、不覆盖。
  5. 发票号全局唯一:号面 = 前缀 + 账期尾日 + 随机段;唯一约束兜底,撞号显式失败。
  6. 欠费不开票:欠费态标记非空的客户,其申请开票被 400 拒,申请保持可处理态;作废与驳回不受此约束。
  7. 金额单位为分:申请与发票金额列一律为分整数;与元口径汇总源换算按 1 元 = 100 分取整到分,链路中不出现二进制浮点中间量。
  8. 抬头开票即冻结:抬头随开票复制为票面快照;更正抬头只能另建申请或作废重开。
  9. 处置串行化:开票、驳回、作废共用同一取单入口并施加行锁;并发同申请只有先到者成功,后到者得到 409。
  10. 操作人可空但不丢线索:操作人不在本仓用户表时外键置空、线索入审计明细,绝不因外键约束把正常处置打成 500。
  11. 变更必留审计:建申请、开票、驳回、作废四类动作与业务变更同事务落审计(动作、目标类型、目标标识、明细)。
  12. 主体引用受限删除:收票主体与资金主体的引用为受限删除;申请人对用户表、申请对发票为置空删除。
  13. 门户与运营面同源:客户侧展示与运营台账消费同一份数据,不存在两套发票事实。
  14. 票种受实名门禁:专票需企业实名达 L2,普票需主体达 L1;个人主体恒不可开专票。

6.2 验收标准

# 验收口径
1 建申请后立即可见:状态为可处理态、发票号与发票标识为空,且审计中有一条「申请已创建」
2 收票主体不存在时建申请:404;金额为 0 或负数:入参 422 拒绝;尾日早于首日:400;票种非枚举值:400
3 主体实名等级不足时建申请:专票需企业 L2(否则 422),普票需 L1(否则 422),个人开专票恒拒
4 开票成功后:发票实体状态为 issued 且带发票号,申请状态为已开票、发票指针非空、处置时刻被推进
5 发票号满足「前缀 + 账期尾日 + 8 位随机段」,且跨组织不重复;人为构造同号写入被唯一约束拒绝
6 欠费态标记非空的客户开票:400,申请保持可处理态,且不产生任何发票实体
7 同一申请重复开票:第二次 409,发票实体总数不增加
8 并发对同一申请发起两次开票:恰好一次成功、另一次 409,发票实体恰好一张
9 驳回成功后:申请为已驳回、处理记录落库,该组织下发票实体数不增
10 已驳回的申请再开票或再驳回:均 409
11 非已开票态发起作废:409;已开票发起作废:原发票置作废且号不变,申请退回可处理态且发票指针清空
12 作废后重开:新票号与作废票号不同,同一组织下发票实体为两张(一作废一已开),作废票状态不被改写
13 作废事务中任一步失败:发票状态与申请状态一并回滚,不出现半废或半开态
14 外部操作人(不在本仓用户表中)发起开票:返回 200 且正常出票,审计操作人外键为空、明细中留有跨服务操作人线索
15 内部运营链无令牌或令牌不含运营范围:全部 5 个端点均 403
16 运营面权限范围不匹配(如只有信用额度范围):发票台账 403
17 台账筛选:按主体与状态筛选后条数正确,行内含组织名;已开票行带发票号,未开票行为空
18 抬头口径:未显式给抬头时申请抬头等于组织名与信用代码;开票后修改组织名,已开发票的票面抬头不变
19 金额口径:申请金额以分为单位落入申请与发票两表,元到分的换算按 1 元 = 100 分取整到分,无中间浮点误差
20 已开发票不因欠费态变化而被改写:欠费只在开票动作上拦截,已开票的申请状态保持已开票

6.3 待后续设计细化的开放项

  1. 金额自动校验:「申请金额不高于账期实际消耗」的自动判定需等计价口径落定,当前为人工核对与处理记录留痕。
  2. 客户侧提交入口的编排:客户侧提交与查看的位置、权限与频次限制归用户自助与门户域,本域只提供申请人归属与抬头快照口径。
  3. 抬头编辑语义:是否允许在未开票状态下更正申请抬头(而非另建申请),以及抬头字段的规范化与校验(税号格式、抬头长度)。
  4. 重复申请的去重口径:同一主体同一账期存在多条未处置申请时的合并或提示规则;当前不设账期唯一约束。
  5. 开票数据源的自动化:账期汇总自动发起开票的条件(封账时点、对账通过阈值、异常人工介入),以及分口径与元口径对账的全自动闭合。
  6. 票据外部状态的回接:外部开票处理端的开票结果、拒开与红冲结果如何落回本域(当前本域只承载人工开票的结果记录)。