接入汇付企业开户失败待办

This commit is contained in:
jsowell
2026-07-22 16:12:40 +08:00
parent 7e27bada2d
commit e663a22303
10 changed files with 313 additions and 6 deletions

View File

@@ -11,7 +11,7 @@
## 路由安全 ## 路由安全
- 默认允许 `invoiceDetail``financeDetail`个业务路由。 - 默认允许 `invoiceDetail``financeDetail``accountUserInfo`个业务路由。
- 新业务路由上线前必须追加到 `todo.route-whitelist`,不允许直接信任请求体中的路由名。 - 新业务路由上线前必须追加到 `todo.route-whitelist`,不允许直接信任请求体中的路由名。
- `route_params` 必须是 JSON 对象,只允许 `params``query` 两层结构,最终值只能是字符串、数字、布尔值或空值。 - `route_params` 必须是 JSON 对象,只允许 `params``query` 两层结构,最终值只能是字符串、数字、布尔值或空值。
- 用户端详情、已读、完成和取消接口继续使用当前登录用户 ID 作为 SQL 条件。 - 用户端详情、已读、完成和取消接口继续使用当前登录用户 ID 作为 SQL 条件。

View File

@@ -121,6 +121,9 @@ public class OrderService {
@Autowired @Autowired
private AdapayMemberAccountService adapayMemberAccountService; private AdapayMemberAccountService adapayMemberAccountService;
@Autowired
private AdapayAccountTodoService adapayAccountTodoService;
@Autowired @Autowired
private ClearingWithdrawInfoService clearingWithdrawInfoService; private ClearingWithdrawInfoService clearingWithdrawInfoService;
@@ -1417,7 +1420,7 @@ public class OrderService {
} }
adapayMemberAccount.setAuditState(auditState); adapayMemberAccount.setAuditState(auditState);
// 逻辑删除记录,并删除缓存 // 逻辑删除记录,并删除缓存
adapayMemberAccountService.updateAdapayMemberAccount(adapayMemberAccount); adapayAccountTodoService.handleCorpMemberFailed(adapayMemberAccount);
if (StringUtils.isNotBlank(adapayMemberAccount.getMerchantId())) { if (StringUtils.isNotBlank(adapayMemberAccount.getMerchantId())) {
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + adapayMemberAccount.getMerchantId()); redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + adapayMemberAccount.getMerchantId());
} }
@@ -1454,7 +1457,7 @@ public class OrderService {
adapayMemberAccount.setSettleAccountId(settleAccountId); adapayMemberAccount.setSettleAccountId(settleAccountId);
} }
adapayMemberAccount.setDelFlag(DelFlagEnum.NORMAL.getValue()); adapayMemberAccount.setDelFlag(DelFlagEnum.NORMAL.getValue());
adapayMemberAccountService.updateAdapayMemberAccount(adapayMemberAccount); adapayAccountTodoService.handleCorpMemberSucceeded(adapayMemberAccount);
if (StringUtils.isNotBlank(adapayMemberAccount.getMerchantId())) { if (StringUtils.isNotBlank(adapayMemberAccount.getMerchantId())) {
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + adapayMemberAccount.getMerchantId()); redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + adapayMemberAccount.getMerchantId());
} }

View File

@@ -219,4 +219,4 @@ sms:
todo: todo:
operator-admin-role-id: 3 operator-admin-role-id: 3
retention-days: 180 retention-days: 180
route-whitelist: invoiceDetail,financeDetail route-whitelist: invoiceDetail,financeDetail,accountUserInfo

View File

