--- schema: "hl-changelog/v2" ticket: "8507" title: "往来期初 ledgerType 新增 SUPPLIER_RECV 供应商应收账套,支撑财务初始化「应收初始化」tab" consumer: "admin" author: "yst(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "implemented" frontend_owner: "mmg" frontend_ref: "cdfaf7932a9bf3008760a4b728f2d9fe0ffff421" target_release: "v2.1" verified_at: "2026-09-29" status_note: "POST /admin/finance/opening-balances 的 ledgerType 枚举新增 SUPPLIER_RECV(供应商应收:他欠我们的杂项应收,如押金退还/赔偿款/口车费),配套应收性质字典 fin_recv_nature(DEPOSIT_REFUND/COMPENSATION/CAR_FEE/OTHER)+ 错误码 598409/598410;同时收紧金额方向:SUPPLIER 只认 openingPayable>0,CUSTOMER/SUPPLIER_RECV 只认 openingReceivable>0。测试环境已部署并行为验证通过。前端需新增财务初始化「应收初始化」tab(7 字段表单,字段映射见本文档 §三/§四)。;前端 2026-09-29 已交付:fin-init 新增「供应商应收」tab(7 元素表单,应收性质走 recvNatures 字典不写死,isPrimary=1 默认选中,记账日期只读不传参),CUSTOMER 同步收紧为单应收框,STAFF 维持双向;fin-init spec 21 例全绿" updated_at: "2026-09-29" base: "dev-v3" --- # finance:往来期初新增供应商应收账套 SUPPLIER_RECV(应收初始化 tab 对接)(管理后台) **服务**: hl-order-service-v3(finance 模块,同进程) **PR**: [#8509](https://git.1814.love/wx/HL/pulls/8509)、[#8513](https://git.1814.love/wx/HL/pulls/8513) **Issue**: [#8507](https://git.1814.love/wx/HL/issues/8507) --- ## 一、接口背景 管理后台「财务初始化」要新增一个**「应收初始化」tab**:记录**供应商欠我们**的杂项应收(押金退还、赔偿款、口车费等,不走订单流水的零散应收)。 支撑这个 tab,往来期初接口 `POST /admin/finance/opening-balances` 的账套枚举 `ledgerType` 新增第三个值 **`SUPPLIER_RECV`**(供应商应收)。 原有账套语义不变: | 账套值 | 中文 | 方向 | 谁欠谁 | |---|---|---|---| | `SUPPLIER` | 供应商应付 | 应付 | 我们欠供应商 | | `CUSTOMER` | 客户应收 | 应收 | 客户欠我们 | | `SUPPLIER_RECV` ✨ | 供应商应收 | 应收 | 供应商欠我们(本次新增) | > ⚠️ 枚举值是 `SUPPLIER_RECV`(13 字符),**不是** `SUPPLIER_RECEIVABLE`(18 字符超列宽,#8511 已改短)。前端写常量时别用长名。 ## 二、变更清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 往来期初一次录入 | POST | `/admin/finance/opening-balances` | 修改接口 | `ledgerType` 新增 `SUPPLIER_RECV`;金额方向校验收紧(见 §十) | 配套数据源接口(均**早已上线**,无变更,仅列出供新 tab 对接):供应商下拉 `GET /admin/supplier/items/list`、应收性质下拉 `GET /admin/finance/receipt/options`、所属公司下拉 `GET /v3/admin/travel-agency/enabled`,详见 §4.3 ~ §4.5。 ## 三、接口详情 - **路径**:`POST /admin/finance/opening-balances` - **认证**:网关 JWT(admin) - **幂等性**:否。但同一往来对象同一账套**仅可录一次**初始期初,重复提交被 598401 拦截(要改金额走「期初调整」),不会产生脏数据 - **限流**:无特殊限流 **应收初始化(SUPPLIER_RECV)表单最终形态(7 个元素)**: | # | 表单元素 | 控件 | 必填 | 提交字段 | |---|---|---|---|---| | 1 | 供应商 | 下拉(可搜索) | ✅ | `refId` + `refName` | | 2 | 应收性质 | 下拉 | ✅ | `customerCategory`(复用此字段存应收性质码值) | | 3 | 应收金额 | 数字输入 | ✅ | `openingReceivable` | | 4 | 所属公司 | 下拉 | ✅ | `companyId` | | 5 | 记账日期 | 只读展示 | — | **后端取,前端不传** | | 6 | 佐证材料 | 图片上传 | ❌ 选填 | `evidenceUrl` | | 7 | 备注 | 文本域 | ❌ 选填 | `remark`(≤ 200 字) | **本 tab 表单上不要出现的元素**: | 不要出现 | 原因 | |---|---| | 应付金额框 | 供应商应收账套只有应收方向,`openingPayable` 不传 | | 供应商 ID / 名称手填框 | 必须走下拉,ID 与名称由选项带出 | | 记账日期输入框 | 只读展示,后端取当前未封账账期 startDate 落库 | ## 四、接口入参 ### 4.1 路径参数 / Query 参数 无。 ### 4.2 请求体字段(SUPPLIER_RECV 账套) | 字段 | 类型 | 必填 | 说明 | 校验规则 | |------|------|------|------|----------| | `ledgerType` | String | ✅ | 账套类型。本 tab **固定传 `SUPPLIER_RECV`** | 枚举,见 §6.1 | | `refId` | String | ✅ | 供应商 ID(取下拉项的 `supplierId`) | 必须是存在的供应商 | | `refName` | String | ✅ | 供应商名称(取下拉项的 `fullName`) | ≤ 128 字 | | `customerCategory` | String | ✅ | 应收性质码值(复用该字段,取应收性质下拉的 `value`) | 必须是应收性质字典 `fin_recv_nature` 的值,见 §6.2;缺失 → 598409,取值非法 → 598410 | | `companyId` | String | ✅ | 所属公司主体 ID(取下拉项的 `agencyId`) | 必须是启用中的公司主体,否则 598407 | | `openingReceivable` | Number | ✅ | 应收期初金额(元) | 必填且 **> 0**,否则 598405 | | `evidenceUrl` | String | ❌ | 佐证材料图片 URL | 可空 / 不传 | | `remark` | String | ❌ | 备注 | ≤ 200 字 | **不要传的字段**: | 字段 | 说明 | |---|---| | `openingPayable` | 应付期初金额,应收账套(CUSTOMER / SUPPLIER_RECV)不传 | | 记账日期相关字段 | 请求体里没有记账日期字段,后端自动取当前未封账账期 startDate | > ⚠️ `refId` / `companyId` 后端是 Long 大数(雪花 ID),JSON 序列化为字符串下发;前端 JS 一律**当字符串处理**,不要 `Number()` 转换,防精度丢失。 ### 4.3 供应商下拉数据源 - **路径**:`GET /admin/supplier/items/list`(资源服务,已上线) - **认证**:网关 JWT(admin) Query 参数(全部选填): | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `status` | String | ❌ | 本表单**固定传 `ACTIVE`**(只在用供应商可录期初) | | `keyword` | String | ❌ | 名称 / 编号模糊搜索 | | `limit` | Integer | ❌ | 默认 50,最大 200 | 响应 `data` 为数组,每项字段: | 字段 | 类型 | 说明 | |------|------|------| | `supplierId` | String | 供应商 ID(Long 序列化字符串) | | `fullName` | String | 供应商全称(下拉显示用) | | `shortName` | String | 简称 | | `supplierNo` | String | 供应商编号 | | `statusName` | String | 状态中文名 | **取值映射**:下拉显示 `fullName`;选中后提交 `refId = supplierId`、`refName = fullName`。 ### 4.4 应收性质下拉数据源 - **路径**:`GET /admin/finance/receipt/options`(#8501 已上线) - **认证**:网关 JWT(admin) - **入参**:无 - **取数**:取响应 `data.recvNatures` 数组(每项 `value` / `label`) - **取值映射**:下拉显示 `label`;选中后提交 `customerCategory = value`。字典 `fin_recv_nature` 值见 §6.2 ### 4.5 所属公司下拉数据源 - **路径**:`GET /v3/admin/travel-agency/enabled`(order-v3,已上线) - **认证**:网关 JWT(admin) - **入参**:无 响应 `data` 为数组,每项字段: | 字段 | 类型 | 说明 | |------|------|------| | `agencyId` | String | 公司主体 ID(Long 序列化字符串) | | `agencyName` | String | 公司名称(下拉显示用) | | `isPrimary` | Integer | 是否主体公司:1 = 是,0 = 否 | **取值映射**:下拉显示 `agencyName`;`isPrimary = 1` 的主体公司**默认选中**;选中后提交 `companyId = agencyId`。公司名称由后端自取快照,前端不用传。 ### 4.6 记账日期展示值来源(只读,不入参) 记账日期 = 当前**未封账**账期的 `startDate`,后端落库时自动取,前端不传。若表单上要展示该值: - **路径**:`GET /admin/finance/account-periods/page` - **取数**:取返回列表中 `isClosed = 0`(未封账)那一行的 `startDate` 展示即可 ## 五、出参 | 字段 | 类型 | 说明 | |------|------|------| | `data` | String | 新建期初行 ID(Long 序列化字符串,JS 当字符串处理) | 外层为标准响应包:`code` / `data` / `message` / `success`。 ## 六、枚举 / 数据字典 ### 6.1 ledgerType(账套类型) **所属字段**:请求体 `ledgerType` | **类型**:`String` | **必填**:✅ | 值 | 中文 | 说明 | |----|------|------| | `SUPPLIER` | 供应商应付 | 我们欠供应商;只传 `openingPayable` | | `CUSTOMER` | 客户应收 | 客户欠我们;只传 `openingReceivable` | | `SUPPLIER_RECV` ✨ | 供应商应收 | 供应商欠我们(杂项应收);只传 `openingReceivable`,且 `customerCategory` 必填 | ### 6.2 customerCategory(应收性质,字典 fin_recv_nature) **所属字段**:请求体 `customerCategory` | **类型**:`String` | **必填**:✅(SUPPLIER_RECV 账套必填) | 值 | 中文 | 说明 | |----|------|------| | `DEPOSIT_REFUND` | 押金退还 | 供应商应退未退的押金 | | `COMPENSATION` | 赔偿款 | 供应商应赔付的款项 | | `CAR_FEE` | 口车费 | 供应商应付的口车费用 | | `OTHER` | 其他应收 | 其他杂项应收 | > 数据源是 §4.4 的 `/admin/finance/receipt/options`(`recvNatures` 数组),**前端不要写死这 4 个值**,后端字典加值时下拉自动多一项。 ## 七、错误码 | code | 含义 | 触发场景 | 前端 toast 建议文案 | |------|------|----------|---------------------| | 598409 | 应收性质必填 | SUPPLIER_RECV 账套未传 `customerCategory` | 请选择应收性质 | | 598410 | 应收性质取值非法 | `customerCategory` 不在应收性质字典内 | 应收性质无效 | | 598405 | 期初金额未填或 ≤ 0 | `openingReceivable` 缺失 / 为 0 / 负数 | 请填写正确的应收金额 | | 598401 | 该往来对象期初已录过 | 同一供应商同账套重复提交(要改走「期初调整」) | 该供应商期初已录入,要改走「期初调整」 | | 598407 | 所属公司非法或已停用 | `companyId` 不存在或公司主体已停用 | 所属公司无效 | | 598403 | 无未封账账期 | 当前没有 `isClosed = 0` 的账期,无法落记账日期 | 当前无进行中账期 | | 598406 | 账套非法 | `ledgerType` 不是 SUPPLIER / CUSTOMER / SUPPLIER_RECV 之一 | 账套须为 SUPPLIER/CUSTOMER/SUPPLIER_RECV | ## 八、示例 ### 8.1 典型成功 **请求**: ```http POST /admin/finance/opening-balances Authorization: Bearer Content-Type: application/json ``` ```json { "ledgerType": "SUPPLIER_RECV", "refId": "1834567890123456789", "refName": "内蒙古某车队有限公司", "customerCategory": "DEPOSIT_REFUND", "companyId": "1723456789012345678", "openingReceivable": 1500.00, "evidenceUrl": "https://oss.example.com/finance/evidence/x.jpg", "remark": "2025 年度押金应退未退" } ``` **响应**: ```json { "code": 200, "data": "1956789012345678901", "message": "成功", "success": true } ``` ### 8.2 边界(佐证、备注均不传,金额最小值) **场景说明**:`evidenceUrl` / `remark` 选填,最小合法请求 6 个字段;金额边界 0.01。 **请求**: ```json { "ledgerType": "SUPPLIER_RECV", "refId": "1834567890123456789", "refName": "内蒙古某车队有限公司", "customerCategory": "OTHER", "companyId": "1723456789012345678", "openingReceivable": 0.01 } ``` **响应**: ```json { "code": 200, "data": "1956789012345678902", "message": "成功", "success": true } ``` ### 8.3 业务失败(应收性质未传,触发 598409) **请求**: ```json { "ledgerType": "SUPPLIER_RECV", "refId": "1834567890123456789", "refName": "内蒙古某车队有限公司", "companyId": "1723456789012345678", "openingReceivable": 1500.00 } ``` **响应**: ```json { "code": 598409, "data": null, "message": "应收性质必填", "success": false } ``` > 其他常见失败:重复提交同一供应商 → `598401`;`ledgerType` 拼错(如 `SUPPLIER_RECEIVABLE` 长名)→ `598406`。 ## 九、业务边界 - ✅ **适用场景**:供应商存在杂项应收(押金退还 / 赔偿款 / 口车费 / 其他)首次录期初,且当前存在未封账账期、所属公司主体启用中。 - ❌ **不适用场景**: - 该供应商已录过本账套期初 → 598401,改金额请走「期初调整」,不要重复提交; - 无未封账账期 → 598403; - 所属公司已停用 → 598407。 - ⚠️ **特殊边界**: - 记账日期恒等于当前未封账账期 startDate(后端落库),不可指定历史 / 未来日期; - 一账套一方向:SUPPLIER_RECV 只录应收,录应付请切「应付初始化」tab(`SUPPLIER` 账套)。 ## 十、修改前后对比 ### 10.1 字段 / 枚举级对比 | 字段 | 改前 | 改后 | |------|------|------| | `ledgerType` | 2 个值:`SUPPLIER` / `CUSTOMER` | 3 个值:新增 **`SUPPLIER_RECV`**(供应商应收) | | `customerCategory` | 仅 CUSTOMER 账套使用 | SUPPLIER_RECV 账套**复用此字段**存应收性质码值(必填,字典 `fin_recv_nature`) | ### 10.2 行为级对比(金额方向收紧,本次同步生效) | 账套 | 改前 | 改后 | |------|------|------| | `SUPPLIER` | 应收 / 应付金额校验相对宽松 | **只认 `openingPayable` > 0**,应收方向不传 | | `CUSTOMER` | 同上 | **只认 `openingReceivable` > 0**,应付方向不传 | | `SUPPLIER_RECV` | (不存在) | **只认 `openingReceivable` > 0**,且 `customerCategory` 必填 | ## 十一、影响评估 / 回滚 - **是否破坏向后兼容**:基本否。`SUPPLIER` / `CUSTOMER` 已有表单若本来就按「一账套一方向」正确提交(只传一个方向金额),不受金额方向收紧影响;`SUPPLIER_RECV` 是纯新增枚举值,老代码不传它即可。 - **前端是否必须同步上线**:是(针对「应收初始化」tab)。该 tab 未上线前后端能力空转无影响;tab 上线时必须按本文档字段映射对接。 - **影响已有数据**:无(无需前端配合的数据迁移)。 - **回滚方案**:后端 revert 两个 PR 即可;前端 tab 未上线则无需回滚。 ## 十二、注意事项 - ⚠️ 枚举值是 **`SUPPLIER_RECV`(13 字符)**,不是 `SUPPLIER_RECEIVABLE`(18 字符超列宽,#8511 已改短)。拼错会触发 598406。 - ⚠️ `refId` / `companyId` / 响应 `data` 均为 Long 大数序列化的字符串,JS 全程当字符串处理,禁止 `Number()` 转换。 - ⚠️ `customerCategory` 字段在 SUPPLIER_RECV 账套下语义是「应收性质」,值必须来自 §4.4 下拉(字典 `fin_recv_nature`),**不要写死 4 个码值**。 - ⚠️ 记账日期前端不传;如需展示,查 `GET /admin/finance/account-periods/page` 取 `isClosed = 0` 行的 `startDate`。 - ⚠️ 佐证材料 `evidenceUrl` 是**选填**,不要加必填校验拦截提交。 - 本次已部署测试服并行为验证通过,前端可立即联调。 ## 十三、关联 / 联系人 ### 13.1 链接 - **Issue**: [#8507](https://git.1814.love/wx/HL/issues/8507) - **PR**: [#8509](https://git.1814.love/wx/HL/pulls/8509)、[#8513](https://git.1814.love/wx/HL/pulls/8513) - **前置依赖**: [#8501](https://git.1814.love/wx/HL/issues/8501)(应收性质字典 + `/receipt/options` 接口)、[#8511](https://git.1814.love/wx/HL/issues/8511)(枚举值改短 SUPPLIER_RECEIVABLE → SUPPLIER_RECV) ### 13.2 联系人 - **后端负责人**: yst