新增bd bdleader 工资结算

This commit is contained in:
tianfeng 2025-10-29 16:07:24 +08:00
parent 068a58cf00
commit dd6314cd74
44 changed files with 2516 additions and 1 deletions

View File

@ -2,6 +2,7 @@ package com.red.circle.order.inner.endpoint.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import com.red.circle.order.inner.model.dto.inapp.BatchUserRechargeStatisticsDTO;
import com.red.circle.order.inner.model.dto.inapp.CountUserInAppPurchaseDTO;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO;
import java.time.LocalDateTime;
@ -51,4 +52,21 @@ public interface InAppPurchaseDetailsClientApi {
@PostMapping("/statistics")
ResultResponse<Map<String, Object>> statistics(
@RequestBody @Validated InAppPurchaseDetailsQryCmd qryCmd);
/**
* 批量查询用户充值统计.
* 统计指定用户列表在指定时间范围内的支付成功订单金额总和.
*
* @param userIds 用户ID列表
* @param sysOrigin 系统来源
* @param startTime 开始时间可选
* @param endTime 结束时间可选
* @return 用户充值统计列表包含用户ID总金额订单数量
*/
@PostMapping("/batchUserRechargeStatistics")
ResultResponse<List<BatchUserRechargeStatisticsDTO>> batchUserRechargeStatistics(
@RequestBody List<Long> userIds,
@RequestParam("sysOrigin") String sysOrigin,
@RequestParam(value = "startTime", required = false) LocalDateTime startTime,
@RequestParam(value = "endTime", required = false) LocalDateTime endTime);
}

View File

@ -0,0 +1,35 @@
package com.red.circle.order.inner.model.dto.inapp;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 批量用户充值统计 DTO.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class BatchUserRechargeStatisticsDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户ID.
*/
private Long userId;
/**
* 支付成功的总金额美元.
*/
private BigDecimal totalAmount;
/**
* 支付成功的订单数量.
*/
private Integer orderCount;
}

View File

@ -0,0 +1,31 @@
package com.red.circle.other.inner.enums.team;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 团队工资统计 CO.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class TeamSalaryStatisticsCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 团队成员用户ID列表.
*/
private List<Long> userIds;
/**
* 工资总金额美元.
*/
private BigDecimal totalSalary;
}

View File

@ -141,6 +141,10 @@ public enum UserBankWaterEvent {
* 邀新用户红包提现.
*/
NEW_USER_RED_PACKET_WITHDRAW("New user red packet withdraw"),
BD_SALARY_SETTLEMENT("BD工资结算"),
BD_LEADER_SALARY_SETTLEMENT("BD Leader工资结算"),
;
private final String describe;

View File

@ -88,4 +88,19 @@ public interface InAppPurchaseDetailsService {
List<InAppPurchaseDetails> listAll();
void updateMetadata(String id, Map<String, String> metadata);
/**
* 批量统计用户充值.
*
* @param userIds 用户ID列表
* @param sysOrigin 系统来源
* @param startTime 开始时间可选
* @param endTime 结束时间可选
* @return 用户充值统计列表按用户ID分组
*/
List<CountUserInAppPurchase> batchUserRechargeStatistics(
List<Long> userIds,
String sysOrigin,
LocalDateTime startTime,
LocalDateTime endTime);
}

View File

@ -26,4 +26,9 @@ public class CountUserInAppPurchase implements Serializable {
@JsonSerialize(using = ToStringSerializer.class)
private BigDecimal amount;
/**
* 订单数量.
*/
private Integer orderCount;
}

View File

@ -348,4 +348,57 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
InAppPurchaseDetails.class
);
}
@Override
public List<CountUserInAppPurchase> batchUserRechargeStatistics(
List<Long> userIds,
String sysOrigin,
LocalDateTime startTime,
LocalDateTime endTime) {
if (CollectionUtils.isEmpty(userIds)) {
return Lists.newArrayList();
}
// 构建查询条件
Criteria criteria = Criteria.where("sysOrigin").is(sysOrigin)
.and("acceptUserId").in(userIds)
.and("status").is(InAppPurchaseStatus.SUCCESS); // 只统计支付成功的订单
// 如果指定了时间范围添加时间条件
if (Objects.nonNull(startTime) && Objects.nonNull(endTime)) {
criteria.andOperator(
Criteria.where("createTime").gte(startTime),
Criteria.where("createTime").lte(endTime)
);
}
// 使用 MongoDB 聚合管道按用户ID分组统计
AggregationResults<BasicDBObject> aggregationResults = mongoTemplate.aggregate(
Aggregation.newAggregation(
Aggregation.match(criteria),
Aggregation.group("acceptUserId")
.sum("amountUsd").as("totalAmount")
.count().as("orderCount"),
Aggregation.sort(Sort.by(Sort.Order.asc("_id"))) // 按用户ID排序
),
InAppPurchaseDetails.class,
BasicDBObject.class);
List<BasicDBObject> basicDBObjectList = aggregationResults.getMappedResults();
if (CollectionUtils.isEmpty(basicDBObjectList)) {
return Lists.newArrayList();
}
// 转换为 CountUserInAppPurchase 对象
return basicDBObjectList.stream().map(obj -> new CountUserInAppPurchase()
.setUserId(obj.getLong("_id")) // MongoDB group _id 就是 acceptUserId
.setAmount(obj.get("totalAmount") != null
? new BigDecimal(obj.get("totalAmount").toString())
: BigDecimal.ZERO)
.setOrderCount(obj.getInt("orderCount", 0))
)
.collect(Collectors.toList());
}
}

View File

@ -5,6 +5,7 @@ import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetai
import com.red.circle.order.infra.database.mongo.order.entity.assist.CountUserInAppPurchase;
import com.red.circle.order.infra.database.mongo.order.query.InAppPurchaseDetailsQuery;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import com.red.circle.order.inner.model.dto.inapp.BatchUserRechargeStatisticsDTO;
import com.red.circle.order.inner.model.dto.inapp.CountUserInAppPurchaseDTO;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO;
import java.util.List;
@ -23,4 +24,29 @@ public interface InAppPurchaseDetailsInnerConvert {
List<InAppPurchaseDetailsDTO> toListInAppPurchaseDetailsDTO(List<InAppPurchaseDetails> details);
List<CountUserInAppPurchaseDTO> toListCountUserInAppPurchase(List<CountUserInAppPurchase> list);
/**
* 转换为批量用户充值统计DTO列表.
*
* @param list CountUserInAppPurchase列表
* @return BatchUserRechargeStatisticsDTO列表
*/
List<BatchUserRechargeStatisticsDTO> toBatchUserRechargeStatisticsDTOList(List<CountUserInAppPurchase> list);
/**
* 转换为批量用户充值统计DTO.
*
* @param source CountUserInAppPurchase
* @return BatchUserRechargeStatisticsDTO
*/
default BatchUserRechargeStatisticsDTO toBatchUserRechargeStatisticsDTO(CountUserInAppPurchase source) {
if (source == null) {
return null;
}
BatchUserRechargeStatisticsDTO dto = new BatchUserRechargeStatisticsDTO();
dto.setUserId(source.getUserId());
dto.setTotalAmount(source.getAmount());
dto.setOrderCount(source.getOrderCount());
return dto;
}
}

View File

@ -3,6 +3,7 @@ package com.red.circle.order.inner.endpoint;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.order.inner.endpoint.api.InAppPurchaseDetailsClientApi;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import com.red.circle.order.inner.model.dto.inapp.BatchUserRechargeStatisticsDTO;
import com.red.circle.order.inner.model.dto.inapp.CountUserInAppPurchaseDTO;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO;
import com.red.circle.order.inner.service.InAppPurchaseDetailsClientService;
@ -57,4 +58,16 @@ public class InAppPurchaseDetailsClientEndpoint implements InAppPurchaseDetailsC
);
}
@Override
public ResultResponse<List<BatchUserRechargeStatisticsDTO>> batchUserRechargeStatistics(
List<Long> userIds,
String sysOrigin,
LocalDateTime startTime,
LocalDateTime endTime) {
return ResultResponse.success(
inAppPurchaseDetailsClientService.batchUserRechargeStatistics(
userIds, sysOrigin, startTime, endTime)
);
}
}

View File

@ -31,4 +31,19 @@ public interface InAppPurchaseDetailsClientService {
List<CountUserInAppPurchaseDTO> countUserInAppPurchase(String sysOrigin, LocalDateTime startTime, LocalDateTime endTime, Integer limit);
/**
* 批量查询用户充值统计.
*
* @param userIds 用户ID列表
* @param sysOrigin 系统来源
* @param startTime 开始时间可选
* @param endTime 结束时间可选
* @return 用户充值统计列表
*/
List<com.red.circle.order.inner.model.dto.inapp.BatchUserRechargeStatisticsDTO> batchUserRechargeStatistics(
List<Long> userIds,
String sysOrigin,
LocalDateTime startTime,
LocalDateTime endTime);
}

