|
|
@@ -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) |
|