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

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

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