View File

@ -55,4 +55,19 @@ public class InAppPurchaseDetailsClientServiceImpl implements InAppPurchaseDetai
return inAppPurchaseDetailsInnerConvert.toListCountUserInAppPurchase(countUserInAppPurchases);
}
@Override
public List<com.red.circle.order.inner.model.dto.inapp.BatchUserRechargeStatisticsDTO> batchUserRechargeStatistics(
List<Long> userIds,
String sysOrigin,
LocalDateTime startTime,
LocalDateTime endTime) {
List<CountUserInAppPurchase> countUserInAppPurchases =
inAppPurchaseDetailsService.batchUserRechargeStatistics(
userIds, sysOrigin, startTime, endTime);
return inAppPurchaseDetailsInnerConvert.toBatchUserRechargeStatisticsDTOList(
countUserInAppPurchases);
}
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.component.redis.annotation.TaskCacheLock;
import com.red.circle.other.app.service.BdLeaderSalarySettlementService;
import com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* BD Leader 工资结算定时任务.
* 每月1号和16号 0点执行半月结算
*
* @author AI Assistant
* @since 2025-10-29
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class BdLeaderSalarySettlementTask {
private final BdLeaderSalarySettlementService bdLeaderSalarySettlementService;
/**
* 每月1号和16号 0点执行半月结算.
*/
@Scheduled(cron = "0 0 0 1,16 * ?", zone = "Asia/Riyadh")
@TaskCacheLock(key = "BD_LEADER_SALARY_SETTLEMENT", expireSecond = 86400)
public void processBdLeaderSalarySettlement() {
long startTime = System.currentTimeMillis();
log.warn("========== BD Leader工资结算定时任务开始 ==========");
try {
// 判断是上半月还是下半月
Integer billBelong = TeamBillCycleUtils.getCalcBillBelong();
String billTitle = TeamBillCycleUtils.parseBillBelongToDateRangeStr(billBelong);
log.warn("结算周期:{} - {}", billBelong, billTitle);
// 执行 BD Leader 工资结算
bdLeaderSalarySettlementService.processAllBdLeaderSettlement(billBelong, billTitle);
log.warn("========== BD Leader工资结算定时任务完成耗时{} ms ==========",
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("========== BD Leader工资结算定时任务异常 ==========", e);
}
}
/**
* 测试方法手动触发.
*/
public void processBdLeaderSalarySettlementTest(Integer billBelong) {
String billTitle = TeamBillCycleUtils.parseBillBelongToDateRangeStr(billBelong);
bdLeaderSalarySettlementService.processAllBdLeaderSettlement(billBelong, billTitle);
}
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.component.redis.annotation.TaskCacheLock;
import com.red.circle.other.app.service.BdSalarySettlementService;
import com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* BD 工资结算定时任务.
* 每月1号和16号 0点执行半月结算
*
* @author AI Assistant
* @since 2025-10-29
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class BdSalarySettlementTask {
private final BdSalarySettlementService bdSalarySettlementService;
/**
* 每月1号和16号 0点执行半月结算.
*/
@Scheduled(cron = "0 0 0 1,16 * ?", zone = "Asia/Riyadh")
@TaskCacheLock(key = "BD_SALARY_SETTLEMENT", expireSecond = 86400)
public void processBdSalarySettlement() {
long startTime = System.currentTimeMillis();
log.warn("========== BD工资结算定时任务开始 ==========");
try {
// 判断是上半月还是下半月
Integer billBelong = TeamBillCycleUtils.getCalcBillBelong();
String billTitle = TeamBillCycleUtils.parseBillBelongToDateRangeStr(billBelong);
log.warn("结算周期:{} - {}", billBelong, billTitle);
// 执行 BD 工资结算
bdSalarySettlementService.processAllBdSettlement(billBelong, billTitle);
log.warn("========== BD工资结算定时任务完成耗时{} ms ==========",
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("========== BD工资结算定时任务异常 ==========", e);
}
}
/**
* 测试方法手动触发.
*/
public void processBdSalarySettlementTest(Integer billBelong) {
String billTitle = TeamBillCycleUtils.parseBillBelongToDateRangeStr(billBelong);
bdSalarySettlementService.processAllBdSettlement(billBelong, billTitle);
}
}

View File

@ -0,0 +1,274 @@
package com.red.circle.other.app.service;
import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.other.app.dto.clientobject.BdLeaderSalarySettlementCO;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalaryPolicy;
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.database.mongo.service.bd.BdLeaderSalaryPolicyService;
import com.red.circle.other.infra.database.mongo.service.bd.BdLeaderSalarySettlementRecordService;
import com.red.circle.other.infra.database.mongo.service.bd.BdSalarySettlementRecordService;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentBaseInfo;
import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.wallet.inner.endpoint.bank.UserBankBalanceClient;
import com.red.circle.wallet.inner.endpoint.bank.UserBankRunningWaterClient;
import com.red.circle.wallet.inner.model.cmd.UserBankBalanceIncrCmd;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* BD Leader 工资结算服务实现.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class BdLeaderSalarySettlementServiceImpl implements BdLeaderSalarySettlementService {
private final BusinessDevelopmentBaseInfoService businessDevelopmentBaseInfoService;
private final BdSalarySettlementRecordService bdSalarySettlementRecordService;
private final BdLeaderSalaryPolicyService bdLeaderSalaryPolicyService;
private final BdLeaderSalarySettlementRecordService bdLeaderSalarySettlementRecordService;
private final UserBankBalanceClient userBankBalanceClient;
private final UserBankRunningWaterClient userBankRunningWaterClient;
@Override
public void processAllBdLeaderSettlement(Integer billBelong, String billTitle) {
log.warn("开始处理所有 BD Leader 工资结算,结算周期:{}", billBelong);
// 查询所有 BD Leader
List<BusinessDevelopmentBaseInfo> allBdLeaders = businessDevelopmentBaseInfoService.listAll();
if (CollectionUtils.isEmpty(allBdLeaders)) {
log.warn("没有找到任何 BD Leader 数据");
return;
}
// BD Leader 用户 ID 分组去重
Map<Long, List<BusinessDevelopmentBaseInfo>> bdLeaderGroupMap = allBdLeaders.stream()
.filter(info -> info.getBdLeadUserId() != null)
.collect(Collectors.groupingBy(BusinessDevelopmentBaseInfo::getBdLeadUserId));
log.warn("共找到 {} 个 BD Leader", bdLeaderGroupMap.size());
int successCount = 0;
int failCount = 0;
// 遍历每个 BD Leader 进行结算
for (Long bdLeaderUserId : bdLeaderGroupMap.keySet()) {
try {
processSingleBdLeaderSettlement(bdLeaderUserId, billBelong, billTitle);
successCount++;
} catch (Exception e) {
log.error("BD Leader {} 工资结算失败", bdLeaderUserId, e);
failCount++;
}
}
log.warn("BD Leader 工资结算完成,成功:{},失败:{}", successCount, failCount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public BdLeaderSalarySettlementCO processSingleBdLeaderSettlement(Long bdLeaderUserId, Integer billBelong, String billTitle) {
log.info("开始处理 BD Leader {} 的工资结算", bdLeaderUserId);
// 1. 生成幂等业务号
String bizNo = generateBizNo(bdLeaderUserId, billBelong);
// 2. 幂等性检查
if (bdLeaderSalarySettlementRecordService.existsByBizNo(bizNo)) {
log.warn("BD Leader {} 在周期 {} 已经结算过,跳过", bdLeaderUserId, billBelong);
BdLeaderSalarySettlementRecord existingRecord = bdLeaderSalarySettlementRecordService.getByBizNo(bizNo);
return convertToCO(existingRecord);
}
// 3. 统计 BD Leader 的团队数据
List<Long> bdUserIds = businessDevelopmentBaseInfoService.listBdUserIdsByLeaderId(bdLeaderUserId);
BdLeaderSalarySettlementRecord record = new BdLeaderSalarySettlementRecord();
record.setId(IdWorkerUtils.getIdStr());
record.setTimeId(IdWorkerUtils.getId());
record.setSysOrigin(SysOriginPlatformEnum.LIKEI.name());
record.setBdLeaderUserId(bdLeaderUserId);
record.setBillBelong(billBelong);
record.setBillTitle(billTitle);
record.setBdNumber(bdUserIds.size());
record.setBizNo(bizNo);
record.setStatus(BdSettlementStatus.PENDING);
// 4. 统计所有 BD 的工资总和
BigDecimal totalBdSalary = BigDecimal.ZERO;
for (Long bdUserId : bdUserIds) {
BdSalarySettlementRecord bdRecord = bdSalarySettlementRecordService.getByUserIdAndBillBelong(bdUserId, billBelong);
if (bdRecord != null && bdRecord.getSettlementSalary() != null) {
totalBdSalary = totalBdSalary.add(bdRecord.getSettlementSalary());
}
}
record.setTeamSalaryAmount(totalBdSalary.setScale(2, RoundingMode.DOWN));
// 5. 匹配政策并计算工资
calculateLeaderSalary(record);
// 6. 保存结算记录
bdLeaderSalarySettlementRecordService.create(record);
// 7. 如果工资大于 0则发放工资
if (record.getSettlementSalary() != null && record.getSettlementSalary().compareTo(BigDecimal.ZERO) > 0) {
try {
// 调用钱包服务发放工资
issueSalary(bdLeaderUserId, record);
// 更新状态为成功
bdLeaderSalarySettlementRecordService.updateStatus(record.getId(), BdSettlementStatus.SUCCESS, null);
record.setStatus(BdSettlementStatus.SUCCESS);
log.info("BD Leader {} 工资发放成功,金额:{} USD", bdLeaderUserId, record.getSettlementSalary());
} catch (Exception e) {
log.error("BD Leader {} 工资发放失败", bdLeaderUserId, e);
bdLeaderSalarySettlementRecordService.updateStatus(record.getId(), BdSettlementStatus.FAILED, e.getMessage());
record.setStatus(BdSettlementStatus.FAILED);
record.setFailureReason(e.getMessage());
throw e;
}
} else {
log.info("BD Leader {} 本周期未达到结算标准,无工资发放", bdLeaderUserId);
}
return convertToCO(record);
}
/**
* 匹配政策并计算 BD Leader 工资.
*/
private void calculateLeaderSalary(BdLeaderSalarySettlementRecord record) {
// 获取所有启用的政策
List<BdLeaderSalaryPolicy> policies = bdLeaderSalaryPolicyService.listEnabled();
if (CollectionUtils.isEmpty(policies)) {
record.setSettlementSalary(BigDecimal.ZERO);
return;
}
// 按等级倒序排序匹配最高等级
policies.sort((p1, p2) -> p2.getLevel().compareTo(p1.getLevel()));
for (BdLeaderSalaryPolicy policy : policies) {
// 必须同时满足 BD 数量和团队工资
if (record.getBdNumber() >= policy.getRequiredBdNumber()
&& record.getTeamSalaryAmount().compareTo(policy.getRequiredTeamSalary()) >= 0) {
record.setHitLevel(policy.getLevel());
record.setBasicSalary(policy.getBasicSalary());
record.setRewardAmount(policy.getRewardAmount());
// 计算最终工资Basic Salary + Reward Amount如果满足奖励条件
BigDecimal finalSalary = policy.getBasicSalary();
// 检查是否满足奖励条件本周期有新增 BD
boolean rewardEligible = checkRewardEligibility(record.getBdLeaderUserId(), record.getBillBelong());
record.setRewardEligible(rewardEligible);
if (rewardEligible && policy.getRewardAmount() != null) {
finalSalary = finalSalary.add(policy.getRewardAmount());
}
record.setSettlementSalary(finalSalary.setScale(2, RoundingMode.DOWN));
return;
}
}
// 未匹配到任何政策
record.setSettlementSalary(BigDecimal.ZERO);
}
/**
* 检查是否满足奖励条件本周期是否有新增 BD.
*/
private boolean checkRewardEligibility(Long bdLeaderUserId, Integer billBelong) {
// TODO: 实现逻辑 - 检查本周期是否有新增 BD
// 目前简化处理假设只要有 BD 就满足条件
List<Long> bdUserIds = businessDevelopmentBaseInfoService.listBdUserIdsByLeaderId(bdLeaderUserId);
return !CollectionUtils.isEmpty(bdUserIds);
}
/**
* 发放工资到美元账户.
*/
private void issueSalary(Long bdLeaderUserId, BdLeaderSalarySettlementRecord record) {
// 1. 增加美元余额
userBankBalanceClient.incr(new UserBankBalanceIncrCmd()
.setUserId(bdLeaderUserId)
.setSysOrigin(record.getSysOrigin())
.setAmount(com.red.circle.tool.core.tuple.PennyAmount.ofDollar(record.getSettlementSalary()))
);
// 2. 插入流水记录
String remark = String.format("结算周期:%s等级%d基础工资%s USD",
record.getBillTitle(), record.getHitLevel(), record.getBasicSalary());
if (record.getRewardEligible()) {
remark += String.format(",奖励:%s USD", record.getRewardAmount());
}
userBankRunningWaterClient.add(new UserBankRunningWaterDTO()
.setId(IdWorkerUtils.getId())
.setUserId(bdLeaderUserId)
.setSysOrigin(record.getSysOrigin())
.setType(ReceiptType.INCOME.getType())
.setAmount(record.getSettlementSalary())
.setEvent(UserBankWaterEvent.BD_LEADER_SALARY_SETTLEMENT.name())
.setEventDescribe("BD Leader工资结算")
.setTrackId(record.getId())
.setRemark(remark)
.setBalance(userBankBalanceClient.getBalance(bdLeaderUserId).getBody().getAccurateBalance())
.setCreateTime(TimestampUtils.now())
.setCreateUser(0L)
.setCreateUserOrigin(0)
);
}
/**
* 生成业务号幂等性保证.
*/
private String generateBizNo(Long bdLeaderUserId, Integer billBelong) {
return String.format("BD_LEADER_SALARY_%d_%d", bdLeaderUserId, billBelong);
}
/**
* 转换为 CO 对象.
*/
private BdLeaderSalarySettlementCO convertToCO(BdLeaderSalarySettlementRecord record) {
BdLeaderSalarySettlementCO co = new BdLeaderSalarySettlementCO();
co.setBdLeaderUserId(record.getBdLeaderUserId());
co.setBillBelong(record.getBillBelong());
co.setBillTitle(record.getBillTitle());
co.setBdNumber(record.getBdNumber());
co.setTeamSalaryAmount(record.getTeamSalaryAmount());
co.setHitLevel(record.getHitLevel());
co.setBasicSalary(record.getBasicSalary());
co.setRewardAmount(record.getRewardAmount());
co.setRewardEligible(record.getRewardEligible());
co.setNewRechargeAgentCount(record.getNewRechargeAgentCount());
co.setSettlementSalary(record.getSettlementSalary());
co.setStatus(record.getStatus().name());
co.setFailureReason(record.getFailureReason());
return co;
}
}

View File

@ -0,0 +1,365 @@
package com.red.circle.other.app.service;
import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.other.app.dto.clientobject.BdSalarySettlementCO;
import com.red.circle.other.app.dto.clientobject.BdTeamStatisticsCO;
import com.red.circle.other.infra.enums.BdPolicyType;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalaryPolicy;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalarySettlementRecord;
import com.red.circle.other.infra.database.mongo.service.bd.BdSalaryPolicyService;
import com.red.circle.other.infra.database.mongo.service.bd.BdSalarySettlementRecordService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberTargetService;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentTeam;
import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentTeamService;
import com.red.circle.other.inner.enums.team.TeamSalaryStatisticsCO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.order.inner.endpoint.api.InAppPurchaseDetailsClientApi;
import com.red.circle.order.inner.model.dto.inapp.BatchUserRechargeStatisticsDTO;
import com.red.circle.wallet.inner.endpoint.bank.UserBankBalanceClient;
import com.red.circle.wallet.inner.endpoint.bank.UserBankRunningWaterClient;
import com.red.circle.wallet.inner.model.cmd.UserBankBalanceIncrCmd;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
* BD 工资结算服务实现.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class BdSalarySettlementServiceImpl implements BdSalarySettlementService {
private final BusinessDevelopmentTeamService businessDevelopmentTeamService;
private final TeamMemberTargetService teamMemberTargetService;
private final BdSalaryPolicyService bdSalaryPolicyService;
private final BdSalarySettlementRecordService bdSalarySettlementRecordService;
private final UserBankBalanceClient userBankBalanceClient;
private final UserBankRunningWaterClient userBankRunningWaterClient;
private final InAppPurchaseDetailsClientApi inAppPurchaseDetailsClientApi;
@Override
public void processAllBdSettlement(Integer billBelong, String billTitle) {
log.warn("开始处理所有 BD 工资结算,结算周期:{}", billBelong);
// 查询所有 BD
List<BusinessDevelopmentTeam> allBdTeams = businessDevelopmentTeamService.listAll();
if (CollectionUtils.isEmpty(allBdTeams)) {
log.warn("没有找到任何 BD 数据");
return;
}
// BD 用户 ID 分组去重
Map<Long, List<BusinessDevelopmentTeam>> bdGroupMap = allBdTeams.stream()
.collect(Collectors.groupingBy(BusinessDevelopmentTeam::getUserId));
log.warn("共找到 {} 个 BD", bdGroupMap.size());
int successCount = 0;
int failCount = 0;
// 遍历每个 BD 进行结算
for (Long bdUserId : bdGroupMap.keySet()) {
try {
processSingleBdSettlement(bdUserId, billBelong, billTitle);
successCount++;
} catch (Exception e) {
log.error("BD {} 工资结算失败", bdUserId, e);
failCount++;
}
}
log.warn("BD 工资结算完成,成功:{},失败:{}", successCount, failCount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public BdSalarySettlementCO processSingleBdSettlement(Long bdUserId, Integer billBelong, String billTitle) {
log.info("开始处理 BD {} 的工资结算", bdUserId);
// 1. 生成幂等业务号
String bizNo = generateBizNo(bdUserId, billBelong);
// 2. 幂等性检查
if (bdSalarySettlementRecordService.existsByBizNo(bizNo)) {
log.warn("BD {} 在周期 {} 已经结算过,跳过", bdUserId, billBelong);
BdSalarySettlementRecord existingRecord = bdSalarySettlementRecordService.getByBizNo(bizNo);
return convertToCO(existingRecord);
}
// 3. 统计 BD 团队数据
BdTeamStatisticsCO teamStatistics = statisticsBdTeam(bdUserId, billBelong);
// 4. 匹配政策并计算工资
BdSalarySettlementRecord record = calculateSalary(bdUserId, billBelong, billTitle, teamStatistics, bizNo);
// 5. 保存结算记录
bdSalarySettlementRecordService.create(record);
// 6. 如果工资大于 0则发放工资
if (record.getSettlementSalary() != null && record.getSettlementSalary().compareTo(BigDecimal.ZERO) > 0) {
try {
// 调用钱包服务发放工资
issueSalary(bdUserId, record);
// 更新状态为成功
bdSalarySettlementRecordService.updateStatus(record.getId(), BdSettlementStatus.SUCCESS, null);
record.setStatus(BdSettlementStatus.SUCCESS);
log.info("BD {} 工资发放成功,金额:{} USD", bdUserId, record.getSettlementSalary());
} catch (Exception e) {
log.error("BD {} 工资发放失败", bdUserId, e);
bdSalarySettlementRecordService.updateStatus(record.getId(), BdSettlementStatus.FAILED, e.getMessage());
record.setStatus(BdSettlementStatus.FAILED);
record.setFailureReason(e.getMessage());
throw e;
}
} else {
log.info("BD {} 本周期未达到结算标准,无工资发放", bdUserId);
}
return convertToCO(record);
}
/**
* 统计 BD 的团队数据.
*/
private BdTeamStatisticsCO statisticsBdTeam(Long bdUserId, Integer billBelong) {
// 查询 BD 下的所有 Agent 团队
List<BusinessDevelopmentTeam> bdTeams = businessDevelopmentTeamService.listByBdUserId(bdUserId);
BdTeamStatisticsCO statistics = new BdTeamStatisticsCO();
statistics.setBdUserId(bdUserId);
statistics.setAgencyNumber(bdTeams.size());
if (CollectionUtils.isEmpty(bdTeams)) {
statistics.setTeamSalaryAmount(BigDecimal.ZERO);
statistics.setTeamRechargeAmount(BigDecimal.ZERO);
return statistics;
}
// 统计团队工资总和和收集所有用户ID
BigDecimal totalSalary = BigDecimal.ZERO;
List<Long> allUserIds = new ArrayList<>();
for (BusinessDevelopmentTeam bdTeam : bdTeams) {
// TeamMemberTarget 统计该团队的工资和用户ID
TeamSalaryStatisticsCO teamStatistics =
teamMemberTargetService.statisticsTeamSalaryByTeamIdAndBillBelong(
bdTeam.getTeamId(), billBelong);
totalSalary = totalSalary.add(teamStatistics.getTotalSalary());
allUserIds.addAll(teamStatistics.getUserIds());
}
statistics.setTeamSalaryAmount(totalSalary.setScale(2, RoundingMode.DOWN));
// 统计团队充值总和从订单服务统计
BigDecimal totalRecharge = statisticsTeamRecharge(allUserIds, billBelong);
statistics.setTeamRechargeAmount(totalRecharge.setScale(2, RoundingMode.DOWN));
return statistics;
}
/**
* 统计团队充值总和.
*
* @param userIds 用户ID列表
* @param billBelong 结算周期格式yyyyMM
* @return 充值总金额美元
*/
private BigDecimal statisticsTeamRecharge(List<Long> userIds, Integer billBelong) {
if (CollectionUtils.isEmpty(userIds)) {
return BigDecimal.ZERO;
}
try {
// 计算结算周期的开始和结束时间
int year = billBelong / 100;
int month = billBelong % 100;
java.time.LocalDateTime startTime = java.time.LocalDateTime.of(year, month, 1, 0, 0, 0);
java.time.LocalDateTime endTime = startTime.plusMonths(1).minusSeconds(1);
// 调用订单服务批量查询用户充值统计
com.red.circle.framework.dto.ResultResponse<List<BatchUserRechargeStatisticsDTO>> response =
inAppPurchaseDetailsClientApi.batchUserRechargeStatistics(
userIds,
SysOriginPlatformEnum.LIKEI.name(),
startTime,
endTime
);
if (response == null || !response.getStatus() || CollectionUtils.isEmpty(response.getBody())) {
log.warn("查询用户充值统计失败或无数据userIds: {}, billBelong: {}", userIds.size(), billBelong);
return BigDecimal.ZERO;
}
// 统计所有用户的充值总和
BigDecimal totalRecharge = response.getBody().stream()
.map(BatchUserRechargeStatisticsDTO::getTotalAmount)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
log.info("统计团队充值成功,用户数: {}, 结算周期: {}, 充值总额: {} USD",
userIds.size(), billBelong, totalRecharge);
return totalRecharge;
} catch (Exception e) {
log.error("统计团队充值失败userIds: {}, billBelong: {}", userIds.size(), billBelong, e);
return BigDecimal.ZERO;
}
}
/**
* 匹配政策并计算工资.
*/
private BdSalarySettlementRecord calculateSalary(Long bdUserId, Integer billBelong, String billTitle,
BdTeamStatisticsCO teamStatistics, String bizNo) {
BdSalarySettlementRecord record = new BdSalarySettlementRecord();
record.setId(IdWorkerUtils.getIdStr());
record.setTimeId(IdWorkerUtils.getId());
record.setSysOrigin(SysOriginPlatformEnum.LIKEI.name());
record.setBdUserId(bdUserId);
record.setBillBelong(billBelong);
record.setBillTitle(billTitle);
record.setAgencyNumber(teamStatistics.getAgencyNumber());
record.setTeamSalaryAmount(teamStatistics.getTeamSalaryAmount());
record.setTeamRechargeAmount(teamStatistics.getTeamRechargeAmount());
record.setBizNo(bizNo);
record.setStatus(BdSettlementStatus.PENDING);
// 获取所有启用的政策
List<BdSalaryPolicy> salaryPolicies = bdSalaryPolicyService.listEnabledByType(BdPolicyType.TEAM_SALARY);
List<BdSalaryPolicy> rechargePolicies = bdSalaryPolicyService.listEnabledByType(BdPolicyType.TEAM_RECHARGE);
// 匹配政策1Team Salary Policy
BigDecimal salarySalary = matchAndCalculate(salaryPolicies, teamStatistics.getAgencyNumber(),
teamStatistics.getTeamSalaryAmount(), BdPolicyType.TEAM_SALARY, record);
// 匹配政策2Team Recharge Policy
BigDecimal rechargeSalary = matchAndCalculate(rechargePolicies, teamStatistics.getAgencyNumber(),
teamStatistics.getTeamRechargeAmount(), BdPolicyType.TEAM_RECHARGE, record);
// 选择最高工资
if (salarySalary.compareTo(rechargeSalary) >= 0) {
record.setHitPolicyType(BdPolicyType.TEAM_SALARY);
record.setSettlementSalary(salarySalary);
} else {
record.setHitPolicyType(BdPolicyType.TEAM_RECHARGE);
record.setSettlementSalary(rechargeSalary);
}
return record;
}
/**
* 匹配政策并计算工资.
*/
private BigDecimal matchAndCalculate(List<BdSalaryPolicy> policies, Integer agencyNumber,
BigDecimal teamAmount, BdPolicyType policyType,
BdSalarySettlementRecord record) {
if (CollectionUtils.isEmpty(policies)) {
return BigDecimal.ZERO;
}
// 按等级倒序排序匹配最高等级
policies.sort((p1, p2) -> p2.getLevel().compareTo(p1.getLevel()));
for (BdSalaryPolicy policy : policies) {
// 必须同时满足 Agent 数量和团队金额
if (agencyNumber >= policy.getRequiredAgencyNumber()
&& teamAmount.compareTo(policy.getRequiredTeamAmount()) >= 0) {
// 计算工资团队金额 * 提成比例
BigDecimal salary = teamAmount.multiply(policy.getCommissionRate())
.divide(new BigDecimal("100"), 2, RoundingMode.DOWN);
// 记录命中的政策信息如果这个政策更优
if (policyType == BdPolicyType.TEAM_SALARY &&
(record.getHitPolicyType() == null || record.getHitPolicyType() == BdPolicyType.TEAM_SALARY)) {
record.setHitLevel(policy.getLevel());
record.setCommissionRate(policy.getCommissionRate());
}
return salary;
}
}
return BigDecimal.ZERO;
}
/**
* 发放工资到美元账户.
*/
private void issueSalary(Long bdUserId, BdSalarySettlementRecord record) {
// 1. 增加美元余额
userBankBalanceClient.incr(new UserBankBalanceIncrCmd()
.setUserId(bdUserId)
.setSysOrigin(record.getSysOrigin())
.setAmount(com.red.circle.tool.core.tuple.PennyAmount.ofDollar(record.getSettlementSalary()))
);
// 2. 插入流水记录
userBankRunningWaterClient.add(new UserBankRunningWaterDTO()
.setId(IdWorkerUtils.getId())
.setUserId(bdUserId)
.setSysOrigin(record.getSysOrigin())
.setType(ReceiptType.INCOME.getType())
.setAmount(record.getSettlementSalary())
.setEvent(UserBankWaterEvent.BD_SALARY_SETTLEMENT.name())
.setEventDescribe("BD工资结算")
.setTrackId(record.getId())
.setRemark(String.format("结算周期:%s等级%d提成比例%s%%",
record.getBillTitle(), record.getHitLevel(), record.getCommissionRate()))
.setBalance(userBankBalanceClient.getBalance(bdUserId).getBody().getAccurateBalance())
.setCreateTime(TimestampUtils.now())
.setCreateUser(0L)
.setCreateUserOrigin(0)
);
}
/**
* 生成业务号幂等性保证.
*/
private String generateBizNo(Long bdUserId, Integer billBelong) {
return String.format("BD_SALARY_%d_%d", bdUserId, billBelong);
}
/**
* 转换为 CO 对象.
*/
private BdSalarySettlementCO convertToCO(BdSalarySettlementRecord record) {
BdSalarySettlementCO co = new BdSalarySettlementCO();
co.setBdUserId(record.getBdUserId());
co.setBillBelong(record.getBillBelong());
co.setBillTitle(record.getBillTitle());
co.setAgencyNumber(record.getAgencyNumber());
co.setTeamSalaryAmount(record.getTeamSalaryAmount());
co.setTeamRechargeAmount(record.getTeamRechargeAmount());
co.setHitPolicyType(record.getHitPolicyType().name());
co.setHitLevel(record.getHitLevel());
co.setCommissionRate(record.getCommissionRate());
co.setSettlementSalary(record.getSettlementSalary());
co.setStatus(record.getStatus().name());
co.setFailureReason(record.getFailureReason());
return co;
}
}

View File

@ -0,0 +1,40 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* BD Leader 政策信息.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class BdLeaderPolicyInfoCO {
/**
* 等级.
*/
private Integer level;
/**
* 所需 BD 数量.
*/
private Integer requiredBdNumber;
/**
* 团队工资要求美元.
*/
private BigDecimal requiredTeamSalary;
/**
* 基础工资美元.
*/
private BigDecimal basicSalary;
/**
* 奖励金额美元.
*/
private BigDecimal rewardAmount;
}

View File

@ -0,0 +1,80 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* BD Leader 工资结算结果.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class BdLeaderSalarySettlementCO {
/**
* BD Leader 用户 ID.
*/
private Long bdLeaderUserId;
/**
* 结算周期.
*/
private Integer billBelong;
/**
* 结算周期标题.
*/
private String billTitle;
/**
* BD 数量.
*/
private Integer bdNumber;
/**
* 团队工资总和美元.
*/
private BigDecimal teamSalaryAmount;
/**
* 命中的等级.
*/
private Integer hitLevel;
/**
* 基础工资美元.
*/
private BigDecimal basicSalary;
/**
* 奖励金额美元.
*/
private BigDecimal rewardAmount;
/**
* 是否获得奖励.
*/
private Boolean rewardEligible;
/**
* 本周期新增充值代理数量.
*/
private Integer newRechargeAgentCount;
/**
* 最终结算工资美元.
*/
private BigDecimal settlementSalary;
/**
* 结算状态.
*/
private String status;
/**
* 失败原因.
*/
private String failureReason;
}

View File

@ -0,0 +1,40 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* BD 政策信息.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class BdPolicyInfoCO {
/**
* 政策类型.
*/
private String policyType;
/**
* 等级.
*/
private Integer level;
/**
* 所需 Agent 数量.
*/
private Integer requiredAgencyNumber;
/**
* 团队金额要求美元.
*/
private BigDecimal requiredTeamAmount;
/**
* 提成比例百分比.
*/
private BigDecimal commissionRate;
}

View File

@ -0,0 +1,75 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* BD 工资结算结果.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class BdSalarySettlementCO {
/**
* BD 用户 ID.
*/
private Long bdUserId;
/**
* 结算周期.
*/
private Integer billBelong;
/**
* 结算周期标题.
*/
private String billTitle;
/**
* Agent 数量.
*/
private Integer agencyNumber;
/**
* 团队工资总和美元.
*/
private BigDecimal teamSalaryAmount;
/**
* 团队充值总和美元.
*/
private BigDecimal teamRechargeAmount;
/**
* 命中的政策类型.
*/
private String hitPolicyType;
/**
* 命中的等级.
*/
private Integer hitLevel;
/**
* 提成比例百分比.
*/
private BigDecimal commissionRate;
/**
* 最终结算工资美元.
*/
private BigDecimal settlementSalary;
/**
* 结算状态.
*/
private String status;
/**
* 失败原因.
*/
private String failureReason;
}

View File

@ -0,0 +1,35 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* BD 团队统计信息.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
public class BdTeamStatisticsCO {
/**
* BD 用户 ID.
*/
private Long bdUserId;
/**
* Agent 数量.
*/
private Integer agencyNumber;
/**
* 团队工资总和美元.
*/
private BigDecimal teamSalaryAmount;
/**
* 团队充值总和美元.
*/
private BigDecimal teamRechargeAmount;
}

View File

@ -0,0 +1,30 @@
package com.red.circle.other.app.service;
import com.red.circle.other.app.dto.clientobject.BdLeaderSalarySettlementCO;
/**
* BD Leader 工资结算服务.
*
* @author AI Assistant
* @since 2025-10-29
*/
public interface BdLeaderSalarySettlementService {
/**
* 处理所有 BD Leader 的工资结算.
*
* @param billBelong 结算周期
* @param billTitle 结算周期标题
*/
void processAllBdLeaderSettlement(Integer billBelong, String billTitle);
/**
* 处理单个 BD Leader 的工资结算.
*
* @param bdLeaderUserId BD Leader 用户 ID
* @param billBelong 结算周期
* @param billTitle 结算周期标题
* @return 结算结果
*/
BdLeaderSalarySettlementCO processSingleBdLeaderSettlement(Long bdLeaderUserId, Integer billBelong, String billTitle);
}

View File

@ -0,0 +1,30 @@
package com.red.circle.other.app.service;
import com.red.circle.other.app.dto.clientobject.BdSalarySettlementCO;
/**
* BD 工资结算服务.
*
* @author AI Assistant
* @since 2025-10-29
*/
public interface BdSalarySettlementService {
/**
* 处理所有 BD 的工资结算.
*
* @param billBelong 结算周期
* @param billTitle 结算周期标题
*/
void processAllBdSettlement(Integer billBelong, String billTitle);
/**
* 处理单个 BD 的工资结算.
*
* @param bdUserId BD 用户 ID
* @param billBelong 结算周期
* @param billTitle 结算周期标题
* @return 结算结果
*/
BdSalarySettlementCO processSingleBdSettlement(Long bdUserId, Integer billBelong, String billTitle);
}

View File

@ -0,0 +1,72 @@
package com.red.circle.other.infra.database.mongo.entity.bd;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
/**
* BD Leader 工资政策配置.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
@Accessors(chain = true)
@Document("bd_leader_salary_policy")
public class BdLeaderSalaryPolicy implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID.
*/
@Id
private String id;
/**
* 等级.
*/
private Integer level;
/**
* 所需 BD 数量.
*/
private Integer requiredBdNumber;
/**
* 团队工资要求美元- 所有 BD 的工资总和.
*/
private BigDecimal requiredTeamSalary;
/**
* 基础工资美元- Basic Salary.
*/
private BigDecimal basicSalary;
/**
* 奖励金额美元- Reward Amount.
*/
private BigDecimal rewardAmount;
/**
* 是否启用.
*/
private Boolean enabled;
/**
* 创建时间.
*/
private Timestamp createTime;
/**
* 更新时间.
*/
private Timestamp updateTime;
}

View File

@ -0,0 +1,127 @@
package com.red.circle.other.infra.database.mongo.entity.bd;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
/**
* BD Leader 工资结算记录.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
@Accessors(chain = true)
@Document("bd_leader_salary_settlement_record")
public class BdLeaderSalarySettlementRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID.
*/
@Id
private String id;
/**
* 时序 ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long timeId;
/**
* 系统来源.
*/
private String sysOrigin;
/**
* BD Leader 用户 ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long bdLeaderUserId;
/**
* 结算周期格式202501 表示2025年1月上半月202502 表示2025年1月下半月.
*/
private Integer billBelong;
/**
* 结算周期标题2025年1月上半月.
*/
private String billTitle;
/**
* BD 数量.
*/
private Integer bdNumber;
/**
* 团队工资总和美元- 所有 BD 的工资总和.
*/
private BigDecimal teamSalaryAmount;
/**
* 命中的等级.
*/
private Integer hitLevel;
/**
* 基础工资美元- Basic Salary.
*/
private BigDecimal basicSalary;
/**
* 奖励金额美元- Reward Amount.
*/
private BigDecimal rewardAmount;
/**
* 是否满足奖励条件本周期是否带来新充值代理.
*/
private Boolean rewardEligible;
/**
* 本周期新增充值代理数量.
*/
private Integer newRechargeAgentCount;
/**
* 最终结算工资美元- Basic Salary + Reward Amount如符合条件.
*/
private BigDecimal settlementSalary;
/**
* 结算状态PENDING | SUCCESS | FAILED.
*/
private BdSettlementStatus status;
/**
* 失败原因.
*/
private String failureReason;
/**
* 幂等业务号防重.
*/
private String bizNo;
/**
* 创建时间.
*/
private Timestamp createTime;
/**
* 更新时间.
*/
private Timestamp updateTime;
}

View File

@ -0,0 +1,75 @@
package com.red.circle.other.infra.database.mongo.entity.bd;
import com.red.circle.other.infra.enums.BdPolicyType;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
/**
* BD 工资政策配置.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
@Accessors(chain = true)
@Document("bd_salary_policy")
public class BdSalaryPolicy implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID.
*/
@Id
private String id;
/**
* 政策类型TEAM_SALARY | TEAM_RECHARGE.
*/
private BdPolicyType policyType;
/**
* 等级.
*/
private Integer level;
/**
* 所需 Agent 数量.
*/
private Integer requiredAgencyNumber;
/**
* 团队金额要求美元.
* - policyType = TEAM_SALARY 表示团队工资总和要求
* - policyType = TEAM_RECHARGE 表示团队充值总和要求
*/
private BigDecimal requiredTeamAmount;
/**
* 提成比例百分比 3% 存储为 3.00.
*/
private BigDecimal commissionRate;
/**
* 是否启用.
*/
private Boolean enabled;
/**
* 创建时间.
*/
private Timestamp createTime;
/**
* 更新时间.
*/
private Timestamp updateTime;
}

View File

@ -0,0 +1,123 @@
package com.red.circle.other.infra.database.mongo.entity.bd;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.other.infra.enums.BdPolicyType;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
/**
* BD 工资结算记录.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Data
@Accessors(chain = true)
@Document("bd_salary_settlement_record")
public class BdSalarySettlementRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID.
*/
@Id
private String id;
/**
* 时序 ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long timeId;
/**
* 系统来源.
*/
private String sysOrigin;
/**
* BD 用户 ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long bdUserId;
/**
* 结算周期格式202501 表示2025年1月上半月202502 表示2025年1月下半月.
*/
private Integer billBelong;
/**
* 结算周期标题2025年1月上半月.
*/
private String billTitle;
/**
* Agent 数量.
*/
private Integer agencyNumber;
/**
* 团队工资总和美元.
*/
private BigDecimal teamSalaryAmount;
/**
* 团队充值总和美元.
*/
private BigDecimal teamRechargeAmount;
/**
* 命中的政策类型TEAM_SALARY | TEAM_RECHARGE.
*/
private BdPolicyType hitPolicyType;
/**
* 命中的等级.
*/
private Integer hitLevel;
/**
* 提成比例百分比.
*/
private BigDecimal commissionRate;
/**
* 最终结算工资美元.
*/
private BigDecimal settlementSalary;
/**
* 结算状态PENDING | SUCCESS | FAILED.
*/
private BdSettlementStatus status;
/**
* 失败原因.
*/
private String failureReason;
/**
* 幂等业务号防重.
*/
private String bizNo;
/**
* 创建时间.
*/
private Timestamp createTime;
/**
* 更新时间.
*/
private Timestamp updateTime;
}

View File

@ -0,0 +1,54 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalaryPolicy;
import java.util.List;
/**
* BD Leader 政策服务.
*
* @author AI Assistant
* @since 2025-10-29
*/
public interface BdLeaderSalaryPolicyService {
/**
* 创建政策.
*/
void create(BdLeaderSalaryPolicy policy);
/**
* 批量创建政策.
*/
void batchCreate(List<BdLeaderSalaryPolicy> policies);
/**
* 更新政策.
*/
void update(BdLeaderSalaryPolicy policy);
/**
* 根据 ID 查询.
*/
BdLeaderSalaryPolicy getById(String id);
/**
* 查询所有启用的政策按等级升序.
*/
List<BdLeaderSalaryPolicy> listEnabled();
/**
* 根据等级查询.
*/
BdLeaderSalaryPolicy getByLevel(Integer level);
/**
* 启用政策.
*/
void enable(String id);
/**
* 禁用政策.
*/
void disable(String id);
}

View File

@ -0,0 +1,85 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalaryPolicy;
import com.red.circle.tool.core.date.TimestampUtils;
import lombok.RequiredArgsConstructor;
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.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* BD Leader 政策服务实现.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Service
@RequiredArgsConstructor
public class BdLeaderSalaryPolicyServiceImpl implements BdLeaderSalaryPolicyService {
private final MongoTemplate mongoTemplate;
@Override
public void create(BdLeaderSalaryPolicy policy) {
policy.setCreateTime(TimestampUtils.now());
policy.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(policy);
}
@Override
public void batchCreate(List<BdLeaderSalaryPolicy> policies) {
policies.forEach(policy -> {
policy.setCreateTime(TimestampUtils.now());
policy.setUpdateTime(TimestampUtils.now());
});
mongoTemplate.insertAll(policies);
}
@Override
public void update(BdLeaderSalaryPolicy policy) {
policy.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(policy);
}
@Override
public BdLeaderSalaryPolicy getById(String id) {
return mongoTemplate.findById(id, BdLeaderSalaryPolicy.class);
}
@Override
public List<BdLeaderSalaryPolicy> listEnabled() {
Query query = new Query(Criteria.where("enabled").is(true));
query.with(Sort.by(Sort.Order.asc("level")));
return mongoTemplate.find(query, BdLeaderSalaryPolicy.class);
}
@Override
public BdLeaderSalaryPolicy getByLevel(Integer level) {
Query query = new Query(Criteria.where("level").is(level)
.and("enabled").is(true));
return mongoTemplate.findOne(query, BdLeaderSalaryPolicy.class);
}
@Override
public void enable(String id) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.set("enabled", true)
.set("updateTime", TimestampUtils.now());
mongoTemplate.updateFirst(query, update, BdLeaderSalaryPolicy.class);
}
@Override
public void disable(String id) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.set("enabled", false)
.set("updateTime", TimestampUtils.now());
mongoTemplate.updateFirst(query, update, BdLeaderSalaryPolicy.class);
}
}

View File

@ -0,0 +1,55 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalarySettlementRecord;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import java.util.List;
/**
* BD Leader 结算记录服务.
*
* @author AI Assistant
* @since 2025-10-29
*/
public interface BdLeaderSalarySettlementRecordService {
/**
* 创建结算记录.
*/
void create(BdLeaderSalarySettlementRecord record);
/**
* 更新结算记录.
*/
void update(BdLeaderSalarySettlementRecord record);
/**
* 根据 ID 查询.
*/
BdLeaderSalarySettlementRecord getById(String id);
/**
* 根据业务号查询幂等性检查.
*/
BdLeaderSalarySettlementRecord getByBizNo(String bizNo);
/**
* 检查业务号是否已存在.
*/
boolean existsByBizNo(String bizNo);
/**
* 根据 BD Leader 用户 ID 和结算周期查询.
*/
BdLeaderSalarySettlementRecord getByUserIdAndBillBelong(Long bdLeaderUserId, Integer billBelong);
/**
* 根据结算周期查询所有记录.
*/
List<BdLeaderSalarySettlementRecord> listByBillBelong(Integer billBelong);
/**
* 更新结算状态.
*/
void updateStatus(String id, BdSettlementStatus status, String failureReason);
}

View File

@ -0,0 +1,79 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalarySettlementRecord;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import com.red.circle.tool.core.date.TimestampUtils;
import lombok.RequiredArgsConstructor;
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.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* BD Leader 结算记录服务实现.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Service
@RequiredArgsConstructor
public class BdLeaderSalarySettlementRecordServiceImpl implements BdLeaderSalarySettlementRecordService {
private final MongoTemplate mongoTemplate;
@Override
public void create(BdLeaderSalarySettlementRecord record) {
record.setCreateTime(TimestampUtils.now());
record.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(record);
}
@Override
public void update(BdLeaderSalarySettlementRecord record) {
record.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(record);
}
@Override
public BdLeaderSalarySettlementRecord getById(String id) {
return mongoTemplate.findById(id, BdLeaderSalarySettlementRecord.class);
}
@Override
public BdLeaderSalarySettlementRecord getByBizNo(String bizNo) {
Query query = new Query(Criteria.where("bizNo").is(bizNo));
return mongoTemplate.findOne(query, BdLeaderSalarySettlementRecord.class);
}
@Override
public boolean existsByBizNo(String bizNo) {
Query query = new Query(Criteria.where("bizNo").is(bizNo));
return mongoTemplate.exists(query, BdLeaderSalarySettlementRecord.class);
}
@Override
public BdLeaderSalarySettlementRecord getByUserIdAndBillBelong(Long bdLeaderUserId, Integer billBelong) {
Query query = new Query(Criteria.where("bdLeaderUserId").is(bdLeaderUserId)
.and("billBelong").is(billBelong));
return mongoTemplate.findOne(query, BdLeaderSalarySettlementRecord.class);
}
@Override
public List<BdLeaderSalarySettlementRecord> listByBillBelong(Integer billBelong) {
Query query = new Query(Criteria.where("billBelong").is(billBelong));
return mongoTemplate.find(query, BdLeaderSalarySettlementRecord.class);
}
@Override
public void updateStatus(String id, BdSettlementStatus status, String failureReason) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.set("status", status)
.set("failureReason", failureReason)
.set("updateTime", TimestampUtils.now());
mongoTemplate.updateFirst(query, update, BdLeaderSalarySettlementRecord.class);
}
}

