[feat] 水表全生命周期管理功能

- 实现水表入库/安装/换表/报废的完整生命周期管理
- 添加详细的操作日志记录,支持操作人、时间戳、照片等
- 实现库存统计功能,按状态、口径、类型、制造商分类统计
- 添加 REST API 接口,支持水表操作和查询
- 创建 DTO 类用于数据传输
- 添加数据库升级脚本,支持新增字段和视图创建
- 插入测试数据验证功能完整性

功能包括:
1. 水表入库操作及日志记录
2. 水表安装操作,继承客户信息
3. 故障换表操作,自动完成新旧表状态转换
4. 水表报废操作,记录报废原因
5. 全生命周期日志查询
6. 库存统计分析
7. 水表详情查询
8. 最近操作记录查询

关闭 Issue #53: [表务] 水表全生命周期管理(入库/安装/换表/报废)
This commit is contained in:
2026-06-14 20:47:27 +08:00
parent 6860aab376
commit c9abf94e57
8 changed files with 439 additions and 17 deletions
+3 -2
View File
@@ -6,8 +6,9 @@
<parent>
<groupId>com.water</groupId>
<artifactId>water-management-system</artifactId>
<version>1.0.0</version>
<artifactId>wm-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>wm-revenue</artifactId>
@@ -0,0 +1,153 @@
package com.water.revenue.controller;
import com.water.revenue.service.MeterService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/meter/lifecycle")
public class MeterLifecycleController {
private final MeterService meterService;
/**
* 水表入库
*/
@PostMapping("/stock-in")
public ResponseEntity<?> stockIn(
@RequestParam String meterNo,
@RequestParam String caliber,
@RequestParam String meterType,
@RequestParam String manufacturer,
@RequestParam int quantity,
@RequestParam String operatorId,
@RequestParam String operatorName) {
try {
meterService.stockIn(meterNo, caliber, meterType, manufacturer, quantity, operatorId, operatorName);
return ResponseEntity.ok().body(Map.of("success", true, "message", "水表入库成功"));
} catch (Exception e) {
log.error("水表入库失败", e);
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "水表入库失败: " + e.getMessage()));
}
}
/**
* 水表安装
*/
@PostMapping("/install")
public ResponseEntity<?> install(
@RequestParam Long meterId,
@RequestParam Long customerId,
@RequestParam String address,
@RequestParam BigDecimal initialReading,
@RequestParam String operatorId,
@RequestParam String operatorName) {
try {
meterService.install(meterId, customerId, address, initialReading, operatorId, operatorName);
return ResponseEntity.ok().body(Map.of("success", true, "message", "水表安装成功"));
} catch (Exception e) {
log.error("水表安装失败", e);
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "水表安装失败: " + e.getMessage()));
}
}
/**
* 故障换表
*/
@PostMapping("/replace")
public ResponseEntity<?> replace(
@RequestParam Long oldMeterId,
@RequestParam Long newMeterId,
@RequestParam BigDecimal oldReading,
@RequestParam String remark,
@RequestParam String operatorId,
@RequestParam String operatorName) {
try {
meterService.replace(oldMeterId, newMeterId, oldReading, remark, operatorId, operatorName);
return ResponseEntity.ok().body(Map.of("success", true, "message", "换表成功"));
} catch (Exception e) {
log.error("换表失败", e);
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "换表失败: " + e.getMessage()));
}
}
/**
* 水表报废
*/
@PostMapping("/scrap")
public ResponseEntity<?> scrap(
@RequestParam Long meterId,
@RequestParam String reason,
@RequestParam String operatorId,
@RequestParam String operatorName) {
try {
meterService.scrap(meterId, reason, operatorId, operatorName);
return ResponseEntity.ok().body(Map.of("success", true, "message", "水表报废成功"));
} catch (Exception e) {
log.error("水表报废失败", e);
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "水表报废失败: " + e.getMessage()));
}
}
/**
* 查询水表生命周期记录
*/
@GetMapping("/lifecycle/{meterId}")
public ResponseEntity<List<Map<String, Object>>> getLifecycle(@PathVariable Long meterId) {
List<Map<String, Object>> lifecycle = meterService.getLifecycle(meterId);
return ResponseEntity.ok(lifecycle);
}
/**
* 获取库存统计
*/
@GetMapping("/inventory/stats")
public ResponseEntity<Map<String, Object>> getInventoryStats() {
Map<String, Object> stats = meterService.getInventoryStats();
return ResponseEntity.ok(stats);
}
/**
* 查询库存水表
*/
@GetMapping("/inventory")
public ResponseEntity<List<Map<String, Object>>> getWarehouseMeters(
@RequestParam(required = false) String caliber,
@RequestParam(required = false) String meterType,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
List<Map<String, Object>> meters = meterService.getWarehouseMeters(caliber, meterType, page, size);
return ResponseEntity.ok(meters);
}
/**
* 获取最近操作记录
*/
@GetMapping("/recent-operations")
public ResponseEntity<List<Map<String, Object>>> getRecentOperations(@RequestParam(defaultValue = "10") int limit) {
List<Map<String, Object>> operations = meterService.getRecentOperations(limit);
return ResponseEntity.ok(operations);
}
/**
* 获取水表详情
*/
@GetMapping("/{meterId}")
public ResponseEntity<Map<String, Object>> getMeterDetails(@PathVariable Long meterId) {
Map<String, Object> details = meterService.getMeterDetails(meterId);
return ResponseEntity.ok(details);
}
}
@@ -0,0 +1,39 @@
package com.water.revenue.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MeterInventoryStatsDTO {
// 总体统计
private Long totalWarehouse;
private Long totalActive;
private Long totalDismantled;
private Long totalScrapped;
// 按状态统计
private Map<String, Long> statusStats;
// 按口径统计
private Map<String, Long> caliberStats;
// 按类型统计
private Map<String, Long> typeStats;
// 按制造商统计
private Map<String, Long> manufacturerStats;
// 最近操作
private List<MeterOperationLogDTO> recentOperations;
}
@@ -0,0 +1,47 @@
package com.water.revenue.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MeterOperationLogDTO {
private Long id;
private Long meterId;
private String meterNo;
private String operationType;
private String operationName;
private BigDecimal oldReading;
private BigDecimal newReading;
private String equipmentNo;
private Long operatorId;
private String operatorName;
private List<String> photos;
private String remark;
private LocalDateTime createdAt;
// 操作类型中文名称映射
public String getOperationName() {
switch (operationType) {
case "install": return "安装";
case "dismantle": return "拆除";
case "replace": return "更换";
case "scrap": return "报废";
case "stock_in": return "入库";
case "repair": return "维修";
case "calibrate": return "校准";
case "refurbish": return "翻新";
default: return operationType;
}
}
}
@@ -17,35 +17,42 @@ public class MeterService {
private final JdbcTemplate jdbcTemplate;
/** 水表入库 */
public void stockIn(String meterNo, String caliber, String meterType, String manufacturer, int quantity) {
@Transactional
public void stockIn(String meterNo, String caliber, String meterType, String manufacturer, int quantity, String operatorId, String operatorName) {
for (int i = 0; i < quantity; i++) {
String no = meterNo + "-" + (i + 1);
jdbcTemplate.update(
"INSERT INTO rev_meter (meter_no, caliber, meter_type, manufacturer, status) VALUES (?,?,?,?,?)",
no, caliber, meterType, manufacturer, "warehouse");
// 记录入库日志
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, operator_id, operator_name, remark) VALUES (?,?,?,?,?)",
jdbcTemplate.queryForObject("SELECT id FROM rev_meter WHERE meter_no = ?", Long.class, no),
"stock_in", operatorId, operatorName, "水表入库:" + meterNo + "-" + (i + 1));
}
log.info("Meter stock in: {} x{}", meterNo, quantity);
}
/** 水表出库安装 */
@Transactional
public void install(Long meterId, Long customerId, String address, BigDecimal initialReading) {
public void install(Long meterId, Long customerId, String address, BigDecimal initialReading, String operatorId, String operatorName) {
jdbcTemplate.update(
"UPDATE rev_meter SET customer_id = ?, install_address = ?, initial_reading = ?, current_reading = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
customerId, address, initialReading, initialReading, meterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, new_reading, remark) VALUES (?,?,?,?,?)",
meterId, "install", BigDecimal.ZERO, initialReading, "新表安装");
"INSERT INTO rev_meter_log (meter_id, operation_type, operator_id, operator_name, old_reading, new_reading, remark) VALUES (?,?,?,?,?,?,?)",
meterId, "install", operatorId, operatorName, BigDecimal.ZERO, initialReading, "新表安装");
}
/** 故障换表 */
@Transactional
public void replace(Long oldMeterId, Long newMeterId, BigDecimal oldReading, String remark) {
public void replace(Long oldMeterId, Long newMeterId, BigDecimal oldReading, String remark, String operatorId, String operatorName) {
// 旧表拆除
jdbcTemplate.update("UPDATE rev_meter SET status = 'dismantled', current_reading = ? WHERE id = ?", oldReading, oldMeterId);
jdbcTemplate.update("UPDATE rev_meter SET status = 'dismantled', current_reading = ?, dismantle_date = CURRENT_DATE WHERE id = ?", oldReading, oldMeterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, remark) VALUES (?,?,?,?)",
oldMeterId, "dismantle", oldReading, remark);
"INSERT INTO rev_meter_log (meter_id, operation_type, operator_id, operator_name, old_reading, remark) VALUES (?,?,?,?,?,?)",
oldMeterId, "dismantle", operatorId, operatorName, oldReading, remark);
// 新表安装(继承客户信息)
Map<String, Object> oldMeter = jdbcTemplate.queryForMap("SELECT customer_id, install_address FROM rev_meter WHERE id = ?", oldMeterId);
@@ -53,16 +60,20 @@ public class MeterService {
"UPDATE rev_meter SET customer_id = ?, install_address = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
oldMeter.get("customer_id"), oldMeter.get("install_address"), newMeterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, new_meter_no, remark) VALUES (?,?,?,?)",
newMeterId, "change", String.valueOf(newMeterId), "替换旧表 #" + oldMeterId);
"INSERT INTO rev_meter_log (meter_id, operation_type, operator_id, operator_name, new_meter_no, remark) VALUES (?,?,?,?,?,?)",
newMeterId, "change", operatorId, operatorName, String.valueOf(newMeterId), "替换旧表 #" + oldMeterId);
log.info("Meter replaced: old={}, new={}, operator={}", oldMeterId, newMeterId, operatorId);
}
/** 水表报废 */
public void scrap(Long meterId, String reason) {
jdbcTemplate.update("UPDATE rev_meter SET status = 'scrapped' WHERE id = ?", meterId);
@Transactional
public void scrap(Long meterId, String reason, String operatorId, String operatorName) {
jdbcTemplate.update("UPDATE rev_meter SET status = 'scrapped', scrapped_date = CURRENT_DATE WHERE id = ?", meterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, remark) VALUES (?,?,?)",
meterId, "scrap", reason);
"INSERT INTO rev_meter_log (meter_id, operation_type, operator_id, operator_name, remark) VALUES (?,?,?,?,?)",
meterId, "scrap", operatorId, operatorName, reason);
log.info("Meter scrapped: id={}, reason={}", meterId, reason);
}
/** 查询水表生命周期记录 */
@@ -71,4 +82,92 @@ public class MeterService {
"SELECT * FROM rev_meter_log WHERE meter_id = ? ORDER BY created_at",
meterId);
}
/** 库存统计 */
public Map<String, Object> getInventoryStats() {
Map<String, Object> stats = new HashMap<>();
// 按状态统计
List<Map<String, Object>> statusStats = jdbcTemplate.queryForList(
"SELECT status, COUNT(*) as count FROM rev_meter GROUP BY status");
stats.put("statusStats", statusStats);
// 按口径统计
List<Map<String, Object>> caliberStats = jdbcTemplate.queryForList(
"SELECT caliber, COUNT(*) as count FROM rev_meter WHERE status = 'warehouse' GROUP BY caliber");
stats.put("caliberStats", caliberStats);
// 按类型统计
List<Map<String, Object>> typeStats = jdbcTemplate.queryForList(
"SELECT meter_type, COUNT(*) as count FROM rev_meter WHERE status = 'warehouse' GROUP BY meter_type");
stats.put("typeStats", typeStats);
// 按制造商统计
List<Map<String, Object>> manufacturerStats = jdbcTemplate.queryForList(
"SELECT manufacturer, COUNT(*) as count FROM rev_meter WHERE status = 'warehouse' GROUP BY manufacturer");
stats.put("manufacturerStats", manufacturerStats);
// 总库存
Long totalWarehouse = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_meter WHERE status = 'warehouse'", Long.class);
stats.put("totalWarehouse", totalWarehouse);
// 活跃水表数
Long totalActive = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_meter WHERE status = 'active'", Long.class);
stats.put("totalActive", totalActive);
// 其他状态计数
Long totalDismantled = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_meter WHERE status = 'dismantled'", Long.class);
stats.put("totalDismantled", totalDismantled);
Long totalScrapped = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_meter WHERE status = 'scrapped'", Long.class);
stats.put("totalScrapped", totalScrapped);
return stats;
}
/** 库存查询 */
public List<Map<String, Object>> getWarehouseMeters(String caliber, String meterType, int page, int size) {
String sql = "SELECT * FROM rev_meter WHERE status = 'warehouse'";
List<Object> params = new ArrayList<>();
if (caliber != null && !caliber.isEmpty()) {
sql += " AND caliber = ?";
params.add(caliber);
}
if (meterType != null && !meterType.isEmpty()) {
sql += " AND meter_type = ?";
params.add(meterType);
}
sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
params.add(size);
params.add(page * size);
return jdbcTemplate.queryForList(sql, params.toArray());
}
/** 获取最近操作记录 */
public List<Map<String, Object>> getRecentOperations(int limit) {
return jdbcTemplate.queryForList(
"SELECT rml.*, rm.meter_no " +
"FROM rev_meter_log rml " +
"JOIN rev_meter rm ON rml.meter_id = rm.id " +
"ORDER BY rml.created_at DESC LIMIT ?", limit);
}
/** 获取水表详情 */
public Map<String, Object> getMeterDetails(Long meterId) {
Map<String, Object> meter = jdbcTemplate.queryForMap(
"SELECT * FROM rev_meter WHERE id = ?", meterId);
// 获取生命周期记录
List<Map<String, Object>> lifecycle = getLifecycle(meterId);
meter.put("lifecycle", lifecycle);
return meter;
}
}
@@ -54,6 +54,9 @@ CREATE TABLE IF NOT EXISTS rev_meter (
install_date DATE,
install_address VARCHAR(300),
status VARCHAR(20) DEFAULT 'active', -- active/dismantled/scrapped/repaired/warehouse
install_date DATE,
scrapped_date DATE,
dismantle_date DATE,
remark VARCHAR(500),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
@@ -64,12 +67,14 @@ COMMENT ON TABLE rev_meter IS '水表档案表';
CREATE TABLE IF NOT EXISTS rev_meter_log (
id BIGSERIAL PRIMARY KEY,
meter_id BIGINT REFERENCES rev_meter(id),
operation_type VARCHAR(30) NOT NULL, -- install/dismantle/repair/change/scrap/calibrate/refurbish
operation_type VARCHAR(30) NOT NULL, -- install/dismantle/repair/change/scrap/calibrate/refurbish/stock_in
old_reading DECIMAL(10,2),
new_reading DECIMAL(10,2),
new_meter_no VARCHAR(50),
operator_id BIGINT,
operator_name VARCHAR(50),
photos JSONB, -- 现场照片URL数组
equipment_no VARCHAR(50), -- 设备编号(如新换的表号)
photos JSONB, -- 现场照片URL数组
remark VARCHAR(500),
created_at TIMESTAMP DEFAULT NOW()
@@ -0,0 +1,38 @@
-- 水表全生命周期管理升级脚本
-- 添加缺失的日期字段到水表表
-- 为水表表添加安装、拆除、报废日期字段
ALTER TABLE rev_meter ADD COLUMN IF NOT EXISTS install_date DATE;
ALTER TABLE rev_meter ADD COLUMN IF NOT EXISTS scrapped_date DATE;
ALTER TABLE rev_meter ADD COLUMN IF NOT EXISTS dismantle_date DATE;
-- 添加照片和设备编号字段到日志表
ALTER TABLE rev_meter_log ADD COLUMN IF NOT EXISTS photos JSONB;
ALTER TABLE rev_meter_log ADD COLUMN IF NOT EXISTS equipment_no VARCHAR(50);
-- 创建水表统计视图
CREATE OR REPLACE VIEW v_meter_statistics AS
SELECT
status,
COUNT(*) as count,
COUNT(CASE WHEN install_date IS NOT NULL THEN 1 END) as installed_count,
COUNT(CASE WHEN scrapped_date IS NOT NULL THEN 1 END) as scrapped_count,
COUNT(CASE WHEN dismantle_date IS NOT NULL THEN 1 END) as dismantled_count
FROM rev_meter
GROUP BY status;
-- 创建库存统计视图
CREATE OR REPLACE VIEW v_meter_inventory AS
SELECT
status,
caliber,
meter_type,
manufacturer,
COUNT(*) as count
FROM rev_meter
GROUP BY status, caliber, meter_type, manufacturer
ORDER BY status, caliber, meter_type, manufacturer;
-- 为水表操作类型添加注释
COMMENT ON COLUMN rev_meter.status IS 'active/dismantled/scrapped/repaired/warehouse';
COMMENT ON COLUMN rev_meter_log.operation_type IS 'install/dismantle/repair/change/scrap/calibrate/refurbish/stock_in';
@@ -0,0 +1,40 @@
-- 水表全生命周期管理测试数据
-- 插入测试用水表数据
-- 插入测试用水表(仓库状态)
INSERT INTO rev_meter (meter_no, caliber, meter_type, manufacturer, status, created_at) VALUES
('WH001', 'DN15', 'mechanical', '威胜', 'warehouse', CURRENT_TIMESTAMP),
('WH002', 'DN20', 'mechanical', '威胜', 'warehouse', CURRENT_TIMESTAMP),
('WH003', 'DN15', 'electromagnetic', '三川', 'warehouse', CURRENT_TIMESTAMP),
('WH004', 'DN25', 'ultrasonic', '宁水', 'warehouse', CURRENT_TIMESTAMP),
('WH005', 'DN40', 'mechanical', '威胜', 'warehouse', CURRENT_TIMESTAMP),
('WH006', 'DN50', 'electromagnetic', '三川', 'warehouse', CURRENT_TIMESTAMP),
('WH007', 'DN80', 'ultrasonic', '宁水', 'warehouse', CURRENT_TIMESTAMP),
('WH008', 'DN100', 'ultrasonic', '宁水', 'warehouse', CURRENT_TIMESTAMP),
('WH009', 'DN15', 'mechanical', '威胜', 'warehouse', CURRENT_TIMESTAMP),
('WH010', 'DN20', 'electromagnetic', '三川', 'warehouse', CURRENT_TIMESTAMP)
ON CONFLICT (meter_no) DO NOTHING;
-- 插入一些已安装的水表用于测试
INSERT INTO rev_meter (meter_no, customer_id, caliber, meter_type, manufacturer, status, install_date, initial_reading, current_reading, created_at) VALUES
('M001', 1, 'DN15', 'mechanical', '威胜', 'active', '2025-01-01', 0.00, 1200.50, CURRENT_TIMESTAMP),
('M002', 2, 'DN20', 'electromagnetic', '三川', 'active', '2025-02-01', 0.00, 2100.75, CURRENT_TIMESTAMP),
('M003', 3, 'DN15', 'ultrasonic', '宁水', 'dismantled', '2025-03-01', 0.00, 3200.00, CURRENT_TIMESTAMP),
('M004', 4, 'DN25', 'mechanical', '威胜', 'scrapped', '2025-04-01', 0.00, 1500.25, CURRENT_TIMESTAMP)
ON CONFLICT (meter_no) DO NOTHING;
-- 插入一些操作记录测试数据
INSERT INTO rev_meter_log (meter_id, operation_type, operator_id, operator_name, old_reading, new_reading, remark, created_at) VALUES
((SELECT id FROM rev_meter WHERE meter_no = 'M001'), 'install', '1001', '张三', 0.00, 100.00, '新表安装', CURRENT_TIMESTAMP - INTERVAL '1 year'),
((SELECT id FROM rev_meter WHERE meter_no = 'M001'), 'calibrate', '1002', '李四', 1100.00, 1100.50, '年度校准', CURRENT_TIMESTAMP - INTERVAL '6 months'),
((SELECT id FROM rev_meter WHERE meter_no = 'M001'), 'repair', '1001', '张三', 1150.00, 1150.00, '更换电池', CURRENT_TIMESTAMP - INTERVAL '3 months'),
((SELECT id FROM rev_meter WHERE meter_no = 'M002'), 'install', '1001', '张三', 0.00, 200.00, '新表安装', CURRENT_TIMESTAMP - INTERVAL '1 year'),
((SELECT id FROM rev_meter WHERE meter_no = 'M002'), 'replace', '1003', '王五', 2000.00, 2100.75, '故障换表', CURRENT_TIMESTAMP - INTERVAL '1 month'),
((SELECT id FROM rev_meter WHERE meter_no = 'M003'), 'install', '1001', '张三', 0.00, 300.00, '新表安装', CURRENT_TIMESTAMP - INTERVAL '2 years'),
((SELECT id FROM rev_meter WHERE meter_no = 'M003'), 'dismantle', '1004', '赵六', 3000.00, 3200.00, '客户要求拆除', CURRENT_TIMESTAMP - INTERVAL '1 month'),
((SELECT id FROM rev_meter WHERE meter_no = 'M004'), 'install', '1001', '张三', 0.00, 400.00, '新表安装', CURRENT_TIMESTAMP - INTERVAL '2 years'),
((SELECT id FROM rev_meter WHERE meter_no = 'M004'), 'scrap', '1004', '赵六', 1500.25, NULL, '到期报废', CURRENT_TIMESTAMP - INTERVAL '1 month')
ON CONFLICT (id) DO NOTHING;