From 58a9fc721134ba7a6182ab2476228b29efe36dae Mon Sep 17 00:00:00 2001 From: jw Date: Sun, 4 Oct 2026 15:58:24 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#8755=20=E5=8F=91=E7=A5=A8?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E6=8E=A5=E7=9F=AD=E4=BF=A1=EF=BC=88=E6=8C=89?= =?UTF-8?q?=E4=B8=8B=E5=8D=95=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=9B=B4=E5=8F=91?= =?UTF-8?q?=EF=BC=89=E6=8E=A8=E9=80=81=E6=97=A5=E5=BF=97=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20SKIP=20+=20=E6=96=B0=E5=A2=9E=E5=8F=91=E7=A5=A8=E7=9F=AD?= =?UTF-8?q?=E7=A0=81=E5=85=8D=E7=99=BB=E5=BD=95=E4=B8=8B=E8=BD=BD=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=EF=BC=88=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3=20/=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3=C2=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ...送接短信推送日志新增SKIP-修改接口-管理后台.md | 449 ++++++++++++++++++ ...�‘票短信短码免登录下载-新增接口-管理后台.md | 259 ++++++++++ 2 files changed, 708 insertions(+) create mode 100644 changelogs-v2/2026-10/04_8755_发票推送接短信推送日志新增SKIP-修改接口-管理后台.md create mode 100644 changelogs-v2/2026-10/04_8755_发票短信短码免登录下载-新增接口-管理后台.md diff --git a/changelogs-v2/2026-10/04_8755_发票推送接短信推送日志新增SKIP-修改接口-管理后台.md b/changelogs-v2/2026-10/04_8755_发票推送接短信推送日志新增SKIP-修改接口-管理后台.md new file mode 100644 index 00000000..fddef4a9 --- /dev/null +++ b/changelogs-v2/2026-10/04_8755_发票推送接短信推送日志新增SKIP-修改接口-管理后台.md @@ -0,0 +1,449 @@ +--- +schema: "hl-changelog/v2" +ticket: "8755" +title: "发票推送接短信(按下单手机号直发):推送日志按真实结果落库,pushStatus 新增 SKIP「未发送」,邮件 / 微信不再假写成功" +consumer: "admin" +author: "jw(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PUT /v3/admin/order/invoice/{id}/push 入参 / 出参 / 错误码不变,行为变:选 sms 时经通知中心按订单下单手机号直发「发票已开具」短信(带 8 位下载短码),推送日志按通知中心真实结果写 SUCCESS / FAILED / SKIP / PENDING;email / wechat 未接入,一律写 SKIP。GET /{id}/push-logs 与 GET /{id} 的 pushLogs[].pushStatus 新增取值 SKIP(statusText「未发送」),failReason 如实写原因。短信模板尚在申请(#8790,wx),模板到位前短信渠道写 SKIP「短信模板未配置,未发送」。已合并 dev-v3(9cd23903a)并部署 TEST,经网关实测,工单 #8755 已关。前端待做两处:推送弹窗 PushModal.vue:15 文案「本期仅记录推送状态,客户暂不会实际收到通知」改为如实说明(短信按下单手机号发送、邮件 / 微信暂不发送);推送日志抽屉 SKIP 行的原因前缀「失败原因:」改为「原因:」,可选给 SKIP 配一个标签色。statusText 已能自动显示「未发送」。" +updated_at: "2026-10-04" +base: "dev-v3" +--- + +# 发票推送接短信:推送日志按真实结果落库、新增 SKIP(管理后台) + +> **服务**: hl-order-service-v3(端口 8086/8186)+ hl-user-service(通知事件配置迁移) +> **PR**: #8776 +> **Issue**: #8755 +> **日期**: 2026-10-03 +> **影响范围**: 管理后台财务「发票管理」页的推送弹窗、推送记录抽屉、发票详情抽屉;接口签名零变化 + +--- + +## ⚠️ 关键变化 + +1. **推送不再是假推送**:选「短信」时经通知中心按**订单下单手机号**直发(不依赖小程序 userId,团期子订单也能收到),短信里带 8 位下载短码,客户免登录下载发票(配套新增接口见同单另一份 changelog)。 +2. **推送日志如实落库**:`pushStatus` 新增 **`SKIP`**(`statusText`「未发送」),`failReason` 写清原因;原先三个渠道一律写 `SUCCESS` 的口径作废。 +3. **邮件 / 微信未接入**:选了照样能推(状态机不变),但日志写 `SKIP`「邮件渠道未接入,未发送」/「微信渠道未接入,未发送」。 +4. **短信模板未到位**(#8790):模板过审填入前,短信渠道日志写 `SKIP`「短信模板未配置,未发送」,客户收不到短信。 +5. **短信结果是异步回写的**:推送接口返回时短信那行先是 `PENDING`,通常 1 秒内回写为终态;推送记录抽屉打开时再拉一次即可。 + +--- + +## 一、背景 + +#4230 曾定「本期不真发」:三个推送渠道监听器只登记一条 `SUCCESS`,前端弹窗也提示「客户暂不会实际收到通知」。jw 2026-10-03 定客户触达一律短信、按下单手机号直发、模板后补,于是本单接通短信并把推送日志改为如实落库。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 推送发票给客户 | PUT | `/v3/admin/order/invoice/{id}/push` | 修改接口 | 入参出参不变;sms 真发短信,各渠道日志按真实结果落库 | +| 2 | 发票推送明细日志 | GET | `/v3/admin/order/invoice/{id}/push-logs` | 修改接口 | `pushStatus` 新增 `SKIP`,`failReason` 如实写原因 | +| 3 | 发票详情 | GET | `/v3/admin/order/invoice/{id}` | 修改接口 | 仅 `pushLogs[]` 同上变化,其余字段不变 | + +网关:本组接口无改动(在既有 `/v3/admin/**` 通配下)。 + +--- + +## 三、接口详情 + +### 1. 推送发票给客户 `PUT /v3/admin/order/invoice/{id}/push` + +**VO**: `InvoicePushReqVO`(入参)/ `Result`(出参) + +#### 使用场景 + +财务在「发票管理 · 待推送 / 已推送」点「推送」,勾选渠道后提交。发票状态 ISSUED → PUSHED(PUSHED 可再次推送);事务提交后各渠道异步发送并写推送日志。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| id | path | string(Long) | 是 | 发票 ID | 发票主键,字符串透传 | +| channels | body | string[] | 是 | 非空;元素 ∈ 字典 `invoice_push_channel`(email / wechat / sms) | 推送渠道,可多选 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| code | int | 200 成功 | +| message | string | 「成功」 | +| data | null | 无数据;各渠道发送结果看推送日志 | + +#### 请求示例 + +```json +{ + "channels": ["sms", "email"] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": null, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +接口本身无空数据形态。通知中心不可用、短信发送失败都**不影响推送接口返回成功**(发送在事务提交后异步进行),失败结果体现在推送日志 `FAILED` / `PENDING`: + +```json +{ + "channel": "sms", + "pushStatus": "FAILED", + "statusText": "失败", + "pushedBy": "王会计", + "pushedAt": "2026-10-03 21:05:45", + "failReason": "通知中心调用失败" +} +``` + +#### 错误响应 + +错误码与改前完全一致: + +| 业务码 | 场景 | +|---|---| +| 581500 | 发票不存在 | +| 581520 | 发票当前状态不允许推送(须 ISSUED / PUSHED) | +| 581521 | 推送渠道为空 | +| 581522 | 推送渠道非法 | + +```json +{ + "code": 581520, + "message": "发票当前状态不允许推送(须已开票)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 状态机不变:ISSUED / PUSHED → PUSHED,`pushedAt` / `pushedChannels` 每次推送覆盖。 +- 每个勾选渠道每次推送写一条日志;sms 先写 `PENDING`,发完回写终态。 +- 短信收件人 = 订单 `customer_phone`(下单手机号);为空时写 `SKIP`「下单手机号为空,未发送短信」,不生成下载码。 +- 短信只在 sms 渠道发;email / wechat 不发、写 `SKIP`。 +- 同一发票连推两次会发两条短信(每次推送各自生成下载码)。 + +### 2. 发票推送明细日志 `GET /v3/admin/order/invoice/{id}/push-logs` + +**VO**: `InvoicePushLogRespVO` + +#### 使用场景 + +推送记录抽屉按推送时间倒序展示每次、每个渠道的推送结果。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| id | path | string(Long) | 是 | 发票 ID | 发票主键 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| channel | string | email / wechat / sms | +| pushStatus | string | SUCCESS / FAILED / PENDING / **SKIP(新增)** | +| statusText | string | 成功 / 失败 / 待发 / **未发送(新增)** | +| pushedBy | string | 操作人真实姓名 | +| pushedAt | string | 推送时间 `yyyy-MM-dd HH:mm:ss` | +| failReason | string \| null | 原因:FAILED / SKIP / PENDING 时有值,SUCCESS 为 null(原注释「预留」,现已启用) | + +#### 请求示例 + +```http +GET /v3/admin/order/invoice/2106369928091283458/push-logs +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": [ + { + "channel": "email", + "pushStatus": "SKIP", + "statusText": "未发送", + "pushedBy": "admin", + "pushedAt": "2026-10-03 21:11:53", + "failReason": "邮件渠道未接入,未发送" + }, + { + "channel": "sms", + "pushStatus": "SKIP", + "statusText": "未发送", + "pushedBy": "admin", + "pushedAt": "2026-10-03 21:05:45", + "failReason": "短信模板未配置,未发送" + } + ], + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +从未推送过: + +```json +{ + "code": 200, + "message": "成功", + "data": [], + "traceId": null, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 581500, + "message": "发票不存在", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 存量日志不订正:本单上线前的历史行仍是 `SUCCESS`(当时并未真发)。 +- `pushStatus` 未知值时 `statusText` 回落为原始值(既有口径)。 +- 短信行在推送后约 1 秒内由 `PENDING` 回写为终态;结果不确定时保留 `PENDING` 并写「以通知中心发送日志为准」。 + +### 3. 发票详情 `GET /v3/admin/order/invoice/{id}` + +**VO**: `AdminInvoiceDetailRespVO` + +#### 使用场景 + +发票详情 / 上传抽屉。本单只影响其中的 `pushLogs[]`(与接口 2 同源同 VO),其余字段零变化。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| id | path | string(Long) | 是 | 发票 ID | 发票主键 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| pushLogs | InvoicePushLogRespVO[] | 字段与接口 2 完全一致:`pushStatus` 新增 SKIP、`statusText` 新增「未发送」、`failReason` 如实写原因 | +| 其余字段 | - | 不变 | + +#### 请求示例 + +```http +GET /v3/admin/order/invoice/2106369928091283458 +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "id": "2106369928091283458", + "orderNo": "HL20260930170915373", + "status": "PUSHED", + "statusText": "已推送", + "invoiceNo": "26152000000087551236", + "pushedChannels": "email", + "pushLogs": [ + { + "channel": "email", + "pushStatus": "SKIP", + "statusText": "未发送", + "pushedBy": "admin", + "pushedAt": "2026-10-03 21:11:53", + "failReason": "邮件渠道未接入,未发送" + }, + { + "channel": "sms", + "pushStatus": "SKIP", + "statusText": "未发送", + "pushedBy": "admin", + "pushedAt": "2026-10-03 21:05:45", + "failReason": "短信模板未配置,未发送" + } + ] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +未推送过的发票 `pushLogs` 为空数组: + +```json +{ + "pushLogs": [] +} +``` + +#### 错误响应 + +```json +{ + "code": 581500, + "message": "发票不存在", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权、其余字段、`status` / `statusText` 口径均不变。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误调用顺序 + +- ✅ 推送记录展示用 `statusText`;`failReason` 非空时都展示(SKIP、PENDING 也有原因),前缀建议用「原因:」而不是「失败原因:」。 +- ✅ 推送成功后若立刻打开推送记录,短信行可能还是 `PENDING`,可在抽屉打开时重新拉取。 +- ❌ 不要把 `SKIP` 渲染成失败(红色),它表示「没发」而不是「发失败」。 +- ❌ 不要继续展示「本期仅记录推送状态,客户暂不会实际收到通知」。 + +--- + +## 五、数据库行为 + +- **order-v3**:零 DDL。`invoice_push_log.push_status`(VARCHAR(16)、无 CHECK)新增写入值 `SKIP`;`fail_reason` 由「预留」改为如实写原因。短信推送每次写一条 `invoice_download_code`(建表见同单新增接口 changelog)。 +- **user-service**:Flyway `V20261003_8755__invoice_pushed_notification.sql` 往 `notification_event_config` 插(或按 `uk_event_code` 收敛)`INVOICE_PUSHED`:只开短信、模板哨兵 `TODO_PLACEHOLDER`、`sms_field_mapping` 为 orderNo / invoiceNo / amount / code;重放不覆盖运营已填的模板编码。通知中心每次发送写一行 `notification_send_log`(`biz_type=INVOICE_PUSH`、`biz_id`=推送日志 ID,收件人脱敏)。 + +--- + +## 六、边界行为 + +| 场景 | 推送日志 | +|---|---| +| 选 sms,模板未配置(当前) | SKIP「短信模板未配置,未发送」 | +| 选 sms,下单手机号为空 | SKIP「下单手机号为空,未发送短信」 | +| 选 sms,阿里云受理(带回执号) | SUCCESS | +| 选 sms,短信通道为模拟档(无回执号) | SKIP「短信通道为模拟档(未配置短信网关),未真实发送」 | +| 选 sms,阿里云拒发 | FAILED「短信发送失败:<原因>」 | +| 选 sms,通知中心不可用 | FAILED「通知中心调用失败」 | +| 选 sms,投递结果不确定 / 查不到发送记录 | PENDING(写明以通知中心发送日志为准) | +| 选 email / wechat | SKIP「邮件 / 微信渠道未接入,未发送」 | +| 发票状态非 ISSUED / PUSHED | 581520,不写日志(同改前) | + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|----|------|------| +| 选 sms | 不发,日志写 SUCCESS | 经通知中心按下单手机号直发,日志按真实结果写 | +| 选 email / wechat | 不发,日志写 SUCCESS | 不发,日志写 SKIP + 原因 | +| `pushStatus` 取值 | SUCCESS(实际恒定)/ FAILED / PENDING(预留) | SUCCESS / FAILED / PENDING / **SKIP** | +| `statusText` 取值 | 成功 / 失败 / 待发 | 成功 / 失败 / 待发 / **未发送** | +| `failReason` | 恒 null | FAILED / SKIP / PENDING 时写原因 | +| 短信行写入时机 | 推送后异步一次写入 | 推送后异步先写 PENDING,发完回写终态 | +| 入参 / 出参 / 错误码 / 状态机 | — | 不变 | + +--- + +## 六.7、影响评估 + +- **前端(hl-ui v2.1 实查)**: + - `src/views/order/invoice/components/PushLogsDrawer.vue:30` 显示 `log.statusText || log.pushStatus`,「未发送」**自动生效**;`:27` 标签色 `STATUS_TYPE[s] || 'default'`,SKIP 落灰色,可选补一个色。 + - `PushLogsDrawer.vue:41`把 `failReason` 冠以「失败原因:」前缀展示——SKIP 行也会显示,**前缀建议改「原因:」**。 + - `src/views/order/invoice/components/PushModal.vue:15`「本期仅记录推送状态,客户暂不会实际收到通知。」**需改**为如实说明:短信按订单下单手机号发送(模板生效后),邮件、微信暂不发送。 + - `src/api/invoice.js:191` 注释中的取值说明可同步补 SKIP。 +- **历史数据**:上线前的推送日志仍是 SUCCESS(当时未真发),不订正。 +- **其他调用方**:无。小程序端、通知中心其他事件、团期 / 订单流程零影响。 + +--- + +## 七、不影响范围 + +- **仅影响**: 推送接口的发送行为与推送日志取值(3 个管理端接口的 `pushLogs` 相关字段)。 +- **零影响**: + - 发票申请、开票、重传、重开、列表、统计接口 + - 小程序发票查询 / 下载 / 重开 + - 通知中心其他事件 +- 零权限种子变更、零网关变更(本组接口)。 + +--- + +## 八、测试环境已验证 + +部署:hl-user-service、hl-order-service-v3、hl-gateway = dev-v3 @ 9cd23903a(2026-10-03 20:56 / 20:59 / 21:00);TEST `flyway_schema_history` user `20261003.8755` success=1,`INVOICE_PUSHED` 配置行读回只开短信、模板 `TODO_PLACEHOLDER`;构建身份探针 6/6 命中新代码。经网关 `https://api.test.1814.love` 真实鉴权实测(2026-10-03 21:05–21:11),工单 #8755 已验收关单。 + +| # | 场景 | 结果 | +|---|---|---| +| 1 | 团期子订单(user_id 为空)发票 ISSUED,推 `["sms"]` | 200;发票 → PUSHED;推送日志 SKIP「短信模板未配置,未发送」;通知中心发送日志一行 `INVOICE_PUSHED / SMS / status=3`,收件人 `138****2356`,`params_json` 无手机号;生成下载码 1 枚(7 天) | +| 2 | 另一张 ISSUED 发票推 `["email","wechat"]` | 200;发票 → PUSHED;两行 SKIP,原因分别为微信 / 邮件渠道未接入;不生成下载码 | +| 3 | 已 PUSHED 发票再推 `["email"]` | 200;仍 PUSHED,`pushedAt` / `pushedChannels` 已更新 | +| 4 | `GET /{id}/push-logs`、`GET /{id}` | `pushStatus=SKIP`、`statusText=未发送`、`failReason` 如实 | +| 5 | 日志脱敏 | TEST order-v3 日志 11 位手机号出现 0 次 | +| 6 | TEST 短信网关形态 | `aliyun.sms.access-key-id` 为真实 AK(非模拟档);哨兵期在模板检查处跳过,未触达阿里云、未发出短信 | + +--- + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|----|-------|------|------------| +| — | #4230 | 发票推送「本期不真发」 | ⚠️ 被本单取代 | +| — | #3713 | 通知中心 directPhone 收件通道 | ✅ | +| — | #8754 | 成团通知客户短信(同样按下单手机号直发) | ✅ | +| **本 PR #8776** | **#8755** | 发票推送接短信 + 凭短码免登录下载 | ✅ 最新 | + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#8755](https://git.1814.love/wx/HL/issues/8755) +- 关联 PR: [wx/HL#8776](https://git.1814.love/wx/HL/pulls/8776) +- 短信模板申请跟进: [wx/HL#8790](https://git.1814.love/wx/HL/issues/8790) +- 同单配套新增接口: `changelogs-v2/2026-10/04_8755_发票短信短码免登录下载-新增接口-管理后台.md` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#8755](https://git.1814.love/wx/HL/issues/8755) +- **PR**: [#8776](https://git.1814.love/wx/HL/pulls/8776) +- **Merge commit**: [9cd23903a](https://git.1814.love/wx/HL/commit/9cd23903abe8edc75e13ca2e7c4d5c65bd9e4453) + +### 联系人 + +- **后端负责人**: @jw +- **前端负责人**: @mmg +- **短信模板**: @wx(#8790) diff --git a/changelogs-v2/2026-10/04_8755_发票短信短码免登录下载-新增接口-管理后台.md b/changelogs-v2/2026-10/04_8755_发票短信短码免登录下载-新增接口-管理后台.md new file mode 100644 index 00000000..3e3f12e7 --- /dev/null +++ b/changelogs-v2/2026-10/04_8755_发票短信短码免登录下载-新增接口-管理后台.md @@ -0,0 +1,259 @@ +--- +schema: "hl-changelog/v2" +ticket: "8755" +title: "发票短信短码免登录下载:新增公开端点 GET /v3/open/invoice/{code},客户凭短信里的 8 位短码 302 到现签 1 小时的发票文件" +consumer: "multiple" +author: "jw(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +updated_at: "2026-10-04" +base: "dev-v3" +status_note: "新增公开端点 GET /v3/open/invoice/{code}(网关新路由 invoice-open-v3 + SKIP_URLS,无登录态,凭码即鉴权)。财务推送发票选短信时,order-v3 为本次推送生成 8 位 base62 短码(列 utf8mb4_bin 区分大小写、7 天有效),客户点短信链接经本端点 302 到现签 1 小时的 OSS 发票文件;按客户端 IP 30 次/分钟限流。端点只给短信链接用,不给管理后台 / 小程序前端调用,Swagger 不展示,前端零适配。已合并 dev-v3(9cd23903a),部署 TEST(gateway、order-v3、user-service),经网关匿名实测 302 + PDF 字节一致、篡改 / 过期 / 限流均按约定拒绝,工单 #8755 已验收关单。短信模板尚在申请(#8790,wx),模板到位前客户收不到短信,本端点可用但无人拿到码。" +--- + +# 发票短信短码:新增免登录下载端点(公开) + +> **服务**: hl-order-service-v3(端口 8086/8186)+ hl-gateway(新路由与免鉴权白名单) +> **PR**: #8776 +> **Issue**: #8755 +> **日期**: 2026-10-03 +> **影响范围**: 新增一个对外公开端点,只给发票推送短信里的链接用;管理后台、小程序前端零适配 + +--- + +## ⚠️ 关键变化 + +1. **新增公开端点** `GET /v3/open/invoice/{code}`:不带任何登录态,凭 8 位短码访问,成功 **302** 到现签 1 小时的发票文件(OSS 私有桶)。 +2. **网关新前缀** `/v3/open/invoice/**`:新路由 `invoice-open-v3` → order-service-v3,并加入 `JwtAuthFilter.SKIP_URLS`;客户端伪造的 `X-User-Id` / `X-Admin-Id` 会被网关剥掉。 +3. **短码来源**:财务在发票管理页推送并勾选「短信」时,每推一次生成一枚新码(`invoice_download_code`),7 天有效;码绑定发票,财务重传文件后旧码下载到的是新文件,发票作废后码失效。 +4. **短信模板未到位**:通知配置 `INVOICE_PUSHED` 仍是哨兵 `TODO_PLACEHOLDER`(申请见 #8790),客户暂时收不到短信;端点已上线可用。 + +--- + +## 一、背景 + +发票推送原先是假推送(#4230 定「本期不真发」)。#8755 把短信渠道接到通知中心、按订单下单手机号直发;团期子订单绝大多数没有 userId,小程序发票下载要求本人登录(`order.userId == 当前用户`),这些客户在小程序里拿不到发票,所以短信里放一个免登录的下载链接。阿里云短信「链接参数」变量最多 8 位,放不下签名令牌,于是落库存 8 位随机短码。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 发票短码下载 | GET | `/v3/open/invoice/{code}` | 新增接口 | 免登录;成功 302 到现签 1 小时的发票文件;按 IP 30 次/分钟限流 | + +网关:新增路由 `invoice-open-v3`(`Path=/v3/open/invoice/**` → `lb://hl-order-service-v3`),`/v3/open/invoice/**` 进 `SKIP_URLS`。 + +--- + +## 三、接口详情 + +### 1. 发票短码下载 `GET /v3/open/invoice/{code}` + +**VO**: `ResponseEntity`(成功为 302 无响应体,失败为通用 `Result`) + +#### 使用场景 + +客户收到「发票已开具」短信,点短信里的链接(模板正文写死对外域名 + 路径,变量只放 `code`),浏览器经本端点跳到发票 PDF 直接查看 / 下载。不给任何前端页面调用。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| code | path | string | 是 | 恰好 8 位字母数字 `^[0-9A-Za-z]{8}$`,**区分大小写** | 发票推送短信里的下载短码 | + +无请求头要求:不需要 `Authorization`,带了也不参与鉴权(网关对本前缀剥掉身份头)。 + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| HTTP 状态 | int | 成功固定 `302 Found` | +| Location | header string | 现签的 OSS 下载 URL,有效 1 小时(user-service `oss.signed-url-expire`),PDF 以 inline 方式打开 | +| Cache-Control | header string | 固定 `no-store`,签名 URL 不进任何缓存 | +| body | - | 成功无响应体 | + +#### 请求示例 + +```http +GET /v3/open/invoice/K7mQ2xRb HTTP/1.1 +Host: api.test.1814.love +``` + +#### 响应示例 + +成功(302,无响应体;以下为响应头的结构化描述): + +```json +{ + "httpStatus": 302, + "headers": { + "Location": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/invoice/2026/10/03/64674f6aa6086d1e42d7abcc2b1b50e0.pdf?Expires=1791036463&OSSAccessKeyId=***&Signature=***", + "Cache-Control": "no-store" + }, + "body": null +} +``` + +#### 空数据 / 降级响应 + +没有空数据形态。文件服务(user-service 签名接口)不可用时不跳转,返回业务码 581519: + +```json +{ + "code": 581519, + "message": "发票下载服务暂时不可用,请稍后重试", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 错误响应 + +失败一律 HTTP 200 + 业务码,不带任何发票 / 订单字段: + +| 业务码 | 场景 | +|---|---| +| 581527 | 码格式不符(非 8 位字母数字)、查无此码、大小写或任一字符被改动 | +| 581528 | 码已过期(默认生成后 7 天) | +| 581517 | 码对应的发票已不可下载(已作废 / 不存在 / 文件地址异常),文案「发票尚未开具,暂时无法下载」沿用既有码 | +| 581519 | 现签下载链接失败(文件服务不可用) | +| 100501 | 同一来源 60 秒内超过 30 次 | + +```json +{ + "code": 581527, + "message": "发票下载链接无效", + "data": null, + "traceId": null, + "success": false +} +``` + +```json +{ + "code": 581528, + "message": "发票下载链接已过期,请联系客服重新推送", + "data": null, + "traceId": null, + "success": false +} +``` + +```json +{ + "code": 100501, + "message": "访问过于频繁,请稍后再试", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 每次「推送 + 勾选短信」且下单手机号非空时才生成码;邮件 / 微信推送、手机号为空都不生成。 +- 同一张发票多次推送会有多枚码,各自 7 天有效、互不作废。 +- 码绑定发票而不是文件:财务「重新上传」后,旧码下载到的是新文件;发票作废(重开)后旧码返回 581517。 +- 302 的 Location 每次点击现签,有效 1 小时;客户隔天再点同一短信链接会拿到新的签名 URL(码 7 天内有效)。 +- 限流按客户端 IP(取 `X-Forwarded-For` 第一段),30 次 / 60 秒。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误调用顺序 + +- ✅ 只在短信模板正文里写死「对外域名 + `/v3/open/invoice/` + `${code}`」(或经 nginx 短路径转发到本路径,见 #8790),变量只放 8 位码。 +- ✅ 浏览器直接打开即可,跟随 302。 +- ❌ 不要在管理后台 / 小程序里拼这个链接给用户:后台有发票详情与下载能力,小程序有登录态下载接口。 +- ❌ 不要把 Location 里的签名 URL 存下来复用,它 1 小时过期。 +- ❌ 不要对码做大小写归一化,码区分大小写。 + +--- + +## 五、数据库行为 + +- **order-v3**:Flyway `V20261003_8755__create_invoice_download_code.sql` 新建表 `invoice_download_code`(`code_id` 雪花主键、`short_code VARCHAR(16) utf8mb4_bin` 唯一键、`invoice_id`、`order_id`、`push_log_id`、`expire_at` + BaseDO 五列)。码在发票短信推送时写入(每次推送一行),本端点**只读不写**。 +- 有效期配置项 `hl.order-v3.invoice.download-code-ttl-days`,缺省 7(未写入 nacos,取默认值)。 + +--- + +## 六、边界行为 + +| 场景 | 行为 | +|---|---| +| 正常码、发票 ISSUED / PUSHED | 302 到现签 1 小时的发票文件 | +| 翻转码里一个字母的大小写 | 581527(列 `utf8mb4_bin` + 服务内逐字节比对,双保险) | +| 改动任一字符 / 位数不对 | 581527 | +| 码过期 | 581528 | +| 发票已作废 | 581517 | +| 文件服务不可用 | 581519 | +| 同一 IP 60 秒内第 31 次起 | 100501 | +| 带伪造的 `X-User-Id` / `X-Admin-Id` | 网关剥掉,按匿名处理 | +| 访问 `/v3/open/` 下其他路径 | 网关仍按需登录拦截(白名单只放行发票前缀) | + +--- + +## 七、不影响范围 + +- **仅影响**: 新增一个公开端点、一张表、一条网关路由与白名单项。 +- **零影响**: + - 小程序发票下载 `GET /v3/internal/mp/order/invoice/{id}/download`(登录态 + 本人校验,行为不变;内部改为复用抽出的 ossKey 解析,逐字等价) + - 管理后台发票列表、详情、开票、重传接口 + - Swagger 文档(本端点方法级 hidden,不进任何分组) +- 零权限种子变更、零前端适配。 + +--- + +## 八、测试环境已验证 + +部署:hl-user-service、hl-order-service-v3、hl-gateway = dev-v3 @ 9cd23903a(2026-10-03 20:56 / 20:59 / 21:00);TEST `flyway_schema_history` order-v3 `20261003.8755` success=1,`invoice_download_code.short_code` 排序规则实测 `utf8mb4_bin`;构建身份探针:匿名请求不存在的码连打 6 次全部 581527。经网关 `https://api.test.1814.love` 匿名实测(2026-10-03 21:05–21:11),工单 #8755 已验收关单。 + +| # | 场景 | 结果 | +|---|---|---| +| 1 | 团期子订单(user_id 为空)发票推送短信后取码访问 | 302;`Cache-Control: no-store`;响应体 0 字节;Location 签名有效期 3600 秒 | +| 2 | 跟随 302 下载 | `%PDF-1.4`,SHA-256 与上传原件逐字节一致 | +| 3 | 翻转一个字母大小写 / 改最后一位 / 多一位 | 均 581527 | +| 4 | 把该码 `expire_at` 临时改到过去 | 581528;验后已还原,还原后恢复 302 | +| 5 | 同一来源 1.2 秒内连发 35 次 | 前 30 次 581527,第 31 次起 100501 | +| 6 | 日志脱敏 | TEST order-v3 日志里完整短码出现 0 次,只打前 2 位(如 `oK******`) | + +--- + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|----|-------|------|------------| +| — | #4230 | 发票推送「本期不真发」定案 | ⚠️ 被 #8755 取代(短信渠道已接通知中心) | +| — | #3780 | 车务 H5 录入链接短链化(短码进短信链接先例) | ✅ | +| **本 PR #8776** | **#8755** | 发票推送接短信 + 凭短码免登录下载 | ✅ 最新 | + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#8755](https://git.1814.love/wx/HL/issues/8755) +- 关联 PR: [wx/HL#8776](https://git.1814.love/wx/HL/pulls/8776) +- 短信模板申请跟进: [wx/HL#8790](https://git.1814.love/wx/HL/issues/8790) +- 同单配套变更(推送日志新增 SKIP): `changelogs-v2/2026-10/04_8755_发票推送接短信推送日志新增SKIP-修改接口-管理后台.md` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#8755](https://git.1814.love/wx/HL/issues/8755) +- **PR**: [#8776](https://git.1814.love/wx/HL/pulls/8776) +- **Merge commit**: [9cd23903a](https://git.1814.love/wx/HL/commit/9cd23903abe8edc75e13ca2e7c4d5c65bd9e4453) + +### 联系人 + +- **后端负责人**: @jw +- **短信模板**: @wx(#8790)