feat(wm-revenue): #83 首页营收总览+查询统计(Dashboard+多维度报表+导出)
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.RevenueDashboardService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "营收总览")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class RevenueDashboardController {
|
||||
|
||||
private final RevenueDashboardService dashboardService;
|
||||
|
||||
@GetMapping("/overview")
|
||||
@Operation(summary = "首页总览")
|
||||
public R<Map<String, Object>> getOverview() {
|
||||
return R.ok(dashboardService.getOverview());
|
||||
}
|
||||
|
||||
@GetMapping("/trend")
|
||||
@Operation(summary = "营收趋势")
|
||||
public R<List<Map<String, Object>>> getRevenueTrend(
|
||||
@Parameter(description = "周期:day|week|month")
|
||||
@RequestParam(defaultValue = "month") String period) {
|
||||
return R.ok(dashboardService.getRevenueTrend(period));
|
||||
}
|
||||
|
||||
@GetMapping("/area/{area}")
|
||||
@Operation(summary = "区域营收")
|
||||
public R<Map<String, Object>> getAreaRevenue(
|
||||
@Parameter(description = "区域") @PathVariable String area) {
|
||||
return R.ok(dashboardService.getAreaRevenue(area));
|
||||
}
|
||||
|
||||
@GetMapping("/payment-channels")
|
||||
@Operation(summary = "支付渠道统计")
|
||||
public R<List<Map<String, Object>>> getPaymentChannelStats() {
|
||||
return R.ok(dashboardService.getPaymentChannelStats());
|
||||
}
|
||||
|
||||
@GetMapping("/customer-types")
|
||||
@Operation(summary = "客户类型统计")
|
||||
public R<List<Map<String, Object>>> getCustomerTypeStats() {
|
||||
return R.ok(dashboardService.getCustomerTypeStats());
|
||||
}
|
||||
|
||||
@GetMapping("/top-overdue")
|
||||
@Operation(summary = "欠费大户")
|
||||
public R<List<Map<String, Object>>> getTopOverdueCustomers(
|
||||
@Parameter(description = "数量限制")
|
||||
@RequestParam(defaultValue = "10") Integer limit) {
|
||||
return R.ok(dashboardService.getTopOverdueCustomers(limit));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.RevenueQueryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "营收查询统计")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/query")
|
||||
@RequiredArgsConstructor
|
||||
public class RevenueQueryController {
|
||||
|
||||
private final RevenueQueryService queryService;
|
||||
|
||||
@GetMapping("/bills")
|
||||
@Operation(summary = "账单查询")
|
||||
public R<Map<String, Object>> queryBills(
|
||||
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
|
||||
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate,
|
||||
@Parameter(description = "区域") @RequestParam(required = false) String area,
|
||||
@Parameter(description = "客户类型") @RequestParam(required = false) String customerType,
|
||||
@Parameter(description = "状态") @RequestParam(required = false) String status,
|
||||
@Parameter(description = "最小金额") @RequestParam(required = false) BigDecimal minAmount,
|
||||
@Parameter(description = "最大金额") @RequestParam(required = false) BigDecimal maxAmount,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer size) {
|
||||
|
||||
Map<String, Object> filters = new HashMap<>();
|
||||
if (startDate != null) filters.put("startDate", startDate);
|
||||
if (endDate != null) filters.put("endDate", endDate);
|
||||
if (area != null) filters.put("area", area);
|
||||
if (customerType != null) filters.put("customerType", customerType);
|
||||
if (status != null) filters.put("status", status);
|
||||
if (minAmount != null) filters.put("minAmount", minAmount);
|
||||
if (maxAmount != null) filters.put("maxAmount", maxAmount);
|
||||
filters.put("page", page);
|
||||
filters.put("size", size);
|
||||
|
||||
return R.ok(queryService.queryBills(filters));
|
||||
}
|
||||
|
||||
@GetMapping("/payments")
|
||||
@Operation(summary = "缴费查询")
|
||||
public R<Map<String, Object>> queryPayments(
|
||||
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
|
||||
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate,
|
||||
@Parameter(description = "区域") @RequestParam(required = false) String area,
|
||||
@Parameter(description = "支付方式") @RequestParam(required = false) String payMethod,
|
||||
@Parameter(description = "支付渠道") @RequestParam(required = false) String payChannel,
|
||||
@Parameter(description = "最小金额") @RequestParam(required = false) BigDecimal minAmount,
|
||||
@Parameter(description = "最大金额") @RequestParam(required = false) BigDecimal maxAmount,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer size) {
|
||||
|
||||
Map<String, Object> filters = new HashMap<>();
|
||||
if (startDate != null) filters.put("startDate", startDate);
|
||||
if (endDate != null) filters.put("endDate", endDate);
|
||||
if (area != null) filters.put("area", area);
|
||||
if (payMethod != null) filters.put("payMethod", payMethod);
|
||||
if (payChannel != null) filters.put("payChannel", payChannel);
|
||||
if (minAmount != null) filters.put("minAmount", minAmount);
|
||||
if (maxAmount != null) filters.put("maxAmount", maxAmount);
|
||||
filters.put("page", page);
|
||||
filters.put("size", size);
|
||||
|
||||
return R.ok(queryService.queryPayments(filters));
|
||||
}
|
||||
|
||||
@GetMapping("/report/monthly")
|
||||
@Operation(summary = "月度报表")
|
||||
public R<Map<String, Object>> monthlyReport(
|
||||
@Parameter(description = "年份") @RequestParam Integer year,
|
||||
@Parameter(description = "月份") @RequestParam Integer month) {
|
||||
return R.ok(queryService.monthlyReport(year, month));
|
||||
}
|
||||
|
||||
@GetMapping("/report/yearly")
|
||||
@Operation(summary = "年度报表")
|
||||
public R<Map<String, Object>> yearlyReport(
|
||||
@Parameter(description = "年份") @RequestParam Integer year) {
|
||||
return R.ok(queryService.yearlyReport(year));
|
||||
}
|
||||
|
||||
@GetMapping("/export/bills")
|
||||
@Operation(summary = "导出账单CSV")
|
||||
public R<String> exportBills(
|
||||
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
|
||||
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate,
|
||||
@Parameter(description = "区域") @RequestParam(required = false) String area,
|
||||
@Parameter(description = "客户类型") @RequestParam(required = false) String customerType,
|
||||
@Parameter(description = "状态") @RequestParam(required = false) String status) {
|
||||
|
||||
Map<String, Object> filters = new HashMap<>();
|
||||
if (startDate != null) filters.put("startDate", startDate);
|
||||
if (endDate != null) filters.put("endDate", endDate);
|
||||
if (area != null) filters.put("area", area);
|
||||
if (customerType != null) filters.put("customerType", customerType);
|
||||
if (status != null) filters.put("status", status);
|
||||
|
||||
return R.ok(queryService.exportBills(filters));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RevenueDashboardService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 首页总览
|
||||
*/
|
||||
public Map<String, Object> getOverview() {
|
||||
log.info("Getting revenue overview");
|
||||
|
||||
// 总营收
|
||||
BigDecimal totalRevenue = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(paid_fee), 0) FROM rev_bill", BigDecimal.class);
|
||||
|
||||
// 本月营收
|
||||
BigDecimal monthRevenue = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(paid_fee), 0) FROM rev_bill WHERE bill_period = ?",
|
||||
BigDecimal.class,
|
||||
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||
|
||||
// 今日营收
|
||||
BigDecimal todayRevenue = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM rev_payment WHERE DATE(paid_at) = CURRENT_DATE",
|
||||
BigDecimal.class);
|
||||
|
||||
// 待缴账单数
|
||||
Integer pendingBills = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_bill WHERE status IN ('pending', 'partial')",
|
||||
Integer.class);
|
||||
|
||||
// 逾期账单数
|
||||
Integer overdueBills = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_bill WHERE status = 'overdue'",
|
||||
Integer.class);
|
||||
|
||||
// 逾期金额
|
||||
BigDecimal overdueAmount = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(total_fee - paid_fee), 0) FROM rev_bill WHERE status = 'overdue'",
|
||||
BigDecimal.class);
|
||||
|
||||
// 收缴率
|
||||
BigDecimal collectionRate = calculateCollectionRate();
|
||||
|
||||
// 客户总数
|
||||
Integer customerCount = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_customer", Integer.class);
|
||||
|
||||
Map<String, Object> overview = new HashMap<>();
|
||||
overview.put("totalRevenue", totalRevenue);
|
||||
overview.put("monthRevenue", monthRevenue);
|
||||
overview.put("todayRevenue", todayRevenue);
|
||||
overview.put("pendingBills", pendingBills);
|
||||
overview.put("overdueBills", overdueBills);
|
||||
overview.put("overdueAmount", overdueAmount);
|
||||
overview.put("collectionRate", collectionRate);
|
||||
overview.put("customerCount", customerCount);
|
||||
|
||||
log.info("Overview retrieved: totalRevenue={}, monthRevenue={}", totalRevenue, monthRevenue);
|
||||
return overview;
|
||||
}
|
||||
|
||||
/**
|
||||
* 营收趋势(按日/周/月)
|
||||
*/
|
||||
public List<Map<String, Object>> getRevenueTrend(String period) {
|
||||
log.info("Getting revenue trend for period: {}", period);
|
||||
|
||||
String dateFormat;
|
||||
int days;
|
||||
|
||||
switch (period.toLowerCase()) {
|
||||
case "day":
|
||||
dateFormat = "DATE(paid_at)";
|
||||
days = 30;
|
||||
break;
|
||||
case "week":
|
||||
dateFormat = "DATE_TRUNC('week', paid_at)";
|
||||
days = 84; // 12 weeks
|
||||
break;
|
||||
case "month":
|
||||
default:
|
||||
dateFormat = "TO_CHAR(paid_at, 'YYYY-MM')";
|
||||
days = 365;
|
||||
break;
|
||||
}
|
||||
|
||||
LocalDate startDate = LocalDate.now().minusDays(days);
|
||||
|
||||
String sql = String.format(
|
||||
"SELECT %s as period, COALESCE(SUM(amount), 0) as revenue, COUNT(*) as count " +
|
||||
"FROM rev_payment " +
|
||||
"WHERE paid_at >= ? " +
|
||||
"GROUP BY %s " +
|
||||
"ORDER BY period",
|
||||
dateFormat, dateFormat);
|
||||
|
||||
List<Map<String, Object>> trends = jdbcTemplate.queryForList(sql, startDate);
|
||||
|
||||
log.info("Revenue trend retrieved: {} data points", trends.size());
|
||||
return trends;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按区域统计营收
|
||||
*/
|
||||
public Map<String, Object> getAreaRevenue(String area) {
|
||||
log.info("Getting area revenue for: {}", area);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 区域总营收
|
||||
BigDecimal totalRevenue = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(b.paid_fee), 0) FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE c.area = ?",
|
||||
BigDecimal.class, area);
|
||||
|
||||
// 区域账单数
|
||||
Integer billCount = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE c.area = ?",
|
||||
Integer.class, area);
|
||||
|
||||
// 区域欠费
|
||||
BigDecimal overdueAmount = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(b.total_fee - b.paid_fee), 0) FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE c.area = ? AND b.status IN ('pending', 'partial', 'overdue')",
|
||||
BigDecimal.class, area);
|
||||
|
||||
// 区域客户数
|
||||
Integer customerCount = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_customer WHERE area = ?",
|
||||
Integer.class, area);
|
||||
|
||||
result.put("area", area);
|
||||
result.put("totalRevenue", totalRevenue);
|
||||
result.put("billCount", billCount);
|
||||
result.put("overdueAmount", overdueAmount);
|
||||
result.put("customerCount", customerCount);
|
||||
|
||||
log.info("Area revenue for {}: totalRevenue={}, billCount={}", area, totalRevenue, billCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按支付渠道统计
|
||||
*/
|
||||
public List<Map<String, Object>> getPaymentChannelStats() {
|
||||
log.info("Getting payment channel statistics");
|
||||
|
||||
List<Map<String, Object>> stats = jdbcTemplate.queryForList(
|
||||
"SELECT pay_channel, COUNT(*) as count, COALESCE(SUM(amount), 0) as total " +
|
||||
"FROM rev_payment " +
|
||||
"GROUP BY pay_channel " +
|
||||
"ORDER BY total DESC");
|
||||
|
||||
log.info("Payment channel stats retrieved: {} channels", stats.size());
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客户类型统计
|
||||
*/
|
||||
public List<Map<String, Object>> getCustomerTypeStats() {
|
||||
log.info("Getting customer type statistics");
|
||||
|
||||
List<Map<String, Object>> stats = jdbcTemplate.queryForList(
|
||||
"SELECT c.customer_type, COUNT(b.id) as billCount, " +
|
||||
"COALESCE(SUM(b.total_fee), 0) as totalAmount, " +
|
||||
"COALESCE(SUM(b.paid_fee), 0) as paidAmount " +
|
||||
"FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"GROUP BY c.customer_type " +
|
||||
"ORDER BY totalAmount DESC");
|
||||
|
||||
log.info("Customer type stats retrieved: {} types", stats.size());
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 欠费大户排行
|
||||
*/
|
||||
public List<Map<String, Object>> getTopOverdueCustomers(int limit) {
|
||||
log.info("Getting top overdue customers, limit: {}", limit);
|
||||
|
||||
List<Map<String, Object>> customers = jdbcTemplate.queryForList(
|
||||
"SELECT c.id, c.customer_name, c.customer_no, c.area, " +
|
||||
"COUNT(b.id) as overdueBillCount, " +
|
||||
"COALESCE(SUM(b.total_fee - b.paid_fee), 0) as overdueAmount " +
|
||||
"FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE b.status IN ('overdue', 'pending', 'partial') " +
|
||||
"GROUP BY c.id, c.customer_name, c.customer_no, c.area " +
|
||||
"HAVING SUM(b.total_fee - b.paid_fee) > 0 " +
|
||||
"ORDER BY overdueAmount DESC " +
|
||||
"LIMIT ?",
|
||||
limit);
|
||||
|
||||
log.info("Top overdue customers retrieved: {} customers", customers.size());
|
||||
return customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算收缴率
|
||||
*/
|
||||
private BigDecimal calculateCollectionRate() {
|
||||
BigDecimal totalBilled = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(total_fee), 0) FROM rev_bill", BigDecimal.class);
|
||||
|
||||
if (totalBilled.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal totalPaid = jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(paid_fee), 0) FROM rev_bill", BigDecimal.class);
|
||||
|
||||
return totalPaid.multiply(BigDecimal.valueOf(100))
|
||||
.divide(totalBilled, 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RevenueQueryService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 多维度账单查询
|
||||
*/
|
||||
public Map<String, Object> queryBills(Map<String, Object> filters) {
|
||||
log.info("Querying bills with filters: {}", filters);
|
||||
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT b.*, c.customer_name, c.customer_no, c.area, c.customer_type, m.meter_no " +
|
||||
"FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"JOIN rev_meter m ON b.meter_id = m.id " +
|
||||
"WHERE 1=1");
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
// 时间范围
|
||||
if (filters.get("startDate") != null) {
|
||||
sql.append(" AND b.created_at >= ?::timestamp");
|
||||
params.add(filters.get("startDate"));
|
||||
}
|
||||
if (filters.get("endDate") != null) {
|
||||
sql.append(" AND b.created_at <= ?::timestamp");
|
||||
params.add(filters.get("endDate"));
|
||||
}
|
||||
|
||||
// 区域
|
||||
if (filters.get("area") != null && !filters.get("area").toString().isEmpty()) {
|
||||
sql.append(" AND c.area = ?");
|
||||
params.add(filters.get("area"));
|
||||
}
|
||||
|
||||
// 客户类型
|
||||
if (filters.get("customerType") != null && !filters.get("customerType").toString().isEmpty()) {
|
||||
sql.append(" AND c.customer_type = ?");
|
||||
params.add(filters.get("customerType"));
|
||||
}
|
||||
|
||||
// 状态
|
||||
if (filters.get("status") != null && !filters.get("status").toString().isEmpty()) {
|
||||
sql.append(" AND b.status = ?");
|
||||
params.add(filters.get("status"));
|
||||
}
|
||||
|
||||
// 金额范围
|
||||
if (filters.get("minAmount") != null) {
|
||||
sql.append(" AND b.total_fee >= ?");
|
||||
params.add(new BigDecimal(filters.get("minAmount").toString()));
|
||||
}
|
||||
if (filters.get("maxAmount") != null) {
|
||||
sql.append(" AND b.total_fee <= ?");
|
||||
params.add(new BigDecimal(filters.get("maxAmount").toString()));
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY b.created_at DESC");
|
||||
|
||||
// 分页
|
||||
int page = filters.get("page") != null ? Integer.parseInt(filters.get("page").toString()) : 1;
|
||||
int size = filters.get("size") != null ? Integer.parseInt(filters.get("size").toString()) : 20;
|
||||
int offset = (page - 1) * size;
|
||||
|
||||
sql.append(" LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add(offset);
|
||||
|
||||
List<Map<String, Object>> bills = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 统计总数
|
||||
String countSql = sql.toString().replaceFirst("SELECT .*? FROM", "SELECT COUNT(*) FROM")
|
||||
.replaceAll(" ORDER BY.*", "");
|
||||
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class,
|
||||
params.subList(0, params.size() - 2).toArray());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("records", bills);
|
||||
result.put("total", total);
|
||||
result.put("page", page);
|
||||
result.put("size", size);
|
||||
result.put("pages", (total + size - 1) / size);
|
||||
|
||||
log.info("Bills queried: {} records, total: {}", bills.size(), total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多维度缴费查询
|
||||
*/
|
||||
public Map<String, Object> queryPayments(Map<String, Object> filters) {
|
||||
log.info("Querying payments with filters: {}", filters);
|
||||
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT p.*, b.bill_no, b.total_fee, c.customer_name, c.customer_no, c.area " +
|
||||
"FROM rev_payment p " +
|
||||
"JOIN rev_bill b ON p.bill_id = b.id " +
|
||||
"JOIN rev_customer c ON p.customer_id = c.id " +
|
||||
"WHERE 1=1");
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
// 时间范围
|
||||
if (filters.get("startDate") != null) {
|
||||
sql.append(" AND p.paid_at >= ?::timestamp");
|
||||
params.add(filters.get("startDate"));
|
||||
}
|
||||
if (filters.get("endDate") != null) {
|
||||
sql.append(" AND p.paid_at <= ?::timestamp");
|
||||
params.add(filters.get("endDate"));
|
||||
}
|
||||
|
||||
// 区域
|
||||
if (filters.get("area") != null && !filters.get("area").toString().isEmpty()) {
|
||||
sql.append(" AND c.area = ?");
|
||||
params.add(filters.get("area"));
|
||||
}
|
||||
|
||||
// 支付方式
|
||||
if (filters.get("payMethod") != null && !filters.get("payMethod").toString().isEmpty()) {
|
||||
sql.append(" AND p.pay_method = ?");
|
||||
params.add(filters.get("payMethod"));
|
||||
}
|
||||
|
||||
// 支付渠道
|
||||
if (filters.get("payChannel") != null && !filters.get("payChannel").toString().isEmpty()) {
|
||||
sql.append(" AND p.pay_channel = ?");
|
||||
params.add(filters.get("payChannel"));
|
||||
}
|
||||
|
||||
// 金额范围
|
||||
if (filters.get("minAmount") != null) {
|
||||
sql.append(" AND p.amount >= ?");
|
||||
params.add(new BigDecimal(filters.get("minAmount").toString()));
|
||||
}
|
||||
if (filters.get("maxAmount") != null) {
|
||||
sql.append(" AND p.amount <= ?");
|
||||
params.add(new BigDecimal(filters.get("maxAmount").toString()));
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY p.paid_at DESC");
|
||||
|
||||
// 分页
|
||||
int page = filters.get("page") != null ? Integer.parseInt(filters.get("page").toString()) : 1;
|
||||
int size = filters.get("size") != null ? Integer.parseInt(filters.get("size").toString()) : 20;
|
||||
int offset = (page - 1) * size;
|
||||
|
||||
sql.append(" LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add(offset);
|
||||
|
||||
List<Map<String, Object>> payments = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 统计总数
|
||||
String countSql = sql.toString().replaceFirst("SELECT .*? FROM", "SELECT COUNT(*) FROM")
|
||||
.replaceAll(" ORDER BY.*", "");
|
||||
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class,
|
||||
params.subList(0, params.size() - 2).toArray());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("records", payments);
|
||||
result.put("total", total);
|
||||
result.put("page", page);
|
||||
result.put("size", size);
|
||||
result.put("pages", (total + size - 1) / size);
|
||||
|
||||
log.info("Payments queried: {} records, total: {}", payments.size(), total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 月度营收报表
|
||||
*/
|
||||
public Map<String, Object> monthlyReport(int year, int month) {
|
||||
log.info("Generating monthly report: {}-{}", year, month);
|
||||
|
||||
String period = String.format("%04d-%02d", year, month);
|
||||
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
report.put("year", year);
|
||||
report.put("month", month);
|
||||
report.put("period", period);
|
||||
|
||||
// 账单统计
|
||||
Map<String, Object> billStats = jdbcTemplate.queryForMap(
|
||||
"SELECT COUNT(*) as billCount, " +
|
||||
"COALESCE(SUM(total_fee), 0) as totalAmount, " +
|
||||
"COALESCE(SUM(paid_fee), 0) as paidAmount, " +
|
||||
"COALESCE(SUM(total_fee - paid_fee), 0) as unpaidAmount " +
|
||||
"FROM rev_bill WHERE bill_period = ?",
|
||||
period);
|
||||
report.putAll(billStats);
|
||||
|
||||
// 缴费统计
|
||||
Map<String, Object> paymentStats = jdbcTemplate.queryForMap(
|
||||
"SELECT COUNT(*) as paymentCount, " +
|
||||
"COALESCE(SUM(amount), 0) as paymentAmount " +
|
||||
"FROM rev_payment p " +
|
||||
"JOIN rev_bill b ON p.bill_id = b.id " +
|
||||
"WHERE b.bill_period = ?",
|
||||
period);
|
||||
report.putAll(paymentStats);
|
||||
|
||||
// 按区域统计
|
||||
List<Map<String, Object>> areaStats = jdbcTemplate.queryForList(
|
||||
"SELECT c.area, COUNT(b.id) as billCount, " +
|
||||
"COALESCE(SUM(b.total_fee), 0) as totalAmount, " +
|
||||
"COALESCE(SUM(b.paid_fee), 0) as paidAmount " +
|
||||
"FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE b.bill_period = ? " +
|
||||
"GROUP BY c.area " +
|
||||
"ORDER BY totalAmount DESC",
|
||||
period);
|
||||
report.put("areaStats", areaStats);
|
||||
|
||||
// 按支付方式统计
|
||||
List<Map<String, Object>> paymentMethodStats = jdbcTemplate.queryForList(
|
||||
"SELECT p.pay_method, COUNT(*) as count, " +
|
||||
"COALESCE(SUM(p.amount), 0) as amount " +
|
||||
"FROM rev_payment p " +
|
||||
"JOIN rev_bill b ON p.bill_id = b.id " +
|
||||
"WHERE b.bill_period = ? " +
|
||||
"GROUP BY p.pay_method " +
|
||||
"ORDER BY amount DESC",
|
||||
period);
|
||||
report.put("paymentMethodStats", paymentMethodStats);
|
||||
|
||||
log.info("Monthly report generated for {}: totalAmount={}, paidAmount={}",
|
||||
period, billStats.get("totalAmount"), billStats.get("paidAmount"));
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 年度营收报表
|
||||
*/
|
||||
public Map<String, Object> yearlyReport(int year) {
|
||||
log.info("Generating yearly report: {}", year);
|
||||
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
report.put("year", year);
|
||||
|
||||
// 年度总计
|
||||
Map<String, Object> yearlyStats = jdbcTemplate.queryForMap(
|
||||
"SELECT COUNT(*) as billCount, " +
|
||||
"COALESCE(SUM(total_fee), 0) as totalAmount, " +
|
||||
"COALESCE(SUM(paid_fee), 0) as paidAmount, " +
|
||||
"COALESCE(SUM(total_fee - paid_fee), 0) as unpaidAmount " +
|
||||
"FROM rev_bill WHERE EXTRACT(YEAR FROM created_at) = ?",
|
||||
year);
|
||||
report.putAll(yearlyStats);
|
||||
|
||||
// 年度缴费统计
|
||||
Map<String, Object> paymentStats = jdbcTemplate.queryForMap(
|
||||
"SELECT COUNT(*) as paymentCount, " +
|
||||
"COALESCE(SUM(p.amount), 0) as paymentAmount " +
|
||||
"FROM rev_payment p " +
|
||||
"JOIN rev_bill b ON p.bill_id = b.id " +
|
||||
"WHERE EXTRACT(YEAR FROM b.created_at) = ?",
|
||||
year);
|
||||
report.putAll(paymentStats);
|
||||
|
||||
// 按月统计
|
||||
List<Map<String, Object>> monthlyStats = jdbcTemplate.queryForList(
|
||||
"SELECT bill_period as month, COUNT(*) as billCount, " +
|
||||
"COALESCE(SUM(total_fee), 0) as totalAmount, " +
|
||||
"COALESCE(SUM(paid_fee), 0) as paidAmount " +
|
||||
"FROM rev_bill " +
|
||||
"WHERE EXTRACT(YEAR FROM created_at) = ? " +
|
||||
"GROUP BY bill_period " +
|
||||
"ORDER BY bill_period",
|
||||
year);
|
||||
report.put("monthlyStats", monthlyStats);
|
||||
|
||||
// 按区域统计
|
||||
List<Map<String, Object>> areaStats = jdbcTemplate.queryForList(
|
||||
"SELECT c.area, COUNT(b.id) as billCount, " +
|
||||
"COALESCE(SUM(b.total_fee), 0) as totalAmount, " +
|
||||
"COALESCE(SUM(b.paid_fee), 0) as paidAmount " +
|
||||
"FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE EXTRACT(YEAR FROM b.created_at) = ? " +
|
||||
"GROUP BY c.area " +
|
||||
"ORDER BY totalAmount DESC",
|
||||
year);
|
||||
report.put("areaStats", areaStats);
|
||||
|
||||
log.info("Yearly report generated for {}: totalAmount={}, paidAmount={}",
|
||||
year, yearlyStats.get("totalAmount"), yearlyStats.get("paidAmount"));
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出账单(CSV 格式)
|
||||
*/
|
||||
public String exportBills(Map<String, Object> filters) {
|
||||
log.info("Exporting bills with filters: {}", filters);
|
||||
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT b.bill_no, c.customer_name, c.customer_no, c.area, c.customer_type, " +
|
||||
"m.meter_no, b.bill_period, b.consumption, b.water_fee, b.sewage_fee, " +
|
||||
"b.total_fee, b.paid_fee, b.status, b.due_date, b.created_at " +
|
||||
"FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"JOIN rev_meter m ON b.meter_id = m.id " +
|
||||
"WHERE 1=1");
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
// 应用过滤条件(同 queryBills)
|
||||
if (filters.get("startDate") != null) {
|
||||
sql.append(" AND b.created_at >= ?::timestamp");
|
||||
params.add(filters.get("startDate"));
|
||||
}
|
||||
if (filters.get("endDate") != null) {
|
||||
sql.append(" AND b.created_at <= ?::timestamp");
|
||||
params.add(filters.get("endDate"));
|
||||
}
|
||||
if (filters.get("area") != null && !filters.get("area").toString().isEmpty()) {
|
||||
sql.append(" AND c.area = ?");
|
||||
params.add(filters.get("area"));
|
||||
}
|
||||
if (filters.get("customerType") != null && !filters.get("customerType").toString().isEmpty()) {
|
||||
sql.append(" AND c.customer_type = ?");
|
||||
params.add(filters.get("customerType"));
|
||||
}
|
||||
if (filters.get("status") != null && !filters.get("status").toString().isEmpty()) {
|
||||
sql.append(" AND b.status = ?");
|
||||
params.add(filters.get("status"));
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY b.created_at DESC LIMIT 10000"); // 限制导出数量
|
||||
|
||||
List<Map<String, Object>> bills = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 生成 CSV
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("账单编号,客户名称,客户编号,区域,客户类型,水表号,账期,用水量,水费,污水处理费,")
|
||||
.append("总金额,已缴金额,状态,到期日期,创建时间\n");
|
||||
|
||||
for (Map<String, Object> bill : bills) {
|
||||
csv.append(escapeCsv(bill.get("bill_no"))).append(",")
|
||||
.append(escapeCsv(bill.get("customer_name"))).append(",")
|
||||
.append(escapeCsv(bill.get("customer_no"))).append(",")
|
||||
.append(escapeCsv(bill.get("area"))).append(",")
|
||||
.append(escapeCsv(bill.get("customer_type"))).append(",")
|
||||
.append(escapeCsv(bill.get("meter_no"))).append(",")
|
||||
.append(escapeCsv(bill.get("bill_period"))).append(",")
|
||||
.append(bill.get("consumption")).append(",")
|
||||
.append(bill.get("water_fee")).append(",")
|
||||
.append(bill.get("sewage_fee")).append(",")
|
||||
.append(bill.get("total_fee")).append(",")
|
||||
.append(bill.get("paid_fee")).append(",")
|
||||
.append(escapeCsv(bill.get("status"))).append(",")
|
||||
.append(bill.get("due_date")).append(",")
|
||||
.append(bill.get("created_at")).append("\n");
|
||||
}
|
||||
|
||||
log.info("Bills exported: {} records", bills.size());
|
||||
return csv.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* CSV 字段转义
|
||||
*/
|
||||
private String escapeCsv(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String str = value.toString();
|
||||
if (str.contains(",") || str.contains("\"") || str.contains("\n")) {
|
||||
return "\"" + str.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 营收统计缓存表(定时汇总,加速 Dashboard 查询)
|
||||
CREATE TABLE IF NOT EXISTS rev_revenue_daily (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stat_date DATE NOT NULL,
|
||||
area VARCHAR(50),
|
||||
customer_type VARCHAR(20),
|
||||
total_bills INT DEFAULT 0,
|
||||
total_amount DECIMAL(14,2) DEFAULT 0,
|
||||
paid_amount DECIMAL(14,2) DEFAULT 0,
|
||||
overdue_amount DECIMAL(14,2) DEFAULT 0,
|
||||
new_customers INT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(stat_date, area, customer_type)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rev_daily_date ON rev_revenue_daily(stat_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_rev_daily_area ON rev_revenue_daily(area);
|
||||
Reference in New Issue
Block a user