feat(wm-revenue): #84 用户管理+业务参数设置(用户档案CRUD+水价阶梯+收费配置)

- CustomerArchiveService: 用户档案CRUD/分页查询/销户/统计(JdbcTemplate风格)
- BillingConfigService: 阶梯水价查询更新/收费配置/污水处理费率/全量配置
- CustomerArchiveController: 6个端点 (/revenue/customer/*)
- BillingConfigController: 6个端点 (/revenue/config/*)
- DDL: rev_billing_config/rev_customer/rev_water_price + 默认数据
This commit is contained in:
2026-06-15 12:30:33 +08:00
parent 69ea8a877d
commit 69b0355d6c
5 changed files with 254 additions and 0 deletions
@@ -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,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,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;