Compare commits
2
Commits
69ea8a877d
...
7447e748a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7447e748a0 | ||
|
|
69b0355d6c |
@@ -0,0 +1,36 @@
|
|||||||
|
package com.water.revenue.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.revenue.service.BillingConfigService;
|
||||||
|
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/config")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BillingConfigController {
|
||||||
|
private final BillingConfigService svc;
|
||||||
|
|
||||||
|
@Operation(summary = "查询阶梯水价") @GetMapping("/water-price/{customerType}")
|
||||||
|
public R<List<Map<String, Object>>> getWaterPrice(@Parameter(description = "residential/commercial/industrial") @PathVariable String customerType) {
|
||||||
|
return R.ok(svc.getWaterPrice(customerType));
|
||||||
|
}
|
||||||
|
@Operation(summary = "更新水价") @PutMapping("/water-price/{customerType}")
|
||||||
|
public R<String> updateWaterPrice(@PathVariable String customerType, @RequestBody List<Map<String, Object>> tiers) {
|
||||||
|
svc.updateWaterPrice(customerType, tiers); return R.ok("OK");
|
||||||
|
}
|
||||||
|
@Operation(summary = "查询收费配置") @GetMapping("/billing")
|
||||||
|
public R<Map<String, Object>> getBillingConfig() { return R.ok(svc.getBillingConfig()); }
|
||||||
|
@Operation(summary = "更新收费配置") @PutMapping("/billing")
|
||||||
|
public R<String> updateBillingConfig(@RequestBody Map<String, String> config) { svc.updateBillingConfig(config); return R.ok("OK"); }
|
||||||
|
@Operation(summary = "污水处理费率") @GetMapping("/sewage-rate")
|
||||||
|
public R<Map<String, Object>> getSewageRate() { return R.ok(svc.getSewageRate()); }
|
||||||
|
@Operation(summary = "所有业务参数") @GetMapping("/all")
|
||||||
|
public R<List<Map<String, Object>>> listAllConfigs() { return R.ok(svc.listAllConfigs()); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.water.revenue.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.revenue.service.CustomerArchiveService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "用户档案管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/revenue/customer")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerArchiveController {
|
||||||
|
private final CustomerArchiveService svc;
|
||||||
|
|
||||||
|
@Operation(summary = "创建用户档案") @PostMapping
|
||||||
|
public R<Map<String, Object>> create(@RequestBody Map<String, String> req) {
|
||||||
|
return R.ok(svc.create(req.get("name"), req.get("type"), req.get("area"), req.get("address"), req.get("phone"), req.get("idCard")));
|
||||||
|
}
|
||||||
|
@Operation(summary = "更新用户") @PutMapping("/{id}")
|
||||||
|
public R<String> update(@PathVariable Long id, @RequestBody Map<String, Object> fields) { svc.update(id, fields); return R.ok("OK"); }
|
||||||
|
@Operation(summary = "用户详情") @GetMapping("/{id}")
|
||||||
|
public R<Map<String, Object>> getById(@PathVariable Long id) { return R.ok(svc.getById(id)); }
|
||||||
|
@Operation(summary = "分页列表") @GetMapping("/list")
|
||||||
|
public R<Map<String, Object>> list(@RequestParam(required = false) String type, @RequestParam(required = false) String area,
|
||||||
|
@RequestParam(required = false) String status, @RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) {
|
||||||
|
return R.ok(svc.list(type, area, status, keyword, page, size));
|
||||||
|
}
|
||||||
|
@Operation(summary = "销户") @PutMapping("/{id}/cancel")
|
||||||
|
public R<String> cancel(@PathVariable Long id) { svc.cancel(id); return R.ok("OK"); }
|
||||||
|
@Operation(summary = "统计") @GetMapping("/stats")
|
||||||
|
public R<Map<String, Object>> stats(@RequestParam(required = false) String area) { return R.ok(svc.stats(area)); }
|
||||||
|
}
|
||||||
@@ -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,59 @@
|
|||||||
|
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 org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j @Service @RequiredArgsConstructor
|
||||||
|
public class BillingConfigService {
|
||||||
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getWaterPrice(String customerType) {
|
||||||
|
return jdbc.queryForList("SELECT * FROM rev_water_price WHERE customer_type = ? ORDER BY tier_level", customerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateWaterPrice(String ct, List<Map<String, Object>> tiers) {
|
||||||
|
jdbc.update("DELETE FROM rev_water_price WHERE customer_type = ?", ct);
|
||||||
|
for (Map<String, Object> t : tiers) {
|
||||||
|
jdbc.update("INSERT INTO rev_water_price (customer_type, tier_level, min_usage, max_usage, price, sewage_fee, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
|
||||||
|
ct, t.get("tierLevel"), t.get("minUsage"), t.get("maxUsage"),
|
||||||
|
new BigDecimal(t.get("price").toString()),
|
||||||
|
t.containsKey("sewageFee") ? new BigDecimal(t.get("sewageFee").toString()) : new BigDecimal("0.85"),
|
||||||
|
LocalDateTime.now(), LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getBillingConfig() {
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
jdbc.queryForList("SELECT * FROM rev_billing_config ORDER BY config_key").forEach(c -> r.put(c.get("config_key").toString(), c.get("config_value")));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateBillingConfig(Map<String, String> updates) {
|
||||||
|
for (var e : updates.entrySet()) {
|
||||||
|
int rows = jdbc.update("UPDATE rev_billing_config SET config_value = ?, updated_at = ? WHERE config_key = ?", e.getValue(), LocalDateTime.now(), e.getKey());
|
||||||
|
if (rows == 0) jdbc.update("INSERT INTO rev_billing_config (config_key, config_value, config_type, description, updated_at) VALUES (?,?,'string','自定义',?)", e.getKey(), e.getValue(), LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getSewageRate() {
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
jdbc.queryForList("SELECT customer_type, sewage_fee FROM rev_water_price GROUP BY customer_type, sewage_fee ORDER BY customer_type")
|
||||||
|
.forEach(row -> r.put(row.get("customer_type").toString(), row.get("sewage_fee")));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> listAllConfigs() {
|
||||||
|
List<Map<String, Object>> all = new ArrayList<>();
|
||||||
|
Map<String, Object> s1 = new LinkedHashMap<>(); s1.put("category", "收费配置"); s1.put("items", jdbc.queryForList("SELECT * FROM rev_billing_config ORDER BY config_key")); all.add(s1);
|
||||||
|
Map<String, Object> s2 = new LinkedHashMap<>(); s2.put("category", "水价配置"); s2.put("items", jdbc.queryForList("SELECT * FROM rev_water_price ORDER BY customer_type, tier_level")); all.add(s2);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
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 org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerArchiveService {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> create(String name, String type, String area, String address,
|
||||||
|
String phone, String idCard) {
|
||||||
|
String customerNo = "CUS-" + System.currentTimeMillis();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO rev_customer (customer_no, name, type, area, address, phone, id_card, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
|
||||||
|
customerNo, name, type, area, address, phone, idCard, now, now);
|
||||||
|
Long id = jdbcTemplate.queryForObject("SELECT id FROM rev_customer WHERE customer_no = ?", Long.class, customerNo);
|
||||||
|
log.info("创建用户档案: customerNo={}", customerNo);
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
r.put("id", id); r.put("customerNo", customerNo); r.put("name", name);
|
||||||
|
r.put("type", type); r.put("area", area); r.put("address", address);
|
||||||
|
r.put("phone", phone); r.put("status", "active"); r.put("createdAt", now);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long id, Map<String, Object> fields) {
|
||||||
|
List<String> set = new ArrayList<>(); List<Object> params = new ArrayList<>();
|
||||||
|
for (String f : new String[]{"name","type","area","address","phone","id_card","status"}) {
|
||||||
|
if (fields.containsKey(f)) { set.add(f + " = ?"); params.add(fields.get(f)); }
|
||||||
|
}
|
||||||
|
if (set.isEmpty()) throw new RuntimeException("没有可更新的字段");
|
||||||
|
set.add("updated_at = ?"); params.add(LocalDateTime.now()); params.add(id);
|
||||||
|
jdbcTemplate.update("UPDATE rev_customer SET " + String.join(", ", set) + " WHERE id = ?", params.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getById(Long id) {
|
||||||
|
Map<String, Object> c = jdbcTemplate.queryForMap("SELECT * FROM rev_customer WHERE id = ?", id);
|
||||||
|
c.put("meters", jdbcTemplate.queryForList("SELECT * FROM rev_water_meter WHERE customer_no = ? ORDER BY install_date DESC", c.get("customer_no")));
|
||||||
|
c.put("recentBills", jdbcTemplate.queryForList("SELECT * FROM rev_bill WHERE customer_no = ? ORDER BY bill_period DESC LIMIT 5", c.get("customer_no")));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> list(String type, String area, String status, String keyword, int page, int size) {
|
||||||
|
StringBuilder w = new StringBuilder("WHERE 1=1"); List<Object> p = new ArrayList<>();
|
||||||
|
if (type != null && !type.isEmpty()) { w.append(" AND type = ?"); p.add(type); }
|
||||||
|
if (area != null && !area.isEmpty()) { w.append(" AND area = ?"); p.add(area); }
|
||||||
|
if (status != null && !status.isEmpty()) { w.append(" AND status = ?"); p.add(status); }
|
||||||
|
if (keyword != null && !keyword.isEmpty()) {
|
||||||
|
w.append(" AND (name LIKE ? OR customer_no LIKE ? OR phone LIKE ?)");
|
||||||
|
String kw = "%" + keyword + "%"; p.add(kw); p.add(kw); p.add(kw);
|
||||||
|
}
|
||||||
|
Long total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM rev_customer " + w, Long.class, p.toArray());
|
||||||
|
List<Object> qp = new ArrayList<>(p); qp.add(size); qp.add((page - 1) * size);
|
||||||
|
List<Map<String, Object>> records = jdbcTemplate.queryForList(
|
||||||
|
"SELECT * FROM rev_customer " + w + " ORDER BY created_at DESC LIMIT ? OFFSET ?", qp.toArray());
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
r.put("records", records); r.put("total", total); r.put("page", page);
|
||||||
|
r.put("size", size); r.put("pages", (int) Math.ceil((double) total / size));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void cancel(Long id) {
|
||||||
|
int rows = jdbcTemplate.update("UPDATE rev_customer SET status = 'cancelled', updated_at = ? WHERE id = ? AND status = 'active'", LocalDateTime.now(), id);
|
||||||
|
if (rows == 0) throw new RuntimeException("用户不存在或已销户: " + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> stats(String area) {
|
||||||
|
StringBuilder w = new StringBuilder("WHERE 1=1"); List<Object> p = new ArrayList<>();
|
||||||
|
if (area != null && !area.isEmpty()) { w.append(" AND area = ?"); p.add(area); }
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
r.put("total", jdbcTemplate.queryForObject("SELECT COUNT(*) FROM rev_customer " + w, Long.class, p.toArray()));
|
||||||
|
r.put("byType", jdbcTemplate.queryForList("SELECT type, COUNT(*) as count FROM rev_customer " + w + " GROUP BY type", p.toArray()));
|
||||||
|
r.put("byArea", jdbcTemplate.queryForList("SELECT area, COUNT(*) as count FROM rev_customer " + w + " GROUP BY area", p.toArray()));
|
||||||
|
r.put("byStatus", jdbcTemplate.queryForList("SELECT status, COUNT(*) as count FROM rev_customer " + w + " GROUP BY status", p.toArray()));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- V84: 用户管理 + 业务参数设置
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rev_billing_config (
|
||||||
|
id BIGSERIAL PRIMARY KEY, config_key VARCHAR(50) UNIQUE NOT NULL, config_value VARCHAR(200) NOT NULL,
|
||||||
|
config_type VARCHAR(20) DEFAULT 'string', description VARCHAR(200), updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
INSERT INTO rev_billing_config (config_key, config_value, config_type, description) VALUES
|
||||||
|
('billing_cycle_days','30','int','收费周期(天)'), ('overdue_penalty_rate','0.001','decimal','滞纳金日费率'),
|
||||||
|
('min_consumption','5','decimal','最低消费水量(m³)'), ('reading_day_of_month','1','int','每月抄表日'),
|
||||||
|
('bill_due_days','15','int','账单到期天数'), ('invoice_auto_issue','true','boolean','自动开具电子发票')
|
||||||
|
ON CONFLICT (config_key) DO NOTHING;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rev_customer (
|
||||||
|
id BIGSERIAL PRIMARY KEY, customer_no VARCHAR(32) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL DEFAULT 'residential', area VARCHAR(50), address VARCHAR(200),
|
||||||
|
phone VARCHAR(20), id_card VARCHAR(20), status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rev_customer_type ON rev_customer(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rev_customer_area ON rev_customer(area);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rev_customer_status ON rev_customer(status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rev_water_price (
|
||||||
|
id BIGSERIAL PRIMARY KEY, customer_type VARCHAR(20) NOT NULL, tier_level INTEGER NOT NULL,
|
||||||
|
min_usage NUMERIC(10,2), max_usage NUMERIC(10,2), price NUMERIC(10,4) NOT NULL,
|
||||||
|
sewage_fee NUMERIC(10,4) DEFAULT 0.85, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_water_price_type ON rev_water_price(customer_type);
|
||||||
|
INSERT INTO rev_water_price (customer_type, tier_level, min_usage, max_usage, price, sewage_fee) VALUES
|
||||||
|
('residential',1,0,15,3.50,0.85), ('residential',2,15,30,5.20,0.85), ('residential',3,30,NULL,8.00,0.85),
|
||||||
|
('commercial',1,0,50,5.80,1.20), ('commercial',2,50,200,7.50,1.20), ('commercial',3,200,NULL,10.00,1.20),
|
||||||
|
('industrial',1,0,100,4.50,1.50), ('industrial',2,100,500,6.80,1.50), ('industrial',3,500,NULL,9.50,1.50)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
Reference in New Issue
Block a user