spec.md 6.0 KB

Feature Specification: 用户发票抬头管理(发票信息管理)

Feature Branch: 不新建分支(当前 test 分支开发)

Created: 2026-07-24

Status: Draft

Input: User description: "开发票时用户每次都要手填手机码(载具)等信息,做一个像收货地址管理一样的功能,让用户保存常用发票抬头、开票时直接选用、不必重填。"

背景与定位

010(订单 ezPay 发票开立)已实现订单级即时开票,客户每次开票需在 ApplyInvoiceDto 手填买方名称 / 统编 / 邮箱 / 载具等。本期新增用户级发票抬头 CRUD(镜像 InfoAddress 收货地址管理模式),客户保存常用 B2C / B2B 抬头,开票时由客户端选用并预填,后端开票链路(010)完全不改

范围 = 后端 App 端 CRUD 接口;客户端 App 的「我的抬头」管理页 + 订单开票时的「选用抬头」入口由客户端团队后续对接(沿用 010 分工)。本期无任何前端页面,故无 i18n 改动。

User Scenarios & Testing (mandatory)

User Story 1 - 客户管理个人(B2C)发票抬头 (Priority: P1)

客户在 App 新增 / 查看 / 编辑 / 删除个人发票抬头,含姓名 + 载具(手机条码 / 自然人凭证 / ezPay 会员,必填)+ 邮箱(会员载具时必填)。

Why this priority: 个人发票是外卖/餐饮场景最高频的开票类型,与 B2B 同为核心 CRUD 路径。

Independent Test: 调 POST /system/invoice/invoice(B2C + 手机条码载具)→ 成功 → GET /getinvoice 列出该条 → GET /deleinvoice?id= 删除成功。

Acceptance Scenarios:

  1. Given 客户带 token,When 提交 B2C 抬头(姓名 + 手机条码载具),Then 保存成功,列表/详情可见。
  2. Given 客户提交 B2C 抬头但未带载具,When 提交,Then 校验拦截、不入库。
  3. Given 客户带他人抬头 id,When 调详情/改/删,Then 因 userId 不匹配被拒绝。

User Story 2 - 客户管理公司(B2B)发票抬头 (Priority: P1)

客户保存公司抬头,含公司名 + 统编(8 位)+ 邮箱。

Why this priority: B2B 报账是刚需(统编标配),与个人开票同为核心路径。

Independent Test: POST /invoice(B2B + 公司名 + 合法统编 + 邮箱)→ 成功;非法统编 → 拦截。

Acceptance Scenarios:

  1. Given 客户提交 B2B 抬头(公司名 + 合法统编 + 邮箱),When 提交,Then 保存成功。
  2. Given 客户填了非 8 位数字的统编,When 提交,Then 校验拦截、不入库。

Edge Cases

  • 越权:用户 A 带用户 B 抬头 id 调详情/改/删 → 拒绝(userId 不匹配,返回空或失败)。
  • 载具号格式非法(手机条码不以 / 开头、自然人凭证非 2 字母 + 14 数字)→ 保存时拦截。
  • 类型互斥:B2B 带载具字段、或 B2C 带统编 → 校验拒绝(载具仅 B2C、统编仅 B2B)。
  • token 伪造 userId:写库前以 JWT 解析的 userId 强制覆盖,前端传的 userId 无效。
  • 删除/改不存在的 id:返回未命中,不抛异常。
  • ezPay 会员载具(2) 未带邮箱:B2C 选会员载具时邮箱必填,未带则拦截(对齐 010 规则)。

Requirements (mandatory)

Functional Requirements

  • FR-001: 提供客户 App 端接口(JWT 鉴权、userId 隔离)对发票抬头做新增 / 查列表 / 查详情 / 改 / 删。
  • FR-002: 抬头按 categoryB2C / B2B)区分;字段随类型(见 data-model.md、contracts/api.md 校验矩阵)。
  • FR-003: 保存时中校验——必填非空 + B2B 统编 ^\d{8}$ + 载具号码按类型正则(手机条码以 / 开头、自然人凭证 ^[A-Z]{2}\d{14}$、ezPay 会员非空)+ 邮箱格式。
  • FR-004: 用户隔离靠 JWT:JwtUtil.getusid(token) 取 userId,写库前 setUserId 强制覆盖、查/改/删按 user_id 过滤;越权操作拒绝。
  • FR-005: 不改 010 的 applyInvoice / getInvoice;抬头仅为客户端开票时的输入快捷方式。
  • FR-006: 无默认抬头、无数量上限、硬删除(与 InfoAddress 一致)。
  • FR-007: 所有 SQL 变更写入 updatesql/sql.md,不直接执行(项目规范)。

Key Entities

  • 发票抬头(新增,用户级,info_invoice:titleName / category / buyerName / buyerUbn / buyerEmail / carrierType / carrierNum + userId + 审计列。
  • InfoUser(已有):userId 来源(JWT claim id)。
  • 010 ApplyInvoiceDto(不改):开票时客户端从选中抬头取字段填入。

Success Criteria (mandatory)

  • SC-001: 客户能通过 App 端接口完整增删改查自己的 B2C / B2B 抬头。
  • SC-002: 保存时格式非法(统编非 8 位、载具号不合规、必填空、类型互斥)100% 被拦截、不入库。
  • SC-003: 用户 A 100% 无法查 / 改 / 删用户 B 的抬头(userId 隔离)。
  • SC-004: 客户端能拉抬头列表(接口就绪),开票时选用预填现有 ApplyInvoiceDto(UI 由客户端团队对接)。

Assumptions

  • 镜像 InfoAddress(收货地址)用户级 CRUD 模式:实体即 DTO/VO、JWT 隔离、无默认、无上限、硬删除、App 端接口风格(@Anonymous @Auth + @RequestHeader token)。
  • 客户端 App「我的抬头」管理页 + 订单开票时的「选用抬头」入口由客户端团队后续对接,不在本期;本期无任何前端页面,故无 i18n 改动。
  • B2C 抬头必须有载具(对齐 010 当前 applyInvoice 规则:B2C 载具必填,0/1/2 三选一;ezPay 会员载具(2) 还须带邮箱)。
  • 捐赠(DONATION)不作为可存抬头类型(按需求决策);捐赠仍走开票时现选。
  • 抬头是纯输入辅助:开票仍由 010 applyInvoice 接收显式字段并做最终校验兜底,即使抬头存了也会在开票时再校验一次。
  • 仅交付后端 App 端 CRUD;不建后台端管理接口、不建 sys_menu、不建任何 Vue 页面(按需求决策,YAGNI)。