diff --git a/sql/V_meter_lifecycle.sql b/sql/V_meter_lifecycle.sql new file mode 100644 index 00000000..f097d0a3 --- /dev/null +++ b/sql/V_meter_lifecycle.sql @@ -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); diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/MeterController.java b/wm-revenue/src/main/java/com/water/revenue/controller/MeterController.java new file mode 100644 index 00000000..65a34c95 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/MeterController.java @@ -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 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> batchStockIn(@RequestBody List meters) { + try { + return R.ok(lifecycleService.batchStockIn(meters)); + } catch (Exception e) { + log.error("批量入库失败", e); + return R.fail(e.getMessage()); + } + } + + /* ==================== 安装 ==================== */ + + /** + * 水表出库安装 + */ + @PostMapping("/install") + public R 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 getByMeterNo(@PathVariable String meterNo) { + try { + return R.ok(lifecycleService.getByMeterNo(meterNo)); + } catch (Exception e) { + return R.fail(e.getMessage()); + } + } + + /** + * 分页查询水表列表 + */ + @GetMapping("/list") + public R> 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> getByCustomer(@PathVariable String customerNo) { + return R.ok(lifecycleService.getMetersByCustomer(customerNo)); + } + + /** + * 查询安装记录 + */ + @GetMapping("/{meterNo}/install-records") + public R> getInstallRecords(@PathVariable String meterNo) { + return R.ok(lifecycleService.getInstallRecords(meterNo)); + } + + /** + * 查询水表生命周期日志 + */ + @GetMapping("/{meterNo}/lifecycle-logs") + public R> getLifecycleLogs(@PathVariable String meterNo) { + return R.ok(lifecycleService.getLifecycleLogs(meterNo)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/MeterReplaceController.java b/wm-revenue/src/main/java/com/water/revenue/controller/MeterReplaceController.java new file mode 100644 index 00000000..983e58a4 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/MeterReplaceController.java @@ -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 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 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 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 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> getReplaceRecords(@PathVariable String meterNo) { + return R.ok(replaceService.getReplaceRecords(meterNo)); + } + + /** + * 查询待审批列表 + */ + @GetMapping("/pending") + public R> getPendingApprovals() { + return R.ok(replaceService.getPendingApprovals()); + } + + /** + * 分页查询换表记录 + */ + @GetMapping("/list") + public R> list(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String replaceType) { + return R.ok(replaceService.listReplaceRecords(page, size, replaceType)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/MeterStatsController.java b/wm-revenue/src/main/java/com/water/revenue/controller/MeterStatsController.java new file mode 100644 index 00000000..12c26bb7 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/MeterStatsController.java @@ -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> getInventoryStats() { + return R.ok(statsService.getInventoryStats()); + } + + /** + * 安装统计 + */ + @GetMapping("/install") + public R> getInstallStats() { + return R.ok(statsService.getInstallStats()); + } + + /** + * 报废统计 + */ + @GetMapping("/scrap") + public R> getScrapStats() { + return R.ok(statsService.getScrapStats()); + } + + /** + * 水表全生命周期日志 + */ + @GetMapping("/lifecycle/{meterNo}") + public R> getLifecycleLogs(@PathVariable String meterNo) { + return R.ok(statsService.getLifecycleLogs(meterNo)); + } + + /** + * 最近操作记录 + */ + @GetMapping("/recent") + public R> getRecentLogs(@RequestParam(defaultValue = "20") int limit) { + return R.ok(statsService.getRecentLogs(limit)); + } + + /** + * 操作类型统计 + */ + @GetMapping("/action-types") + public R>> getActionTypeStats() { + return R.ok(statsService.getActionTypeStats()); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/MeterInstallRecord.java b/wm-revenue/src/main/java/com/water/revenue/entity/MeterInstallRecord.java new file mode 100644 index 00000000..bf5c42a9 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/MeterInstallRecord.java @@ -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; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/MeterLifecycleLog.java b/wm-revenue/src/main/java/com/water/revenue/entity/MeterLifecycleLog.java new file mode 100644 index 00000000..5d335f7d --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/MeterLifecycleLog.java @@ -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; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/MeterReplaceRecord.java b/wm-revenue/src/main/java/com/water/revenue/entity/MeterReplaceRecord.java new file mode 100644 index 00000000..76b4e429 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/MeterReplaceRecord.java @@ -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; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/WaterMeter.java b/wm-revenue/src/main/java/com/water/revenue/entity/WaterMeter.java new file mode 100644 index 00000000..d43f10e9 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/WaterMeter.java @@ -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; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/enums/MeterStatus.java b/wm-revenue/src/main/java/com/water/revenue/enums/MeterStatus.java new file mode 100644 index 00000000..b3d40abc --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/enums/MeterStatus.java @@ -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; + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/MeterInstallRecordMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/MeterInstallRecordMapper.java new file mode 100644 index 00000000..f7f5fab0 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/MeterInstallRecordMapper.java @@ -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 { + + @Select("SELECT * FROM rev_meter_install_record WHERE meter_no = #{meterNo} ORDER BY install_date DESC") + List selectByMeterNo(@Param("meterNo") String meterNo); + + @Select("SELECT * FROM rev_meter_install_record WHERE customer_no = #{customerNo} ORDER BY install_date DESC") + List selectByCustomerNo(@Param("customerNo") String customerNo); +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/MeterLifecycleLogMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/MeterLifecycleLogMapper.java new file mode 100644 index 00000000..20faae6b --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/MeterLifecycleLogMapper.java @@ -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 { + + @Select("SELECT * FROM rev_meter_lifecycle_log WHERE meter_no = #{meterNo} ORDER BY action_time ASC") + List selectByMeterNo(@Param("meterNo") String meterNo); + + @Select("SELECT * FROM rev_meter_lifecycle_log ORDER BY action_time DESC LIMIT #{limit}") + List selectRecent(@Param("limit") int limit); + + @Select("SELECT action_type, COUNT(*) as count FROM rev_meter_lifecycle_log GROUP BY action_type") + List> countByActionType(); +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/MeterReplaceRecordMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/MeterReplaceRecordMapper.java new file mode 100644 index 00000000..6a057318 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/MeterReplaceRecordMapper.java @@ -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 { + + @Select("SELECT * FROM rev_meter_replace_record WHERE old_meter_no = #{meterNo} OR new_meter_no = #{meterNo} ORDER BY replace_date DESC") + List selectByMeterNo(@Param("meterNo") String meterNo); + + @Select("SELECT * FROM rev_meter_replace_record WHERE customer_no = #{customerNo} ORDER BY replace_date DESC") + List selectByCustomerNo(@Param("customerNo") String customerNo); + + @Select("SELECT * FROM rev_meter_replace_record WHERE approval_status = 'PENDING' ORDER BY created_at ASC") + List selectPendingApprovals(); +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/WaterMeterMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/WaterMeterMapper.java new file mode 100644 index 00000000..086eef78 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/WaterMeterMapper.java @@ -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 { + + @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 selectByCustomerNo(@Param("customerNo") String customerNo); + + @Select("SELECT status, COUNT(*) as count FROM rev_water_meter GROUP BY status") + List> countByStatus(); + + @Select("SELECT diameter, COUNT(*) as count FROM rev_water_meter WHERE status = 'IN_STOCK' GROUP BY diameter") + List> countInStockByDiameter(); + + @Select("SELECT manufacturer, COUNT(*) as count FROM rev_water_meter WHERE status = 'IN_STOCK' GROUP BY manufacturer") + List> countInStockByManufacturer(); +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/MeterLifecycleService.java b/wm-revenue/src/main/java/com/water/revenue/service/MeterLifecycleService.java new file mode 100644 index 00000000..439f478d --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/MeterLifecycleService.java @@ -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 batchStockIn(List 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 listMeters(int page, int size, MeterStatus status, String keyword) { + LambdaQueryWrapper 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 getMetersByCustomer(String customerNo) { + return waterMeterMapper.selectByCustomerNo(customerNo); + } + + /** + * 查询安装记录 + */ + public List getInstallRecords(String meterNo) { + return installRecordMapper.selectByMeterNo(meterNo); + } + + /** + * 查询水表生命周期日志 + */ + public List 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); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/MeterReplaceService.java b/wm-revenue/src/main/java/com/water/revenue/service/MeterReplaceService.java new file mode 100644 index 00000000..3039b06b --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/MeterReplaceService.java @@ -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 getReplaceRecords(String meterNo) { + return replaceRecordMapper.selectByMeterNo(meterNo); + } + + /** + * 查询待审批列表 + */ + public List getPendingApprovals() { + return replaceRecordMapper.selectPendingApprovals(); + } + + /** + * 分页查询换表记录 + */ + public List listReplaceRecords(int page, int size, String replaceType) { + LambdaQueryWrapper 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); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/MeterStatsService.java b/wm-revenue/src/main/java/com/water/revenue/service/MeterStatsService.java new file mode 100644 index 00000000..22e0225f --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/MeterStatsService.java @@ -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 getInventoryStats() { + Map stats = new LinkedHashMap<>(); + + // 按状态统计 + List> 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 getInstallStats() { + Map stats = new LinkedHashMap<>(); + + long installed = countByStatus(MeterStatus.INSTALLED); + stats.put("totalInstalled", installed); + + // 按口径统计已安装 + LambdaQueryWrapper 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() + .select(WaterMeter::getDiameter) + .eq(WaterMeter::getStatus, MeterStatus.INSTALLED) + .groupBy(WaterMeter::getDiameter))); + + return stats; + } + + /* ==================== 报废统计 ==================== */ + + /** + * 报废统计 + */ + public Map getScrapStats() { + Map stats = new LinkedHashMap<>(); + + long scrapped = countByStatus(MeterStatus.SCRAPPED); + stats.put("totalScrapped", scrapped); + + return stats; + } + + /* ==================== 生命周期日志 ==================== */ + + /** + * 获取水表全生命周期日志 + */ + public List getLifecycleLogs(String meterNo) { + return lifecycleLogMapper.selectByMeterNo(meterNo); + } + + /** + * 最近操作记录 + */ + public List getRecentLogs(int limit) { + return lifecycleLogMapper.selectRecent(limit); + } + + /** + * 操作类型统计 + */ + public List> getActionTypeStats() { + return lifecycleLogMapper.countByActionType(); + } + + /* ==================== 内部方法 ==================== */ + + private long countByStatus(MeterStatus status) { + return waterMeterMapper.selectCount( + new LambdaQueryWrapper().eq(WaterMeter::getStatus, status)); + } +} diff --git a/wm-revenue/src/test/java/com/water/revenue/service/MeterLifecycleTest.java b/wm-revenue/src/test/java/com/water/revenue/service/MeterLifecycleTest.java new file mode 100644 index 00000000..8eb11e26 --- /dev/null +++ b/wm-revenue/src/test/java/com/water/revenue/service/MeterLifecycleTest.java @@ -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 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 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()); + } +}