SettlementServiceImpl.java 9.4 KB

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