--- schema: "hl-changelog/v2" ticket: "8388" title: "回执接口守卫:上传加认领人校验(808110/808186,超管不豁免)、列表加房务读门(808090)、缺分片改 808184" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "房务回执上传新增认领人校验(超管不豁免)与可选分片处理;列表新增房务读权限门(不受灰度开关控制,常开);错误码 808184 消息补五种原因的具体文案。测试环境网关已实测各分支,含缺分片 808184 新文案。前端实证(mmg 2026-09-27):uploadReceipt 走 http.upload 同一拦截器统一弹 message,808184 原因入 message 无需按 code 分支;回执入口仅房务角色页,零改动 not_required。" updated_at: "2026-09-26" base: "dev-v3" --- # order-v3:房务最终确认回执接口守卫加固(管理后台) **服务**: hl-order-service-v3 **PR**: #8394(原始改动) + #8399(808184 补 {0} 占位追加) **Issue**: #8388 --- ## ⚠️ 关键变化 1. **上传回执** `POST /admin/house/assignments/requirements/{requirementId}/receipts`: - 新增认领人校验:需求必须被当前操作人认领,否则返回 **808110**(非本人);**SUPER_ADMIN 在本端点不豁免**,与 #8386 驳回需求口径相反。 - 需求处于 `PENDING_CLAIM`(未认领)时先被状态闸口拦下,返回 **808186**;错误码 808116 仅作理论防御(正常调用路径不可达)。 - 缺 file 分片 / 0 字节 file 分片改返 **808184**「回执上传失败:上传文件为空或缺少 file 分片」(改前返 HTTP 500),共 5 种原因文案。 - 顺序:角色门(808090/808091)→ 需求存在(808100)→ 状态闸口(808186)→ 认领人校验(808110)→ 文件校验(808184)。 2. **回执列表** `GET /admin/house/assignments/requirements/{requirementId}/receipts`: - 新增房务读权限门:非房务角色(定制师、车控等)返回 **808090**。房务全角色(含组长)可见;超管可见。 - 无认领限制(组长可看全部、他人接手前查历史回执)。 --- ## 一、背景 回执两个端点(上传、列表)无任何权限校验,导致: 1. 非认领人房务可为他人上传回执,破坏追责链。 2. 定制师、车控等非房务角色可读回执列表含 OSS 直链与酒店联系信息。 同一配房模块的 10 个写操作都带认领人校验;回执缺失属遗漏。缺 file 分片时返回 500 属参数处理缺陷(hl-common-log 共性坑,本单仅止血)。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 上传最终确认回执 | POST | `/admin/house/assignments/requirements/{requirementId}/receipts` | 修改接口 | 加认领人校验、缺分片改 808184 | | 2 | 回执列表 | GET | `/admin/house/assignments/requirements/{requirementId}/receipts` | 修改接口 | 加房务读权限门(808090) | --- ## 三、接口详情 ### 1. 上传最终确认回执 `POST /admin/house/assignments/requirements/{requirementId}/receipts` **VO**: `multipart/form-data: file → Result` #### 使用场景 房务在配房面板「最终确认」区上传回执照片/PDF,证明已与酒店确认房间。仅**该需求的认领人本人**可上传;非房务角色、房务组长、非认领人(含 SUPER_ADMIN)均返回权限错误——超管在本端点**不豁免**认领归属校验,这一点与 #8386(驳回需求超管豁免)口径不同。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | requirementId | Path | Long | 是 | — | 酒店需求 ID | | file | Form | MultipartFile | 是(业务校验) | ≤50MB,类型自动识别 | 回执文件;缺失返 808184 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | receiptId | String | 回执 ID(雪花 ID,序列化为字符串) | | requirementId | String | 需求 ID(雪花 ID,序列化为字符串) | | orderId | String | 订单 ID(雪花 ID,序列化为字符串) | | fileName | String | 原始文件名 | | ossUrl | String | OSS 公网 URL,`domain + "/" + ossKey` 拼接,不签名 | | ossKey | String | OSS Object Key | | fileSize | Long | 文件大小(字节) | | fileType | String | 附件类型:`PDF` / `IMAGE` / `OTHER`(按扩展名识别) | | createTime | LocalDateTime | 上传时间,序列化格式 `yyyy-MM-dd HH:mm:ss` | #### 请求示例 ```http POST /admin/house/assignments/requirements/2103126783048245250/receipts HTTP/1.1 Host: api.test.1814.love:9443 Authorization: Bearer Content-Type: multipart/form-data; boundary=----Boundary ------Boundary Content-Disposition: form-data; name="file"; filename="receipt.jpg" Content-Type: image/jpeg [binary jpeg data] ------Boundary-- ``` #### 响应示例 **成功**: ```json { "code": 200, "message": "成功", "success": true, "data": { "receiptId": "2103801203844689921", "requirementId": "2103126783048245250", "orderId": "2100542287908892650", "fileName": "receipt.jpg", "ossUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/house/finalize-receipt/...", "ossKey": "house/finalize-receipt/2026-09/2103126783048245250_receipt.jpg", "fileSize": 512000, "fileType": "IMAGE", "createTime": "2026-09-26 14:30:00" } } ``` **失败(非认领人)**: ```json { "code": 808110, "message": "需求不属于当前用户", "success": false, "data": null } ``` **失败(缺分片,实测)**: ```json { "code": 808184, "message": "回执上传失败:上传文件为空或缺少 file 分片", "success": false, "data": null } ``` #### 空数据 / 降级响应 - 文件为空或缺 `file` 分片:`code=808184`,message「回执上传失败:上传文件为空或缺少 file 分片」(改前返 HTTP 500)。 #### 错误响应 ```json { "code": 808110, "message": "需求不属于当前用户", "success": false, "data": null } ``` **其他错误码**: - **808090**:未登录或非房务角色,无权操作。 - **808091**:房务组长为只读监督角色,无权执行该操作。 - **808100**:需求不存在。 - **808186**:当前状态不允许上传回执(仅 CLAIMING/PENDING_FINALIZE 可传;PENDING_CLAIM 等其他状态走这个码,不是 808116)。 - **808116**:订单未抢单, 请先抢单再配房(状态闸口之后理论不可达,作纵深防护保留)。 - **808184**:回执上传失败:{0},`{0}` 按触发原因取以下固定文案之一——「上传文件为空或缺少 file 分片」/「文件大小超过 50MB」/「OSS 未配置」/「读取上传文件失败」/「文件存储服务暂不可用,请稍后重试」;异常原文只进服务端日志,不回显给客户端。 #### 业务边界 - **认领校验**:需求 `claimer_id` 必须等于当前操作人;**超管不豁免**,对他人认领的需求上传同样返回 808110。与 #8386(驳回需求,超管豁免)口径不同,前端不要套同一套按钮逻辑。 - **校验顺序**:写门(808090/808091)→ 需求存在(808100)→ 状态闸口(808186)→ 认领归属(808116/808110)→ 文件校验(808184)。 - **文件分片**:`file` 分片改为可选(`required=false`),缺失走 Service 内已有的空文件分支报业务码 808184(改前是 HTML form 漏填触发 `MissingServletRequestPartException`,落到兜底 `Exception` 处理器返 HTTP 200+code 500「系统繁忙」并打 ERROR 日志)。 - **状态限制**:仅 CLAIMING(配房中)与 PENDING_FINALIZE(待确认)两个阶段可上传;PENDING_CLAIM(未抢单)等其他状态返 808186,不是 808116。 - **无事务**:`upload()` 不加 `@Transactional`;OSS `putObject` 成功后才 `insert`,`insert` 失败可能残留孤儿 OSS 对象(由清理 job 兜底);OSS 失败(`putObject` 抛异常)返 808184 且不落库。 --- ### 2. 回执列表 `GET /admin/house/assignments/requirements/{requirementId}/receipts` **VO**: `→ Result>` #### 使用场景 房务或超管查看某需求的全部回执列表(谁上传的、何时、文件链接)。非房务角色返回 808090;房务全角色可见无限制。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | requirementId | Path | Long | 是 | — | 酒店需求 ID | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | receiptId | String | 回执 ID(雪花 ID,序列化为字符串) | | requirementId | String | 需求 ID(雪花 ID,序列化为字符串) | | orderId | String | 订单 ID(雪花 ID,序列化为字符串) | | fileName | String | 原始文件名 | | ossUrl | String | OSS 公网 URL,不签名 | | ossKey | String | OSS Object Key | | fileSize | Long | 文件大小(字节) | | fileType | String | 附件类型:`PDF` / `IMAGE` / `OTHER` | | createTime | LocalDateTime | 上传时间,序列化格式 `yyyy-MM-dd HH:mm:ss` | #### 请求示例 ```http GET /admin/house/assignments/requirements/2103126783048245250/receipts HTTP/1.1 Host: api.test.1814.love:9443 Authorization: Bearer ``` #### 响应示例 **成功(房务)**: ```json { "code": 200, "message": "成功", "success": true, "data": [ { "receiptId": "2103801203844689921", "requirementId": "2103126783048245250", "orderId": "2100542287908892650", "fileName": "receipt_1.jpg", "ossUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/house/finalize-receipt/...", "ossKey": "house/finalize-receipt/2026-09/2103126783048245250_receipt_1.jpg", "fileSize": 512000, "fileType": "IMAGE", "createTime": "2026-09-26 14:30:00" } ] } ``` **失败(定制师)**: ```json { "code": 808090, "message": "未登录或非房务角色,无权操作", "success": false, "data": null } ``` #### 空数据 / 降级响应 - 无回执:`code=200`,`data=[]`。 #### 错误响应 ```json { "code": 808090, "message": "未登录或非房务角色,无权操作", "success": false, "data": null } ``` **说明**: - **808090**(新增):非房务角色返回;该读门调 `HouseReadGuard.assertHouseReadPermission()`,**不受** #8390 引入的灰度开关 `group-batch.acl.enforce.house-read-role` 控制,是常开的角色门。 #### 业务边界 - **角色放行**:ROOM_MANAGER / house_keeper_lead / SUPER_ADMIN 均可读;零角色(#7609 G-2 保留项)亦放行。 - **无认领限制**:组长可看全部需求回执(监督权),他人接手后也可查历史。 - **直链有效期**:测试桶直链公共可读(无签名);生产桶读策略未定,前端不得缓存直链。 --- ## 四、契约约束与正确调用方式 | 场景 | 调用方法 | 说明 | |------|---------|------| | ✅ 认领人上传 | `POST /admin/house/assignments/requirements/{id}/receipts`,`multipart: file=@...` | 该需求的当前认领人上传,返回 200 | | ✅ 房务查列表 | `GET /admin/house/assignments/requirements/{id}/receipts` | 返回 200 + 全部回执 | | ✅ 组长查列表 | 同上(house_keeper_lead 身份)| 无限制,200 返回 | | ❌ 非认领人上传(含 SUPER_ADMIN) | — | 返回 808110,不落库;超管在本端点**不豁免**认领归属校验 | | ❌ 未认领需求(PENDING_CLAIM)上传 | — | 返回 808186(先过状态闸口) | | ❌ 定制师查列表 | — | 返回 808090 | | ❌ 缺 file 分片 | 空 multipart 或无 file 字段 | 返回 808184 | --- ## 五、数据库行为 无变更、无 Flyway。`house_finalize_receipt` 表与逻辑保留;认领人校验在内存判断,无新增存储。 --- ## 六、边界行为 | 场景 | 行为 | |---|---| | 认领人上传 | 返回 200,回执入库入 OSS | | 非认领人上传(含 SUPER_ADMIN) | 返回 808110,无新行 | | 转单后原人上传 | 返回 808110(新人成认领人,原人失去权限) | | PENDING_CLAIM 需求上传 | 返回 808186(状态闸口先于认领校验) | | 组长列表查询 | 返回 200,全列表 | | 定制师列表查询 | 返回 808090,无数据 | | 缺 file 分片 / 0 字节 file 分片 | 返回 808184(HTTP 200、code 业务码,5 种原因文案) | ## 六.6、修改前后对比 | 维度 | 改前 | 改后 | |------|------|------| | 非认领人上传(含超管) | 返回 200,入库 | 返回 808110 | | 缺 file 分片 | HTTP 500、ERROR 日志 | HTTP 200、code 808184,仅 WARN 日志 | | 定制师查列表 | 返回 200 + 列表 | 返回 808090 | ## 六.7、影响评估 - **兼容性**:hl-ui 回执上传与列表操作收新错误码(808090/808091/808110/808186/808184);需处理新文案提示。 - **前端要动的**: - 上传失败按错误码区分:「权限不足」(808090/808091)、「非本人(含超管)」(808110)、「需求未认领」(808186)、「文件」(808184,5 种原因文案,需在 `message` 里判断具体原因而非只判 code)。 - 定制师等非房务角色不再能查回执列表,改改导航或隐藏入口。 - **数据影响**:无;仅加校验,不改历史回执。 - **其它服务**:hl-mp-service、fleet 无直接调用。 --- ## 七、不影响范围 - 房务订单详情页回执展示逻辑(虽然定制师不再能查全列表,已登录进详情页的现有回执链接仍有效)。 - 回执删除接口(无,但实体带 `@TableLogic` 支持软删)。 - 其他配房写操作(转单、释放、分房等),无改动。 --- ## 八、测试环境已验证 部署:测试环境 order-v3 `1f65d7894`(含 e2313790b)。网关 `api.test.1814.love:9443`。 | # | 场景 | 实测结果 | |---|---|---| | 1 | 认领人上传 | code 200,新行 uploader_id = 认领人;已软删清理 | | 2 | 非认领人上传 | code 808110,无新行 | | 3 | PENDING_CLAIM 需求上传 | code 808186 | | 4 | SUPER_ADMIN 对他人认领的需求上传 | code 808110(不豁免) | | 5 | 缺 file 分片(multipart 只带无关字段)×2 | HTTP 200 + `{"code":808184,"message":"回执上传失败:上传文件为空或缺少 file 分片"}` | | 6 | 0 字节 file 分片 | 同 #5,同码同文案 | | 7 | 房务(认领人本人)查列表 | code 200,全部回执 | | 8 | 定制师 / VEHICLE_MANAGER 查列表 | code 808090,无数据 | | 9 | house_keeper_lead / 非认领 ROOM_MANAGER / SUPER_ADMIN 查列表 | code 200,全部回执 | #5、#6 场景 order-v3 日志对应 3 条 WARN `Business error [order.808184]`,无 `Unexpected error` ERROR 行。 --- ## 十、相关文档 - Issue:https://git.1814.love/wx/HL/issues/8388 - PR:https://git.1814.love/wx/HL/pulls/8394(原始改动)、https://git.1814.love/wx/HL/pulls/8399(808184 补 `{0}` 占位追加) - 相关单号:#8386(认领人校验,超管在该单**有**豁免,口径与本单相反)、#8390(读权限门同配方) --- ## 关联 / 联系人 ### 联系人 - **后端负责人**: @wx