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,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`