package com.kym.service.impl; import com.github.pagehelper.PageHelper; import com.kym.common.utils.CommUtil; import com.kym.entity.SettlementRecord; import com.kym.entity.SplitRecord; import com.kym.entity.StationAccount; import com.kym.entity.common.PageBean; import com.kym.entity.queryParams.SettlementQueryParam; import com.kym.entity.vo.SettlementRecordVo; import com.kym.mapper.SettlementRecordMapper; import com.kym.service.SettlementService; import com.kym.service.PlatformAccountService; import com.kym.service.PlatformRevenueRecordService; import com.kym.service.SplitRecordService; import com.kym.service.StationAccountService; import com.kym.service.mybatisplus.MyBaseServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 结算服务实现 * * @author skyline * @since 2026-05-14 */ @Slf4j @Service public class SettlementServiceImpl extends MyBaseServiceImpl implements SettlementService { private static final BigDecimal PLATFORM_FEE_RATE = new BigDecimal("0.1"); private static final DateTimeFormatter PERIOD_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); private final SplitRecordService splitRecordService; private final StationAccountService stationAccountService; private final PlatformAccountService platformAccountService; private final PlatformRevenueRecordService platformRevenueRecordService; public SettlementServiceImpl(SplitRecordService splitRecordService, StationAccountService stationAccountService, PlatformAccountService platformAccountService, PlatformRevenueRecordService platformRevenueRecordService) { this.splitRecordService = splitRecordService; this.stationAccountService = stationAccountService; this.platformAccountService = platformAccountService; this.platformRevenueRecordService = platformRevenueRecordService; } @Override @Transactional(rollbackFor = Exception.class) public void executeMonthlySettlement() { YearMonth lastMonth = YearMonth.now().minusMonths(1); String period = lastMonth.format(PERIOD_FORMATTER); LocalDateTime periodStart = lastMonth.atDay(1).atStartOfDay(); LocalDateTime periodEnd = lastMonth.atEndOfMonth().atTime(LocalTime.MAX); log.info("开始执行结算,周期:{}", period); // 幂等检查:如果本期已有结算记录则跳过 long settledCount = lambdaQuery().eq(SettlementRecord::getSettlementPeriod, period).count(); if (settledCount > 0) { log.warn("本期 {} 已有 {} 条结算记录,跳过重复结算", period, settledCount); return; } // 汇总本期各站点分账数据 var allRecords = splitRecordService.lambdaQuery() .ge(SplitRecord::getCreateTime, periodStart) .le(SplitRecord::getCreateTime, periodEnd) .list(); // 站点 -> {recharge, refund, crossExpend, crossIncome} Set stationIds = allRecords.stream() .flatMap(r -> List.of(r.getFromStationId(), r.getToStationId()).stream()) .collect(Collectors.toSet()); for (String stationId : stationIds) { doStationSettlement(stationId, period, allRecords); } log.info("结算完成,周期:{},涉及站点数:{}", period, stationIds.size()); } private void doStationSettlement(String stationId, String period, List allRecords) { int totalRecharge = sumByType(allRecords, stationId, SplitRecord.TYPE_RECHARGE, true); int totalRefund = sumByType(allRecords, stationId, SplitRecord.TYPE_REFUND, false); int totalCrossExpend = sumByType(allRecords, stationId, SplitRecord.TYPE_CROSS_EXPEND, false); int totalCrossIncome = sumByType(allRecords, stationId, SplitRecord.TYPE_CROSS_INCOME, true); if (totalRecharge == 0 && totalRefund == 0 && totalCrossExpend == 0 && totalCrossIncome == 0) { return; } // 读取上期期末余额作为本期期初 int openingBalance = getOpeningBalance(stationId, period); // 平台服务费基数(仅基于本期流入,不含上期结转) int feeBase = totalRecharge - totalRefund - totalCrossExpend + totalCrossIncome; int platformFee = feeBase > 0 ? new BigDecimal(feeBase).multiply(PLATFORM_FEE_RATE).setScale(0, RoundingMode.DOWN).intValue() : 0; // 可结算总额 = 期初结转 + 本期流入 - 平台费 int available = openingBalance + feeBase - platformFee; SettlementRecord record = new SettlementRecord() .setStationId(stationId) .setSettlementPeriod(period) .setTotalRecharge(totalRecharge) .setTotalRefund(totalRefund) .setTotalCrossIncome(totalCrossIncome) .setTotalCrossExpend(totalCrossExpend) .setOpeningPendingBalance(openingBalance) .setPlatformFeeBase(feeBase) .setPlatformFee(platformFee); if (available > 0) { // 正常结算 record.setSettlementAmount(available) .setClosingPendingBalance(0) .setStatus(SettlementRecord.STATUS_已结算); // 增加站点可提现金额 stationAccountService.lambdaUpdate() .setSql("available_balance = available_balance + {0}", available) .eq(StationAccount::getStationId, stationId) .update(); save(record); // 平台服务费收入记录 if (platformFee > 0) { platformRevenueRecordService.saveRecord(record, stationId); platformAccountService.addRevenue(platformFee); } log.info("站点 {} 结算成功,金额:{} 分,平台费:{} 分", stationId, available, platformFee); } else { // 异常结算 record.setSettlementAmount(0) .setClosingPendingBalance(available) .setStatus(SettlementRecord.STATUS_异常结算) .setRemark("结算金额为负,结转至下期"); save(record); log.warn("站点 {} 异常结算,结转金额:{} 分", stationId, available); } } /** * 获取上期期末余额作为本期期初 */ private int getOpeningBalance(String stationId, String currentPeriod) { SettlementRecord lastRecord = lambdaQuery() .eq(SettlementRecord::getStationId, stationId) .orderByDesc(SettlementRecord::getSettlementPeriod) .last("LIMIT 1") .one(); return lastRecord != null ? lastRecord.getClosingPendingBalance() : 0; } /** * 按类型汇总金额 * @param isIncome true: 按 toStationId 汇总,false: 按 fromStationId 汇总 */ private int sumByType(List records, String stationId, Integer type, boolean isIncome) { return records.stream() .filter(r -> type.equals(r.getType())) .filter(r -> isIncome ? stationId.equals(r.getToStationId()) : stationId.equals(r.getFromStationId())) .mapToInt(r -> r.getAmount() != null ? r.getAmount() : 0) .sum(); } @Override public PageBean listSettlementRecords(SettlementQueryParam params) { PageHelper.startPage(params.getPageNum(), params.getPageSize()); var res = lambdaQuery() .eq(CommUtil.isNotEmptyAndNull(params.getStationId()), SettlementRecord::getStationId, params.getStationId()) .eq(CommUtil.isNotEmptyAndNull(params.getSettlementPeriod()), SettlementRecord::getSettlementPeriod, params.getSettlementPeriod()) .eq(CommUtil.isNotEmptyAndNull(params.getStatus()), SettlementRecord::getStatus, params.getStatus()) .orderByDesc(SettlementRecord::getSettlementPeriod) .orderByDesc(SettlementRecord::getId) .list(); var voList = res.stream().map(item -> { var vo = new SettlementRecordVo(); BeanUtils.copyProperties(item, vo); return vo; }).toList(); return new PageBean<>(voList); } }