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
pageExecute) 需返回 body 作为 HTML 重定向Files:
java/src/main/java/com/payment/platform/module/payment/account/entity/AccountEntity.javajava/src/main/resources/db/migration/V2__add_agreement_no.sql (或直接 ALTER TABLE)Interfaces:
Produces: AccountEntity.agreementNo (String) — 签约协议号,createAccount 时写入
[ ] Step 1: 修改 AccountEntity
// 在 signStatus 字段下方新增:
/** 签约协议号 — 安全发模式下通过 alipay.user.agreement.page.sign 获取 */
private String agreementNo;
[ ] Step 2: 执行数据库迁移
ALTER TABLE pay_account ADD COLUMN IF NOT EXISTS agreement_no VARCHAR(64);
COMMENT ON COLUMN pay_account.agreement_no IS '签约协议号';
[ ] Step 3: 编译验证
cd java && mvn compile -q
# Expected: BUILD SUCCESS
[ ] Step 4: Commit
git add -A
git commit -m "feat: AccountEntity 增加 agreement_no 字段用于安全发签约协议号"
Files:
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:90-109java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:105-108Interfaces:
AlipayClient.pageExecute() (SDK已有的页面跳转方法)Map<String, String> — 返回 {"sign_url": "<重定向HTML>"}旧代码:
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 方法
/** 对应新安全发模式: alipay.user.agreement.page.sign
* 页面跳转接口 — 使用 pageExecute() 而非 execute() */
public Map<String, String> 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
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<Map<String, String>>。
[ ] Step 4: 编译验证
cd java && mvn compile -q
# Expected: BUILD SUCCESS
[ ] Step 5: Commit
git add -A
git commit -m "feat: authorizeApply 迁移到 alipay.user.agreement.page.sign (pageExecute)"
Files:
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:111-143Interfaces:
agreementNo (从 account entity 获取或通过参数传入)Map<String, String> — {"enterprise_id", "account_book_id"}旧代码使用 AlipayCommerceEcTransAccountCreateRequest,新代码使用 AlipayFundAccountbookCreateRequest。
首先检查调用方:
Controller AccountController.java:112-115:
@PostMapping
public Result<Map<String, String>> create(@RequestBody Map<String, Object> b) {
return Result.ok(alipayTransferService.createAccount((String) b.get("enterprise_id")));
}
改为传入 agreement_no:
@PostMapping
public Result<Map<String, String>> create(@RequestBody Map<String, Object> b) {
return Result.ok(alipayTransferService.createAccount(
(String) b.get("enterprise_id"),
(String) b.get("agreement_no")));
}
[ ] Step 2: 重写 createAccount 方法
/** 对应新安全发模式: alipay.fund.accountbook.create */
@Transactional
public Map<String, String> 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<String, String> 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
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: 编译验证
cd java && mvn compile -q
# Expected: BUILD SUCCESS
[ ] Step 5: Commit
git add -A
git commit -m "feat: createAccount 迁移到 alipay.fund.accountbook.create"
Files:
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:145-179Interfaces:
enterpriseId, accountBookId, amount, remarkMap<String, String> — {"url": "<充值页面HTML>"}充值从普通请求变为页面跳转接口 (alipay.fund.trans.page.pay),使用 pageExecute()。
[ ] Step 1: 重写 deposit 方法
/** 对应新安全发模式: alipay.fund.trans.page.pay (页面跳转充值) */
@Transactional
public Map<String, String> 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
import com.alipay.api.domain.AlipayFundTransPagePayModel;
import com.alipay.api.request.AlipayFundTransPagePayRequest;
import com.alipay.api.response.AlipayFundTransPagePayResponse;
[ ] Step 3: 编译验证
cd java && mvn compile -q
# Expected: BUILD SUCCESS
[ ] Step 4: Commit
git add -A
git commit -m "feat: deposit 迁移到 alipay.fund.trans.page.pay (pageExecute)"
Files:
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:189-316java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java (可能需要加参数)Interfaces:
TransferCreateDTO (含 enterpriseId, accountBookId, amount, payeeInfo, extInfo 等)Map<String, Object> — status, order_id, pay_fund_order_id, out_biz_no旧 API: AlipayCommerceEcTransAccountTransferRequest → 新 API: AlipayFundTransUniTransferRequest
[ ] Step 1: 重写 transfer 方法核心逻辑
/**
* 对应新安全发模式: alipay.fund.trans.uni.transfer
* biz_scene=ENTRUST_TRANSFER, product_code=SINGLE_TRANSFER_NO_PWD
*/
@Transactional
public Map<String, Object> transfer(TransferCreateDTO dto) {
String accountBookId = dto.getAccountBookId();
String enterpriseId = dto.getEnterpriseId();
BigDecimal amount = dto.getAmount();
String orderTitle = dto.getOrderTitle();
String remark = dto.getRemark();
Map<String, Object> payeeInfo = dto.getPayeeInfo();
Map<String, Object> extInfo = dto.getExtInfo();
// 检查资金记账本
AccountEntity account = accountMapper.selectOne(
new LambdaQueryWrapper<AccountEntity>()
.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<EnterpriseEntity>()
.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<String, String> 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<String, Object> bankcardInfo = (Map<String, Object>) 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<String, Object> 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
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: 编译验证
cd java && mvn compile -q
# Expected: BUILD SUCCESS
[ ] Step 5: Commit
git add -A
git commit -m "feat: transfer 迁移到 alipay.fund.trans.uni.transfer"
Files:
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:380-417Interfaces:
enterpriseIdProduces: List<Map<String, Object>> — account_book_id, scene, status 列表
[ ] Step 1: 重写 queryAccounts 方法
/** 对应新安全发模式: alipay.fund.accountbook.query */
public List<Map<String, Object>> 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<Map<String, Object>> result = new ArrayList<>();
// 注意: 新API返回结构可能不同,取决于实际 SDK Response 字段
// 通常返回单个 account_book_id 和基本信息
Map<String, Object> 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
import com.alipay.api.domain.AlipayFundAccountbookQueryModel;
import com.alipay.api.request.AlipayFundAccountbookQueryRequest;
import com.alipay.api.response.AlipayFundAccountbookQueryResponse;
[ ] Step 3: 编译验证
cd java && mvn compile -q
# Expected: BUILD SUCCESS
[ ] Step 4: Commit
git add -A
git commit -m "feat: queryAccounts 迁移到 alipay.fund.accountbook.query"
Files:
Modify: java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java (transferSyncAllService 和 syncStatus 方法)
[ ] Step 1: 添加 transferQuery 辅助方法
/**
* 查询单笔转账状态 — alipay.fund.trans.common.query
* 可用 order_id、out_biz_no 或 pay_fund_order_id 任一查询
*/
public Map<String, String> 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<String, String> 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 使用新查询方法
/** 全量同步转账状态 — 使用 alipay.fund.trans.common.query */
public Map<String, Object> transferSyncAllService() {
// 查询所有 DEALING 状态的转账记录
List<TransferEntity> dealingTransfers = transferMapper.selectList(
new LambdaQueryWrapper<TransferEntity>()
.eq(TransferEntity::getStatus, "DEALING"));
int success = 0, fail = 0, dealing = 0;
for (TransferEntity t : dealingTransfers) {
Map<String, String> 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
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
cd java && mvn compile -q
git add -A
git commit -m "feat: 转账状态同步迁移到 alipay.fund.trans.common.query"
Files:
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/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<String, String> 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<TransferEntity>()
.eq(TransferEntity::getOrderNo, orderId));
}
if (transfer == null && outBizNo != null) {
transfer = transferMapper.selectOne(
new LambdaQueryWrapper<TransferEntity>()
.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
cd java && mvn compile -q
git add -A
git commit -m "feat: 新增 TransferNotifyHandler 处理 alipay.fund.trans.order.changed"
Files:
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java:334-378java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:180-188安全发文档中没有直接对应旧 withdraw 的接口。提现功能在新体系中可能通过 alipay.fund.trans.uni.transfer(从记账本转到企业余额户)实现,或标记为待定。
如果短期内不需要提现功能,标记方法为 deprecated:
/** @deprecated 安全发模式下暂不支持独立提现接口,需走 uni.transfer 从记账本转回余额户 */
@Deprecated
@Transactional
public Map<String, String> withdraw(...) { ... }
如果保留,改为走 alipay.fund.trans.uni.transfer + biz_scene=ENTRUST_ALLOCATION(记账本调拨回余额户)。
Files:
Modify: frontend/src/api/module_payment/account.ts
[ ] Step 1: authorizeApply 响应处理改为 HTML 重定向
authorizeApply(data: { enterprise_id: string }) {
return request<ApiResponse<{ sign_url: string }>>({
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 参数
create(data: {
enterprise_id: string;
agreement_no?: string; // 新增
}) {
return request<ApiResponse<{ account_book_id: string }>>({
url: `${API_PATH}`,
method: "post",
data,
});
},
[ ] Step 3: 转账响应字段名适配
// 旧: { status, order_no, fund_order_id }
// 新: { status, order_id, pay_fund_order_id }
// 前端兼容两种字段名
transfer(data) {
return request<ApiResponse<{
status: string;
order_id?: string;
order_no?: string; // 兼容
pay_fund_order_id?: string;
fund_order_id?: string; // 兼容
out_biz_no: string;
}>>({ ... });
}
[ ] Step 4: Commit
git add -A
git commit -m "feat: 前端适配安全发API参数变化"
[ ] Step 1: 启动后端验证编译
cd java && mvn spring-boot:run
# 检查启动日志无错误
[ ] Step 2: 测试签约接口
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: 测试开通记账本
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: 测试转账
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 | 调用方式 |
|---|---|---|---|
| 签约 | 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 |