feat(wallet): route user withdrawals through dual review
This commit is contained in:
parent
e25aed3085
commit
f49e897431
@ -76,7 +76,7 @@ public interface UserBankRunningWaterClientApi {
|
||||
ResultResponse<UserBankWithdrawMoneyApplyDTO> getWithdrawMoneyApplyById(@RequestParam("id") Long id);
|
||||
|
||||
@PostMapping("/approvalMoneyApply")
|
||||
ResultResponse<Void> approvalMoneyApply(@RequestBody UserBankApprovalMoneyApplyCmd apply);
|
||||
ResultResponse<Boolean> approvalMoneyApply(@RequestBody UserBankApprovalMoneyApplyCmd apply);
|
||||
|
||||
/**
|
||||
* 月用户工资流水
|
||||
|
||||
@ -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<String> credentialUrls;
|
||||
|
||||
private String remark;
|
||||
|
||||
@NotNull
|
||||
@Positive
|
||||
private Long reviewerUserId;
|
||||
|
||||
private String reviewerName;
|
||||
|
||||
@NotBlank
|
||||
private String commandId;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<String, Object> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<UserBankWithdrawMoneyApply> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 结算结果;
|
||||
*/
|
||||
|
||||
@ -42,9 +42,18 @@ public interface UserBankWithdrawMoneyApplyService {
|
||||
*/
|
||||
UserBankWithdrawMoneyApply getById(Long id);
|
||||
|
||||
/**
|
||||
* 获取尚未成功进入统一双审中心的申请;固定小批量轮询,避免全表加载.
|
||||
*/
|
||||
List<UserBankWithdrawMoneyApply> listPendingExternalReview(int limit);
|
||||
|
||||
void markExternalReviewSubmitted(Long id, String reviewApplicationId);
|
||||
|
||||
void markExternalReviewFailed(Long id, String errorMessage);
|
||||
|
||||
/**
|
||||
* 审核申请记录.
|
||||
*/
|
||||
void approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply);
|
||||
boolean approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply);
|
||||
|
||||
}
|
||||
|
||||
@ -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<UserBankWithdrawMoneyApply> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -97,9 +97,8 @@ public class UserBankRunningWaterClientEndpoint implements UserBankRunningWaterC
|
||||
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) {
|
||||
userBankRunningWaterClientService.approvalMoneyApply(apply);
|
||||
return ResultResponse.success();
|
||||
public ResultResponse<Boolean> approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply) {
|
||||
return ResultResponse.success(userBankRunningWaterClientService.approvalMoneyApply(apply));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -73,7 +73,7 @@ public interface UserBankRunningWaterClientService {
|
||||
/**
|
||||
* 审核申请记录.
|
||||
*/
|
||||
void approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply);
|
||||
boolean approvalMoneyApply(UserBankApprovalMoneyApplyCmd apply);
|
||||
|
||||
/**
|
||||
* 月用户工资流水.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user