From f49e89743185c9535ab2f03dcb52bd98ff4532b4 Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 17 Jul 2026 18:38:10 +0800 Subject: [PATCH] feat(wallet): route user withdrawals through dual review --- .../api/UserBankRunningWaterClientApi.java | 2 +- ...WithdrawalReviewIntegrationController.java | 104 +++++++++++++++++ .../user/UserBankBalanceBackServiceImpl.java | 24 +++- .../src/main/resources/application.yml | 7 ++ .../command/bank/UserBankWithdrawCmdExe.java | 15 ++- .../HyAppWithdrawalReviewClient.java | 108 ++++++++++++++++++ .../WithdrawalReviewSubmissionTask.java | 57 +++++++++ .../bank/UserBankWithdrawMoneyApply.java | 17 +++ .../UserBankWithdrawMoneyApplyService.java | 11 +- ...UserBankWithdrawMoneyApplyServiceImpl.java | 47 +++++++- .../UserBankRunningWaterClientEndpoint.java | 5 +- .../UserBankRunningWaterClientService.java | 2 +- ...UserBankRunningWaterClientServiceImpl.java | 4 +- .../src/main/resources/application.yml | 10 ++ 14 files changed, 395 insertions(+), 18 deletions(-) create mode 100644 rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/integration/WithdrawalReviewIntegrationController.java create mode 100644 rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/integration/HyAppWithdrawalReviewClient.java create mode 100644 rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/scheduler/WithdrawalReviewSubmissionTask.java diff --git a/rc-service/rc-inner-api/wallet-inner/wallet-inner-api/src/main/java/com/red/circle/wallet/inner/endpoint/bank/api/UserBankRunningWaterClientApi.java b/rc-service/rc-inner-api/wallet-inner/wallet-inner-api/src/main/java/com/red/circle/wallet/inner/endpoint/bank/api/UserBankRunningWaterClientApi.java index 172b7ca5..8cc78604 100644 --- a/rc-service/rc-inner-api/wallet-inner/wallet-inner-api/src/main/java/com/red/circle/wallet/inner/endpoint/bank/api/UserBankRunningWaterClientApi.java +++ b/rc-service/rc-inner-api/wallet-inner/wallet-inner-api/src/main/java/com/red/circle/wallet/inner/endpoint/bank/api/UserBankRunningWaterClientApi.java @@ -76,7 +76,7 @@ public interface UserBankRunningWaterClientApi { ResultResponse getWithdrawMoneyApplyById(@RequestParam("id") Long id); @PostMapping("/approvalMoneyApply") - ResultResponse approvalMoneyApply(@RequestBody UserBankApprovalMoneyApplyCmd apply); + ResultResponse approvalMoneyApply(@RequestBody UserBankApprovalMoneyApplyCmd apply); /** * 月用户工资流水 diff --git a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/integration/WithdrawalReviewIntegrationController.java b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/integration/WithdrawalReviewIntegrationController.java new file mode 100644 index 00000000..1253cc2e --- /dev/null +++ b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/integration/WithdrawalReviewIntegrationController.java @@ -0,0 +1,104 @@ +package com.red.circle.console.adapter.integration; + +import com.red.circle.console.app.service.app.user.UserBankBalanceBackService; +import com.red.circle.wallet.inner.model.cmd.ApprovalMoneyApplyCmd; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.List; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +/** + * hyapp-server 统一提现双审对 Likei 的终态回调入口. + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/integration/withdrawal-review", produces = MediaType.APPLICATION_JSON_VALUE) +public class WithdrawalReviewIntegrationController { + + private final UserBankBalanceBackService userBankBalanceBackService; + + @Value("${hyapp.withdrawal-review.callback-token:}") + private String callbackToken; + + @PostMapping("/decision") + public void applyDecision( + @RequestHeader(value = "Authorization", required = false) String authorization, + @RequestBody @Valid DecisionRequest request) { + verifyToken(authorization); + if (!"PASS".equals(request.getDecision()) && !"NOT_PASS".equals(request.getDecision())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "invalid withdrawal decision"); + } + if ("PASS".equals(request.getDecision()) + && (request.getCredentialUrls() == null || request.getCredentialUrls().isEmpty())) { + // 财务通过必须携带打款凭证,来源端再次校验,不能只依赖 admin 前端按钮门禁。 + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "withdrawal approval credential is required"); + } + + long sourceApplicationId; + try { + sourceApplicationId = Long.parseLong(request.getSourceApplicationId()); + } catch (NumberFormatException exception) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "invalid source withdrawal application id", exception); + } + userBankBalanceBackService.approvalMoneyApply(new ApprovalMoneyApplyCmd() + .setId(sourceApplicationId) + .setCredential(request.getCredentialUrls()) + .setApprovalStatus(request.getDecision()) + .setRemark(request.getRemark()) + .setUpdateUser(request.getReviewerUserId())); + } + + private void verifyToken(String authorization) { + boolean bearer = authorization != null && authorization.startsWith("Bearer "); + String provided = bearer ? authorization.substring("Bearer ".length()).trim() : ""; + String expected = callbackToken == null ? "" : callbackToken.trim(); + boolean valid = !provided.isEmpty() && !expected.isEmpty() + && MessageDigest.isEqual(provided.getBytes(StandardCharsets.UTF_8), + expected.getBytes(StandardCharsets.UTF_8)); + if (!valid) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, + "withdrawal callback authentication failed"); + } + } + + @Data + public static class DecisionRequest { + + @NotBlank + private String sourceApplicationId; + + @NotBlank + private String decision; + + private List credentialUrls; + + private String remark; + + @NotNull + @Positive + private Long reviewerUserId; + + private String reviewerName; + + @NotBlank + private String commandId; + } +} diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBankBalanceBackServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBankBalanceBackServiceImpl.java index e3d913d4..2b679cc4 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBankBalanceBackServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBankBalanceBackServiceImpl.java @@ -784,14 +784,20 @@ public class UserBankBalanceBackServiceImpl implements UserBankBalanceBackServic UserBankWithdrawMoneyApplyDTO apply = ResponseAssert.requiredSuccess( bankRunningWaterClient.getWithdrawMoneyApplyById(param.getId())); ResponseAssert.notNull(ConsoleErrorCode.NOT_FOUND_RECORD, apply); - ResponseAssert.isTrue(ConsoleErrorCode.PROCESSED, Objects - .equals(apply.getLatestApprovalStatus(), WithdrawMoneyApprovalProcessStatus.SUBMIT.name())); + if (!Objects.equals(apply.getLatestApprovalStatus(), + WithdrawMoneyApprovalProcessStatus.SUBMIT.name())) { + // hyapp-server 在 callback 成功、本地事务提交失败时会用同一 command 重试;同终态必须幂等成功,相反终态仍拒绝覆盖。 + ResponseAssert.isTrue(ConsoleErrorCode.PROCESSED, + Objects.equals(apply.getLatestApprovalStatus(), param.getApprovalStatus())); + return; + } UserBankCardDTO bankCard = ResponseAssert.requiredSuccess( userBankCardClient.getById(apply.getBankCardId())); ResponseAssert.notNull(BankErrorCode.INVALID_BANK_CARD_ERROR, bankCard); - bankRunningWaterClient.approvalMoneyApply(new UserBankApprovalMoneyApplyCmd() + Boolean updated = ResponseAssert.requiredSuccess( + bankRunningWaterClient.approvalMoneyApply(new UserBankApprovalMoneyApplyCmd() .setId(apply.getId()) .setUpdateUserOrigin(1) .setUpdateUser(param.getUpdateUser()) @@ -813,7 +819,17 @@ public class UserBankBalanceBackServiceImpl implements UserBankBalanceBackServic .setRemark(param.getRemark()) .setCreateTime(TimestampUtils.now()) ) - ); + )); + + if (!Boolean.TRUE.equals(updated)) { + // Mongo 使用 latestApprovalStatus=SUBMIT 条件更新;并发重复审核只有一个调用者能进入后续退款逻辑。 + UserBankWithdrawMoneyApplyDTO latest = ResponseAssert.requiredSuccess( + bankRunningWaterClient.getWithdrawMoneyApplyById(param.getId())); + ResponseAssert.notNull(ConsoleErrorCode.NOT_FOUND_RECORD, latest); + ResponseAssert.isTrue(ConsoleErrorCode.PROCESSED, + Objects.equals(latest.getLatestApprovalStatus(), param.getApprovalStatus())); + return; + } // 如果是驳回则将金额还给用户银行卡账户且插入返还日志 saveUserBankBalance(param, apply); diff --git a/rc-service/rc-service-console/console-start/src/main/resources/application.yml b/rc-service/rc-service-console/console-start/src/main/resources/application.yml index 524148bb..4e81bdda 100644 --- a/rc-service/rc-service-console/console-start/src/main/resources/application.yml +++ b/rc-service/rc-service-console/console-start/src/main/resources/application.yml @@ -45,6 +45,11 @@ management: health: show-details: always +# 与 hyapp-server 的 Aslan withdrawal source callback_token 保持一致,仅由生产环境注入。 +hyapp: + withdrawal-review: + callback-token: ${HYAPP_WITHDRAWAL_REVIEW_CALLBACK_TOKEN:} + framework: nacos: subscribeServices: @@ -71,3 +76,5 @@ red-circle: - /console/datav/online/room/count - /console/datav/aslan/region-country/statistics - /console/user/base/info/im/sig + # 该入口不走 console 登录态,控制器使用独立资金回调令牌做常量时间鉴权。 + - /console/integration/withdrawal-review/decision diff --git a/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/command/bank/UserBankWithdrawCmdExe.java b/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/command/bank/UserBankWithdrawCmdExe.java index 14246f13..e550868c 100644 --- a/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/command/bank/UserBankWithdrawCmdExe.java +++ b/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/command/bank/UserBankWithdrawCmdExe.java @@ -20,6 +20,7 @@ import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.tuple.PennyAmount; import com.red.circle.wallet.app.dto.clientobject.UserBankWithdrawResultCO; import com.red.circle.wallet.app.dto.cmd.UserBankWithdrawCmd; +import com.red.circle.wallet.app.scheduler.WithdrawalReviewSubmissionTask; import com.red.circle.wallet.domain.gateway.SalaryAccountGateway; import com.red.circle.wallet.infra.database.mongo.entity.bank.UserBankRunningWater; import com.red.circle.wallet.infra.database.mongo.service.bank.UserBankBalanceService; @@ -66,6 +67,7 @@ public class UserBankWithdrawCmdExe { private final UserBankBalanceService userBankBalanceService; private final UserBankRunningWaterService userBankRunningWaterService; private final HostSalaryWithdrawEnabledQryExe hostSalaryWithdrawEnabledQryExe; + private final WithdrawalReviewSubmissionTask withdrawalReviewSubmissionTask; public UserBankWithdrawResultCO execute(UserBankWithdrawCmd cmd) { @@ -122,7 +124,7 @@ public class UserBankWithdrawCmdExe { } // 申请详情 - userBankWithdrawMoneyApplyService.add( + UserBankWithdrawMoneyApply withdrawalApply = new UserBankWithdrawMoneyApply() .setId(applyId) .setSysOrigin(cmd.requireReqSysOrigin()) @@ -139,6 +141,10 @@ public class UserBankWithdrawCmdExe { .setCreateTime(TimestampUtils.now()) )) .setLatestApprovalStatus(WithdrawMoneyApprovalProcessStatus.SUBMIT.name()) + // 申请文档同时承担可靠投递 outbox;网络失败不会丢单,定时任务会按来源申请 ID 幂等重试。 + // 即使运行配置暂未开启也保留 PENDING;启用后定时任务会自动补投停机窗口内的申请。 + .setExternalReviewStatus("PENDING") + .setExternalReviewUpdatedTime(TimestampUtils.now()) .setServiceCharge(serviceCharge) .setActualAmount(actualAmount) .setCreateTime(TimestampUtils.now()) @@ -146,8 +152,11 @@ public class UserBankWithdrawCmdExe { .setCreateUser(cmd.requiredReqUserId()) .setUpdateUser(cmd.requiredReqUserId()) .setCreateUserOrigin(OpUserType.APP.getType()) - .setUpdateUserOrigin(OpUserType.APP.getType()) - ); + .setUpdateUserOrigin(OpUserType.APP.getType()); + userBankWithdrawMoneyApplyService.add(withdrawalApply); + + // 首次尽量实时进入运营队列;失败只落 outbox 错误,不回滚已提交的用户提现,避免出现扣款成功但接口报失败的假象。 + withdrawalReviewSubmissionTask.submit(withdrawalApply, bankCard); walletBankCacheService.setMonthWithdrawal(cmd.requiredReqUserId()); redisService.unlock(key); diff --git a/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/integration/HyAppWithdrawalReviewClient.java b/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/integration/HyAppWithdrawalReviewClient.java new file mode 100644 index 00000000..85e0483e --- /dev/null +++ b/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/integration/HyAppWithdrawalReviewClient.java @@ -0,0 +1,108 @@ +package com.red.circle.wallet.app.integration; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.red.circle.wallet.infra.database.mongo.entity.bank.UserBankCard; +import com.red.circle.wallet.infra.database.mongo.entity.bank.UserBankWithdrawMoneyApply; +import java.math.BigDecimal; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * hyapp-server 统一提现双审提交客户端. + */ +@Component +public class HyAppWithdrawalReviewClient { + + private final ObjectMapper objectMapper; + private final HttpClient httpClient; + private final boolean enabled; + private final String submitUrl; + private final String submitToken; + private final String appCode; + private final String sourceSystem; + private final Duration requestTimeout; + + public HyAppWithdrawalReviewClient( + ObjectMapper objectMapper, + @Value("${hyapp.withdrawal-review.enabled:false}") boolean enabled, + @Value("${hyapp.withdrawal-review.submit-url:}") String submitUrl, + @Value("${hyapp.withdrawal-review.submit-token:}") String submitToken, + @Value("${hyapp.withdrawal-review.app-code:aslan}") String appCode, + @Value("${hyapp.withdrawal-review.source-system:likei}") String sourceSystem, + @Value("${hyapp.withdrawal-review.request-timeout-ms:5000}") long requestTimeoutMs) { + this.objectMapper = objectMapper; + this.enabled = enabled; + this.submitUrl = submitUrl.trim(); + this.submitToken = submitToken.trim(); + this.appCode = appCode.trim().toLowerCase(); + this.sourceSystem = sourceSystem.trim().toLowerCase(); + this.requestTimeout = Duration.ofMillis(Math.max(1000, requestTimeoutMs)); + this.httpClient = HttpClient.newBuilder() + .connectTimeout(this.requestTimeout) + .build(); + } + + public boolean isEnabled() { + return enabled; + } + + public String submit(UserBankWithdrawMoneyApply apply, UserBankCard bankCard) { + if (!enabled) { + throw new IllegalStateException("hyapp withdrawal review is disabled"); + } + if (submitUrl.isBlank() || submitToken.isBlank()) { + throw new IllegalStateException("hyapp withdrawal review submit config is incomplete"); + } + if (apply == null || bankCard == null || apply.getAmount() == null) { + throw new IllegalArgumentException("withdrawal review application is incomplete"); + } + + BigDecimal normalizedAmount = apply.getAmount().stripTrailingZeros(); + long amountMinor = normalizedAmount.movePointRight(2).longValueExact(); + Map payload = new LinkedHashMap<>(); + payload.put("appCode", appCode); + payload.put("sourceSystem", sourceSystem); + payload.put("sourceApplicationId", String.valueOf(apply.getId())); + payload.put("userId", String.valueOf(apply.getSubmitUserId())); + payload.put("salaryAssetType", apply.getAmountType()); + payload.put("withdrawAmount", normalizedAmount.toPlainString()); + payload.put("withdrawAmountMinor", amountMinor); + payload.put("withdrawMethod", bankCard.getCardType()); + payload.put("withdrawAddress", bankCard.getCardNo()); + payload.put("createdAtMs", apply.getCreateTime().getTime()); + + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(submitUrl)) + .timeout(requestTimeout) + .header("Authorization", "Bearer " + submitToken) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofByteArray(objectMapper.writeValueAsBytes(payload))) + .build(); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IllegalStateException( + "hyapp withdrawal review returned " + response.statusCode() + ": " + response.body()); + } + JsonNode data = objectMapper.readTree(response.body()).path("data"); + String reviewApplicationId = data.path("id").asText("").trim(); + if (reviewApplicationId.isBlank()) { + throw new IllegalStateException("hyapp withdrawal review response id is empty"); + } + return reviewApplicationId; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("hyapp withdrawal review request interrupted", exception); + } catch (Exception exception) { + throw new IllegalStateException("submit hyapp withdrawal review failed", exception); + } + } +} diff --git a/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/scheduler/WithdrawalReviewSubmissionTask.java b/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/scheduler/WithdrawalReviewSubmissionTask.java new file mode 100644 index 00000000..b38df3cb --- /dev/null +++ b/rc-service/rc-service-wallet/wallet-application/src/main/java/com/red/circle/wallet/app/scheduler/WithdrawalReviewSubmissionTask.java @@ -0,0 +1,57 @@ +package com.red.circle.wallet.app.scheduler; + +import com.red.circle.wallet.app.integration.HyAppWithdrawalReviewClient; +import com.red.circle.wallet.infra.database.mongo.entity.bank.UserBankCard; +import com.red.circle.wallet.infra.database.mongo.entity.bank.UserBankWithdrawMoneyApply; +import com.red.circle.wallet.infra.database.mongo.service.bank.UserBankCardService; +import com.red.circle.wallet.infra.database.mongo.service.bank.UserBankWithdrawMoneyApplyService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 将 Likei 提现申请可靠投递到 hyapp-server 的统一运营/财务双审队列. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class WithdrawalReviewSubmissionTask { + + private static final int BATCH_SIZE = 50; + + private final HyAppWithdrawalReviewClient reviewClient; + private final UserBankWithdrawMoneyApplyService withdrawApplyService; + private final UserBankCardService userBankCardService; + + @Scheduled(fixedDelayString = "${hyapp.withdrawal-review.retry-delay-ms:30000}") + public void retryPendingApplications() { + if (!reviewClient.isEnabled()) { + return; + } + List applications = + withdrawApplyService.listPendingExternalReview(BATCH_SIZE); + for (UserBankWithdrawMoneyApply application : applications) { + submit(application, null); + } + } + + /** + * 首次提交和定时补偿共用同一入口;失败只记录在原申请文档,用户扣款申请仍然成功,后续由定时任务继续投递. + */ + public void submit(UserBankWithdrawMoneyApply application, UserBankCard knownBankCard) { + if (!reviewClient.isEnabled() || application == null) { + return; + } + try { + UserBankCard bankCard = knownBankCard == null + ? userBankCardService.getById(application.getBankCardId()) : knownBankCard; + String reviewApplicationId = reviewClient.submit(application, bankCard); + withdrawApplyService.markExternalReviewSubmitted(application.getId(), reviewApplicationId); + } catch (Exception exception) { + withdrawApplyService.markExternalReviewFailed(application.getId(), exception.getMessage()); + log.error("Submit withdrawal {} to hyapp review failed", application.getId(), exception); + } + } +} diff --git a/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/entity/bank/UserBankWithdrawMoneyApply.java b/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/entity/bank/UserBankWithdrawMoneyApply.java index ae8e2764..6c08b343 100644 --- a/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/entity/bank/UserBankWithdrawMoneyApply.java +++ b/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/entity/bank/UserBankWithdrawMoneyApply.java @@ -92,6 +92,23 @@ public class UserBankWithdrawMoneyApply implements Serializable { */ private String latestApprovalStatus; + /** + * 统一提现双审投递状态:PENDING/FAILED/SUBMITTED. + */ + private String externalReviewStatus; + + /** + * hyapp-server 生成的统一审核申请 ID. + */ + private String externalReviewApplicationId; + + /** + * 最近一次投递错误,仅用于后台排障,不参与业务判断. + */ + private String externalReviewLastError; + + private Timestamp externalReviewUpdatedTime; + /** * 结算结果; */ diff --git a/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/UserBankWithdrawMoneyApplyService.java b/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/UserBankWithdrawMoneyApplyService.java index 66d6e7b2..59240134 100644 --- a/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/UserBankWithdrawMoneyApplyService.java +++ b/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/UserBankWithdrawMoneyApplyService.java @@ -42,9 +42,18 @@ public interface UserBankWithdrawMoneyApplyService { */ UserBankWithdrawMoneyApply getById(Long id); + /** + * 获取尚未成功进入统一双审中心的申请;固定小批量轮询,避免全表加载. + */ + List listPendingExternalReview(int limit); + + void markExternalReviewSubmitted(Long id, String reviewApplicationId); + + void markExternalReviewFailed(Long id, String errorMessage); + /** * 审核申请记录. */ - void approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply); + boolean approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply); } diff --git a/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/impl/UserBankWithdrawMoneyApplyServiceImpl.java b/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/impl/UserBankWithdrawMoneyApplyServiceImpl.java index e5212312..30310585 100644 --- a/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/impl/UserBankWithdrawMoneyApplyServiceImpl.java +++ b/rc-service/rc-service-wallet/wallet-infrastructure/src/main/java/com/red/circle/wallet/infra/database/mongo/service/bank/impl/UserBankWithdrawMoneyApplyServiceImpl.java @@ -1,5 +1,6 @@ package com.red.circle.wallet.infra.database.mongo.service.bank.impl; +import com.mongodb.client.result.UpdateResult; import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.date.TimestampUtils; import com.red.circle.tool.core.date.ZonedDateTimeUtils; @@ -122,9 +123,49 @@ public class UserBankWithdrawMoneyApplyServiceImpl implements UserBankWithdrawMo } @Override - public void approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) { + public List listPendingExternalReview(int limit) { + Criteria criteria = Criteria.where("latestApprovalStatus").is("SUBMIT") + .and("externalReviewStatus").ne("SUBMITTED"); + return mongoTemplate.find(Query.query(criteria) + .with(Sort.by(Sort.Order.asc("createTime"))) + .limit(Math.max(1, Math.min(limit, 100))), + UserBankWithdrawMoneyApply.class); + } - mongoTemplate.updateFirst(Query.query(Criteria.where("id").in(apply.getId())), + @Override + public void markExternalReviewSubmitted(Long id, String reviewApplicationId) { + mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id) + .and("latestApprovalStatus").is("SUBMIT")), + new Update() + .set("externalReviewStatus", "SUBMITTED") + .set("externalReviewApplicationId", reviewApplicationId) + .set("externalReviewLastError", "") + .set("externalReviewUpdatedTime", TimestampUtils.now()), + UserBankWithdrawMoneyApply.class); + } + + @Override + public void markExternalReviewFailed(Long id, String errorMessage) { + String normalizedError = StringUtils.isBlank(errorMessage) ? "unknown error" : errorMessage; + if (normalizedError.length() > 500) { + normalizedError = normalizedError.substring(0, 500); + } + mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id) + .and("latestApprovalStatus").is("SUBMIT") + .and("externalReviewStatus").ne("SUBMITTED")), + new Update() + .set("externalReviewStatus", "FAILED") + .set("externalReviewLastError", normalizedError) + .set("externalReviewUpdatedTime", TimestampUtils.now()), + UserBankWithdrawMoneyApply.class); + } + + @Override + public boolean approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) { + + // 终态更新必须以 SUBMIT 为前置条件;重复回调不能再次进入退款逻辑。 + UpdateResult result = mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(apply.getId()) + .and("latestApprovalStatus").is("SUBMIT")), new Update() .set("updateTime", TimestampUtils.now()) .set("updateUser", apply.getUpdateUser()) @@ -133,7 +174,7 @@ public class UserBankWithdrawMoneyApplyServiceImpl implements UserBankWithdrawMo .set("settlementResult", apply.getSettlementResult()) .push("approvalProcesses", apply.getProcess()), UserBankWithdrawMoneyApply.class); - + return result.getModifiedCount() == 1; } } diff --git a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/endpoint/bank/UserBankRunningWaterClientEndpoint.java b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/endpoint/bank/UserBankRunningWaterClientEndpoint.java index 7404f5df..95fa6027 100644 --- a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/endpoint/bank/UserBankRunningWaterClientEndpoint.java +++ b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/endpoint/bank/UserBankRunningWaterClientEndpoint.java @@ -97,9 +97,8 @@ public class UserBankRunningWaterClientEndpoint implements UserBankRunningWaterC @Override - public ResultResponse approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) { - userBankRunningWaterClientService.approvalMoneyApply(apply); - return ResultResponse.success(); + public ResultResponse approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) { + return ResultResponse.success(userBankRunningWaterClientService.approvalMoneyApply(apply)); } @Override diff --git a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/UserBankRunningWaterClientService.java b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/UserBankRunningWaterClientService.java index f8c3cc2f..6558f8ca 100644 --- a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/UserBankRunningWaterClientService.java +++ b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/UserBankRunningWaterClientService.java @@ -73,7 +73,7 @@ public interface UserBankRunningWaterClientService { /** * 审核申请记录. */ - void approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply); + boolean approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply); /** * 月用户工资流水. diff --git a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/impl/UserBankRunningWaterClientServiceImpl.java b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/impl/UserBankRunningWaterClientServiceImpl.java index 706ece75..74bc7df3 100644 --- a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/impl/UserBankRunningWaterClientServiceImpl.java +++ b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/bank/impl/UserBankRunningWaterClientServiceImpl.java @@ -93,8 +93,8 @@ public class UserBankRunningWaterClientServiceImpl implements UserBankRunningWat } @Override - public void approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) { - bankWithdrawMoneyApplyService.approvalMoneyApply(apply); + public boolean approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) { + return bankWithdrawMoneyApplyService.approvalMoneyApply(apply); } @Override diff --git a/rc-service/rc-service-wallet/wallet-start/src/main/resources/application.yml b/rc-service/rc-service-wallet/wallet-start/src/main/resources/application.yml index 1e26758d..e16463b5 100644 --- a/rc-service/rc-service-wallet/wallet-start/src/main/resources/application.yml +++ b/rc-service/rc-service-wallet/wallet-start/src/main/resources/application.yml @@ -29,4 +29,14 @@ management: health: show-details: always +# 生产由环境变量开启;本地/测试默认关闭,避免误把开发提现投递到真实财务工作台。 +hyapp: + withdrawal-review: + enabled: ${HYAPP_WITHDRAWAL_REVIEW_ENABLED:false} + submit-url: ${HYAPP_WITHDRAWAL_REVIEW_SUBMIT_URL:} + submit-token: ${HYAPP_WITHDRAWAL_REVIEW_SUBMIT_TOKEN:} + app-code: ${HYAPP_WITHDRAWAL_REVIEW_APP_CODE:aslan} + source-system: ${HYAPP_WITHDRAWAL_REVIEW_SOURCE_SYSTEM:likei} + request-timeout-ms: ${HYAPP_WITHDRAWAL_REVIEW_TIMEOUT_MS:5000} + retry-delay-ms: ${HYAPP_WITHDRAWAL_REVIEW_RETRY_DELAY_MS:30000}