# API Contracts: 用户发票抬头管理 **Date**: 2026-07-24 后端 REST 接口契约。挂在 `InfoInvoiceController`(`@RequestMapping("/system/invoice")`),风格镜像 `InfoAddressController`(`@Anonymous @Auth` + `@RequestHeader String token` + `JwtUtil.getusid(token)` 取 userId)。实体 `InfoInvoice` 即请求体 / 响应体(无专门 DTO/VO,与 InfoAddress 一致)。 > 字段格式 / 必填由 jakarta 校验注解(`@NotBlank`/`@Pattern`/`@Email`)标在 `InfoInvoice` 实体上,springfox 自动识别为 Swagger 的 required/pattern(前端在 Knife4j `/doc.html` 可见字段约束);接口与字段完整含义、示例、按类型的必填条件见本文件。Controller 另用 `@Api`/`@ApiOperation` 给出接口描述。 > 说明:实体在 `ruoyi-system` 模块(无 swagger 依赖),故字段级描述走校验注解 + 本文档,与 010 `ApplyInvoiceDto` 同一约定。 ## 核心约定:按 category 区分两套字段 `category` 是类型区分依据(新增/修改的入参分支、列表渲染分支都以它为准): | category | 含义 | 必传字段 | 不传字段 | |---|---|---|---| | **B2C** | 个人发票 | titleName、buyerName、carrierType、carrierNum(载具=2 时再加 buyerEmail) | buyerUbn | | **B2B** | 公司发票 | titleName、buyerName、buyerUbn、buyerEmail | carrierType、carrierNum | --- ## App 端接口(JWT 鉴权,userId 强制隔离) ### GET `/system/invoice/getinvoice` 列出当前用户全部抬头(按 id 倒序)。 **入参**:`@RequestHeader String token`。 **响应** `AjaxResult` data:`List`。**前端按每行的 `category` 区分类型并渲染对应字段**: | 字段 | B2C 行 | B2B 行 | |---|---|---| | `category` | `"B2C"` | `"B2B"` ← 类型判别字段 | | `titleName` | 有 | 有 | | `buyerName` | 个人姓名 | 公司名 | | `buyerUbn` | `null`(不展示) | 统编(展示) | | `buyerEmail` | 载具=2 时有值,否则 `null` | 邮箱(展示) | | `carrierType` | `0`/`1`/`2`(展示载具) | `null`(不展示) | | `carrierNum` | 载具号码(展示) | `null`(不展示) | > 渲染建议:列表项以 `titleName` 为主标题,副标题按 category 走——B2B 显示「公司名 · 统编」,B2C 显示「姓名 · 载具类型描述」。 ### POST `/system/invoice/invoice` 新增或更新(`saveOrUpdate`:有 id 走更新、无 id 走新增)。 **入参**:`@RequestHeader String token` + `@RequestBody InfoInvoice`(字段格式由 `@Valid` 先校验、条件必填与类型互斥由 service 强校验)。 **完整字段表(前端据此传参)** | 字段 | 类型 | 含义 | 格式 / 示例 | B2C | B2B | |---|---|---|---|---|---| | id | Long | 主键 | 新增不传;修改必传(`101`) | 同左 | 同左 | | titleName | String | 抬头备注名 | 任意,`公司-美食達` | ✅必填 | ✅必填 | | category | String | 发票类型 | `B2C` / `B2B` | ✅必填=`B2C` | ✅必填=`B2B` | | buyerName | String | 买方名称 | B2C 个人姓名 / B2B 公司名 | ✅必填 | ✅必填 | | buyerUbn | String | 统一编号 | 8 位数字 `12345678` | ❌不传 | ✅必填 | | buyerEmail | String | 邮箱 | `ms@example.com` | ⚪载具=2 时必填,其余可空 | ✅必填 | | carrierType | String | 载具类型 | `0`手机条码/`1`自然人凭证/`2`ezPay会员 | ✅必填 | ❌不传 | | carrierNum | String | 载具号码 | 随 carrierType(见下) | ✅必填 | ❌不传 | | userId | Long | 用户id | 后端按 token 自动填,**前端不传** | — | — | **载具号码格式(carrierNum,随 carrierType)** - `0` 手机条码:以 `/` 开头,如 `/ABC1234` - `1` 自然人凭证:`2 位大写字母 + 14 位数字`,如 `AB12345678901234` - `2` ezPay 会员载具:会员账号(非空),且须同时带 `buyerEmail` **处理**:① `getusid(token)` → `setUserId` 强制覆盖(防越权);② `@Valid` 字段格式校验;③ `validateInvoiceProfile` 条件必填 + 类型互斥强校验;④ 更新时校验该 id 属于当前 user;⑤ `saveOrUpdate`。 **响应**:成功 `{code:200, data:id}`;校验失败 `{code:500, msg:"<具体原因>"}`。 **请求示例** B2C / 手机条码载具: ```json { "titleName": "个人-手机条码", "category": "B2C", "buyerName": "王小明", "carrierType": "0", "carrierNum": "/ABC1234" } ``` B2C / ezPay 会员载具(须带邮箱): ```json { "titleName": "个人-会员载具", "category": "B2C", "buyerName": "王小明", "carrierType": "2", "carrierNum": "C1597485444", "buyerEmail": "xm@example.com" } ``` B2B 公司: ```json { "titleName": "公司-美食達", "category": "B2B", "buyerName": "美食達有限公司", "buyerUbn": "12345678", "buyerEmail": "ms@example.com" } ``` 修改(带 id,覆盖该条): ```json { "id": 101, "titleName": "公司-美食達", "category": "B2B", "buyerName": "美食達有限公司", "buyerUbn": "12345678", "buyerEmail": "ms@example.com" } ``` ### GET `/system/invoice/getinvoicexq?id=` 详情;越权(id 不属于当前 user)返回 `data: null`。 **响应** `AjaxResult` data:单条 `InfoInvoice`(字段同列表单行)。 ### GET `/system/invoice/deleinvoice?id=` 删除(硬删除);越权 / 不存在返回失败 `{code:500, msg:"抬头不存在或无权操作"}`。 --- ## 保存校验矩阵(service `validateInvoiceProfile`) | 场景 | category | buyerName | buyerUbn | buyerEmail | carrierType + carrierNum | |------|----------|-----------|----------|------------|--------------------------| | B2C / 手机条码(0) | B2C | ✅必填 | ❌空 | ⚪可选 | ✅必填(0 + 号以 `/` 开头) | | B2C / 自然人凭证(1) | B2C | ✅必填 | ❌空 | ⚪可选 | ✅必填(1 + `^[A-Z]{2}\d{14}$`) | | B2C / ezPay 会员(2) | B2C | ✅必填 | ❌空 | ✅必填 | ✅必填(2 + 号非空) | | B2B 公司 | B2B | ✅必填(公司名) | ✅必填(`^\d{8}$`) | ✅必填(邮箱格式) | ❌空 | 通用:`titleName` 非空;`category ∈ {B2C, B2B}`;`buyerEmail` 若填则校验邮箱格式。 ## 与 010 开票的对接(客户端驱动,不改后端开票) 客户端开票时:① `GET /system/invoice/getinvoice` 拉抬头;② 用户选一条 → 客户端按字段映射填入 `ApplyInvoiceDto`(category / buyerName / buyerUbn / buyerEmail / carrierType / carrierNum)→ ③ `POST /system/userOrder/applyInvoice`(010,不改)。抬头仅作输入快捷方式,010 仍做最终校验兜底。