fix: 代理

This commit is contained in:
luo
2026-09-12 11:27:50 +08:00
parent d3d257cdf8
commit 2308d82d0f
44 changed files with 5470 additions and 200 deletions

View File

@@ -0,0 +1,88 @@
## Context
员工代收款是 8 月迭代新增的财务能力,普通员工与超级管理员共用同一套接口,靠登录态区分数据范围。后台管理端需要新增三类页面,并复用已有的表格、搜索、详情与上传组件。`docs/admin-openapi.yaml` 未随仓库提供,接口字段以后端契约(需求文档 + 创建订单接口 OpenAPI 片段)为准,类型集中在一个文件便于联调收敛。
## Goals / Non-Goals
**Goals**
- 在财务管理下提供收款方式、员工代收款账单、核销申请三类页面。
- 复用 `ArtTableFullScreen``ArtSearchBar``ArtTableHeader``ArtTable``DetailPage``VoucherUpload``PaymentVoucherDialog``useCheckedColumns` 等既有组件与约定。
- 复用既有企微审批场景配置能力,仅新增业务类型。
**Non-Goals**
- 不实现后端接口、数据库、Worker、企微回调。
- 不实现 H5/C 端页面与支付流程。
- 不新增导出任务场景(需求文档未要求)。
## Decisions
### 接口契约
| 能力 | 关键字段 |
|---|---|
| 统一响应 | `{ code, data, msg, timestamp }` |
| 列表分页 | 账单列表返回 `{ items, total, page, size }`,取数处对 `items` / `list` / `records` 做兼容 |
| 收款方式 | `{ id, code, name, sort, enabled, remark, created_at, updated_at }`,列表接口返回 `{ items, page, size, total }`,支持 `page` / `page_size` / `enabled` / `keyword` 筛选 |
| 账单列表项 | `{ id, source_type, source_type_name, source_no, debtor_snapshot, customer_snapshot, receivable_amount, received_amount, reserved_amount, remaining_amount, status, status_name, approval_pending, closed_reason, created_at, updated_at }` |
| 账单详情 | `data``{ bill, refunds, allocations, applications }``refunds` 为退款冲销(`refund_id``source_order_id``refund_amount``reduced_amount``bill_receivable_amount``outcome_name``allocations` 为账单侧分摊(含 `application_id` / `application_status_name` / `attempt_id``applications` 内嵌该申请的 `attempts` |
| 账单统计 | `{ receivable_total, received_total, unsettled_total, pending_bill_count }` |
| 账单筛选 | `page``page_size``source_type``source_no``status``debtor_account_id``customer_id``created_from``YYYY-MM-DD`)、`created_to``YYYY-MM-DD` |
| 核销申请请求体 | `{ payment_method_id, paid_amount, paid_at, payer_name, external_transaction_no, payment_voucher_keys, remark, allocations: [{ bill_id, amount }], acting_reason }` |
| 核销申请列表项 | `{ id, applicant_account_id, acting_operator_id, payment_method_id, payment_method_name, paid_amount, payer_name, external_transaction_no, status, status_name, terminal_reason, decided_at, created_at, updated_at }` |
| 核销申请详情 | `data``{ application, allocations, attempts }``attempts` 保存每次提交的完整材料快照,重新提交不清空历史 |
账单状态为数字枚举 `0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭;申请状态为数字枚举 `0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭;两者展示均优先使用后端 `status_name`
### 页面与路由组织
`/finance` 下新增:
| 路由 | 页面 | 说明 |
|---|---|---|
| `/finance/employee-collection/bills` | 员工代收款账单 | 统计 + 列表 |
| `/finance/employee-collection/bills/detail/:id` | 账单详情 | 隐藏菜单 |
| `/finance/employee-collection/applications` | 核销申请 | 列表 + 创建 |
| `/finance/employee-collection/applications/detail/:id` | 核销申请详情 | 隐藏菜单 |
| `/finance/employee-collection/payment-methods` | 收款方式管理 | 仅超管 |
账单与申请拆分为独立菜单,符合项目「列表页 + 详情页」的既有组织方式,避免单页堆叠过多交互。账单列表的「店铺」筛选用远程搜索复用 `ShopService.getShops`,与退款列表一致。
账单详情与核销申请详情保持只读:页面只在顶部保留「返回」导航,创建核销申请、关闭账单、修改并重新提交等操作入口统一放在列表页的操作列,详情页不出现业务操作按钮。
### 权限编码
新增 `src/config/constants/augustIteration.ts`,沿用 `模块:动作` 风格,例如 `employee_collection:bill_close``employee_collection:application_create`。页面级 `permissions` 用于菜单可见性,按钮级编码用于 `hasAuth()` / `v-permission`
### 金额、时间与附件
- 金额统一以「分」传输,展示时通过 `fenToYuan` / `formatCurrency` 转换,与退款、代理充值保持一致。
- 所有时间字段统一通过 `formatDateTime` 格式化为 `YYYY-MM-DD HH:mm:ss`,不在模板中直接输出后端原始时间字符串。
- 附件仅返回对象 Key`payment_voucher_keys`),展示复用 `PaymentVoucherDialog`,由预签名下载接口换取访问地址。
### 核销申请分摊
创建申请时按账单逐条录入核销金额,并填写付款事实(付款金额、付款方名称、付款时间、外部交易流水号),前端校验:
- 至少选择 1 张账单,最多 N 张;
- 单张核销金额不得大于账单未核销金额,且大于 0
- 付款金额(`paid_amount`,分)不得小于各账单分摊之和(允许存在差额);
- 超管代办时 `acting_reason` 必填;
- 付款凭证至少 1 个 Key。
提交成功后自动发起企微审批;仅已驳回申请可再次进入弹窗修改并重新提交,重新提交生成新的审批实例,历史审批记录只读展示。未配置企微审批场景时后端返回 503前端展示「企微审批场景未配置请联系管理员」并保留已填内容。
### 企微审批场景
`WecomBusinessType` 增加 `employee_collection_approval`,企微审批场景页面下拉新增「员工代收款审批」。模板控件同步、字段查询、字段映射保存全部复用既有 `WecomService`
### 订单付款凭证规则
线下订单创建时,满足「平台账号(`user_type` 为 1 或 2操作 + 非赠送套餐 + 实际收款金额大于 0」条件的订单会生成员工代收款账单此时 `payment_voucher_key` 非必填,字段结构不变,付款凭证改在核销申请中提交。前端以当前登录账号类型、所选套餐是否赠送、套餐有效价格(`effective_retail_price` / `suggested_retail_price` / `retail_price`)判断是否展示提示并放宽必填;赠送套餐或其他非平台账号的线下订单仍需上传凭证。
## Risks / Trade-offs
- **接口字段以契约文档为准**`docs/admin-openapi.yaml` 未入库,字段来自需求文档与创建订单接口片段;类型集中在一个文件,便于联调时收敛修改。
- **员工/超管同接口**:前端不做数据范围过滤,仅做展示与操作可见性控制,数据隔离以后端为准。
- **关闭账单与审批中申请**:前端依据 `approval_pending` 与状态字段禁用关闭按钮,最终一致性以后端校验为准。

View File

@@ -0,0 +1,25 @@
# Change: 员工代收款功能前端对接
## Why
8 月产品迭代新增「员工代收款」能力:员工线下代收款项后,通过创建核销申请、经企业微信审批完成账单核销;超级管理员可维护收款方式、查看全部账单并关闭账单。后端接口已按 `docs/产品迭代8月份/员工代收款功能简介.md` 落地,后台管理端目前缺少收款方式管理、账单列表与核销申请页面,普通员工与超级管理员的分类视图、以及线下订单付款凭证规则尚未接入。
本次已按后端实际契约对齐字段:列表响应统一使用 `items` / `total` / `page` / `size`;账单状态(`0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭)与申请状态(`0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭)为数字枚举;收款方式列表返回 `{ items, page, size, total }` 并支持 `enabled` / `keyword` 筛选;核销申请请求体使用 `paid_amount``paid_at``payer_name``external_transaction_no``payment_voucher_keys``allocations`
## What Changes
- 新增 `employee-collection` 能力的前端类型与服务封装:收款方式 4 个接口、员工代收款账单 4 个接口、核销申请 4 个接口。
- 新增「收款方式管理」页面(仅超级管理员):列表 + 新增/编辑弹窗 + 删除;已被核销申请引用的方式不可删除、不可修改 `code`(未被引用时可改),可停用。
- 新增「员工代收款账单」页面:应收/已核销/未核销/待处理账单统计 + 列表 + 详情;普通员工仅见本人账单,超级管理员可见全部;存在审批中申请时账单不可关闭,关闭必须填写原因。
- 新增「核销申请」页面:列表 + 详情(含分摊账单、付款凭证、付款信息与全部审批尝试记录)+ 创建/重新提交弹窗;一笔线下收款可核销 1N 张账单,填写付款金额、付款方名称、付款时间、外部交易流水号、选择收款方式并上传付款凭证;提交后自动发起企微审批;仅已驳回申请可修改并重新提交,重新提交生成新的审批实例且历史记录不被覆盖。
- 扩展企业微信审批场景:新增 `employee_collection_approval` 业务类型,复用既有企微应用列表、模板控件同步与业务字段查询接口。
- 调整订单创建:由平台账号操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单,该场景 `payment_voucher_key` 改为非必填,付款凭证改在核销申请中提交,字段结构保持不变。
- 附件接口仅返回对象 Key统一通过系统既有预签名下载接口展示。
- **不实现后端接口、数据库、Worker、企微回调****不实现 H5/C 端页面**。
## Impact
- Affected specs: `employee-collection`
- Affected code: `src/types/api/employeeCollection.ts``src/api/modules/employeeCollection.ts``src/views/finance/employee-collection/*``src/router/routes/asyncRoutes.ts``src/router/routesAlias.ts``src/config/constants/augustIteration.ts``src/types/api/wecom.ts``src/views/settings/wecom/scenes/index.vue``src/views/order-management/order-list/index.vue``src/locales/langs/{zh,en}.json`
- Dependencies: `docs/产品迭代8月份/员工代收款功能简介.md`
- Contract note: `docs/admin-openapi.yaml` 未随仓库提供,字段以需求文档描述的后端契约与创建订单接口的 OpenAPI 片段为准;类型集中在单一文件,联调时便于收敛。

View File

@@ -0,0 +1,155 @@
## ADDED Requirements
### Requirement: 收款方式管理
超级管理员 MUST 能够维护线下收款方式,包括名称、编码、排序、状态与备注;普通员工只能看到启用的收款方式。收款方式列表接口 MUST 返回 `{ items, page, size, total }`,并 MUST 支持 `enabled``keyword` 筛选。已被核销申请引用的收款方式 MUST NOT 被删除;引用后修改稳定编码 MUST 被后端拒绝,前端 MUST 展示错误提示。
#### Scenario: 超级管理员新增收款方式
- **GIVEN** 超级管理员已登录并拥有收款方式新增权限
- **WHEN** 其填写名称、唯一编码、排序、状态与备注并提交
- **THEN** 系统 MUST 调用新增接口并在成功后刷新列表
- **AND** 新增成功后 MUST 清空并关闭弹窗
#### Scenario: 删除被引用的收款方式
- **GIVEN** 某收款方式已被业务引用
- **WHEN** 超级管理员尝试删除该方式
- **THEN** 前端 MUST 阻止删除或展示后端返回的业务错误
- **AND** MUST 提示改为停用
#### Scenario: 修改收款方式编码
- **GIVEN** 超级管理员打开编辑弹窗
- **WHEN** 其修改稳定编码并提交
- **THEN** 前端 MUST 提交最新编码
- **AND** 若该方式已被核销申请引用,后端拒绝时前端 MUST 展示错误提示
### Requirement: 员工代收款账单统计
账单页面 MUST 展示应收金额、已核销金额、未核销金额与待处理账单数量四项统计,数据 MUST 来自 `GET /api/admin/employee-collection-bills/statistics`,字段为 `receivable_total``received_total``unsettled_total``pending_bill_count`;前端 MUST NOT 通过遍历当前分页数据自行计算。
#### Scenario: 加载账单统计
- **GIVEN** 用户进入员工代收款账单页面
- **WHEN** 页面初始化或筛选条件变化
- **THEN** 前端 MUST 调用账单统计接口
- **AND** MUST 将后端返回的「分」按元格式化后展示应收、已核销与未核销金额
### Requirement: 员工代收款账单列表与详情
账单列表响应 MUST 为 `{ items, total, page, size }`,前端 MUST 兼容 `items` / `list` / `records` 等列表字段。列表 MUST 支持按来源(`source_type`)、来源单号(`source_no`)、账单状态、客户/店铺(`customer_id`)与创建时间(`created_from` / `created_to``YYYY-MM-DD`)筛选,并展示账单编号、来源、关联单号、负责员工、客户/店铺、应收金额、已核销金额、未核销金额与状态。账单状态 MUST 为数字枚举:`0` 待核销、`1` 部分核销、`2` 已核销、`3` 已关闭,展示 MUST 优先使用后端 `status_name`。账单详情响应 MUST 为 `{ bill, refunds, allocations, applications }``refunds` MUST 展示退款金额、冲减应收、冲销前应收与处理结果,`applications` MUST 可展开查看该申请的审批尝试记录。
#### Scenario: 普通员工查看账单
- **GIVEN** 普通员工已登录
- **WHEN** 其打开账单列表
- **THEN** 列表 MUST 只展示后端返回的本人账单数据
- **AND** MUST NOT 展示仅超管可见的操作入口
#### Scenario: 打开账单详情
- **GIVEN** 用户拥有账单详情权限
- **WHEN** 其点击账单号或详情操作
- **THEN** 前端 MUST 跳转账单详情页并加载对应账单(详情数据取自 `data.bill`
- **AND** 详情 MUST 展示退款冲销、核销分摊与关联核销申请
### Requirement: 关闭账单
超级管理员 MUST 能够关闭账单,且关闭原因必填(最多 500 字符)。仅待核销或部分核销账单可关闭;存在审批中的核销申请(`approval_pending` 为真)时,账单 MUST NOT 被关闭。
#### Scenario: 存在审批中申请时关闭账单
- **GIVEN** 账单存在审批中的核销申请
- **WHEN** 用户查看该账单操作
- **THEN** 关闭入口 MUST 被禁用或不可见
- **AND** MUST 展示不可关闭的原因提示
#### Scenario: 关闭原因必填
- **GIVEN** 账单可关闭
- **WHEN** 超级管理员打开关闭弹窗并留空原因提交
- **THEN** 前端 MUST 阻止提交并提示填写原因
### Requirement: 创建核销申请
员工 MUST 能够使用一笔线下收款核销 1N 张账单。创建申请 MUST 提交收款方式 `payment_method_id`、付款金额 `paid_amount`(分,大于 0、付款方名称 `payer_name`、付款时间 `paid_at`(带时区 RFC3339、外部交易流水号 `external_transaction_no`、付款凭证 `payment_voucher_keys`15 个对象 Key与账单分摊 `allocations`。超级管理员代办时 MUST 填写 `acting_reason`,本人办理 MUST NOT 提交该字段。提交成功后系统 MUST 自动发起企业微信审批。
#### Scenario: 一笔收款核销多张账单
- **GIVEN** 用户选择了多张可核销账单
- **WHEN** 其录入各账单核销金额、选择收款方式并填写付款事实后提交
- **THEN** 前端 MUST 校验各账单核销金额大于 0 且不超过账单未核销余额
- **AND** MUST 以 `allocations: [{ bill_id, amount }]` 提交账单分摊明细
#### Scenario: 付款金额小于核销合计
- **GIVEN** 用户已录入各账单核销金额
- **WHEN** 其填写的付款金额小于核销合计即提交
- **THEN** 前端 MUST 阻止提交并提示付款金额不能小于核销合计
#### Scenario: 缺少付款凭证
- **GIVEN** 用户已选择账单与收款方式
- **WHEN** 其未上传任何付款凭证即提交
- **THEN** 前端 MUST 阻止提交并提示上传付款凭证
#### Scenario: 缺少付款事实
- **GIVEN** 用户已选择账单与收款方式
- **WHEN** 其未填写付款方名称、付款时间或外部交易流水号即提交
- **THEN** 前端 MUST 阻止提交并提示补齐必填项
#### Scenario: 超管代办未填写原因
- **GIVEN** 超级管理员以代办身份创建申请
- **WHEN** 其未填写 `acting_reason` 即提交
- **THEN** 前端 MUST 阻止提交并提示填写代办原因
#### Scenario: 企微审批场景未配置
- **GIVEN** 企业微信审批场景尚未配置
- **WHEN** 用户提交核销申请
- **THEN** 前端 MUST 展示后端返回的 503 提示「企微审批场景未配置,请联系管理员」
- **AND** MUST 保留用户已填写的内容且不产生申请数据
### Requirement: 核销申请列表与详情
核销申请列表响应 MUST 为 `{ items, page, size, total }`MUST 支持按状态、收款方式与创建时间筛选;列表项 MUST 包含 `payment_method_name``paid_amount``status``status_name``created_at`,状态 MUST 优先展示后端 `status_name`。申请详情响应 MUST 为 `{ application, allocations, attempts }``attempts` MUST 按提交顺序展示全部审批尝试记录,包含付款金额、付款方、流水号、付款凭证与审批意见。
#### Scenario: 查看审批历史
- **GIVEN** 申请存在多次审批尝试记录
- **WHEN** 用户打开申请详情
- **THEN** 详情 MUST 按提交顺序展示每次尝试的提交材料与审批状态
- **AND** 历史材料 MUST NOT 因重新提交而被覆盖
### Requirement: 核销申请重新提交
仅已驳回(`status``2`)的申请 MUST 允许修改并重新提交;重新提交 MUST 生成新的企业微信审批实例,且历史审批记录 MUST NOT 被覆盖。重新提交入口 MUST 位于核销申请列表的操作列,详情页 MUST 只读且 MUST NOT 展示业务操作按钮(仅保留返回导航)。
#### Scenario: 重新提交被驳回申请
- **GIVEN** 申请状态为已驳回
- **WHEN** 用户在核销申请列表点击「修改并重新提交」并修改账单分摊、收款方式、付款事实或付款凭证后提交
- **THEN** 前端 MUST 调用修改接口重新提交
- **AND** 成功后 MUST 刷新详情并展示新的审批实例状态
#### Scenario: 非驳回申请不可修改
- **GIVEN** 申请处于审批中或已通过
- **WHEN** 用户查看核销申请列表
- **THEN** 修改并重新提交入口 MUST 不可见或不可用
#### Scenario: 详情页只读
- **GIVEN** 用户打开账单详情或核销申请详情
- **WHEN** 页面渲染完成
- **THEN** 页面 MUST NOT 展示创建核销申请、关闭账单或修改并重新提交等业务操作按钮
- **AND** MUST 只保留返回导航
### Requirement: 企业微信审批场景配置
企业微信审批场景 MUST 支持 `employee_collection_approval` 业务类型,复用既有的应用列表、模板控件同步、业务字段查询与字段映射保存接口。
#### Scenario: 配置员工代收款审批场景
- **GIVEN** 超级管理员打开企微审批场景页面
- **WHEN** 其选择业务类型「员工代收款审批」
- **THEN** 前端 MUST 使用 `employee_collection_approval` 调用模板同步、字段查询与保存接口
### Requirement: 附件预签名展示
附件接口 MUST 只返回对象存储 Key`payment_voucher_keys`);前端 MUST 通过系统既有的预签名下载接口获取实际访问地址后再展示。
#### Scenario: 查看付款凭证
- **GIVEN** 申请包含付款凭证 Key
- **WHEN** 用户点击查看付款凭证
- **THEN** 前端 MUST 先批量换取预签名地址
- **AND** 图片 MUST 支持预览,非图片 MUST 支持查看或下载
### Requirement: 订单付款凭证规则调整
由平台账号(`user_type` 为 1 或 2操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单;该场景 `payment_voucher_key` MUST 变为非必填,付款凭证改在核销申请中提交,订单字段结构 MUST 保持不变。其余线下订单 MUST 继续要求付款凭证。
#### Scenario: 线下订单生成代收款账单
- **GIVEN** 当前登录账号为平台账号,所选套餐非赠送且实际收款金额大于 0支付方式为线下支付
- **WHEN** 用户创建该订单
- **THEN** 前端 MUST 不再强制要求上传付款凭证
- **AND** MUST 提示付款凭证将在核销申请中提交
#### Scenario: 赠送套餐或非平台账号的线下订单
- **GIVEN** 所选套餐为赠送套餐,或当前账号非平台账号,或实际收款金额为 0
- **WHEN** 用户以线下支付方式创建订单
- **THEN** 前端 MUST 继续要求上传付款凭证

View File

@@ -0,0 +1,48 @@
## 1. Contract and API Types
- [x] 1.1 新增 `src/types/api/employeeCollection.ts`:收款方式、账单、核销申请、统计、查询参数与请求/响应类型;列表统一使用 `items` / `total` / `page` / `size`
- [x] 1.2 定义账单状态(`0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭)与申请状态(`0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭)数字枚举,并保留后端 `*_name` 展示字段。
- [x] 1.3 金额字段以「分」传输;附件字段使用 `payment_voucher_keys` 对象存储 Key 数组,展示复用预签名下载接口。
- [x] 1.4 新增 `src/api/modules/employeeCollection.ts` 并在 `src/api/modules/index.ts``src/types/api/index.ts` 导出。
- [x] 1.5 按后端实际契约收敛字段:账单详情为 `{ bill, refunds, allocations, applications }`,核销申请详情为 `{ application, allocations, attempts }`,付款金额/付款方/付款时间/外部交易流水号以 `paid_amount` / `payer_name` / `paid_at` / `external_transaction_no` 提交。
## 2. Permissions, Routes, and Menu
- [x] 2.1 新增 `src/config/constants/augustIteration.ts`,集中声明页面与按钮权限编码。
- [x] 2.2 在 `src/router/routesAlias.ts``src/router/routes/asyncRoutes.ts` 的财务管理下新增账单、核销申请、收款方式路由。
- [x] 2.3 在 `src/locales/langs/zh.json``en.json``menus.financialManagement` 下补充菜单文案。
## 3. Payment Methods Page
- [x] 3.1 实现收款方式列表(名称、编码、排序、状态、备注、创建时间);接口返回 `{ items, page, size, total }`,复用 `ArtTableFullScreen` / `ArtSearchBar` / `ArtTableHeader` / `ArtTable`
- [x] 3.2 实现新增/编辑弹窗;编辑时允许提交 `code`,被核销申请引用时由后端拒绝并提示。
- [x] 3.3 实现删除操作,被引用的方式给出提示并引导停用。
## 4. Bills Pages
- [x] 4.1 实现账单统计卡片(应收、已核销、未核销、待处理账单数量),数据来自 `GET /api/admin/employee-collection-bills/statistics`
- [x] 4.2 实现账单列表与筛选(来源、来源单号、状态、客户/店铺、创建时间范围),展示账单号、来源、关联单号、负责员工、客户/店铺、应收/已核销/未核销金额与状态。
- [x] 4.3 实现账单详情,展示账单信息、退款冲销、核销分摊与关联核销申请(关联申请可展开查看审批尝试记录)。
- [x] 4.4 实现超管关闭账单弹窗,关闭原因必填;存在审批中申请时禁用关闭并给出说明。
- [x] 4.5 列表「创建核销申请」入口按权限与账单状态控制可用性。
## 5. Applications Pages
- [x] 5.1 实现核销申请列表与筛选(状态、收款方式、创建时间),展示申请编号、收款方式、付款金额、状态与提交时间。
- [x] 5.2 实现创建/重新提交弹窗:选择 1N 张可核销账单、按账单录入分摊金额、填写付款金额/付款方名称/付款时间/外部交易流水号、选择收款方式并上传付款凭证。
- [x] 5.3 超管代办时必须填写 `acting_reason`,否则禁止提交。
- [x] 5.4 实现申请详情,展示申请信息、分摊账单、付款凭证与全部审批尝试记录;历史记录只读,不被重新提交覆盖。
- [x] 5.5 仅已驳回申请展示「修改并重新提交」,提交成功后生成新的审批实例并刷新详情。
- [x] 5.6 统一处理 503「企微审批场景未配置」等业务错误保留用户已填内容。
## 6. WeCom Scene and Order Rule
- [x] 6.1 扩展 `WecomBusinessType` 增加 `employee_collection_approval`,并在企微审批场景页面新增可选业务类型。
- [x] 6.2 场景保存复用既有 `inspectTemplate``getBusinessFields``saveScene` 接口,无需新增企微接口。
- [x] 6.3 调整线下订单创建:平台账号操作、非赠送且实际收款金额大于 0 时 `payment_voucher_key` 非必填,字段结构不变。
## 7. Verification
- [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。
- [ ] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。
- [ ] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。

View File

@@ -0,0 +1,92 @@
## Context
AUG26-017 给代理预存款带来两类变化:在线自充的收款方式从「由支付配置自动决定」改为「超管配置允许范围」;线下预存款审批从「金额 + 支付凭证」补齐为「收款方式 + 交易流水号 + 其他凭证」。后端接口边界已经确认:
| 用途 | 接口 |
|---|---|
| 在线可用方式 | `GET /api/admin/agent-self-recharge-payment-methods`,代理/平台可用,超管 403旧接口 `GET /api/admin/agent-recharges/payment-methods` 保留但前端不再调用 |
| 允许范围读取 | `GET /api/admin/system-configs``module``page``page_size`,返回 `list` / `page` / `page_size` / `total` |
| 允许范围写入 | `PUT /api/admin/system-configs/{key}`,请求体 `{ key, value }``value` 为字符串化配置值 |
| 交易流水号识别 | `POST /api/admin/agent-recharges/payment-voucher-ocr`,请求 `{ payment_voucher_key }`,响应 `{ external_transaction_no }` |
| 线下创建 | `POST /api/admin/agent-recharges``payment_method=offline` 时新增 `offline_payment_method_id``external_transaction_no``other_voucher_key` |
| 列表与详情 | `GET /api/admin/agent-recharges``GET /api/admin/agent-recharges/{id}` 新增 5 个响应字段 |
## Goals / Non-Goals
**Goals**
- 在线充值只展示后端返回的实际可用方式,并优雅处理空列表。
- 超管可在既有系统配置页面维护允许范围,无需新增接口或页面。
- 线下预存款申请可提交收款方式、交易流水号与其他凭证,并支持 OCR 预填流水号。
- 列表与详情正确展示两类交易号与收款方式快照。
**Non-Goals**
- 不新增「代理自充设置」独立页面与专用配置接口。
- 不实现允许范围的审计查询接口(后端记录操作者、前后值与时间,前端不查询)。
- 不实现后端接口、商户池交集计算、企微审批回调。
- 不实现 H5/C 端在线充值,不为企业账号做分支。
## Decisions
### 允许范围沿用受控系统配置
允许范围配置项的读取与写入统一走系统配置:读取 `GET /api/admin/system-configs`,写入 `PUT /api/admin/system-configs/{key}`。系统配置页已是元数据驱动的通用渲染器(`value_type` 决定控件、`enum_values` 决定枚举选项、`readonly``sensitive` 决定保护策略),因此后端的允许范围配置项注册后即可在页面中展示与编辑。
前端 MUST NOT 硬编码该配置项的 `config_key`,也 MUST NOT 新增专用设置写接口。该配置项归属 `c2b.payment` 模块,属于现有 `SystemConfigModule` 取值,无需扩展模块枚举。
允许范围的枚举取值为 `wechat_only`(仅微信支付)、`alipay_only`(仅支付宝支付)、`both`(同时支持微信与支付宝);系统配置页展示与选择时使用中文标签,未命中映射时回退展示原值。
超管入口使用一个「代理自充设置」菜单项,跳转到系统配置页面并带 `module=c2b.payment` 过滤条件;系统配置页面除了既有的 `config_key` query还需支持从 `route.query.module` 初始化模块筛选,保证跳转后列表已按模块收敛。
### 在线可用方式
| 项 | 取值 |
|---|---|
| 接口 | `GET /api/admin/agent-self-recharge-payment-methods` |
| 响应 | `{ methods: (wechat \| alipay)[], min_amount, max_amount }` |
| 空列表 | 只提示「当前暂无可用的在线支付方式」,禁用提交,不解释被限制还是无可用商户 |
| 超管 | 该接口对超管返回 403前端在超管视角不请求它仅通过系统配置查看允许范围 |
| 存量单 | 配置变更不影响已创建的待支付单,前端不在配置变更后刷新待支付单 |
金额上下限优先使用接口返回的 `min_amount` / `max_amount`(单位分),接口未返回时回退既有常量。
### 线下预存款创建字段
| 字段 | 必填 | 说明 |
|---|---|---|
| `offline_payment_method_id` | 是 | 取自 `GET /api/admin/employee-collection-payment-methods` 的启用项(`enabled=true``page_size=100` |
| `external_transaction_no` | 是 | 交易流水号OCR 预填后人工确认,可编辑;前端不做重复校验 |
| `other_voucher_key` | 否 | 其他凭证对象键数组,最多 5 个 |
| `payment_voucher_key` | 是 | 支付凭证,至少 1 个,与其他凭证分开提交 |
历史线下单的收款方式只用 `offline_payment_method_code` / `offline_payment_method_name` 快照展示,不用 `offline_payment_method_id` 反查字典当前值。
列表筛选保持后端已有参数集合(`page``page_size``shop_id``status``recharge_source``start_date``end_date`),不新增交易流水号筛选,交易流水号只做展示。
### OCR 预填交互
- 入口:线下代充弹窗支付凭证区的「识别凭证」按钮,取已上传的第一个 `payment_voucher_key`;未上传凭证时禁用。
- 请求:`POST /api/admin/agent-recharges/payment-voucher-ocr`;通过 `BaseService.post` 的第三个参数传 `{ timeout: 30000 }`,请求期间按钮与交易流水号字段展示 loading 并防重复点击。
- 成功:把 `external_transaction_no` 写入交易流水号输入框,字段保持可编辑并提示对照凭证核对。
- 失败(凭证不是图片、对象不存在、识别服务异常):只提示,不清空已填内容、不阻断手工填写与提交。
- 只预填交易流水号,金额、付款人、付款时间、备注一律不预填。
### 凭证上传类型
取上传地址时必须显式声明 `content_type``image/jpeg`,否则 OCR 会以「不是图片」直接拒绝。`VoucherUpload` 新增可选 `contentType` prop代理充值的支付凭证与其他凭证固定传 `image/jpeg`,并把它透传给 `StorageService.getUploadUrl``StorageService.uploadFile`
### 交易流水号展示
`payment_transaction_id` 是在线渠道返回的权威交易号,只有在线单有值;`external_transaction_no` 是线下人工申报的交易流水号。两者独立展示、互不覆盖:在线单只展示前者,线下单只展示后者。
### 详情页保持只读
代理充值详情页只做信息展示,顶部仅保留返回导航;本次新增的交易流水号、收款方式与其他凭证都只读呈现,不引入任何业务操作按钮。创建、确认线下充值、驳回等操作入口仍留在列表页操作列。
## Risks / Trade-offs
- **配置 Key 由后端注册决定**:前端不硬编码,配置项按 `c2b.payment` 模块渲染;若后端最终调整模块归属,只需同步调整菜单跳转的 `module` 参数。
- **OCR 端到端约 15 至 16 秒**:必须配置不低于 30 秒的超时并展示 loading否则用户容易重复点击。
- **强制声明 `image/jpeg`**:按需求文档要求统一声明为 `image/jpeg`,上传非 JPEG 图片时以声明类型为准。
- **收款方式字典项被引用后会冻结**:展示历史单依赖快照字段,避免字典改名或停用导致历史数据展示漂移。

View File

@@ -0,0 +1,36 @@
# Change: 代理自充收款方式配置与线下预存款审批字段
## Why
8 月迭代 AUG26-017对应 PRD-008-021 与 PRD-008-013包含两项要求代理在线自充的收款方式由超级管理员维护「允许范围」仅微信 / 仅支付宝 / 同时支持),代理实际可用方式取允许范围与当前可用商户方式的交集,交集为空时不允许创建在线充值单;线下预存款审批需要补齐「收款方式」「交易流水号」「其他凭证」,其中交易流水号支持从付款凭证 OCR 预填。
后台管理端现状与该要求有差距:在线充值直接调用旧接口 `GET /api/admin/agent-recharges/payment-methods`,线下代充表单只有店铺、支付凭证与运营备注,充值列表与详情也没有交易流水号、线下收款方式快照与其他凭证字段。
本次按后端实际 OpenAPI 契约对齐:可用方式改读 `GET /api/admin/agent-self-recharge-payment-methods`;允许范围的读取与写入沿用受控系统配置 `GET /api/admin/system-configs``PUT /api/admin/system-configs/{key}`;交易流水号识别使用 `POST /api/admin/agent-recharges/payment-voucher-ocr`
## What Changes
- 在线充值可用支付方式改由 `GET /api/admin/agent-self-recharge-payment-methods` 提供,读取 `methods``min_amount``max_amount`;旧接口 `GET /api/admin/agent-recharges/payment-methods` 保留但前端不再调用。
- `methods` 为空时只提示「当前暂无可用的在线支付方式」并禁用提交,不解释是被允许范围限制还是无可用商户。
- 代理自充允许范围不新增专用接口与专用页面:配置项归属 `c2b.payment` 模块,超管沿用「系统配置」页面,通过 `GET /api/admin/system-configs` 读取、`PUT /api/admin/system-configs/{key}` 提交字符串化 `value`;前端不硬编码配置 Key允许范围对代理与平台账号不可见。
- 新增「代理自充设置」菜单(仅超级管理员),跳转到系统配置页面并带 `module=c2b.payment` 过滤条件;系统配置页面支持从路由 query 初始化模块筛选。
- 线下预存款创建弹窗新增:收款方式(数据源 `GET /api/admin/employee-collection-payment-methods` 的启用项,提交 `offline_payment_method_id`)、交易流水号(必填、可编辑)、其他凭证(可选,最多 5 个 `other_voucher_key`);支付凭证 `payment_voucher_key` 仍必填且至少 1 个,与其他凭证分开提交。
- 新增付款凭证 OCR 预填:`POST /api/admin/agent-recharges/payment-voucher-ocr` 请求 `{ payment_voucher_key }`、响应只有 `{ external_transaction_no }`;结果只作预填且始终可编辑,识别失败不阻断人工填写与提交,请求超时不低于 30 秒并展示 loading。
- 充值列表与详情展示新增字段:`external_transaction_no``offline_payment_method_id``offline_payment_method_code``offline_payment_method_name``other_voucher_key`;线下单收款方式使用编码/名称快照展示,不用 `id` 反查字典当前值。
- 代理充值列表不新增交易流水号筛选:`GET /api/admin/agent-recharges` 未提供该查询参数,交易流水号仅做展示。
- 付款凭证与其他凭证获取上传地址时显式声明 `content_type``image/jpeg`
- 交易流水号 `external_transaction_no` 与在线渠道 `payment_transaction_id` 独立展示、互不覆盖;前端不做交易流水号重复校验。
- 不实现后端接口、商户池逻辑、企微审批回调与 H5/C 端页面。
## Impact
- Affected specs: `agent-recharge``system-config-management`
- Affected code:
- `src/types/api/agentRecharge.ts``src/api/modules/agentRecharge.ts``src/types/api/index.ts`
- `src/views/finance/agent-recharge/index.vue``src/views/finance/agent-recharge/detail.vue`
- `src/views/settings/system-configs/index.vue``src/types/api/systemConfig.ts`
- `src/router/routesAlias.ts``src/router/routes/asyncRoutes.ts`
- `src/components/business/VoucherUpload.vue`
- `src/locales/langs/{zh,en}.json`
- Dependencies: `docs/产品迭代8月份/代理.md`
- Out of scope: 代理自充设置独立页面、允许范围审计查询接口、企业账号分支、H5/C 端在线充值。

View File

@@ -0,0 +1,132 @@
## ADDED Requirements
### Requirement: 代理在线自充可用收款方式
前端 SHALL 通过 `GET /api/admin/agent-self-recharge-payment-methods` 获取代理在线自充的可用支付方式与金额范围,并在该接口返回空 `methods` 时禁止创建在线充值单。
#### Scenario: 读取可用支付方式
- **WHEN** 平台账号或代理账号进入代理充值页面并打开在线充值弹窗
- **THEN** 前端 MUST 调用 `GET /api/admin/agent-self-recharge-payment-methods`
- **AND** 前端 MUST 使用响应 `methods` 渲染可选的微信与支付宝方式
- **AND** 前端 MUST 使用响应 `min_amount``max_amount` 作为金额上下限
- **AND** 前端 MUST NOT 再调用 `GET /api/admin/agent-recharges/payment-methods`
#### Scenario: 可用方式为空
- **GIVEN** `GET /api/admin/agent-self-recharge-payment-methods` 返回空 `methods`
- **WHEN** 用户打开在线充值弹窗
- **THEN** 前端 MUST 提示「当前暂无可用的在线支付方式」
- **AND** 前端 MUST 禁用在线充值提交
- **AND** 前端 MUST NOT 解释为空的原因是被允许范围限制还是无可用商户
- **AND** 前端 MUST NOT 因空列表报错或阻断页面其余功能
#### Scenario: 超级管理员不通过该接口读取允许范围
- **WHEN** 当前登录账号为超级管理员
- **THEN** 前端 MUST NOT 为读取允许范围调用 `GET /api/admin/agent-self-recharge-payment-methods`
- **AND** 前端 MUST 统一处理该接口对超级管理员返回的 403
#### Scenario: 允许范围变更不影响存量待支付单
- **WHEN** 允许范围配置发生变更
- **THEN** 前端 MUST NOT 刷新或改写已创建待支付充值单的支付方式与商户信息
### Requirement: 线下预存款申请字段
线下预存款创建请求 SHALL 在 `payment_method``offline` 时提交 `offline_payment_method_id``external_transaction_no` 与可选的 `other_voucher_key`,并与既有的 `payment_voucher_key` 分开提交。
#### Scenario: 提交线下预存款申请
- **GIVEN** 用户以平台账号或超级管理员身份打开线下代充弹窗
- **WHEN** 用户提交申请
- **THEN** 请求体 MUST 包含 `amount``payment_method``offline``shop_id`
- **AND** MUST 包含必填 `offline_payment_method_id`,取值来自 `GET /api/admin/employee-collection-payment-methods` 的启用项
- **AND** MUST 包含必填 `external_transaction_no`
- **AND** MUST 包含至少 1 个 `payment_voucher_key`
- **AND** MAY 包含最多 5 个 `other_voucher_key`
#### Scenario: 支付凭证与其他凭证分开提交
- **WHEN** 用户上传支付凭证与其他凭证
- **THEN** 支付凭证 MUST 通过 `payment_voucher_key` 提交
- **AND** 其他凭证 MUST 通过 `other_voucher_key` 提交
- **AND** 前端 MUST NOT 将两类凭证合并到同一字段
#### Scenario: 交易流水号与在线交易号互不覆盖
- **WHEN** 前端提交或展示线下预存款申请
- **THEN** `external_transaction_no` MUST 只作为线下人工申报的交易流水号
- **AND** 前端 MUST NOT 用 `payment_transaction_id` 覆盖或替代 `external_transaction_no`
- **AND** 前端 MUST NOT 对 `external_transaction_no` 做重复性校验
### Requirement: 付款凭证交易流水号识别预填
前端 SHALL 通过 `POST /api/admin/agent-recharges/payment-voucher-ocr` 以已上传的付款凭证对象键换取交易流水号预填值,识别失败 MUST NOT 阻断人工填写与提交。
#### Scenario: 识别成功预填交易流水号
- **GIVEN** 线下代充弹窗已上传至少 1 个付款凭证
- **WHEN** 用户点击识别凭证
- **THEN** 前端 MUST 调用 `POST /api/admin/agent-recharges/payment-voucher-ocr`,请求体为 `{ payment_voucher_key }`
- **AND** 前端 MUST 将响应 `external_transaction_no` 写入交易流水号输入框
- **AND** 交易流水号输入框 MUST 保持可编辑并提示用户对照凭证核对
#### Scenario: 识别失败不阻断提交
- **GIVEN** OCR 返回失败,包括凭证不是图片、对象不存在或识别服务异常
- **WHEN** 用户点击识别凭证
- **THEN** 前端 MUST 展示失败提示
- **AND** 前端 MUST NOT 阻断用户手动填写交易流水号并提交
- **AND** 前端 MUST NOT 清空用户已填写内容
#### Scenario: 识别请求展示加载态并使用足够超时
- **WHEN** 前端发起 OCR 请求
- **THEN** 前端 MUST 在请求期间展示 loading 并防止重复提交
- **AND** 请求超时 MUST NOT 低于 30 秒
#### Scenario: 只预填交易流水号
- **WHEN** OCR 识别成功
- **THEN** 前端 MUST 只预填 `external_transaction_no`
- **AND** 前端 MUST NOT 预填金额、付款人、付款时间或备注
### Requirement: 代理充值交易流水号与收款方式展示
代理充值列表与详情 SHALL 展示线下预存款的 `external_transaction_no` 与收款方式快照,在线充值只展示 `payment_transaction_id`
#### Scenario: 列表展示线下补充字段
- **WHEN** 列表返回 `payment_method``offline` 的充值单
- **THEN** 列表 MUST 展示 `external_transaction_no`
- **AND** 列表 MUST 使用 `offline_payment_method_name` 展示收款方式
#### Scenario: 详情展示收款方式快照
- **WHEN** 详情返回历史线下充值单
- **THEN** 前端 MUST 使用 `offline_payment_method_code``offline_payment_method_name` 展示收款方式
- **AND** 前端 MUST NOT 用 `offline_payment_method_id` 反查字典当前值
- **AND** 前端 MUST 展示 `other_voucher_key` 对应的其他凭证
#### Scenario: 两类交易号独立展示
- **WHEN** 详情展示在线充值单
- **THEN** 前端 MUST 只展示 `payment_transaction_id`
- **AND** 前端 MUST NOT 把 `external_transaction_no` 当作在线渠道交易号展示
#### Scenario: 列表不新增交易流水号筛选
- **WHEN** 前端实现代理充值列表筛选
- **THEN** 前端 MUST 只使用 `page``page_size``shop_id``status``recharge_source``start_date``end_date`
- **AND** 前端 MUST NOT 新增交易流水号筛选条件
### Requirement: 付款凭证上传类型声明
代理充值付款凭证与其他凭证在获取上传地址时 SHALL 显式声明 `content_type``image/jpeg`
#### Scenario: 上传地址声明图片类型
- **WHEN** 前端为代理充值线下凭证请求上传地址
- **THEN** 请求 MUST 携带 `content_type``image/jpeg`
- **AND** 上传请求 MUST 使用相同的内容类型

View File

@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: 代理自充允许范围配置项
超级管理员维护的代理自充允许范围 SHALL 作为受控系统配置项,通过既有系统配置页面读取与更新,前端 MUST NOT 新增专用接口或专用页面。
#### Scenario: 通过系统配置读取允许范围
- **WHEN** 超级管理员进入系统配置页面
- **THEN** 前端 MUST 调用 `GET /api/admin/system-configs` 获取配置列表
- **AND** 后端注册的代理自充允许范围配置项 MUST 依据返回元数据渲染
- **AND** 前端 MUST NOT 为读取允许范围调用专用接口
#### Scenario: 以枚举控件编辑允许范围
- **GIVEN** 允许范围配置项返回非空 `enum_values`
- **WHEN** 超级管理员编辑该项
- **THEN** 页面 MUST 使用枚举选择控件渲染允许范围
- **AND** 页面 MUST 以字符串化 `value` 调用 `PUT /api/admin/system-configs/{key}`
- **AND** 页面 MUST 在提交前校验取值属于 `enum_values`
- **AND** 允许范围枚举值 MUST 以中文标签展示,包括仅微信支付、仅支付宝支付与同时支持微信与支付宝
#### Scenario: 不硬编码配置 Key
- **WHEN** 前端实现允许范围配置项的展示与编辑
- **THEN** 前端 MUST NOT 硬编码该配置项的 `config_key`
- **AND** 前端 MUST 依据 `config_key``module``value_type``control``enum_values` 等元数据驱动渲染
- **AND** 该配置项归属 `c2b.payment` 模块,前端 MUST NOT 扩展模块枚举取值
#### Scenario: 代理自充设置菜单带入模块筛选
- **GIVEN** 当前登录账号为超级管理员
- **WHEN** 用户点击「代理自充设置」菜单
- **THEN** 前端 MUST 跳转到系统配置页面并携带 `module=c2b.payment`
- **AND** 系统配置页面 MUST 使用该 query 初始化模块筛选并据此查询配置列表
- **AND** 非超级管理员 MUST NOT 看到该菜单
#### Scenario: 允许范围对代理与平台不可见
- **WHEN** 当前登录账号不是超级管理员
- **THEN** 前端 MUST NOT 向代理与平台账号展示允许范围配置的入口或当前值
- **AND** 代理与平台账号 MUST 只能通过 `GET /api/admin/agent-self-recharge-payment-methods` 获取实际可用方式

View File

@@ -0,0 +1,55 @@
## 1. Contract and API Types
- [x] 1.1 `src/types/api/agentRecharge.ts``AgentRecharge` 新增 `external_transaction_no``offline_payment_method_id``offline_payment_method_code``offline_payment_method_name``other_voucher_key``string[] | null`),保持 `payment_transaction_id``payment_voucher_key` 语义不变。
- [x] 1.2 扩展 `CreateAgentRechargeOfflineRequest`:新增必填 `offline_payment_method_id`、必填 `external_transaction_no`、可选 `other_voucher_key`(最多 5 个)。
- [x] 1.3 新增 `AgentRechargePaymentVoucherOcrRequest { payment_voucher_key }``AgentRechargePaymentVoucherOcrResponse { external_transaction_no }`
- [x] 1.4 在 `src/types/api/index.ts` 导出新增类型。
## 2. Agent Recharge Service
- [x] 2.1 `src/api/modules/agentRecharge.ts` 新增 `getSelfRechargePaymentMethods()`,请求 `GET /api/admin/agent-self-recharge-payment-methods`
- [x] 2.2 新增 `recognizePaymentVoucher(data)`,请求 `POST /api/admin/agent-recharges/payment-voucher-ocr`,超时不低于 30000 毫秒。
- [x] 2.3 保留旧 `getPaymentMethods()` 方法,但页面不再调用。
## 3. Online Recharge Available Methods
- [x] 3.1 在线充值弹窗改用 `getSelfRechargePaymentMethods()` 读取 `methods` / `min_amount` / `max_amount`,并把 `wechat``alipay` 映射为微信、支付宝选项。
- [x] 3.2 `methods` 为空时提示「当前暂无可用的在线支付方式」并禁用提交,不解释原因。
- [x] 3.3 超管视角不请求该接口403 兜底);允许范围变更后不刷新已创建的待支付单。
## 4. Super Admin Allow-Range Config
- [x] 4.1 允许范围沿用系统配置页通用渲染:`GET /api/admin/system-configs` 读取、`PUT /api/admin/system-configs/{key}` 提交字符串化 `value`;不新增专用接口与专用页面。
- [x] 4.2 `wechat_only` / `alipay_only` / `both` 在系统配置页展示与选择时显示中文标签,未命中映射时回退原值。
- [x] 4.3 新增「代理自充设置」菜单(仅超级管理员),跳转到系统配置页面并带 `module=c2b.payment`;系统配置页支持从 `route.query.module` 初始化模块筛选。
- [x] 4.4 在 `zh.json``en.json``menus.settings` 下补充 `agentSelfRecharge` 中英文文案,并清理系统配置页预存的空样式块。
## 5. Offline Pre-deposit Create Dialog
- [x] 5.1 新增「收款方式」下拉,数据来自 `GET /api/admin/employee-collection-payment-methods``enabled=true``page_size=100`),必填,提交 `offline_payment_method_id`
- [x] 5.2 新增「交易流水号」输入(必填、可编辑),并提示对照凭证核对。
- [x] 5.3 新增「其他凭证」上传(可选,最多 5 个),提交 `other_voucher_key`;支付凭证仍必填且至少 1 个,两者分开提交。
- [x] 5.4 `VoucherUpload` 新增可选 `contentType` prop 并透传给 `getUploadUrl``uploadFile`;代理充值线下凭证固定传 `image/jpeg`
- [x] 5.5 线下提交体按契约组装:`{ amount, payment_method: offline, shop_id, offline_payment_method_id, external_transaction_no, payment_voucher_key, other_voucher_key, remark }`,未填写的可选字段不提交。
## 6. Voucher OCR Prefill
- [x] 6.1 支付凭证区新增「识别凭证」按钮,取第一个已上传凭证 Key未上传凭证时禁用。
- [x] 6.2 识别期间展示 loading按钮与交易流水号字段并防重复点击超时不低于 30 秒。
- [x] 6.3 识别成功写入 `external_transaction_no`,字段保持可编辑;失败只提示,不清空已填内容、不阻断提交。
- [x] 6.4 不预填金额、付款人、付款时间与备注。
## 7. List and Detail Display
- [x] 7.1 列表新增线下交易流水号与线下收款方式(`offline_payment_method_name` 快照)展示,仅线下单展示。
- [x] 7.2 详情新增交易流水号、收款方式(`offline_payment_method_code``offline_payment_method_name` 快照)与其他凭证预览;在线单只展示 `payment_transaction_id`
- [x] 7.3 不使用 `offline_payment_method_id` 反查字典当前值,也不做交易流水号重复校验。
- [x] 7.4 列表不新增交易流水号筛选,保持后端已有查询参数集合不变。
## 8. Verification
- [ ] 8.1 校验 `methods` 为空、超管调用新接口 403、允许范围变更不影响存量待支付单三种情况。
- [ ] 8.2 校验 OCR 成功、失败、超时三条路径均不阻断人工填写与提交。
- [ ] 8.3 校验线下单与在线单的两类交易号、收款方式快照与其他凭证展示正确。
- [x] 8.4 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`
- [x] 8.5 运行 `openspec validate update-agent-self-recharge-and-offline-approval --strict`

View File

@@ -0,0 +1,54 @@
# 商户池详情与商户凭证契约设计
## Context
支付商户与商户池管理页已经上线,运营在使用时遇到三个问题:商户池详情入口和支付商户不一致且信息不完整、新增商户池直接报错、商户凭证缺少字段枚举与类型校验。本次优化只新增详情页、收敛成员顺序数据源,并在既有凭证写入表单上叠加契约校验,不改变接口路径与请求结构。
## Goals
- 让商户池详情的入口和展示完整度与支付商户详情对齐。
- 让新增/编辑商户池恢复可用,并且拖拽排序结果仍按顺序提交。
- 让前端提交的商户凭证满足后端的字段枚举、必填键、值类型与商户标识约束。
## Non-Goals
- 不改变“更换凭证”表单的交互方式与凭证值不回显约束。
- 不新增后端接口。
## Decisions
### 商户池详情复用支付商户详情的入口模式
支付商户详情已采用“列表点击名称 → 独立详情页”的模式,因此商户池改为同样方式:列表名称列渲染为可点击文本,点击后跳转 `/settings/payment-merchant-pools/pool-detail/:id`,并移除行操作里的“详情”抽屉。
详情页复用 `src/components/common/DetailPage.vue`,按“基本信息”“轮询配置”“运行状态”三组展示字段。创建时间、更新时间只在接口返回时渲染,避免出现空白字段行。
替代方案是保留抽屉并补齐字段;但两个详情入口不一致会让运营难以形成稳定预期,因此不采用。
### 成员商户名称由前端解析
详情接口只返回 `member_ids`。详情页在加载详情后按该商户池的 `payment_method` 拉取支付商户列表,把成员 ID 映射为商户名称展示,同时保留成员数量。解析不到名称时展示“未知商户”,不展示 `member_ids` 原始 ID避免把内部标识暴露给运营。
### 成员顺序使用单一数据源
原实现同时监听 `form.member_ids``orderedMemberIds`,并在两个回调中互相赋新数组,形成“赋值 → 触发 → 再赋值”的无限循环,从而在新增商户池时触发 `Maximum recursive updates exceeded`
修复方式是把成员顺序收敛为单一数据源:`orderedMemberIds` 改为基于 `form.member_ids``computed`get 返回成员数组set 写回成员数组)。`VueDraggable` 通过 `v-model` 触发 setter 写回 `form.member_ids`,不再存在互相触发的 watch。
### 凭证字段枚举固化在类型模块并在提交前校验
凭证必填键由后端契约按 `payment_method``provider_type` 组合给出,前端与该契约保持一致,因此把枚举定义在 `src/types/api/paymentMerchantPools.ts``PAYMENT_CREDENTIAL_FIELD_SPECS` 描述每个 `provider_type` 的必填键与可选键,`PAYMENT_CREDENTIAL_BOOLEAN_KEYS``PAYMENT_CREDENTIAL_INTEGER_KEYS` 描述 `ali_production``ali_pay_expire_minutes` 的值类型,`PAYMENT_MERCHANT_IDENTITY_KEYS` 描述商户标识必须一致的凭证字段。枚举之外的字段名一律拦截,避免提交必然被后端拒绝的请求。
`buildPaymentCredentials` 负责提交前校验并转换:拒绝不属于当前服务商的字段名、补齐必填键检查、把布尔字段与整数字段从输入框字符串转换为布尔值/数字、校验 `merchant_identity` 与对应凭证字段一致。表单继续使用“字段名 + 字段值”的通用编辑方式,只在分隔线下方展示当前服务商的必填/可选字段提示。
替代方案是把凭证表单改成按服务商渲染固定中文标签字段;该方案会改变既有交互与凭证只写约定,本次不采用。
### 凭证仅平台账号可读写
凭证读取与写入入口复用既有平台账号限制:`/settings/payment-merchant-pools` 及其详情路由都带 `allowedUserTypes: [1, 2]`,页面内 `canManage` 再按 `isPlatformAccount` 过滤操作。凭证值只在当前编辑会话内存中存在,提交、取消或关闭后清空,不进入 Pinia 持久化、浏览器存储、日志或错误上报。
## Risks and Trade-offs
- 详情页与支付商户详情一样受 `allowedUserTypes: [1, 2]` 限制,代理与企业账号无法访问。
- 商户池成员名称依赖支付商户列表接口,成员数量超过单页上限时可能解析不到名称,此时展示“未知商户”而不是原始 ID。
- 凭证字段枚举与后端校验规则必须保持一致;后端新增字段时前端需要同步更新枚举,否则提交会被前端拦截。

View File

@@ -0,0 +1,48 @@
# Change: 商户池详情与商户凭证契约
## Why
`docs/产品迭代8月份/支付商户优化.md` 记录了支付商户与商户池管理页的三个问题,加上运营补充的凭证契约与权限要求,本次处理三件事:
- 商户池详情与支付商户详情的入口不一致:支付商户是点击名称进入独立详情页,商户池却是行操作里的详情抽屉,且详情没有展示接口已返回的全部字段(例如 `time_period_started_at`),成员只给出数量。
- 点击“新增商户池”时页面抛出 `Maximum recursive updates exceeded in component <PaymentMerchantPoolManagement>`,新增和编辑商户池流程完全不可用。
- 商户凭证的字段契约此前只存在于接口文档:必填键由 `payment_method``provider_type` 组合决定,前端提交前没有字段枚举与类型校验,容易出现漏填必填键、`ali_production` / `ali_pay_expire_minutes` 以字符串提交、`merchant_identity` 与凭证字段不一致等会被后端拒绝的请求。
“更换凭证”表单的交互(手工逐行填写字段名与字段值、值不回显)按反馈保持原有行为,本次只在其上叠加字段契约校验。
## What Changes
- 新增商户池详情页,并改为点击列表中商户池名称进入,与支付商户详情保持一致;移除列表行操作中的“详情”入口。
- 商户池详情页展示详情接口返回的业务字段:商户池名称、支付方式、启停状态、成员数量、成员商户、轮询策略、统计周期、金额/笔数/时间阈值、时间起点、路由世代、备注,以及接口返回时的创建时间和更新时间。
- 成员商户按支付商户名称展示,不展示 `member_ids` 原始 ID名称无法解析时展示“未知商户”占位文案。
- 合并商户池表单中 `form.member_ids``orderedMemberIds` 的双向 `watch` 为单一数据源,消除递归更新错误,同时保持拖拽排序结果按顺序提交。
- 将商户凭证字段枚举(各 `payment_method` + `provider_type` 组合的必填键与可选键)固化到前端类型模块,并在提交前校验字段枚举、必填键、值类型(布尔/整数/字符串)与 `merchant_identity` 一致性。
- 凭证写入表单展示当前服务商组合的必填/可选字段提示,降低漏填必填键的概率。
- 明确凭证访问控制:仅超级管理员(`user_type=1`)与平台用户(`user_type=2`)可读写商户凭证,凭证内容不进入日志、审计与支付快照。
## Impact
- Affected specs: `payment-merchant-pool-management`
- Affected code:
- `src/views/settings/payment-merchant-pools/pool-detail.vue`(新增)
- `src/views/settings/payment-merchant-pools/components/PoolManagement.vue`
- `src/views/settings/payment-merchant-pools/components/MerchantManagement.vue`
- `src/types/api/paymentMerchantPools.ts`
- `src/router/routes/asyncRoutes.ts`
- `src/router/routesAlias.ts`
- `src/locales/langs/zh.json``src/locales/langs/en.json`
- Dependencies:
- 后端 `GET /api/admin/payment-merchant-pools/{id}` 返回 `member_ids``routing_epoch``time_period_started_at` 等字段。
- 后端按 `payment_method``provider_type` 组合校验凭证必填键与值类型。
- 成员商户名称由前端使用同 `payment_method` 的支付商户列表解析,不要求后端在商户池详情返回名称。
- Compatibility:
- 不改变“更换凭证”表单的交互方式与凭证值不回显约束。
- 不影响支付商户列表与支付商户详情页的信息结构。
- 不改变商户池创建、更新、启用、停用的请求结构。
## Non-Goals
- 不按服务商渲染带中文标签的固定凭证表单,仍保留“字段名 + 字段值”的通用编辑方式。
- 不新增商户池详情、凭证校验之外的后端接口。
- 不在商户池详情页展示 `id``member_ids` 等内部标识。
- 不改动商户池轮询策略、成员排序规则和阈值换算规则。

View File

@@ -0,0 +1,155 @@
## ADDED Requirements
### Requirement: 商户池详情入口
系统 SHALL 与支付商户保持一致,以点击列表中商户池名称的方式进入独立详情页,并且不再在行操作中提供“详情”入口。
#### Scenario: 点击商户池名称进入详情页
- **GIVEN** 管理员位于支付商户与商户池管理页的“商户池”页签
- **WHEN** 管理员点击列表中某个商户池名称
- **THEN** 系统 MUST 跳转到该商户池的详情页
- **AND** 详情页 MUST 通过 `GET /api/admin/payment-merchant-pools/{id}` 加载数据
- **AND** 列表行操作 MUST NOT 再提供“详情”操作
#### Scenario: 商户池名称以可点击样式展示
- **GIVEN** 商户池列表加载成功
- **WHEN** 页面渲染商户池名称列
- **THEN** 商户池名称 MUST 以可点击样式展示
- **AND** 点击名称 MUST NOT 触发表格行选中或其他行操作
#### Scenario: 非平台账号不能查看商户池详情
- **GIVEN** 当前登录账号的 `user_type` 不是 `1``2`
- **WHEN** 用户点击商户池名称或直接访问商户池详情页地址
- **THEN** 系统 MUST NOT 展示商户池详情数据
- **AND** 路由 MUST 按平台账号限制拦截该访问
### Requirement: 商户池详情信息完整
系统 SHALL 在商户池详情页展示详情接口返回的全部业务字段,并 SHALL 以支付商户名称展示成员商户,不得展示 `id``member_ids` 原始标识。
#### Scenario: 展示轮询策略与阈值字段
- **GIVEN** 管理员进入某个商户池详情页
- **WHEN** 详情数据加载成功
- **THEN** 详情 MUST 展示商户池名称、支付方式、启停状态、轮询策略、路由世代和备注
- **AND** `strategy``amount``count` 时 MUST 展示统计周期和对应阈值
- **AND** `strategy``time` 时 MUST 展示时间单位、时间长度和时间起点
#### Scenario: 成员商户按名称展示
- **GIVEN** 商户池详情返回 `member_ids`
- **WHEN** 页面展示成员信息
- **THEN** 页面 MUST 展示成员商户名称和成员数量
- **AND** 页面 MUST NOT 展示 `member_ids` 原始 ID
- **AND** 成员名称无法解析时 MUST 展示“未知商户”而不是 ID
#### Scenario: 未返回的字段不渲染
- **GIVEN** 商户池详情接口未返回创建时间或更新时间
- **WHEN** 页面展示商户池详情
- **THEN** 页面 MUST NOT 渲染对应字段的空白行
### Requirement: 商户池成员顺序单一数据源
系统 SHALL 使用单一数据源维护商户池成员顺序,保证新增、编辑和拖拽排序成员时不会出现递归更新错误,并且提交顺序与页面展示顺序一致。
#### Scenario: 新增商户池不再递归更新
- **WHEN** 管理员点击“新增商户池”
- **THEN** 系统 MUST NOT 抛出 `Maximum recursive updates exceeded`
- **AND** 表单 MUST 正常打开并允许选择成员
#### Scenario: 拖拽后按展示顺序提交
- **GIVEN** 商户池表单中存在多个成员商户
- **WHEN** 管理员拖拽调整成员顺序并提交
- **THEN** `member_ids` MUST 按拖拽后的顺序提交
- **AND** 再次编辑该商户池时 MUST 按提交顺序展示成员
### Requirement: 商户凭证字段枚举契约
系统 SHALL 按 `payment_method``provider_type` 组合枚举商户凭证的必填键与可选键,并在提交前校验字段枚举、必填键、值类型与商户标识一致性。
字段枚举以后端契约为准(键名与渠道配置字段一致),前端必须与后端保持一致,后端新增或调整字段时需同步更新前端枚举。`credentials` MUST 为扁平 JSON 对象,必填键为:
- `payment_method=wechat``provider_type=wechat``wx_mch_id``wx_api_v3_key``wx_cert_content``wx_key_content``wx_serial_no``wx_notify_url`
- `payment_method=wechat``provider_type=wechat_v2``wx_mch_id``wx_api_v2_key``wx_notify_url`
- `payment_method=wechat``provider_type=fuiou``fy_mchnt_cd``fy_ins_cd``fy_term_id``fy_private_key``fy_public_key``fy_api_url``fy_notify_url`
- `payment_method=alipay``provider_type=alipay``ali_app_id``ali_private_key``ali_public_key``ali_notify_url``ali_return_url`
可选键为:`wechat` 可附 `wx_api_v2_key``alipay` 可附 `ali_production`(布尔,是否生产环境)与 `ali_pay_expire_minutes`(整数,支付过期分钟数)。除 `ali_production``ali_pay_expire_minutes` 外,凭证值 MUST 为字符串。`merchant_identity` MUST 分别等于 `wx_mch_id``fy_mchnt_cd``ali_app_id`
#### Scenario: 提交微信直连凭证
- **GIVEN** 管理员提交 `payment_method=wechat``provider_type=wechat` 的商户凭证
- **THEN** 请求 MUST 包含 `wx_mch_id``wx_api_v3_key``wx_cert_content``wx_key_content``wx_serial_no``wx_notify_url`
- **AND** 请求 MAY 附带 `wx_api_v2_key`
- **AND** `merchant_identity` MUST 等于 `wx_mch_id`
#### Scenario: 提交微信直连 V2 凭证
- **GIVEN** 管理员提交 `payment_method=wechat``provider_type=wechat_v2` 的商户凭证
- **THEN** 请求 MUST 包含 `wx_mch_id``wx_api_v2_key``wx_notify_url`
- **AND** `merchant_identity` MUST 等于 `wx_mch_id`
#### Scenario: 提交富友凭证
- **GIVEN** 管理员提交 `payment_method=wechat``provider_type=fuiou` 的商户凭证
- **THEN** 请求 MUST 包含 `fy_mchnt_cd``fy_ins_cd``fy_term_id``fy_private_key``fy_public_key``fy_api_url``fy_notify_url`
- **AND** `merchant_identity` MUST 等于 `fy_mchnt_cd`
#### Scenario: 提交支付宝凭证
- **GIVEN** 管理员提交 `payment_method=alipay``provider_type=alipay` 的商户凭证
- **THEN** 请求 MUST 包含 `ali_app_id``ali_private_key``ali_public_key``ali_notify_url``ali_return_url`
- **AND** 请求 MAY 附带 `ali_production``ali_pay_expire_minutes`
- **AND** `merchant_identity` MUST 等于 `ali_app_id`
#### Scenario: 缺少必填字段时拒绝提交
- **WHEN** 提交的凭证缺少当前服务商组合的必填键
- **THEN** 前端 MUST 阻止提交并提示缺失的凭证字段名
#### Scenario: 拒绝不属于当前服务商的凭证字段
- **WHEN** 提交的凭证包含必填键与可选键之外的字段名
- **THEN** 前端 MUST 阻止提交并提示该字段不属于当前服务商支持的字段
#### Scenario: 校验凭证值类型
- **GIVEN** 凭证包含 `ali_production``ali_pay_expire_minutes`
- **WHEN** 管理员提交凭证
- **THEN** `ali_production` MUST 以布尔值提交
- **AND** `ali_pay_expire_minutes` MUST 以正整数提交
- **AND** 其余凭证字段 MUST 以字符串提交
#### Scenario: 校验商户标识一致性
- **WHEN** `merchant_identity``wx_mch_id``fy_mchnt_cd``ali_app_id` 的值不一致
- **THEN** 前端 MUST 阻止提交并提示商户标识必须与对应凭证字段一致
### Requirement: 商户凭证访问控制
系统 SHALL 仅允许超级管理员(`user_type=1`)与平台用户(`user_type=2`)读取和写入商户凭证,且凭证内容 MUST NOT 进入日志、审计记录或支付快照。
#### Scenario: 平台账号可读写凭证
- **GIVEN** 当前登录账号的 `user_type``1``2`
- **WHEN** 账号打开支付商户管理页、支付商户详情页或提交商户凭证
- **THEN** 系统 MUST 允许读取凭证配置状态与写入凭证
#### Scenario: 非平台账号不可读写凭证
- **GIVEN** 当前登录账号的 `user_type``3``4`
- **WHEN** 账号访问支付商户管理页或支付商户详情页
- **THEN** 系统 MUST 拒绝访问并 MUST NOT 返回凭证字段或凭证状态
- **AND** 页面 MUST NOT 渲染凭证写入入口
#### Scenario: 凭证不写入日志与快照
- **WHEN** 创建、更新或删除支付商户成功或失败
- **THEN** 凭证内容 MUST NOT 出现在浏览器存储、页面日志、错误上报或支付快照中
- **AND** 页面 MUST 只展示“已配置/未配置”状态与 `credential_version`

View File

@@ -0,0 +1,33 @@
# Implementation Tasks
## 1. 商户池详情入口
- [x] 1.1 新增商户池详情页 `pool-detail.vue`,复用 `DetailPage` 展示基本信息、轮询配置和运行状态。
- [x] 1.2 新增 `/settings/payment-merchant-pools/pool-detail/:id` 路由与路由别名,并按平台账号(`allowedUserTypes: [1, 2]`)限制访问。
- [x] 1.3 商户池列表名称列改为可点击文本,点击跳转对应详情页;移除行操作中的“详情”入口。
- [x] 1.4 补充中英文路由标题文案 `menus.settings.detailsOfPaymentMerchantPool`
## 2. 商户池详情内容
- [x] 2.1 详情页展示详情接口返回的全部业务字段:名称、支付方式、启停状态、成员数量、成员商户、轮询策略、统计周期、金额/笔数/时间阈值、时间起点、路由世代和备注。
- [x] 2.2 成员按支付商户名称展示,不展示 `member_ids` 原始 ID无法解析时展示“未知商户”。
- [x] 2.3 创建时间、更新时间等字段仅在接口返回时渲染,避免空白字段行。
## 3. 商户池表单稳定性
- [x] 3.1 将 `orderedMemberIds` 改为基于 `form.member_ids` 的单一数据源,移除互相赋值的双向 watch。
- [x] 3.2 验证新增、编辑、切换支付方式、添加成员、移除成员和拖拽排序后提交的成员顺序正确。
- 新增/编辑/切换支付方式直接重置 `form.member_ids`;添加与移除成员在 `form.member_ids` 上增删;拖拽经 `VueDraggable``v-model` 写回同一数组,`buildPayload` 按数组顺序提交。
## 4. 商户凭证字段枚举与校验
- [x] 4.1 在 `src/types/api/paymentMerchantPools.ts` 固化凭证字段枚举:`PAYMENT_CREDENTIAL_FIELD_SPECS`(各 `provider_type` 的必填键与可选键)、`PAYMENT_CREDENTIAL_BOOLEAN_KEYS``PAYMENT_CREDENTIAL_INTEGER_KEYS``PAYMENT_MERCHANT_IDENTITY_KEYS`
- [x] 4.2 实现 `buildPaymentCredentials`:校验字段枚举与必填键,把 `ali_production` 转为布尔值、`ali_pay_expire_minutes` 转为正整数,其余字段保持字符串,并校验 `merchant_identity` 与对应凭证字段一致。
- [x] 4.3 支付商户凭证写入表单接入该校验,并在“写入支付凭证”分隔线下方展示当前服务商组合的必填/可选字段提示。
- [x] 4.4 凭证读写仍仅限超级管理员(`user_type=1`)与平台用户(`user_type=2`):路由 `allowedUserTypes` 与页面 `canManage` 双层限制,凭证值不进入持久化、日志与错误上报。
## 5. 验证
- [x] 5.1 运行 `eslint``vue-tsc --noEmit``vite build`,确认改动通过类型检查和构建。
- `eslint`(改动文件)、`vue-tsc --noEmit` 均通过;`vite build --mode development` 成功产出 `MerchantManagement``PoolManagement``pool-detail` chunk。
- [x] 5.2 执行 `openspec validate update-payment-merchant-pool-optimization --strict`