|
@@ -1,15 +1,28 @@
|
|
|
package com.payment.platform.module.payment.account.service;
|
|
package com.payment.platform.module.payment.account.service;
|
|
|
|
|
|
|
|
import com.alipay.api.AlipayApiException;
|
|
import com.alipay.api.AlipayApiException;
|
|
|
-import com.alipay.api.domain.AlipayCommerceEcTransAccountCreateModel;
|
|
|
|
|
-import com.alipay.api.domain.AlipayCommerceEcTransAccountDepositModel;
|
|
|
|
|
-import com.alipay.api.domain.AlipayCommerceEcTransAccountQueryModel;
|
|
|
|
|
-import com.alipay.api.domain.AlipayCommerceEcTransAccountTransferModel;
|
|
|
|
|
-import com.alipay.api.domain.AlipayCommerceEcTransAccountWithdrawModel;
|
|
|
|
|
-import com.alipay.api.domain.AlipayCommerceEcTransAuthorizeApplyModel;
|
|
|
|
|
-import com.alipay.api.domain.TransParticipant;
|
|
|
|
|
-import com.alipay.api.request.*;
|
|
|
|
|
-import com.alipay.api.response.*;
|
|
|
|
|
|
|
+import com.alipay.api.AlipayClient;
|
|
|
|
|
+import com.alipay.api.domain.AccessParams;
|
|
|
|
|
+import com.alipay.api.domain.AlipayFundAccountbookCreateModel;
|
|
|
|
|
+import com.alipay.api.domain.AlipayFundAccountbookQueryModel;
|
|
|
|
|
+import com.alipay.api.domain.AlipayFundTransCommonQueryModel;
|
|
|
|
|
+import com.alipay.api.domain.AlipayFundTransPagePayModel;
|
|
|
|
|
+import com.alipay.api.domain.AlipayFundTransUniTransferModel;
|
|
|
|
|
+import com.alipay.api.domain.AlipayUserAgreementPageSignModel;
|
|
|
|
|
+import com.alipay.api.domain.BankcardExtInfo;
|
|
|
|
|
+import com.alipay.api.domain.Participant;
|
|
|
|
|
+import com.alipay.api.request.AlipayFundAccountbookCreateRequest;
|
|
|
|
|
+import com.alipay.api.request.AlipayFundAccountbookQueryRequest;
|
|
|
|
|
+import com.alipay.api.request.AlipayFundTransCommonQueryRequest;
|
|
|
|
|
+import com.alipay.api.request.AlipayFundTransPagePayRequest;
|
|
|
|
|
+import com.alipay.api.request.AlipayFundTransUniTransferRequest;
|
|
|
|
|
+import com.alipay.api.request.AlipayUserAgreementPageSignRequest;
|
|
|
|
|
+import com.alipay.api.response.AlipayFundAccountbookCreateResponse;
|
|
|
|
|
+import com.alipay.api.response.AlipayFundAccountbookQueryResponse;
|
|
|
|
|
+import com.alipay.api.response.AlipayFundTransCommonQueryResponse;
|
|
|
|
|
+import com.alipay.api.response.AlipayFundTransPagePayResponse;
|
|
|
|
|
+import com.alipay.api.response.AlipayFundTransUniTransferResponse;
|
|
|
|
|
+import com.alipay.api.response.AlipayUserAgreementPageSignResponse;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
|
import com.payment.platform.common.utils.RedisLockUtil;
|
|
import com.payment.platform.common.utils.RedisLockUtil;
|
|
@@ -37,7 +50,9 @@ import java.time.OffsetDateTime;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 支付宝转账服务 — 对应 Python AccountService 中的 Alipay 调用部分
|
|
|
|
|
|
|
+ * 支付宝安全发转账服务 — 迁移到安全发-服务商模式 API
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 旧 API (alipay.commerce.ec.trans.*) -> 新 API (alipay.fund.* / alipay.user.agreement.*)
|
|
|
*/
|
|
*/
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Service
|
|
@Service
|
|
@@ -53,139 +68,174 @@ public class AlipayTransferService {
|
|
|
private final ObjectMapper objectMapper;
|
|
private final ObjectMapper objectMapper;
|
|
|
private final RedisLockUtil redisLockUtil;
|
|
private final RedisLockUtil redisLockUtil;
|
|
|
|
|
|
|
|
- // 自注入用于独立事务(对应 Python 的 async with async_db_session())
|
|
|
|
|
- // @Lazy 避免构造期间的循环依赖
|
|
|
|
|
@org.springframework.beans.factory.annotation.Autowired
|
|
@org.springframework.beans.factory.annotation.Autowired
|
|
|
@org.springframework.context.annotation.Lazy
|
|
@org.springframework.context.annotation.Lazy
|
|
|
private AlipayTransferService self;
|
|
private AlipayTransferService self;
|
|
|
|
|
|
|
|
- // 用于重试任务回调通知 — 对应 Python retry_dealing_transfers 中调用 OpenTransferService.open_return_service
|
|
|
|
|
@org.springframework.beans.factory.annotation.Autowired
|
|
@org.springframework.beans.factory.annotation.Autowired
|
|
|
@org.springframework.context.annotation.Lazy
|
|
@org.springframework.context.annotation.Lazy
|
|
|
private com.payment.platform.module.payment.openapi.service.OpenapiService openapiService;
|
|
private com.payment.platform.module.payment.openapi.service.OpenapiService openapiService;
|
|
|
|
|
|
|
|
|
|
+ // ---- 安全发常量 ----
|
|
|
|
|
+ private static final String PRODUCT_CODE = "SINGLE_TRANSFER_NO_PWD";
|
|
|
|
|
+ private static final String BIZ_SCENE_TRANSFER = "ENTRUST_TRANSFER";
|
|
|
|
|
+ private static final String BIZ_SCENE_ALLOCATION = "DIRECT_ALLOCATION";
|
|
|
|
|
+ private static final String SCENE_CODE_ACCOUNT_BOOK = "SATF_FUND_BOOK";
|
|
|
|
|
+
|
|
|
private static final Map<String, String> TRANSFER_ERROR_HINTS = new LinkedHashMap<>();
|
|
private static final Map<String, String> TRANSFER_ERROR_HINTS = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
static {
|
|
static {
|
|
|
TRANSFER_ERROR_HINTS.put("SYSTEM_ERROR", "系统繁忙,请稍后重试");
|
|
TRANSFER_ERROR_HINTS.put("SYSTEM_ERROR", "系统繁忙,请稍后重试");
|
|
|
TRANSFER_ERROR_HINTS.put("INVALID_PARAMETER", "请求参数有误,请检查后重试");
|
|
TRANSFER_ERROR_HINTS.put("INVALID_PARAMETER", "请求参数有误,请检查后重试");
|
|
|
- TRANSFER_ERROR_HINTS.put("AMOUNT_LESS_THAN_ONE_CENT", "转账金额不能低于 0.01 元");
|
|
|
|
|
TRANSFER_ERROR_HINTS.put("BALANCE_IS_NOT_ENOUGH", "企业余额不足,建议充值");
|
|
TRANSFER_ERROR_HINTS.put("BALANCE_IS_NOT_ENOUGH", "企业余额不足,建议充值");
|
|
|
TRANSFER_ERROR_HINTS.put("BANK_RESPONSE_ERROR", "银行处理失败:账户异常");
|
|
TRANSFER_ERROR_HINTS.put("BANK_RESPONSE_ERROR", "银行处理失败:账户异常");
|
|
|
TRANSFER_ERROR_HINTS.put("CARD_BIN_ERROR", "收款银行账号不正确,请确认");
|
|
TRANSFER_ERROR_HINTS.put("CARD_BIN_ERROR", "收款银行账号不正确,请确认");
|
|
|
- TRANSFER_ERROR_HINTS.put("DUPLICATE_DIFFERENT_REQUEST", "重复请求但参数不一致,请检查");
|
|
|
|
|
TRANSFER_ERROR_HINTS.put("EXCEED_LIMIT_SM_MIN_AMOUNT", "转账金额不能低于 0.1 元");
|
|
TRANSFER_ERROR_HINTS.put("EXCEED_LIMIT_SM_MIN_AMOUNT", "转账金额不能低于 0.1 元");
|
|
|
- TRANSFER_ERROR_HINTS.put("EXCEED_LIMIT_DM_MAX_AMOUNT", "超出单日转账限额,请明天再试或联系管理员提升限额");
|
|
|
|
|
- TRANSFER_ERROR_HINTS.put("INVALID_ACCOUNT_BOOK", "资金专户不存在,请检查专户号");
|
|
|
|
|
|
|
+ TRANSFER_ERROR_HINTS.put("EXCEED_LIMIT_DM_MAX_AMOUNT", "超出单日转账限额");
|
|
|
TRANSFER_ERROR_HINTS.put("INVALID_CARDNO", "无效的收款银行卡号");
|
|
TRANSFER_ERROR_HINTS.put("INVALID_CARDNO", "无效的收款银行卡号");
|
|
|
- TRANSFER_ERROR_HINTS.put("INVALID_IDENTITY_TYPE", "收款方身份类型不匹配");
|
|
|
|
|
TRANSFER_ERROR_HINTS.put("NO_AGREEMENT", "无转账权限,请联系管理员");
|
|
TRANSFER_ERROR_HINTS.put("NO_AGREEMENT", "无转账权限,请联系管理员");
|
|
|
- TRANSFER_ERROR_HINTS.put("PAYEE_CARD_INFO_ERROR", "收款方账号或银行卡信息有误,请核实");
|
|
|
|
|
|
|
+ TRANSFER_ERROR_HINTS.put("PAYEE_CARD_INFO_ERROR", "收款方账号或银行卡信息有误");
|
|
|
TRANSFER_ERROR_HINTS.put("PAYEE_NOT_EXIST", "收款账号不存在或姓名有误");
|
|
TRANSFER_ERROR_HINTS.put("PAYEE_NOT_EXIST", "收款账号不存在或姓名有误");
|
|
|
TRANSFER_ERROR_HINTS.put("PAYER_BALANCE_NOT_ENOUGH", "付款方余额不足,建议充值");
|
|
TRANSFER_ERROR_HINTS.put("PAYER_BALANCE_NOT_ENOUGH", "付款方余额不足,建议充值");
|
|
|
TRANSFER_ERROR_HINTS.put("REQUEST_PROCESSING", "系统处理中,请稍后重试");
|
|
TRANSFER_ERROR_HINTS.put("REQUEST_PROCESSING", "系统处理中,请稍后重试");
|
|
|
- TRANSFER_ERROR_HINTS.put("TRANS_AUTH_NO_EXIST", "转账授权协议不存在,请先签约");
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 对应 Python authorize_apply_service */
|
|
|
|
|
|
|
+ // ==================== 1. 签约(页面跳转) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * alipay.user.agreement.page.sign — 页面跳转接口
|
|
|
|
|
+ * 必填: personal_product_code, product_code, sign_scene, access_params
|
|
|
|
|
+ */
|
|
|
public Map<String, String> authorizeApply(String enterpriseId) {
|
|
public Map<String, String> authorizeApply(String enterpriseId) {
|
|
|
try {
|
|
try {
|
|
|
- AlipayCommerceEcTransAuthorizeApplyModel model = new AlipayCommerceEcTransAuthorizeApplyModel();
|
|
|
|
|
- model.setEnterpriseId(enterpriseId);
|
|
|
|
|
-
|
|
|
|
|
- AlipayCommerceEcTransAuthorizeApplyRequest request = new AlipayCommerceEcTransAuthorizeApplyRequest();
|
|
|
|
|
|
|
+ AlipayUserAgreementPageSignModel model = new AlipayUserAgreementPageSignModel();
|
|
|
|
|
+ model.setPersonalProductCode("GENERAL_WITHHOLDING_P");
|
|
|
|
|
+ model.setProductCode("GENERAL_WITHHOLDING");
|
|
|
|
|
+ model.setSignScene("DEFAULT|DEFAULT");
|
|
|
|
|
+ model.setExternalAgreementNo(enterpriseId + "_" + System.currentTimeMillis());
|
|
|
|
|
+ AccessParams accessParams = new AccessParams();
|
|
|
|
|
+ accessParams.setChannel("ALIPAYAPP");
|
|
|
|
|
+ model.setAccessParams(accessParams);
|
|
|
|
|
+
|
|
|
|
|
+ AlipayUserAgreementPageSignRequest request = new AlipayUserAgreementPageSignRequest();
|
|
|
request.setBizModel(model);
|
|
request.setBizModel(model);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAuthorizeApplyResponse response =
|
|
|
|
|
- alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
|
|
|
|
|
+ AlipayUserAgreementPageSignResponse response =
|
|
|
|
|
+ alipayClientFactory.getClient(enterpriseId).pageExecute(request);
|
|
|
|
|
|
|
|
- if (!response.isSuccess())
|
|
|
|
|
- throw new BusinessException(400, "申请转账授权失败: " + response.getMsg());
|
|
|
|
|
|
|
+ if (response == null)
|
|
|
|
|
+ throw new BusinessException(400, "申请签约失败: 无响应");
|
|
|
|
|
|
|
|
- return Map.of("sign_url", response.getSignUrl() != null ? response.getSignUrl() : "");
|
|
|
|
|
|
|
+ String body = response.getBody();
|
|
|
|
|
+ return Map.of("sign_url", body != null ? body : "");
|
|
|
} catch (AlipayApiException e) {
|
|
} catch (AlipayApiException e) {
|
|
|
- throw new BusinessException(400, "申请转账授权失败: " + e.getMessage());
|
|
|
|
|
|
|
+ throw new BusinessException(400, "申请签约失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 对应 Python create_account_service */
|
|
|
|
|
|
|
+ // ==================== 2. 开通记账本 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * alipay.fund.accountbook.create
|
|
|
|
|
+ * 必填: merchant_user_id, merchant_user_type, scene_code
|
|
|
|
|
+ * 选填: ext_info (含 agreement_no)
|
|
|
|
|
+ */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
- public Map<String, String> createAccount(String enterpriseId) {
|
|
|
|
|
|
|
+ public Map<String, String> createAccount(String enterpriseId, String agreementNo) {
|
|
|
try {
|
|
try {
|
|
|
- AlipayCommerceEcTransAccountCreateModel model = new AlipayCommerceEcTransAccountCreateModel();
|
|
|
|
|
- model.setEnterpriseId(enterpriseId);
|
|
|
|
|
- model.setAccountType("ALL");
|
|
|
|
|
- model.setScene("B2B_TRANS");
|
|
|
|
|
|
|
+ AlipayFundAccountbookCreateModel model = new AlipayFundAccountbookCreateModel();
|
|
|
|
|
+ model.setMerchantUserId(enterpriseId);
|
|
|
|
|
+ model.setMerchantUserType("BUSINESS_ORGANIZATION");
|
|
|
|
|
+ model.setSceneCode(SCENE_CODE_ACCOUNT_BOOK);
|
|
|
|
|
+
|
|
|
|
|
+ if (agreementNo != null && !agreementNo.isBlank()) {
|
|
|
|
|
+ Map<String, String> extInfo = new LinkedHashMap<>();
|
|
|
|
|
+ extInfo.put("agreement_no", agreementNo);
|
|
|
|
|
+ try { model.setExtInfo(objectMapper.writeValueAsString(extInfo)); }
|
|
|
|
|
+ catch (Exception ignored) {}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountCreateRequest request = new AlipayCommerceEcTransAccountCreateRequest();
|
|
|
|
|
|
|
+ AlipayFundAccountbookCreateRequest request = new AlipayFundAccountbookCreateRequest();
|
|
|
request.setBizModel(model);
|
|
request.setBizModel(model);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountCreateResponse response =
|
|
|
|
|
|
|
+ AlipayFundAccountbookCreateResponse response =
|
|
|
alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
|
|
|
|
|
|
if (!response.isSuccess())
|
|
if (!response.isSuccess())
|
|
|
- throw new BusinessException(400, "开通资金专户失败: " + response.getMsg());
|
|
|
|
|
|
|
+ throw new BusinessException(400, "开通资金记账本失败: " + response.getMsg());
|
|
|
|
|
|
|
|
AccountEntity account = new AccountEntity();
|
|
AccountEntity account = new AccountEntity();
|
|
|
account.setEnterpriseId(enterpriseId);
|
|
account.setEnterpriseId(enterpriseId);
|
|
|
account.setAccountBookId(response.getAccountBookId());
|
|
account.setAccountBookId(response.getAccountBookId());
|
|
|
- account.setAccountType(model.getAccountType());
|
|
|
|
|
- account.setScene(model.getScene());
|
|
|
|
|
|
|
+ account.setAccountType("ALL");
|
|
|
|
|
+ account.setScene("B2B_TRANS");
|
|
|
account.setStatus("ACTIVE");
|
|
account.setStatus("ACTIVE");
|
|
|
- account.setSignStatus("PENDING");
|
|
|
|
|
|
|
+ account.setSignStatus(agreementNo != null ? "SIGNED" : "PENDING");
|
|
|
|
|
+ account.setAgreementNo(agreementNo);
|
|
|
accountMapper.insert(account);
|
|
accountMapper.insert(account);
|
|
|
|
|
|
|
|
return Map.of("enterprise_id", enterpriseId,
|
|
return Map.of("enterprise_id", enterpriseId,
|
|
|
"account_book_id", response.getAccountBookId() != null ? response.getAccountBookId() : "");
|
|
"account_book_id", response.getAccountBookId() != null ? response.getAccountBookId() : "");
|
|
|
} catch (AlipayApiException e) {
|
|
} catch (AlipayApiException e) {
|
|
|
- throw new BusinessException(400, "开通资金专户失败: " + e.getMessage());
|
|
|
|
|
|
|
+ throw new BusinessException(400, "开通资金记账本失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 对应 Python deposit_service — 充值并保存记录到 DB */
|
|
|
|
|
|
|
+ // ==================== 3. 充值(页面跳转) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * alipay.fund.trans.page.pay — 页面跳转接口
|
|
|
|
|
+ * 必填: out_biz_no, trans_amount, product_code, biz_scene, payee_info
|
|
|
|
|
+ */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public Map<String, String> deposit(String enterpriseId, String accountBookId, BigDecimal amount, String remark) {
|
|
public Map<String, String> deposit(String enterpriseId, String accountBookId, BigDecimal amount, String remark) {
|
|
|
try {
|
|
try {
|
|
|
- AlipayCommerceEcTransAccountDepositModel model = new AlipayCommerceEcTransAccountDepositModel();
|
|
|
|
|
- model.setEnterpriseId(enterpriseId);
|
|
|
|
|
- model.setAccountBookId(accountBookId);
|
|
|
|
|
- model.setAmount(amount.toPlainString());
|
|
|
|
|
|
|
+ AlipayFundTransPagePayModel model = new AlipayFundTransPagePayModel();
|
|
|
model.setOutBizNo(SnowflakeIdGenerator.nextIdStr());
|
|
model.setOutBizNo(SnowflakeIdGenerator.nextIdStr());
|
|
|
|
|
+ model.setTransAmount(amount.toPlainString());
|
|
|
|
|
+ model.setProductCode(PRODUCT_CODE);
|
|
|
|
|
+ model.setBizScene(BIZ_SCENE_ALLOCATION);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountDepositRequest request = new AlipayCommerceEcTransAccountDepositRequest();
|
|
|
|
|
|
|
+ Participant payee = new Participant();
|
|
|
|
|
+ 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);
|
|
request.setBizModel(model);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountDepositResponse response =
|
|
|
|
|
- alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
|
|
|
|
|
+ AlipayFundTransPagePayResponse response =
|
|
|
|
|
+ alipayClientFactory.getClient(enterpriseId).pageExecute(request);
|
|
|
|
|
|
|
|
- if (!response.isSuccess())
|
|
|
|
|
- throw new BusinessException(400, "充值失败: " + response.getMsg());
|
|
|
|
|
|
|
+ if (response == null || response.getBody() == null)
|
|
|
|
|
+ throw new BusinessException(400, "充值页面生成失败: 无响应");
|
|
|
|
|
|
|
|
- // 保存充值记录(对应 Python deposit_service L308-318)
|
|
|
|
|
DepositEntity deposit = new DepositEntity();
|
|
DepositEntity deposit = new DepositEntity();
|
|
|
deposit.setEnterpriseId(enterpriseId);
|
|
deposit.setEnterpriseId(enterpriseId);
|
|
|
deposit.setOutBizNo(model.getOutBizNo());
|
|
deposit.setOutBizNo(model.getOutBizNo());
|
|
|
deposit.setAccountBookId(accountBookId);
|
|
deposit.setAccountBookId(accountBookId);
|
|
|
deposit.setAmount(amount);
|
|
deposit.setAmount(amount);
|
|
|
- deposit.setUrl(response.getUrl());
|
|
|
|
|
|
|
+ deposit.setUrl(response.getBody());
|
|
|
deposit.setStatus("DEALING");
|
|
deposit.setStatus("DEALING");
|
|
|
deposit.setRemark(remark);
|
|
deposit.setRemark(remark);
|
|
|
depositMapper.insert(deposit);
|
|
depositMapper.insert(deposit);
|
|
|
|
|
|
|
|
- return Map.of("url", response.getUrl() != null ? response.getUrl() : "");
|
|
|
|
|
|
|
+ return Map.of("url", response.getBody());
|
|
|
} catch (AlipayApiException e) {
|
|
} catch (AlipayApiException e) {
|
|
|
throw new BusinessException(400, "充值失败: " + e.getMessage());
|
|
throw new BusinessException(400, "充值失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 4. 转账 ====================
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 对应 Python transfer_service — 资金专户转账,完整逻辑
|
|
|
|
|
- *
|
|
|
|
|
- * 关键差异说明:
|
|
|
|
|
- * - Python FAIL 分支使用独立 DB session 写入失败记录,
|
|
|
|
|
- * 本方法将 FAIL 写入拆分到 internalSaveFailedTransfer(),
|
|
|
|
|
- * 该子方法用 REQUIRES_NEW 独立事务保证失败记录不被外层回滚吞掉。
|
|
|
|
|
|
|
+ * alipay.fund.trans.uni.transfer
|
|
|
|
|
+ * 必填: out_biz_no, trans_amount, biz_scene, product_code, payee_info, payer_info
|
|
|
|
|
+ * 选填: order_title, remark, business_params
|
|
|
*/
|
|
*/
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public Map<String, Object> transfer(TransferCreateDTO dto) {
|
|
public Map<String, Object> transfer(TransferCreateDTO dto) {
|
|
|
String accountBookId = dto.getAccountBookId();
|
|
String accountBookId = dto.getAccountBookId();
|
|
@@ -196,86 +246,103 @@ public class AlipayTransferService {
|
|
|
Map<String, Object> payeeInfo = dto.getPayeeInfo();
|
|
Map<String, Object> payeeInfo = dto.getPayeeInfo();
|
|
|
Map<String, Object> extInfo = dto.getExtInfo();
|
|
Map<String, Object> extInfo = dto.getExtInfo();
|
|
|
|
|
|
|
|
- // 检查资金专户(对应 Python account/service.py L351-358)
|
|
|
|
|
AccountEntity account = accountMapper.selectOne(
|
|
AccountEntity account = accountMapper.selectOne(
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AccountEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AccountEntity>()
|
|
|
.eq(AccountEntity::getAccountBookId, accountBookId));
|
|
.eq(AccountEntity::getAccountBookId, accountBookId));
|
|
|
- if (account == null) throw new BusinessException(400, "资金账户不存在");
|
|
|
|
|
|
|
+ if (account == null) throw new BusinessException(400, "资金记账本不存在");
|
|
|
|
|
|
|
|
- // 租户范围校验 — 对应 Python L355-356: if account.tenant_id != auth.tenant_id
|
|
|
|
|
- // dto.getTenantId() 为 null 时跳过(admin 端点未注入租户ID,保持向前兼容)
|
|
|
|
|
- if (dto.getTenantId() != null && !dto.getTenantId().equals(account.getTenantId())) {
|
|
|
|
|
|
|
+ if (dto.getTenantId() != null && !dto.getTenantId().equals(account.getTenantId()))
|
|
|
throw new BusinessException(403, "无权限操作");
|
|
throw new BusinessException(403, "无权限操作");
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
if (enterpriseId != null && !enterpriseId.equals(account.getEnterpriseId()))
|
|
if (enterpriseId != null && !enterpriseId.equals(account.getEnterpriseId()))
|
|
|
throw new BusinessException(400, "参数错误");
|
|
throw new BusinessException(400, "参数错误");
|
|
|
|
|
|
|
|
- // 自动生成转账标题
|
|
|
|
|
if ((orderTitle == null || orderTitle.isBlank()) && account.getEnterpriseId() != null) {
|
|
if ((orderTitle == null || orderTitle.isBlank()) && account.getEnterpriseId() != null) {
|
|
|
EnterpriseEntity ent = enterpriseMapper.selectOne(
|
|
EnterpriseEntity ent = enterpriseMapper.selectOne(
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EnterpriseEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EnterpriseEntity>()
|
|
|
.eq(EnterpriseEntity::getEnterpriseId, account.getEnterpriseId()));
|
|
.eq(EnterpriseEntity::getEnterpriseId, account.getEnterpriseId()));
|
|
|
- if (ent == null) throw new BusinessException(400, "资金账户所属企业不存在");
|
|
|
|
|
|
|
+ if (ent == null) throw new BusinessException(400, "记账本所属企业不存在");
|
|
|
if (ent.getName() != null) orderTitle = "来自" + ent.getName() + "转账";
|
|
if (ent.getName() != null) orderTitle = "来自" + ent.getName() + "转账";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- AlipayCommerceEcTransAccountTransferModel model = new AlipayCommerceEcTransAccountTransferModel();
|
|
|
|
|
- model.setEnterpriseId(account.getEnterpriseId());
|
|
|
|
|
- model.setAccountBookId(account.getAccountBookId());
|
|
|
|
|
|
|
+ AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
|
|
|
model.setOutBizNo(SnowflakeIdGenerator.nextIdStr());
|
|
model.setOutBizNo(SnowflakeIdGenerator.nextIdStr());
|
|
|
- model.setAmount(amount.toPlainString());
|
|
|
|
|
|
|
+ model.setTransAmount(amount.toPlainString());
|
|
|
|
|
+ model.setBizScene(BIZ_SCENE_TRANSFER);
|
|
|
|
|
+ model.setProductCode(PRODUCT_CODE);
|
|
|
model.setOrderTitle(orderTitle);
|
|
model.setOrderTitle(orderTitle);
|
|
|
|
|
+ if (remark != null && !remark.isBlank()) model.setRemark(remark);
|
|
|
|
|
+
|
|
|
|
|
+ // 必填: 付款方(记账本)
|
|
|
|
|
+ Participant payer = new Participant();
|
|
|
|
|
+ payer.setIdentityType("ACCOUNT_BOOK_ID");
|
|
|
|
|
+ payer.setIdentity(account.getAccountBookId());
|
|
|
|
|
+ if (account.getAgreementNo() != null && !account.getAgreementNo().isBlank()) {
|
|
|
|
|
+ Map<String, String> payerExt = new LinkedHashMap<>();
|
|
|
|
|
+ payerExt.put("agreement_no", account.getAgreementNo());
|
|
|
|
|
+ try { payer.setExtInfo(objectMapper.writeValueAsString(payerExt)); }
|
|
|
|
|
+ catch (Exception ignored) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ model.setPayerInfo(payer);
|
|
|
|
|
|
|
|
- TransParticipant payee = new TransParticipant();
|
|
|
|
|
|
|
+ // 必填: 收款方
|
|
|
|
|
+ Participant payee = new Participant();
|
|
|
payee.setIdentityType((String) payeeInfo.get("identity_type"));
|
|
payee.setIdentityType((String) payeeInfo.get("identity_type"));
|
|
|
payee.setName((String) payeeInfo.get("name"));
|
|
payee.setName((String) payeeInfo.get("name"));
|
|
|
payee.setIdentity((String) payeeInfo.get("identity"));
|
|
payee.setIdentity((String) payeeInfo.get("identity"));
|
|
|
|
|
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
|
|
|
|
+ // 选填: 银行卡扩展
|
|
|
Map<String, Object> bankcardInfo = (Map<String, Object>) payeeInfo.get("bankcard_ext_info");
|
|
Map<String, Object> bankcardInfo = (Map<String, Object>) payeeInfo.get("bankcard_ext_info");
|
|
|
if (bankcardInfo != null) {
|
|
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) { /* SDK 类不存在则跳过 */ }
|
|
|
|
|
|
|
+ BankcardExtInfo bce = new BankcardExtInfo();
|
|
|
|
|
+ Class<?> bceClass = bce.getClass();
|
|
|
|
|
+ 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.setBankcardExtInfo(bce);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
model.setPayeeInfo(payee);
|
|
model.setPayeeInfo(payee);
|
|
|
- if (remark != null && !remark.isBlank()) model.setRemark(remark);
|
|
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountTransferRequest request = new AlipayCommerceEcTransAccountTransferRequest();
|
|
|
|
|
|
|
+ // 选填: 转账场景信息(通过 businessParams JSON 透传)
|
|
|
|
|
+ if (extInfo != null && (extInfo.containsKey("transfer_scene_name")
|
|
|
|
|
+ || extInfo.containsKey("transfer_scene_report_infos"))) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Map<String, Object> bizParams = new LinkedHashMap<>();
|
|
|
|
|
+ if (extInfo.containsKey("transfer_scene_name"))
|
|
|
|
|
+ bizParams.put("transfer_scene_name", extInfo.get("transfer_scene_name"));
|
|
|
|
|
+ if (extInfo.containsKey("transfer_scene_report_infos"))
|
|
|
|
|
+ bizParams.put("transfer_scene_report_infos", extInfo.get("transfer_scene_report_infos"));
|
|
|
|
|
+ model.setBusinessParams(objectMapper.writeValueAsString(bizParams));
|
|
|
|
|
+ } catch (Exception ignored) {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
|
|
|
request.setBizModel(model);
|
|
request.setBizModel(model);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountTransferResponse response =
|
|
|
|
|
|
|
+ AlipayFundTransUniTransferResponse response =
|
|
|
alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
|
|
|
|
|
|
String subCode = response.getSubCode() != null ? response.getSubCode() : "";
|
|
String subCode = response.getSubCode() != null ? response.getSubCode() : "";
|
|
|
String subMsg = response.getSubMsg() != null ? response.getSubMsg() : "";
|
|
String subMsg = response.getSubMsg() != null ? response.getSubMsg() : "";
|
|
|
|
|
|
|
|
- // 构建转账记录
|
|
|
|
|
TransferEntity transfer = new TransferEntity();
|
|
TransferEntity transfer = new TransferEntity();
|
|
|
- transfer.setEnterpriseId(model.getEnterpriseId());
|
|
|
|
|
|
|
+ transfer.setEnterpriseId(account.getEnterpriseId());
|
|
|
transfer.setOutBizNo(model.getOutBizNo());
|
|
transfer.setOutBizNo(model.getOutBizNo());
|
|
|
- transfer.setAccountBookId(model.getAccountBookId());
|
|
|
|
|
- transfer.setAmount(new BigDecimal(model.getAmount()));
|
|
|
|
|
|
|
+ transfer.setAccountBookId(account.getAccountBookId());
|
|
|
|
|
+ transfer.setAmount(new BigDecimal(model.getTransAmount()));
|
|
|
transfer.setOrderTitle(model.getOrderTitle());
|
|
transfer.setOrderTitle(model.getOrderTitle());
|
|
|
transfer.setPayeeInfo(toJson(payeeInfo));
|
|
transfer.setPayeeInfo(toJson(payeeInfo));
|
|
|
transfer.setPayeeType((String) payeeInfo.get("identity_type"));
|
|
transfer.setPayeeType((String) payeeInfo.get("identity_type"));
|
|
|
transfer.setStatus(response.getStatus());
|
|
transfer.setStatus(response.getStatus());
|
|
|
- transfer.setOrderNo(response.getOrderNo());
|
|
|
|
|
- transfer.setFundOrderId(response.getFundOrderId());
|
|
|
|
|
|
|
+ transfer.setOrderNo(response.getOrderId());
|
|
|
|
|
+ transfer.setFundOrderId(response.getPayFundOrderId());
|
|
|
transfer.setRemark(remark != null ? remark : "");
|
|
transfer.setRemark(remark != null ? remark : "");
|
|
|
if (extInfo != null) transfer.setExtInfo(toJson(extInfo));
|
|
if (extInfo != null) transfer.setExtInfo(toJson(extInfo));
|
|
|
|
|
|
|
|
if (!response.isSuccess()) {
|
|
if (!response.isSuccess()) {
|
|
|
- // 构建友好提示(对应 Python L426-433)
|
|
|
|
|
String hint = TRANSFER_ERROR_HINTS.get(subCode);
|
|
String hint = TRANSFER_ERROR_HINTS.get(subCode);
|
|
|
if (hint == null) {
|
|
if (hint == null) {
|
|
|
for (var entry : TRANSFER_ERROR_HINTS.entrySet()) {
|
|
for (var entry : TRANSFER_ERROR_HINTS.entrySet()) {
|
|
@@ -284,18 +351,14 @@ public class AlipayTransferService {
|
|
|
}
|
|
}
|
|
|
if (hint == null) hint = !subMsg.isEmpty() ? subMsg : response.getMsg();
|
|
if (hint == null) hint = !subMsg.isEmpty() ? subMsg : response.getMsg();
|
|
|
|
|
|
|
|
- log.error("支付宝转账失败: code={}, msg={}, sub_code={}, sub_msg={}", response.getCode(), response.getMsg(), subCode, subMsg);
|
|
|
|
|
|
|
+ log.error("支付宝转账失败: code={}, msg={}, sub_code={}, sub_msg={}",
|
|
|
|
|
+ response.getCode(), response.getMsg(), subCode, subMsg);
|
|
|
|
|
|
|
|
- // 独立事务写入失败记录(对应 Python L438-447)
|
|
|
|
|
- // Python: async with async_db_session() as _session: async with _session.begin():
|
|
|
|
|
- // 这里在 FAIL 时不调用 insert(当前事务会回滚),而是通过 self 代理调用独立事务方法
|
|
|
|
|
self.insertFailedTransfer(transfer, response.getMsg(), subCode, subMsg,
|
|
self.insertFailedTransfer(transfer, response.getMsg(), subCode, subMsg,
|
|
|
remark != null ? remark : "");
|
|
remark != null ? remark : "");
|
|
|
-
|
|
|
|
|
throw new BusinessException(400, "转账失败: " + hint);
|
|
throw new BusinessException(400, "转账失败: " + hint);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // DEALING 状态设置重试
|
|
|
|
|
if ("DEALING".equals(response.getStatus())) {
|
|
if ("DEALING".equals(response.getStatus())) {
|
|
|
transfer.setRetryCount(0);
|
|
transfer.setRetryCount(0);
|
|
|
transfer.setNextRetryAt(OffsetDateTime.now().plusMinutes(5));
|
|
transfer.setNextRetryAt(OffsetDateTime.now().plusMinutes(5));
|
|
@@ -305,118 +368,95 @@ public class AlipayTransferService {
|
|
|
|
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
result.put("status", response.getStatus());
|
|
result.put("status", response.getStatus());
|
|
|
- result.put("order_no", response.getOrderNo());
|
|
|
|
|
- result.put("fund_order_id", response.getFundOrderId());
|
|
|
|
|
|
|
+ result.put("order_id", response.getOrderId());
|
|
|
|
|
+ result.put("pay_fund_order_id", response.getPayFundOrderId());
|
|
|
result.put("out_biz_no", model.getOutBizNo());
|
|
result.put("out_biz_no", model.getOutBizNo());
|
|
|
return result;
|
|
return result;
|
|
|
-
|
|
|
|
|
} catch (AlipayApiException e) {
|
|
} catch (AlipayApiException e) {
|
|
|
throw new BusinessException(400, "转账失败: " + e.getMessage());
|
|
throw new BusinessException(400, "转账失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 5. 查询记账本 ====================
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 独立事务写入 FAIL 记录,对应 Python 的 async with async_db_session() 开始独立事务
|
|
|
|
|
- *
|
|
|
|
|
- * 注意: 必须通过 self 代理调用才能让 Spring AOP 拦截到 @Transactional(REQUIRES_NEW)
|
|
|
|
|
|
|
+ * alipay.fund.accountbook.query
|
|
|
|
|
+ * 必填: merchant_user_id, scene_code
|
|
|
*/
|
|
*/
|
|
|
- @Transactional(propagation = Propagation.REQUIRES_NEW)
|
|
|
|
|
- public void insertFailedTransfer(TransferEntity transfer, String alipayMsg,
|
|
|
|
|
- String subCode, String subMsg, String userRemark) {
|
|
|
|
|
- transfer.setStatus("FAIL");
|
|
|
|
|
- // 对应 Python: transfer_data["remark"] = f"{result.msg} ({sub_code} {sub_msg})"
|
|
|
|
|
- transfer.setRemark(alipayMsg + " (" + subCode + " " + subMsg + ")");
|
|
|
|
|
- transfer.setErrorCode(subCode);
|
|
|
|
|
- transfer.setErrorMsg(subMsg);
|
|
|
|
|
- transferMapper.insert(transfer);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /** 对应 Python withdraw_service — 提现并保存记录到 DB */
|
|
|
|
|
- @Transactional
|
|
|
|
|
- public Map<String, String> withdraw(String enterpriseId, String accountBookId, BigDecimal amount, String remark) {
|
|
|
|
|
- // 校验企业存在(对应 Python L599-603)
|
|
|
|
|
- EnterpriseEntity enterprise = enterpriseMapper.selectOne(
|
|
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EnterpriseEntity>()
|
|
|
|
|
- .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
|
|
|
|
|
- if (enterprise == null) throw new BusinessException(400, "企业不存在");
|
|
|
|
|
-
|
|
|
|
|
|
|
+ public List<Map<String, Object>> queryAccounts(String enterpriseId) {
|
|
|
try {
|
|
try {
|
|
|
- AlipayCommerceEcTransAccountWithdrawModel model = new AlipayCommerceEcTransAccountWithdrawModel();
|
|
|
|
|
- model.setEnterpriseId(enterpriseId);
|
|
|
|
|
- model.setAccountBookId(accountBookId);
|
|
|
|
|
- model.setAmount(amount.toPlainString());
|
|
|
|
|
- model.setOutBizNo(SnowflakeIdGenerator.nextIdStr());
|
|
|
|
|
- if (remark != null && !remark.isBlank()) model.setRemark(remark);
|
|
|
|
|
|
|
+ AlipayFundAccountbookQueryModel model = new AlipayFundAccountbookQueryModel();
|
|
|
|
|
+ model.setMerchantUserId(enterpriseId);
|
|
|
|
|
+ model.setSceneCode(SCENE_CODE_ACCOUNT_BOOK);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountWithdrawRequest request = new AlipayCommerceEcTransAccountWithdrawRequest();
|
|
|
|
|
|
|
+ AlipayFundAccountbookQueryRequest request = new AlipayFundAccountbookQueryRequest();
|
|
|
request.setBizModel(model);
|
|
request.setBizModel(model);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountWithdrawResponse response =
|
|
|
|
|
|
|
+ AlipayFundAccountbookQueryResponse response =
|
|
|
alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
|
|
|
|
|
|
if (!response.isSuccess())
|
|
if (!response.isSuccess())
|
|
|
- throw new BusinessException(400, "提现失败: " + response.getMsg());
|
|
|
|
|
-
|
|
|
|
|
- // 保存提现记录(对应 Python L629-640)
|
|
|
|
|
- WithdrawEntity withdraw = new WithdrawEntity();
|
|
|
|
|
- withdraw.setEnterpriseId(enterpriseId);
|
|
|
|
|
- withdraw.setOutBizNo(model.getOutBizNo());
|
|
|
|
|
- withdraw.setAccountBookId(accountBookId);
|
|
|
|
|
- withdraw.setAmount(amount);
|
|
|
|
|
- // 专户提现到余额户是同步操作,状态直接记 SUCCESS
|
|
|
|
|
- withdraw.setStatus("SUCCESS");
|
|
|
|
|
- withdraw.setOrderNo(response.getOrderNo());
|
|
|
|
|
- withdraw.setRemark(remark);
|
|
|
|
|
- withdrawMapper.insert(withdraw);
|
|
|
|
|
|
|
+ throw new BusinessException(400, "查询记账本失败: " + response.getMsg());
|
|
|
|
|
|
|
|
- log.info("资金专户提现发起成功: 企业: {}, 金额: {}", enterpriseId, amount);
|
|
|
|
|
-
|
|
|
|
|
- return Map.of("enterprise_id", enterpriseId, "account_book_id", accountBookId);
|
|
|
|
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
|
|
+ m.put("account_book_id", response.getAccountBookId());
|
|
|
|
|
+ m.put("available_amount", response.getAvailableAmount());
|
|
|
|
|
+ m.put("scene", "B2B_TRANS");
|
|
|
|
|
+ result.add(m);
|
|
|
|
|
+ return result;
|
|
|
} catch (AlipayApiException e) {
|
|
} catch (AlipayApiException e) {
|
|
|
- throw new BusinessException(400, "提现失败: " + e.getMessage());
|
|
|
|
|
|
|
+ throw new BusinessException(400, "查询记账本失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 对应 Python query_account_service — 仅返回 B2B_TRANS 场景的账户 */
|
|
|
|
|
- public List<Map<String, Object>> queryAccounts(String enterpriseId) {
|
|
|
|
|
|
|
+ // ==================== 6. 查询转账状态 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * alipay.fund.trans.common.query
|
|
|
|
|
+ * orderId / outBizNo 二选一必填
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, String> queryTransferStatus(String orderId, String outBizNo) {
|
|
|
try {
|
|
try {
|
|
|
- AlipayCommerceEcTransAccountQueryModel model = new AlipayCommerceEcTransAccountQueryModel();
|
|
|
|
|
- model.setEnterpriseId(enterpriseId);
|
|
|
|
|
|
|
+ AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
|
|
|
|
|
+ model.setProductCode(PRODUCT_CODE);
|
|
|
|
|
+ model.setBizScene(BIZ_SCENE_TRANSFER);
|
|
|
|
|
+ if (orderId != null && !orderId.isBlank()) {
|
|
|
|
|
+ model.setOrderId(orderId);
|
|
|
|
|
+ } else if (outBizNo != null && !outBizNo.isBlank()) {
|
|
|
|
|
+ model.setOutBizNo(outBizNo);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return Map.of("status", "UNKNOWN", "error", "order_id 和 out_biz_no 不能同时为空");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountQueryRequest request = new AlipayCommerceEcTransAccountQueryRequest();
|
|
|
|
|
|
|
+ AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
|
|
|
request.setBizModel(model);
|
|
request.setBizModel(model);
|
|
|
|
|
|
|
|
- AlipayCommerceEcTransAccountQueryResponse response =
|
|
|
|
|
- alipayClientFactory.getClient(enterpriseId).execute(request);
|
|
|
|
|
-
|
|
|
|
|
- if (!response.isSuccess())
|
|
|
|
|
- throw new BusinessException(400, "查询资金专户失败: " + response.getMsg());
|
|
|
|
|
|
|
+ AlipayClient client = alipayClientFactory.getClient();
|
|
|
|
|
+ AlipayFundTransCommonQueryResponse response = client.execute(request);
|
|
|
|
|
|
|
|
- List<Map<String, Object>> result = new ArrayList<>();
|
|
|
|
|
- if (response.getAccountList() != null) {
|
|
|
|
|
- for (var account : response.getAccountList()) {
|
|
|
|
|
- // 仅返回 B2B_TRANS 场景的账户(对应 Python L696-698)
|
|
|
|
|
- String scene;
|
|
|
|
|
- try { scene = (String) account.getClass().getMethod("getScene").invoke(account); }
|
|
|
|
|
- catch (Exception e) { continue; }
|
|
|
|
|
- if (!"B2B_TRANS".equals(scene)) continue;
|
|
|
|
|
-
|
|
|
|
|
- Map<String, Object> m = new LinkedHashMap<>();
|
|
|
|
|
- try {
|
|
|
|
|
- m.put("account_book_id", account.getClass().getMethod("getAccountBookId").invoke(account));
|
|
|
|
|
- m.put("scene", scene);
|
|
|
|
|
- m.put("status", account.getClass().getMethod("getStatus").invoke(account));
|
|
|
|
|
- result.add(m);
|
|
|
|
|
- } catch (Exception ignored) {}
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!response.isSuccess()) {
|
|
|
|
|
+ log.warn("查询转账状态失败: code={}, msg={}", response.getCode(), response.getMsg());
|
|
|
|
|
+ return Map.of("status", "UNKNOWN", "error",
|
|
|
|
|
+ response.getMsg() != null ? response.getMsg() : "unknown");
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ 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;
|
|
return result;
|
|
|
} catch (AlipayApiException e) {
|
|
} catch (AlipayApiException e) {
|
|
|
- throw new BusinessException(400, "查询资金专户失败: " + e.getMessage());
|
|
|
|
|
|
|
+ log.error("查询转账状态异常", e);
|
|
|
|
|
+ return Map.of("status", "UNKNOWN", "error", e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 对应 Python transfer_sync_status_service — 手动同步转账状态 */
|
|
|
|
|
|
|
+ // ==================== 7. 手动同步状态 ====================
|
|
|
|
|
+
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public Map<String, Object> syncStatus(String outBizNo, String status, String errorCode, String errorMsg) {
|
|
public Map<String, Object> syncStatus(String outBizNo, String status, String errorCode, String errorMsg) {
|
|
|
TransferEntity transfer = transferMapper.selectOne(
|
|
TransferEntity transfer = transferMapper.selectOne(
|
|
@@ -424,10 +464,8 @@ public class AlipayTransferService {
|
|
|
.eq(TransferEntity::getOutBizNo, outBizNo));
|
|
.eq(TransferEntity::getOutBizNo, outBizNo));
|
|
|
if (transfer == null) throw new BusinessException(404, "转账记录不存在: " + outBizNo);
|
|
if (transfer == null) throw new BusinessException(404, "转账记录不存在: " + outBizNo);
|
|
|
|
|
|
|
|
- // 对应 Python L965-966: DEALING 且目标状态为 SUCCESS 时才允许同步
|
|
|
|
|
- if (!"DEALING".equals(transfer.getStatus())) {
|
|
|
|
|
|
|
+ if (!"DEALING".equals(transfer.getStatus()))
|
|
|
throw new BusinessException(400, "转账记录当前状态为 " + transfer.getStatus() + ",无需同步");
|
|
throw new BusinessException(400, "转账记录当前状态为 " + transfer.getStatus() + ",无需同步");
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
transfer.setStatus(status);
|
|
transfer.setStatus(status);
|
|
|
if (errorCode != null) transfer.setErrorCode(errorCode);
|
|
if (errorCode != null) transfer.setErrorCode(errorCode);
|
|
@@ -442,31 +480,85 @@ public class AlipayTransferService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 8. 提现(保留旧实现) ====================
|
|
|
|
|
|
|
|
- // ─────────── 退避时间映射 ───────────
|
|
|
|
|
- private static final int[] RETRY_INTERVALS = {5, 10, 45, 60}; // 累计: +5min, +15min, +1h, +2h
|
|
|
|
|
|
|
+ /** @deprecated 安全发模式暂不提供独立提现接口 */
|
|
|
|
|
+ @Deprecated
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public Map<String, String> withdraw(String enterpriseId, String accountBookId, BigDecimal amount, String remark) {
|
|
|
|
|
+ EnterpriseEntity enterprise = enterpriseMapper.selectOne(
|
|
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EnterpriseEntity>()
|
|
|
|
|
+ .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
|
|
|
|
|
+ if (enterprise == null) throw new BusinessException(400, "企业不存在");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ Class<?> modelClass = Class.forName("com.alipay.api.domain.AlipayCommerceEcTransAccountWithdrawModel");
|
|
|
|
|
+ Class<?> requestClass = Class.forName("com.alipay.api.request.AlipayCommerceEcTransAccountWithdrawRequest");
|
|
|
|
|
+ Class<?> responseClass = Class.forName("com.alipay.api.response.AlipayCommerceEcTransAccountWithdrawResponse");
|
|
|
|
|
+
|
|
|
|
|
+ Object model = modelClass.getDeclaredConstructor().newInstance();
|
|
|
|
|
+ modelClass.getMethod("setEnterpriseId", String.class).invoke(model, enterpriseId);
|
|
|
|
|
+ modelClass.getMethod("setAccountBookId", String.class).invoke(model, accountBookId);
|
|
|
|
|
+ modelClass.getMethod("setAmount", String.class).invoke(model, amount.toPlainString());
|
|
|
|
|
+ modelClass.getMethod("setOutBizNo", String.class).invoke(model, SnowflakeIdGenerator.nextIdStr());
|
|
|
|
|
+ if (remark != null && !remark.isBlank())
|
|
|
|
|
+ modelClass.getMethod("setRemark", String.class).invoke(model, remark);
|
|
|
|
|
+
|
|
|
|
|
+ Object request = requestClass.getDeclaredConstructor().newInstance();
|
|
|
|
|
+ requestClass.getMethod("setBizModel", modelClass).invoke(request, model);
|
|
|
|
|
+
|
|
|
|
|
+ Object response = alipayClientFactory.getClient(enterpriseId)
|
|
|
|
|
+ .execute((com.alipay.api.AlipayRequest) request);
|
|
|
|
|
+
|
|
|
|
|
+ boolean isSuccess = (boolean) responseClass.getMethod("isSuccess").invoke(response);
|
|
|
|
|
+ if (!isSuccess)
|
|
|
|
|
+ throw new BusinessException(400, "提现失败: " + responseClass.getMethod("getMsg").invoke(response));
|
|
|
|
|
+
|
|
|
|
|
+ WithdrawEntity withdraw = new WithdrawEntity();
|
|
|
|
|
+ withdraw.setEnterpriseId(enterpriseId);
|
|
|
|
|
+ withdraw.setOutBizNo((String) modelClass.getMethod("getOutBizNo").invoke(model));
|
|
|
|
|
+ withdraw.setAccountBookId(accountBookId);
|
|
|
|
|
+ withdraw.setAmount(amount);
|
|
|
|
|
+ withdraw.setStatus("SUCCESS");
|
|
|
|
|
+ withdraw.setOrderNo((String) responseClass.getMethod("getOrderNo").invoke(response));
|
|
|
|
|
+ withdraw.setRemark(remark);
|
|
|
|
|
+ withdrawMapper.insert(withdraw);
|
|
|
|
|
|
|
|
- // 分布式锁保证多实例不重复执行 — 对应 Python pg_try_advisory_lock(99999)
|
|
|
|
|
|
|
+ log.info("资金记账本提现发起成功: 企业: {}, 金额: {}", enterpriseId, amount);
|
|
|
|
|
+ return Map.of("enterprise_id", enterpriseId, "account_book_id", accountBookId);
|
|
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
|
|
+ throw new BusinessException(400, "旧版提现接口已不可用,请使用 uni.transfer");
|
|
|
|
|
+ } catch (AlipayApiException e) {
|
|
|
|
|
+ throw new BusinessException(400, "提现失败: " + e.getMessage());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new BusinessException(400, "提现失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 独立事务 FAIL 记录 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
|
|
|
|
|
+ public void insertFailedTransfer(TransferEntity transfer, String alipayMsg,
|
|
|
|
|
+ String subCode, String subMsg, String userRemark) {
|
|
|
|
|
+ transfer.setStatus("FAIL");
|
|
|
|
|
+ transfer.setRemark(alipayMsg + " (" + subCode + " " + subMsg + ")");
|
|
|
|
|
+ transfer.setErrorCode(subCode);
|
|
|
|
|
+ transfer.setErrorMsg(subMsg);
|
|
|
|
|
+ transferMapper.insert(transfer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 反查重试定时任务 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private static final int[] RETRY_INTERVALS = {5, 10, 45, 60};
|
|
|
private static final String RETRY_LOCK_KEY = "retry:dealing_transfers";
|
|
private static final String RETRY_LOCK_KEY = "retry:dealing_transfers";
|
|
|
- private static final long RETRY_LOCK_TTL = 120; // 秒,进程崩溃后自动释放
|
|
|
|
|
|
|
+ private static final long RETRY_LOCK_TTL = 120;
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 反查转账状态定时任务 — 对应 Python retry_dealing_transfers (L1128-1230)
|
|
|
|
|
- *
|
|
|
|
|
- * 每分钟执行一次,使用 AtomicBoolean 防重入(单实例)。
|
|
|
|
|
- * 多实例部署时需替换为 PostgreSQL advisory lock (pg_try_advisory_lock)。
|
|
|
|
|
- */
|
|
|
|
|
@org.springframework.scheduling.annotation.Scheduled(fixedDelay = 60_000, initialDelay = 5_000)
|
|
@org.springframework.scheduling.annotation.Scheduled(fixedDelay = 60_000, initialDelay = 5_000)
|
|
|
public void retryDealingTransfers() {
|
|
public void retryDealingTransfers() {
|
|
|
String lockValue = redisLockUtil.lock(RETRY_LOCK_KEY, RETRY_LOCK_TTL);
|
|
String lockValue = redisLockUtil.lock(RETRY_LOCK_KEY, RETRY_LOCK_TTL);
|
|
|
- if (lockValue == null) {
|
|
|
|
|
- log.debug("[重试任务] 上一轮尚未完成或另一实例执行中,跳过");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (lockValue == null) { log.debug("[重试任务] 上一轮尚未完成,跳过"); return; }
|
|
|
try {
|
|
try {
|
|
|
- int scanned = 0;
|
|
|
|
|
- int processed = 0;
|
|
|
|
|
-
|
|
|
|
|
|
|
+ int scanned = 0, processed = 0;
|
|
|
var now = OffsetDateTime.now();
|
|
var now = OffsetDateTime.now();
|
|
|
var dealingList = transferMapper.selectList(
|
|
var dealingList = transferMapper.selectList(
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TransferEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TransferEntity>()
|
|
@@ -476,80 +568,61 @@ public class AlipayTransferService {
|
|
|
.le(TransferEntity::getNextRetryAt, now)
|
|
.le(TransferEntity::getNextRetryAt, now)
|
|
|
.orderByAsc(TransferEntity::getNextRetryAt));
|
|
.orderByAsc(TransferEntity::getNextRetryAt));
|
|
|
|
|
|
|
|
- if (dealingList.isEmpty()) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (dealingList.isEmpty()) return;
|
|
|
scanned = dealingList.size();
|
|
scanned = dealingList.size();
|
|
|
log.info("[重试任务] 扫描到 {} 条待反查记录", scanned);
|
|
log.info("[重试任务] 扫描到 {} 条待反查记录", scanned);
|
|
|
|
|
|
|
|
for (TransferEntity transfer : dealingList) {
|
|
for (TransferEntity transfer : dealingList) {
|
|
|
String outBizNo = transfer.getOutBizNo();
|
|
String outBizNo = transfer.getOutBizNo();
|
|
|
- String enterpriseId = transfer.getEnterpriseId();
|
|
|
|
|
String orderNo = transfer.getOrderNo() != null ? transfer.getOrderNo() : "";
|
|
String orderNo = transfer.getOrderNo() != null ? transfer.getOrderNo() : "";
|
|
|
int retryCount = transfer.getRetryCount() != null ? transfer.getRetryCount() : 0;
|
|
int retryCount = transfer.getRetryCount() != null ? transfer.getRetryCount() : 0;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- String syncResult = syncTransferDetail(outBizNo, enterpriseId, orderNo);
|
|
|
|
|
- if (syncResult != null) {
|
|
|
|
|
- // SUCCESS 或 FAIL 已由 syncTransferDetail 更新DB
|
|
|
|
|
- // 只更新 retryCount/nextRetryAt,避免全字段 updateById 覆盖 syncTransferDetail 写入的 status
|
|
|
|
|
|
|
+ Map<String, String> status = queryTransferStatus(
|
|
|
|
|
+ !orderNo.isEmpty() ? orderNo : null, outBizNo);
|
|
|
|
|
+ String s = status.get("status");
|
|
|
|
|
+ if ("SUCCESS".equals(s) || "FAIL".equals(s) || "REFUND".equals(s)) {
|
|
|
TransferEntity patch = new TransferEntity();
|
|
TransferEntity patch = new TransferEntity();
|
|
|
patch.setId(transfer.getId());
|
|
patch.setId(transfer.getId());
|
|
|
|
|
+ patch.setStatus(s);
|
|
|
patch.setRetryCount(4);
|
|
patch.setRetryCount(4);
|
|
|
patch.setNextRetryAt(null);
|
|
patch.setNextRetryAt(null);
|
|
|
transferMapper.updateById(patch);
|
|
transferMapper.updateById(patch);
|
|
|
- log.info("[重试任务] out_biz_no={}, retry={}/4, result={}, 已更新",
|
|
|
|
|
- outBizNo, retryCount + 1, syncResult);
|
|
|
|
|
|
|
+ log.info("[重试任务] out_biz_no={}, retry={}/4, result={}", outBizNo, retryCount + 1, s);
|
|
|
processed++;
|
|
processed++;
|
|
|
- // GAP FIX 4: 回调 open_return_service — 对应 Python L1190-1193
|
|
|
|
|
try {
|
|
try {
|
|
|
openapiService.notifyTransferResult(
|
|
openapiService.notifyTransferResult(
|
|
|
- orderNo != null && !orderNo.isEmpty() ? orderNo : outBizNo,
|
|
|
|
|
- outBizNo, transfer.getTenantId());
|
|
|
|
|
- } catch (Exception ignored) {
|
|
|
|
|
- // Python 中回调失败仅忽略,不阻断主流程
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ !orderNo.isEmpty() ? orderNo : outBizNo, outBizNo, transfer.getTenantId());
|
|
|
|
|
+ } catch (Exception ignored) {}
|
|
|
} else {
|
|
} else {
|
|
|
- // 仍为 DEALING → 退避
|
|
|
|
|
boolean isCardTransfer = "BANK_CARD".equals(transfer.getPayeeType());
|
|
boolean isCardTransfer = "BANK_CARD".equals(transfer.getPayeeType());
|
|
|
- int maxRetries = isCardTransfer ? 3 : 0; // 银行卡3次,支付宝1次(不重试)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ int maxRetries = isCardTransfer ? 3 : 0;
|
|
|
if (retryCount < maxRetries) {
|
|
if (retryCount < maxRetries) {
|
|
|
int accumulated = 0;
|
|
int accumulated = 0;
|
|
|
- for (int i = 0; i <= retryCount && i < RETRY_INTERVALS.length; i++) {
|
|
|
|
|
|
|
+ for (int i = 0; i <= retryCount && i < RETRY_INTERVALS.length; i++)
|
|
|
accumulated += RETRY_INTERVALS[i];
|
|
accumulated += RETRY_INTERVALS[i];
|
|
|
- }
|
|
|
|
|
transfer.setRetryCount(retryCount + 1);
|
|
transfer.setRetryCount(retryCount + 1);
|
|
|
transfer.setNextRetryAt(transfer.getCreatedTime() != null
|
|
transfer.setNextRetryAt(transfer.getCreatedTime() != null
|
|
|
? transfer.getCreatedTime().plusMinutes(accumulated)
|
|
? transfer.getCreatedTime().plusMinutes(accumulated)
|
|
|
: OffsetDateTime.now().plusMinutes(accumulated));
|
|
: OffsetDateTime.now().plusMinutes(accumulated));
|
|
|
- log.info("[重试任务] out_biz_no={}, payee_type={}, retry={}/{}, 仍DEALING, 下次={}",
|
|
|
|
|
- outBizNo, transfer.getPayeeType(), retryCount + 1, maxRetries + 1, transfer.getNextRetryAt());
|
|
|
|
|
} else {
|
|
} else {
|
|
|
transfer.setStatus("FAIL");
|
|
transfer.setStatus("FAIL");
|
|
|
transfer.setRetryCount(4);
|
|
transfer.setRetryCount(4);
|
|
|
transfer.setNextRetryAt(null);
|
|
transfer.setNextRetryAt(null);
|
|
|
- String failReason = isCardTransfer ? "反查超时判定为失败" : "支付宝转账仅重试1次仍DEALING";
|
|
|
|
|
- transfer.setErrorMsg(failReason);
|
|
|
|
|
- log.info("[重试任务] out_biz_no={}, payee_type={}, retry={}/{}次DEALING, 判FAIL: {}",
|
|
|
|
|
- outBizNo, transfer.getPayeeType(), retryCount + 1, maxRetries + 1, failReason);
|
|
|
|
|
- // GAP FIX 4: 回调 open_return_service — 对应 Python L1213-1217
|
|
|
|
|
|
|
+ transfer.setErrorMsg(isCardTransfer ? "反查超时判定为失败" : "仅重试1次仍DEALING");
|
|
|
try {
|
|
try {
|
|
|
openapiService.notifyTransferResult(
|
|
openapiService.notifyTransferResult(
|
|
|
- orderNo != null && !orderNo.isEmpty() ? orderNo : outBizNo,
|
|
|
|
|
- outBizNo, transfer.getTenantId());
|
|
|
|
|
- } catch (Exception ignored) {
|
|
|
|
|
- // Python 中回调失败仅忽略,不阻断主流程
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ !orderNo.isEmpty() ? orderNo : outBizNo, outBizNo, transfer.getTenantId());
|
|
|
|
|
+ } catch (Exception ignored) {}
|
|
|
}
|
|
}
|
|
|
transferMapper.updateById(transfer);
|
|
transferMapper.updateById(transfer);
|
|
|
processed++;
|
|
processed++;
|
|
|
}
|
|
}
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.warn("[重试任务] trans.order.query 异常: out_biz_no={}, err={}", outBizNo, e.getMessage());
|
|
|
|
|
|
|
+ log.warn("[重试任务] 查询异常: out_biz_no={}, err={}", outBizNo, e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- log.info("[重试任务] ====== 执行完成 ====== 处理 {} 条,扫描 {} 条", processed, scanned);
|
|
|
|
|
|
|
+ log.info("[重试任务] 完成: {} 处理/{} 扫描", processed, scanned);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
log.error("[重试任务] 执行异常", e);
|
|
log.error("[重试任务] 执行异常", e);
|
|
|
} finally {
|
|
} finally {
|
|
@@ -557,134 +630,50 @@ public class AlipayTransferService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 全量同步转账状态 — 对应 Python transfer_sync_all_service (L990-1038)
|
|
|
|
|
- *
|
|
|
|
|
- * 遍历所有 DEALING 记录,调用 trans.order.query 逐一同步。
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 全量同步 — alipay.fund.trans.common.query */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public Map<String, Object> transferSyncAllService() {
|
|
public Map<String, Object> transferSyncAllService() {
|
|
|
- var allTransfers = transferMapper.selectList(
|
|
|
|
|
|
|
+ var all = transferMapper.selectList(
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TransferEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TransferEntity>()
|
|
|
.eq(TransferEntity::getStatus, "DEALING")
|
|
.eq(TransferEntity::getStatus, "DEALING")
|
|
|
.orderByAsc(TransferEntity::getId));
|
|
.orderByAsc(TransferEntity::getId));
|
|
|
|
|
|
|
|
- int synced = 0;
|
|
|
|
|
- int errors = 0;
|
|
|
|
|
- java.util.List<Map<String, Object>> details = new java.util.ArrayList<>();
|
|
|
|
|
-
|
|
|
|
|
- for (TransferEntity transfer : allTransfers) {
|
|
|
|
|
- String outBizNo = transfer.getOutBizNo();
|
|
|
|
|
- String eid = transfer.getEnterpriseId();
|
|
|
|
|
- String orderNo = transfer.getOrderNo() != null ? transfer.getOrderNo() : "";
|
|
|
|
|
- if (outBizNo == null || eid == null) continue;
|
|
|
|
|
|
|
+ int synced = 0, errors = 0;
|
|
|
|
|
+ List<Map<String, Object>> details = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
+ for (TransferEntity t : all) {
|
|
|
|
|
+ String outBizNo = t.getOutBizNo();
|
|
|
|
|
+ String orderNo = t.getOrderNo() != null ? t.getOrderNo() : "";
|
|
|
|
|
+ if (outBizNo == null) continue;
|
|
|
try {
|
|
try {
|
|
|
- String newStatus = syncTransferDetail(outBizNo, eid, orderNo);
|
|
|
|
|
- if (newStatus != null) {
|
|
|
|
|
|
|
+ Map<String, String> status = queryTransferStatus(
|
|
|
|
|
+ !orderNo.isEmpty() ? orderNo : null, outBizNo);
|
|
|
|
|
+ String s = status.get("status");
|
|
|
|
|
+ if ("SUCCESS".equals(s) || "FAIL".equals(s) || "REFUND".equals(s)) {
|
|
|
|
|
+ t.setStatus(s);
|
|
|
|
|
+ if (status.get("pay_fund_order_id") != null)
|
|
|
|
|
+ t.setFundOrderId(status.get("pay_fund_order_id"));
|
|
|
|
|
+ transferMapper.updateById(t);
|
|
|
synced++;
|
|
synced++;
|
|
|
- details.add(Map.of("out_biz_no", outBizNo, "old_status", transfer.getStatus(), "new_status", newStatus));
|
|
|
|
|
|
|
+ details.add(Map.of("out_biz_no", outBizNo, "new_status", s));
|
|
|
} else {
|
|
} else {
|
|
|
- details.add(Map.of("out_biz_no", outBizNo, "status", transfer.getStatus(), "action", "no_change"));
|
|
|
|
|
|
|
+ details.add(Map.of("out_biz_no", outBizNo, "action", "no_change"));
|
|
|
}
|
|
}
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
errors++;
|
|
errors++;
|
|
|
- details.add(Map.of("out_biz_no", outBizNo, "status", transfer.getStatus(), "error", e.getMessage()));
|
|
|
|
|
- log.warn("全量同步 - 查询失败: out_biz_no={}, err={}", outBizNo, e.getMessage());
|
|
|
|
|
|
|
+ details.add(Map.of("out_biz_no", outBizNo, "error", e.getMessage()));
|
|
|
|
|
+ log.warn("全量同步失败: out_biz_no={}, err={}", outBizNo, e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("total", allTransfers.size());
|
|
|
|
|
|
|
+ result.put("total", all.size());
|
|
|
result.put("synced", synced);
|
|
result.put("synced", synced);
|
|
|
result.put("errors", errors);
|
|
result.put("errors", errors);
|
|
|
result.put("details", details);
|
|
result.put("details", details);
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 单笔查询转账状态 — 对应 Python _sync_transfer_detail (L1040-1122)
|
|
|
|
|
- *
|
|
|
|
|
- * 调用 alipay.commerce.ec.trans.order.query
|
|
|
|
|
- * 参数: enterprise_id + out_biz_no + order_no(两个都传)
|
|
|
|
|
- *
|
|
|
|
|
- * @return 新状态字符串 (SUCCESS/FAIL),或 null(仍为 DEALING 或查询失败)
|
|
|
|
|
- */
|
|
|
|
|
- private String syncTransferDetail(String outBizNo, String enterpriseId, String orderNo) {
|
|
|
|
|
- try {
|
|
|
|
|
- // 动态加载,避免编译时依赖 SDK 中可能不存在的类
|
|
|
|
|
- Class<?> modelClass = Class.forName("com.alipay.api.domain.AlipayCommerceEcTransOrderQueryModel");
|
|
|
|
|
- Class<?> requestClass = Class.forName("com.alipay.api.request.AlipayCommerceEcTransOrderQueryRequest");
|
|
|
|
|
- Class<?> responseClass = Class.forName("com.alipay.api.response.AlipayCommerceEcTransOrderQueryResponse");
|
|
|
|
|
-
|
|
|
|
|
- Object model = modelClass.getDeclaredConstructor().newInstance();
|
|
|
|
|
- modelClass.getMethod("setEnterpriseId", String.class).invoke(model, enterpriseId);
|
|
|
|
|
- if (outBizNo != null && !outBizNo.isEmpty())
|
|
|
|
|
- modelClass.getMethod("setOutBizNo", String.class).invoke(model, outBizNo);
|
|
|
|
|
- if (orderNo != null && !orderNo.isEmpty())
|
|
|
|
|
- modelClass.getMethod("setOrderNo", String.class).invoke(model, orderNo);
|
|
|
|
|
-
|
|
|
|
|
- Object request = requestClass.getDeclaredConstructor().newInstance();
|
|
|
|
|
- requestClass.getMethod("setBizModel", modelClass).invoke(request, model);
|
|
|
|
|
-
|
|
|
|
|
- Object response = alipayClientFactory.getClient(enterpriseId).execute(
|
|
|
|
|
- (com.alipay.api.AlipayRequest) request);
|
|
|
|
|
-
|
|
|
|
|
- if (response == null) return null;
|
|
|
|
|
-
|
|
|
|
|
- boolean isSuccess = (boolean) responseClass.getMethod("isSuccess").invoke(response);
|
|
|
|
|
- if (!isSuccess) {
|
|
|
|
|
- String subCode = (String) responseClass.getMethod("getSubCode").invoke(response);
|
|
|
|
|
- if ("ORDER_NOT_EXIST".equals(subCode)) return null;
|
|
|
|
|
- log.warn("trans.order.query 失败: out_biz_no={}, sub_code={}", outBizNo, subCode);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- String alipayStatus = (String) responseClass.getMethod("getStatus").invoke(response);
|
|
|
|
|
- if (alipayStatus == null || "DEALING".equals(alipayStatus)) return null;
|
|
|
|
|
-
|
|
|
|
|
- // 更新本地记录
|
|
|
|
|
- TransferEntity transfer = transferMapper.selectOne(
|
|
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TransferEntity>()
|
|
|
|
|
- .eq(TransferEntity::getOutBizNo, outBizNo));
|
|
|
|
|
- if (transfer == null) return alipayStatus;
|
|
|
|
|
-
|
|
|
|
|
- transfer.setStatus(alipayStatus);
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- Object apiOrderNo = responseClass.getMethod("getOrderNo").invoke(response);
|
|
|
|
|
- if (apiOrderNo != null) transfer.setOrderNo((String) apiOrderNo);
|
|
|
|
|
- } catch (Exception ignored) {}
|
|
|
|
|
- try {
|
|
|
|
|
- Object apiFundOrderId = responseClass.getMethod("getPayFundOrderId").invoke(response);
|
|
|
|
|
- if (apiFundOrderId != null) transfer.setFundOrderId((String) apiFundOrderId);
|
|
|
|
|
- } catch (Exception ignored) {}
|
|
|
|
|
- try {
|
|
|
|
|
- Object apiAmount = responseClass.getMethod("getAmount").invoke(response);
|
|
|
|
|
- if (apiAmount != null) transfer.setAmount(new java.math.BigDecimal(apiAmount.toString()));
|
|
|
|
|
- } catch (Exception ignored) {}
|
|
|
|
|
-
|
|
|
|
|
- if ("FAIL".equals(alipayStatus)) {
|
|
|
|
|
- try {
|
|
|
|
|
- Object subMsg = responseClass.getMethod("getSubMsg").invoke(response);
|
|
|
|
|
- if (subMsg == null) subMsg = responseClass.getMethod("getMsg").invoke(response);
|
|
|
|
|
- if (subMsg != null) transfer.setRemark((String) subMsg);
|
|
|
|
|
- } catch (Exception ignored) {}
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- transferMapper.updateById(transfer);
|
|
|
|
|
- log.info("转账同步(trans.order.query): out_biz_no={}, status={}", outBizNo, alipayStatus);
|
|
|
|
|
- return alipayStatus;
|
|
|
|
|
-
|
|
|
|
|
- } catch (ClassNotFoundException e) {
|
|
|
|
|
- log.warn("SDK 不支持 trans.order.query");
|
|
|
|
|
- return null;
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.warn("trans.order.query 异常: out_biz_no={}, err={}", outBizNo, e.getMessage());
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // ==================== util ====================
|
|
|
|
|
|
|
|
private String toJson(Object obj) {
|
|
private String toJson(Object obj) {
|
|
|
try { return objectMapper.writeValueAsString(obj); } catch (Exception e) { return "{}"; }
|
|
try { return objectMapper.writeValueAsString(obj); } catch (Exception e) { return "{}"; }
|