本域总览见《账号计费中心 · 结构大纲》。本文为该域完整设计:定位与边界 / 角色与依赖 / 数据模型 / 核心流程 / 接口契约 / 关键约束与验收标准。 章节顺序理由:先数据模型(事实结构)→ 再核心流程(结构之上的运行路径)→ 最后接口契约(流程的对外投影)。
定位:账期费用的收据化环节。回答三个问题——「这个账期该开多少」(账期费用汇总对量)、「开出来的是哪一张票」(发票实体与发票号)、「票开错了怎么退回去」(作废重开)。申请流与发票实体严格分离:申请是请求与处置的过程,发票是开票结果,一张申请可以被驳回零次或多次开票,也可以在作废后退回重开。
关键口径
| 概念 | 承载 | 语义 |
|---|---|---|
| 申请 | invoice_applications |
请求流:收票主体 + 账期 + 金额 + 票种 + 抬头;状态机主体 |
| 发票 | customer_invoices |
开票结果:发票号、开票时刻、抬头快照;不出现在申请列表以外 |
| 账期 | period_start / period_end |
日粒度闭区间,含首含尾 |
| 金额 | amount_cents |
分、整数;与跨域汇总源的元口径按 1 元 = 100 分换算 |
| 抬头 | title_info |
抬头与税号;缺省取组织企业资料 |
| 票种 | invoice_type |
仅两值:普票 / 专票 |
| 收票主体 | organization_id |
收票主体是组织;资金主体是钱包,二者一一对应 |
| 欠费态 | customers.overdue_since |
开票前置拦截的读侧判据(口径见 §4.3) |
边界
overdue_since 的置位与清除只在钱包变更事务末尾发生;本域只读该标记。归属口径:本域端点 = 内部运营链 /internal/invoice-applications(5 个)+ 运营面 /api/account/ops/invoices/applications(5 个)。两通道共用同一实现。
能力矩阵
| 能力 | 运营面 | 内部运营链(持运营令牌的调用方) | 门户用户 | 计费路由与计量方 |
|---|---|---|---|---|
| 代客建申请 | ✓(scope saas:ops:invoice) |
✓ | ✗ | ✗ |
| 客户侧提交申请 | ✗ | ✗ | ✓(申请人落 applicant_user_id) |
✗ |
| 查申请台账(筛选 + 附组织名与发票号) | ✓ | ✓ | ✗ | ✗ |
| 开票 | ✓ | ✓ | ✗ | ✗ |
| 驳回 | ✓ | ✓ | ✗ | ✗ |
| 作废重开 | ✓ | ✓ | ✗ | ✗ |
X-Operator-Id。上游依赖
| 上游 | 依赖内容 | 语义 |
|---|---|---|
| 租户与组织域 | 组织存在性、组织名、信用代码、实名等级、主体性质 | 收票主体与抬头缺省值、票种门禁的判定输入 |
| 钱包与账务域 | 由组织反查钱包、欠费态标记 | 发票实体挂钱包主体;欠费账期不可开 |
| 计量与计费域 | 账期用量与金额汇总 | 申请金额的核对数据源(人工核对) |
| 实名与合规域 | 开票实名门禁判定 | 普票需主体达 L1、专票需企业达 L2 |
下游被引用
| 引用方 | 引用内容 | 语义 |
|---|---|---|
| 运营管理域 | 变更动作与操作人审计行 | 申请与发票的全部处置动作留痕,供运营台与合规追溯 |
| 用户自助与门户域 | 发票列表与票面字段 | 客户视角的发票展示(金额按分口径的展示规范详见 D12) |
| 账期账单消费方 | 发票金额与账期区间 | 账期账单的「票面」一侧;金额与用量对量以计量与计费域口径为准 |
关联线是单向指针:申请可指向一张发票,发票不反向持有申请。作废重开时申请指针被清空并重新指向新发票,旧发票行原样留存供追溯。
| 表 | 职责 | 关键字段 |
|---|---|---|
| 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 |
amount_cents 为分口径整数列,不接受小数;跨域汇总源为元、6 位小数,两者在开票前按 1 元 = 100 分对齐(§4.8)。invoice_no 上建唯一约束,跨组织、跨账期、跨票种一律不重复;唯一约束是最终防线,生成规则只负责低撞号概率(§4.3)。invoice_no 在发票实体建立时即写入,可空仅为兼容历史基线缺省;业务上不存在「已开票而无号」的发票。submitted:建申请即入可处理态;两表的状态列都是字符串列,取值由服务层枚举把关,不依赖数据库层检查约束。pending:本域写入路径只产生 issued 与 void 两值;pending 是表级缺省的历史基线值,不表述业务态。title_info 为文档型列;申请上的抬头在开票时复制到发票实体,此后组织企业资料变更不影响已开发票票面。handled_at 记录最近一次处置时刻,三个处置动作都会推进它;申请列表按 created_at 降序返回,行附组织名与已开发票号。title_info。invalid_amount」,防绕过入参层的调用路径。customers.overdue_since 非空即拦(400 overdue_not_invoiceable),拦截失败后申请保持可处理态、不产生任何发票实体。该标记是派生标记(只在钱包变更事务末尾重算),此处取的是「客户当前处于欠费态」,即开票时点的服务状态口径;这与生命周期出口的债务判据(只认现金余额为负)是两个不同用途:出口拦的是未清偿债务,开票拦的是账期服务状态,故此处按标记判定,不改写为现金判据。INV + 账期尾日的年月日八位 + 8 位十六进制随机段(大写),定长 19 字符;尾日段使同账期票据在号面上可归堆,随机段使同秒并发不撞号。全局唯一由 invoice_no 上的唯一约束兜底,跨组织不重号;随机段撞号(概率量级 1/2^32)会由唯一约束冲突显式失败,不静默改号。submitted / issued / rejected;issued 是唯一可作废的状态,作废后退回 submitted(回环)。rejected 在处置语义上为终态:已驳回的申请既不能再开票也不能再驳回(均 409),需要重新开票时另建申请。rejected:作废只能对自己开出的票发起。submitted 后再走 §4.3,新发票号为新建实体的号,与已作废的旧号必然不同;旧票行状态保持作废、号不被复用。issued,进入 409 分支;从机制上不可能出现一申请两票。| 方法 | 路径 | 语义 |
|---|---|---|
| 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,可重新开票 |
forbidden;操作人经请求头 X-Operator-Id 传入,可空。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 |
作废重开(发票作废 + 申请退回) |
| 方向 | 承载 | 语义 |
|---|---|---|
| 客户提交 | 申请行上的客户侧申请人列 | 客户侧提交的申请携带申请人;代客申请该列为空,操作人在审计 |
| 客户查看 | 用户自助与门户域 | 发票列表与票面字段的展示由门户承载(展示规范详见 D12) |
submitted 可被开票或驳回;已开票的申请不可再驳回,已驳回的申请不可再开票或再驳回(一律 409)。| # | 验收口径 |
|---|---|
| 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 | 已开发票不因欠费态变化而被改写:欠费只在开票动作上拦截,已开票的申请状态保持已开票 |