# 发票模块完整 API 文档(管理端 + 小程序端) > 发票功能:用户对已支付订单申请开票 → 管理员审核开票 → 用户下载电子发票。支持换开(最多3次) --- ## 一、状态流转 ``` 用户申请 → PENDING(待开票) ├─ 管理员开票完成 → ISSUED(已开票) └─ 管理员标记失败 → FAILED(开票失败)→ 管理员重新开票 → ISSUED ISSUED → 用户换开 → 原发票 VOIDED(已作废)+ 新发票 PENDING ISSUED → 管理员作废 → VOIDED ``` ## 二、关联字典 | 字典 dict_type | 说明 | 枚举值 | |---|---|---| | `invoice_status` | 发票状态 | `PENDING`=待开票, `ISSUED`=已开票, `FAILED`=开票失败, `VOIDED`=已作废 | | `invoice_title_type` | 抬头类型 | `PERSONAL`=个人, `COMPANY`=企业 | ## 三、通用响应结构 InvoiceVO 所有发票接口返回的数据结构(管理端直接返回 `InvoiceVO`,小程序端经 Feign 透传为 `Map`,字段相同): | 字段 | 类型 | 说明 | |---|---|---| | `id` | String | 发票ID | | `orderId` | String | 订单ID | | `orderNo` | String | 订单编号 | | `amount` | String | 发票金额(元) | | `titleType` | String | 抬头类型:`PERSONAL`=个人 / `COMPANY`=企业(字典 `invoice_title_type`) | | `invoiceTitle` | String | 发票抬头(个人姓名或企业名称) | | `taxNumber` | String | 纳税人识别号(企业发票有值) | | `email` | String | 接收邮箱 | | `invoiceType` | String | 发票类型,固定为"电子普通发票" | | `invoiceContent` | String | 发票内容,固定为"明细" | | `status` | String | 发票状态(字典 `invoice_status`) | | `fileUrl` | String | 发票文件URL(已开票后有值) | | `failReason` | String | 开票失败原因(失败时有值) | | `contactPhone` | String | 联系电话 | | `remark` | String | 备注信息 | | `bankName` | String | 开户行(企业发票) | | `bankAccount` | String | 银行账号(企业发票) | | `companyAddress` | String | 企业地址(企业发票) | | `companyPhone` | String | 企业电话(企业发票) | | `invoiceNo` | String | 发票号码(已开票后有值) | | `issuedAt` | String | 开票时间,格式 `yyyy-MM-dd HH:mm:ss` | | `issuedBy` | String | 开票操作人(管理员ID) | | `userId` | String | 申请用户ID | | `originalInvoiceId` | String | 原发票ID(换开时有值,指向被作废的原发票) | | `reissueCount` | Integer | 换开次数(0~3) | | `createdAt` | String | 申请时间 | | `updatedAt` | String | 更新时间 | --- ## 四、小程序端接口(C端用户) > 网关路径前缀:`/mp/invoice` > 需要用户登录(携带 Authorization: Bearer token) ### 4.1 申请开票 ``` POST /mp/invoice/apply ``` **使用场景**:用户在订单详情页点击「申请发票」,填写开票信息后提交 **请求参数** (JSON Body): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `orderId` | String | ✅ | 订单ID | | `invoiceTitle` | String | ✅ | 发票抬头(个人姓名/企业名称),最长100字 | | `titleType` | String | 否 | 抬头类型,默认 `PERSONAL`。可选:`PERSONAL`=个人, `COMPANY`=企业 | | `taxNumber` | String | 企业必填 | 纳税人识别号,最长30位。`titleType=COMPANY` 时必填 | | `email` | String | 否 | 接收邮箱(邮箱格式校验),最长100字 | | `contactPhone` | String | 否 | 联系电话,最长20位 | | `remark` | String | 否 | 备注信息,最长500字 | | `bankName` | String | 否 | 开户行(企业发票选填),最长100字 | | `bankAccount` | String | 否 | 银行账号(企业发票选填),最长30位 | | `companyAddress` | String | 否 | 企业地址(企业发票选填),最长200字 | | `companyPhone` | String | 否 | 企业电话(企业发票选填),最长20位 | **响应**:`Result`(见第三节) **校验规则**: - 订单必须属于当前用户 - 订单状态须为:已支付 / 已确认 / 待出发 / 旅行中 / 已完成 / 已付定金 - 同一订单不可重复申请(已有待开票或已开票的发票时拒绝) - 开票金额自动取订单实付金额(`paidAmount`),如无则取总价(`totalPrice`) - 发票类型固定「电子普通发票」,内容固定「明细」 **调用示例**: ```json // 个人发票 { "orderId": "1902000000000001", "invoiceTitle": "张三", "titleType": "PERSONAL", "email": "zhangsan@example.com", "contactPhone": "13800138000" } // 企业发票 { "orderId": "1902000000000001", "invoiceTitle": "呼伦贝尔XX旅行社有限公司", "titleType": "COMPANY", "taxNumber": "91150000MA00000X00", "email": "finance@company.com", "bankName": "中国银行海拉尔支行", "bankAccount": "1234567890123456789", "companyAddress": "呼伦贝尔市海拉尔区XX路XX号", "companyPhone": "0470-1234567" } ``` --- ### 4.2 发票详情(按发票ID) ``` GET /mp/invoice/{id} ``` **使用场景**:从发票列表点进详情页 **路径参数**: | 参数 | 类型 | 说明 | |---|---|---| | `id` | Long | 发票ID | **响应**:`Result` **校验规则**:只能查看自己的发票 --- ### 4.3 通过订单ID查询发票 ``` GET /mp/invoice/order/{orderId} ``` **使用场景**:订单详情页判断是否已开票。返回 `data=null` 表示未开票,可显示「申请发票」按钮 **路径参数**: | 参数 | 类型 | 说明 | |---|---|---| | `orderId` | Long | 订单ID | **响应**:`Result`(未开票时 `data` 为 `null`) --- ### 4.4 发票换开 ``` POST /mp/invoice/{invoiceId}/reissue ``` **使用场景**:已开票后发现抬头/税号填错,点击「换开发票」修改信息重新开具 **路径参数**: | 参数 | 类型 | 说明 | |---|---|---| | `invoiceId` | Long | 原发票ID | **请求参数** (JSON Body): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoiceTitle` | String | ✅ | 新发票抬头,2~100字 | | `titleType` | String | ✅ | 新抬头类型:`PERSONAL` / `COMPANY` | | `taxNumber` | String | 企业必填 | 新纳税人识别号 | | `email` | String | 否 | 新接收邮箱 | **响应**:`Result`(返回新创建的发票记录,原发票自动作废) **校验规则**: - 只能对状态为 `ISSUED`(已开票)的发票换开 - 换开次数上限 **3次**(reissueCount >= 3 时拒绝) - 企业抬头必须填税号 - 换开后:原发票状态变为 `VOIDED`,新发票状态为 `PENDING`,`originalInvoiceId` 指向原发票 **调用示例**: ```json { "invoiceTitle": "呼伦贝尔XX旅行社有限公司", "titleType": "COMPANY", "taxNumber": "91150000MA00000X00", "email": "finance@company.com" } ``` --- ## 五、管理端接口(后台管理员) > 网关路径前缀:`/admin/invoice` > 需要管理员登录(携带 Admin-Token) ### 5.1 上传发票文件 ``` POST /admin/invoice/upload ``` **使用场景**:管理员在开票前先上传发票PDF/图片文件 **请求参数** (form-data): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `file` | File | ✅ | 发票文件,支持 PDF / JPG / JPEG / PNG,最大 10MB | **响应**: ```json { "code": 200, "message": "success", "data": { "fileUrl": "https://oss.xxx.com/invoice/20260325/abc123.pdf" } } ``` --- ### 5.2 发票分页列表 ``` GET /admin/invoice/page ``` **使用场景**:发票管理列表页,支持筛选 **请求参数** (Query): | 参数 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `status` | String | 否 | - | 按状态筛选(字典 `invoice_status`):`PENDING` / `ISSUED` / `FAILED` / `VOIDED` | | `orderNo` | String | 否 | - | 按订单编号模糊搜索 | | `userId` | Long | 否 | - | 按用户ID筛选 | | `page` | int | 否 | 1 | 页码(最小1) | | `pageSize` | int | 否 | 20 | 每页条数(1~100) | **响应**: ```json { "code": 200, "message": "success", "data": { "records": [ InvoiceVO, ... ], "total": 100, "current": 1, "size": 20 } } ``` --- ### 5.3 发票详情 ``` GET /admin/invoice/{id} ``` **路径参数**:`id` — 发票ID **响应**:`Result` --- ### 5.4 开票完成 ``` PUT /admin/invoice/{id}/issue ``` **使用场景**:管理员线下开票完成后,上传发票文件并填写发票号码,标记开票完成 **路径参数**:`id` — 发票ID **请求参数** (JSON Body): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `fileUrl` | String | ✅ | 发票文件URL(通过 5.1 上传接口获取) | | `invoiceNo` | String | ✅ | 发票号码(12位数字) | **响应**:`Result` **校验规则**:只有 `PENDING`(待开票)或 `FAILED`(开票失败)状态的发票可操作 **调用示例**: ```json { "fileUrl": "https://oss.xxx.com/invoice/20260325/abc123.pdf", "invoiceNo": "012345678901" } ``` --- ### 5.5 开票失败 ``` PUT /admin/invoice/{id}/fail ``` **使用场景**:发票信息有误,管理员标记失败并填写原因,用户可修正后重新申请 **路径参数**:`id` — 发票ID **请求参数** (JSON Body): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `failReason` | String | ✅ | 失败原因 | **响应**:`Result` **校验规则**:只有 `PENDING`(待开票)状态的发票可操作 **调用示例**: ```json { "failReason": "发票抬头与纳税人识别号不匹配,请核实后重新申请" } ``` --- ### 5.6 作废发票 ``` PUT /admin/invoice/{id}/void ``` **使用场景**:管理员主动作废已开票的发票 **路径参数**:`id` — 发票ID **请求参数**:无 **响应**:`Result` **校验规则**:已作废的发票不可重复作废 --- ## 六、前端页面对接建议 ### 小程序端页面 **订单详情页**: - 调用 `GET /mp/invoice/order/{orderId}` 判断是否已开票 - `data=null` → 显示「申请发票」按钮(订单状态需满足条件) - `data.status=PENDING` → 显示「开票中」 - `data.status=ISSUED` → 显示「查看发票」+「换开发票」按钮 - `data.status=FAILED` → 显示失败原因 + 「重新申请」按钮 - `data.status=VOIDED` → 显示「已作废」 **申请开票页**: - 默认「个人」抬头,切换「企业」后展开额外字段(税号必填 + 开户行/银行账号/地址/电话选填) - 金额自动显示(不可编辑,由后端计算) **发票详情页**: - 显示全部字段信息 - `status=ISSUED` 时显示发票文件下载链接(`fileUrl`)和发票号码(`invoiceNo`) - `status=ISSUED` 且 `reissueCount < 3` 时显示「换开」按钮 ### 管理端页面 **发票列表页**: - 顶部筛选栏:状态下拉(全部/待开票/已开票/开票失败/已作废)+ 订单号搜索框 - 列表列:订单号、用户、金额、抬头、状态、申请时间、操作 - 操作按钮: - `PENDING` → 「开票」「标记失败」 - `FAILED` → 「重新开票」 - `ISSUED` → 「作废」 - `VOIDED` → 无操作 **开票操作**: 1. 先调 `POST /admin/invoice/upload` 上传发票文件 → 拿到 `fileUrl` 2. 再调 `PUT /admin/invoice/{id}/issue` 传入 `fileUrl` + `invoiceNo`