2026-07-15-alipay-safe-pay-migration.md 34 KB

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

    // 在 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 字段用于安全发签约协议号"
    

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<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-108authorizeApply 现在返回 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)"
    

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<String, String>{"enterprise_id", "account_book_id"}

旧代码使用 AlipayCommerceEcTransAccountCreateRequest,新代码使用 AlipayFundAccountbookCreateRequest

  • Step 1: 修改 createAccount 签名,增加 agreementNo 参数

首先检查调用方:

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"
    

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

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<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_nametransfer_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"
    

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<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"
    

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 辅助方法

    /**
    * 查询单笔转账状态 — 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"
    

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/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"
    

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:

/** @deprecated 安全发模式下暂不支持独立提现接口,需走 uni.transfer 从记账本转回余额户 */
@Deprecated
@Transactional
public Map<String, String> 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 重定向

    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参数变化"
    

Task 11: 集成测试与验证

  • [ ] 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 新 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