update 汇付

This commit is contained in:
jsowell
2026-05-29 13:33:16 +08:00
parent 6f659783b5
commit 9e1c7ac606
16 changed files with 207 additions and 21 deletions

View File

@@ -405,6 +405,85 @@ export function getAdapayOpenDetailV2(data) {
- 个人无结算账户:主按钮为“创建对私结算账户”。
- 结算账户删除前提示:删除后支付分账将不可用,需重新创建后恢复。
## Review 待修复计划
### 问题一:结算账户创建成功后缓存未失效
风险:
- `createSettleAccount` 成功写入 `settle_account_id` 后,当前使用 `updateAdapayMemberAccountByMemberId` 更新本地记录。
- `updateAdapayMemberAccountByMemberId` 只更新数据库,没有清理 `ADAPAY_MEMBER_ACCOUNT + merchantId` 缓存。
- `/adapay/member/v2/detail` 继续读取旧缓存时,页面可能仍显示“未创建结算账户”,导致状态停留在 `PERSONAL_OPENED_NO_SETTLE``CORP_OPENED_NO_SETTLE`
修改计划:
-`AdapayMemberAccountServiceImpl.updateAdapayMemberAccountByMemberId` 中补齐缓存清理。
- 优先使用传入对象的 `merchantId` 清理缓存。
- 若调用方只传 `adapayMemberId`,则先按 `adapayMemberId` 查询当前记录,拿到 `merchantId` 后清理缓存。
- 调整 `AdapayService.createSettleAccount``changeBankCard` 等调用点,尽量在更新对象中补充 `merchantId`,减少额外查询。
验收标准:
- 创建结算账户成功后,立即刷新 V2 页面,状态应变为 `PERSONAL_COMPLETED``CORP_COMPLETED`
- 结算账户 ID、银行卡信息能立即展示不依赖 Redis 过期。
### 问题二:删除结算账户缺少业务阻断校验
风险:
- 文档已确认删除结算账户前需要校验未结算分账/在途提现。
- 当前 `deleteSettleAccount` 只校验 V2 action 和结算账户是否存在,随后直接调用汇付删除并清空本地结算账户。
- 若存在未结算分账或在途提现,删除结算账户会影响后续结算、提现或分账状态追踪。
修改计划:
-`AdapayService.deleteSettleAccount` 调用汇付删除前增加业务校验方法,例如 `assertSettleAccountCanDelete(merchantId)`
- 校验维度先按现有数据能力落地,明确数据源和阻断状态:
- 在途提现:查询 `clearing_withdraw_info`,若存在 `withdraw_status = '0'` 的记录,阻断删除。
- 清分账单:查询 `clearing_bill_info`,若存在 `bill_status in ('0', '1', '3', '5')` 的记录,阻断删除,分别对应未清分、清分在途、提现申请中、等待处理。
- 未分账订单:查询 `order_unsplit_record`,若存在 `status = 'unsplit'` 且订单归属当前 `merchantId` 的记录,阻断删除。
- 为未分账订单补齐按商户查询能力:
-`OrderUnsplitRecordMapper` 新增 `countUnsplitByMerchantId(merchantId)`
- SQL 通过 `order_unsplit_record.order_code` 关联 `order_basic_info.order_code`,按 `order_basic_info.merchant_id` 过滤当前商户。
-`OrderUnsplitRecordService` 增加同名方法,供 `AdapayService.assertSettleAccountCanDelete` 调用。
- 为提现和清分账单尽量使用轻量查询:
- 优先新增 `ClearingWithdrawInfoService.countProcessingByMerchantId(merchantId)`,只统计 `withdraw_status = '0'`
- 优先新增 `ClearingBillInfoService.countBlockingByMerchantId(merchantId)`,只统计 `bill_status in ('0', '1', '3', '5')`
- 若已有列表查询能满足,可先复用现有方法,但最终实现应避免在删除前加载大量历史记录。
- 阻断时返回明确错误信息,例如“存在在途提现,请完成后再删除结算账户”或“存在未结算分账,请完成结算后再删除结算账户”。
- 前端保留现有确认弹窗,但以后端校验结果作为最终准入。
验收标准:
- 无在途提现、无未结算分账时,可以删除结算账户并清空本地 `settle_account_id`
- 存在 `withdraw_status = '0'` 的提现记录时,删除接口返回业务错误,汇付删除请求不会发起,本地结算账户不会清空。
- 存在 `bill_status in ('0', '1', '3', '5')` 的清分账单时,删除接口返回业务错误,汇付删除请求不会发起,本地结算账户不会清空。
- 存在 `status = 'unsplit'` 且归属当前商户的未分账订单时,删除接口返回业务错误,汇付删除请求不会发起,本地结算账户不会清空。
### 问题三:删除汇付用户未接入 V2 action 后端校验
风险:
- V2 计划要求后端统一动作校验,避免绕过前端按钮限制。
- 当前创建、更新、创建/删除结算账户已接入 `assertActionAllowed`
- `deleteAdapayMember` 未校验 `DELETE_MEMBER`,直接调用接口时可能删除企业审核中的本地记录,导致后续汇付回调无法匹配记录。
修改计划:
-`AdapayService.deleteAdapayMember` 开始处增加 `assertActionAllowed(dto.getMerchantId(), AdapayOpenActionEnum.DELETE_MEMBER)`
- 保留现有 `adapayMemberId` 一致性校验和“需先删除结算账户”校验,作为二次保护。
- 确认 `resolveActions` 中仅允许以下状态删除会员:
- `PERSONAL_OPENED_NO_SETTLE`
- `CORP_FAILED`
- 若后续业务允许更多状态删除,再由状态机统一扩展,不在接口中散落判断。
验收标准:
- 企业审核中状态调用删除汇付用户接口,应返回“不允许执行删除”类业务错误。
- 企业失败且无结算账户时,可以删除本地失败记录。
- 个人已开户但未创建结算账户时,可以删除本地会员记录。
- 已有结算账户时仍必须先删除结算账户。
## 分阶段实施清单
### 阶段一:后端状态与接口
@@ -453,4 +532,3 @@ export function getAdapayOpenDetailV2(data) {
回答:确定补齐采集。
4. 删除结算账户前是否需要校验未结算分账/在途提现?本计划建议后续补一层业务校验。
回答:需要校验 。

View File

@@ -80,6 +80,9 @@ public class AdapayService {
@Autowired
private ClearingBillInfoService clearingBillInfoService;
@Autowired
private OrderUnsplitRecordService orderUnsplitRecordService;
@Autowired
private PileMerchantInfoService pileMerchantInfoService;
@@ -331,6 +334,7 @@ public class AdapayService {
AdapayMemberAccount updateRecord = new AdapayMemberAccount();
updateRecord.setAdapayMemberId(adapayMemberAccount.getAdapayMemberId());
updateRecord.setMerchantId(dto.getMerchantId());
updateRecord.setSettleAccountId((String) settleCount.get("id"));
adapayMemberAccountService.updateAdapayMemberAccountByMemberId(updateRecord);
}
@@ -848,16 +852,26 @@ public class AdapayService {
memberParams.put("telphone", dto.getTelphone());
memberParams.put("email", dto.getEmail());
memberParams.put("notify_url", ADAPAY_CALLBACK_URL);
File file = ZipUtil.createZipFileFromImages(dto.getImgList());
Map<String, Object> member = CorpMember.create(memberParams, file, config.getWechatAppId());
Map<String, Object> member;
try {
File file = ZipUtil.createZipFileFromImages(dto.getImgList());
member = CorpMember.create(memberParams, file, config.getWechatAppId());
} catch (BaseAdaPayException e) {
markCorpMemberCreateFailed(adapayMemberAccount, e.getMessage());
throw e;
} catch (RuntimeException e) {
markCorpMemberCreateFailed(adapayMemberAccount, e.getMessage());
throw e;
}
log.info("创建企业账户param:{}, result:{}", JSON.toJSONString(memberParams), JSON.toJSONString(member));
if (member == null) {
String errorMsg = "创建企业汇付用户失败";
markCorpMemberCreateFailed(adapayMemberAccount, errorMsg);
throw new BusinessException("", errorMsg);
}
if (StringUtils.equals((String) member.get("status"), "failed")) {
String error_msg = (String) member.get("error_msg");
adapayMemberAccount.setStatus(Constants.TWO); // 创建失败,改状态
adapayMemberAccount.setRemark(error_msg);
adapayMemberAccount.setAuditState(AdapayAuditStateEnum.ACCOUNT_OPENING_FAILED.getValue());
adapayMemberAccount.setAuditDesc(error_msg);
adapayMemberAccountService.updateAdapayMemberAccount(adapayMemberAccount);
markCorpMemberCreateFailed(adapayMemberAccount, error_msg);
throw new BusinessException("", error_msg);
}
@@ -874,6 +888,19 @@ public class AdapayService {
// adapayMemberAccountService.updateAdapayMemberAccount(adapayMemberAccount);
}
/**
* 企业开户请求已写入本地待审核记录后如果附件处理、SDK 调用或汇付同步返回失败,
* 需要明确落到失败态,避免页面长期卡在“企业审核中”且无法重新提交。
*/
private void markCorpMemberCreateFailed(AdapayMemberAccount adapayMemberAccount, String errorMsg) {
String message = StringUtils.isNotBlank(errorMsg) ? errorMsg : "创建企业汇付用户失败";
adapayMemberAccount.setStatus(Constants.TWO);
adapayMemberAccount.setRemark(message);
adapayMemberAccount.setAuditState(AdapayAuditStateEnum.ACCOUNT_OPENING_FAILED.getValue());
adapayMemberAccount.setAuditDesc(message);
adapayMemberAccountService.updateAdapayMemberAccount(adapayMemberAccount);
}
/**
* 提现逻辑/创建取现对象
*
@@ -1122,6 +1149,7 @@ public class AdapayService {
update.setStatus(Constants.ZERO);
update.setAuditState(AdapayAuditStateEnum.AWAIT_AUDIT.getValue());
update.setLastOrderNo(orderNo);
update.setMerchantId(dto.getMerchantId());
adapayMemberAccountService.updateAdapayMemberAccount(update);
return null;
}
@@ -1678,6 +1706,7 @@ public class AdapayService {
if (settleAccount != null && !StringUtils.equals((String) settleAccount.get("status"), "failed")) {
AdapayMemberAccount updateRecord = new AdapayMemberAccount();
updateRecord.setAdapayMemberId(adapayMemberId);
updateRecord.setMerchantId(dto.getMerchantId());
updateRecord.setSettleAccountId((String) settleAccount.get("id"));
adapayMemberAccountService.updateAdapayMemberAccountByMemberId(updateRecord);
}
@@ -1783,11 +1812,13 @@ public class AdapayService {
throw new BusinessException("", "未查询到结算账户");
}
assertSettleAccountCanDelete(dto.getMerchantId());
this.createDeleteSettleAccountRequest(adapayMemberId, settleAccountId, wechatAppId);
adapayMemberAccountService.clearSettleAccountByMerchantId(dto.getMerchantId());
}
public void deleteAdapayMember(DeleteAdapayMemberDTO dto) throws BaseAdaPayException {
assertActionAllowed(dto.getMerchantId(), AdapayOpenActionEnum.DELETE_MEMBER);
AdapayMemberAccount adapayMemberAccount = requireCurrentMemberAccount(dto.getMerchantId());
if (StringUtils.isNotBlank(dto.getAdapayMemberId())
&& !StringUtils.equals(dto.getAdapayMemberId(), adapayMemberAccount.getAdapayMemberId())) {
@@ -1800,6 +1831,21 @@ public class AdapayService {
adapayMemberAccountService.deleteAccountByMerchantId(dto.getMerchantId());
}
/**
* 删除结算账户前必须确认没有在途提现、未完成清分或未分账订单。
*/
private void assertSettleAccountCanDelete(String merchantId) {
if (clearingWithdrawInfoService.countProcessingByMerchantId(merchantId) > 0) {
throw new BusinessException("", "存在在途提现,请完成后再删除结算账户");
}
if (clearingBillInfoService.countBlockingByMerchantId(merchantId) > 0) {
throw new BusinessException("", "存在未完成清分或提现申请中的账单,请处理完成后再删除结算账户");
}
if (orderUnsplitRecordService.countUnsplitByMerchantId(merchantId) > 0) {
throw new BusinessException("", "存在未结算分账,请完成结算后再删除结算账户");
}
}
private AdapayMemberAccount requireCurrentMemberAccount(String merchantId) {
AdapayMemberAccount adapayMemberAccount = adapayMemberAccountService.selectByMerchantId(merchantId);
if (adapayMemberAccount == null) {
@@ -1876,13 +1922,7 @@ public class AdapayService {
if (!corp) {
return AdapayOpenStatusEnum.NONE;
}
// 企业用户:先按汇付审核状态判断,其次按本地 status 兜底
if (AdapayAuditStateEnum.ACCOUNT_SUCCESSFUL.getValue().equals(auditState) || hasSettleAccount) {
return AdapayOpenStatusEnum.CORP_COMPLETED;
}
if (AdapayAuditStateEnum.ACCOUNT_OPENED_NO_SETTLEMENT_ACCOUNT.getValue().equals(auditState)) {
return AdapayOpenStatusEnum.CORP_OPENED_NO_SETTLE;
}
// 企业用户:审核中/失败必须优先于是否已有结算账户,企业资料更新后也会重新进入审核流。
if (AdapayAuditStateEnum.AUDIT_FAILED.getValue().equals(auditState)
|| AdapayAuditStateEnum.ACCOUNT_OPENING_FAILED.getValue().equals(auditState)
|| Constants.TWO.equals(account.getStatus())) {
@@ -1892,6 +1932,12 @@ public class AdapayService {
|| Constants.ZERO.equals(account.getStatus())) {
return AdapayOpenStatusEnum.CORP_AUDITING;
}
if (AdapayAuditStateEnum.ACCOUNT_SUCCESSFUL.getValue().equals(auditState) || hasSettleAccount) {
return AdapayOpenStatusEnum.CORP_COMPLETED;
}
if (AdapayAuditStateEnum.ACCOUNT_OPENED_NO_SETTLEMENT_ACCOUNT.getValue().equals(auditState)) {
return AdapayOpenStatusEnum.CORP_OPENED_NO_SETTLE;
}
// 审核通过但没拿到汇付细节时status=1按已开户但暂无结算账户处理
if (Constants.ONE.equals(account.getStatus())) {
return AdapayOpenStatusEnum.CORP_OPENED_NO_SETTLE;

View File

@@ -39,4 +39,6 @@ public interface ClearingBillInfoMapper {
List<MerchantClearingBillVO> getMerchantClearingBillList(GetClearingBillDTO dto);
List<ClearingBillInfo> selectByWithdrawCode(@Param("withdrawCode") String withdrawCode);
int countBlockingByMerchantId(@Param("merchantId") String merchantId);
}

View File

@@ -73,4 +73,6 @@ public interface ClearingWithdrawInfoMapper {
List<ClearingWithdrawInfo> selectByMerchantId(@Param("merchantId") String merchantId);
BigDecimal queryTotalWithdraw(String merchantId);
int countProcessingByMerchantId(@Param("merchantId") String merchantId);
}

View File

@@ -27,4 +27,6 @@ public interface OrderUnsplitRecordMapper {
int insertOrUpdateSelective(OrderUnsplitRecord record);
List<OrderUnsplitRecord> queryUnsplitOrders(@Param("startTime") String startTime, @Param("endTime") String endTime);
int countUnsplitByMerchantId(@Param("merchantId") String merchantId);
}

View File

@@ -40,5 +40,6 @@ public interface ClearingBillInfoService {
ClearingBillInfo selectByMerchantIdAndTradeDate(String merchantId, String tradeDate);
List<ClearingBillInfo> selectByWithdrawCode(String withdrawCode);
}
int countBlockingByMerchantId(String merchantId);
}

View File

@@ -44,4 +44,6 @@ public interface ClearingWithdrawInfoService{
BigDecimal queryTotalWithdraw(String merchantId);
int countProcessingByMerchantId(String merchantId);
}

View File

@@ -10,4 +10,6 @@ public interface OrderUnsplitRecordService {
List<OrderUnsplitRecord> queryUnsplitOrders(String startTime, String endTime);
void updateOrderUnsplitRecord(OrderUnsplitRecord orderUnsplitRecord);
int countUnsplitByMerchantId(String merchantId);
}

View File

@@ -205,7 +205,17 @@ public class AdapayMemberAccountServiceImpl implements AdapayMemberAccountServic
@Override
public void updateAdapayMemberAccountByMemberId(AdapayMemberAccount adapayMemberAccount) {
String merchantId = adapayMemberAccount.getMerchantId();
if (StringUtils.isBlank(merchantId) && StringUtils.isNotBlank(adapayMemberAccount.getAdapayMemberId())) {
AdapayMemberAccount current = adapayMemberAccountMapper.selectByMemberId(adapayMemberAccount.getAdapayMemberId());
if (current != null) {
merchantId = current.getMerchantId();
}
}
adapayMemberAccountMapper.updateAdapayMemberAccountByMemberId(adapayMemberAccount);
if (StringUtils.isNotBlank(merchantId)) {
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + merchantId);
}
}
/**

View File

@@ -112,9 +112,13 @@ public class ClearingBillInfoServiceImpl implements ClearingBillInfoService {
return result;
}
@Override
public int countBlockingByMerchantId(String merchantId) {
return clearingBillInfoMapper.countBlockingByMerchantId(merchantId);
}
@Override
public List<ClearingBillInfo> selectByWithdrawCode(String withdrawCode) {
return clearingBillInfoMapper.selectByWithdrawCode(withdrawCode);
}
}

View File

@@ -164,4 +164,9 @@ public class ClearingWithdrawInfoServiceImpl implements ClearingWithdrawInfoServ
return clearingWithdrawInfoMapper.queryTotalWithdraw(merchantId);
}
@Override
public int countProcessingByMerchantId(String merchantId) {
return clearingWithdrawInfoMapper.countProcessingByMerchantId(merchantId);
}
}

View File

@@ -28,4 +28,9 @@ public class OrderUnsplitRecordServiceImpl implements OrderUnsplitRecordService
public void updateOrderUnsplitRecord(OrderUnsplitRecord orderUnsplitRecord) {
orderUnsplitRecordMapper.insertOrUpdateSelective(orderUnsplitRecord);
}
@Override
public int countUnsplitByMerchantId(String merchantId) {
return orderUnsplitRecordMapper.countUnsplitByMerchantId(merchantId);
}
}

View File

@@ -441,6 +441,9 @@
<result column="update_time" property="updateTime" />
<result column="update_by" property="updateBy" />
<result column="del_flag" property="delFlag" />
<result column="audit_state" property="auditState" />
<result column="audit_desc" property="auditDesc" />
<result column="last_order_no" property="lastOrderNo" />
</resultMap>
<select id="selectAdapayMemberAccountList" parameterType="com.jsowell.pile.domain.AdapayMemberAccount" resultMap="AdapayMemberAccountResult">

View File

@@ -789,4 +789,12 @@
where del_flag = '0'
and withdraw_code = #{withdrawCode,jdbcType=VARCHAR}
</select>
<select id="countBlockingByMerchantId" resultType="int">
select count(1)
from clearing_bill_info
where del_flag = '0'
and merchant_id = #{merchantId,jdbcType=VARCHAR}
and bill_status in ('0', '1', '3', '5')
</select>
</mapper>

View File

@@ -574,4 +574,12 @@
and withdraw_status = '1'
and merchant_id = #{merchantId,jdbcType=VARCHAR}
</select>
<select id="countProcessingByMerchantId" resultType="int">
select count(1)
from clearing_withdraw_info
where del_flag = '0'
and withdraw_status = '0'
and merchant_id = #{merchantId,jdbcType=VARCHAR}
</select>
</mapper>

View File

@@ -393,4 +393,12 @@
and order_time between #{startTime,jdbcType=VARCHAR} and #{endTime,jdbcType=VARCHAR}
order by order_time
</select>
<select id="countUnsplitByMerchantId" resultType="int">
select count(1)
from order_unsplit_record t1
join order_basic_info t2 on t2.order_code = t1.order_code and t2.del_flag = '0'
where t1.status = 'unsplit'
and t2.merchant_id = #{merchantId,jdbcType=VARCHAR}
</select>
</mapper>