# 批量付款到户有密 — 账号级改造设计(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, , '0'); INSERT INTO sys_menu (parent_id, title, permission, type, "order", status) VALUES (, '制单授权', 'module_payment:batch:authorize', 3, 1, '0'), (, '批量制单', 'module_payment:batch:create', 3, 2, '0'), (, '制单历史', 'module_payment:batch:list', 3, 3, '0'), (, '批次详情', '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) |