View File

@ -0,0 +1,60 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalaryPolicy;
import com.red.circle.other.infra.enums.BdPolicyType;
import java.util.List;
/**
* BD 政策服务.
*
* @author AI Assistant
* @since 2025-10-29
*/
public interface BdSalaryPolicyService {
/**
* 创建政策.
*/
void create(BdSalaryPolicy policy);
/**
* 批量创建政策.
*/
void batchCreate(List<BdSalaryPolicy> policies);
/**
* 更新政策.
*/
void update(BdSalaryPolicy policy);
/**
* 根据 ID 查询.
*/
BdSalaryPolicy getById(String id);
/**
* 查询所有启用的政策.
*/
List<BdSalaryPolicy> listEnabled();
/**
* 根据政策类型查询启用的政策按等级升序.
*/
List<BdSalaryPolicy> listEnabledByType(BdPolicyType policyType);
/**
* 根据政策类型和等级查询.
*/
BdSalaryPolicy getByTypeAndLevel(BdPolicyType policyType, Integer level);
/**
* 启用政策.
*/
void enable(String id);
/**
* 禁用政策.
*/
void disable(String id);
}

View File

@ -0,0 +1,95 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalaryPolicy;
import com.red.circle.other.infra.enums.BdPolicyType;
import com.red.circle.tool.core.date.TimestampUtils;
import lombok.RequiredArgsConstructor;
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.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* BD 政策服务实现.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Service
@RequiredArgsConstructor
public class BdSalaryPolicyServiceImpl implements BdSalaryPolicyService {
private final MongoTemplate mongoTemplate;
@Override
public void create(BdSalaryPolicy policy) {
policy.setCreateTime(TimestampUtils.now());
policy.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(policy);
}
@Override
public void batchCreate(List<BdSalaryPolicy> policies) {
policies.forEach(policy -> {
policy.setCreateTime(TimestampUtils.now());
policy.setUpdateTime(TimestampUtils.now());
});
mongoTemplate.insertAll(policies);
}
@Override
public void update(BdSalaryPolicy policy) {
policy.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(policy);
}
@Override
public BdSalaryPolicy getById(String id) {
return mongoTemplate.findById(id, BdSalaryPolicy.class);
}
@Override
public List<BdSalaryPolicy> listEnabled() {
Query query = new Query(Criteria.where("enabled").is(true));
query.with(Sort.by(Sort.Order.asc("policyType"), Sort.Order.asc("level")));
return mongoTemplate.find(query, BdSalaryPolicy.class);
}
@Override
public List<BdSalaryPolicy> listEnabledByType(BdPolicyType policyType) {
Query query = new Query(Criteria.where("enabled").is(true)
.and("policyType").is(policyType));
query.with(Sort.by(Sort.Order.asc("level")));
return mongoTemplate.find(query, BdSalaryPolicy.class);
}
@Override
public BdSalaryPolicy getByTypeAndLevel(BdPolicyType policyType, Integer level) {
Query query = new Query(Criteria.where("policyType").is(policyType)
.and("level").is(level)
.and("enabled").is(true));
return mongoTemplate.findOne(query, BdSalaryPolicy.class);
}
@Override
public void enable(String id) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.set("enabled", true)
.set("updateTime", TimestampUtils.now());
mongoTemplate.updateFirst(query, update, BdSalaryPolicy.class);
}
@Override
public void disable(String id) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.set("enabled", false)
.set("updateTime", TimestampUtils.now());
mongoTemplate.updateFirst(query, update, BdSalaryPolicy.class);
}
}

