feat(wm-revenue): #53 水表全生命周期管理

- Entity: WaterMeter, MeterInstallRecord, MeterReplaceRecord, MeterLifecycleLog
- Enum: MeterStatus (IN_STOCK/INSTALLED/DISMANTLED/SCRAPPED/REPAIRING)
- Mapper: WaterMeterMapper, MeterInstallRecordMapper, MeterReplaceRecordMapper, MeterLifecycleLogMapper
- Service: MeterLifecycleService (入库/安装/查询), MeterReplaceService (换表/报废/审批), MeterStatsService (统计)
- Controller: MeterController (/api/revenue/meter/*), MeterReplaceController (/api/revenue/meter-replace/*), MeterStatsController (/api/revenue/meter-stats/*)
- DDL: V_meter_lifecycle.sql (4张表 + 索引)
- Test: MeterLifecycleTest (12个测试用例)
This commit is contained in:
2026-06-15 08:37:38 +08:00
parent db18fdbd98
commit ba535c47e2
17 changed files with 1643 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
-- ============================================================
-- 水表全生命周期管理 DDL
-- 包含: rev_water_meter, rev_meter_install_record,
-- rev_meter_replace_record, rev_meter_lifecycle_log
-- ============================================================
-- 1. 水表主表
CREATE TABLE IF NOT EXISTS rev_water_meter (
id BIGSERIAL PRIMARY KEY,
meter_no VARCHAR(64) NOT NULL UNIQUE,
model VARCHAR(64),
diameter INTEGER,
manufacturer VARCHAR(128),
production_date DATE,
install_date DATE,
install_address VARCHAR(512),
customer_no VARCHAR(64),
status VARCHAR(32) NOT NULL DEFAULT 'IN_STOCK',
in_stock_time TIMESTAMP,
out_stock_time TIMESTAMP,
initial_reading NUMERIC(14,4) DEFAULT 0,
current_reading NUMERIC(14,4) DEFAULT 0,
dismantle_date DATE,
scrapped_date DATE,
remark VARCHAR(512),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE rev_water_meter IS '水表主表';
COMMENT ON COLUMN rev_water_meter.meter_no IS '水表编号';
COMMENT ON COLUMN rev_water_meter.model IS '水表型号';
COMMENT ON COLUMN rev_water_meter.diameter IS '口径(mm)';
COMMENT ON COLUMN rev_water_meter.manufacturer IS '制造商';
COMMENT ON COLUMN rev_water_meter.production_date IS '生产日期';
COMMENT ON COLUMN rev_water_meter.install_date IS '安装日期';
COMMENT ON COLUMN rev_water_meter.install_address IS '安装地址';
COMMENT ON COLUMN rev_water_meter.customer_no IS '客户编号';
COMMENT ON COLUMN rev_water_meter.status IS '状态: IN_STOCK/INSTALLED/DISMANTLED/SCRAPPED/REPAIRING';
COMMENT ON COLUMN rev_water_meter.in_stock_time IS '入库时间';
COMMENT ON COLUMN rev_water_meter.out_stock_time IS '出库时间';
COMMENT ON COLUMN rev_water_meter.initial_reading IS '初始读数';
COMMENT ON COLUMN rev_water_meter.current_reading IS '当前读数';
COMMENT ON COLUMN rev_water_meter.dismantle_date IS '拆除日期';
COMMENT ON COLUMN rev_water_meter.scrapped_date IS '报废日期';
-- 2. 水表安装记录
CREATE TABLE IF NOT EXISTS rev_meter_install_record (
id BIGSERIAL PRIMARY KEY,
meter_no VARCHAR(64) NOT NULL,
customer_no VARCHAR(64),
installer VARCHAR(128),
install_date DATE,
install_address VARCHAR(512),
old_meter_no VARCHAR(64),
initial_reading NUMERIC(14,4) DEFAULT 0,
remark VARCHAR(512),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE rev_meter_install_record IS '水表安装记录';
-- 3. 水表换表记录
CREATE TABLE IF NOT EXISTS rev_meter_replace_record (
id BIGSERIAL PRIMARY KEY,
old_meter_no VARCHAR(64) NOT NULL,
new_meter_no VARCHAR(64) NOT NULL,
customer_no VARCHAR(64),
replace_type VARCHAR(32) NOT NULL,
reason VARCHAR(512),
replacer VARCHAR(128),
replace_date DATE,
old_reading NUMERIC(14,4),
new_reading NUMERIC(14,4),
approval_status VARCHAR(32) DEFAULT 'APPROVED',
approver VARCHAR(128),
approval_time TIMESTAMP,
remark VARCHAR(512),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE rev_meter_replace_record IS '水表换表记录';
COMMENT ON COLUMN rev_meter_replace_record.replace_type IS '换表类型: FAULT-故障, EXPIRED-到期';
COMMENT ON COLUMN rev_meter_replace_record.approval_status IS '审批状态: PENDING/APPROVED/REJECTED';
-- 4. 水表生命周期日志
CREATE TABLE IF NOT EXISTS rev_meter_lifecycle_log (
id BIGSERIAL PRIMARY KEY,
meter_no VARCHAR(64) NOT NULL,
action_type VARCHAR(32) NOT NULL,
action_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
operator VARCHAR(128),
detail VARCHAR(1024),
old_meter_no VARCHAR(64),
new_meter_no VARCHAR(64),
customer_no VARCHAR(64),
remark VARCHAR(512),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE rev_meter_lifecycle_log IS '水表生命周期日志';
COMMENT ON COLUMN rev_meter_lifecycle_log.action_type IS '操作类型: STOCK_IN/INSTALL/DISMANTLE/REPLACE/SCRAP/REPAIR';
-- ============================================================
-- 索引
-- ============================================================
-- rev_water_meter 索引
CREATE INDEX IF NOT EXISTS idx_water_meter_status ON rev_water_meter (status);
CREATE INDEX IF NOT EXISTS idx_water_meter_customer ON rev_water_meter (customer_no);
CREATE INDEX IF NOT EXISTS idx_water_meter_diameter ON rev_water_meter (diameter);
CREATE INDEX IF NOT EXISTS idx_water_meter_manufacturer ON rev_water_meter (manufacturer);
CREATE INDEX IF NOT EXISTS idx_water_meter_created ON rev_water_meter (created_at);
-- rev_meter_install_record 索引
CREATE INDEX IF NOT EXISTS idx_install_record_meter ON rev_meter_install_record (meter_no);
CREATE INDEX IF NOT EXISTS idx_install_record_customer ON rev_meter_install_record (customer_no);
CREATE INDEX IF NOT EXISTS idx_install_record_date ON rev_meter_install_record (install_date);
-- rev_meter_replace_record 索引
CREATE INDEX IF NOT EXISTS idx_replace_record_old ON rev_meter_replace_record (old_meter_no);
CREATE INDEX IF NOT EXISTS idx_replace_record_new ON rev_meter_replace_record (new_meter_no);
CREATE INDEX IF NOT EXISTS idx_replace_record_customer ON rev_meter_replace_record (customer_no);
CREATE INDEX IF NOT EXISTS idx_replace_record_approval ON rev_meter_replace_record (approval_status);
CREATE INDEX IF NOT EXISTS idx_replace_record_date ON rev_meter_replace_record (replace_date);
-- rev_meter_lifecycle_log 索引
CREATE INDEX IF NOT EXISTS idx_lifecycle_log_meter ON rev_meter_lifecycle_log (meter_no);
CREATE INDEX IF NOT EXISTS idx_lifecycle_log_action ON rev_meter_lifecycle_log (action_type);
CREATE INDEX IF NOT EXISTS idx_lifecycle_log_time ON rev_meter_lifecycle_log (action_time);
CREATE INDEX IF NOT EXISTS idx_lifecycle_log_customer ON rev_meter_lifecycle_log (customer_no);
@@ -0,0 +1,123 @@
package com.water.revenue.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.result.R;
import com.water.revenue.entity.MeterInstallRecord;
import com.water.revenue.entity.MeterLifecycleLog;
import com.water.revenue.entity.WaterMeter;
import com.water.revenue.enums.MeterStatus;
import com.water.revenue.service.MeterLifecycleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
/**
* 水表管理 — 入库 / 安装 / 查询
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/revenue/meter")
public class MeterController {
private final MeterLifecycleService lifecycleService;
/* ==================== 入库 ==================== */
/**
* 水表入库登记
*/
@PostMapping("/stock-in")
public R<WaterMeter> stockIn(@RequestBody WaterMeter meter) {
try {
return R.ok(lifecycleService.stockIn(meter));
} catch (Exception e) {
log.error("水表入库失败", e);
return R.fail(e.getMessage());
}
}
/**
* 批量入库
*/
@PostMapping("/stock-in/batch")
public R<List<WaterMeter>> batchStockIn(@RequestBody List<WaterMeter> meters) {
try {
return R.ok(lifecycleService.batchStockIn(meters));
} catch (Exception e) {
log.error("批量入库失败", e);
return R.fail(e.getMessage());
}
}
/* ==================== 安装 ==================== */
/**
* 水表出库安装
*/
@PostMapping("/install")
public R<MeterInstallRecord> install(@RequestParam String meterNo,
@RequestParam String customerNo,
@RequestParam String installer,
@RequestParam String installAddress,
@RequestParam(required = false, defaultValue = "0") BigDecimal initialReading) {
try {
return R.ok(lifecycleService.installMeter(meterNo, customerNo, installer, installAddress, initialReading));
} catch (Exception e) {
log.error("水表安装失败", e);
return R.fail(e.getMessage());
}
}
/* ==================== 查询 ==================== */
/**
* 根据表号查询水表详情
*/
@GetMapping("/{meterNo}")
public R<WaterMeter> getByMeterNo(@PathVariable String meterNo) {
try {
return R.ok(lifecycleService.getByMeterNo(meterNo));
} catch (Exception e) {
return R.fail(e.getMessage());
}
}
/**
* 分页查询水表列表
*/
@GetMapping("/list")
public R<Page<WaterMeter>> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) MeterStatus status,
@RequestParam(required = false) String keyword) {
return R.ok(lifecycleService.listMeters(page, size, status, keyword));
}
/**
* 查询客户绑定的水表
*/
@GetMapping("/customer/{customerNo}")
public R<List<WaterMeter>> getByCustomer(@PathVariable String customerNo) {
return R.ok(lifecycleService.getMetersByCustomer(customerNo));
}
/**
* 查询安装记录
*/
@GetMapping("/{meterNo}/install-records")
public R<List<MeterInstallRecord>> getInstallRecords(@PathVariable String meterNo) {
return R.ok(lifecycleService.getInstallRecords(meterNo));
}
/**
* 查询水表生命周期日志
*/
@GetMapping("/{meterNo}/lifecycle-logs")
public R<List<MeterLifecycleLog>> getLifecycleLogs(@PathVariable String meterNo) {
return R.ok(lifecycleService.getLifecycleLogs(meterNo));
}
}
@@ -0,0 +1,114 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.entity.MeterReplaceRecord;
import com.water.revenue.service.MeterReplaceService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
/**
* 水表换表 / 报废管理
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/revenue/meter-replace")
public class MeterReplaceController {
private final MeterReplaceService replaceService;
/**
* 故障换表
*/
@PostMapping("/fault")
public R<MeterReplaceRecord> faultReplace(@RequestParam String oldMeterNo,
@RequestParam String newMeterNo,
@RequestParam String replacer,
@RequestParam String reason,
@RequestParam(required = false) BigDecimal oldReading) {
try {
return R.ok(replaceService.faultReplace(oldMeterNo, newMeterNo, replacer, reason, oldReading));
} catch (Exception e) {
log.error("故障换表失败", e);
return R.fail(e.getMessage());
}
}
/**
* 到期换表
*/
@PostMapping("/expired")
public R<MeterReplaceRecord> expiredReplace(@RequestParam String oldMeterNo,
@RequestParam String newMeterNo,
@RequestParam String replacer,
@RequestParam(required = false) BigDecimal oldReading) {
try {
return R.ok(replaceService.expiredReplace(oldMeterNo, newMeterNo, replacer, oldReading));
} catch (Exception e) {
log.error("到期换表失败", e);
return R.fail(e.getMessage());
}
}
/**
* 水表报废
*/
@PostMapping("/scrap")
public R<Void> scrapMeter(@RequestParam String meterNo,
@RequestParam String reason,
@RequestParam String operator) {
try {
replaceService.scrapMeter(meterNo, reason, operator);
return R.ok();
} catch (Exception e) {
log.error("水表报废失败", e);
return R.fail(e.getMessage());
}
}
/**
* 报废审批
*/
@PostMapping("/approve/{recordId}")
public R<MeterReplaceRecord> approve(@PathVariable Long recordId,
@RequestParam String approver,
@RequestParam boolean approved,
@RequestParam(required = false) String remark) {
try {
return R.ok(replaceService.approveReplace(recordId, approver, approved, remark));
} catch (Exception e) {
log.error("审批失败", e);
return R.fail(e.getMessage());
}
}
/**
* 查询换表记录
*/
@GetMapping("/records/{meterNo}")
public R<List<MeterReplaceRecord>> getReplaceRecords(@PathVariable String meterNo) {
return R.ok(replaceService.getReplaceRecords(meterNo));
}
/**
* 查询待审批列表
*/
@GetMapping("/pending")
public R<List<MeterReplaceRecord>> getPendingApprovals() {
return R.ok(replaceService.getPendingApprovals());
}
/**
* 分页查询换表记录
*/
@GetMapping("/list")
public R<List<MeterReplaceRecord>> list(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String replaceType) {
return R.ok(replaceService.listReplaceRecords(page, size, replaceType));
}
}
@@ -0,0 +1,71 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.entity.MeterLifecycleLog;
import com.water.revenue.service.MeterStatsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 水表统计 — 库存 / 安装 / 报废 / 生命周期日志
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/revenue/meter-stats")
public class MeterStatsController {
private final MeterStatsService statsService;
/**
* 库存统计
*/
@GetMapping("/inventory")
public R<Map<String, Object>> getInventoryStats() {
return R.ok(statsService.getInventoryStats());
}
/**
* 安装统计
*/
@GetMapping("/install")
public R<Map<String, Object>> getInstallStats() {
return R.ok(statsService.getInstallStats());
}
/**
* 报废统计
*/
@GetMapping("/scrap")
public R<Map<String, Object>> getScrapStats() {
return R.ok(statsService.getScrapStats());
}
/**
* 水表全生命周期日志
*/
@GetMapping("/lifecycle/{meterNo}")
public R<List<MeterLifecycleLog>> getLifecycleLogs(@PathVariable String meterNo) {
return R.ok(statsService.getLifecycleLogs(meterNo));
}
/**
* 最近操作记录
*/
@GetMapping("/recent")
public R<List<MeterLifecycleLog>> getRecentLogs(@RequestParam(defaultValue = "20") int limit) {
return R.ok(statsService.getRecentLogs(limit));
}
/**
* 操作类型统计
*/
@GetMapping("/action-types")
public R<List<Map<String, Object>>> getActionTypeStats() {
return R.ok(statsService.getActionTypeStats());
}
}
@@ -0,0 +1,51 @@
package com.water.revenue.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 水表安装记录
*/
@Data
@TableName("rev_meter_install_record")
public class MeterInstallRecord {
@TableId(type = IdType.AUTO)
private Long id;
/** 水表编号 */
private String meterNo;
/** 客户编号 */
private String customerNo;
/** 安装人 */
private String installer;
/** 安装日期 */
private LocalDate installDate;
/** 安装地址 */
private String installAddress;
/** 旧水表编号(换表安装时) */
private String oldMeterNo;
/** 初始读数 */
private BigDecimal initialReading;
/** 备注 */
private String remark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,48 @@
package com.water.revenue.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 水表生命周期日志
*/
@Data
@TableName("rev_meter_lifecycle_log")
public class MeterLifecycleLog {
@TableId(type = IdType.AUTO)
private Long id;
/** 水表编号 */
private String meterNo;
/** 操作类型:STOCK_IN/INSTALL/DISMANTLE/REPLACE/SCRAP/REPAIR */
private String actionType;
/** 操作时间 */
private LocalDateTime actionTime;
/** 操作人 */
private String operator;
/** 操作详情 */
private String detail;
/** 旧表编号 */
private String oldMeterNo;
/** 新表编号 */
private String newMeterNo;
/** 关联客户编号 */
private String customerNo;
/** 备注 */
private String remark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
}
@@ -0,0 +1,66 @@
package com.water.revenue.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 水表换表记录
*/
@Data
@TableName("rev_meter_replace_record")
public class MeterReplaceRecord {
@TableId(type = IdType.AUTO)
private Long id;
/** 旧水表编号 */
private String oldMeterNo;
/** 新水表编号 */
private String newMeterNo;
/** 客户编号 */
private String customerNo;
/** 换表类型:FAULT-故障换表, EXPIRED-到期换表 */
private String replaceType;
/** 换表原因 */
private String reason;
/** 换表人 */
private String replacer;
/** 换表日期 */
private LocalDate replaceDate;
/** 旧表读数 */
private BigDecimal oldReading;
/** 新表初始读数 */
private BigDecimal newReading;
/** 审批状态:PENDING-待审批, APPROVED-已通过, REJECTED-已拒绝 */
private String approvalStatus;
/** 审批人 */
private String approver;
/** 审批时间 */
private LocalDateTime approvalTime;
/** 备注 */
private String remark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,76 @@
package com.water.revenue.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.water.revenue.enums.MeterStatus;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 水表实体
*/
@Data
@TableName("rev_water_meter")
public class WaterMeter {
@TableId(type = IdType.AUTO)
private Long id;
/** 水表编号 */
private String meterNo;
/** 水表型号 */
private String model;
/** 口径(mm) */
private Integer diameter;
/** 制造商 */
private String manufacturer;
/** 生产日期 */
private LocalDate productionDate;
/** 安装日期 */
private LocalDate installDate;
/** 安装地址 */
private String installAddress;
/** 客户编号 */
private String customerNo;
/** 水表状态 */
private MeterStatus status;
/** 入库时间 */
private LocalDateTime inStockTime;
/** 出库时间 */
private LocalDateTime outStockTime;
/** 初始读数 */
private BigDecimal initialReading;
/** 当前读数 */
private BigDecimal currentReading;
/** 拆除日期 */
private LocalDate dismantleDate;
/** 报废日期 */
private LocalDate scrappedDate;
/** 备注 */
private String remark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,19 @@
package com.water.revenue.enums;
import lombok.Getter;
@Getter
public enum MeterStatus {
IN_STOCK("在库"),
INSTALLED("已安装"),
DISMANTLED("已拆除"),
SCRAPPED("已报废"),
REPAIRING("维修中");
private final String description;
MeterStatus(String description) {
this.description = description;
}
}
@@ -0,0 +1,19 @@
package com.water.revenue.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.revenue.entity.MeterInstallRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface MeterInstallRecordMapper extends BaseMapper<MeterInstallRecord> {
@Select("SELECT * FROM rev_meter_install_record WHERE meter_no = #{meterNo} ORDER BY install_date DESC")
List<MeterInstallRecord> selectByMeterNo(@Param("meterNo") String meterNo);
@Select("SELECT * FROM rev_meter_install_record WHERE customer_no = #{customerNo} ORDER BY install_date DESC")
List<MeterInstallRecord> selectByCustomerNo(@Param("customerNo") String customerNo);
}
@@ -0,0 +1,23 @@
package com.water.revenue.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.revenue.entity.MeterLifecycleLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface MeterLifecycleLogMapper extends BaseMapper<MeterLifecycleLog> {
@Select("SELECT * FROM rev_meter_lifecycle_log WHERE meter_no = #{meterNo} ORDER BY action_time ASC")
List<MeterLifecycleLog> selectByMeterNo(@Param("meterNo") String meterNo);
@Select("SELECT * FROM rev_meter_lifecycle_log ORDER BY action_time DESC LIMIT #{limit}")
List<MeterLifecycleLog> selectRecent(@Param("limit") int limit);
@Select("SELECT action_type, COUNT(*) as count FROM rev_meter_lifecycle_log GROUP BY action_type")
List<Map<String, Object>> countByActionType();
}
@@ -0,0 +1,22 @@
package com.water.revenue.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.revenue.entity.MeterReplaceRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface MeterReplaceRecordMapper extends BaseMapper<MeterReplaceRecord> {
@Select("SELECT * FROM rev_meter_replace_record WHERE old_meter_no = #{meterNo} OR new_meter_no = #{meterNo} ORDER BY replace_date DESC")
List<MeterReplaceRecord> selectByMeterNo(@Param("meterNo") String meterNo);
@Select("SELECT * FROM rev_meter_replace_record WHERE customer_no = #{customerNo} ORDER BY replace_date DESC")
List<MeterReplaceRecord> selectByCustomerNo(@Param("customerNo") String customerNo);
@Select("SELECT * FROM rev_meter_replace_record WHERE approval_status = 'PENDING' ORDER BY created_at ASC")
List<MeterReplaceRecord> selectPendingApprovals();
}
@@ -0,0 +1,29 @@
package com.water.revenue.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.revenue.entity.WaterMeter;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface WaterMeterMapper extends BaseMapper<WaterMeter> {
@Select("SELECT * FROM rev_water_meter WHERE meter_no = #{meterNo} LIMIT 1")
WaterMeter selectByMeterNo(@Param("meterNo") String meterNo);
@Select("SELECT * FROM rev_water_meter WHERE customer_no = #{customerNo} AND status = 'INSTALLED'")
List<WaterMeter> selectByCustomerNo(@Param("customerNo") String customerNo);
@Select("SELECT status, COUNT(*) as count FROM rev_water_meter GROUP BY status")
List<Map<String, Object>> countByStatus();
@Select("SELECT diameter, COUNT(*) as count FROM rev_water_meter WHERE status = 'IN_STOCK' GROUP BY diameter")
List<Map<String, Object>> countInStockByDiameter();
@Select("SELECT manufacturer, COUNT(*) as count FROM rev_water_meter WHERE status = 'IN_STOCK' GROUP BY manufacturer")
List<Map<String, Object>> countInStockByManufacturer();
}
@@ -0,0 +1,181 @@
package com.water.revenue.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.revenue.entity.MeterInstallRecord;
import com.water.revenue.entity.MeterLifecycleLog;
import com.water.revenue.entity.WaterMeter;
import com.water.revenue.enums.MeterStatus;
import com.water.revenue.mapper.MeterInstallRecordMapper;
import com.water.revenue.mapper.MeterLifecycleLogMapper;
import com.water.revenue.mapper.WaterMeterMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 水表生命周期服务 — 入库 / 出库安装 / 绑定用户 / 安装记录
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MeterLifecycleService {
private final WaterMeterMapper waterMeterMapper;
private final MeterInstallRecordMapper installRecordMapper;
private final MeterLifecycleLogMapper lifecycleLogMapper;
/* ==================== 入库登记 ==================== */
/**
* 水表入库登记
*/
@Transactional
public WaterMeter stockIn(WaterMeter meter) {
// 检查表号唯一性
WaterMeter existing = waterMeterMapper.selectByMeterNo(meter.getMeterNo());
if (existing != null) {
throw new IllegalStateException("水表编号已存在: " + meter.getMeterNo());
}
meter.setStatus(MeterStatus.IN_STOCK);
meter.setInStockTime(LocalDateTime.now());
meter.setInitialReading(BigDecimal.ZERO);
meter.setCurrentReading(BigDecimal.ZERO);
waterMeterMapper.insert(meter);
// 记录日志
addLog(meter.getMeterNo(), "STOCK_IN", meter.getMeterNo(), null, null,
"水表入库: 型号=" + meter.getModel() + ", 口径=" + meter.getDiameter());
log.info("水表入库成功: {}", meter.getMeterNo());
return meter;
}
/**
* 批量入库
*/
@Transactional
public List<WaterMeter> batchStockIn(List<WaterMeter> meters) {
for (WaterMeter meter : meters) {
stockIn(meter);
}
return meters;
}
/* ==================== 出库安装 ==================== */
/**
* 水表出库安装(绑定客户)
*/
@Transactional
public MeterInstallRecord installMeter(String meterNo, String customerNo, String installer,
String installAddress, BigDecimal initialReading) {
WaterMeter meter = waterMeterMapper.selectByMeterNo(meterNo);
if (meter == null) {
throw new IllegalArgumentException("水表不存在: " + meterNo);
}
if (meter.getStatus() != MeterStatus.IN_STOCK) {
throw new IllegalStateException("水表状态不允许安装, 当前状态: " + meter.getStatus());
}
// 更新水表状态
meter.setStatus(MeterStatus.INSTALLED);
meter.setCustomerNo(customerNo);
meter.setInstallAddress(installAddress);
meter.setInstallDate(LocalDate.now());
meter.setOutStockTime(LocalDateTime.now());
meter.setInitialReading(initialReading != null ? initialReading : BigDecimal.ZERO);
meter.setCurrentReading(meter.getInitialReading());
waterMeterMapper.updateById(meter);
// 创建安装记录
MeterInstallRecord record = new MeterInstallRecord();
record.setMeterNo(meterNo);
record.setCustomerNo(customerNo);
record.setInstaller(installer);
record.setInstallDate(LocalDate.now());
record.setInstallAddress(installAddress);
record.setInitialReading(meter.getInitialReading());
installRecordMapper.insert(record);
// 记录日志
addLog(meterNo, "INSTALL", meterNo, null, customerNo,
"水表安装: 客户=" + customerNo + ", 地址=" + installAddress + ", 安装人=" + installer);
log.info("水表安装成功: {} -> 客户 {}", meterNo, customerNo);
return record;
}
/* ==================== 查询 ==================== */
/**
* 根据表号查询水表
*/
public WaterMeter getByMeterNo(String meterNo) {
WaterMeter meter = waterMeterMapper.selectByMeterNo(meterNo);
if (meter == null) {
throw new IllegalArgumentException("水表不存在: " + meterNo);
}
return meter;
}
/**
* 分页查询水表列表
*/
public Page<WaterMeter> listMeters(int page, int size, MeterStatus status, String keyword) {
LambdaQueryWrapper<WaterMeter> wrapper = new LambdaQueryWrapper<>();
if (status != null) {
wrapper.eq(WaterMeter::getStatus, status);
}
if (keyword != null && !keyword.isEmpty()) {
wrapper.and(w -> w.like(WaterMeter::getMeterNo, keyword)
.or().like(WaterMeter::getCustomerNo, keyword)
.or().like(WaterMeter::getInstallAddress, keyword));
}
wrapper.orderByDesc(WaterMeter::getCreatedAt);
return waterMeterMapper.selectPage(new Page<>(page, size), wrapper);
}
/**
* 查询客户绑定的水表
*/
public List<WaterMeter> getMetersByCustomer(String customerNo) {
return waterMeterMapper.selectByCustomerNo(customerNo);
}
/**
* 查询安装记录
*/
public List<MeterInstallRecord> getInstallRecords(String meterNo) {
return installRecordMapper.selectByMeterNo(meterNo);
}
/**
* 查询水表生命周期日志
*/
public List<MeterLifecycleLog> getLifecycleLogs(String meterNo) {
return lifecycleLogMapper.selectByMeterNo(meterNo);
}
/* ==================== 内部方法 ==================== */
private void addLog(String meterNo, String actionType, String detail,
String oldMeterNo, String customerNo, String logDetail) {
MeterLifecycleLog l = new MeterLifecycleLog();
l.setMeterNo(meterNo);
l.setActionType(actionType);
l.setActionTime(LocalDateTime.now());
l.setOperator("system");
l.setDetail(logDetail);
l.setOldMeterNo(oldMeterNo);
l.setCustomerNo(customerNo);
lifecycleLogMapper.insert(l);
}
}
@@ -0,0 +1,211 @@
package com.water.revenue.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.revenue.entity.MeterLifecycleLog;
import com.water.revenue.entity.MeterReplaceRecord;
import com.water.revenue.entity.WaterMeter;
import com.water.revenue.enums.MeterStatus;
import com.water.revenue.mapper.MeterLifecycleLogMapper;
import com.water.revenue.mapper.MeterReplaceRecordMapper;
import com.water.revenue.mapper.WaterMeterMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 水表换表 / 报废服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MeterReplaceService {
private final WaterMeterMapper waterMeterMapper;
private final MeterReplaceRecordMapper replaceRecordMapper;
private final MeterLifecycleLogMapper lifecycleLogMapper;
/* ==================== 故障换表 ==================== */
/**
* 故障换表
*/
@Transactional
public MeterReplaceRecord faultReplace(String oldMeterNo, String newMeterNo,
String replacer, String reason, BigDecimal oldReading) {
return doReplace(oldMeterNo, newMeterNo, "FAULT", replacer, reason, oldReading);
}
/**
* 到期换表
*/
@Transactional
public MeterReplaceRecord expiredReplace(String oldMeterNo, String newMeterNo,
String replacer, BigDecimal oldReading) {
return doReplace(oldMeterNo, newMeterNo, "EXPIRED", replacer, "水表到期更换", oldReading);
}
/**
* 执行换表
*/
@Transactional
protected MeterReplaceRecord doReplace(String oldMeterNo, String newMeterNo,
String replaceType, String replacer,
String reason, BigDecimal oldReading) {
WaterMeter oldMeter = waterMeterMapper.selectByMeterNo(oldMeterNo);
if (oldMeter == null) {
throw new IllegalArgumentException("旧水表不存在: " + oldMeterNo);
}
if (oldMeter.getStatus() != MeterStatus.INSTALLED) {
throw new IllegalStateException("旧水表状态不允许换表, 当前状态: " + oldMeter.getStatus());
}
WaterMeter newMeter = waterMeterMapper.selectByMeterNo(newMeterNo);
if (newMeter == null) {
throw new IllegalArgumentException("新水表不存在: " + newMeterNo);
}
if (newMeter.getStatus() != MeterStatus.IN_STOCK) {
throw new IllegalStateException("新水表状态不允许安装, 当前状态: " + newMeter.getStatus());
}
// 旧表拆除
oldMeter.setStatus(MeterStatus.DISMANTLED);
oldMeter.setDismantleDate(LocalDate.now());
oldMeter.setCurrentReading(oldReading != null ? oldReading : oldMeter.getCurrentReading());
waterMeterMapper.updateById(oldMeter);
// 新表安装(继承客户信息)
newMeter.setStatus(MeterStatus.INSTALLED);
newMeter.setCustomerNo(oldMeter.getCustomerNo());
newMeter.setInstallAddress(oldMeter.getInstallAddress());
newMeter.setInstallDate(LocalDate.now());
newMeter.setOutStockTime(LocalDateTime.now());
newMeter.setInitialReading(BigDecimal.ZERO);
newMeter.setCurrentReading(BigDecimal.ZERO);
waterMeterMapper.updateById(newMeter);
// 创建换表记录
MeterReplaceRecord record = new MeterReplaceRecord();
record.setOldMeterNo(oldMeterNo);
record.setNewMeterNo(newMeterNo);
record.setCustomerNo(oldMeter.getCustomerNo());
record.setReplaceType(replaceType);
record.setReason(reason);
record.setReplacer(replacer);
record.setReplaceDate(LocalDate.now());
record.setOldReading(oldReading != null ? oldReading : oldMeter.getCurrentReading());
record.setNewReading(BigDecimal.ZERO);
record.setApprovalStatus("APPROVED");
replaceRecordMapper.insert(record);
// 记录日志
addLog(oldMeterNo, "DISMANTLE", oldMeterNo, newMeterNo, oldMeter.getCustomerNo(),
"旧表拆除: 原因=" + reason + ", 换表人=" + replacer);
addLog(newMeterNo, "REPLACE", oldMeterNo, newMeterNo, oldMeter.getCustomerNo(),
"新表安装(换表): 替换旧表 " + oldMeterNo + ", 换表人=" + replacer);
log.info("换表成功: {} -> {}", oldMeterNo, newMeterNo);
return record;
}
/* ==================== 到期报废 ==================== */
/**
* 水表报废
*/
@Transactional
public void scrapMeter(String meterNo, String reason, String operator) {
WaterMeter meter = waterMeterMapper.selectByMeterNo(meterNo);
if (meter == null) {
throw new IllegalArgumentException("水表不存在: " + meterNo);
}
if (meter.getStatus() == MeterStatus.INSTALLED) {
throw new IllegalStateException("已安装的水表不能直接报废, 请先拆除");
}
meter.setStatus(MeterStatus.SCRAPPED);
meter.setScrappedDate(LocalDate.now());
meter.setRemark(reason);
waterMeterMapper.updateById(meter);
addLog(meterNo, "SCRAP", meterNo, null, meter.getCustomerNo(),
"水表报废: 原因=" + reason + ", 操作人=" + operator);
log.info("水表报废: {} - {}", meterNo, reason);
}
/**
* 报废审批
*/
@Transactional
public MeterReplaceRecord approveReplace(Long recordId, String approver, boolean approved, String remark) {
MeterReplaceRecord record = replaceRecordMapper.selectById(recordId);
if (record == null) {
throw new IllegalArgumentException("换表记录不存在: " + recordId);
}
if (!"PENDING".equals(record.getApprovalStatus())) {
throw new IllegalStateException("该记录已审批, 当前状态: " + record.getApprovalStatus());
}
record.setApprovalStatus(approved ? "APPROVED" : "REJECTED");
record.setApprover(approver);
record.setApprovalTime(LocalDateTime.now());
if (remark != null && !remark.isEmpty()) {
record.setRemark(record.getRemark() != null ? record.getRemark() + "; " + remark : remark);
}
replaceRecordMapper.updateById(record);
log.info("换表审批: id={}, approved={}, approver={}", recordId, approved, approver);
return record;
}
/* ==================== 查询 ==================== */
/**
* 查询换表记录
*/
public List<MeterReplaceRecord> getReplaceRecords(String meterNo) {
return replaceRecordMapper.selectByMeterNo(meterNo);
}
/**
* 查询待审批列表
*/
public List<MeterReplaceRecord> getPendingApprovals() {
return replaceRecordMapper.selectPendingApprovals();
}
/**
* 分页查询换表记录
*/
public List<MeterReplaceRecord> listReplaceRecords(int page, int size, String replaceType) {
LambdaQueryWrapper<MeterReplaceRecord> wrapper = new LambdaQueryWrapper<>();
if (replaceType != null && !replaceType.isEmpty()) {
wrapper.eq(MeterReplaceRecord::getReplaceType, replaceType);
}
wrapper.orderByDesc(MeterReplaceRecord::getCreatedAt);
wrapper.last("LIMIT " + size + " OFFSET " + (page * size));
return replaceRecordMapper.selectList(wrapper);
}
/* ==================== 内部方法 ==================== */
private void addLog(String meterNo, String actionType, String oldMeterNo,
String newMeterNo, String customerNo, String detail) {
MeterLifecycleLog l = new MeterLifecycleLog();
l.setMeterNo(meterNo);
l.setActionType(actionType);
l.setActionTime(LocalDateTime.now());
l.setOperator("system");
l.setDetail(detail);
l.setOldMeterNo(oldMeterNo);
l.setNewMeterNo(newMeterNo);
l.setCustomerNo(customerNo);
lifecycleLogMapper.insert(l);
}
}
@@ -0,0 +1,144 @@
package com.water.revenue.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.revenue.entity.MeterLifecycleLog;
import com.water.revenue.entity.WaterMeter;
import com.water.revenue.enums.MeterStatus;
import com.water.revenue.mapper.MeterLifecycleLogMapper;
import com.water.revenue.mapper.MeterReplaceRecordMapper;
import com.water.revenue.mapper.WaterMeterMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 水表统计服务 — 生命周期日志 / 库存统计 / 安装统计 / 报废统计
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MeterStatsService {
private final WaterMeterMapper waterMeterMapper;
private final MeterLifecycleLogMapper lifecycleLogMapper;
private final MeterReplaceRecordMapper replaceRecordMapper;
/* ==================== 库存统计 ==================== */
/**
* 库存综合统计
*/
public Map<String, Object> getInventoryStats() {
Map<String, Object> stats = new LinkedHashMap<>();
// 按状态统计
List<Map<String, Object>> statusStats = waterMeterMapper.countByStatus();
stats.put("statusStats", statusStats);
// 在库数量
long inStock = countByStatus(MeterStatus.IN_STOCK);
stats.put("totalInStock", inStock);
// 已安装数量
long installed = countByStatus(MeterStatus.INSTALLED);
stats.put("totalInstalled", installed);
// 已拆除
long dismantled = countByStatus(MeterStatus.DISMANTLED);
stats.put("totalDismantled", dismantled);
// 已报废
long scrapped = countByStatus(MeterStatus.SCRAPPED);
stats.put("totalScrapped", scrapped);
// 维修中
long repairing = countByStatus(MeterStatus.REPAIRING);
stats.put("totalRepairing", repairing);
// 按口径统计库存
stats.put("byDiameter", waterMeterMapper.countInStockByDiameter());
// 按制造商统计库存
stats.put("byManufacturer", waterMeterMapper.countInStockByManufacturer());
// 总量
long total = waterMeterMapper.selectCount(null);
stats.put("totalCount", total);
return stats;
}
/* ==================== 安装统计 ==================== */
/**
* 安装统计
*/
public Map<String, Object> getInstallStats() {
Map<String, Object> stats = new LinkedHashMap<>();
long installed = countByStatus(MeterStatus.INSTALLED);
stats.put("totalInstalled", installed);
// 按口径统计已安装
LambdaQueryWrapper<WaterMeter> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(WaterMeter::getStatus, MeterStatus.INSTALLED)
.select(WaterMeter::getDiameter)
.groupBy(WaterMeter::getDiameter);
// Using raw SQL for aggregate
stats.put("installedByDiameter",
waterMeterMapper.selectMaps(
new LambdaQueryWrapper<WaterMeter>()
.select(WaterMeter::getDiameter)
.eq(WaterMeter::getStatus, MeterStatus.INSTALLED)
.groupBy(WaterMeter::getDiameter)));
return stats;
}
/* ==================== 报废统计 ==================== */
/**
* 报废统计
*/
public Map<String, Object> getScrapStats() {
Map<String, Object> stats = new LinkedHashMap<>();
long scrapped = countByStatus(MeterStatus.SCRAPPED);
stats.put("totalScrapped", scrapped);
return stats;
}
/* ==================== 生命周期日志 ==================== */
/**
* 获取水表全生命周期日志
*/
public List<MeterLifecycleLog> getLifecycleLogs(String meterNo) {
return lifecycleLogMapper.selectByMeterNo(meterNo);
}
/**
* 最近操作记录
*/
public List<MeterLifecycleLog> getRecentLogs(int limit) {
return lifecycleLogMapper.selectRecent(limit);
}
/**
* 操作类型统计
*/
public List<Map<String, Object>> getActionTypeStats() {
return lifecycleLogMapper.countByActionType();
}
/* ==================== 内部方法 ==================== */
private long countByStatus(MeterStatus status) {
return waterMeterMapper.selectCount(
new LambdaQueryWrapper<WaterMeter>().eq(WaterMeter::getStatus, status));
}
}
@@ -0,0 +1,313 @@
package com.water.revenue.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.revenue.entity.MeterInstallRecord;
import com.water.revenue.entity.MeterLifecycleLog;
import com.water.revenue.entity.MeterReplaceRecord;
import com.water.revenue.entity.WaterMeter;
import com.water.revenue.enums.MeterStatus;
import com.water.revenue.mapper.MeterInstallRecordMapper;
import com.water.revenue.mapper.MeterLifecycleLogMapper;
import com.water.revenue.mapper.MeterReplaceRecordMapper;
import com.water.revenue.mapper.WaterMeterMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MeterLifecycleTest {
@Mock
private WaterMeterMapper waterMeterMapper;
@Mock
private MeterInstallRecordMapper installRecordMapper;
@Mock
private MeterReplaceRecordMapper replaceRecordMapper;
@Mock
private MeterLifecycleLogMapper lifecycleLogMapper;
private MeterLifecycleService lifecycleService;
private MeterReplaceService replaceService;
private MeterStatsService statsService;
@BeforeEach
void setUp() {
lifecycleService = new MeterLifecycleService(waterMeterMapper, installRecordMapper, lifecycleLogMapper);
replaceService = new MeterReplaceService(waterMeterMapper, replaceRecordMapper, lifecycleLogMapper);
statsService = new MeterStatsService(waterMeterMapper, lifecycleLogMapper, replaceRecordMapper);
}
/* ==================== Helper ==================== */
private WaterMeter buildMeter(String meterNo, MeterStatus status) {
WaterMeter m = new WaterMeter();
m.setId(1L);
m.setMeterNo(meterNo);
m.setModel("DN20");
m.setDiameter(20);
m.setManufacturer("TestMfg");
m.setProductionDate(LocalDate.of(2025, 1, 1));
m.setStatus(status);
m.setInStockTime(LocalDateTime.now());
m.setInitialReading(BigDecimal.ZERO);
m.setCurrentReading(BigDecimal.ZERO);
m.setCreatedAt(LocalDateTime.now());
m.setUpdatedAt(LocalDateTime.now());
return m;
}
/* ==================== Test 1: 水表入库 ==================== */
@Test
@DisplayName("Test 1: 水表入库登记 - 正常入库")
void testStockIn_Success() {
WaterMeter meter = buildMeter("WM-001", null);
when(waterMeterMapper.selectByMeterNo("WM-001")).thenReturn(null);
when(waterMeterMapper.insert(any(WaterMeter.class))).thenReturn(1);
when(lifecycleLogMapper.insert(any(MeterLifecycleLog.class))).thenReturn(1);
WaterMeter result = lifecycleService.stockIn(meter);
assertEquals(MeterStatus.IN_STOCK, result.getStatus());
assertNotNull(result.getInStockTime());
verify(waterMeterMapper).insert(any(WaterMeter.class));
verify(lifecycleLogMapper).insert(any(MeterLifecycleLog.class));
}
/* ==================== Test 2: 重复入库 ==================== */
@Test
@DisplayName("Test 2: 水表入库登记 - 表号重复抛异常")
void testStockIn_DuplicateMeterNo() {
WaterMeter meter = buildMeter("WM-001", null);
when(waterMeterMapper.selectByMeterNo("WM-001")).thenReturn(buildMeter("WM-001", MeterStatus.IN_STOCK));
assertThrows(IllegalStateException.class, () -> lifecycleService.stockIn(meter));
verify(waterMeterMapper, never()).insert(any());
}
/* ==================== Test 3: 水表安装 ==================== */
@Test
@DisplayName("Test 3: 水表出库安装 - 正常安装")
void testInstallMeter_Success() {
WaterMeter meter = buildMeter("WM-001", MeterStatus.IN_STOCK);
when(waterMeterMapper.selectByMeterNo("WM-001")).thenReturn(meter);
when(waterMeterMapper.updateById(any(WaterMeter.class))).thenReturn(1);
when(installRecordMapper.insert(any(MeterInstallRecord.class))).thenReturn(1);
when(lifecycleLogMapper.insert(any(MeterLifecycleLog.class))).thenReturn(1);
MeterInstallRecord record = lifecycleService.installMeter(
"WM-001", "CUST-001", "installer-A", "Test Address", BigDecimal.TEN);
assertEquals(MeterStatus.INSTALLED, meter.getStatus());
assertEquals("CUST-001", meter.getCustomerNo());
assertEquals("Test Address", meter.getInstallAddress());
assertNotNull(record);
verify(installRecordMapper).insert(any(MeterInstallRecord.class));
}
/* ==================== Test 4: 安装状态不对 ==================== */
@Test
@DisplayName("Test 4: 水表安装 - 状态不是 IN_STOCK 抛异常")
void testInstallMeter_WrongStatus() {
WaterMeter meter = buildMeter("WM-001", MeterStatus.INSTALLED);
when(waterMeterMapper.selectByMeterNo("WM-001")).thenReturn(meter);
assertThrows(IllegalStateException.class, () ->
lifecycleService.installMeter("WM-001", "CUST-001", "installer", "addr", BigDecimal.ZERO));
}
/* ==================== Test 5: 故障换表 ==================== */
@Test
@DisplayName("Test 5: 故障换表 - 正常流程")
void testFaultReplace_Success() {
WaterMeter oldMeter = buildMeter("OLD-001", MeterStatus.INSTALLED);
oldMeter.setCustomerNo("CUST-001");
oldMeter.setInstallAddress("Addr");
oldMeter.setCurrentReading(new BigDecimal("100"));
WaterMeter newMeter = buildMeter("NEW-001", MeterStatus.IN_STOCK);
when(waterMeterMapper.selectByMeterNo("OLD-001")).thenReturn(oldMeter);
when(waterMeterMapper.selectByMeterNo("NEW-001")).thenReturn(newMeter);
when(waterMeterMapper.updateById(any(WaterMeter.class))).thenReturn(1);
when(replaceRecordMapper.insert(any(MeterReplaceRecord.class))).thenReturn(1);
when(lifecycleLogMapper.insert(any(MeterLifecycleLog.class))).thenReturn(1);
MeterReplaceRecord record = replaceService.faultReplace(
"OLD-001", "NEW-001", "replacer-A", "表具故障", new BigDecimal("100"));
assertEquals(MeterStatus.DISMANTLED, oldMeter.getStatus());
assertEquals(MeterStatus.INSTALLED, newMeter.getStatus());
assertEquals("CUST-001", newMeter.getCustomerNo());
assertEquals("FAULT", record.getReplaceType());
assertEquals("APPROVED", record.getApprovalStatus());
// 验证日志被记录了2次(拆除+安装)
verify(lifecycleLogMapper, times(2)).insert(any(MeterLifecycleLog.class));
}
/* ==================== Test 6: 水表报废 ==================== */
@Test
@DisplayName("Test 6: 水表报废 - 正常报废已拆除的水表")
void testScrapMeter_Success() {
WaterMeter meter = buildMeter("WM-SCRAP", MeterStatus.DISMANTLED);
when(waterMeterMapper.selectByMeterNo("WM-SCRAP")).thenReturn(meter);
when(waterMeterMapper.updateById(any(WaterMeter.class))).thenReturn(1);
when(lifecycleLogMapper.insert(any(MeterLifecycleLog.class))).thenReturn(1);
replaceService.scrapMeter("WM-SCRAP", "超期使用", "operator-A");
assertEquals(MeterStatus.SCRAPPED, meter.getStatus());
assertNotNull(meter.getScrappedDate());
verify(waterMeterMapper).updateById(meter);
}
/* ==================== Test 7: 已安装水表不能直接报废 ==================== */
@Test
@DisplayName("Test 7: 水表报废 - 已安装水表不能直接报废")
void testScrapMeter_InstalledMeterRejected() {
WaterMeter meter = buildMeter("WM-ACTIVE", MeterStatus.INSTALLED);
when(waterMeterMapper.selectByMeterNo("WM-ACTIVE")).thenReturn(meter);
assertThrows(IllegalStateException.class, () ->
replaceService.scrapMeter("WM-ACTIVE", "reason", "operator"));
}
/* ==================== Test 8: 报废审批 ==================== */
@Test
@DisplayName("Test 8: 报废审批 - 审批通过")
void testApproveReplace_Approved() {
MeterReplaceRecord record = new MeterReplaceRecord();
record.setId(1L);
record.setApprovalStatus("PENDING");
when(replaceRecordMapper.selectById(1L)).thenReturn(record);
when(replaceRecordMapper.updateById(any(MeterReplaceRecord.class))).thenReturn(1);
MeterReplaceRecord result = replaceService.approveReplace(1L, "admin", true, "同意");
assertEquals("APPROVED", result.getApprovalStatus());
assertEquals("admin", result.getApprover());
assertNotNull(result.getApprovalTime());
}
/* ==================== Test 9: 重复审批 ==================== */
@Test
@DisplayName("Test 9: 报废审批 - 已审批的记录不能再次审批")
void testApproveReplace_AlreadyApproved() {
MeterReplaceRecord record = new MeterReplaceRecord();
record.setId(1L);
record.setApprovalStatus("APPROVED");
when(replaceRecordMapper.selectById(1L)).thenReturn(record);
assertThrows(IllegalStateException.class, () ->
replaceService.approveReplace(1L, "admin", true, null));
}
/* ==================== Test 10: 库存统计 ==================== */
@Test
@DisplayName("Test 10: 库存统计 - 返回完整统计信息")
void testGetInventoryStats() {
when(waterMeterMapper.countByStatus()).thenReturn(
List.of(Map.of("status", "IN_STOCK", "count", 10L),
Map.of("status", "INSTALLED", "count", 20L)));
when(waterMeterMapper.countInStockByDiameter()).thenReturn(
List.of(Map.of("diameter", 20, "count", 5L)));
when(waterMeterMapper.countInStockByManufacturer()).thenReturn(
List.of(Map.of("manufacturer", "TestMfg", "count", 10L)));
// mock selectCount for individual status queries
when(waterMeterMapper.selectCount(any(LambdaQueryWrapper.class)))
.thenReturn(10L) // IN_STOCK
.thenReturn(20L) // INSTALLED
.thenReturn(3L) // DISMANTLED
.thenReturn(2L) // SCRAPPED
.thenReturn(1L); // REPAIRING
Map<String, Object> stats = statsService.getInventoryStats();
assertNotNull(stats);
assertEquals(10L, stats.get("totalInStock"));
assertEquals(20L, stats.get("totalInstalled"));
assertEquals(3L, stats.get("totalDismantled"));
assertEquals(2L, stats.get("totalScrapped"));
assertEquals(1L, stats.get("totalRepairing"));
assertNotNull(stats.get("byDiameter"));
assertNotNull(stats.get("byManufacturer"));
}
/* ==================== Test 11: 生命周期日志查询 ==================== */
@Test
@DisplayName("Test 11: 生命周期日志查询")
void testGetLifecycleLogs() {
MeterLifecycleLog log1 = new MeterLifecycleLog();
log1.setMeterNo("WM-001");
log1.setActionType("STOCK_IN");
log1.setActionTime(LocalDateTime.now());
MeterLifecycleLog log2 = new MeterLifecycleLog();
log2.setMeterNo("WM-001");
log2.setActionType("INSTALL");
log2.setActionTime(LocalDateTime.now());
when(lifecycleLogMapper.selectByMeterNo("WM-001")).thenReturn(Arrays.asList(log1, log2));
List<MeterLifecycleLog> logs = lifecycleService.getLifecycleLogs("WM-001");
assertEquals(2, logs.size());
assertEquals("STOCK_IN", logs.get(0).getActionType());
assertEquals("INSTALL", logs.get(1).getActionType());
}
/* ==================== Test 12: 到期换表 ==================== */
@Test
@DisplayName("Test 12: 到期换表 - EXPIRED 类型")
void testExpiredReplace() {
WaterMeter oldMeter = buildMeter("OLD-002", MeterStatus.INSTALLED);
oldMeter.setCustomerNo("CUST-002");
oldMeter.setInstallAddress("Addr2");
WaterMeter newMeter = buildMeter("NEW-002", MeterStatus.IN_STOCK);
when(waterMeterMapper.selectByMeterNo("OLD-002")).thenReturn(oldMeter);
when(waterMeterMapper.selectByMeterNo("NEW-002")).thenReturn(newMeter);
when(waterMeterMapper.updateById(any(WaterMeter.class))).thenReturn(1);
when(replaceRecordMapper.insert(any(MeterReplaceRecord.class))).thenReturn(1);
when(lifecycleLogMapper.insert(any(MeterLifecycleLog.class))).thenReturn(1);
MeterReplaceRecord record = replaceService.expiredReplace(
"OLD-002", "NEW-002", "replacer-B", new BigDecimal("200"));
assertEquals("EXPIRED", record.getReplaceType());
assertEquals("水表到期更换", record.getReason());
}
}