package com.tcm.training.admin.statistics.service; import cn.idev.excel.FastExcel; import com.tcm.training.admin.trade.service.PaymentFlowAppService; import com.tcm.training.admin.statistics.excel.RevenueStatisticsExcelRow; import com.tcm.training.admin.statistics.mapper.RevenueStatisticsMapper; import com.tcm.training.admin.statistics.row.RevenueStatisticsDailyRow; import com.tcm.training.admin.statistics.vo.RevenueStatisticsOverviewVO; import com.tcm.training.admin.statistics.vo.RevenueStatisticsTrendItemVO; import com.tcm.training.admin.statistics.vo.RevenueStatisticsVO; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URLEncoder; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class RevenueStatisticsAppService { private static final String FILTER_ALL = "ALL"; private static final String FILTER_HEALTH = "HEALTH"; private static final String FILTER_MEMBER = "MEMBER"; private static final String BUSINESS_TYPE_HEALTH = "COURSE_PURCHASE"; private static final String BUSINESS_TYPE_MEMBER = "MEMBER_PURCHASE"; private final RevenueStatisticsMapper revenueStatisticsMapper; private final PaymentFlowAppService paymentFlowAppService; public RevenueStatisticsAppService(RevenueStatisticsMapper revenueStatisticsMapper, PaymentFlowAppService paymentFlowAppService) { this.revenueStatisticsMapper = revenueStatisticsMapper; this.paymentFlowAppService = paymentFlowAppService; } public RevenueStatisticsVO statistics(String type, LocalDate dateFrom, LocalDate dateTo) { DateRange range = normalizeRange(dateFrom, dateTo); String normalizedType = normalizeType(type); paymentFlowAppService.syncFlows(); List rows = revenueStatisticsMapper.selectDailyStatistics( resolveBusinessType(normalizedType), range.getStart().atStartOfDay(), LocalDateTime.of(range.getEnd(), LocalTime.MAX) ); List trend = buildTrend(rows, range); RevenueStatisticsVO result = new RevenueStatisticsVO(); result.setOverview(buildOverview(trend)); result.setTrend(trend); return result; } public void export(HttpServletResponse response, String type, LocalDate dateFrom, LocalDate dateTo) throws IOException { RevenueStatisticsVO statistics = statistics(type, dateFrom, dateTo); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("营收数据统计", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); List exports = new ArrayList(); for (RevenueStatisticsTrendItemVO item : statistics.getTrend()) { RevenueStatisticsExcelRow export = new RevenueStatisticsExcelRow(); export.setDate(item.getDate()); export.setTotalIncome(formatAmount(item.getTotalIncome())); export.setHealthIncome(formatAmount(item.getHealthIncome())); export.setMemberIncome(formatAmount(item.getMemberIncome())); export.setRefundAmount(formatAmount(item.getRefundAmount())); export.setProfit(formatAmount(item.getProfit())); exports.add(export); } FastExcel.write(response.getOutputStream(), RevenueStatisticsExcelRow.class) .sheet("营收数据") .doWrite(exports); } private List buildTrend(List rows, DateRange range) { Map rowMap = new HashMap(); for (RevenueStatisticsDailyRow row : rows) { rowMap.put(row.getStatDate(), row); } List result = new ArrayList(); LocalDate cursor = range.getStart(); while (!cursor.isAfter(range.getEnd())) { RevenueStatisticsDailyRow row = rowMap.get(cursor); BigDecimal healthIncome = nvl(row == null ? null : row.getHealthIncome()); BigDecimal memberIncome = nvl(row == null ? null : row.getMemberIncome()); BigDecimal healthRefundAmount = nvl(row == null ? null : row.getHealthRefundAmount()); BigDecimal memberRefundAmount = nvl(row == null ? null : row.getMemberRefundAmount()); BigDecimal refundAmount = healthRefundAmount.add(memberRefundAmount); BigDecimal totalIncome = healthIncome.add(memberIncome); RevenueStatisticsTrendItemVO item = new RevenueStatisticsTrendItemVO(); item.setDate(cursor.toString()); item.setHealthIncome(healthIncome); item.setMemberIncome(memberIncome); item.setHealthRefundAmount(healthRefundAmount); item.setMemberRefundAmount(memberRefundAmount); item.setTotalIncome(totalIncome); item.setRefundAmount(refundAmount); item.setProfit(totalIncome.subtract(refundAmount)); result.add(item); cursor = cursor.plusDays(1); } return result; } private RevenueStatisticsOverviewVO buildOverview(List trend) { RevenueStatisticsOverviewVO overview = new RevenueStatisticsOverviewVO(); BigDecimal totalIncome = BigDecimal.ZERO; BigDecimal healthIncome = BigDecimal.ZERO; BigDecimal memberIncome = BigDecimal.ZERO; BigDecimal refundAmount = BigDecimal.ZERO; BigDecimal profit = BigDecimal.ZERO; for (RevenueStatisticsTrendItemVO item : trend) { totalIncome = totalIncome.add(nvl(item.getTotalIncome())); healthIncome = healthIncome.add(nvl(item.getHealthIncome())); memberIncome = memberIncome.add(nvl(item.getMemberIncome())); refundAmount = refundAmount.add(nvl(item.getRefundAmount())); profit = profit.add(nvl(item.getProfit())); } overview.setTotalIncome(totalIncome); overview.setHealthIncome(healthIncome); overview.setMemberIncome(memberIncome); overview.setRefundAmount(refundAmount); overview.setProfit(profit); return overview; } private String normalizeType(String type) { String normalized = trimToNull(type); if (normalized == null) { return FILTER_ALL; } if (FILTER_ALL.equals(normalized) || FILTER_HEALTH.equals(normalized) || FILTER_MEMBER.equals(normalized)) { return normalized; } throw new IllegalArgumentException("统计类型不合法"); } private String resolveBusinessType(String normalizedType) { if (FILTER_HEALTH.equals(normalizedType)) { return BUSINESS_TYPE_HEALTH; } if (FILTER_MEMBER.equals(normalizedType)) { return BUSINESS_TYPE_MEMBER; } return null; } private DateRange normalizeRange(LocalDate dateFrom, LocalDate dateTo) { LocalDate start = dateFrom; LocalDate end = dateTo; if (start == null && end == null) { start = LocalDate.now(); end = start; } else if (start == null) { start = end; } else if (end == null) { end = start; } if (start.isAfter(end)) { throw new IllegalArgumentException("开始日期不能晚于结束日期"); } return new DateRange(start, end); } private String formatAmount(BigDecimal value) { return nvl(value).setScale(2, RoundingMode.HALF_UP).toPlainString(); } private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private static class DateRange { private final LocalDate start; private final LocalDate end; private DateRange(LocalDate start, LocalDate end) { this.start = start; this.end = end; } private LocalDate getStart() { return start; } private LocalDate getEnd() { return end; } } }