View File

@ -0,0 +1,55 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalarySettlementRecord;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import java.util.List;
/**
* BD 结算记录服务.
*
* @author AI Assistant
* @since 2025-10-29
*/
public interface BdSalarySettlementRecordService {
/**
* 创建结算记录.
*/
void create(BdSalarySettlementRecord record);
/**
* 更新结算记录.
*/
void update(BdSalarySettlementRecord record);
/**
* 根据 ID 查询.
*/
BdSalarySettlementRecord getById(String id);
/**
* 根据业务号查询幂等性检查.
*/
BdSalarySettlementRecord getByBizNo(String bizNo);
/**
* 检查业务号是否已存在.
*/
boolean existsByBizNo(String bizNo);
/**
* 根据 BD 用户 ID 和结算周期查询.
*/
BdSalarySettlementRecord getByUserIdAndBillBelong(Long bdUserId, Integer billBelong);
/**
* 根据结算周期查询所有记录.
*/
List<BdSalarySettlementRecord> listByBillBelong(Integer billBelong);
/**
* 更新结算状态.
*/
void updateStatus(String id, BdSettlementStatus status, String failureReason);
}

View File

@ -0,0 +1,79 @@
package com.red.circle.other.infra.database.mongo.service.bd;
import com.red.circle.other.infra.database.mongo.entity.bd.BdSalarySettlementRecord;
import com.red.circle.other.infra.enums.BdSettlementStatus;
import com.red.circle.tool.core.date.TimestampUtils;
import lombok.RequiredArgsConstructor;
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.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* BD 结算记录服务实现.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Service
@RequiredArgsConstructor
public class BdSalarySettlementRecordServiceImpl implements BdSalarySettlementRecordService {
private final MongoTemplate mongoTemplate;
@Override
public void create(BdSalarySettlementRecord record) {
record.setCreateTime(TimestampUtils.now());
record.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(record);
}
@Override
public void update(BdSalarySettlementRecord record) {
record.setUpdateTime(TimestampUtils.now());
mongoTemplate.save(record);
}
@Override
public BdSalarySettlementRecord getById(String id) {
return mongoTemplate.findById(id, BdSalarySettlementRecord.class);
}
@Override
public BdSalarySettlementRecord getByBizNo(String bizNo) {
Query query = new Query(Criteria.where("bizNo").is(bizNo));
return mongoTemplate.findOne(query, BdSalarySettlementRecord.class);
}
@Override
public boolean existsByBizNo(String bizNo) {
Query query = new Query(Criteria.where("bizNo").is(bizNo));
return mongoTemplate.exists(query, BdSalarySettlementRecord.class);
}
@Override
public BdSalarySettlementRecord getByUserIdAndBillBelong(Long bdUserId, Integer billBelong) {
Query query = new Query(Criteria.where("bdUserId").is(bdUserId)
.and("billBelong").is(billBelong));
return mongoTemplate.findOne(query, BdSalarySettlementRecord.class);
}
@Override
public List<BdSalarySettlementRecord> listByBillBelong(Integer billBelong) {
Query query = new Query(Criteria.where("billBelong").is(billBelong));
return mongoTemplate.find(query, BdSalarySettlementRecord.class);
}
@Override
public void updateStatus(String id, BdSettlementStatus status, String failureReason) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.set("status", status)
.set("failureReason", failureReason)
.set("updateTime", TimestampUtils.now());
mongoTemplate.updateFirst(query, update, BdSalarySettlementRecord.class);
}
}

