新增 查询BD Details(聚合所有交易记录)

This commit is contained in:
tianfeng 2025-11-17 15:31:46 +08:00
parent 766be4ec80
commit 087707df47
6 changed files with 500 additions and 0 deletions

View File

@ -6,8 +6,10 @@ import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberCO;
import com.red.circle.other.app.dto.clientobject.team.*;
import com.red.circle.other.app.dto.clientobject.team.bd.BdDetailItemCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryWithdrawCmd;
import com.red.circle.other.app.dto.cmd.team.*;
import com.red.circle.other.app.dto.cmd.team.bd.BdDetailsQryCmd;
import com.red.circle.other.app.service.team.TeamBdHistoryService;
import com.red.circle.other.app.service.team.TeamBdService;
import com.red.circle.other.app.service.TeamBdBillService;
@ -416,5 +418,19 @@ public class TeamBdRestController extends BaseController {
return teamBdHistoryService.queryBdLeaderHistoryMore(cmd);
}
/**
* 查询BD Details聚合所有交易记录
*
* @param cmd 查询命令
* @return BD Details 分页结果
* @eo.name 查询BD Details
* @eo.url /withdraw/details
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/withdraw/details")
public List<BdDetailItemCO> queryBdDetails(@Validated BdDetailsQryCmd cmd) {
return teamBdBillService.queryBdDetails(cmd);
}
}

View File

@ -0,0 +1,336 @@
package com.red.circle.other.app.command.team.bd.query;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.dto.clientobject.team.bd.BdDetailItemCO;
import com.red.circle.other.app.dto.cmd.team.bd.BdDetailsQryCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalarySettlementRecord;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalarySettlementRecord;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import com.red.circle.wallet.inner.endpoint.bank.UserBankRunningWaterClient;
import com.red.circle.wallet.inner.model.cmd.UserBankRunningWaterQryCmd;
import com.red.circle.wallet.inner.model.dto.UserBankRunningWaterDTO;
import com.red.circle.wallet.inner.model.enums.UserBankWaterEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* BD Details 查询执行器
*
* @author tf
* @since 2025-11-17
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class BdDetailsQryExe {
private final UserBankRunningWaterClient bankRunningWaterClient;
private final MongoTemplate mongoTemplate;
private final UserProfileGateway userProfileGateway;
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yy/MM/dd HH:mm");
/**
* 执行查询
*/
public List<BdDetailItemCO> execute(BdDetailsQryCmd cmd) {
List<BdDetailItemCO> allItems = new ArrayList<>();
// 1. 查询 Wallet 流水记录Cash out, Transfer, Exchange
if (shouldIncludeWalletRecords(cmd.getFilterType())) {
List<BdDetailItemCO> walletItems = queryWalletRecords(
cmd.getReqUserId(), cmd.getFilterType(), cmd.getReqSysOrigin().getOrigin());
allItems.addAll(walletItems);
}
// 2. 批量查询并填充 Transfer 记录的用户信息
fillTransferUserInfo(allItems);
// 3. 按时间倒序排序
allItems.sort((a, b) -> b.getTime().compareTo(a.getTime()));
return allItems;
}
/**
* 查询钱包流水记录
*/
private List<BdDetailItemCO> queryWalletRecords(Long userId, String filterType, String sysOrigin) {
List<BdDetailItemCO> items = new ArrayList<>();
// 构建查询条件
UserBankRunningWaterQryCmd qryCmd = new UserBankRunningWaterQryCmd();
qryCmd.setUserId(userId);
qryCmd.setLimit(500);
qryCmd.setSysOrigin(sysOrigin);
// 调用 Wallet 服务查询流水
var response = bankRunningWaterClient.pageRunningWater(qryCmd);
if (response == null || !response.checkSuccess() || response.getBody() == null) {
return items;
}
List<UserBankRunningWaterDTO> waterList = response.getBody();
// 转换为 BdDetailItemCO
for (UserBankRunningWaterDTO water : waterList) {
BdDetailItemCO item = convertWalletRecord(water, filterType);
if (item != null) {
items.add(item);
}
}
return items;
}
/**
* 转换钱包流水记录
*/
private BdDetailItemCO convertWalletRecord(UserBankRunningWaterDTO water, String filterType) {
String event = water.getEvent();
if (event == null) {
return null;
}
// 根据 event 类型判断是否需要包含
BdDetailItemCO item = new BdDetailItemCO();
item.setRecordId(water.getId());
item.setTime(water.getCreateTime());
item.setTimeText(TIME_FORMATTER.format(water.getCreateTime().toLocalDateTime()));
// Cash out (提现)
if (event.equals(UserBankWaterEvent.WITHDRAW.name()) ||
event.equals(UserBankWaterEvent.APPLY_WITHDRAW.name())) {
if (!shouldIncludeType(filterType, "CASH_OUT")) {
return null;
}
item.setType("CASH_OUT");
item.setTypeText("Cash out");
// 提现是支出金额为负
item.setAmount(water.getAmount().negate());
item.setAmountText("$" + water.getAmount().negate().toString());
// 提现状态需要从额外的表查询这里暂时设置默认值
item.setStatus("Under review");
item.setStatusText("Under review");
return item;
}
// Transfer (转账)
if (event.equals(UserBankWaterEvent.TRANSFER_GOLD.name())) {
if (!shouldIncludeType(filterType, "TRANSFER")) {
return null;
}
item.setType("TRANSFER");
item.setTypeText("Transfer");
// 转账金币显示正值
item.setAmount(water.getAmount());
item.setAmountText("+" + water.getAmount() + " coins");
item.setUserInfo(new BdDetailItemCO.BdDetailUserInfo().setUserId(Long.valueOf(water.getTrackId())));
return item;
}
// Exchange coins (兑换金币)
if (event.equals(UserBankWaterEvent.EXCHANGE_GOLD_COINS.name()) ||
event.equals(UserBankWaterEvent.BILL_EXCHANGE_GOLD_COINS.name())) {
if (!shouldIncludeType(filterType, "EXCHANGE")) {
return null;
}
item.setType("EXCHANGE");
item.setTypeText("Exchange coins");
// 兑换是支出金额为负
item.setAmount(water.getAmount().negate());
item.setAmountText("-$" + water.getAmount().toString());
return item;
}
// BD 收入
if (event.equals(UserBankWaterEvent.BD_SALARY_SETTLEMENT.name())) {
if (!shouldIncludeType(filterType, "BD_SALARY_SETTLEMENT")) {
return null;
}
item.setType("BD_INCOME");
item.setTypeText("BD's income");
item.setAmount(water.getAmount());
item.setAmountText("+$" + water.getAmount().toString());
return item;
}
// BDLEADER 收入
if (event.equals(UserBankWaterEvent.BD_LEADER_SALARY_SETTLEMENT.name())) {
if (!shouldIncludeType(filterType, "BD_LEADER_SALARY_SETTLEMENT")) {
return null;
}
item.setType("BD_LEADER_INCOME");
item.setTypeText("BD Leader's income");
item.setAmount(water.getAmount());
item.setAmountText("+$" + water.getAmount().toString());
return item;
}
return null;
}
/**
* 查询 BD 收入记录
*/
private List<BdDetailItemCO> queryBdIncomeRecords(Long userId) {
List<BdDetailItemCO> items = new ArrayList<>();
// 构建查询条件
Criteria criteria = Criteria.where("bdUserId").is(userId)
.and("status").is(BdSettlementStatus.SUCCESS.name());
Query query = new Query(criteria)
.with(Sort.by(Sort.Direction.DESC, "createTime"))
.limit(1000);
List<BdSalarySettlementRecord> records = mongoTemplate.find(
query, BdSalarySettlementRecord.class);
// 转换为 BdDetailItemCO
for (BdSalarySettlementRecord record : records) {
BdDetailItemCO item = new BdDetailItemCO();
item.setRecordId(record.getTimeId());
item.setType("BD_INCOME");
item.setTypeText("BD's income");
item.setAmount(record.getSettlementSalary());
item.setAmountText("+$" + record.getSettlementSalary().toString());
item.setTime(record.getCreateTime());
item.setTimeText(TIME_FORMATTER.format(record.getCreateTime().toLocalDateTime()));
items.add(item);
}
return items;
}
/**
* 查询 BD Leader 收入记录
*/
private List<BdDetailItemCO> queryBdLeaderIncomeRecords(Long userId) {
List<BdDetailItemCO> items = new ArrayList<>();
// 构建查询条件
Criteria criteria = Criteria.where("bdLeaderUserId").is(userId)
.and("status").is(BdSettlementStatus.SUCCESS.name());
Query query = new Query(criteria)
.with(Sort.by(Sort.Direction.DESC, "createTime"))
.limit(1000);
List<BdLeaderSalarySettlementRecord> records = mongoTemplate.find(
query, BdLeaderSalarySettlementRecord.class);
// 转换为 BdDetailItemCO
for (BdLeaderSalarySettlementRecord record : records) {
BdDetailItemCO item = new BdDetailItemCO();
item.setRecordId(record.getTimeId());
item.setType("BD_LEADER_INCOME");
item.setTypeText("BD Leader's income");
item.setAmount(record.getSettlementSalary());
item.setAmountText("+$" + record.getSettlementSalary().toString());
item.setTime(record.getCreateTime());
item.setTimeText(TIME_FORMATTER.format(record.getCreateTime().toLocalDateTime()));
items.add(item);
}
return items;
}
/**
* 是否应该包含钱包记录类型
*/
private boolean shouldIncludeWalletRecords(String filterType) {
if (filterType == null || filterType.equals("ALL")) {
return true;
}
return filterType.equals("CASH_OUT") ||
filterType.equals("TRANSFER") ||
filterType.equals("EXCHANGE");
}
/**
* 是否应该包含指定类型
*/
private boolean shouldIncludeType(String filterType, String targetType) {
if (filterType == null || filterType.equals("ALL")) {
return true;
}
return filterType.equals(targetType);
}
/**
* 填充 Transfer 记录的用户信息
*/
private void fillTransferUserInfo(List<BdDetailItemCO> items) {
// 收集所有需要查询用户信息的 userId
Set<Long> userIds = items.stream()
.filter(item -> item.getUserInfo() != null && item.getUserInfo().getUserId() != null)
.map(item -> item.getUserInfo().getUserId())
.collect(Collectors.toSet());
if (userIds.isEmpty()) {
return;
}
// 批量查询用户信息
Map<Long, UserProfile> userProfileMap = userProfileGateway.mapByUserIds(userIds);
// 填充用户信息
items.stream()
.filter(item -> item.getUserInfo() != null && item.getUserInfo().getUserId() != null)
.forEach(item -> {
Long userId = item.getUserInfo().getUserId();
UserProfile profile = userProfileMap.get(userId);
if (profile != null) {
item.getUserInfo()
.setAccount(profile.getAccount())
.setUserNickname(profile.getUserNickname())
.setAvatar(profile.getUserAvatar());
}
});
}
/**
* 分页处理
*/
private PageResult<BdDetailItemCO> paginate(List<BdDetailItemCO> allItems, Integer pageNum, Integer pageSize) {
if (pageNum == null || pageNum < 1) {
pageNum = 1;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10;
}
int total = allItems.size();
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, total);
List<BdDetailItemCO> pageItems = start < total ?
allItems.subList(start, end) : new ArrayList<>();
PageResult<BdDetailItemCO> result = new PageResult<>();
result.setRecords(pageItems);
result.setTotal((long) total);
result.setSize((long) pageSize);
result.setCurrent((long) pageNum);
return result;
}
}

