SettlementServiceImpl.java 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. package com.kym.service.impl;
  2. import com.github.pagehelper.PageHelper;
  3. import com.kym.common.utils.CommUtil;
  4. import com.kym.entity.SettlementRecord;
  5. import com.kym.entity.SplitRecord;
  6. import com.kym.entity.StationAccount;
  7. import com.kym.entity.common.PageBean;
  8. import com.kym.entity.queryParams.SettlementQueryParam;
  9. import com.kym.entity.vo.SettlementRecordVo;
  10. import com.kym.mapper.SettlementRecordMapper;
  11. import com.kym.service.SettlementService;
  12. import com.kym.service.PlatformAccountService;
  13. import com.kym.service.PlatformRevenueRecordService;
  14. import com.kym.service.SplitRecordService;
  15. import com.kym.service.StationAccountService;
  16. import com.kym.service.mybatisplus.MyBaseServiceImpl;
  17. import lombok.extern.slf4j.Slf4j;
  18. import org.springframework.beans.BeanUtils;
  19. import org.springframework.stereotype.Service;
  20. import org.springframework.transaction.annotation.Transactional;
  21. import java.math.BigDecimal;
  22. import java.math.RoundingMode;
  23. import java.time.LocalDate;
  24. import java.time.LocalDateTime;
  25. import java.time.LocalTime;
  26. import java.time.YearMonth;
  27. import java.time.format.DateTimeFormatter;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.Set;
  31. import java.util.stream.Collectors;
  32. /**
  33. * 结算服务实现
  34. *
  35. * @author skyline
  36. * @since 2026-05-14
  37. */
  38. @Slf4j
  39. @Service
  40. public class SettlementServiceImpl extends MyBaseServiceImpl<SettlementRecordMapper, SettlementRecord> implements SettlementService {
  41. private static final BigDecimal PLATFORM_FEE_RATE = new BigDecimal("0.1");
  42. private static final DateTimeFormatter PERIOD_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
  43. private final SplitRecordService splitRecordService;
  44. private final StationAccountService stationAccountService;
  45. private final PlatformAccountService platformAccountService;
  46. private final PlatformRevenueRecordService platformRevenueRecordService;
  47. public SettlementServiceImpl(SplitRecordService splitRecordService, StationAccountService stationAccountService,
  48. PlatformAccountService platformAccountService, PlatformRevenueRecordService platformRevenueRecordService) {
  49. this.splitRecordService = splitRecordService;
  50. this.stationAccountService = stationAccountService;
  51. this.platformAccountService = platformAccountService;
  52. this.platformRevenueRecordService = platformRevenueRecordService;
  53. }
  54. @Override
  55. @Transactional(rollbackFor = Exception.class)
  56. public void executeMonthlySettlement() {
  57. YearMonth lastMonth = YearMonth.now().minusMonths(1);
  58. String period = lastMonth.format(PERIOD_FORMATTER);
  59. LocalDateTime periodStart = lastMonth.atDay(1).atStartOfDay();
  60. LocalDateTime periodEnd = lastMonth.atEndOfMonth().atTime(LocalTime.MAX);
  61. log.info("开始执行结算,周期:{}", period);
  62. // 幂等检查:如果本期已有结算记录则跳过
  63. long settledCount = lambdaQuery().eq(SettlementRecord::getSettlementPeriod, period).count();
  64. if (settledCount > 0) {
  65. log.warn("本期 {} 已有 {} 条结算记录,跳过重复结算", period, settledCount);
  66. return;
  67. }
  68. // 汇总本期各站点分账数据
  69. var allRecords = splitRecordService.lambdaQuery()
  70. .ge(SplitRecord::getCreateTime, periodStart)
  71. .le(SplitRecord::getCreateTime, periodEnd)
  72. .list();
  73. // 站点 -> {recharge, refund, crossExpend, crossIncome}
  74. Set<String> stationIds = allRecords.stream()
  75. .flatMap(r -> List.of(r.getFromStationId(), r.getToStationId()).stream())
  76. .collect(Collectors.toSet());
  77. for (String stationId : stationIds) {
  78. doStationSettlement(stationId, period, allRecords);
  79. }
  80. log.info("结算完成,周期:{},涉及站点数:{}", period, stationIds.size());
  81. }
  82. private void doStationSettlement(String stationId, String period, List<SplitRecord> allRecords) {
  83. int totalRecharge = sumByType(allRecords, stationId, SplitRecord.TYPE_RECHARGE, true);
  84. int totalRefund = sumByType(allRecords, stationId, SplitRecord.TYPE_REFUND, false);
  85. int totalCrossExpend = sumByType(allRecords, stationId, SplitRecord.TYPE_CROSS_EXPEND, false);
  86. int totalCrossIncome = sumByType(allRecords, stationId, SplitRecord.TYPE_CROSS_INCOME, true);
  87. if (totalRecharge == 0 && totalRefund == 0 && totalCrossExpend == 0 && totalCrossIncome == 0) {
  88. return;
  89. }
  90. // 读取上期期末余额作为本期期初
  91. int openingBalance = getOpeningBalance(stationId, period);
  92. // 平台服务费基数(仅基于本期流入,不含上期结转)
  93. int feeBase = totalRecharge - totalRefund - totalCrossExpend + totalCrossIncome;
  94. int platformFee = feeBase > 0 ? new BigDecimal(feeBase).multiply(PLATFORM_FEE_RATE).setScale(0, RoundingMode.DOWN).intValue() : 0;
  95. // 可结算总额 = 期初结转 + 本期流入 - 平台费
  96. int available = openingBalance + feeBase - platformFee;
  97. SettlementRecord record = new SettlementRecord()
  98. .setStationId(stationId)
  99. .setSettlementPeriod(period)
  100. .setTotalRecharge(totalRecharge)
  101. .setTotalRefund(totalRefund)
  102. .setTotalCrossIncome(totalCrossIncome)
  103. .setTotalCrossExpend(totalCrossExpend)
  104. .setOpeningPendingBalance(openingBalance)
  105. .setPlatformFeeBase(feeBase)
  106. .setPlatformFee(platformFee);
  107. if (available > 0) {
  108. // 正常结算
  109. record.setSettlementAmount(available)
  110. .setClosingPendingBalance(0)
  111. .setStatus(SettlementRecord.STATUS_已结算);
  112. // 增加站点可提现金额
  113. stationAccountService.lambdaUpdate()
  114. .setSql("available_balance = available_balance + {0}", available)
  115. .eq(StationAccount::getStationId, stationId)
  116. .update();
  117. save(record);
  118. // 平台服务费收入记录
  119. if (platformFee > 0) {
  120. platformRevenueRecordService.saveRecord(record, stationId);
  121. platformAccountService.addRevenue(platformFee);
  122. }
  123. log.info("站点 {} 结算成功,金额:{} 分,平台费:{} 分", stationId, available, platformFee);
  124. } else {
  125. // 异常结算
  126. record.setSettlementAmount(0)
  127. .setClosingPendingBalance(available)
  128. .setStatus(SettlementRecord.STATUS_异常结算)
  129. .setRemark("结算金额为负,结转至下期");
  130. save(record);
  131. log.warn("站点 {} 异常结算,结转金额:{} 分", stationId, available);
  132. }
  133. }
  134. /**
  135. * 获取上期期末余额作为本期期初
  136. */
  137. private int getOpeningBalance(String stationId, String currentPeriod) {
  138. SettlementRecord lastRecord = lambdaQuery()
  139. .eq(SettlementRecord::getStationId, stationId)
  140. .orderByDesc(SettlementRecord::getSettlementPeriod)
  141. .last("LIMIT 1")
  142. .one();
  143. return lastRecord != null ? lastRecord.getClosingPendingBalance() : 0;
  144. }
  145. /**
  146. * 按类型汇总金额
  147. * @param isIncome true: 按 toStationId 汇总,false: 按 fromStationId 汇总
  148. */
  149. private int sumByType(List<SplitRecord> records, String stationId, Integer type, boolean isIncome) {
  150. return records.stream()
  151. .filter(r -> type.equals(r.getType()))
  152. .filter(r -> isIncome ? stationId.equals(r.getToStationId()) : stationId.equals(r.getFromStationId()))
  153. .mapToInt(r -> r.getAmount() != null ? r.getAmount() : 0)
  154. .sum();
  155. }
  156. @Override
  157. public PageBean<SettlementRecordVo> listSettlementRecords(SettlementQueryParam params) {
  158. PageHelper.startPage(params.getPageNum(), params.getPageSize());
  159. var res = lambdaQuery()
  160. .eq(CommUtil.isNotEmptyAndNull(params.getStationId()), SettlementRecord::getStationId, params.getStationId())
  161. .eq(CommUtil.isNotEmptyAndNull(params.getSettlementPeriod()), SettlementRecord::getSettlementPeriod, params.getSettlementPeriod())
  162. .eq(CommUtil.isNotEmptyAndNull(params.getStatus()), SettlementRecord::getStatus, params.getStatus())
  163. .orderByDesc(SettlementRecord::getSettlementPeriod)
  164. .orderByDesc(SettlementRecord::getId)
  165. .list();
  166. var voList = res.stream().map(item -> {
  167. var vo = new SettlementRecordVo();
  168. BeanUtils.copyProperties(item, vo);
  169. return vo;
  170. }).toList();
  171. return new PageBean<>(voList);
  172. }
  173. }