View File

@ -561,4 +561,9 @@ public final class TeamBillCycleUtils {
}
}
public static String parseBillBelongToDateRangeStr(Integer billBelong) {
String[] strings = parseBillBelongToDateRange(billBelong);
return strings[0] + "-" + strings[1];
}
}

View File

@ -5,6 +5,9 @@ import com.red.circle.other.infra.database.mongo.dto.team.query.TeamMemberTarget
import com.red.circle.other.inner.enums.team.TeamMemberTargetIndex;
import com.red.circle.other.inner.enums.team.TeamMemberTargetSettlementResult;
import com.red.circle.other.inner.enums.team.TeamMemberTargetStatus;
import com.red.circle.other.inner.enums.team.TeamSalaryStatisticsCO;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -192,4 +195,25 @@ public interface TeamMemberTargetService {
*/
Set<String> repeatTargetHistory();
/**
* 统计某个团队在某个结算周期的工资总和.
*
* @param teamId 团队ID
* @param billBelong 结算周期
* @return 工资总和美元
* @deprecated 使用 {@link #statisticsTeamSalaryByTeamIdAndBillBelong(Long, Integer)} 代替
*/
@Deprecated
BigDecimal sumTeamSalaryByTeamIdAndBillBelong(Long teamId, Integer billBelong);
/**
* 统计某个团队在某个结算周期的工资信息包含用户列表和工资总和.
*
* @param teamId 团队ID
* @param billBelong 结算周期
* @return 团队工资统计信息包含用户ID列表和工资总和
*/
TeamSalaryStatisticsCO statisticsTeamSalaryByTeamIdAndBillBelong(
Long teamId, Integer billBelong);
}

