api.md 6.4 KB

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<InfoInvoice>前端按每行的 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自然人凭证/2ezPay会员 ✅必填 ❌不传
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 / 手机条码载具:

{ "titleName": "个人-手机条码", "category": "B2C", "buyerName": "王小明", "carrierType": "0", "carrierNum": "/ABC1234" }

B2C / ezPay 会员载具(须带邮箱):

{ "titleName": "个人-会员载具", "category": "B2C", "buyerName": "王小明", "carrierType": "2", "carrierNum": "C1597485444", "buyerEmail": "xm@example.com" }

B2B 公司:

{ "titleName": "公司-美食達", "category": "B2B", "buyerName": "美食達有限公司", "buyerUbn": "12345678", "buyerEmail": "ms@example.com" }

修改(带 id,覆盖该条):

{ "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 仍做最终校验兜底。