| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- 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<SettlementRecordMapper, SettlementRecord> 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<String> 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<SplitRecord> 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<SplitRecord> 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<SettlementRecordVo> 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);
- }
- }
|