View File

@ -13,12 +13,14 @@ import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberTar
import com.red.circle.other.inner.enums.team.TeamMemberTargetIndex;
import com.red.circle.other.inner.enums.team.TeamMemberTargetSettlementResult;
import com.red.circle.other.inner.enums.team.TeamMemberTargetStatus;
import com.red.circle.other.inner.enums.team.TeamSalaryStatisticsCO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.core.date.ZonedId;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.List;
@ -608,5 +610,53 @@ public class TeamMemberTargetServiceImpl implements TeamMemberTargetService {
return userIds;
}
@Override
public BigDecimal sumTeamSalaryByTeamIdAndBillBelong(Long teamId, Integer billBelong) {
List<TeamMemberTarget> targets = listByBillBelong(teamId, billBelong);
if (CollectionUtils.isEmpty(targets)) {
return BigDecimal.ZERO;
}
return targets.stream()
.filter(target -> target.getSettlementResult() != null
&& target.getSettlementResult().getTotalSalary() != null)
.map(target -> target.getSettlementResult().getTotalSalary())
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Override
public TeamSalaryStatisticsCO statisticsTeamSalaryByTeamIdAndBillBelong(
Long teamId, Integer billBelong) {
List<TeamMemberTarget> targets = listByBillBelong(teamId, billBelong);
TeamSalaryStatisticsCO statistics = new TeamSalaryStatisticsCO();
if (CollectionUtils.isEmpty(targets)) {
statistics.setUserIds(List.of());
statistics.setTotalSalary(BigDecimal.ZERO);
return statistics;
}
// 提取所有用户ID
List<Long> userIds = targets.stream()
.map(TeamMemberTarget::getUserId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
// 统计工资总和
BigDecimal totalSalary = targets.stream()
.filter(target -> target.getSettlementResult() != null
&& target.getSettlementResult().getTotalSalary() != null)
.map(target -> target.getSettlementResult().getTotalSalary())
.reduce(BigDecimal.ZERO, BigDecimal::add);
statistics.setUserIds(userIds);
statistics.setTotalSalary(totalSalary);
return statistics;
}
}

View File

@ -46,4 +46,14 @@ public interface BusinessDevelopmentBaseInfoService extends
*/
Map<Long, Long> mapCountByLeaderUserIds(Set<Long> bdLeadUserIds);
/**
* 查询所有 BD Leader.
*/
List<BusinessDevelopmentBaseInfo> listAll();
/**
* 根据 BD Leader ID 查询其管理的所有 BD 用户 ID.
*/
List<Long> listBdUserIdsByLeaderId(Long bdLeadUserId);
}

