2026-08-26-alipay-batch-pay-account-level-design.md 16 KB

批量付款到户有密 — 账号级改造设计(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 动态路由),顶层菜单示例:/accountmodule_payment/account/index
  • 批量支付功能位于「资金专户转账页」account/index.vuebatch-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(授权记录即主体,租户级)

实体基类:PaymentEnterpriseBaseEntityPaymentTenantBaseEntity(移除 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(租户级)

实体基类同上改为 PaymentTenantBaseEntitypay_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 自动隔离。batchCreatedto.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:迁至 BatchSubjectServiceenterprise_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/batchcomponent_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 清单

-- 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 代码实证:paybatch* 三表不在拦截器豁免名单
D5 order 冗余 service_provider_id 主体解绑后批次操作仍可用 client
D6 服务拆分:BatchSubjectService(授权)+ AlipayBatchPayService(批次) AlipayBatchPayService 已 600+ 行
D7 制单历史主体筛选用 payer_uid order 已有该字段(= 主体 uid)