View File

@ -1,17 +1,21 @@
package com.red.circle.other.app.service;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.command.bd.query.TeamBdMemberBillDetailQryExe;
import com.red.circle.other.app.command.bd.query.TeamBdMemberBillListQryExe;
import com.red.circle.other.app.command.bd.query.TeamBdMonthlyIncomeQryExe;
import com.red.circle.other.app.command.bd.query.TeamBdWithdrawExe;
import com.red.circle.other.app.command.team.bd.query.BdDetailsQryExe;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberBillCO;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberBillDetailCO;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberCO;
import com.red.circle.other.app.dto.clientobject.TeamBdMonthlyIncomeCO;
import com.red.circle.other.app.dto.clientobject.team.bd.BdDetailItemCO;
import com.red.circle.other.app.dto.cmd.TeamBdMemberBillCmd;
import com.red.circle.other.app.dto.cmd.TeamBdMemberBillDetailCmd;
import com.red.circle.other.app.dto.cmd.TeamBdMonthlyIncomeCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryWithdrawCmd;
import com.red.circle.other.app.dto.cmd.team.bd.BdDetailsQryCmd;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -33,6 +37,7 @@ public class TeamBdBillServiceImpl implements TeamBdBillService {
private final TeamBdMemberBillDetailQryExe teamBdMemberBillDetailQryExe;
private final TeamBdMonthlyIncomeQryExe teamBdMonthlyIncomeQryExe;
private final TeamBdWithdrawExe teamBdWithdrawExe;
private final BdDetailsQryExe bdDetailsQryExe;
@Override
public TeamBdMemberCO queryMemberBillList(TeamBdMemberBillCmd cmd) {
@ -54,4 +59,8 @@ public class TeamBdBillServiceImpl implements TeamBdBillService {
teamBdWithdrawExe.applyWithdraw(cmd);
}
@Override
public List<BdDetailItemCO> queryBdDetails(BdDetailsQryCmd cmd) {
return bdDetailsQryExe.execute(cmd);
}
}

View File

@ -0,0 +1,103 @@
package com.red.circle.other.app.dto.clientobject.team.bd;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.dto.ClientObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.sql.Timestamp;
/**
* BD Details 明细项
*
* @author tf
* @since 2025-11-17
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BdDetailItemCO extends ClientObject {
/**
* 记录类型: CASH_OUT, TRANSFER, EXCHANGE, BD_INCOME, BD_LEADER_INCOME
*/
private String type;
/**
* 类型显示文本
*/
private String typeText;
/**
* 金额美元- 正数表示收入负数表示支出
*/
private BigDecimal amount;
/**
* 金额显示文本带符号
*/
private String amountText;
/**
* 时间
*/
private Timestamp time;
/**
* 时间显示文本
*/
private String timeText;
/**
* 状态: Under review, Approved, Rejection仅提现有
*/
private String status;
/**
* 状态显示文本
*/
private String statusText;
/**
* 用户信息仅转账有
*/
private BdDetailUserInfo userInfo;
/**
* 原始记录ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long recordId;
/**
* 详细信息扩展字段
*/
private Object details;
/**
* BD Details 用户信息
*/
@Data
@Accessors(chain = true)
public static class BdDetailUserInfo {
/**
* 用户ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 用户名称
*/
private String userNickname;
private String account;
/**
* 用户头像
*/
private String avatar;
}
}

