فهرست منبع

docs: 批量付款账号级改造设计 - 新增批量付款菜单、多授权主体、主体筛选、服务商下拉

alphaH 4 روز پیش
والد
کامیت
f56f3a0a8d
1فایلهای تغییر یافته به همراه257 افزوده شده و 0 حذف شده
  1. 257 0
      .claude/plan/2026-08-26-alipay-batch-pay-account-level-design.md

+ 257 - 0
.claude/plan/2026-08-26-alipay-batch-pay-account-level-design.md

@@ -0,0 +1,257 @@
+# 批量付款到户有密 — 账号级改造设计(2026-08-26)
+
+## 1. 背景与目标
+
+批量有密产品原为**企业级**架构:授权记录绑定企业(`pay_batch_authorize.enterprise_id`,一企业一协议,`uk_batch_authorize_active(enterprise_id, participant_id)` 唯一索引),制单付款方恒为企业自身身份,功能挂在「资金专户转账页」菜单的 tab 内。
+
+现调整为**账号级**架构,与企业管理功能隔离:
+
+1. 新增「批量付款」菜单,将原有批量支付功能迁移到新菜单
+2. 一个租户账号可维护多个授权主体(个人/企业支付宝账号均可签约,不限制签约次数,可一直签约新主体),需要表单
+3. 制单、制单历史可选择主体筛选
+4. 授权时可能获取不到 `pay_service_provider`,表单给出服务商下拉框选择(与新增企业的服务商下拉框一致)
+
+## 2. 授权接口实证(alipay.fund.authorize.uni.apply)
+
+来源:Playwright 抓取 https://opendocs.alipay.com/pre-open/fc872015_alipay.fund.authorize.uni.apply (2026-08-26,邀测站点「批量付款到户有密」产品)
+
+### 2.1 业务请求参数
+
+| 参数 | 必选 | 说明 |
+|---|---|---|
+| `product_code` | 必选 | 固定 `FUND_AUTHORIZATION` |
+| `biz_scene` | 必选 | 固定 `AUTHORIZE_FLOW` |
+| `out_biz_no` | 必选 | 授权申请外部业务号,幂等控制 |
+| `authorize_link_type` | 必选 | `SHORT_URL`(短链接)/ `TOKEN`(组件token) |
+| `back_url` | 必选 | 回跳地址(示例 `alipays://xxxxxxx`) |
+| `principal_info` | 必选 | AuthParticipantInfo 授权主体:`participant_id_type` + `participant_id` + `name`(示例 `ALIPAY_USER_ID` / `2088...` / 名称) |
+| `channel` | 可选 | `pc` / `tinyapp` |
+| `sub_biz_scene` | 可选 | 示例 `COMMON` |
+| `apply_expire_time` | 可选 | 跳转链接有效期,默认 7 天 |
+| `auth_expire_time` | 可选 | 授权失效时间,默认 2115-01-01 |
+| `back_url_type` | 可选 | `BACK_ALIPAY`(默认)/ `BACK_OUT_ALIPAY` |
+| `partner_info` / `third_party_info` | 可选 | AuthParticipantInfo |
+| `auth_biz_param` / `authorize_check_rule` | 可选 | 本次不使用 |
+
+**关键结论**:`principal_info` 主体可任意指定(个人/企业支付宝账号),`participant_id` 为 uid(`ALIPAY_USER_ID`)。支持多主体、多协议,即账号级多授权的基础。
+
+### 2.2 业务错误码(新增,影响授权逻辑)
+
+| 错误码 | 含义 | 处理 |
+|---|---|---|
+| `USER_AUTHORIZATION_EXIST` | 已存在授权协议,无需再次授权,请通过查询接口查询授权信息 | 同租户同主体已签约 → 前端提示「该账号已签约」,不重复发起(配合唯一索引预检) |
+| `EXISTS_STOPPED_AUTHORIZE` | 存在已失效的授权,请更换 outBizNo 后重试 | 主体解绑后重新授权必须换新 `out_biz_no`(现有 rebind 逻辑复用) |
+| `USER_NOT_EXIST` | 用户不存在,请检查申请账号合法性 | 表单 uid 非法提示 |
+| `UN_SUPPORT_AUTHORIZE_TERMINAL_TYPE` | 不支持的授权端 | channel 校验(本产品固定 pc) |
+
+## 3. 现状(代码实证)
+
+### 3.1 数据模型(java)
+
+- `BatchAuthorizeEntity extends PaymentEnterpriseBaseEntity`(表 `pay_batch_authorize`):`out_biz_no / participant_id / participant_id_type / agreement_no / status(AUTHING/AUTHED/UNBIND) / authorize_link / authorize_expire_time` + 基类 `tenant_id / enterprise_id / id / created_time`
+- `BatchOrderEntity extends PaymentEnterpriseBaseEntity`(表 `pay_batch_order`):`out_batch_no / batch_trans_id / total_amount / total_count / order_title / status(INIT/WAIT_PAY/SUCCESS/DISUSE/FAIL/INVALID) / payer_uid / agreement_no / transfer_scene_name / transfer_scene_report_infos / time_expire / remark / pay_url / error_code / error_msg`
+- `BatchDetailEntity extends PaymentEnterpriseBaseEntity`(表 `pay_batch_detail`):`batch_id / out_biz_no / amount / remark / payee_identity / payee_identity_type / payee_name / status / error_code / error_msg`
+- 唯一索引:`uk_batch_authorize_active(enterprise_id, participant_id)`
+
+### 3.2 租户隔离机制(无需改造,直接复用)
+
+`TenantInnerInterceptor`(MyBatis-Plus `TenantLineInnerInterceptor`)自动为 `pay_*` 表追加 `tenant_id` 条件:
+
+- 已登录非超管 → 自动按 `LoginUser.tenant_id` 过滤
+- 超管(`isSuperuser`)→ 跳过租户隔离,可看全部
+- 无认证(支付宝通知回调 / 定时任务)→ 跳过
+- insert 时自动填充 `tenant_id`
+
+`pay_batch_authorize / pay_batch_order / pay_batch_detail` 均不在 IGNORE_TABLES / CONDITIONAL_TABLES 名单 → 已登录即自动租户级隔离。**接口无需传租户参数。**
+
+### 3.3 制单与批次操作(AlipayBatchPayService)
+
+- `batchCreate(BatchCreateDTO)`:付款方从企业解析(`payerIdentity(ent)`:identity 优先回退 enterprise_id),协议号从该企业+付款方最新 AUTHED 授权记录自动带出,client 用 `getClient(enterpriseId, BIZ_TYPE)`
+- `renderPay / batchQuery / batchClose`:显式 `.eq(enterpriseId)` 条件;client 用 `getClient(enterpriseId, BIZ_TYPE)`
+- `batchList / batchDetail / batchExport / authorizeList`:enterprise_id 显式筛选
+- `getPendingBatches`:全表非终态(无认证,租户拦截器跳过)—— 不改
+- `BatchPayHandler`(通知):按 `out_biz_no` / `out_batch_no` 匹配,无 enterprise 依赖 —— 不改
+
+### 3.4 AlipayClientFactory 客户端解析链
+
+- `getClient(enterpriseId, bizType)`:企业 → `serviceProviderId` → ① profile(`pay_service_provider_profile` 业务专属凭证)→ ② 服务商默认凭证 → ③ yml fallback
+- `getClientByProvider(Long providerId)`:直接按服务商(默认凭证)
+- `getClientByProfile(spId, bizType)`:**private**,服务商+业务专属凭证
+
+改造需要新增 public 入口:`getClientByProvider(providerId, bizType)`(先 profile 后默认,与现有企业链路同构,仅少企业中间层)。
+
+### 3.5 前端(vue3 + Element Plus)
+
+- 菜单为动态菜单(`sys_menu` 表下发,`permission.store.ts` 动态路由),顶层菜单示例:`/account` → `module_payment/account/index`
+- 批量支付功能位于「资金专户转账页」`account/index.vue` 的 `batch-pay` tab:二级 tabs(制单授权 `BatchPayAuthorize` / 批次列表 `BatchPayList` / 创建批次 `BatchPayCreate`),详情 `BatchPayDetail` 条件渲染
+- API 层 `batch.ts`:`/payment/account/batch` 前缀,全部接口带 `enterprise_id`
+- 服务商下拉:`ProviderAPI.options()`(`EnterpriseForm.vue` 同款,返回 `{ id, scope_label }`)
+- 权限点:`module_payment:account:authorize / transfer / transfer:list / transfer:detail`
+
+## 4. 数据模型改造
+
+### 4.1 `pay_batch_authorize`(授权记录即主体,租户级)
+
+实体基类:`PaymentEnterpriseBaseEntity` → `PaymentTenantBaseEntity`(移除 `enterprise_id` 绑定)。
+
+| 字段 | 改动 |
+|---|---|
+| `participant_id` / `participant_id_type` | 保留(表单录入的支付宝 uid,`ALIPAY_USER_ID`) |
+| `participant_name` | **新增** —— 主体名称,表单必填,同时作为 `principal_info.name` |
+| `service_provider_id` | **新增** —— 表单服务商下拉 |
+| `out_biz_no / agreement_no / authorize_link / authorize_expire_time / status` | 保留,每条主体记录独立维护 |
+| 唯一索引 | `uk_batch_authorize_active(enterprise_id, participant_id)` → `(tenant_id, participant_id)`,防同租户重复签约(对应 `USER_AUTHORIZATION_EXIST`) |
+
+### 4.2 `pay_batch_order` / `pay_batch_detail`(租户级)
+
+实体基类同上改为 `PaymentTenantBaseEntity`。`pay_batch_order` 新增:
+
+| 字段 | 说明 |
+|---|---|
+| `service_provider_id` | 制单时冗余主体服务商 —— 主体解绑后批次仍可支付/查询/关闭(client 不依赖主体记录存活) |
+
+`payer_uid`(= 主体 uid)作为主体标识,制单历史按主体筛选直接用 `payer_uid`。
+
+### 4.3 存量数据
+
+不迁移。存量行保留(`enterprise_id` 非空旧行不再被新查询命中)。索引改为租户+主体唯一,避免与存量冲突。
+
+## 5. 后端设计
+
+### 5.1 服务拆分
+
+- **新建 `BatchSubjectService`**(主体/授权管理):`apply / rebind / query / list`
+  - `apply(participant_name, participant_id, service_provider_id)` → `principal_info.name` = 主体名称(新传字段)、`participant_id` = 表单 uid;client 用 `getClientByProvider(service_provider_id)`
+  - `USER_AUTHORIZATION_EXIST` → 提示「该账号已签约,请直接选择使用」(配合唯一索引预检)
+  - `EXISTS_STOPPED_AUTHORIZE` → rebind 换 `out_biz_no`(现有逻辑复用,每条主体记录独立维护)
+- **`AlipayBatchPayService` 保留批次操作并改造**:`create / renderPay / batchQuery / batchClose / list / detail / export / getPendingBatches`
+  - 文件已 600+ 行,授权逻辑(多主体表单、rebind、错误处理)拆出,避免继续膨胀
+
+### 5.2 租户隔离
+
+所有接口移除 `enterprise_id` 参数,依赖 `TenantInnerInterceptor` 自动隔离。`batchCreate` 中 `dto.getTenantId()` 恒空、由拦截器 insert 自动填充的模式不变。
+
+### 5.3 制单改造(`batchCreate`)
+
+- `BatchCreateDTO` 新增 `participant_id`(制单时从「已授权主体」列表选主体,付款方)
+- 付款方 = 主体 uid(不再解析企业身份);协议号 = 该主体授权记录 `agreement_no`(自动带出,规则不变)
+- client:`alipayClientFactory.getClient(providerId, BIZ_TYPE)`(新增 public 方法,见 3.4)
+- 落库:`order.service_provider_id` = 主体服务商;`order.payer_uid` = 主体 uid;`order.enterprise_id` 不再设置
+- 未完成授权的主体不可制单(本地预检:该主体需有 AUTHED 授权记录;`AUTH_INFO_NOT_EXISTS` 兜底保留)
+
+### 5.4 批次操作改造
+
+- `renderPay / batchQuery / batchClose`:显式 `.eq(enterpriseId)` 条件删除(依赖租户拦截器);client 用 `order.service_provider_id` + BIZ_TYPE
+- `list / detail / export`:enterprise_id 筛选删除;`list` 新增 `participant_id` 筛选(= `payer_uid`)
+- `authorizeList`:迁至 `BatchSubjectService`,`enterprise_id` 筛选删除,新增按租户自动隔离
+
+### 5.5 通知 / 定时
+
+- `BatchPayHandler`:不改(按 `out_biz_no` / `out_batch_no` 匹配)
+- `getPendingBatches`:不改(全表非终态)
+
+### 5.6 权限与路由
+
+- controller `@RequestMapping`:`/payment/account/batch` → `/payment/batch`
+- `@PreAuthorize` 换新权限:`module_payment:batch:authorize / create / list / detail`
+
+## 6. 前端设计
+
+### 6.1 新菜单
+
+`sys_menu` 顶层菜单「批量付款」:`route_path=/payment/batch`,`component_path=module_payment/batch/index`,权限点 `module_payment:batch:list`(及子权限点)。配套 SQL 见第 7 节。
+
+### 6.2 新页面 `module_payment/batch/index.vue`
+
+结构照搬现有「批量付款」tab:一级 tabs「制单授权 / 批量制单 / 制单历史」,详情条件渲染在底部。`account/index.vue` 移除 `batch-pay` tab 及其 import / handler(`BatchPayAuthorize/BatchPayList/BatchPayCreate/BatchPayDetail` 相关),其余转账功能不动。
+
+### 6.3 制单授权 tab(重写 BatchPayAuthorize → 列表+表单形态)
+
+- 授权列表表格:主体名称 / 支付宝uid / 服务商 / 状态 / 协议号 / 授权链接 / 操作
+- 「新增授权」按钮 → dialog 表单:**主体名称 + 支付宝uid + 服务商下拉**(`ProviderAPI.options()` 同新增企业页)
+- 行操作:生成/重新生成授权链接(二维码+复制)、刷新状态;AUTHED 行展示协议号、不提供重复生成
+
+### 6.4 批量制单 tab(改造 BatchPayCreate)
+
+顶部加「付款主体」下拉(已授权主体:`名称(uid)`,未授权不可选),明细录入不变。
+
+### 6.5 制单历史 tab(改造 BatchPayList)
+
+筛选区「企业」下拉 →「主体」下拉(按付款方 uid 筛选)+ 状态/时间筛选不变;平台超管可筛选任意主体。
+
+### 6.6 详情(BatchPayDetail)
+
+移除 `enterpriseId` prop 与相关请求参数,租户自动隔离。
+
+### 6.7 API 层 `batch.ts`
+
+- 路径:`/payment/account/batch` → `/payment/batch`
+- 全部方法移除 `enterprise_id` 参数
+- `authorizeApply / authorizeRebind` 参数改为 `participant_name / participant_id / service_provider_id`
+- `authorizeList` 支持主体筛选;`batchList` 新增 `participant_id` 筛选
+- `v-hasPerm` 同步换 `module_payment:batch:*`
+
+## 7. SQL 清单
+
+```sql
+-- 1. pay_batch_authorize 主体字段
+ALTER TABLE pay_batch_authorize
+    ADD COLUMN participant_name varchar(128),
+    ADD COLUMN service_provider_id bigint;
+
+-- 2. 唯一索引改租户级(先删旧)
+DROP INDEX IF EXISTS uk_batch_authorize_active;
+CREATE UNIQUE INDEX uk_batch_authorize_active
+    ON pay_batch_authorize (tenant_id, participant_id)
+    WHERE status <> 'UNBIND';  -- 解绑后可重新签约
+
+-- 3. pay_batch_order 冗余服务商
+ALTER TABLE pay_batch_order ADD COLUMN service_provider_id bigint;
+
+-- 4. 新菜单 + 权限点(sys_menu,parent_id 按实际顶层菜单排序取)
+INSERT INTO sys_menu (parent_id, title, route_name, route_path, component_path, permission, type, "order", status)
+VALUES (NULL, '批量付款', 'payment-batch', '/payment/batch', 'module_payment/batch/index', 'module_payment:batch:list', 2, <order>, '0');
+INSERT INTO sys_menu (parent_id, title, permission, type, "order", status)
+VALUES (<batchMenuId>, '制单授权', 'module_payment:batch:authorize', 3, 1, '0'),
+       (<batchMenuId>, '批量制单', 'module_payment:batch:create', 3, 2, '0'),
+       (<batchMenuId>, '制单历史', 'module_payment:batch:list', 3, 3, '0'),
+       (<batchMenuId>, '批次详情', 'module_payment:batch:detail', 3, 4, '0');
+
+-- 注:UNBIND 态部分唯一索引需先清存量重复行(联调期数据),执行前确认
+```
+
+> 说明:唯一索引过滤条件 `WHERE status <> 'UNBIND'` 为建议形态,实现时以存量数据核对为准(联调期数据可直接清理)。
+
+## 8. 测试策略
+
+后端(JUnit + Mockito,沿用 `AlipayBatchPayServiceTest` 模式):
+
+- `BatchSubjectService`:apply 传主体参数(principal_info.name/uid 断言);`USER_AUTHORIZATION_EXIST` 映射;rebind 换 out_biz_no;list 租户隔离
+- `batchCreate`:按主体制单(payer=主体 uid、agreement 带出、`order.service_provider_id` 落库);client 解析走 `getClientByProvider`;未授权主体预检拦截
+- `renderPay / batchQuery / batchClose`:不再依赖 enterprise 条件;client 用 `order.service_provider_id`
+- `AlipayClientFactory`:新增 `getClientByProvider(providerId, bizType)` 解析链(profile → 默认)
+- 存量 59 个测试的 enterprise 断言同步更新
+
+前端:
+
+- Playwright MCP 验证关键路径:新菜单可见 → 新增授权(表单+服务商下拉)→ 生成授权链接 → 制单选主体 → 创建批次 → 历史按主体筛选 → 详情 → 支付/关闭
+- `account/index.vue` 不再出现批量付款 tab
+
+## 9. 非目标
+
+- 不迁移存量企业级数据
+- 不改通知(`BatchPayHandler`)与定时同步(`getPendingBatches`)
+- 不开放 openapi(`TenantApiKeyAuthFilter` 场景的批量接口)—— 批量接口保持管理端认证
+- 不做主体解绑主动操作(支付宝侧解绑,本地被动同步 UNBIND)
+
+## 10. 决策记录
+
+| # | 决策 | 依据 |
+|---|---|---|
+| D1 | 授权记录即主体(扩展 pay_batch_authorize,不建主体表) | 用户选择;现有表复用,改动最小 |
+| D2 | 新菜单单页多 tab(制单授权/批量制单/制单历史) | 用户选择;与 account/index.vue 现有模式一致 |
+| D3 | 存量数据不迁移 | 用户选择;联调期测试数据 |
+| D4 | 租户隔离复用 TenantInnerInterceptor,接口去 enterprise_id | 代码实证:pay_batch_* 三表不在拦截器豁免名单 |
+| D5 | order 冗余 service_provider_id | 主体解绑后批次操作仍可用 client |
+| D6 | 服务拆分:BatchSubjectService(授权)+ AlipayBatchPayService(批次) | AlipayBatchPayService 已 600+ 行 |
+| D7 | 制单历史主体筛选用 payer_uid | order 已有该字段(= 主体 uid) |