View File

@ -94,4 +94,14 @@ public interface BusinessDevelopmentTeamService extends
*/
void deleteAllByUserId(Long userId);
/**
* 查询所有 BD 团队.
*/
List<BusinessDevelopmentTeam> listAll();
/**
* 根据 BD 用户 ID 查询其管理的所有团队.
*/
List<BusinessDevelopmentTeam> listByBdUserId(Long bdUserId);
}

View File

@ -179,4 +179,25 @@ public class BusinessDevelopmentBaseInfoServiceImpl extends
.groupingBy(BusinessDevelopmentBaseInfo::getBdLeadUserId, Collectors.counting()));
}
@Override
public List<BusinessDevelopmentBaseInfo> listAll() {
return list();
}
@Override
public List<Long> listBdUserIdsByLeaderId(Long bdLeadUserId) {
List<BusinessDevelopmentBaseInfo> bdList = query()
.select(BusinessDevelopmentBaseInfo::getUserId)
.eq(BusinessDevelopmentBaseInfo::getBdLeadUserId, bdLeadUserId)
.list();
if (CollectionUtils.isEmpty(bdList)) {
return java.util.Collections.emptyList();
}
return bdList.stream()
.map(BusinessDevelopmentBaseInfo::getUserId)
.collect(Collectors.toList());
}
}