View File

@ -0,0 +1,25 @@
package com.red.circle.other.app.dto.cmd.team.bd;
import com.red.circle.common.business.dto.PageQueryCmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* BD Details 查询命令
*
* @author tf
* @since 2025-11-17
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BdDetailsQryCmd extends AppExtCommand {
/**
* 筛选类型可选: CASH_OUT, TRANSFER, EXCHANGE, BD_INCOME, BD_LEADER_INCOME, ALL
* 默认为 ALL表示查询所有类型
*/
private String filterType;
}

View File

@ -1,13 +1,16 @@
package com.red.circle.other.app.service;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberBillCO;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberBillDetailCO;
import com.red.circle.other.app.dto.clientobject.TeamBdMemberCO;
import com.red.circle.other.app.dto.clientobject.TeamBdMonthlyIncomeCO;
import com.red.circle.other.app.dto.clientobject.team.bd.BdDetailItemCO;
import com.red.circle.other.app.dto.cmd.TeamBdMemberBillCmd;
import com.red.circle.other.app.dto.cmd.TeamBdMemberBillDetailCmd;
import com.red.circle.other.app.dto.cmd.TeamBdMonthlyIncomeCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryWithdrawCmd;
import com.red.circle.other.app.dto.cmd.team.bd.BdDetailsQryCmd;
import java.util.List;
@ -44,4 +47,12 @@ public interface TeamBdBillService {
TeamBdMonthlyIncomeCO queryMonthlyIncome(TeamBdMonthlyIncomeCmd cmd);
void applyWithdraw(LotteryWithdrawCmd cmd);
/**
* 查询 BD Details聚合所有交易记录.
*
* @param cmd 查询命令
* @return BD Details 分页结果
*/
List<BdDetailItemCO> queryBdDetails(BdDetailsQryCmd cmd);
}