From 69b0355d6c55a91595766c3c4f5eba50bb2deb17 Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Mon, 15 Jun 2026 12:30:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(wm-revenue):=20#84=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86+=E4=B8=9A=E5=8A=A1=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=EF=BC=88=E7=94=A8=E6=88=B7=E6=A1=A3=E6=A1=88?= =?UTF-8?q?CRUD+=E6=B0=B4=E4=BB=B7=E9=98=B6=E6=A2=AF+=E6=94=B6=E8=B4=B9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomerArchiveService: 用户档案CRUD/分页查询/销户/统计(JdbcTemplate风格) - BillingConfigService: 阶梯水价查询更新/收费配置/污水处理费率/全量配置 - CustomerArchiveController: 6个端点 (/revenue/customer/*) - BillingConfigController: 6个端点 (/revenue/config/*) - DDL: rev_billing_config/rev_customer/rev_water_price + 默认数据 --- .../controller/BillingConfigController.java | 36 ++++++++ .../controller/CustomerArchiveController.java | 36 ++++++++ .../revenue/service/BillingConfigService.java | 59 ++++++++++++ .../service/CustomerArchiveService.java | 89 +++++++++++++++++++ .../sql/V84__customer_billing_config.sql | 34 +++++++ 5 files changed, 254 insertions(+) create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/BillingConfigController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/CustomerArchiveController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/BillingConfigService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/CustomerArchiveService.java create mode 100644 wm-revenue/src/main/resources/sql/V84__customer_billing_config.sql diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/BillingConfigController.java b/wm-revenue/src/main/java/com/water/revenue/controller/BillingConfigController.java new file mode 100644 index 00000000..17018a30 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/BillingConfigController.java @@ -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>> getWaterPrice(@Parameter(description = "residential/commercial/industrial") @PathVariable String customerType) { + return R.ok(svc.getWaterPrice(customerType)); + } + @Operation(summary = "更新水价") @PutMapping("/water-price/{customerType}") + public R updateWaterPrice(@PathVariable String customerType, @RequestBody List> tiers) { + svc.updateWaterPrice(customerType, tiers); return R.ok("OK"); + } + @Operation(summary = "查询收费配置") @GetMapping("/billing") + public R> getBillingConfig() { return R.ok(svc.getBillingConfig()); } + @Operation(summary = "更新收费配置") @PutMapping("/billing") + public R updateBillingConfig(@RequestBody Map config) { svc.updateBillingConfig(config); return R.ok("OK"); } + @Operation(summary = "污水处理费率") @GetMapping("/sewage-rate") + public R> getSewageRate() { return R.ok(svc.getSewageRate()); } + @Operation(summary = "所有业务参数") @GetMapping("/all") + public R>> listAllConfigs() { return R.ok(svc.listAllConfigs()); } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/CustomerArchiveController.java b/wm-revenue/src/main/java/com/water/revenue/controller/CustomerArchiveController.java new file mode 100644 index 00000000..331c46c5 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/CustomerArchiveController.java @@ -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> create(@RequestBody Map 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 update(@PathVariable Long id, @RequestBody Map fields) { svc.update(id, fields); return R.ok("OK"); } + @Operation(summary = "用户详情") @GetMapping("/{id}") + public R> getById(@PathVariable Long id) { return R.ok(svc.getById(id)); } + @Operation(summary = "分页列表") @GetMapping("/list") + public R> 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 cancel(@PathVariable Long id) { svc.cancel(id); return R.ok("OK"); } + @Operation(summary = "统计") @GetMapping("/stats") + public R> stats(@RequestParam(required = false) String area) { return R.ok(svc.stats(area)); } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/BillingConfigService.java b/wm-revenue/src/main/java/com/water/revenue/service/BillingConfigService.java new file mode 100644 index 00000000..d884576e --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/BillingConfigService.java @@ -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> 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> tiers) { + jdbc.update("DELETE FROM rev_water_price WHERE customer_type = ?", ct); + for (Map 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 getBillingConfig() { + Map 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 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 getSewageRate() { + Map 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> listAllConfigs() { + List> all = new ArrayList<>(); + Map s1 = new LinkedHashMap<>(); s1.put("category", "收费配置"); s1.put("items", jdbc.queryForList("SELECT * FROM rev_billing_config ORDER BY config_key")); all.add(s1); + Map 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; + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/CustomerArchiveService.java b/wm-revenue/src/main/java/com/water/revenue/service/CustomerArchiveService.java new file mode 100644 index 00000000..81f1b6a4 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/CustomerArchiveService.java @@ -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 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 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 fields) { + List set = new ArrayList<>(); List 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 getById(Long id) { + Map 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 list(String type, String area, String status, String keyword, int page, int size) { + StringBuilder w = new StringBuilder("WHERE 1=1"); List 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 qp = new ArrayList<>(p); qp.add(size); qp.add((page - 1) * size); + List> records = jdbcTemplate.queryForList( + "SELECT * FROM rev_customer " + w + " ORDER BY created_at DESC LIMIT ? OFFSET ?", qp.toArray()); + Map 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 stats(String area) { + StringBuilder w = new StringBuilder("WHERE 1=1"); List p = new ArrayList<>(); + if (area != null && !area.isEmpty()) { w.append(" AND area = ?"); p.add(area); } + Map 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; + } +} diff --git a/wm-revenue/src/main/resources/sql/V84__customer_billing_config.sql b/wm-revenue/src/main/resources/sql/V84__customer_billing_config.sql new file mode 100644 index 00000000..4ceb325d --- /dev/null +++ b/wm-revenue/src/main/resources/sql/V84__customer_billing_config.sql @@ -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;