# Phase 1 Data Model: 订单 ezPay 电子发票开立 **Date**: 2026-06-16 ## 新增表:pos_order_invoice(订单电子发票,与 pos_order 1:1) > DDL 写入 `updatesql/sql.md`,由开发者手动执行(项目规范)。当前态表:一笔订单至多一行;作废后状态置「作废」、允许重开(同行覆盖,MVP 不留历史明细)。 | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT PK AUTO | 主键 | | order_id | BIGINT NOT NULL | 关联 `pos_order.id`(UNIQUE) | | order_no | VARCHAR(64) | 冗余 `pos_order.ddId`,便于排查 | | store_id | BIGINT NOT NULL | 冗余 `pos_order` 所属门店 id(关联 `pos_store.id`) | | invoice_category | VARCHAR(8) | 发票类型:`B2C` / `B2B` | | buyer_name | VARCHAR(100) | 买方名称(B2C=客户名/邮箱名,B2B=公司名) | | buyer_ubn | VARCHAR(16) | 买方统一编号(统编 8 码,B2B 必填) | | buyer_email | VARCHAR(200) | 接收邮箱(B2C 无载具时用) | | carrier_type | VARCHAR(8) | 载具类型:`0`手机条码/`1`自然人凭证/`2`ezPay会员载具(可空) | | carrier_num | VARCHAR(64) | 载具号码(carrier_type 非空时必填) | | invoice_number | VARCHAR(16) | ezPay 发票号(开立成功后填,字轨号段分配) | | random_num | VARCHAR(8) | 防伪随机码(ezPay 返回) | | invoice_status | TINYINT NOT NULL DEFAULT 0 | `0`未开 / `1`已开 / `2`失败 / `3`作废 | | total_amt | INT | 含税总额(= 订单实付 amount) | | sales_amt | INT | 销售额(未税,= round(total/1.05)) | | tax_amt | INT | 税额(= total - sales) | | fail_reason | VARCHAR(500) | 开票/作废失败原因 | | invoice_url | VARCHAR(500) | 发票查看凭证 / 链接 | | apply_time | DATETIME | 客户申请开票时间 | | issue_time | DATETIME | 开立成功时间 | | invalid_time | DATETIME | 作废时间 | | create_time | DATETIME | 创建时间 | | update_time | DATETIME | 更新时间 | | create_by | VARCHAR(64) | 创建人 | | update_by | VARCHAR(64) | 更新人 | **索引**: - `UNIQUE KEY uk_order_id (order_id)` —— 防重复开票(D5) - `KEY idx_status (invoice_status)` - `KEY idx_store (store_id)` ## 状态机(invoice_status) ``` 开票成功 作废(ezPay invalid 成功) ┌─────────────────┐ ┌──────────────────┐ ▼ │ ▼ │ [0 未开] ──成功──> [1 已开] ──作废──> [3 作废] │ │ │ 失败 │ 失败 ▼ ▼ [2 失败] <──重试─── (失败/作废/未开 均可重试开票) └──重试成功──> [1 已开] ``` - 开票前置:`invoice_status ∈ {0 未开, 2 失败, 3 作废}` 才允许发起开票;`1 已开` 拒绝(FR-005)。 - 重开:作废(3) 后可再次开票 → 状态先置 0、再走开票成功置 1(同行,新 invoice_number 覆盖旧号)。 ## 复用实体(不改) - **PosOrder**(`pos_order`):开票来源。取 `id`/`ddId`/`amount`/`food`/`state`(=3 完成)/`payStatus`(=1)/门店归属(`shId`/`mdId`)。门店 id 判定:依据 CLAUDE.md 商家类型口径(普通/夜市商家用 `shId`,摊位用 `mdId`)对齐 `pos_store`。 - **PosStoreEzpay**(009,`pos_store_ezpay`):开票凭证来源。仅 `ezpayStatus=2 且 isEnabled=1` 的门店可开票;取 `merchantId`/`hashKey`/`hashIv` 构造 `EzPayConfig`。 - **PosStore**(`pos_store`):`invoice_exempt=1` 的门店不提供开票(009)。 ## 金额拆分(开票时计算,不入订单表) 按 research D1(**不含运费**,订单构成 `amount = foodAmount − 优惠 + freight`): ``` invoice_total = amount - freight // 商品实付(已扣优惠、不含运费) total_amt = invoice_total // 含税总额 sales_amt = round(invoice_total / 1.05) // 销售额未税 tax_amt = total_amt - sales_amt // 税额 ``` 逐商品明细由 `food` JSON 组装(D2),优惠按比例分摊到各商品使 ΣItemAmt = total_amt(D3);运费不进发票。