@@ -0,0 +1,96 @@
package com.jsowell.pile.service;
import com.jsowell.common.constant.Constants;
import com.jsowell.pile.constant.AdapayAccountTodoConstants;
import com.jsowell.pile.domain.AdapayMemberAccount;
import com.jsowell.pile.domain.PileMerchantInfo;
import com.jsowell.pile.service.impl.AdapayAccountTodoServiceImpl;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.service.TodoTaskAssigneeService;
import com.jsowell.system.service.TodoTaskService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class AdapayAccountTodoServiceImplTest {
@Test
void failed_shouldUpdateAccountAndCreateAdminTodo() {
Dependencies dependencies = new Dependencies();
AdapayAccountTodoServiceImpl service = dependencies.createService();
AdapayMemberAccount account = account();
PileMerchantInfo merchantInfo = new PileMerchantInfo();
merchantInfo.setDeptId("200");
when(dependencies.merchantService.selectPileMerchantInfoById(100L)).thenReturn(merchantInfo);
when(dependencies.assigneeService.findActiveOperatorAdminUserIdsByDeptTree(200L))
.thenReturn(Collections.singletonList(8L));
account.setAuditDesc("证件资料不清晰");
service.handleCorpMemberFailed(account);
verify(dependencies.accountService).updateAdapayMemberAccount(account);
ArgumentCaptor<TodoTaskCreateCommand> captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class);
verify(dependencies.todoTaskService).createTask(captor.capture());
TodoTaskCreateCommand command = captor.getValue();
assertEquals(AdapayAccountTodoConstants.TASK_TYPE_CORP_MEMBER_FAILED, command.getTaskType());
assertEquals(AdapayAccountTodoConstants.BUSINESS_TYPE, command.getBusinessType());
assertEquals("100", command.getBusinessId());
assertEquals("accountUserInfo", command.getRouteName());
assertEquals(8L, command.getAssigneeUserId());
}
@Test
void succeeded_shouldUpdateAccountAndCompleteFailureTodosAsSystem() {
Dependencies dependencies = new Dependencies();
AdapayAccountTodoServiceImpl service = dependencies.createService();
AdapayMemberAccount account = account();
service.handleCorpMemberSucceeded(account);
verify(dependencies.accountService).updateAdapayMemberAccount(account);
verify(dependencies.todoTaskService).completeByBusinessAsSystem(
AdapayAccountTodoConstants.BUSINESS_TYPE, "100", null, "adapay-callback");
}
private static AdapayMemberAccount account() {
AdapayMemberAccount account = new AdapayMemberAccount();
account.setId(1);
account.setMerchantId("100");
account.setAdapayMemberId("member-100");
account.setStatus(Constants.ONE);
return account;
}
private static void setField(Object target, String fieldName, Object value) {
try {
Field field = AdapayAccountTodoServiceImpl.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private static class Dependencies {
private final AdapayMemberAccountService accountService = mock(AdapayMemberAccountService.class);
private final PileMerchantInfoService merchantService = mock(PileMerchantInfoService.class);
private final TodoTaskAssigneeService assigneeService = mock(TodoTaskAssigneeService.class);
private final TodoTaskService todoTaskService = mock(TodoTaskService.class);
private AdapayAccountTodoServiceImpl createService() {
AdapayAccountTodoServiceImpl service = new AdapayAccountTodoServiceImpl();
setField(service, "adapayMemberAccountService", accountService);
setField(service, "pileMerchantInfoService", merchantService);
setField(service, "todoTaskAssigneeService", assigneeService);
setField(service, "todoTaskService", todoTaskService);
return service;
}
}
}

View File

@@ -140,6 +140,19 @@ class TodoTaskServiceImplTest {
verify(mapper, never()).insertTodoTask(any(SysTodoTask.class)); verify(mapper, never()).insertTodoTask(any(SysTodoTask.class));
} }
@Test
void completeByBusinessAsSystem_shouldAllowNullCompletedBy() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
when(mapper.completeTodoByBusiness("ADAPAY_CORP_MEMBER_ACCOUNT", "100", null,
null, "adapay-callback")).thenReturn(1);
assertEquals(1, service.completeByBusinessAsSystem("ADAPAY_CORP_MEMBER_ACCOUNT", "100",
null, "adapay-callback"));
verify(mapper).completeTodoByBusiness("ADAPAY_CORP_MEMBER_ACCOUNT", "100", null,
null, "adapay-callback");
}
private static TodoTaskCreateCommand createCommand() { private static TodoTaskCreateCommand createCommand() {
return TodoTaskCreateCommand.builder() return TodoTaskCreateCommand.builder()
.taskType("INVOICE_REVIEW") .taskType("INVOICE_REVIEW")

View File

@@ -0,0 +1,13 @@
package com.jsowell.pile.constant;
/**
* 汇付账户待办常量。
*/
public final class AdapayAccountTodoConstants {
public static final String TASK_TYPE_CORP_MEMBER_FAILED = "ADAPAY_CORP_MEMBER_FAILED";
public static final String BUSINESS_TYPE = "ADAPAY_CORP_MEMBER_ACCOUNT";
public static final String ROUTE_NAME = "accountUserInfo";
private AdapayAccountTodoConstants() {
}
}

View File

@@ -0,0 +1,12 @@
package com.jsowell.pile.service;
import com.jsowell.pile.domain.AdapayMemberAccount;
/**
* 汇付账户状态与待办联动服务。
*/
public interface AdapayAccountTodoService {
void handleCorpMemberFailed(AdapayMemberAccount account);
void handleCorpMemberSucceeded(AdapayMemberAccount account);
}

View File

@@ -0,0 +1,154 @@
package com.jsowell.pile.service.impl;
import com.alibaba.fastjson2.JSON;
import com.jsowell.common.exception.ServiceException;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.constant.AdapayAccountTodoConstants;
import com.jsowell.pile.domain.AdapayMemberAccount;
import com.jsowell.pile.domain.PileMerchantInfo;
import com.jsowell.pile.service.AdapayAccountTodoService;
import com.jsowell.pile.service.AdapayMemberAccountService;
import com.jsowell.pile.service.PileMerchantInfoService;
import com.jsowell.system.constant.TodoTaskConstants;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.service.TodoTaskAssigneeService;
import com.jsowell.system.service.TodoTaskService;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 汇付账户状态与待办联动服务实现。
*/
@Service
public class AdapayAccountTodoServiceImpl implements AdapayAccountTodoService {
private static final Logger log = LoggerFactory.getLogger(AdapayAccountTodoServiceImpl.class);
private static final String CALLBACK_OPERATOR = "adapay-callback";
@Autowired
private AdapayMemberAccountService adapayMemberAccountService;
@Autowired
private PileMerchantInfoService pileMerchantInfoService;
@Autowired
private TodoTaskAssigneeService todoTaskAssigneeService;
@Autowired
private TodoTaskService todoTaskService;
@Override
@Transactional(rollbackFor = Exception.class)
public void handleCorpMemberFailed(AdapayMemberAccount account) {
validateAccount(account);
adapayMemberAccountService.updateAdapayMemberAccount(account);
createCorpMemberFailedTodos(account);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void handleCorpMemberSucceeded(AdapayMemberAccount account) {
validateAccount(account);
adapayMemberAccountService.updateAdapayMemberAccount(account);
if (StringUtils.isNotBlank(account.getMerchantId())) {
todoTaskService.completeByBusinessAsSystem(AdapayAccountTodoConstants.BUSINESS_TYPE,
account.getMerchantId(), null, CALLBACK_OPERATOR);
}
}
private void createCorpMemberFailedTodos(AdapayMemberAccount account) {
if (StringUtils.isBlank(account.getMerchantId())) {
log.warn("汇付企业开户失败记录缺少运营商无法创建待办accountId={}, adapayMemberId={}",
account.getId(), account.getAdapayMemberId());
return;
}
PileMerchantInfo merchantInfo;
try {
merchantInfo = pileMerchantInfoService.selectPileMerchantInfoById(
Long.parseLong(account.getMerchantId()));
} catch (NumberFormatException exception) {
log.warn("汇付企业开户失败记录运营商 ID 非法无法创建待办merchantId={}",
account.getMerchantId());
return;
}
if (merchantInfo == null || StringUtils.isBlank(merchantInfo.getDeptId())) {
log.warn("汇付企业开户失败记录未找到运营商部门无法创建待办merchantId={}",
account.getMerchantId());
return;
}
Long deptId;
try {
deptId = Long.parseLong(merchantInfo.getDeptId());
} catch (NumberFormatException exception) {
log.warn("汇付企业开户失败记录运营商部门 ID 非法无法创建待办merchantId={}, deptId={}",
account.getMerchantId(), merchantInfo.getDeptId());
return;
}
List<Long> assigneeUserIds = todoTaskAssigneeService.findActiveOperatorAdminUserIdsByDeptTree(deptId);
if (CollectionUtils.isEmpty(assigneeUserIds)) {
log.warn("汇付企业开户失败记录运营商部门下没有正常管理员无法创建待办merchantId={}, deptId={}",
account.getMerchantId(), deptId);
return;
}
String summary = buildSummary(account.getAuditDesc());
String routeParams = buildRouteParams(account.getMerchantId());
for (Long assigneeUserId : assigneeUserIds) {
String idempotentKey = AdapayAccountTodoConstants.TASK_TYPE_CORP_MEMBER_FAILED + ":"
+ AdapayAccountTodoConstants.BUSINESS_TYPE + ":" + account.getMerchantId() + ":"
+ assigneeUserId;
todoTaskService.createTask(TodoTaskCreateCommand.builder()
.taskType(AdapayAccountTodoConstants.TASK_TYPE_CORP_MEMBER_FAILED)
.title("汇付企业开户失败")
.summary(summary)
.content(truncate(account.getAuditDesc(), 20000))
.businessType(AdapayAccountTodoConstants.BUSINESS_TYPE)
.businessId(account.getMerchantId())
.routeName(AdapayAccountTodoConstants.ROUTE_NAME)
.routeParams(routeParams)
.assigneeUserId(assigneeUserId)
.assigneeMerchantId(account.getMerchantId())
.priority(TodoTaskConstants.PRIORITY_IMPORTANT)
.idempotentKey(idempotentKey)
.createBy(CALLBACK_OPERATOR)
.build());
}
}
private String buildSummary(String auditDesc) {
String summary = "汇付企业开户失败,请补充或修正开户资料";
if (StringUtils.isNotBlank(auditDesc)) {
summary += ",原因:" + auditDesc.trim();
}
return summary.length() <= 500 ? summary : summary.substring(0, 500);
}
private String truncate(String value, int maxLength) {
if (value == null || value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength);
}
private String buildRouteParams(String merchantId) {
Map<String, Object> params = new HashMap<>(1);
params.put("id", merchantId);
Map<String, Object> routeParams = new HashMap<>(1);
routeParams.put("params", params);
return JSON.toJSONString(routeParams);
}
private void validateAccount(AdapayMemberAccount account) {
if (account == null || account.getId() == null) {
throw new ServiceException("汇付账户记录不能为空");
}
}
}

View File

@@ -33,6 +33,9 @@ public interface TodoTaskService {
int completeByBusiness(String businessType, String businessId, Long assigneeUserId, int completeByBusiness(String businessType, String businessId, Long assigneeUserId,
Long operatorId, String operatorName); Long operatorId, String operatorName);
int completeByBusinessAsSystem(String businessType, String businessId, Long assigneeUserId,
String operatorName);
boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason); boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason);
boolean cancel(Long todoId, Long operatorId, String operatorName, String reason); boolean cancel(Long todoId, Long operatorId, String operatorName, String reason);

View File

@@ -33,9 +33,9 @@ public class TodoTaskServiceImpl implements TodoTaskService {
private static final String SYSTEM_OPERATOR = "system"; private static final String SYSTEM_OPERATOR = "system";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Set<String> DEFAULT_ROUTE_WHITELIST = new HashSet<>( private static final Set<String> DEFAULT_ROUTE_WHITELIST = new HashSet<>(
Arrays.asList("invoiceDetail", "financeDetail")); Arrays.asList("invoiceDetail", "financeDetail", "accountUserInfo"));
@Value("${todo.route-whitelist:invoiceDetail,financeDetail}") @Value("${todo.route-whitelist:invoiceDetail,financeDetail,accountUserInfo}")
private String routeWhitelist; private String routeWhitelist;
@Autowired @Autowired
@@ -162,6 +162,19 @@ public class TodoTaskServiceImpl implements TodoTaskService {
operatorId, normalizeOperator(operatorName, operatorId)); operatorId, normalizeOperator(operatorName, operatorId));
} }
@Override
@Transactional(rollbackFor = Exception.class)
public int completeByBusinessAsSystem(String businessType, String businessId, Long assigneeUserId,
String operatorName) {
String safeBusinessType = normalizeRequired(businessType, "业务类型", 64);
String safeBusinessId = normalizeRequired(businessId, "业务主键", 64);
if (assigneeUserId != null) {
validateUserId(assigneeUserId);
}
return todoTaskMapper.completeTodoByBusiness(safeBusinessType, safeBusinessId, assigneeUserId,
null, normalizeOperator(operatorName, null));
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason) { public boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason) {