diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/team/TeamBdRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/team/TeamBdRestController.java index c0818654..d7b11c86 100644 --- a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/team/TeamBdRestController.java +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/team/TeamBdRestController.java @@ -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 queryBdDetails(@Validated BdDetailsQryCmd cmd) { + return teamBdBillService.queryBdDetails(cmd); + } } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/bd/query/BdDetailsQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/bd/query/BdDetailsQryExe.java new file mode 100644 index 00000000..ba9fd9cb --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/bd/query/BdDetailsQryExe.java @@ -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 execute(BdDetailsQryCmd cmd) { + List allItems = new ArrayList<>(); + + // 1. 查询 Wallet 流水记录(Cash out, Transfer, Exchange) + if (shouldIncludeWalletRecords(cmd.getFilterType())) { + List 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 queryWalletRecords(Long userId, String filterType, String sysOrigin) { + List 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 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 queryBdIncomeRecords(Long userId) { + List 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 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 queryBdLeaderIncomeRecords(Long userId) { + List 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 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 items) { + // 收集所有需要查询用户信息的 userId + Set 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 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 paginate(List 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 pageItems = start < total ? + allItems.subList(start, end) : new ArrayList<>(); + + PageResult result = new PageResult<>(); + result.setRecords(pageItems); + result.setTotal((long) total); + result.setSize((long) pageSize); + result.setCurrent((long) pageNum); + + return result; + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/TeamBdBillServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/TeamBdBillServiceImpl.java index 07dc6dbd..bbf1bf85 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/TeamBdBillServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/TeamBdBillServiceImpl.java @@ -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 queryBdDetails(BdDetailsQryCmd cmd) { + return bdDetailsQryExe.execute(cmd); + } } diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/team/bd/BdDetailItemCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/team/bd/BdDetailItemCO.java new file mode 100644 index 00000000..71aaccec --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/team/bd/BdDetailItemCO.java @@ -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; + } +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/team/bd/BdDetailsQryCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/team/bd/BdDetailsQryCmd.java new file mode 100644 index 00000000..ddabf968 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/team/bd/BdDetailsQryCmd.java @@ -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; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/TeamBdBillService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/TeamBdBillService.java index 71dcf3d7..671d837d 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/TeamBdBillService.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/TeamBdBillService.java @@ -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 queryBdDetails(BdDetailsQryCmd cmd); }