# AlipayTransferService: 企业码 → 安全发 API 迁移 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将账户转账体系的5个核心接口从旧的企业码 API (`alipay.commerce.ec.trans.*`) 迁移到安全发-服务商模式 API **Architecture:** 新流程以 `agreement_no` (签约协议号) 贯穿全链路:签约获取协议号 → 开记账本传入协议号 → 转账时 payer_info.ext_info 传入协议号。所有接口使用 SDK 4.39.218.ALL 已有类,无需升级。 **Tech Stack:** Java 21, Spring Boot 3.x, MyBatis-Plus, PostgreSQL, Alipay SDK 4.39.218.ALL ## Global Constraints - SDK 版本 4.39.218.ALL 已包含所有新 API 类,不可升级 - 北京时间 (ZoneOffset.ofHours(8)) 统一用于所有时间计算 - 签名类型 RSA2,字符集 UTF-8 - 页面跳转接口 (`pageExecute`) 需返回 body 作为 HTML 重定向 - 接口路径和参数名保持向后兼容,不破坏前端 --- ### Task 1: AccountEntity 增加 agreement_no 字段 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/entity/AccountEntity.java` - New: `java/src/main/resources/db/migration/V2__add_agreement_no.sql` (或直接 ALTER TABLE) **Interfaces:** - Produces: `AccountEntity.agreementNo` (String) — 签约协议号,createAccount 时写入 - [ ] **Step 1: 修改 AccountEntity** ```java // 在 signStatus 字段下方新增: /** 签约协议号 — 安全发模式下通过 alipay.user.agreement.page.sign 获取 */ private String agreementNo; ``` - [ ] **Step 2: 执行数据库迁移** ```sql ALTER TABLE pay_account ADD COLUMN IF NOT EXISTS agreement_no VARCHAR(64); COMMENT ON COLUMN pay_account.agreement_no IS '签约协议号'; ``` - [ ] **Step 3: 编译验证** ```bash cd java && mvn compile -q # Expected: BUILD SUCCESS ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "feat: AccountEntity 增加 agreement_no 字段用于安全发签约协议号" ``` --- ### Task 2: 重写 authorizeApply() — 签约页面跳转 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:90-109` - Modify: `java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:105-108` **Interfaces:** - Consumes: `AlipayClient.pageExecute()` (SDK已有的页面跳转方法) - Produces: `Map` — 返回 `{"sign_url": "<重定向HTML>"}` 旧代码: ```java AlipayCommerceEcTransAuthorizeApplyModel model = new AlipayCommerceEcTransAuthorizeApplyModel(); model.setEnterpriseId(enterpriseId); AlipayCommerceEcTransAuthorizeApplyRequest request = new AlipayCommerceEcTransAuthorizeApplyRequest(); request.setBizModel(model); AlipayCommerceEcTransAuthorizeApplyResponse response = client.execute(request); return Map.of("sign_url", response.getSignUrl()); ``` - [ ] **Step 1: 重写 authorizeApply 方法** ```java /** 对应新安全发模式: alipay.user.agreement.page.sign * 页面跳转接口 — 使用 pageExecute() 而非 execute() */ public Map authorizeApply(String enterpriseId) { try { AlipayUserAgreementPageSignModel model = new AlipayUserAgreementPageSignModel(); // 个人签约产品码 — 需与支付宝签约确定,安全发场景待确认 model.setPersonalProductCode("GENERAL_WITHHOLDING_P"); // 签约第三方主体类型: 平台商户 model.setThirdPartyType("PARTNER"); // 销售产品码 model.setProductCode("GENERAL_WITHHOLDING"); // 签约场景 model.setSignScene("DEFAULT|DEFAULT"); // 商户签约号 — 使用 enterpriseId 保证唯一 model.setExternalAgreementNo(enterpriseId + "_" + System.currentTimeMillis()); // 页面接入参数 AccessParams accessParams = new AccessParams(); accessParams.setChannel("ALIPAYAPP"); model.setAccessParams(accessParams); AlipayUserAgreementPageSignRequest request = new AlipayUserAgreementPageSignRequest(); request.setBizModel(model); // 页面跳转接口: 使用 pageExecute 而非 execute AlipayUserAgreementPageSignResponse response = alipayClientFactory.getClient(enterpriseId).pageExecute(request); if (response == null) { throw new BusinessException(400, "申请签约失败: 无响应"); } // pageExecute 返回的 body 是完整的 HTML 重定向页面 String body = response.getBody(); return Map.of("sign_url", body != null ? body : ""); } catch (AlipayApiException e) { throw new BusinessException(400, "申请签约失败: " + e.getMessage()); } } ``` - [ ] **Step 2: 添加 import** ```java import com.alipay.api.domain.AlipayUserAgreementPageSignModel; import com.alipay.api.domain.AccessParams; import com.alipay.api.request.AlipayUserAgreementPageSignRequest; import com.alipay.api.response.AlipayUserAgreementPageSignResponse; ``` - [ ] **Step 3: Controller 响应类型适配** `AccountController.java:105-108` — `authorizeApply` 现在返回 `sign_url`(HTML 重定向页面),前端需直接渲染该 HTML 或提取 URL 跳转。Controller 签名不变,仍然是 `Result>`。 - [ ] **Step 4: 编译验证** ```bash cd java && mvn compile -q # Expected: BUILD SUCCESS ``` - [ ] **Step 5: Commit** ```bash git add -A git commit -m "feat: authorizeApply 迁移到 alipay.user.agreement.page.sign (pageExecute)" ``` --- ### Task 3: 重写 createAccount() — 资金记账本开通 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:111-143` **Interfaces:** - Consumes: `agreementNo` (从 account entity 获取或通过参数传入) - Produces: `Map` — `{"enterprise_id", "account_book_id"}` 旧代码使用 `AlipayCommerceEcTransAccountCreateRequest`,新代码使用 `AlipayFundAccountbookCreateRequest`。 - [ ] **Step 1: 修改 createAccount 签名,增加 agreementNo 参数** 首先检查调用方: Controller `AccountController.java:112-115`: ```java @PostMapping public Result> create(@RequestBody Map b) { return Result.ok(alipayTransferService.createAccount((String) b.get("enterprise_id"))); } ``` 改为传入 `agreement_no`: ```java @PostMapping public Result> create(@RequestBody Map b) { return Result.ok(alipayTransferService.createAccount( (String) b.get("enterprise_id"), (String) b.get("agreement_no"))); } ``` - [ ] **Step 2: 重写 createAccount 方法** ```java /** 对应新安全发模式: alipay.fund.accountbook.create */ @Transactional public Map createAccount(String enterpriseId, String agreementNo) { try { AlipayFundAccountbookCreateModel model = new AlipayFundAccountbookCreateModel(); model.setMerchantUserId(enterpriseId); model.setMerchantUserType("BUSINESS_ORGANIZATION"); model.setSceneCode("SATF_FUND_BOOK"); // 扩展信息: 传入签约协议号 if (agreementNo != null && !agreementNo.isBlank()) { try { ObjectMapper om = new ObjectMapper(); Map extInfo = new LinkedHashMap<>(); extInfo.put("agreement_no", agreementNo); model.setExtInfo(om.writeValueAsString(extInfo)); } catch (Exception e) { throw new BusinessException(400, "序列化 ext_info 失败: " + e.getMessage()); } } AlipayFundAccountbookCreateRequest request = new AlipayFundAccountbookCreateRequest(); request.setBizModel(model); AlipayFundAccountbookCreateResponse response = alipayClientFactory.getClient(enterpriseId).execute(request); if (!response.isSuccess()) throw new BusinessException(400, "开通资金记账本失败: " + response.getMsg()); AccountEntity account = new AccountEntity(); account.setEnterpriseId(enterpriseId); account.setAccountBookId(response.getAccountBookId()); account.setAccountType("ALL"); account.setScene("B2B_TRANS"); account.setStatus("ACTIVE"); account.setSignStatus("SIGNED"); // 新流程: 已签约状态 account.setAgreementNo(agreementNo); accountMapper.insert(account); return Map.of("enterprise_id", enterpriseId, "account_book_id", response.getAccountBookId() != null ? response.getAccountBookId() : ""); } catch (AlipayApiException e) { throw new BusinessException(400, "开通资金记账本失败: " + e.getMessage()); } } ``` - [ ] **Step 3: 添加 import** ```java import com.alipay.api.domain.AlipayFundAccountbookCreateModel; import com.alipay.api.request.AlipayFundAccountbookCreateRequest; import com.alipay.api.response.AlipayFundAccountbookCreateResponse; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.LinkedHashMap; ``` - [ ] **Step 4: 编译验证** ```bash cd java && mvn compile -q # Expected: BUILD SUCCESS ``` - [ ] **Step 5: Commit** ```bash git add -A git commit -m "feat: createAccount 迁移到 alipay.fund.accountbook.create" ``` --- ### Task 4: 重写 deposit() — 记账本充值 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:145-179` **Interfaces:** - Consumes: `enterpriseId`, `accountBookId`, `amount`, `remark` - Produces: `Map` — `{"url": "<充值页面HTML>"}` 充值从普通请求变为页面跳转接口 (`alipay.fund.trans.page.pay`),使用 `pageExecute()`。 - [ ] **Step 1: 重写 deposit 方法** ```java /** 对应新安全发模式: alipay.fund.trans.page.pay (页面跳转充值) */ @Transactional public Map deposit(String enterpriseId, String accountBookId, BigDecimal amount, String remark) { try { AlipayFundTransPagePayModel model = new AlipayFundTransPagePayModel(); model.setOutBizNo(SnowflakeIdGenerator.nextIdStr()); model.setTransAmount(amount.toPlainString()); model.setProductCode("SINGLE_TRANSFER_NO_PWD"); model.setBizScene("DIRECT_ALLOCATION"); // 安全发充值场景 // 收款方: 记账本 TransParticipant payee = new TransParticipant(); payee.setIdentityType("ACCOUNT_BOOK_ID"); payee.setIdentity(accountBookId); model.setPayeeInfo(payee); if (remark != null && !remark.isBlank()) model.setRemark(remark); AlipayFundTransPagePayRequest request = new AlipayFundTransPagePayRequest(); request.setBizModel(model); AlipayFundTransPagePayResponse response = alipayClientFactory.getClient(enterpriseId).pageExecute(request); if (response == null || response.getBody() == null) throw new BusinessException(400, "充值页面生成失败: 无响应"); // 保存充值记录 DepositEntity deposit = new DepositEntity(); deposit.setEnterpriseId(enterpriseId); deposit.setOutBizNo(model.getOutBizNo()); deposit.setAccountBookId(accountBookId); deposit.setAmount(amount); deposit.setUrl(response.getBody()); deposit.setStatus("DEALING"); deposit.setRemark(remark); depositMapper.insert(deposit); return Map.of("url", response.getBody()); } catch (AlipayApiException e) { throw new BusinessException(400, "充值失败: " + e.getMessage()); } } ``` - [ ] **Step 2: 添加 import** ```java import com.alipay.api.domain.AlipayFundTransPagePayModel; import com.alipay.api.request.AlipayFundTransPagePayRequest; import com.alipay.api.response.AlipayFundTransPagePayResponse; ``` - [ ] **Step 3: 编译验证** ```bash cd java && mvn compile -q # Expected: BUILD SUCCESS ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "feat: deposit 迁移到 alipay.fund.trans.page.pay (pageExecute)" ``` --- ### Task 5: 重写 transfer() — 核心转账接口 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:189-316` - Modify: `java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java` (可能需要加参数) **Interfaces:** - Consumes: `TransferCreateDTO` (含 enterpriseId, accountBookId, amount, payeeInfo, extInfo 等) - Produces: `Map` — status, order_id, pay_fund_order_id, out_biz_no 旧 API: `AlipayCommerceEcTransAccountTransferRequest` → 新 API: `AlipayFundTransUniTransferRequest` - [ ] **Step 1: 重写 transfer 方法核心逻辑** ```java /** * 对应新安全发模式: alipay.fund.trans.uni.transfer * biz_scene=ENTRUST_TRANSFER, product_code=SINGLE_TRANSFER_NO_PWD */ @Transactional public Map transfer(TransferCreateDTO dto) { String accountBookId = dto.getAccountBookId(); String enterpriseId = dto.getEnterpriseId(); BigDecimal amount = dto.getAmount(); String orderTitle = dto.getOrderTitle(); String remark = dto.getRemark(); Map payeeInfo = dto.getPayeeInfo(); Map extInfo = dto.getExtInfo(); // 检查资金记账本 AccountEntity account = accountMapper.selectOne( new LambdaQueryWrapper() .eq(AccountEntity::getAccountBookId, accountBookId)); if (account == null) throw new BusinessException(400, "资金记账本不存在"); // 租户范围校验 if (dto.getTenantId() != null && !dto.getTenantId().equals(account.getTenantId())) { throw new BusinessException(403, "无权限操作"); } if (enterpriseId != null && !enterpriseId.equals(account.getEnterpriseId())) throw new BusinessException(400, "参数错误"); // 自动生成转账标题 if ((orderTitle == null || orderTitle.isBlank()) && account.getEnterpriseId() != null) { EnterpriseEntity ent = enterpriseMapper.selectOne( new LambdaQueryWrapper() .eq(EnterpriseEntity::getEnterpriseId, account.getEnterpriseId())); if (ent == null) throw new BusinessException(400, "记账本所属企业不存在"); if (ent.getName() != null) orderTitle = "来自" + ent.getName() + "转账"; } try { AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel(); model.setOutBizNo(SnowflakeIdGenerator.nextIdStr()); model.setTransAmount(amount.toPlainString()); model.setBizScene("ENTRUST_TRANSFER"); // 安全发服务商版转账 model.setProductCode("SINGLE_TRANSFER_NO_PWD"); // 安全发固定产品码 model.setOrderTitle(orderTitle); if (remark != null && !remark.isBlank()) model.setRemark(remark); // 付款方: 记账本 TransParticipant payer = new TransParticipant(); payer.setIdentityType("ACCOUNT_BOOK_ID"); payer.setIdentity(account.getAccountBookId()); // ext_info 传入签约协议号 if (account.getAgreementNo() != null && !account.getAgreementNo().isBlank()) { try { ObjectMapper om = new ObjectMapper(); Map payerExt = new LinkedHashMap<>(); payerExt.put("agreement_no", account.getAgreementNo()); payer.setExtInfo(om.writeValueAsString(payerExt)); } catch (Exception e) { throw new BusinessException(400, "序列化 payer ext_info 失败: " + e.getMessage()); } } model.setPayerInfo(payer); // 收款方 TransParticipant payee = new TransParticipant(); payee.setIdentityType((String) payeeInfo.get("identity_type")); payee.setName((String) payeeInfo.get("name")); payee.setIdentity((String) payeeInfo.get("identity")); // 银行卡扩展信息 @SuppressWarnings("unchecked") Map bankcardInfo = (Map) payeeInfo.get("bankcard_ext_info"); if (bankcardInfo != null) { try { Class bceClass = Class.forName("com.alipay.api.domain.BankCardExtInfo"); Object bce = bceClass.getDeclaredConstructor().newInstance(); for (var entry : bankcardInfo.entrySet()) { String setter = "set" + Character.toUpperCase(entry.getKey().charAt(0)) + entry.getKey().substring(1); try { bceClass.getMethod(setter, String.class).invoke(bce, entry.getValue()); } catch (Exception ignored) {} } payee.getClass().getMethod("setBankcardExtInfo", bceClass).invoke(payee, bce); } catch (Exception ignored) {} } model.setPayeeInfo(payee); // 转账场景信息 if (extInfo != null && extInfo.containsKey("transfer_scene_name")) { model.setTransferSceneName((String) extInfo.get("transfer_scene_name")); } if (extInfo != null && extInfo.containsKey("transfer_scene_report_infos")) { model.setTransferSceneReportInfos(/* ... */); } AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); request.setBizModel(model); AlipayFundTransUniTransferResponse response = alipayClientFactory.getClient(enterpriseId).execute(request); String subCode = response.getSubCode() != null ? response.getSubCode() : ""; String subMsg = response.getSubMsg() != null ? response.getSubMsg() : ""; // 构建转账记录 (字段名适配新API) TransferEntity transfer = new TransferEntity(); transfer.setEnterpriseId(account.getEnterpriseId()); transfer.setOutBizNo(model.getOutBizNo()); transfer.setAccountBookId(account.getAccountBookId()); transfer.setAmount(new BigDecimal(model.getTransAmount())); transfer.setOrderTitle(model.getOrderTitle()); transfer.setPayeeInfo(toJson(payeeInfo)); transfer.setPayeeType((String) payeeInfo.get("identity_type")); transfer.setStatus(response.getStatus()); transfer.setOrderNo(response.getOrderId()); // 新API返回 orderId 而非 orderNo transfer.setFundOrderId(response.getPayFundOrderId()); // 新API返回 payFundOrderId transfer.setRemark(remark != null ? remark : ""); if (extInfo != null) transfer.setExtInfo(toJson(extInfo)); if (!response.isSuccess()) { String hint = TRANSFER_ERROR_HINTS.get(subCode); if (hint == null) { for (var entry : TRANSFER_ERROR_HINTS.entrySet()) { if (subMsg.contains(entry.getKey())) { hint = entry.getValue(); break; } } } if (hint == null) hint = !subMsg.isEmpty() ? subMsg : response.getMsg(); log.error("支付宝转账失败: code={}, msg={}, sub_code={}, sub_msg={}", response.getCode(), response.getMsg(), subCode, subMsg); self.insertFailedTransfer(transfer, response.getMsg(), subCode, subMsg, remark != null ? remark : ""); throw new BusinessException(400, "转账失败: " + hint); } // DEALING 状态设置重试 if ("DEALING".equals(response.getStatus())) { transfer.setRetryCount(0); transfer.setNextRetryAt(OffsetDateTime.now().plusMinutes(5)); } transferMapper.insert(transfer); Map result = new LinkedHashMap<>(); result.put("status", response.getStatus()); result.put("order_id", response.getOrderId()); result.put("pay_fund_order_id", response.getPayFundOrderId()); result.put("out_biz_no", model.getOutBizNo()); return result; } catch (AlipayApiException e) { throw new BusinessException(400, "转账失败: " + e.getMessage()); } } ``` - [ ] **Step 2: 添加 import** ```java import com.alipay.api.domain.AlipayFundTransUniTransferModel; import com.alipay.api.request.AlipayFundTransUniTransferRequest; import com.alipay.api.response.AlipayFundTransUniTransferResponse; import com.fasterxml.jackson.databind.ObjectMapper; ``` - [ ] **Step 3: 检查 TransferCreateDTO 是否需要新增字段** 检查 `TransferCreateDTO.java`。当前字段: `enterpriseId, accountBookId, outBizNo, amount, orderTitle, remark, payeeInfo, employeeId, priority, extInfo, tenantId`。新接口无需额外字段,`extInfo` 可传 `transfer_scene_name` 和 `transfer_scene_report_infos`。 - [ ] **Step 4: 编译验证** ```bash cd java && mvn compile -q # Expected: BUILD SUCCESS ``` - [ ] **Step 5: Commit** ```bash git add -A git commit -m "feat: transfer 迁移到 alipay.fund.trans.uni.transfer" ``` --- ### Task 6: 重写 queryAccounts() — 记账本查询 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:380-417` **Interfaces:** - Consumes: `enterpriseId` - Produces: `List>` — account_book_id, scene, status 列表 - [ ] **Step 1: 重写 queryAccounts 方法** ```java /** 对应新安全发模式: alipay.fund.accountbook.query */ public List> queryAccounts(String enterpriseId) { try { AlipayFundAccountbookQueryModel model = new AlipayFundAccountbookQueryModel(); model.setMerchantUserId(enterpriseId); model.setSceneCode("SATF_FUND_BOOK"); AlipayFundAccountbookQueryRequest request = new AlipayFundAccountbookQueryRequest(); request.setBizModel(model); AlipayFundAccountbookQueryResponse response = alipayClientFactory.getClient(enterpriseId).execute(request); if (!response.isSuccess()) throw new BusinessException(400, "查询记账本失败: " + response.getMsg()); List> result = new ArrayList<>(); // 注意: 新API返回结构可能不同,取决于实际 SDK Response 字段 // 通常返回单个 account_book_id 和基本信息 Map m = new LinkedHashMap<>(); m.put("account_book_id", response.getAccountBookId()); m.put("scene", "B2B_TRANS"); m.put("status", response.getStatus()); result.add(m); return result; } catch (AlipayApiException e) { throw new BusinessException(400, "查询记账本失败: " + e.getMessage()); } } ``` - [ ] **Step 2: 添加 import** ```java import com.alipay.api.domain.AlipayFundAccountbookQueryModel; import com.alipay.api.request.AlipayFundAccountbookQueryRequest; import com.alipay.api.response.AlipayFundAccountbookQueryResponse; ``` - [ ] **Step 3: 编译验证** ```bash cd java && mvn compile -q # Expected: BUILD SUCCESS ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "feat: queryAccounts 迁移到 alipay.fund.accountbook.query" ``` --- ### Task 7: 重写转账状态同步 — alipay.fund.trans.common.query **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java` (transferSyncAllService 和 syncStatus 方法) - [ ] **Step 1: 添加 transferQuery 辅助方法** ```java /** * 查询单笔转账状态 — alipay.fund.trans.common.query * 可用 order_id、out_biz_no 或 pay_fund_order_id 任一查询 */ public Map queryTransferStatus(String orderId, String outBizNo) { try { AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel(); if (orderId != null && !orderId.isBlank()) { model.setOrderId(orderId); } else if (outBizNo != null && !outBizNo.isBlank()) { model.setOutBizNo(outBizNo); } else { throw new BusinessException(400, "order_id 和 out_biz_no 不能同时为空"); } model.setProductCode("SINGLE_TRANSFER_NO_PWD"); model.setBizScene("ENTRUST_TRANSFER"); AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest(); request.setBizModel(model); AlipayClient client = alipayClientFactory.getClient(); // 使用默认客户端 AlipayFundTransCommonQueryResponse response = client.execute(request); if (!response.isSuccess()) { log.warn("查询转账状态失败: code={}, msg={}", response.getCode(), response.getMsg()); return Map.of("status", "UNKNOWN", "error", response.getMsg()); } Map result = new LinkedHashMap<>(); result.put("status", response.getStatus() != null ? response.getStatus() : "UNKNOWN"); result.put("order_id", response.getOrderId()); result.put("out_biz_no", response.getOutBizNo()); result.put("pay_fund_order_id", response.getPayFundOrderId()); result.put("trans_amount", response.getTransAmount()); result.put("pay_date", response.getPayDate()); return result; } catch (AlipayApiException e) { log.error("查询转账状态异常", e); return Map.of("status", "UNKNOWN", "error", e.getMessage()); } } ``` - [ ] **Step 2: 更新 transferSyncAllService 使用新查询方法** ```java /** 全量同步转账状态 — 使用 alipay.fund.trans.common.query */ public Map transferSyncAllService() { // 查询所有 DEALING 状态的转账记录 List dealingTransfers = transferMapper.selectList( new LambdaQueryWrapper() .eq(TransferEntity::getStatus, "DEALING")); int success = 0, fail = 0, dealing = 0; for (TransferEntity t : dealingTransfers) { Map status = queryTransferStatus(t.getOrderNo(), null); String s = status.get("status"); if ("SUCCESS".equals(s)) { t.setStatus("SUCCESS"); transferMapper.updateById(t); success++; } else if ("FAIL".equals(s) || "REFUND".equals(s)) { t.setStatus(s); t.setErrorMsg(status.get("error")); transferMapper.updateById(t); fail++; } else { dealing++; } } return Map.of("total", dealingTransfers.size(), "success", success, "fail", fail, "dealing", dealing); } ``` - [ ] **Step 3: 添加 import** ```java import com.alipay.api.domain.AlipayFundTransCommonQueryModel; import com.alipay.api.request.AlipayFundTransCommonQueryRequest; import com.alipay.api.response.AlipayFundTransCommonQueryResponse; import com.alipay.api.AlipayClient; ``` - [ ] **Step 4: 编译验证 + Commit** ```bash cd java && mvn compile -q git add -A git commit -m "feat: 转账状态同步迁移到 alipay.fund.trans.common.query" ``` --- ### Task 8: 添加异步通知处理器 — alipay.fund.trans.order.changed **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/notification/handler/BillHandler.java` (或新建 `TransferNotifyHandler.java`) 新通知 `alipay.fund.trans.order.changed` 用于接收转账状态变更通知。需要在 NotificationService 中注册处理。 - [ ] **Step 1: 创建 TransferNotifyHandler.java** ```java // 路径: java/src/main/java/com/payment/platform/module/payment/notification/handler/TransferNotifyHandler.java package com.payment.platform.module.payment.notification.handler; import com.payment.platform.module.payment.account.entity.TransferEntity; import com.payment.platform.module.payment.account.mapper.TransferMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Map; /** 转账状态变更通知处理器 — alipay.fund.trans.order.changed */ @Slf4j @Component @RequiredArgsConstructor public class TransferNotifyHandler extends BaseNotifyHandler { private final TransferMapper transferMapper; @Override protected String[] acceptedMethods() { return new String[]{"alipay.fund.trans.order.changed"}; } @Override protected void handle(String msgMethod, Map params) { String orderId = params.get("order_id"); String outBizNo = params.get("out_biz_no"); String status = params.get("status"); String payFundOrderId = params.get("pay_fund_order_id"); log.info("转账状态变更通知: order_id={}, out_biz_no={}, status={}", orderId, outBizNo, status); TransferEntity transfer = null; if (orderId != null) { transfer = transferMapper.selectOne( new LambdaQueryWrapper() .eq(TransferEntity::getOrderNo, orderId)); } if (transfer == null && outBizNo != null) { transfer = transferMapper.selectOne( new LambdaQueryWrapper() .eq(TransferEntity::getOutBizNo, outBizNo)); } if (transfer == null) { log.warn("转账通知: 未找到对应记录 order_id={}, out_biz_no={}", orderId, outBizNo); return; } if ("SUCCESS".equals(status) || "FAIL".equals(status) || "REFUND".equals(status)) { transfer.setStatus(status); if (payFundOrderId != null) transfer.setFundOrderId(payFundOrderId); transferMapper.updateById(transfer); log.info("转账状态同步完成: order_id={}, status={}", orderId, status); } } } ``` - [ ] **Step 2: 编译验证 + Commit** ```bash cd java && mvn compile -q git add -A git commit -m "feat: 新增 TransferNotifyHandler 处理 alipay.fund.trans.order.changed" ``` --- ### Task 9: 清理 withdraw() — 提现接口 **Files:** - Modify: `java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:334-378` - Modify: `java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:180-188` 安全发文档中没有直接对应旧 `withdraw` 的接口。提现功能在新体系中可能通过 `alipay.fund.trans.uni.transfer`(从记账本转到企业余额户)实现,或标记为待定。 - [ ] **Step 1: 标记 withdraw 为 DEPRECATED 或改造** 如果短期内不需要提现功能,标记方法为 deprecated: ```java /** @deprecated 安全发模式下暂不支持独立提现接口,需走 uni.transfer 从记账本转回余额户 */ @Deprecated @Transactional public Map withdraw(...) { ... } ``` 如果保留,改为走 `alipay.fund.trans.uni.transfer` + `biz_scene=ENTRUST_ALLOCATION`(记账本调拨回余额户)。 - [ ] **Step 2: 编译验证 + Commit** --- ### Task 10: 前端 API 适配 **Files:** - Modify: `frontend/src/api/module_payment/account.ts` - [ ] **Step 1: authorizeApply 响应处理改为 HTML 重定向** ```typescript authorizeApply(data: { enterprise_id: string }) { return request>({ url: `${API_PATH}/authorize/apply`, method: "post", data, }).then(res => { // sign_url 现在是完整的 HTML 重定向页面 // 提取 URL 后跳转,或直接 document.write if (res.data?.sign_url) { document.open(); document.write(res.data.sign_url); document.close(); } return res; }); }, ``` - [ ] **Step 2: create 接口增加 agreement_no 参数** ```typescript create(data: { enterprise_id: string; agreement_no?: string; // 新增 }) { return request>({ url: `${API_PATH}`, method: "post", data, }); }, ``` - [ ] **Step 3: 转账响应字段名适配** ```typescript // 旧: { status, order_no, fund_order_id } // 新: { status, order_id, pay_fund_order_id } // 前端兼容两种字段名 transfer(data) { return request>({ ... }); } ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "feat: 前端适配安全发API参数变化" ``` --- ### Task 11: 集成测试与验证 - [ ] **Step 1: 启动后端验证编译** ```bash cd java && mvn spring-boot:run # 检查启动日志无错误 ``` - [ ] **Step 2: 测试签约接口** ```bash curl -X POST http://localhost:8001/api/v1/payment/account/authorize/apply \ -H "Content-Type: application/json" \ -d '{"enterprise_id":"test_enterprise"}' # Expected: 返回 sign_url HTML ``` - [ ] **Step 3: 测试开通记账本** ```bash curl -X POST http://localhost:8001/api/v1/payment/account \ -H "Content-Type: application/json" \ -d '{"enterprise_id":"test_enterprise","agreement_no":"2025XXXXXX"}' # Expected: 返回 account_book_id ``` - [ ] **Step 4: 测试转账** ```bash curl -X POST http://localhost:8001/api/v1/payment/account/transfer \ -H "Content-Type: application/json" \ -d '{"enterprise_id":"test","account_book_id":"2088xxx","amount":1.00,"payee_info":{"identity_type":"ALIPAY_USER_ID","identity":"2088xxx","name":"测试用户"}}' # Expected: 返回 status, order_id ``` --- ## API 映射速查表 | 功能 | 旧 API | 新 API | 调用方式 | |------|--------|--------|----------| | 签约 | `alipay.commerce.ec.trans.authorize.apply` | `alipay.user.agreement.page.sign` | `pageExecute()` | | 开记账本 | `alipay.commerce.ec.trans.account.create` | `alipay.fund.accountbook.create` | `execute()` | | 充值 | `alipay.commerce.ec.trans.account.deposit` | `alipay.fund.trans.page.pay` | `pageExecute()` | | 转账 | `alipay.commerce.ec.trans.account.transfer` | `alipay.fund.trans.uni.transfer` | `execute()` | | 查记账本 | `alipay.commerce.ec.trans.account.query` | `alipay.fund.accountbook.query` | `execute()` | | 查转账 | — | `alipay.fund.trans.common.query` | `execute()` | | 通知 | `alipay.commerce.ec.consume.change.notify` | `alipay.fund.trans.order.changed` | 异步接收 | ## 安全发转账关键参数 | 参数 | 值 | 说明 | |------|----|------| | `biz_scene` | `ENTRUST_TRANSFER` | 服务商版转账 | | `product_code` | `SINGLE_TRANSFER_NO_PWD` | 安全发固定产品码 | | `payer_info.identity_type` | `ACCOUNT_BOOK_ID` | 付款方为记账本 | | `payer_info.identity` | 记账本ID | 从 createAccount 获取 | | `payer_info.ext_info` | `{"agreement_no":"..."}` | 签约协议号 | | `payee_info.identity_type` | `ALIPAMOBILE_USER_ID` | 支付宝用户ID |