View File

@ -188,4 +188,16 @@ public class BusinessDevelopmentTeamServiceImpl extends
.execute();
}
@Override
public List<BusinessDevelopmentTeam> listAll() {
return list();
}
@Override
public List<BusinessDevelopmentTeam> listByBdUserId(Long bdUserId) {
return query()
.eq(BusinessDevelopmentTeam::getUserId, bdUserId)
.list();
}
}

View File

@ -0,0 +1,44 @@
package com.red.circle.other.infra.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* BD 政策类型枚举.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Getter
@AllArgsConstructor
public enum BdPolicyType {
/**
* 团队工资政策.
*/
TEAM_SALARY("团队工资政策"),
/**
* 团队充值政策.
*/
TEAM_RECHARGE("团队充值政策");
/**
* 描述.
*/
private final String description;
/**
* 判断是否为团队工资政策.
*/
public boolean isTeamSalary() {
return this == TEAM_SALARY;
}
/**
* 判断是否为团队充值政策.
*/
public boolean isTeamRecharge() {
return this == TEAM_RECHARGE;
}
}

View File

@ -0,0 +1,56 @@
package com.red.circle.other.infra.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* BD 结算状态枚举.
*
* @author AI Assistant
* @since 2025-10-29
*/
@Getter
@AllArgsConstructor
public enum BdSettlementStatus {
/**
* 待结算.
*/
PENDING("待结算"),
/**
* 结算成功.
*/
SUCCESS("结算成功"),
/**
* 结算失败.
*/
FAILED("结算失败");
/**
* 描述.
*/
private final String description;
/**
* 判断是否成功.
*/
public boolean isSuccess() {
return this == SUCCESS;
}
/**
* 判断是否失败.
*/
public boolean isFailed() {
return this == FAILED;
}
/**
* 判断是否待处理.
*/
public boolean isPending() {
return this == PENDING;
}
}

View File

@ -1,3 +1,4 @@
import com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.crypto.SecurityUtils;
import org.junit.Test;
@ -7,7 +8,9 @@ public class ApiTest {
@Test
public void test() {
System.out.println(new BCryptPasswordEncoder().encode("8826681"));
String[] strings = TeamBillCycleUtils.parseBillBelongToDateRange(20251001);
System.out.println(strings[0]);
System.out.println(strings[1]);
}
@Test